diff --git a/apps/extension/package.json b/apps/extension/package.json index afe0d102858..31401636bb2 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -18,7 +18,7 @@ "@uniswap/analytics-events": "2.43.0", "@uniswap/client-embeddedwallet": "0.0.16", "@uniswap/sdk-core": "7.9.0", - "@uniswap/universal-router-sdk": "4.19.5", + "@hkdex-tmp/universal_router_sdk": "1.0.3", "@uniswap/v3-sdk": "3.25.2", "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", diff --git a/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts b/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts index 5990962ebb1..1090ed99fff 100644 --- a/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts +++ b/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts @@ -1,4 +1,4 @@ -import { CommandType } from '@uniswap/universal-router-sdk' +import { CommandType } from '@hkdex-tmp/universal_router_sdk' import { FeeAmount as FeeAmountV3 } from '@uniswap/v3-sdk' import { BigNumberSchema } from 'src/app/features/dappRequests/types/EthersTypes' import { z } from 'zod' diff --git a/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts b/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts index adff8212d30..924a5bc22b3 100644 --- a/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts +++ b/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts @@ -1,4 +1,4 @@ -import { CommandParser, UniversalRouterCall } from '@uniswap/universal-router-sdk' +import { CommandParser, UniversalRouterCall } from '@hkdex-tmp/universal_router_sdk' import { V4BaseActionsParser, V4RouterCall } from '@uniswap/v4-sdk' import { EthSendTransactionRPCActions } from 'src/app/features/dappRequests/types/DappRequestTypes' import { parseCalldata as parseNfPMCalldata } from 'src/app/features/dappRequests/types/NonfungiblePositionManager' diff --git a/apps/extension/wxt.config.ts b/apps/extension/wxt.config.ts index 6fa77e81623..45c8b956a46 100644 --- a/apps/extension/wxt.config.ts +++ b/apps/extension/wxt.config.ts @@ -170,7 +170,7 @@ export default defineConfig({ '@uniswap/v3-sdk', '@uniswap/v4-sdk', '@uniswap/router-sdk', - '@uniswap/universal-router-sdk', + '@hkdex-tmp/universal_router_sdk', '@uniswap/uniswapx-sdk', '@uniswap/permit2-sdk', 'jsbi', @@ -286,7 +286,7 @@ export default defineConfig({ '@uniswap/v3-sdk', '@uniswap/v4-sdk', '@uniswap/router-sdk', - '@uniswap/universal-router-sdk', + '@hkdex-tmp/universal_router_sdk', '@uniswap/uniswapx-sdk', '@uniswap/permit2-sdk', 'jsbi', diff --git a/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap b/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap index a7a02717069..6a097d8735c 100644 --- a/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap +++ b/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap @@ -437,7 +437,7 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = ` disabled={false} focusVisibleStyle={ { - "backgroundColor": "#E500A5", + "backgroundColor": "#3d7fff", } } forwardedRef={[Function]} @@ -462,7 +462,7 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#FF37C7", + "backgroundColor": "#4177e2", "borderBottomColor": "transparent", "borderBottomLeftRadius": 16, "borderBottomRightRadius": 16, @@ -504,7 +504,7 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#FF37C7", + "backgroundColor": "#4177e2", "borderBottomColor": "transparent", "borderBottomLeftRadius": 16, "borderBottomRightRadius": 16, diff --git a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap index da2b061869e..d18613f43ac 100644 --- a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap +++ b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap @@ -100,7 +100,7 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = ` maxFontSizeMultiplier={1.2} style={ { - "color": "#FF37C7", + "color": "#4177e2", "fontFamily": "Basel Grotesk", "fontSize": 17, "fontWeight": "500", diff --git a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap index b29ce1681af..20dedf53a4a 100644 --- a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap +++ b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap @@ -5,16 +5,16 @@ exports[`renders selection circle 1`] = ` style={ { "alignItems": "center", - "borderBottomColor": "#FF37C7", + "borderBottomColor": "#4177e2", "borderBottomLeftRadius": 999999, "borderBottomRightRadius": 999999, "borderBottomWidth": 1, - "borderLeftColor": "#FF37C7", + "borderLeftColor": "#4177e2", "borderLeftWidth": 1, - "borderRightColor": "#FF37C7", + "borderRightColor": "#4177e2", "borderRightWidth": 1, "borderStyle": "solid", - "borderTopColor": "#FF37C7", + "borderTopColor": "#4177e2", "borderTopLeftRadius": 999999, "borderTopRightRadius": 999999, "borderTopWidth": 1, @@ -28,7 +28,7 @@ exports[`renders selection circle 1`] = ` { ' { expect(responseText).toContain(' - Uniswap Interface + HSKSwap | Trade Crypto on DeFi's Leading Exchange + + + - - + + + @@ -18,13 +22,16 @@ + + + - {extensionEligible && } + {/* Get App banner is hidden - only English is supported */} + {/* {extensionEligible && } */} {renderUkBanner && } {renderUniswapWrapped2025Banner} diff --git a/apps/web/src/pages/CreatePosition/CreatePosition.tsx b/apps/web/src/pages/CreatePosition/CreatePosition.tsx index e90d11fd5a3..e6d1e0370ec 100644 --- a/apps/web/src/pages/CreatePosition/CreatePosition.tsx +++ b/apps/web/src/pages/CreatePosition/CreatePosition.tsx @@ -112,7 +112,8 @@ const Toolbar = () => { } = useCreateLiquidityContext() const { protocolVersion } = positionState const customSlippageTolerance = useTransactionSettingsStore((s) => s.customSlippageTolerance) - const [versionDropdownOpen, setVersionDropdownOpen] = useState(false) + // 注释掉版本选择器 - 本期只做 V3 基础添加流动性 + // const [versionDropdownOpen, setVersionDropdownOpen] = useState(false) const [showResetModal, setShowResetModal] = useState(false) @@ -134,38 +135,39 @@ const Toolbar = () => { } }, [handleReset, isTestnetModeEnabled, prevIsTestnetModeEnabled]) - const handleVersionChange = useCallback( - (version: ProtocolVersion) => { - const versionUrl = getProtocolVersionLabel(version) - if (versionUrl) { - // Ensure useLiquidityUrlState is synced - setTimeout(() => navigate(`/positions/create/${versionUrl}`), 1) - } + // 注释掉版本切换功能 - 本期只做 V3 基础添加流动性 + // const handleVersionChange = useCallback( + // (version: ProtocolVersion) => { + // const versionUrl = getProtocolVersionLabel(version) + // if (versionUrl) { + // // Ensure useLiquidityUrlState is synced + // setTimeout(() => navigate(`/positions/create/${versionUrl}`), 1) + // } - setPositionState({ - ...DEFAULT_POSITION_STATE, - protocolVersion: version, - }) - setPriceRangeState(DEFAULT_PRICE_RANGE_STATE) - setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) - setVersionDropdownOpen(false) - }, - [setPositionState, setPriceRangeState, setStep, navigate], - ) + // setPositionState({ + // ...DEFAULT_POSITION_STATE, + // protocolVersion: version, + // }) + // setPriceRangeState(DEFAULT_PRICE_RANGE_STATE) + // setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) + // setVersionDropdownOpen(false) + // }, + // [setPositionState, setPriceRangeState, setStep, navigate], + // ) - const versionOptions = useMemo( - () => - [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2] - .filter((version) => version !== protocolVersion) - .map((version) => ( - handleVersionChange(version)}> - - {t('position.new.protocol', { protocol: getProtocolVersionLabel(version) })} - - - )), - [handleVersionChange, protocolVersion, t], - ) + // const versionOptions = useMemo( + // () => + // [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2] + // .filter((version) => version !== protocolVersion) + // .map((version) => ( + // handleVersionChange(version)}> + // + // {t('position.new.protocol', { protocol: getProtocolVersionLabel(version) })} + // + // + // )), + // [handleVersionChange, protocolVersion, t], + // ) return ( @@ -177,7 +179,8 @@ const Toolbar = () => { setShowResetModal(true)} isDisabled={isNativeTokenAOnly} /> - { alignRight > {versionOptions} - + */} ; tokenB: Maybe }>({ - tokenA: initialInputs.tokenA, + tokenA: initialInputs.tokenA ?? initialInputs.defaultInitialToken, tokenB: initialInputs.tokenB, }) + // Update currencyInputs when initialInputs change (e.g., when URL params are auto-initialized) + useEffect(() => { + setCurrencyInputs({ + tokenA: initialInputs.tokenA ?? initialInputs.defaultInitialToken, + tokenB: initialInputs.tokenB, + }) + }, [initialInputs.tokenA, initialInputs.tokenB, initialInputs.defaultInitialToken]) + return ( @@ -248,7 +261,7 @@ function CreatePositionContent({ currencyInputs={currencyInputs} setCurrencyInputs={setCurrencyInputs} initialPositionState={{ - fee: initialInputs.fee ?? undefined, + fee: initialInputs.fee, // fee is already guaranteed to have default value from useLiquidityUrlState hook: initialInputs.hook ?? undefined, protocolVersion: initialProtocolVersion, }} diff --git a/apps/web/src/pages/CreatePosition/CreatePositionModal.tsx b/apps/web/src/pages/CreatePosition/CreatePositionModal.tsx index 7fbbc4e6f56..b046d956448 100644 --- a/apps/web/src/pages/CreatePosition/CreatePositionModal.tsx +++ b/apps/web/src/pages/CreatePosition/CreatePositionModal.tsx @@ -77,6 +77,7 @@ export function CreatePositionModal({ setTransactionError(false) const isValidTx = isValidLiquidityTxContext(txInfo) + if ( !account || !isSignerMnemonicAccountDetails(account) || diff --git a/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx b/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx index ca0e94c378f..b1444a1197c 100644 --- a/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx +++ b/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx @@ -1,11 +1,12 @@ /* eslint-disable max-lines */ import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { Pool as V3Pool } from '@uniswap/v3-sdk' -import { Pool as V4Pool } from '@uniswap/v4-sdk' +import { Interface } from '@ethersproject/abi' import { TradingApi } from '@universe/api' import { useDepositInfo } from 'components/Liquidity/Create/hooks/useDepositInfo' +import { useOnChainLpApproval } from 'components/Liquidity/Create/hooks/useOnChainLpApproval' import { DYNAMIC_FEE_DATA, PositionState } from 'components/Liquidity/Create/types' import { useCreatePositionDependentAmountFallback } from 'components/Liquidity/hooks/useDependentAmountFallback' import { getTokenOrZeroAddress, validateCurrencyInput } from 'components/Liquidity/utils/currency' @@ -43,6 +44,13 @@ import { AccountDetails } from 'uniswap/src/features/wallet/types/AccountDetails import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' +/** + * Check if a chain ID is a HashKey chain + */ +function isHashKeyChain(chainId: number | undefined): boolean { + return chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet +} + /** * @internal - exported for testing */ @@ -80,7 +88,7 @@ export function generateAddLiquidityApprovalParams({ token1: getTokenOrZeroAddress(displayCurrencies.TOKEN1), amount0: currencyAmounts.TOKEN0.quotient.toString(), amount1: currencyAmounts.TOKEN1.quotient.toString(), - generatePermitAsTransaction: protocolVersion === ProtocolVersion.V4 ? canBatchTransactions : undefined, + generatePermitAsTransaction: undefined, // HashKey Chain only supports V3, no V4 permit support } satisfies TradingApi.CheckApprovalLPRequest } @@ -106,7 +114,7 @@ export function generateCreateCalldataQueryParams({ approvalCalldata?: TradingApi.CheckApprovalLPResponse positionState: PositionState ticks: [Maybe, Maybe] - poolOrPair: V3Pool | V4Pool | Pair | undefined + poolOrPair: V3Pool | Pair | undefined displayCurrencies: { [field in PositionField]: Maybe } currencyAmounts?: { [field in PositionField]?: Maybe> } independentField: PositionField @@ -119,7 +127,8 @@ export function generateCreateCalldataQueryParams({ !apiProtocolItems || !currencyAmounts?.TOKEN0 || !currencyAmounts.TOKEN1 || - !validateCurrencyInput(displayCurrencies) + !validateCurrencyInput(displayCurrencies) || + !positionState.fee // Ensure fee is defined ) { return undefined } @@ -181,7 +190,7 @@ export function generateCreateCalldataQueryParams({ return undefined } - const pool = poolOrPair as V4Pool | V3Pool | undefined + const pool = poolOrPair as V3Pool | undefined if (!pool || !displayCurrencies.TOKEN0 || !displayCurrencies.TOKEN1) { return undefined } @@ -204,6 +213,19 @@ export function generateCreateCalldataQueryParams({ const independentAmount = currencyAmounts[independentField] const dependentAmount = currencyAmounts[dependentField] + // Ensure fee is defined + if (!positionState.fee?.feeAmount) { + return undefined + } + + // V3 pool configuration (HashKey Chain only supports V3) + const poolConfig: any = { + tickSpacing, + token0: getTokenOrZeroAddress(displayCurrencies.TOKEN0), + token1: getTokenOrZeroAddress(displayCurrencies.TOKEN1), + fee: positionState.fee.isDynamic ? DYNAMIC_FEE_DATA.feeAmount : positionState.fee.feeAmount, + } + return { simulateTransaction: !( permitData || @@ -224,13 +246,7 @@ export function generateCreateCalldataQueryParams({ position: { tickLower: tickLower ?? undefined, tickUpper: tickUpper ?? undefined, - pool: { - tickSpacing, - token0: getTokenOrZeroAddress(displayCurrencies.TOKEN0), - token1: getTokenOrZeroAddress(displayCurrencies.TOKEN1), - fee: positionState.fee?.isDynamic ? DYNAMIC_FEE_DATA.feeAmount : positionState.fee?.feeAmount, - hooks: positionState.hook, - }, + pool: poolConfig, }, } satisfies TradingApi.CreateLPPositionRequest } @@ -252,10 +268,20 @@ export function generateCreatePositionTxRequest({ createCalldata?: TradingApi.CreateLPPositionResponse createCalldataQueryParams?: TradingApi.CreateLPPositionRequest currencyAmounts?: { [field in PositionField]?: Maybe> } - poolOrPair: Pair | undefined + poolOrPair: V3Pool | Pair | undefined canBatchTransactions: boolean }): CreatePositionTxAndGasInfo | undefined { - if (!createCalldata || !currencyAmounts?.TOKEN0 || !currencyAmounts.TOKEN1) { + if (!currencyAmounts?.TOKEN0 || !currencyAmounts.TOKEN1) { + return undefined + } + + // For HashKey chains, Trading API doesn't support creating LP positions + // So createCalldata will be undefined, and we'll use async step instead + const chainId = currencyAmounts.TOKEN0.currency.chainId + const isHashKey = isHashKeyChain(chainId) + + // For non-HashKey chains, createCalldata is required + if (!isHashKey && !createCalldata) { return undefined } @@ -287,16 +313,31 @@ export function generateCreatePositionTxRequest({ const validatedToken0PermitTransaction = validateTransactionRequest(approvalCalldata?.token0PermitTransaction) const validatedToken1PermitTransaction = validateTransactionRequest(approvalCalldata?.token1PermitTransaction) - const txRequest = validateTransactionRequest(createCalldata.create) - if (!txRequest && !(validatedToken0PermitTransaction || validatedToken1PermitTransaction)) { + // For HashKey chains, we don't have createCalldata from Trading API + // So txRequest will be undefined, and we'll use async step with createPositionRequestArgs + const txRequest = createCalldata?.create ? validateTransactionRequest(createCalldata.create) : undefined + + // For HashKey chains, allow missing txRequest (will use async step) + // For other chains, require txRequest unless using permit transactions + if (!isHashKey && !txRequest && !(validatedToken0PermitTransaction || validatedToken1PermitTransaction)) { // Allow missing txRequest if mismatched (unsigned flow using token0PermitTransaction/2) return undefined } - const queryParams: TradingApi.CreateLPPositionRequest | undefined = - protocolVersion === ProtocolVersion.V4 - ? { ...createCalldataQueryParams, batchPermitData: validatedPermitRequest } - : createCalldataQueryParams + // HashKey Chain only supports V3, so no need for V4-specific batchPermitData handling + const queryParams: TradingApi.CreateLPPositionRequest | undefined = createCalldataQueryParams + + // For HashKey chains, get sqrtRatioX96 from poolOrPair if available (V3 pools only) + // For other chains, get it from createCalldata + let sqrtRatioX96: string | undefined + if (isHashKey && poolOrPair && protocolVersion !== ProtocolVersion.V2) { + // For V3, poolOrPair is a V3Pool, not a Pair + const pool = poolOrPair as V3Pool + sqrtRatioX96 = pool.sqrtRatioX96.toString() + } + if (!sqrtRatioX96) { + sqrtRatioX96 = createCalldata?.sqrtRatioX96 + } return { type: LiquidityTransactionType.Create, @@ -307,7 +348,9 @@ export function generateCreatePositionTxRequest({ type: LiquidityTransactionType.Create, currency0Amount: currencyAmounts.TOKEN0, currency1Amount: currencyAmounts.TOKEN1, - liquidityToken: protocolVersion === ProtocolVersion.V2 ? poolOrPair?.liquidityToken : undefined, + liquidityToken: protocolVersion === ProtocolVersion.V2 && poolOrPair && 'liquidityToken' in poolOrPair + ? (poolOrPair as Pair).liquidityToken + : undefined, }, approveToken0Request: validatedApprove0Request, approveToken1Request: validatedApprove1Request, @@ -319,7 +362,7 @@ export function generateCreatePositionTxRequest({ token0PermitTransaction: validatedToken0PermitTransaction, token1PermitTransaction: validatedToken1PermitTransaction, positionTokenPermitTransaction: undefined, - sqrtRatioX96: createCalldata.sqrtRatioX96, + sqrtRatioX96, } satisfies CreatePositionTxAndGasInfo } @@ -343,37 +386,89 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) protocolVersion, currencies, ticks, - poolOrPair, + poolOrPair: rawPoolOrPair, depositState, creatingPoolOrPair, currentTransactionStep, positionState, setRefetch, } = useCreateLiquidityContext() + + // Filter out V4 pools - HashKey Chain only supports V3 + const poolOrPair = useMemo(() => { + if (!rawPoolOrPair) return undefined + // If it's a Pair (V2), return as is + if ('liquidityToken' in rawPoolOrPair) { + return rawPoolOrPair + } + // If it's a V3Pool (has 'fee' and 'tickSpacing' properties), return as is + if ('fee' in rawPoolOrPair && 'tickSpacing' in rawPoolOrPair && 'token0' in rawPoolOrPair) { + const token0 = rawPoolOrPair.token0 + // V3Pool has Token, V4Pool has Currency - check if token0 is Token + if ('address' in token0) { + return rawPoolOrPair as V3Pool + } + } + // Otherwise, it's V4Pool, return undefined for HashKey chains + return undefined + }, [rawPoolOrPair]) + const account = useWallet().evmAccount + + if (!currencies?.display) { + throw new TypeError('currencies.display is undefined in CreatePositionTxContext') + } + const { TOKEN0, TOKEN1 } = currencies.display + + if (!depositState) { + throw new TypeError('depositState is undefined in CreatePositionTxContext') + } + const { exactField } = depositState - const invalidRange = protocolVersion !== ProtocolVersion.V2 && isInvalidRange(ticks[0], ticks[1]) + let invalidRange: boolean + try { + invalidRange = protocolVersion !== ProtocolVersion.V2 && isInvalidRange(ticks?.[0], ticks?.[1]) + } catch (error) { + invalidRange = false // Default to false on error + } + const depositInfoProps = useMemo(() => { - const [tickLower, tickUpper] = ticks - const outOfRange = isOutOfRange({ - poolOrPair, - lowerTick: tickLower, - upperTick: tickUpper, - }) + try { + const [tickLower, tickUpper] = ticks || [undefined, undefined] + const outOfRange = isOutOfRange({ + poolOrPair, + lowerTick: tickLower, + upperTick: tickUpper, + }) - return { - protocolVersion, - poolOrPair, - address: account?.address, - token0: TOKEN0, - token1: TOKEN1, - tickLower: protocolVersion !== ProtocolVersion.V2 ? (tickLower ?? undefined) : undefined, - tickUpper: protocolVersion !== ProtocolVersion.V2 ? (tickUpper ?? undefined) : undefined, - exactField, - exactAmounts: depositState.exactAmounts, - skipDependentAmount: protocolVersion === ProtocolVersion.V2 ? false : outOfRange || invalidRange, + return { + protocolVersion, + poolOrPair, + address: account?.address, + token0: TOKEN0, + token1: TOKEN1, + tickLower: protocolVersion !== ProtocolVersion.V2 ? (tickLower ?? undefined) : undefined, + tickUpper: protocolVersion !== ProtocolVersion.V2 ? (tickUpper ?? undefined) : undefined, + exactField, + exactAmounts: depositState?.exactAmounts, + skipDependentAmount: protocolVersion === ProtocolVersion.V2 ? false : outOfRange || invalidRange, + } + } catch (error) { + // Return minimal props on error + return { + protocolVersion, + poolOrPair, + address: account?.address, + token0: TOKEN0, + token1: TOKEN1, + tickLower: undefined, + tickUpper: undefined, + exactField, + exactAmounts: depositState?.exactAmounts, + skipDependentAmount: true, // Skip dependent amount on error + } } }, [TOKEN0, TOKEN1, exactField, ticks, poolOrPair, depositState, account?.address, protocolVersion, invalidRange]) @@ -395,7 +490,113 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) const [transactionError, setTransactionError] = useState(false) + // Check if this is a HashKey chain (Trading API doesn't support HashKey chains) + const isHashKey = isHashKeyChain(poolOrPair?.chainId) + + // Use on-chain approval check for HashKey chains + const token0Amount = useMemo(() => { + const amount = currencyAmounts?.TOKEN0 + if (!amount || !(amount.currency instanceof Token)) return undefined + return amount as CurrencyAmount + }, [currencyAmounts?.TOKEN0]) + + const token1Amount = useMemo(() => { + const amount = currencyAmounts?.TOKEN1 + if (!amount || !(amount.currency instanceof Token)) return undefined + return amount as CurrencyAmount + }, [currencyAmounts?.TOKEN1]) + + const onChainApproval = useOnChainLpApproval({ + token0: TOKEN0 instanceof Token ? TOKEN0 : undefined, + token1: TOKEN1 instanceof Token ? TOKEN1 : undefined, + amount0: token0Amount, + amount1: token1Amount, + owner: account?.address, + chainId: poolOrPair?.chainId, + }) + + // Build approval transaction requests for HashKey chains based on on-chain check + // Supports both traditional ERC20 approve and Permit2 authorization + const hashKeyApprovalCalldata = useMemo(() => { + try { + if (!isHashKey || !onChainApproval.positionManagerAddress || !poolOrPair?.chainId) { + return undefined + } + + const positionManagerAddress = onChainApproval.positionManagerAddress + const approveInterface = new Interface(['function approve(address spender,uint256 value)']) + + // Only build approval transactions if needed (considering Permit2 authorization) + // If Permit2 authorization is valid, we don't need traditional approve + const token0NeedsApproval = onChainApproval.token0NeedsApproval && TOKEN0 instanceof Token && currencyAmounts?.TOKEN0 + const token1NeedsApproval = onChainApproval.token1NeedsApproval && TOKEN1 instanceof Token && currencyAmounts?.TOKEN1 + + const token0ApprovalTx = token0NeedsApproval && TOKEN0 instanceof Token && positionManagerAddress + ? { + to: TOKEN0.address, + data: approveInterface.encodeFunctionData('approve', [ + positionManagerAddress, + MaxUint256.toString(), + ]), + value: '0x0', + chainId: poolOrPair.chainId, + } + : undefined + + const token1ApprovalTx = token1NeedsApproval && TOKEN1 instanceof Token && positionManagerAddress + ? { + to: TOKEN1.address, + data: approveInterface.encodeFunctionData('approve', [ + positionManagerAddress, + MaxUint256.toString(), + ]), + value: '0x0', + chainId: poolOrPair.chainId, + } + : undefined + + // Return in Trading API format for compatibility + // Only include approvals if they are needed + const token0Approval = token0ApprovalTx ? validateTransactionRequest(token0ApprovalTx) : undefined + const token1Approval = token1ApprovalTx ? validateTransactionRequest(token1ApprovalTx) : undefined + + // Note: Permit2 permit transactions (token0PermitTransaction/token1PermitTransaction) + // are typically generated by Trading API. For HashKey Chain, we currently only support + // traditional approve transactions. If Permit2 authorization exists and is valid, + // no approval transactions are needed. + + // Return undefined if no approvals needed (similar to Trading API behavior) + if (!token0Approval && !token1Approval) { + return undefined + } + + return { + token0Approval, + token1Approval, + // TODO: Add Permit2 permit transaction support for HashKey Chain if needed + // token0PermitTransaction: ..., + // token1PermitTransaction: ..., + } as TradingApi.CheckApprovalLPResponse | undefined + } catch (error) { + return undefined + } + }, [ + isHashKey, + onChainApproval.positionManagerAddress, + onChainApproval.token0NeedsApproval, + onChainApproval.token1NeedsApproval, + onChainApproval.token0NeedsPermit2Approval, + onChainApproval.token1NeedsPermit2Approval, + TOKEN0, + TOKEN1, + currencyAmounts, + poolOrPair?.chainId, + ]) + const addLiquidityApprovalParams = useMemo(() => { + if (!currencies?.display) { + return undefined + } return generateAddLiquidityApprovalParams({ address: account?.address, protocolVersion, @@ -403,10 +604,14 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) currencyAmounts, canBatchTransactions, }) - }, [account?.address, protocolVersion, currencies.display, currencyAmounts, canBatchTransactions]) + }, [account?.address, protocolVersion, currencies?.display, currencyAmounts, canBatchTransactions]) + + // For HashKey chains, skip Trading API and use on-chain check + const shouldEnableTradingApiApprovalQuery = + !!addLiquidityApprovalParams && !inputError && !transactionError && !invalidRange && !isHashKey const { - data: approvalCalldata, + data: tradingApiApprovalCalldata, error: approvalError, isLoading: approvalLoading, refetch: approvalRefetch, @@ -414,10 +619,13 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) params: addLiquidityApprovalParams, staleTime: 5 * ONE_SECOND_MS, retry: false, - enabled: !!addLiquidityApprovalParams && !inputError && !transactionError && !invalidRange, + enabled: shouldEnableTradingApiApprovalQuery, }) - if (approvalError) { + // Use on-chain approval data for HashKey chains, Trading API data for others + const approvalCalldata = isHashKey ? hashKeyApprovalCalldata : tradingApiApprovalCalldata + + if (approvalError && !isHashKey) { const message = parseErrorMessageTitle(approvalError, { defaultTitle: 'unknown CheckLpApprovalQuery' }) logger.error(message, { tags: { file: 'CreatePositionTxContext', function: 'useEffect' }, @@ -430,6 +638,9 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) const gasFeeToken1PermitUSD = useUSDCurrencyAmountOfGasFee(poolOrPair?.chainId, approvalCalldata?.gasFeeToken1Permit) const createCalldataQueryParams = useMemo(() => { + if (!currencies?.display || !depositState) { + return undefined + } return generateCreateCalldataQueryParams({ account, approvalCalldata, @@ -438,7 +649,7 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) creatingPoolOrPair, displayCurrencies: currencies.display, ticks, - poolOrPair, + poolOrPair: poolOrPair as V3Pool | Pair | undefined, // Filter out V4Pool - HashKey Chain only supports V3 currencyAmounts, independentField: depositState.exactField, slippageTolerance: customSlippageTolerance, @@ -451,9 +662,9 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) ticks, poolOrPair, positionState, - depositState.exactField, + depositState?.exactField, customSlippageTolerance, - currencies.display, + currencies?.display, protocolVersion, ]) @@ -461,14 +672,21 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) currentTransactionStep?.step.type === TransactionStepType.IncreasePositionTransaction || currentTransactionStep?.step.type === TransactionStepType.IncreasePositionTransactionAsync + // For HashKey chains, we don't require approvalCalldata from Trading API + // For other chains, approvalCalldata is required + const requiresApprovalCalldata = !isHashKey + + // For HashKey chains, Trading API doesn't support creating LP positions + // So we disable the query entirely for HashKey chains const isQueryEnabled = + !isHashKey && // Disable Trading API query for HashKey chains !isUserCommittedToCreate && !inputError && !transactionError && !approvalLoading && !approvalError && !invalidRange && - Boolean(approvalCalldata) && + (requiresApprovalCalldata ? Boolean(approvalCalldata) : true) && Boolean(createCalldataQueryParams) const { @@ -555,7 +773,7 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) createCalldata, createCalldataQueryParams, currencyAmounts, - poolOrPair: protocolVersion === ProtocolVersion.V2 ? poolOrPair : undefined, + poolOrPair: protocolVersion === ProtocolVersion.V2 && poolOrPair instanceof Pair ? poolOrPair : undefined, canBatchTransactions, }) }, [ @@ -568,6 +786,7 @@ export function CreatePositionTxContextProvider({ children }: PropsWithChildren) canBatchTransactions, ]) + const value = useMemo( (): CreatePositionTxContextType => ({ txInfo, diff --git a/apps/web/src/pages/Explore/index.tsx b/apps/web/src/pages/Explore/index.tsx index 9f192e158cb..cd4aa079b56 100644 --- a/apps/web/src/pages/Explore/index.tsx +++ b/apps/web/src/pages/Explore/index.tsx @@ -252,7 +252,7 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { {currentKey === ExploreTab.Pools && ( - diff --git a/apps/web/src/pages/MigrateV2/index.tsx b/apps/web/src/pages/MigrateV2/index.tsx index 739809cd173..cbefdc76310 100644 --- a/apps/web/src/pages/MigrateV2/index.tsx +++ b/apps/web/src/pages/MigrateV2/index.tsx @@ -6,7 +6,6 @@ import { LightCard } from 'components/Card/cards' import { AutoColumn } from 'components/deprecated/Column' import MigrateSushiPositionCard from 'components/PositionCard/Sushi' import MigrateV2PositionCard from 'components/PositionCard/V2' -import { SwitchLocaleLink } from 'components/SwitchLocaleLink' import { Dots } from 'components/swap/styled' import { V2Unsupported } from 'components/V2Unsupported' import { useAccount } from 'hooks/useAccount' @@ -207,7 +206,7 @@ export default function MigrateV2() { - + {/* */} ) } diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemA.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemA.tsx index b82a3bb3a1f..2be2ffa02f8 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemA.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemA.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemA({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemA({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemB.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemB.tsx index 97a95e05515..174dd57da35 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemB.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemB.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemB({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemB({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemC.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemC.tsx index 43409765e33..8e262158413 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemC.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemC.tsx @@ -2,7 +2,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emb import { useId } from 'react' import { useSporeColors } from 'ui/src' -export function EmblemC({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemC({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() const clipPathId = useId() diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemD.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemD.tsx index 6d2dce5ec4b..6494f47899c 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemD.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemD.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemD({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemD({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemE.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemE.tsx index d07e8cff91b..a19e9e39be1 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemE.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemE.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemE({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemE({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemF.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemF.tsx index f39c3cf8cb7..048afbf43d9 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemF.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemF.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemF({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemF({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemG.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemG.tsx index d0e5263fc54..22d31f8b672 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemG.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemG.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemG({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemG({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemH.tsx b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemH.tsx index 2568174c990..2a470109700 100644 --- a/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemH.tsx +++ b/apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemH.tsx @@ -1,7 +1,7 @@ import { EmblemProps } from 'pages/Portfolio/components/AnimatedStyledBanner/Emblems/types' import { useSporeColors } from 'ui/src' -export function EmblemH({ fill = '#FF37C7', opacity = 1, ...props }: EmblemProps): JSX.Element { +export function EmblemH({ fill = '#4177e2', opacity = 1, ...props }: EmblemProps): JSX.Element { const colors = useSporeColors() return ( diff --git a/apps/web/src/pages/Positions/TopPools.tsx b/apps/web/src/pages/Positions/TopPools.tsx index 92b14c1ebe2..3b7c9e1352d 100644 --- a/apps/web/src/pages/Positions/TopPools.tsx +++ b/apps/web/src/pages/Positions/TopPools.tsx @@ -5,12 +5,14 @@ import { ALL_NETWORKS_ARG } from '@universe/api' import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { ExternalArrowLink } from 'components/Liquidity/ExternalArrowLink' import { useAccount } from 'hooks/useAccount' +import { useHSKSubgraphPools } from 'hooks/useHSKSubgraphPools' import { TopPoolsSection } from 'pages/Positions/TopPoolsSection' import { useTranslation } from 'react-i18next' import { useTopPools } from 'state/explore/topPools' import { Flex, useMedia } from 'ui/src' import { useExploreStatsQuery } from 'uniswap/src/data/rest/exploreStats' import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useMemo } from 'react' const MAX_BOOSTED_POOLS = 3 @@ -21,47 +23,133 @@ export function TopPools({ chainId }: { chainId: UniverseChainId | null }) { const media = useMedia() const isBelowXlScreen = !media.xl + // 使用 HSK Subgraph 获取所有 pools 数据(获取足够多的数量以确保获取全部) + // 注意:GraphQL 查询可能有限制,如果 pools 数量超过限制,可能需要分页获取 const { - data: exploreStatsData, - isLoading: exploreStatsLoading, - error: exploreStatsError, - } = useExploreStatsQuery({ - input: { chainId: chainId ? chainId.toString() : ALL_NETWORKS_ARG }, - }) + data: hskPools, + isLoading: hskPoolsLoading, + error: hskPoolsError, + } = useHSKSubgraphPools(1000) // 获取足够多的 pools(1000 应该足够覆盖所有 pools) - const { topPools, topBoostedPools } = useTopPools({ - topPoolData: { data: exploreStatsData, isLoading: exploreStatsLoading, isError: !!exploreStatsError }, - sortState: { sortDirection: OrderDirection.Desc, sortBy: PoolSortFields.TVL }, + // 调试信息 + console.log('[TopPools] HSK Subgraph 数据:', { + hskPools, + hskPoolsLoading, + hskPoolsError, + poolsCount: hskPools?.length, }) - const displayBoostedPools = - topBoostedPools && topBoostedPools.length > 0 && Boolean(account.address) && isLPIncentivesEnabled + // 按 TVL 排序 pools + const sortedHSKPools = useMemo(() => { + if (!hskPools) { + console.log('[TopPools] hskPools 为空') + return [] + } + console.log('[TopPools] 开始排序,pools 数量:', hskPools.length) + const sorted = [...hskPools].sort((a, b) => { + // totalLiquidity.value 是 number 类型 + const tvlA = typeof a.totalLiquidity?.value === 'number' + ? a.totalLiquidity.value + : parseFloat(String(a.totalLiquidity?.value || '0')) + const tvlB = typeof b.totalLiquidity?.value === 'number' + ? b.totalLiquidity.value + : parseFloat(String(b.totalLiquidity?.value || '0')) + return tvlB - tvlA + }) + console.log('[TopPools] 排序后的 pools (前6个):', sorted.slice(0, 6).map(p => ({ + id: p.id, + token0: p.token0?.symbol, + token1: p.token1?.symbol, + tvl: p.totalLiquidity?.value + }))) + // 输出所有 pools 的完整数据 + console.log('[TopPools] 所有 pools 数据 (完整):', { + totalCount: sorted.length, + allPools: sorted.map(p => ({ + id: p.id, + address: p.address, + token0: { + symbol: p.token0?.symbol, + name: p.token0?.name, + address: p.token0?.address, + }, + token1: { + symbol: p.token1?.symbol, + name: p.token1?.name, + address: p.token1?.address, + }, + totalLiquidity: p.totalLiquidity?.value, + feeTier: p.feeTier, + volume24h: p.volume24h?.value, + volume30d: p.volume30d?.value, + txCount: p.txCount, + })) + }) + return sorted + }, [hskPools]) + + // 屏蔽原有的 explore stats 查询 + // const { + // data: exploreStatsData, + // isLoading: exploreStatsLoading, + // error: exploreStatsError, + // } = useExploreStatsQuery({ + // input: { chainId: chainId ? chainId.toString() : ALL_NETWORKS_ARG }, + // }) + + // const { topPools: exploreTopPools, topBoostedPools } = useTopPools({ + // topPoolData: { data: exploreStatsData, isLoading: exploreStatsLoading, isError: !!exploreStatsError }, + // sortState: { sortDirection: OrderDirection.Desc, sortBy: PoolSortFields.TVL }, + // }) + + // 只使用 HSK Subgraph 数据(所有 pools 都是 V3) + // 按 TVL 排序后,只取前 6 个 + const topPools = useMemo(() => { + return sortedHSKPools.slice(0, 6) + }, [sortedHSKPools]) + const isLoading = hskPoolsLoading + const hasError = !!hskPoolsError + + // 屏蔽 boosted pools(因为我们已经屏蔽了原有的 explore stats) + const displayBoostedPools = false // HSK Subgraph 数据不包含 boosted pools const displayTopPools = topPools && topPools.length > 0 - if (!isBelowXlScreen) { - return null - } + // 调试信息 + console.log('[TopPools] 渲染状态:', { + isBelowXlScreen, + topPoolsLength: topPools?.length, + displayTopPools, + isLoading, + hasError, + media: media, + }) + + // 临时:即使屏幕很大也显示,用于调试 + // if (!isBelowXlScreen) { + // return null + // } return ( - {displayBoostedPools && ( + {/* {displayBoostedPools && ( {t('explore.more.unichain')} - )} + )} */} {displayTopPools && ( - - + + {/* 隐藏 explore more pools 链接 */} + {/* {t('explore.more.pools')} - + */} )} diff --git a/apps/web/src/pages/Positions/TopPoolsCard.tsx b/apps/web/src/pages/Positions/TopPoolsCard.tsx index 6e0411f48bb..02e301ff5ac 100644 --- a/apps/web/src/pages/Positions/TopPoolsCard.tsx +++ b/apps/web/src/pages/Positions/TopPoolsCard.tsx @@ -1,4 +1,5 @@ import { gqlToCurrency, supportedChainIdFromGQLChain, unwrapToken } from 'appGraphql/data/util' +import { Percent } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { LPIncentiveRewardsBadge } from 'components/Liquidity/LPIncentives/LPIncentiveRewardsBadge' @@ -7,19 +8,89 @@ import { Trans } from 'react-i18next' import { PoolStat } from 'state/explore/types' import { Flex, Text } from 'ui/src' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' export function TopPoolsCard({ pool }: { pool: PoolStat }) { const { defaultChainId } = useEnabledChains() const { formatPercent } = useLocalizationContext() + // Console pool 数据用于调试 + console.log('[TopPoolsCard] Pool 数据:', { + id: pool.id, + token0: pool.token0, + token1: pool.token1, + apr: pool.apr, + aprType: typeof pool.apr, + aprIsPercent: pool.apr instanceof Percent, + aprHasToFixed: pool.apr && typeof (pool.apr as any).toFixed === 'function', + totalLiquidity: pool.totalLiquidity, + volume1Day: pool.volume1Day, + protocolVersion: pool.protocolVersion, + feeTier: pool.feeTier, + boostedApr: pool.boostedApr, + }) + const chainId = supportedChainIdFromGQLChain(pool.chain as GraphQLApi.Chain) ?? defaultChainId + + // 尝试使用 gqlToCurrency,如果失败则直接使用 pool 中的 symbol const token0 = pool.token0 ? gqlToCurrency(unwrapToken(chainId, pool.token0)) : undefined const token1 = pool.token1 ? gqlToCurrency(unwrapToken(chainId, pool.token1)) : undefined + // 如果 gqlToCurrency 返回 undefined(比如数据来自 HSK subgraph),直接使用 pool 中的 symbol + const token0Symbol = token0?.symbol || pool.token0?.symbol || '--' + const token1Symbol = token1?.symbol || pool.token1?.symbol || '--' + const formattedApr = pool.boostedApr ? formatPercent(pool.boostedApr) : null + // 安全地格式化 APR:检查 pool.apr 是否是 Percent 对象,如果是则使用 toFixed,否则转换为数字 + const formatApr = (apr: Percent | any): string => { + if (!apr) { + return formatPercent(0) + } + // 检查是否有 toFixed 方法(Percent 对象应该有) + if (typeof apr.toFixed === 'function') { + return formatPercent(apr.toFixed(3)) + } + // 如果没有 toFixed 方法,尝试从 numerator 和 denominator 计算 + // Percent 对象有 numerator 和 denominator 属性 + // 注意:protobuf 序列化后,numerator 和 denominator 可能是数组(JSBI 序列化) + if ('numerator' in apr && 'denominator' in apr) { + let numValue: number + let denValue: number + + // 处理数组格式(protobuf 序列化后的 JSBI) + if (Array.isArray(apr.numerator)) { + // JSBI 序列化为数组,需要转换为数字 + // 空数组表示 0 + if (apr.numerator.length === 0) { + numValue = 0 + } else { + // JSBI 数组格式:[sign, ...digits],sign 是 1 或 -1 + // 简化处理:如果是空数组或只有一个元素,直接使用 + numValue = apr.numerator.length > 0 ? Number(apr.numerator[0] || 0) : 0 + } + } else { + numValue = Number(apr.numerator) || 0 + } + + if (Array.isArray(apr.denominator)) { + if (apr.denominator.length === 0) { + denValue = 1 + } else { + denValue = Number(apr.denominator[0] || 1) + } + } else { + denValue = Number(apr.denominator) || 1 + } + + // 计算百分比:Percent 是 numerator/denominator,转换为百分比需要 * 100 + const percentValue = denValue > 0 ? (numValue / denValue) * 100 : 0 + return formatPercent(percentValue.toFixed(3)) + } + // 最后的回退:尝试直接转换为数字 + return formatPercent(Number(apr) || 0) + } + return ( - {token0?.symbol} / {token1?.symbol} + {token0Symbol} / {token1Symbol} @@ -49,7 +122,7 @@ export function TopPoolsCard({ pool }: { pool: PoolStat }) { - {formatPercent(pool.apr.toFixed(3))} + {formatApr(pool.apr)} {formattedApr && } diff --git a/apps/web/src/pages/Positions/TopPoolsSection.tsx b/apps/web/src/pages/Positions/TopPoolsSection.tsx index 629349ad0be..edb9cee6da3 100644 --- a/apps/web/src/pages/Positions/TopPoolsSection.tsx +++ b/apps/web/src/pages/Positions/TopPoolsSection.tsx @@ -5,6 +5,14 @@ import { PoolStat } from 'state/explore/types' import { Flex, Text } from 'ui/src' export function TopPoolsSection({ pools, title, isLoading }: { pools: PoolStat[]; title: string; isLoading: boolean }) { + // 调试信息 + console.log('[TopPoolsSection] 渲染状态:', { + poolsLength: pools.length, + isLoading, + title, + pools: pools.slice(0, 3).map((p) => ({ id: p.id, token0: p.token0?.symbol, token1: p.token1?.symbol })), + }) + if (isLoading) { return ( @@ -18,6 +26,11 @@ export function TopPoolsSection({ pools, title, isLoading }: { pools: PoolStat[] ) } + if (!pools || pools.length === 0) { + console.log('[TopPoolsSection] 没有 pools 数据') + return null + } + return ( {title} diff --git a/apps/web/src/pages/Positions/index.tsx b/apps/web/src/pages/Positions/index.tsx index 795d43a7e94..1b8d67fd5a0 100644 --- a/apps/web/src/pages/Positions/index.tsx +++ b/apps/web/src/pages/Positions/index.tsx @@ -1,20 +1,16 @@ /* eslint-disable max-lines */ import { PositionStatus, ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { FeatureFlags, useFeatureFlag } from '@universe/gating' -import PROVIDE_LIQUIDITY from 'assets/images/provideLiquidity.png' import tokenLogo from 'assets/images/token-logo.png' -import V4_HOOK from 'assets/images/v4Hooks.png' import { ExpandoRow } from 'components/AccountDrawer/MiniPortfolio/ExpandoRow' -import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' -import { ExternalArrowLink } from 'components/Liquidity/ExternalArrowLink' import { LiquidityPositionCard, LiquidityPositionCardLoader } from 'components/Liquidity/LiquidityPositionCard' import { LpIncentiveClaimModal } from 'components/Liquidity/LPIncentives/LpIncentiveClaimModal' -import LpIncentiveRewardsCard from 'components/Liquidity/LPIncentives/LpIncentiveRewardsCard' import { PositionsHeader } from 'components/Liquidity/PositionsHeader' import { PositionInfo } from 'components/Liquidity/types' import { getPositionUrl } from 'components/Liquidity/utils/getPositionUrl' import { parseRestPosition } from 'components/Liquidity/utils/parseFromRest' +import { useAppKit } from 'components/Web3Provider/reownConfig' import { useAccount } from 'hooks/useAccount' import { useLpIncentives } from 'hooks/useLpIncentives' import { atom, useAtom } from 'jotai' @@ -27,11 +23,8 @@ import { usePendingLPTransactionsChangeListener } from 'state/transactions/hooks import { useRequestPositionsForSavedPairs } from 'state/user/hooks' import { ClickableTamaguiStyle } from 'theme/components/styles' import { Anchor, Button, Flex, Text, useMedia } from 'ui/src' -import { CloseIconWithHover } from 'ui/src/components/icons/CloseIconWithHover' -import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' import { Pools } from 'ui/src/components/icons/Pools' import { Wallet } from 'ui/src/components/icons/Wallet' -import { uniswapUrls } from 'uniswap/src/constants/urls' import { useGetPositionsInfiniteQuery } from 'uniswap/src/data/rest/getPositions' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' @@ -50,7 +43,7 @@ const PAGE_SIZE = 25 function DisconnectedWalletView() { const { t } = useTranslation() - const accountDrawer = useAccountDrawer() + const { open } = useAppKit() const setMenu = useSetMenu() const connectedWithoutEVM = useIsMissingPlatformWallet(Platform.EVM) @@ -58,7 +51,7 @@ function DisconnectedWalletView() { if (connectedWithoutEVM) { setMenu({ variant: MenuStateVariant.CONNECT_PLATFORM, platform: Platform.EVM }) } - accountDrawer.open() + open({ view: 'Connect' }) } return ( @@ -90,7 +83,7 @@ function DisconnectedWalletView() { size="small" emphasis="secondary" tag="a" - href="/positions/create/v4" + href="/positions/create/v3" $platform-web={{ textDecoration: 'none', }} @@ -103,7 +96,8 @@ function DisconnectedWalletView() { - + {/* 未连接钱包时隐藏 "providing liquidity" 和 "hooks on v4" 内容 */} + {/* - + */} ) } @@ -163,7 +157,7 @@ function EmptyPositionsView() { variant="default" size="small" tag="a" - href="/positions/create/v4" + href="/positions/create/v3" $platform-web={{ textDecoration: 'none', }} @@ -214,8 +208,9 @@ function LearnMoreTile({ ) } -const chainFilterAtom = atom(null) -const versionFilterAtom = atom([ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2]) +// 本期只做 V3 基础添加流动性 - 默认筛选 HashKey Chain + V3 +const chainFilterAtom = atom(UniverseChainId.HashKey) +const versionFilterAtom = atom([ProtocolVersion.V3]) const statusFilterAtom = atom([PositionStatus.IN_RANGE, PositionStatus.OUT_OF_RANGE]) function VirtualizedPositionsList({ @@ -263,7 +258,7 @@ function VirtualizedPositionsList({ @@ -402,7 +397,7 @@ export default function Pool() { $lg={{ px: '$spacing20' }} > - {isLPIncentivesEnabled && ( + {/* {isLPIncentivesEnabled && ( { @@ -412,8 +407,8 @@ export default function Pool() { setTokenRewards={setTokenRewards} initialHasCollectedRewards={hasCollectedRewards} /> - )} - + )} */} + )} - {!statusFilter.includes(PositionStatus.CLOSED) && !closedCTADismissed && account.address && ( + {/* {!statusFilter.includes(PositionStatus.CLOSED) && !closedCTADismissed && account.address && ( setClosedCTADismissed(true)} size="$icon.20" /> - )} - {isConnected && ( + )} */} + {/* 注释掉 Pool Finder 入口 - 本期只做基础添加流动性 */} + {/* {isConnected && ( {t('pool.import.link.description')} @@ -505,11 +501,11 @@ export default function Pool() { - )} + )} */} - {isConnected && ( + {/* {isConnected && ( {t('liquidity.learnMoreLabel')} @@ -528,7 +524,7 @@ export default function Pool() { {t('common.button.learn')} - )} + )} */} {isLPIncentivesEnabled && ( diff --git a/apps/web/src/pages/RouteDefinitions.tsx b/apps/web/src/pages/RouteDefinitions.tsx index 48a0eca12b2..ec738ca6689 100644 --- a/apps/web/src/pages/RouteDefinitions.tsx +++ b/apps/web/src/pages/RouteDefinitions.tsx @@ -3,7 +3,6 @@ import { getExploreDescription, getExploreTitle } from 'pages/getExploreTitle' import { getPortfolioDescription, getPortfolioTitle } from 'pages/getPortfolioTitle' import { getAddLiquidityPageTitle, getPositionPageDescription, getPositionPageTitle } from 'pages/getPositionPageTitle' // High-traffic pages (index and /swap) should not be lazy-loaded. -import Landing from 'pages/Landing' import Swap from 'pages/Swap' import { lazy, ReactNode, Suspense, useMemo } from 'react' import { matchPath, Navigate, Route, Routes, useLocation } from 'react-router' @@ -127,7 +126,7 @@ export const routes: RouteDefinition[] = [ getTitle: () => StaticTitlesAndDescriptions.UniswapTitle, getDescription: () => StaticTitlesAndDescriptions.SwapDescription, getElement: (args) => { - return args.browserRouterEnabled && args.hash ? : + return args.browserRouterEnabled && args.hash ? : }, }), createRouteDefinition({ @@ -217,40 +216,41 @@ export const routes: RouteDefinition[] = [ getDescription: () => i18n.t('title.createGovernanceTo'), getElement: () => , }), - createRouteDefinition({ - path: '/buy', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.SwapTitle, - }), - createRouteDefinition({ - path: '/sell', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.SwapTitle, - }), + // createRouteDefinition({ + // path: '/buy', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.SwapTitle, + // }), + // createRouteDefinition({ + // path: '/sell', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.SwapTitle, + // }), createRouteDefinition({ path: '/send', getElement: () => , getTitle: () => i18n.t('title.sendTokens'), }), - createRouteDefinition({ - path: '/limits', - getElement: () => , - getTitle: () => i18n.t('title.placeLimit'), - }), - createRouteDefinition({ - path: '/limit', - getElement: () => , - getTitle: () => i18n.t('title.placeLimit'), - }), - createRouteDefinition({ - path: '/buy', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.SwapTitle, - }), + // createRouteDefinition({ + // path: '/limits', + // getElement: () => , + // getTitle: () => i18n.t('title.placeLimit'), + // }), + // createRouteDefinition({ + // path: '/limit', + // getElement: () => , + // getTitle: () => i18n.t('title.placeLimit'), + // }), + // createRouteDefinition({ + // path: '/buy', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.SwapTitle, + // }), createRouteDefinition({ path: '/swap', getElement: () => , getTitle: () => StaticTitlesAndDescriptions.SwapTitle, + getDescription: () => StaticTitlesAndDescriptions.SwapDescription, }), // Refreshed pool routes createRouteDefinition({ @@ -284,18 +284,19 @@ export const routes: RouteDefinition[] = [ getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), - createRouteDefinition({ - path: '/migrate/v2/:chainName/:pairAddress', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, - getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, - }), - createRouteDefinition({ - path: '/migrate/v3/:chainName/:tokenId', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.MigrateTitleV3, - getDescription: () => StaticTitlesAndDescriptions.MigrateDescriptionV4, - }), + // 注释掉迁移路由 - 本期不做迁移功能 + // createRouteDefinition({ + // path: '/migrate/v2/:chainName/:pairAddress', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, + // getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, + // }), + // createRouteDefinition({ + // path: '/migrate/v3/:chainName/:tokenId', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.MigrateTitleV3, + // getDescription: () => StaticTitlesAndDescriptions.MigrateDescriptionV4, + // }), // Legacy pool routes createRouteDefinition({ path: '/pool', @@ -303,12 +304,13 @@ export const routes: RouteDefinition[] = [ getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), - createRouteDefinition({ - path: '/pool/v2/find', - getElement: () => , - getTitle: getPositionPageDescription, - getDescription: getPositionPageDescription, - }), + // 注释掉 Pool Finder 路由 - 本期只做基础添加流动性 + // createRouteDefinition({ + // path: '/pool/v2/find', + // getElement: () => , + // getTitle: getPositionPageDescription, + // getDescription: getPositionPageDescription, + // }), createRouteDefinition({ path: '/pool/v2', getElement: () => , @@ -321,12 +323,13 @@ export const routes: RouteDefinition[] = [ getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), - createRouteDefinition({ - path: '/pools/v2/find', - getElement: () => , - getTitle: getPositionPageTitle, - getDescription: getPositionPageDescription, - }), + // 注释掉 Pool Finder 路由 - 本期只做基础添加流动性 + // createRouteDefinition({ + // path: '/pools/v2/find', + // getElement: () => , + // getTitle: getPositionPageTitle, + // getDescription: getPositionPageDescription, + // }), createRouteDefinition({ path: '/pools', getElement: () => , @@ -339,13 +342,14 @@ export const routes: RouteDefinition[] = [ getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), - createRouteDefinition({ - path: '/add/v2', - nestedPaths: [':currencyIdA', ':currencyIdA/:currencyIdB'], - getElement: () => , - getTitle: getAddLiquidityPageTitle, - getDescription: () => StaticTitlesAndDescriptions.AddLiquidityDescription, - }), + // 注释掉 V2 添加流动性路由 - 本期只做 V3 基础添加流动性 + // createRouteDefinition({ + // path: '/add/v2', + // nestedPaths: [':currencyIdA', ':currencyIdA/:currencyIdB'], + // getElement: () => , + // getTitle: getAddLiquidityPageTitle, + // getDescription: () => StaticTitlesAndDescriptions.AddLiquidityDescription, + // }), createRouteDefinition({ path: '/add', nestedPaths: [ @@ -370,18 +374,19 @@ export const routes: RouteDefinition[] = [ getTitle: () => i18n.t('title.removePoolLiquidity'), getDescription: () => i18n.t('title.removev3Liquidity'), }), - createRouteDefinition({ - path: '/migrate/v2', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, - getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, - }), - createRouteDefinition({ - path: '/migrate/v2/:address', - getElement: () => , - getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, - getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, - }), + // 注释掉 V2 迁移路由 - 本期只做 V3 基础添加流动性 + // createRouteDefinition({ + // path: '/migrate/v2', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, + // getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, + // }), + // createRouteDefinition({ + // path: '/migrate/v2/:address', + // getElement: () => , + // getTitle: () => StaticTitlesAndDescriptions.MigrateTitle, + // getDescription: () => StaticTitlesAndDescriptions.MigrateDescription, + // }), createRouteDefinition({ path: EXTENSION_PASSKEY_AUTH_PATH, getElement: () => , diff --git a/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx b/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx index 0484223e100..16032c51fd3 100644 --- a/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx +++ b/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx @@ -1,4 +1,4 @@ -import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { useAppKit } from 'components/Web3Provider/reownConfig' import { useConnectionStatus } from 'features/accounts/store/hooks' import { useBuyFormContext } from 'pages/Swap/Buy/BuyFormContext' import { useTranslation } from 'react-i18next' @@ -14,7 +14,7 @@ interface BuyFormButtonProps { export function BuyFormButton({ forceDisabled }: BuyFormButtonProps) { const isDisconnected = useConnectionStatus('aggregate').isDisconnected - const accountDrawer = useAccountDrawer() + const { open } = useAppKit() const { t } = useTranslation() const isShortMobileDevice = useIsShortMobileDevice() @@ -29,7 +29,7 @@ export function BuyFormButton({ forceDisabled }: BuyFormButtonProps) { if (isDisconnected || isMissingPlatformWallet) { return ( - - - - + <> + + + + + + + + + + + ) } diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants.ts b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants.ts index 7d397ff058a..29547fef9eb 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants.ts +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants.ts @@ -18,6 +18,14 @@ export const CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS: Partial - + {!isWebApp && /* Interface renders its own header with multiple tabs */} - {!hideSettings && } + {/* Original Swap+Settings button moved to swapblock position below */} + {/* {!hideSettings && } */} {!hideContent && ( - + )} @@ -77,18 +83,48 @@ export function SwapFormScreen({ ) } -function SwapFormContent(): JSX.Element { +function SwapFormContent({ + hideSettings, + filteredSettings, + isBridgeTrade, +}: { + hideSettings: boolean + filteredSettings: TransactionSettingConfig[] + isBridgeTrade: boolean +}): JSX.Element { const { trade, isCrossChain } = useSwapFormScreenStore((state) => ({ trade: state.trade, isCrossChain: state.isCrossChain, })) const priceUXEnabled = usePriceUXEnabled() + const { autoSlippageTolerance } = useSlippageSettings() return ( - + {/* Original Swap+Settings button moved here to occupy space within the border */} + {!hideSettings && ( + <> + + {/* Swap text */} + + Swap + + {/* Settings button */} + + + {/* Border below Swap+Settings with 8px left/right extension */} + + + )} + diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenDetails.web.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenDetails.web.tsx index 2b812afd7ea..9f29e18a0f6 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenDetails.web.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenDetails.web.tsx @@ -1,17 +1,17 @@ import { Accordion, Flex } from 'ui/src' import { SwapFormButton } from 'uniswap/src/features/transactions/swap/components/SwapFormButton/SwapFormButton' -import { ExpandableRows } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/ExpandableRows' -import { SwapFormScreenFooter } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/SwapFormScreenFooter' +// import { ExpandableRows } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/ExpandableRows' +// import { SwapFormScreenFooter } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/SwapFormScreenFooter' import { SwapFormWarningModals } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormWarningModals/SwapFormWarningModals' import { useSwapFormScreenStore } from 'uniswap/src/features/transactions/swap/form/stores/swapFormScreenStore/useSwapFormScreenStore' import { SwapFormWarningStoreContextProvider } from 'uniswap/src/features/transactions/swap/form/stores/swapFormWarningStore/SwapFormWarningStoreContextProvider' -import { usePriceUXEnabled } from 'uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled' +// import { usePriceUXEnabled } from 'uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled' export function SwapFormScreenDetails(): JSX.Element { - const isPriceUXEnabled = usePriceUXEnabled() - const { tokenColor, showFooter } = useSwapFormScreenStore((state) => ({ + // const isPriceUXEnabled = usePriceUXEnabled() + const { tokenColor } = useSwapFormScreenStore((state) => ({ tokenColor: state.tokenColor, - showFooter: state.showFooter, + // showFooter: state.showFooter, })) return ( @@ -24,15 +24,17 @@ export function SwapFormScreenDetails(): JSX.Element { } `} - + - + {/* SwapFormScreenFooter removed - footer content (gas info, warnings) are hidden to prevent space changes */} + {/* */} - {showFooter && !isPriceUXEnabled ? : null} + {/* ExpandableRows removed - fee, network cost, etc. details are hidden before modal opens */} + {/* {showFooter && !isPriceUXEnabled ? : null} */} ) diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/GasAndWarningRows.web.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/GasAndWarningRows.web.tsx index e75566903af..bdd03334e19 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/GasAndWarningRows.web.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/GasAndWarningRows.web.tsx @@ -3,7 +3,7 @@ import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types' import { isSVMChain } from 'uniswap/src/features/platforms/utils/chains' import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning' -import { TradeInfoRow } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow' +// import { TradeInfoRow } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow' import { useDebouncedGasInfo } from 'uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/useDebouncedGasInfo' import { useParsedSwapWarnings } from 'uniswap/src/features/transactions/swap/hooks/useSwapWarnings/useSwapWarnings' import { useSwapFormStoreDerivedSwapInfo } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/useSwapFormStore' @@ -49,11 +49,12 @@ export function GasAndWarningRows(): JSX.Element { /> )} - {!insufficientGasFundsWarning && ( + {/* TradeInfoRow removed - rate display and accordion trigger are hidden */} + {/* {!insufficientGasFundsWarning && ( - )} + )} */} diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow.tsx index 2938946f4f3..31246eda617 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/TradeInfoRow.tsx @@ -43,55 +43,7 @@ export function TradeInfoRow({ gasInfo, warning }: { gasInfo: GasInfo; warning?: const outputChainId = currencies.output?.currency.chainId const showCanonicalBridge = isWebApp && warning?.type === WarningLabel.NoQuotesFound && inputChainId !== outputChainId - return ( - - - {debouncedTrade && !warning && ( - - )} - - {warning && ( - - - - - {warning.title} - - - - )} - - - {showCanonicalBridge ? ( - - ) : debouncedTrade ? ( - - {({ open }: { open: boolean }) => ( - - - )} - - ) : ( - - )} - - ) + // TradeInfoRow removed - rate display and accordion trigger are hidden + // Only show gas info without rate and accordion + return } diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwitchCurrenciesButton.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwitchCurrenciesButton.tsx index a92aa260da7..418be436225 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwitchCurrenciesButton.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwitchCurrenciesButton.tsx @@ -16,8 +16,8 @@ const SWAP_DIRECTION_BUTTON_SIZE = { small: spacing.spacing8, }, borderWidth: { - regular: spacing.spacing4, - small: spacing.spacing1, + regular: 1, + small: 1, }, } as const diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/hooks/useCurrencyInputFocusedStyle.ts b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/hooks/useCurrencyInputFocusedStyle.ts index b23b700f4e8..3dc3f22b62f 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/hooks/useCurrencyInputFocusedStyle.ts +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/hooks/useCurrencyInputFocusedStyle.ts @@ -3,10 +3,10 @@ import type { FlexProps } from 'ui/src' function getCurrencyInputFocusedStyle(isFocused: boolean): FlexProps { return { borderColor: isFocused ? '$surface3' : '$transparent', - backgroundColor: isFocused ? '$surface1' : '$surface2', + backgroundColor: '$surface1', // Always use surface1 for consistent background hoverStyle: { borderColor: isFocused ? '$surface3Hovered' : '$transparent', - backgroundColor: isFocused ? '$surface1' : '$surface2Hovered', + backgroundColor: '$surface1', // Always use surface1 for consistent background }, } } diff --git a/packages/uniswap/src/features/transactions/swap/plan/utils.ts b/packages/uniswap/src/features/transactions/swap/plan/utils.ts index 66c5de47c01..1446a107999 100644 --- a/packages/uniswap/src/features/transactions/swap/plan/utils.ts +++ b/packages/uniswap/src/features/transactions/swap/plan/utils.ts @@ -21,13 +21,18 @@ export async function handleSwitchChains(params: { const swapChainId = swapTxContext.trade.inputAmount.currency.chainId - if (isJupiter(swapTxContext) || swapChainId === startChainId) { + // If startChainId is undefined, we assume we're already on the correct chain (or don't need to switch) + // This is common when the account hasn't been initialized yet or we're testing a single chain + if (isJupiter(swapTxContext) || swapChainId === startChainId || startChainId === undefined) { return { chainSwitchFailed: false } } + try { const chainSwitched = await selectChain(swapChainId) - return { chainSwitchFailed: !chainSwitched } + } catch (error) { + return { chainSwitchFailed: true } + } } export function stepHasFinalized(step: TradingApi.PlanStep): boolean { diff --git a/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx b/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx index 35cec8fe86a..85222e0600e 100644 --- a/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx +++ b/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx @@ -86,6 +86,12 @@ export function useCreateSwapReviewCallbacks(ctx: { const onFailure = useCallback( (error?: Error, onPressRetry?: () => void) => { + if (process.env.NODE_ENV === 'development') { + console.log('[Swap Result] Swap failed:', { + error: error?.message || 'Unknown error', + errorDetails: error, + }) + } resetCurrentStep() // Create a new txId for the next transaction, as the existing one may be used in state to track the failed submission. @@ -99,6 +105,13 @@ export function useCreateSwapReviewCallbacks(ctx: { ) const onSuccess = useCallback(() => { + if (process.env.NODE_ENV === 'development') { + console.log('[Swap Result] Swap succeeded!', { + chainId: derivedSwapInfo.chainId, + inputCurrency: derivedSwapInfo.currencies.input?.currency.symbol, + outputCurrency: derivedSwapInfo.currencies.output?.currency.symbol, + }) + } // For Unichain networks, trigger confirmation and branch to stall+fetch logic (ie handle in component) if (isFlashblocksEnabled && shouldShowConfirmedState) { resetCurrentStep() diff --git a/packages/uniswap/src/features/transactions/swap/review/hooks/useTokenApprovalInfo.ts b/packages/uniswap/src/features/transactions/swap/review/hooks/useTokenApprovalInfo.ts index 973bf95a107..9dd43a499b3 100644 --- a/packages/uniswap/src/features/transactions/swap/review/hooks/useTokenApprovalInfo.ts +++ b/packages/uniswap/src/features/transactions/swap/review/hooks/useTokenApprovalInfo.ts @@ -37,6 +37,12 @@ function useApprovalWillBeBatchedWithSwap(chainId: UniverseChainId, routing: Tra const swapDelegationInfo = useUniswapContextSelector((ctx) => ctx.getSwapDelegationInfo?.(chainId)) const isBatchableFlow = Boolean(routing && !isUniswapX({ routing })) + const isHashKeyChain = chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet + + if (isHashKeyChain) { + // HashKey swaps use direct router calldata and are not atomic-batched with approvals. + return false + } return Boolean((canBatchTransactions || swapDelegationInfo?.delegationAddress) && isBatchableFlow) } @@ -47,6 +53,7 @@ export function useTokenApprovalInfo(params: TokenApprovalInfoParams): ApprovalT const isWrap = wrapType !== WrapType.NotApplicable /** Approval is included elsewhere for Chained Actions so it can be skipped */ const isChained = routing === TradingApi.Routing.CHAINED + const isHashKeyChain = chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet const address = account?.address const inputWillBeWrapped = routing && isUniswapX({ routing }) @@ -125,6 +132,17 @@ export function useTokenApprovalInfo(params: TokenApprovalInfoParams): ApprovalT } if (data && !error) { + if (process.env.NODE_ENV === 'development' && isHashKeyChain) { + console.log('[approval debug][HashKey] checkApproval result', { + approvalRequestArgs, + data, + hasApproval: !!data.approval, + hasCancel: !!data.cancel, + gasFee: data.gasFee, + cancelGasFee: data.cancelGasFee, + }) + } + // API returns null if no approval is required // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/classic/classicSwapTxAndGasInfoService.ts b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/classic/classicSwapTxAndGasInfoService.ts index ade68068025..8a81e073ca9 100644 --- a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/classic/classicSwapTxAndGasInfoService.ts +++ b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/classic/classicSwapTxAndGasInfoService.ts @@ -19,7 +19,33 @@ export function createClassicSwapTxAndGasInfoService(ctx: { const service: SwapTxAndGasInfoService = { async getSwapTxAndGasInfo(params) { + if (process.env.NODE_ENV === 'development') { + console.log('[execute] classicSwapTxAndGasInfoService.getSwapTxAndGasInfo called:', { + hasTrade: !!params.trade, + routing: params.trade?.routing, + hasApprovalTxInfo: !!params.approvalTxInfo, + hasDerivedSwapInfo: !!params.derivedSwapInfo, + }) + } + const swapTxInfo = await getEVMSwapTransactionRequestInfo(params) + + if (process.env.NODE_ENV === 'development') { + console.log('[execute] classicSwapTxAndGasInfoService - Got swapTxInfo:', { + hasSwapTxInfo: !!swapTxInfo, + hasSwapRequestArgs: !!swapTxInfo.swapRequestArgs, + swapRequestArgs: swapTxInfo.swapRequestArgs ? { + deadline: swapTxInfo.swapRequestArgs.deadline, + deadlineDate: swapTxInfo.swapRequestArgs.deadline ? new Date(swapTxInfo.swapRequestArgs.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapTxInfo.swapRequestArgs.quote, + simulateTransaction: swapTxInfo.swapRequestArgs.simulateTransaction, + allKeys: Object.keys(swapTxInfo.swapRequestArgs), + } : 'swapRequestArgs is undefined', + hasTxRequests: !!swapTxInfo.txRequests, + txRequestCount: swapTxInfo.txRequests?.length || 0, + }) + } + const permitTxInfo = getPermitTxInfo(params.trade) return getClassicSwapTxAndGasInfo({ ...params, swapTxInfo, permitTxInfo }) diff --git a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/evmSwapInstructionsService.ts b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/evmSwapInstructionsService.ts index 3c896cb72e5..e8b1c4b5bf4 100644 --- a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/evmSwapInstructionsService.ts +++ b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/evmSwapInstructionsService.ts @@ -24,7 +24,7 @@ import { ApprovalAction } from 'uniswap/src/features/transactions/swap/types/tra import { tradingApiToUniverseChainId } from 'uniswap/src/features/transactions/swap/utils/tradingApi' type SwapInstructions = - | { response: SwapData; unsignedPermit: null; swapRequestParams: null } + | { response: SwapData; unsignedPermit: null; swapRequestParams: TradingApi.CreateSwapRequest | null } | { response: null; unsignedPermit: TradingApi.Permit; swapRequestParams: TradingApi.CreateSwapRequest } /** A service utility capable of fetching swap instructions or returning unsigned permit data when instructions cannot yet be fetched. */ @@ -69,12 +69,22 @@ function createLegacyEVMSwapInstructionsService( alreadyApproved, }) + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createLegacyEVMSwapInstructionsService - Prepared swapRequestParams:', { + deadline: swapRequestParams.deadline, + deadlineDate: swapRequestParams.deadline ? new Date(swapRequestParams.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapRequestParams.quote, + simulateTransaction: swapRequestParams.simulateTransaction, + }) + } + if (signatureMissing) { return { response: null, unsignedPermit: permitData, swapRequestParams } } const response = await swapRepository.fetchSwapData(swapRequestParams) - return { response, unsignedPermit: null, swapRequestParams: null } + // Keep swapRequestParams even when we have a response, so deadline is preserved in swapTxContext + return { response, unsignedPermit: null, swapRequestParams } }, } @@ -100,8 +110,18 @@ function createBatchedEVMSwapInstructionsService( overrideSimulation: true, // always simulate for batched transactions }) + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createBatchedEVMSwapInstructionsService - Prepared swapRequestParams:', { + deadline: swapRequestParams.deadline, + deadlineDate: swapRequestParams.deadline ? new Date(swapRequestParams.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapRequestParams.quote, + simulateTransaction: swapRequestParams.simulateTransaction, + }) + } + const response = await swapRepository.fetchSwapData(swapRequestParams) - return { response, unsignedPermit: null, swapRequestParams: null } + // Keep swapRequestParams even when we have a response, so deadline is preserved in swapTxContext + return { response, unsignedPermit: null, swapRequestParams } }, } diff --git a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/utils.ts b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/utils.ts index 3baef326bbf..218bb9ee597 100644 --- a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/utils.ts +++ b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/utils.ts @@ -4,6 +4,7 @@ import type { ApprovalTxInfo } from 'uniswap/src/features/transactions/swap/revi import type { EVMSwapInstructionsService } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/evm/evmSwapInstructionsService' import type { TransactionRequestInfo } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils' import { + createPrepareSwapRequestParams, createProcessSwapResponse, getSwapInputExceedsBalance, } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils' @@ -31,6 +32,7 @@ export function createGetEVMSwapTransactionRequestInfo(ctx: { const { gasStrategy, transactionSettings, instructionService } = ctx const processSwapResponse = createProcessSwapResponse({ gasStrategy }) + const prepareSwapRequestParams = createPrepareSwapRequestParams({ gasStrategy }) const getEVMSwapTransactionRequestInfo: GetEVMSwapTransactionRequestInfoFn = async ({ trade, @@ -46,23 +48,102 @@ export function createGetEVMSwapTransactionRequestInfo(ctx: { const approvalUnknown = approvalAction === ApprovalAction.Unknown const skip = getSwapInputExceedsBalance({ derivedSwapInfo }) || approvalUnknown + + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createGetEVMSwapTransactionRequestInfo - Before getSwapInstructions:', { + skip, + approvalAction, + approvalUnknown, + inputExceedsBalance: getSwapInputExceedsBalance({ derivedSwapInfo }), + hasInstructionService: !!instructionService, + }) + } + + // Always prepare swapRequestParams, even if skip is true, so deadline is preserved + const alreadyApproved = approvalAction === ApprovalAction.None && !swapQuoteResponse.permitTransaction + const swapRequestParams = prepareSwapRequestParams({ + swapQuoteResponse, + signature: undefined, + transactionSettings, + alreadyApproved, + }) + + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createGetEVMSwapTransactionRequestInfo - Prepared swapRequestParams:', { + deadline: swapRequestParams.deadline, + deadlineDate: swapRequestParams.deadline ? new Date(swapRequestParams.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapRequestParams.quote, + simulateTransaction: swapRequestParams.simulateTransaction, + }) + } + const { data, error } = await tryCatch( skip ? Promise.resolve(undefined) : instructionService.getSwapInstructions({ swapQuoteResponse, transactionSettings, approvalAction }), ) + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createGetEVMSwapTransactionRequestInfo - After getSwapInstructions:', { + hasData: !!data, + data: data ? { + hasResponse: !!data.response, + hasUnsignedPermit: !!data.unsignedPermit, + hasSwapRequestParams: !!data.swapRequestParams, + swapRequestParams: data.swapRequestParams ? { + deadline: data.swapRequestParams.deadline, + deadlineDate: data.swapRequestParams.deadline ? new Date(data.swapRequestParams.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!data.swapRequestParams.quote, + simulateTransaction: data.swapRequestParams.simulateTransaction, + } : 'swapRequestParams is undefined', + } : 'data is undefined', + hasError: !!error, + error: error?.message, + }) + } + const isRevokeNeeded = tokenApprovalInfo.action === ApprovalAction.RevokeAndPermit2Approve + + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createGetEVMSwapTransactionRequestInfo - Before processSwapResponse:', { + hasResponse: !!data?.response, + hasSwapRequestParams: !!data?.swapRequestParams, + swapRequestParams: data?.swapRequestParams ? { + deadline: data.swapRequestParams.deadline, + deadlineDate: data.swapRequestParams.deadline ? new Date(data.swapRequestParams.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!data.swapRequestParams.quote, + simulateTransaction: data.swapRequestParams.simulateTransaction, + } : 'swapRequestParams is undefined', + hasUnsignedPermit: !!data?.unsignedPermit, + }) + } + + // Use swapRequestParams from data if available, otherwise use the one we prepared + const finalSwapRequestParams = data?.swapRequestParams ?? swapRequestParams + const swapTxInfo = processSwapResponse({ response: data?.response ?? undefined, error, permitData: data?.unsignedPermit, swapQuote, + trade, isSwapLoading: false, isRevokeNeeded, - swapRequestParams: data?.swapRequestParams ?? undefined, + swapRequestParams: finalSwapRequestParams, }) + if (process.env.NODE_ENV === 'development') { + console.log('[execute] createGetEVMSwapTransactionRequestInfo - After processSwapResponse:', { + hasSwapRequestArgs: !!swapTxInfo.swapRequestArgs, + swapRequestArgs: swapTxInfo.swapRequestArgs ? { + deadline: swapTxInfo.swapRequestArgs.deadline, + deadlineDate: swapTxInfo.swapRequestArgs.deadline ? new Date(swapTxInfo.swapRequestArgs.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapTxInfo.swapRequestArgs.quote, + simulateTransaction: swapTxInfo.swapRequestArgs.simulateTransaction, + } : 'swapRequestArgs is undefined', + }) + } + return swapTxInfo } diff --git a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils.ts b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils.ts index 01a1403a058..4f19c36cce7 100644 --- a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils.ts +++ b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/utils.ts @@ -16,6 +16,7 @@ import { getTradeSettingsDeadline } from 'uniswap/src/data/apiClients/tradingApi import { getChainLabel } from 'uniswap/src/features/chains/utils' import { convertGasFeeToDisplayValue, useActiveGasStrategy } from 'uniswap/src/features/gas/hooks' import type { GasFeeResult } from 'uniswap/src/features/gas/types' +import { SwapRouter as V3SwapRouter } from '@uniswap/router-sdk' import { SwapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import type { TransactionSettings } from 'uniswap/src/features/transactions/components/settings/types' @@ -46,6 +47,7 @@ import type { import { ApprovalAction } from 'uniswap/src/features/transactions/swap/types/trade' import { mergeGasFeeResults } from 'uniswap/src/features/transactions/swap/utils/gas' import { isClassic } from 'uniswap/src/features/transactions/swap/utils/routing' +import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format' import { validatePermit, validateTransactionRequest, @@ -54,6 +56,7 @@ import { import { SWAP_GAS_URGENCY_OVERRIDE } from 'uniswap/src/features/transactions/swap/utils/tradingApi' import type { ValidatedTransactionRequest } from 'uniswap/src/features/transactions/types/transactionRequests' import { CurrencyField } from 'uniswap/src/types/currency' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { logger } from 'utilities/src/logger/logger' import { isExtensionApp, isMobileApp, isWebApp } from 'utilities/src/platform' import type { ITraceContext } from 'utilities/src/telemetry/trace/TraceContext' @@ -99,12 +102,14 @@ export function createPrepareSwapRequestParams({ gasStrategy }: { gasStrategy: G transactionSettings, alreadyApproved, overrideSimulation, + blockTimestamp, }: { swapQuoteResponse: ClassicQuoteResponse | BridgeQuoteResponse | WrapQuoteResponse | UnwrapQuoteResponse signature: string | undefined transactionSettings: TransactionSettings alreadyApproved: boolean overrideSimulation?: boolean + blockTimestamp?: bigint | number }): TradingApi.CreateSwapRequest { const isBridgeTrade = swapQuoteResponse.routing === TradingApi.Routing.BRIDGE const permitData = swapQuoteResponse.permitData @@ -118,7 +123,7 @@ export function createPrepareSwapRequestParams({ gasStrategy }: { gasStrategy: G */ const shouldSimulateTxn = overrideSimulation ?? (isBridgeTrade ? false : alreadyApproved) - const deadline = getTradeSettingsDeadline(transactionSettings.customDeadline) + const deadline = getTradeSettingsDeadline(transactionSettings.customDeadline, blockTimestamp) return { quote: swapQuoteResponse.quote, @@ -191,51 +196,355 @@ export function getSimulationError({ return null } +/** + * Calculate gasFee from quote response + * If gasFee is directly available, use it. Otherwise, calculate from gasPriceWei * gasUseEstimate + */ +function getGasFeeFromQuote( + swapQuote: TradingApi.ClassicQuote | TradingApi.BridgeQuote | undefined, + gasStrategy: GasStrategy, +): { value: string | undefined; displayValue: string | undefined } { + if (!swapQuote) { + return { value: undefined, displayValue: undefined } + } + + // Try to use gasFee directly if available + if ('gasFee' in swapQuote && swapQuote.gasFee) { + return { + value: swapQuote.gasFee, + displayValue: convertGasFeeToDisplayValue(swapQuote.gasFee, gasStrategy), + } + } + + // Calculate from gasPriceWei * gasUseEstimate if available + if ('gasPriceWei' in swapQuote && 'gasUseEstimate' in swapQuote) { + const gasPriceWei = swapQuote.gasPriceWei + const gasUseEstimate = swapQuote.gasUseEstimate + + if (gasPriceWei && gasUseEstimate && typeof gasPriceWei === 'string' && typeof gasUseEstimate === 'string') { + try { + // Calculate: gasFee = gasPriceWei * gasUseEstimate + const gasFeeValue = (BigInt(gasPriceWei) * BigInt(gasUseEstimate)).toString() + return { + value: gasFeeValue, + displayValue: convertGasFeeToDisplayValue(gasFeeValue, gasStrategy), + } + } catch (error) { + // If calculation fails, return undefined + return { value: undefined, displayValue: undefined } + } + } + } + + return { value: undefined, displayValue: undefined } +} + +/** + * Build transaction request from quote methodParameters when swap API response is not available + */ +function getRouterAddressForChain(chainId: number): { routerAddress?: string; routerSource?: string } { + // Get router address from chainId + // For HashKey chains, use the first address from CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS if available + // Otherwise, try to get from UNIVERSAL_ROUTER_ADDRESS function + let routerAddress: string | undefined + let routerSource: string | undefined + + // Try to get from CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS first (for HashKey chains) + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS } = require('uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants') + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { UniverseChainId: UniverseChainIdEnum } = require('uniswap/src/features/chains/types') + + // Try both numeric key and enum key lookup + const chainIdAsEnum = chainId as UniverseChainId + let addresses = CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS[chainIdAsEnum] + + // If not found, try looking up by numeric value (for HashKey chains: 133, 177) + if (!addresses && (chainId === 133 || chainId === 177)) { + // Try direct numeric lookup + addresses = CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS[chainId as keyof typeof CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS] + // Also try enum lookup + if (!addresses) { + const hashKeyEnumValue = chainId === 133 ? UniverseChainIdEnum.HashKeyTestnet : UniverseChainIdEnum.HashKey + addresses = CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS[hashKeyEnumValue] + } + } + if (addresses && addresses.length > 0) { + routerAddress = addresses[0] + routerSource = 'CHAIN_TO_UNIVERSAL_ROUTER_ADDRESS' + } + } catch (error) { + // Ignore error + } + + // Fallback to UNIVERSAL_ROUTER_ADDRESS if available + if (!routerAddress) { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { UNIVERSAL_ROUTER_ADDRESS, UniversalRouterVersion } = require('@hkdex-tmp/universal_router_sdk') + routerAddress = UNIVERSAL_ROUTER_ADDRESS(UniversalRouterVersion.V1_2, chainId) + routerSource = 'UNIVERSAL_ROUTER_ADDRESS' + } catch (error) { + // Ignore error + } + } + + return { routerAddress, routerSource } +} + +function getGasLimitWithBuffer( + gasUseEstimate?: string | number, + { multiplier = 1.2 }: { multiplier?: number } = {}, +): string | undefined { + if (!gasUseEstimate) { + return undefined + } + try { + const gasUseEstimateStr = String(gasUseEstimate) + const factor = Math.floor(multiplier * 100) + const gasLimitWithBuffer = (BigInt(gasUseEstimateStr) * BigInt(factor)) / BigInt(100) + return gasLimitWithBuffer.toString() + } catch { + return undefined + } +} + +function buildTxRequestFromQuote( + swapQuote: TradingApi.ClassicQuote | TradingApi.BridgeQuote | undefined, + chainId: number, +): providers.TransactionRequest[] | undefined { + // Type assertion: methodParameters exists in ClassicQuote but may not be in type definition + const quoteWithMethodParams = swapQuote as (TradingApi.ClassicQuote | TradingApi.BridgeQuote) & { + methodParameters?: { calldata: string; value: string } + } + + if (!quoteWithMethodParams?.methodParameters) { + return undefined + } + + const { calldata, value } = quoteWithMethodParams.methodParameters + + if (!calldata) { + return undefined + } + + const { routerAddress } = getRouterAddressForChain(chainId) + + if (!routerAddress) { + // For HashKey chains (133, 177), they may not use Universal Router + if (chainId === 133 || chainId === 177) { + logger.error('HashKey chain does not have Universal Router configured', { + tags: { file: 'utils.ts', function: 'buildTxRequestFromQuote' }, + extra: { chainId }, + }) + return undefined + } + logger.error('Could not determine Universal Router address', { + tags: { file: 'utils.ts', function: 'buildTxRequestFromQuote' }, + extra: { chainId }, + }) + return undefined + } + + // Build transaction request from quote methodParameters + // Note: chainId is required for validateTransactionRequest to pass validation + const txRequest: providers.TransactionRequest = { + to: routerAddress, + data: calldata, + value: value && value !== '0x00' ? value : undefined, + chainId, // Required for validation + } + + // Add gas limit from quote if available + // This is critical - quote's gasUseEstimate is more accurate than provider's estimate + // Add 20% buffer to gas limit for safety (to account for price changes, etc.) + const quoteWithGasEstimate = quoteWithMethodParams as (TradingApi.ClassicQuote | TradingApi.BridgeQuote) & { + gasUseEstimate?: string | number + } + const isHashKey = chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet + const gasLimitBuffered = getGasLimitWithBuffer(quoteWithGasEstimate.gasUseEstimate, { + multiplier: isHashKey ? 1.6 : 1.2, + }) + if (gasLimitBuffered) { + txRequest.gasLimit = gasLimitBuffered + } + + return [txRequest] +} + +function buildTxRequestFromTrade( + trade: ClassicTrade, + chainId: number, + deadline?: number, +): providers.TransactionRequest[] | undefined { + const { routerAddress } = getRouterAddressForChain(chainId) + if (!routerAddress) { + return undefined + } + + const slippageTolerance = slippageToleranceToPercent(trade.slippageTolerance) + const deadlineOrPreviousBlockhash = String(deadline ?? trade.deadline ?? 0) + const feeOptions = + trade.swapFee?.recipient && trade.swapFee.feeField === CurrencyField.OUTPUT + ? { fee: trade.swapFee.percent, recipient: trade.swapFee.recipient } + : undefined + + const gasLimitBuffered = getGasLimitWithBuffer((trade as any)?.quote?.quote?.gasUseEstimate, { + multiplier: chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet ? 1.6 : 1.2, + }) + + const { calldata, value } = V3SwapRouter.swapCallParameters(trade, { + slippageTolerance, + deadlineOrPreviousBlockhash, + fee: feeOptions, + }) + + const txRequest: providers.TransactionRequest = { + to: routerAddress, + data: calldata, + value: value && value !== '0x00' ? value : undefined, + chainId, + ...(gasLimitBuffered ? { gasLimit: gasLimitBuffered } : {}), + } + + return [txRequest] +} + export function createProcessSwapResponse({ gasStrategy }: { gasStrategy: GasStrategy }) { return function processSwapResponse({ response, error, swapQuote, + trade, isSwapLoading, permitData, swapRequestParams, isRevokeNeeded, permitsDontNeedSignature, + chainId, }: { response: SwapData | undefined error: Error | null swapQuote: TradingApi.ClassicQuote | TradingApi.BridgeQuote | undefined + trade?: ClassicTrade isSwapLoading: boolean permitData: TradingApi.NullablePermit | undefined swapRequestParams: TradingApi.CreateSwapRequest | undefined isRevokeNeeded: boolean permitsDontNeedSignature?: boolean + chainId?: number }): TransactionRequestInfo { - // We use the gasFee estimate from quote, as its more accurate - const swapGasFee = { - value: swapQuote?.gasFee, - displayValue: convertGasFeeToDisplayValue(swapQuote?.gasFee, gasStrategy), + // Try to get chainId from swapQuote if not provided + let finalChainId = chainId + if (!finalChainId && swapQuote) { + // Try to get chainId from quote's token chain IDs + const quoteWithChainIds = swapQuote as (TradingApi.ClassicQuote | TradingApi.BridgeQuote) & { + tokenInChainId?: number | string + tokenOutChainId?: number | string + } + const tokenInChainId = 'tokenInChainId' in quoteWithChainIds ? quoteWithChainIds.tokenInChainId : undefined + const tokenOutChainId = 'tokenOutChainId' in quoteWithChainIds ? quoteWithChainIds.tokenOutChainId : undefined + + if (tokenInChainId && typeof tokenInChainId === 'number') { + finalChainId = tokenInChainId + } else if (tokenOutChainId && typeof tokenOutChainId === 'number') { + finalChainId = tokenOutChainId } + } + + // Final fallback: use HashKeyTestnet (133) since we only support HSK chains + if (!finalChainId) { + finalChainId = 133 // UniverseChainId.HashKeyTestnet + } + + // We use the gasFee estimate from quote, as its more accurate + // Calculate gasFee from quote response (either directly from gasFee or from gasPriceWei * gasUseEstimate) + const swapGasFee = getGasFeeFromQuote(swapQuote, gasStrategy) // This is a case where simulation fails on backend, meaning txn is expected to fail const simulationError = getSimulationError({ swapQuote, isRevokeNeeded }) const gasEstimateError = simulationError ?? error + // Only set error if there's actually an error (not just an empty object) + // Check if error is a valid Error instance or has meaningful content + let finalError: Error | null = null + if (gasEstimateError) { + if (gasEstimateError instanceof Error) { + finalError = gasEstimateError + } else if (typeof gasEstimateError === 'object' && gasEstimateError !== null) { + // Check if it's not just an empty object + const errorKeys = Object.keys(gasEstimateError) + if (errorKeys.length > 0) { + // Convert to Error if it has content + finalError = new Error(JSON.stringify(gasEstimateError)) + } + } + } + const gasFeeResult = { value: swapGasFee.value, displayValue: swapGasFee.displayValue, isLoading: isSwapLoading, - error: gasEstimateError, + error: finalError, } const gasEstimate: SwapGasFeeEstimation = { swapEstimate: response?.gasEstimate, } + const isHashKeyChain = finalChainId === UniverseChainId.HashKey || finalChainId === UniverseChainId.HashKeyTestnet + + // Use swap API transactions if available, otherwise build from quote methodParameters + let txRequests: providers.TransactionRequest[] | undefined + + if (isHashKeyChain && trade) { + txRequests = buildTxRequestFromTrade(trade, finalChainId, swapRequestParams?.deadline) + if (!txRequests) { + logger.error('HashKey chain failed to build SwapRouter02 txRequest from trade', { + tags: { file: 'utils.ts', function: 'processSwapResponse' }, + extra: { chainId: finalChainId }, + }) + } + } else if (response?.transactions) { + txRequests = response.transactions + } else if (finalChainId && swapQuote) { + txRequests = buildTxRequestFromQuote(swapQuote, finalChainId) + if (!txRequests) { + if (process.env.NODE_ENV === 'development') { + console.error('[Swap] Error: Failed to build transaction requests from quote', { + chainId: finalChainId, + originalChainId: chainId, + hasSwapQuote: !!swapQuote, + hasMethodParameters: !!(swapQuote as any)?.methodParameters, + methodParameters: (() => { + const methodParams = (swapQuote as any)?.methodParameters + if (methodParams) { + return { + hasCalldata: !!methodParams.calldata, + hasValue: !!methodParams.value, + calldata: methodParams.calldata?.substring(0, 20) + '...', + } + } + return undefined + })(), + // This error means swap cannot proceed - txRequests is required + willCauseValidationFailure: true, + }) + } + } + } else { + // This should not happen if finalChainId fallback is working correctly + // But if it does, we should still try to build with finalChainId + if (finalChainId && swapQuote) { + txRequests = buildTxRequestFromQuote(swapQuote, finalChainId) + } + } + return { gasFeeResult, - txRequests: response?.transactions, + txRequests, permitData: permitsDontNeedSignature ? undefined : permitData, gasEstimate, includesDelegation: response?.includesDelegation, @@ -332,6 +641,7 @@ export function createGasFields({ permitTxInfo.gasFeeResult, ) + const gasFeeEstimation: SwapGasFeeEstimation = { ...swapTxInfo.gasEstimate, approvalEstimate: approvalGasFeeResult.gasEstimate, diff --git a/packages/uniswap/src/features/transactions/swap/services/executeSwapService.ts b/packages/uniswap/src/features/transactions/swap/services/executeSwapService.ts index f817d1aae13..1a7c358afb8 100644 --- a/packages/uniswap/src/features/transactions/swap/services/executeSwapService.ts +++ b/packages/uniswap/src/features/transactions/swap/services/executeSwapService.ts @@ -49,30 +49,67 @@ export function createExecuteSwapService(ctx: { const swapTxContext = ctx.getSwapTxContext?.() const account = ctx.getAccount?.() + if (process.env.NODE_ENV === 'development') { + console.log('[execute] executeSwapService executeSwap called:', { + account: account ? { + address: account.address, + chainId: account.chainId, + accountType: account.accountType, + } : undefined, + swapTxContext: swapTxContext ? { + routing: swapTxContext.routing, + hasTxRequests: !!swapTxContext.txRequests, + txRequestCount: swapTxContext.txRequests?.length || 0, + hasTrade: !!swapTxContext.trade, + includesDelegation: swapTxContext.includesDelegation, + hasSwapRequestArgs: 'swapRequestArgs' in swapTxContext, + swapRequestArgs: swapTxContext.swapRequestArgs ? { + deadline: swapTxContext.swapRequestArgs.deadline, + deadlineDate: swapTxContext.swapRequestArgs.deadline ? new Date(swapTxContext.swapRequestArgs.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!swapTxContext.swapRequestArgs.quote, + simulateTransaction: swapTxContext.swapRequestArgs.simulateTransaction, + allKeys: Object.keys(swapTxContext.swapRequestArgs), + } : 'swapRequestArgs is undefined', + swapTxContextKeys: Object.keys(swapTxContext), + } : undefined, + currencyAmounts: currencyAmounts ? { + input: currencyAmounts.input?.toExact(), + output: currencyAmounts.output?.toExact(), + } : undefined, + currencyAmountsUSDValue: currencyAmountsUSDValue, + txId, + wrapType, + customSlippageTolerance, + }) + } + if ( !account || !swapTxContext || !isSignerMnemonicAccountDetails(account) || !isValidSwapTxContext(swapTxContext) ) { - ctx.onFailure( - new Error( - !account + const errorMessage = !account ? 'No account available' : !swapTxContext ? 'Missing swap transaction context' : !isSignerMnemonicAccountDetails(account) ? 'Invalid account type - must be signer mnemonic account' - : 'Invalid swap transaction context', - ), - ) + : 'Invalid swap transaction context' + + if (process.env.NODE_ENV === 'development') { + console.error('[Swap] Error: Validation failed in executeSwapService', { + error: errorMessage, + }) + } + + ctx.onFailure(new Error(errorMessage)) return } const { presetPercentage, preselectAsset } = ctx.getPresetInfo() - ctx - .onExecuteSwap({ + const executeParams = { account, swapTxContext, currencyInAmountUSD: currencyAmountsUSDValue[CurrencyField.INPUT] ?? undefined, @@ -89,8 +126,50 @@ export function createExecuteSwapService(ctx: { isFiatInputMode: ctx.getIsFiatMode?.(), wrapType, inputCurrencyAmount: currencyAmounts.input ?? undefined, + } + + if (process.env.NODE_ENV === 'development') { + console.log('[execute] executeSwapService calling onExecuteSwap with params:', executeParams) + console.log('[execute] executeSwapService - Detailed swapTxContext:', { + routing: executeParams.swapTxContext?.routing, + hasTxRequests: !!executeParams.swapTxContext?.txRequests, + txRequestCount: executeParams.swapTxContext?.txRequests?.length || 0, + txRequests: executeParams.swapTxContext?.txRequests?.map((tx, idx) => ({ + index: idx, + to: tx.to, + data: tx.data?.substring(0, 20) + '...', + value: tx.value?.toString(), + gasLimit: tx.gasLimit?.toString(), + gasPrice: tx.gasPrice?.toString(), + chainId: tx.chainId, + })), + hasTrade: !!executeParams.swapTxContext?.trade, + trade: executeParams.swapTxContext?.trade ? { + routing: executeParams.swapTxContext.trade.routing, + inputAmount: executeParams.swapTxContext.trade.inputAmount?.toExact(), + outputAmount: executeParams.swapTxContext.trade.outputAmount?.toExact(), + deadline: executeParams.swapTxContext.trade.deadline, + deadlineDate: executeParams.swapTxContext.trade.deadline ? new Date(executeParams.swapTxContext.trade.deadline * 1000).toLocaleString('zh-CN') : undefined, + } : undefined, + includesDelegation: executeParams.swapTxContext?.includesDelegation, + hasSwapRequestArgs: 'swapRequestArgs' in (executeParams.swapTxContext || {}), + swapRequestArgs: executeParams.swapTxContext?.swapRequestArgs ? { + deadline: executeParams.swapTxContext.swapRequestArgs.deadline, + deadlineDate: executeParams.swapTxContext.swapRequestArgs.deadline ? new Date(executeParams.swapTxContext.swapRequestArgs.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!executeParams.swapTxContext.swapRequestArgs.quote, + simulateTransaction: executeParams.swapTxContext.swapRequestArgs.simulateTransaction, + allKeys: Object.keys(executeParams.swapTxContext.swapRequestArgs), + fullSwapRequestArgs: executeParams.swapTxContext.swapRequestArgs, + } : 'swapRequestArgs is undefined', + swapTxContextKeys: executeParams.swapTxContext ? Object.keys(executeParams.swapTxContext) : [], + }) + } + + ctx.onExecuteSwap(executeParams) + .catch((error) => { + const swapError = error instanceof Error ? error : new Error(String(error)) + ctx.onFailure(swapError) }) - .catch(ctx.onFailure) }, } } diff --git a/packages/uniswap/src/features/transactions/swap/services/hooks/usePrepareSwap.ts b/packages/uniswap/src/features/transactions/swap/services/hooks/usePrepareSwap.ts index ec134e49a94..8e6ac78cde5 100644 --- a/packages/uniswap/src/features/transactions/swap/services/hooks/usePrepareSwap.ts +++ b/packages/uniswap/src/features/transactions/swap/services/hooks/usePrepareSwap.ts @@ -19,7 +19,7 @@ const getIsViewOnlyWallet = (activeAccount?: AccountDetails): boolean => { return activeAccount?.accountType === AccountType.Readonly } -export function usePrepareSwap(ctx: { warningService: WarningService }): () => void { +export function usePrepareSwap(ctx: { warningService: WarningService; onExecuteSwapDirectly?: () => void }): () => void { const { handleShowTokenWarningModal, handleShowBridgingWarningModal, @@ -79,6 +79,7 @@ export function usePrepareSwap(ctx: { warningService: WarningService }): () => v // ctx warningService: ctx.warningService, logger, + onExecuteSwapDirectly: ctx.onExecuteSwapDirectly, }), ) } diff --git a/packages/uniswap/src/features/transactions/swap/services/prepareSwapService.ts b/packages/uniswap/src/features/transactions/swap/services/prepareSwapService.ts index a9b9c9cc0a1..d7875ff0674 100644 --- a/packages/uniswap/src/features/transactions/swap/services/prepareSwapService.ts +++ b/packages/uniswap/src/features/transactions/swap/services/prepareSwapService.ts @@ -20,12 +20,14 @@ export function createPrepareSwap( const getAction = createGetAction(ctx) const handleEventAction = createHandleEventAction(ctx) - const action = getAction({ + const skipWarnings = { skipBridgingWarning: ctx.warningService.getSkipBridgingWarning(), skipMaxTransferWarning: ctx.warningService.getSkipMaxTransferWarning(), skipTokenProtectionWarning: ctx.warningService.getSkipTokenProtectionWarning(), skipBridgedAssetWarning: ctx.warningService.getSkipBridgedAssetWarning(), - }) + } + + const action = getAction(skipWarnings) handleEventAction(action) } catch (error) { @@ -35,6 +37,9 @@ export function createPrepareSwap( function: 'prepareSwap', }, }) + if (process.env.NODE_ENV === 'development') { + console.error('[prepareSwap] Error:', error) + } } // always reset the warning service after the action is handled ctx.warningService.reset() @@ -154,6 +159,7 @@ interface HandleEventActionContext { onConnectWallet?: (platform?: Platform) => void updateSwapForm: (newState: Partial) => void setScreen: (screen: TransactionScreen) => void + onExecuteSwapDirectly?: () => void } function createHandleEventAction(ctx: HandleEventActionContext): (action: ReviewAction) => void { @@ -167,6 +173,7 @@ function createHandleEventAction(ctx: HandleEventActionContext): (action: Review onConnectWallet, updateSwapForm, setScreen, + onExecuteSwapDirectly, } = ctx function handleEventAction(action: ReviewAction): void { switch (action.type) { @@ -193,8 +200,14 @@ function createHandleEventAction(ctx: HandleEventActionContext): (action: Review handleShowBridgedAssetModal() break case ReviewActionType.PROCEED_TO_REVIEW: - updateSwapForm({ txId: createTransactionId() }) + const txId = createTransactionId() + updateSwapForm({ txId }) + // If onExecuteSwapDirectly is provided, execute swap directly instead of showing review screen + if (onExecuteSwapDirectly) { + onExecuteSwapDirectly() + } else { setScreen(TransactionScreen.Review) + } break } } diff --git a/packages/uniswap/src/features/transactions/swap/services/tradeService/evmTradeService.ts b/packages/uniswap/src/features/transactions/swap/services/tradeService/evmTradeService.ts index 5a0cb0be4b9..6469529891a 100644 --- a/packages/uniswap/src/features/transactions/swap/services/tradeService/evmTradeService.ts +++ b/packages/uniswap/src/features/transactions/swap/services/tradeService/evmTradeService.ts @@ -103,6 +103,7 @@ export function createEVMTradeService(ctx: EVMTradeServiceContext): TradeService const quoteHash = getIdentifierForQuote(quoteRequestArgs) // Step 5: Transform quote to trade + // Pass customSlippageTolerance to ensure trade uses user configuration instead of API response const result = transformQuoteToTrade({ quote: quoteResponse, amountSpecified: validatedInput.amount, @@ -111,6 +112,7 @@ export function createEVMTradeService(ctx: EVMTradeServiceContext): TradeService currencyOut: validatedInput.currencyOut, requestTradeType: validatedInput.requestTradeType, }, + customSlippageTolerance: input.customSlippageTolerance, }) // Return trade with gas estimates return { diff --git a/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/buildQuoteRequest.ts b/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/buildQuoteRequest.ts index d93bbd29d59..bce005f374b 100644 --- a/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/buildQuoteRequest.ts +++ b/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/buildQuoteRequest.ts @@ -121,14 +121,17 @@ export interface ParsedTradeInput { export function parseTradeInputForTradingApiQuote(input: UseTradeArgs): ParsedTradeInput { const { currencyIn, currencyOut, requestTradeType } = parseQuoteCurrencies(input) + const tokenInChainId = toTradingApiSupportedChainId(currencyIn?.chainId) + const tokenOutChainId = toTradingApiSupportedChainId(currencyOut?.chainId) + return { currencyIn, currencyOut, amount: input.amountSpecified, requestTradeType, activeAccountAddress: input.account?.address, - tokenInChainId: toTradingApiSupportedChainId(currencyIn?.chainId), - tokenOutChainId: toTradingApiSupportedChainId(currencyOut?.chainId), + tokenInChainId, + tokenOutChainId, tokenInAddress: getTokenAddressForApi(currencyIn), tokenOutAddress: getTokenAddressForApi(currencyOut), generatePermitAsTransaction: input.generatePermitAsTransaction, @@ -140,15 +143,16 @@ export function parseTradeInputForTradingApiQuote(input: UseTradeArgs): ParsedTr // Takes parsed input and returns validated input or undefined export function validateParsedInput(input: ParsedTradeInput): ValidatedTradeInput | undefined { // Check all conditions that would make the input invalid + // Reject zero amount - quote should not be fetched when amount is 0 if ( !input.tokenInChainId || !input.tokenOutChainId || !input.tokenInAddress || !input.tokenOutAddress || !input.amount || + isZeroAmount(input.amount) || !input.currencyIn || !input.currencyOut || - isZeroAmount(input.amount) || areCurrenciesEqual(input.currencyIn, input.currencyOut) ) { return undefined diff --git a/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/transformQuoteToTrade.ts b/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/transformQuoteToTrade.ts index ab49565b8ae..44a39b0818d 100644 --- a/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/transformQuoteToTrade.ts +++ b/packages/uniswap/src/features/transactions/swap/services/tradeService/transformations/transformQuoteToTrade.ts @@ -22,6 +22,7 @@ export function transformQuoteToTrade(input: { quote: DiscriminatedQuoteResponse | null amountSpecified: Maybe> quoteCurrencyData: QuoteCurrencyData + customSlippageTolerance?: number }): QuoteWithTradeAndGasEstimate { if (!input.quote) { return null @@ -35,6 +36,18 @@ export function transformQuoteToTrade(input: { requestTradeType === TradingApi.TradeType.EXACT_INPUT ? SdkTradeType.EXACT_INPUT : SdkTradeType.EXACT_OUTPUT const gasEstimate = getGasEstimate(input.quote) + // Override quote.slippage with customSlippageTolerance if provided + // This ensures trade.slippageTolerance uses user configuration instead of API response + const quoteWithCustomSlippage = input.customSlippageTolerance !== undefined && input.quote.routing === TradingApi.Routing.CLASSIC + ? { + ...input.quote, + quote: { + ...input.quote.quote, + slippage: input.customSlippageTolerance, + }, + } + : input.quote + const formattedTrade = currencyIn && currencyOut ? transformTradingApiResponseToTrade({ @@ -42,7 +55,7 @@ export function transformQuoteToTrade(input: { currencyOut, tradeType, deadline: inXMinutesUnix(DEFAULT_SWAP_VALIDITY_TIME_MINS), // TODO(MOB-3050): set deadline as `quoteRequestArgs.deadline` - data: input.quote, + data: quoteWithCustomSlippage, }) : null @@ -57,7 +70,7 @@ export function transformQuoteToTrade(input: { : null return { - ...input.quote, + ...quoteWithCustomSlippage, gasEstimate, trade, } diff --git a/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDefaultSwapFormState.ts b/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDefaultSwapFormState.ts index 4705b5cebcf..a894685f264 100644 --- a/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDefaultSwapFormState.ts +++ b/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDefaultSwapFormState.ts @@ -19,7 +19,7 @@ export const getDefaultState = (defaultChainId: UniverseChainId): Readonly bigint | undefined) | undefined +if (isWebApp) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const useCurrentBlockTimestampModule = require('apps/web/src/hooks/useCurrentBlockTimestamp') + useCurrentBlockTimestamp = useCurrentBlockTimestampModule.default || useCurrentBlockTimestampModule +} function useSwapTransactionRequestInfo({ derivedSwapInfo, @@ -31,26 +38,80 @@ function useSwapTransactionRequestInfo({ tokenApprovalInfo: TokenApprovalInfo | undefined }): TransactionRequestInfo { const trace = useTrace() - const gasStrategy = useActiveGasStrategy(derivedSwapInfo.chainId, 'general') const transactionSettings = useAllTransactionSettings() const permitData = derivedSwapInfo.trade.trade?.quote.permitData // On interface, we do not fetch signature until after swap is clicked, as it requires user interaction. const { data: signature } = usePermit2SignatureWithData({ permitData, skip: isWebApp }) + // Keep track of the last successful quote to use if current quote fails + const lastSuccessfulQuoteRef = useRef( + undefined, + ) + const swapQuoteResponse = useMemo(() => { const quote = derivedSwapInfo.trade.trade?.quote if (quote && (isClassic(quote) || isBridge(quote) || isWrap(quote))) { + // Update the last successful quote + lastSuccessfulQuoteRef.current = quote return quote } + // If current quote is not available, use the last successful quote + if (lastSuccessfulQuoteRef.current) { + return lastSuccessfulQuoteRef.current + } return undefined }, [derivedSwapInfo.trade.trade?.quote]) const swapQuote = swapQuoteResponse?.quote - const swapDelegationInfo = useUniswapContextSelector((ctx) => ctx.getSwapDelegationInfo?.(derivedSwapInfo.chainId)) + // Get current chainId from active account if derivedSwapInfo.chainId is not available + // We only support HSK chains (HashKey = 177, HashKeyTestnet = 133) + const activeAccount = useActiveAccount(Platform.EVM) + const currentChainId = (activeAccount as { chainId?: UniverseChainId } | undefined)?.chainId ?? derivedSwapInfo.chainId + + // Resolve chainId with multiple fallbacks + // Priority: 1. currentChainId (from account or derivedSwapInfo), 2. swapQuote tokenInChainId, 3. swapQuote tokenOutChainId, 4. default HashKeyTestnet + const resolvedChainId = useMemo(() => { + // First try: use currentChainId if available + if (currentChainId) { + return currentChainId + } + + // Second try: get from swapQuote tokenInChainId + if (swapQuote && 'tokenInChainId' in swapQuote && swapQuote.tokenInChainId) { + return swapQuote.tokenInChainId as UniverseChainId + } + + // Third try: get from swapQuote tokenOutChainId + if (swapQuote && 'tokenOutChainId' in swapQuote && swapQuote.tokenOutChainId) { + return swapQuote.tokenOutChainId as UniverseChainId + } + + // Final fallback: use HashKeyTestnet (133) since we only support HSK chains + return UniverseChainId.HashKeyTestnet + }, [currentChainId, swapQuote]) + + const gasStrategy = useActiveGasStrategy(resolvedChainId, 'general') + + const swapDelegationInfo = useUniswapContextSelector((ctx) => ctx.getSwapDelegationInfo?.(resolvedChainId)) const overrideSimulation = !!swapDelegationInfo?.delegationAddress + // Get block timestamp for accurate deadline calculation + // Only fetch on web app (where useCurrentBlockTimestamp is available) + const blockTimestamp = isWebApp && useCurrentBlockTimestamp ? useCurrentBlockTimestamp() : undefined + + if (process.env.NODE_ENV === 'development') { + console.log('[swap debug] useTransactionRequestInfo - Preparing swap request params:', { + customDeadline: transactionSettings.customDeadline, + hasSwapQuoteResponse: !!swapQuoteResponse, + alreadyApproved: tokenApprovalInfo?.action === ApprovalAction.None && !swapQuoteResponse?.permitTransaction, + overrideSimulation, + blockTimestamp: blockTimestamp?.toString(), + usingBlockTimestamp: !!blockTimestamp, + }) + } + const prepareSwapRequestParams = useMemo(() => createPrepareSwapRequestParams({ gasStrategy }), [gasStrategy]) const swapRequestParams = useMemo(() => { @@ -60,13 +121,16 @@ function useSwapTransactionRequestInfo({ const alreadyApproved = tokenApprovalInfo?.action === ApprovalAction.None && !swapQuoteResponse.permitTransaction - return prepareSwapRequestParams({ + const requestParams = prepareSwapRequestParams({ swapQuoteResponse, signature: signature ?? undefined, transactionSettings, alreadyApproved, overrideSimulation, + blockTimestamp, }) + + return requestParams }, [ swapQuoteResponse, tokenApprovalInfo?.action, @@ -74,60 +138,42 @@ function useSwapTransactionRequestInfo({ signature, transactionSettings, overrideSimulation, + blockTimestamp, ]) const canBatchTransactions = useUniswapContextSelector((ctx) => - ctx.getCanBatchTransactions?.(derivedSwapInfo.chainId), + ctx.getCanBatchTransactions?.(resolvedChainId), ) const permitsDontNeedSignature = !!canBatchTransactions - const shouldSkipSwapRequest = getShouldSkipSwapRequest({ - derivedSwapInfo, - tokenApprovalInfo, - signature: signature ?? undefined, - permitsDontNeedSignature, - }) - - const tradingApiSwapRequestMs = useDynamicConfigValue({ - config: DynamicConfigs.Swap, - key: SwapConfigKey.TradingApiSwapRequestMs, - defaultValue: FALLBACK_SWAP_REQUEST_POLL_INTERVAL_MS, - }) - - const { - data, - error, - isLoading: isSwapLoading, - } = useTradingApiSwapQuery( - { - params: shouldSkipSwapRequest ? undefined : swapRequestParams, - refetchInterval: tradingApiSwapRequestMs, - staleTime: tradingApiSwapRequestMs, - // We add a small buffer in case connection is too slow - immediateGcTime: tradingApiSwapRequestMs + ONE_SECOND_MS * 5, - }, - { - canBatchTransactions, - swapDelegationAddress: swapDelegationInfo?.delegationAddress, - includesDelegation: swapDelegationInfo?.delegationInclusion, - }, - ) + + // Skip swap API call - use quote methodParameters directly instead + // Don't call swap API - we'll use quote methodParameters directly + const data = undefined + const error = null + const isSwapLoading = false const processSwapResponse = useMemo(() => createProcessSwapResponse({ gasStrategy }), [gasStrategy]) - const result = useMemo( - () => - processSwapResponse({ + const result = useMemo(() => { + // Ensure resolvedChainId is always defined + const chainIdToUse = resolvedChainId ?? UniverseChainId.HashKeyTestnet + + const processResult = processSwapResponse({ response: data, error, swapQuote, + trade: derivedSwapInfo.trade.trade ?? undefined, isSwapLoading, permitData, swapRequestParams, isRevokeNeeded: tokenApprovalInfo?.action === ApprovalAction.RevokeAndPermit2Approve, permitsDontNeedSignature, - }), - [ + chainId: chainIdToUse, + }) + + return processResult + }, [ data, error, isSwapLoading, @@ -137,8 +183,12 @@ function useSwapTransactionRequestInfo({ processSwapResponse, tokenApprovalInfo?.action, permitsDontNeedSignature, - ], - ) + resolvedChainId, + derivedSwapInfo, + swapQuoteResponse, + activeAccount, + currentChainId, + ]) // Only log analytics events once per request const previousRequestIdRef = useRef(swapQuoteResponse?.requestId) diff --git a/packages/uniswap/src/features/transactions/swap/utils/gas.ts b/packages/uniswap/src/features/transactions/swap/utils/gas.ts index e31dd89ca12..8be515f6e6d 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/gas.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/gas.ts @@ -23,14 +23,38 @@ export function sumGasFees(gasFees: (string | undefined)[]): string | undefined * - displayValue: Sum of all display values (undefined if any result has error or missing value) */ export function mergeGasFeeResults(...gasFeeResults: GasFeeResult[]): GasFeeResult { - const error = gasFeeResults.map((g) => g.error).find((e) => !!e) ?? null + // Filter out "Approval action unknown" errors - these are informational and shouldn't block swap + // Also filter out empty objects and empty Error instances + const meaningfulErrors = gasFeeResults + .map((g) => g.error) + .filter((e) => { + if (!e) return false + if (e instanceof Error) { + // Filter out empty Error objects and "Approval action unknown" errors + if (!e.message || e.message.length === 0) return false + if (e.message === 'Approval action unknown') return false + return true + } + if (typeof e === 'object' && Object.keys(e).length > 0) return true + // Empty object {} should be filtered out + return false + }) + + // Use the first meaningful error, or null if none + const error = meaningfulErrors[0] ?? null + const isLoading = gasFeeResults.some((r) => r.isLoading) - const expectedValueMissing = gasFeeResults.some((r) => r.value === undefined) + // Only consider value missing if it's missing AND there's a meaningful error + // If value is missing but error is null (or filtered out), we can still proceed if other results have values + const resultsWithValues = gasFeeResults.filter((r) => r.value !== undefined) + const expectedValueMissing = resultsWithValues.length === 0 && error !== null + if (expectedValueMissing || error) { return { value: undefined, displayValue: undefined, error, isLoading } } + // Sum only the values that are defined const value = sumGasFees(gasFeeResults.map((r) => r.value)) const displayValue = sumGasFees(gasFeeResults.map((r) => r.displayValue)) return { value, displayValue, error, isLoading } diff --git a/packages/uniswap/src/features/transactions/swap/utils/generateSwapTransactionSteps.ts b/packages/uniswap/src/features/transactions/swap/utils/generateSwapTransactionSteps.ts index 1f6da056acf..16c805095a9 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/generateSwapTransactionSteps.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/generateSwapTransactionSteps.ts @@ -1,3 +1,4 @@ +import { Interface } from '@ethersproject/abi' import { createApprovalTransactionStep } from 'uniswap/src/features/transactions/steps/approve' import { createPermit2SignatureStep } from 'uniswap/src/features/transactions/steps/permit2Signature' import { createPermit2TransactionStep } from 'uniswap/src/features/transactions/steps/permit2Transaction' @@ -13,6 +14,36 @@ import { import { orderUniswapXSteps } from 'uniswap/src/features/transactions/swap/steps/uniswapxSteps' import { isValidSwapTxContext, SwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { isBridge, isClassic, isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import type { ValidatedTransactionRequest } from 'uniswap/src/features/transactions/types/transactionRequests' + +function buildHashKeyApprovalTxRequest({ + trade, + swapTxRequest, +}: { + trade: ClassicTrade | BridgeTrade + swapTxRequest: ValidatedTransactionRequest | undefined +}): ValidatedTransactionRequest | undefined { + const chainId = trade.inputAmount.currency.chainId + const isHashKeyChain = chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet + if (!isHashKeyChain) { + return undefined + } + + const spender = swapTxRequest?.to?.toString() + if (!spender) { + return undefined + } + + const approveInterface = new Interface(['function approve(address spender,uint256 value)']) + + return { + to: trade.inputAmount.currency.wrapped.address, + data: approveInterface.encodeFunctionData('approve', [spender, trade.inputAmount.quotient.toString()]), + value: '0x0', + chainId, + } +} export function generateSwapTransactionSteps(txContext: SwapTxAndGasInfo): TransactionStep[] { const isValidSwap = isValidSwapTxContext(txContext) @@ -30,7 +61,7 @@ export function generateSwapTransactionSteps(txContext: SwapTxAndGasInfo): Trans }) const approval = createApprovalTransactionStep({ ...requestFields, - txRequest: approveTxRequest, + txRequest: approveTxRequest ?? buildHashKeyApprovalTxRequest({ trade, swapTxRequest: txContext.txRequests?.[0] }), amount: trade.inputAmount.quotient.toString(), }) @@ -77,13 +108,14 @@ export function generateSwapTransactionSteps(txContext: SwapTxAndGasInfo): Trans permit: undefined, swap: createSwapTransactionStepBatched(txContext.txRequests), }) + } else { + return orderClassicSwapSteps({ + revocation, + approval, + permit: undefined, + swap: createSwapTransactionStep(txContext.txRequests[0]), + }) } - return orderClassicSwapSteps({ - revocation, - approval, - permit: undefined, - swap: createSwapTransactionStep(txContext.txRequests[0]), - }) } } diff --git a/packages/uniswap/src/features/transactions/swap/utils/protocols.ts b/packages/uniswap/src/features/transactions/swap/utils/protocols.ts index 8f0fa23647f..c34d197ddbf 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/protocols.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/protocols.ts @@ -6,14 +6,10 @@ import { createGetSupportedChainId } from 'uniswap/src/features/chains/hooks/use import { UniverseChainId } from 'uniswap/src/features/chains/types' import { createGetV4SwapEnabled, useV4SwapEnabled } from 'uniswap/src/features/transactions/swap/hooks/useV4SwapEnabled' +// HSKSwap only supports V3 export const DEFAULT_PROTOCOL_OPTIONS = [ // `as const` allows us to derive a type narrower than ProtocolItems, and the `...` spread removes readonly, allowing DEFAULT_PROTOCOL_OPTIONS to be passed around as an argument without `readonly` - ...([ - TradingApi.ProtocolItems.UNISWAPX_V2, - TradingApi.ProtocolItems.V4, - TradingApi.ProtocolItems.V3, - TradingApi.ProtocolItems.V2, - ] as const), + ...([TradingApi.ProtocolItems.V3] as const), ] export type FrontendSupportedProtocol = (typeof DEFAULT_PROTOCOL_OPTIONS)[number] diff --git a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts index 42cbd5c9e96..d4e98b08884 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts @@ -373,7 +373,17 @@ function isTradingApiSupportedChainId(chainId?: number): chainId is TradingApi.C } export function toTradingApiSupportedChainId(chainId: Maybe): TradingApi.ChainId | undefined { - if (!chainId || !isTradingApiSupportedChainId(chainId)) { + if (!chainId) { + return undefined + } + + // HashKey chains are supported by the Trading API (even if not in TradingApi.ChainId enum) + // The backend will handle the conversion to internal format ("hsk" or "hsktest") + if (chainId === 177 || chainId === 133) { + return chainId as TradingApi.ChainId + } + + if (!isTradingApiSupportedChainId(chainId)) { return undefined } return chainId @@ -482,29 +492,23 @@ export function createGetQuoteRoutingParams(ctx: { const { isUSDQuote } = input // for USD quotes, we avoid routing through UniswapX // hooksOptions should not be sent for USD quotes + // HSKSwap only supports V3 if (isUSDQuote) { return { - protocols: [TradingApi.ProtocolItems.V2, TradingApi.ProtocolItems.V3, TradingApi.ProtocolItems.V4], + protocols: [TradingApi.ProtocolItems.V3], } } const protocols = ctx.getProtocols() - let finalProtocols = [...protocols] - let hooksOptions: TradingApi.HooksOptions + // HSKSwap only supports V3, filter out V2, V4, and UniswapX + const v3OnlyProtocols = protocols.filter((p) => p === TradingApi.ProtocolItems.V3) - const isV4HookPoolsEnabled = ctx.getIsV4HookPoolsEnabled() + // If no V3 in protocols, force V3 (shouldn't happen with DEFAULT_PROTOCOL_OPTIONS, but safety check) + const finalProtocols = v3OnlyProtocols.length > 0 ? v3OnlyProtocols : [TradingApi.ProtocolItems.V3] - if (isV4HookPoolsEnabled) { - if (!protocols.includes(TradingApi.ProtocolItems.V4)) { - finalProtocols = [...protocols, TradingApi.ProtocolItems.V4] // we need to re-add v4 to protocols if v4 hooks is toggled on - hooksOptions = TradingApi.HooksOptions.V4_HOOKS_ONLY - } else { - hooksOptions = TradingApi.HooksOptions.V4_HOOKS_INCLUSIVE - } - } else { - hooksOptions = TradingApi.HooksOptions.V4_NO_HOOKS - } + // HSKSwap doesn't support V4 hooks + const hooksOptions = TradingApi.HooksOptions.V4_NO_HOOKS return { protocols: finalProtocols, hooksOptions } } diff --git a/packages/uniswap/src/i18n/i18n-setup-interface.tsx b/packages/uniswap/src/i18n/i18n-setup-interface.tsx index 8e8858fe8af..9904b9ffb33 100644 --- a/packages/uniswap/src/i18n/i18n-setup-interface.tsx +++ b/packages/uniswap/src/i18n/i18n-setup-interface.tsx @@ -29,10 +29,12 @@ export function setupi18n(): undefined { return enUsLocale } - const fileName = getLocaleTranslationKey(locale) + // Only English is supported - return undefined for other languages + return undefined - // eslint-disable-next-line no-unsanitized/method - return import(`./locales/translations/${fileName}.json`) + // const fileName = getLocaleTranslationKey(locale) + // // eslint-disable-next-line no-unsanitized/method + // return import(`./locales/translations/${fileName}.json`) }), ) // eslint-disable-next-line max-params diff --git a/packages/uniswap/src/i18n/i18n-setup.tsx b/packages/uniswap/src/i18n/i18n-setup.tsx index 61cfdfc348a..ad0f73ea034 100644 --- a/packages/uniswap/src/i18n/i18n-setup.tsx +++ b/packages/uniswap/src/i18n/i18n-setup.tsx @@ -3,58 +3,60 @@ import 'uniswap/src/i18n/locales/@types/i18next' import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import enUS from 'uniswap/src/i18n/locales/source/en-US.json' -import esES from 'uniswap/src/i18n/locales/translations/es-ES.json' -import frFR from 'uniswap/src/i18n/locales/translations/fr-FR.json' -import idID from 'uniswap/src/i18n/locales/translations/id-ID.json' -import jaJP from 'uniswap/src/i18n/locales/translations/ja-JP.json' -import nlNL from 'uniswap/src/i18n/locales/translations/nl-NL.json' -import ptPT from 'uniswap/src/i18n/locales/translations/pt-PT.json' -import ruRU from 'uniswap/src/i18n/locales/translations/ru-RU.json' -import trTR from 'uniswap/src/i18n/locales/translations/tr-TR.json' -import viVN from 'uniswap/src/i18n/locales/translations/vi-VN.json' -import zhCN from 'uniswap/src/i18n/locales/translations/zh-CN.json' -import zhTW from 'uniswap/src/i18n/locales/translations/zh-TW.json' +// Only English is supported - other languages are disabled +// import esES from 'uniswap/src/i18n/locales/translations/es-ES.json' +// import frFR from 'uniswap/src/i18n/locales/translations/fr-FR.json' +// import idID from 'uniswap/src/i18n/locales/translations/id-ID.json' +// import jaJP from 'uniswap/src/i18n/locales/translations/ja-JP.json' +// import nlNL from 'uniswap/src/i18n/locales/translations/nl-NL.json' +// import ptPT from 'uniswap/src/i18n/locales/translations/pt-PT.json' +// import ruRU from 'uniswap/src/i18n/locales/translations/ru-RU.json' +// import trTR from 'uniswap/src/i18n/locales/translations/tr-TR.json' +// import viVN from 'uniswap/src/i18n/locales/translations/vi-VN.json' +// import zhCN from 'uniswap/src/i18n/locales/translations/zh-CN.json' +// import zhTW from 'uniswap/src/i18n/locales/translations/zh-TW.json' import { MissingI18nInterpolationError } from 'uniswap/src/i18n/shared' import { getWalletDeviceLocale } from 'uniswap/src/i18n/utils' import { logger } from 'utilities/src/logger/logger' +// Only English is supported - other languages are disabled const resources = { - 'zh-Hans': { translation: zhCN, statsigKey: 'zh-CN' }, - 'zh-Hant': { translation: zhTW, statsigKey: 'zh-TW' }, - 'nl-NL': { translation: nlNL, statsigKey: 'nl-NL' }, 'en-US': { translation: enUS, statsigKey: 'en-US' }, - 'fr-FR': { translation: frFR, statsigKey: 'fr-FR' }, - 'id-ID': { translation: idID, statsigKey: 'id-ID' }, - 'ja-JP': { translation: jaJP, statsigKey: 'ja-JP' }, - 'pt-PT': { translation: ptPT, statsigKey: 'pt-PT' }, - 'ru-RU': { translation: ruRU, statsigKey: 'ru-RU' }, + // 'zh-Hans': { translation: zhCN, statsigKey: 'zh-CN' }, + // 'zh-Hant': { translation: zhTW, statsigKey: 'zh-TW' }, + // 'nl-NL': { translation: nlNL, statsigKey: 'nl-NL' }, + // 'fr-FR': { translation: frFR, statsigKey: 'fr-FR' }, + // 'id-ID': { translation: idID, statsigKey: 'id-ID' }, + // 'ja-JP': { translation: jaJP, statsigKey: 'ja-JP' }, + // 'pt-PT': { translation: ptPT, statsigKey: 'pt-PT' }, + // 'ru-RU': { translation: ruRU, statsigKey: 'ru-RU' }, // Spanish locales that use `,` as the decimal separator - 'es-419': { translation: esES, statsigKey: 'es-ES' }, - 'es-BZ': { translation: esES, statsigKey: 'es-ES' }, - 'es-CU': { translation: esES, statsigKey: 'es-ES' }, - 'es-DO': { translation: esES, statsigKey: 'es-ES' }, - 'es-GT': { translation: esES, statsigKey: 'es-ES' }, - 'es-HN': { translation: esES, statsigKey: 'es-ES' }, - 'es-MX': { translation: esES, statsigKey: 'es-ES' }, - 'es-NI': { translation: esES, statsigKey: 'es-ES' }, - 'es-PA': { translation: esES, statsigKey: 'es-ES' }, - 'es-PE': { translation: esES, statsigKey: 'es-ES' }, - 'es-PR': { translation: esES, statsigKey: 'es-ES' }, - 'es-SV': { translation: esES, statsigKey: 'es-ES' }, - 'es-US': { translation: esES, statsigKey: 'es-ES' }, + // 'es-419': { translation: esES, statsigKey: 'es-ES' }, + // 'es-BZ': { translation: esES, statsigKey: 'es-ES' }, + // 'es-CU': { translation: esES, statsigKey: 'es-ES' }, + // 'es-DO': { translation: esES, statsigKey: 'es-ES' }, + // 'es-GT': { translation: esES, statsigKey: 'es-ES' }, + // 'es-HN': { translation: esES, statsigKey: 'es-ES' }, + // 'es-MX': { translation: esES, statsigKey: 'es-ES' }, + // 'es-NI': { translation: esES, statsigKey: 'es-ES' }, + // 'es-PA': { translation: esES, statsigKey: 'es-ES' }, + // 'es-PE': { translation: esES, statsigKey: 'es-ES' }, + // 'es-PR': { translation: esES, statsigKey: 'es-ES' }, + // 'es-SV': { translation: esES, statsigKey: 'es-ES' }, + // 'es-US': { translation: esES, statsigKey: 'es-ES' }, // Spanish locales that use `.` as the decimal separator - 'es-AR': { translation: esES, statsigKey: 'es-ES' }, - 'es-BO': { translation: esES, statsigKey: 'es-ES' }, - 'es-CL': { translation: esES, statsigKey: 'es-ES' }, - 'es-CO': { translation: esES, statsigKey: 'es-ES' }, - 'es-CR': { translation: esES, statsigKey: 'es-ES' }, - 'es-EC': { translation: esES, statsigKey: 'es-ES' }, - 'es-ES': { translation: esES, statsigKey: 'es-ES' }, - 'es-PY': { translation: esES, statsigKey: 'es-ES' }, - 'es-UY': { translation: esES, statsigKey: 'es-ES' }, - 'es-VE': { translation: esES, statsigKey: 'es-ES' }, - 'tr-TR': { translation: trTR, statsigKey: 'tr-TR' }, - 'vi-VN': { translation: viVN, statsigKey: 'vi-VN' }, + // 'es-AR': { translation: esES, statsigKey: 'es-ES' }, + // 'es-BO': { translation: esES, statsigKey: 'es-ES' }, + // 'es-CL': { translation: esES, statsigKey: 'es-ES' }, + // 'es-CO': { translation: esES, statsigKey: 'es-ES' }, + // 'es-CR': { translation: esES, statsigKey: 'es-ES' }, + // 'es-EC': { translation: esES, statsigKey: 'es-ES' }, + // 'es-ES': { translation: esES, statsigKey: 'es-ES' }, + // 'es-PY': { translation: esES, statsigKey: 'es-ES' }, + // 'es-UY': { translation: esES, statsigKey: 'es-ES' }, + // 'es-VE': { translation: esES, statsigKey: 'es-ES' }, + // 'tr-TR': { translation: trTR, statsigKey: 'tr-TR' }, + // 'vi-VN': { translation: viVN, statsigKey: 'vi-VN' }, } const defaultNS = 'translation' @@ -63,7 +65,8 @@ i18n .use(initReactI18next) .init({ defaultNS, - lng: getWalletDeviceLocale(), + lng: 'en-US', // Force English only + // lng: getWalletDeviceLocale(), fallbackLng: 'en-US', resources, interpolation: { diff --git a/packages/uniswap/src/i18n/locales/source/en-US.json b/packages/uniswap/src/i18n/locales/source/en-US.json index 7ac4662ed3d..ad7133c7475 100644 --- a/packages/uniswap/src/i18n/locales/source/en-US.json +++ b/packages/uniswap/src/i18n/locales/source/en-US.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Switch wallet", "common.connectTo": "Connect to {{platform}}", "common.connectWallet.button": "Connect wallet", + "common.connecting": "Connecting", "common.contactUs.button": "Contact us", "common.copied": "Copied", "common.copy.address": "Copy address", @@ -716,7 +717,7 @@ "downloadApp.modal.getStarted.title": "Start swapping in seconds", "downloadApp.modal.getTheApp.title": "Get started with Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap products work seamlessly together to create the best onchain experience.", - "empty.swap.button.text": "Add funds to swap", + "empty.swap.button.text": "Swap Tokens", "error.dataUnavailable": "Data is unavailable at the moment; we’re working on a fix", "error.id": "Error ID: {{eventId}}", "error.jupiterApi.execute.default.title": "Something went wrong with Jupiter API. Please try again.", @@ -964,7 +965,7 @@ "home.upsell.receive.title": "Receive crypto", "home.warning.viewOnly": "This is a view-only wallet", "interface.metatags.description": "Swap crypto on Ethereum, Base, Arbitrum, Polygon, Unichain and more. The DeFi platform trusted by millions.", - "interface.metatags.title": "Uniswap Interface", + "interface.metatags.title": "HSKSwap", "landing.api": "API", "landing.appsOverview": "Built for all the ways you swap", "landing.blog.description": "Catch up on the latest company news, product features and more", @@ -2207,7 +2208,7 @@ "testnet.modal.swapDeepLink.title.toTestnetMode": "Enable testnet mode", "testnet.unsupported": "This functionality is not supported in testnet mode.", "themeToggle.theme": "Theme", - "title.buySellTradeEthereum": "Buy, sell & trade Ethereum and other top tokens on Uniswap", + "title.buySellTradeEthereum": "Buy, sell & trade Ethereum and other top tokens on HSKSwap", "title.createGovernanceOn": "Create a new governance proposal on Uniswap", "title.createGovernanceTo": "Create a new governance proposal to be voted on by UNI holders. UNI tokens represent voting shares in Uniswap governance.", "title.earnFees": "Earn fees when others swap on Uniswap by adding tokens to liquidity pools.", @@ -2226,9 +2227,9 @@ "title.removev3Liquidity": "Remove your tokens from v3 liquidity pools.", "title.sendCrypto": "Send crypto", "title.sendTokens": "Send tokens on Uniswap", - "title.swappingMadeSimple": "Instantly buy and sell crypto on Ethereum, Base, Arbitrum, Polygon, Unichain and more. The DeFi platform trusted by millions.", + "title.swappingMadeSimple": "Instantly buy and sell crypto on Ethereum, Base, Arbitrum, Polygon, HashKey Chain and more. The DeFi platform trusted by millions.", "title.tradeTokens": "Trade tokens and provide liquidity. Real-time prices, charts, transaction data, and more.", - "title.uniswapTradeCrypto": "Uniswap | Trade Crypto on DeFi’s Leading Exchange ", + "title.uniswapTradeCrypto": "HSKSwap | Trade Crypto on DeFi's Leading Exchange", "title.uniToken": "UNI tokens represent voting shares in Uniswap governance. You can vote on each proposal yourself or delegate your votes to a third party.", "title.voteOnGov": "Vote on governance proposals on Uniswap", "token.balances.main": "Your balance", @@ -2367,6 +2368,7 @@ "tokens.selector.section.favorite": "Favorites", "tokens.selector.section.otherNetworksSearchResults": "Tokens found on other networks", "tokens.selector.section.otherSearchResults": "Other tokens on {{network}}", + "tokens.selector.section.poolTokens": "Pool tokens", "tokens.selector.section.recent": "Recent searches", "tokens.selector.section.search": "Search results", "tokens.selector.section.trending": "Tokens by 24H volume", diff --git a/packages/uniswap/src/i18n/locales/translations/es-ES.json b/packages/uniswap/src/i18n/locales/translations/es-ES.json index 74954ee2c95..cb29377436c 100644 --- a/packages/uniswap/src/i18n/locales/translations/es-ES.json +++ b/packages/uniswap/src/i18n/locales/translations/es-ES.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Cambiar billetera", "common.connectTo": "Conectar a {{platform}}", "common.connectWallet.button": "Conectar billetera", + "common.connecting": "Conectando", "common.contactUs.button": "Contáctanos", "common.copied": "Copiado", "common.copy.address": "Copiar dirección", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Comienza a hacer intercambios en segundos", "downloadApp.modal.getTheApp.title": "Comienza con Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Los productos de Uniswap funcionan perfectamente juntos para crear la mejor experiencia en la cadena.", - "empty.swap.button.text": "Agregar fondos para hacer intercambios", + "empty.swap.button.text": "Intercambiar tokens", "error.dataUnavailable": "Los datos no están disponibles en este momento; estamos trabajando para solucionarlo", "error.id": "ID del error: {{eventId}}", "error.jupiterApi.execute.default.title": "Ocurrió un error con la API de Jupiter. Inténtalo de nuevo.", diff --git a/packages/uniswap/src/i18n/locales/translations/fil-PH.json b/packages/uniswap/src/i18n/locales/translations/fil-PH.json index 6aa457d12e3..7d33bdb162f 100644 --- a/packages/uniswap/src/i18n/locales/translations/fil-PH.json +++ b/packages/uniswap/src/i18n/locales/translations/fil-PH.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "I-switch ang wallet", "common.connectTo": "Kumonekta sa {{platform}}", "common.connectWallet.button": "Ikonekta ang wallet", + "common.connecting": "Kumokonekta", "common.contactUs.button": "Makipag-ugnayan sa amin", "common.copied": "Nakopya na", "common.copy.address": "Kopyahin ang address", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Simulang mag-swap sa loob lang ng ilang segundo", "downloadApp.modal.getTheApp.title": "Magsimula sa Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Maayos na gumagana nang magkakasama ang mga produkto ng Uniswap para magawa ang pinakamagandang onchain na experience.", - "empty.swap.button.text": "Magdagdag ng mga pondo para mag-swap", + "empty.swap.button.text": "Mag-swap ng Tokens", "error.dataUnavailable": "Hindi available ang data sa ngayon; inaayos na namin ito", "error.id": "Error ID: {{eventId}}", "error.jupiterApi.execute.default.title": "Nagkaproblema sa Jupiter API. Pakisubukan ulit.", diff --git a/packages/uniswap/src/i18n/locales/translations/fr-FR.json b/packages/uniswap/src/i18n/locales/translations/fr-FR.json index 8b07ccf4c4b..2a6af3f218a 100644 --- a/packages/uniswap/src/i18n/locales/translations/fr-FR.json +++ b/packages/uniswap/src/i18n/locales/translations/fr-FR.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Changer de wallet", "common.connectTo": "Se connecter à {{platform}}", "common.connectWallet.button": "Connecter le wallet", + "common.connecting": "Connexion en cours", "common.contactUs.button": "Nous contacter", "common.copied": "Copié", "common.copy.address": "Copier l’adresse", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Commencez à échanger en quelques secondes", "downloadApp.modal.getTheApp.title": "Commencer avec Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Les produits Uniswap fonctionnent parfaitement ensemble pour créer la meilleure expérience en chaîne.", - "empty.swap.button.text": "Ajouter des fonds à échanger", + "empty.swap.button.text": "Échanger des jetons", "error.dataUnavailable": "Les données ne sont pas disponibles pour le moment ; nous travaillons à corriger ceci", "error.id": "ID de l'erreur : {{eventId}}", "error.jupiterApi.execute.default.title": "Une erreur est survenue avec l’API Jupiter, veuillez réessayer.", diff --git a/packages/uniswap/src/i18n/locales/translations/id-ID.json b/packages/uniswap/src/i18n/locales/translations/id-ID.json index 6c0f6952db9..66a6b42bffd 100644 --- a/packages/uniswap/src/i18n/locales/translations/id-ID.json +++ b/packages/uniswap/src/i18n/locales/translations/id-ID.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Ganti dompet", "common.connectTo": "Hubungkan ke {{platform}}", "common.connectWallet.button": "Hubungkan dompet", + "common.connecting": "Menghubungkan", "common.contactUs.button": "Hubungi kami", "common.copied": "Disalin", "common.copy.address": "Salin alamat", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Tukarkan sekarang dengan cepat", "downloadApp.modal.getTheApp.title": "Mulai menggunakan Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Semua produk Uniswap bekerja sama dengan lancar demi menciptakan pengalaman onchain terbaik.", - "empty.swap.button.text": "Tambah dana ke pertukaran", + "empty.swap.button.text": "Tukar Token", "error.dataUnavailable": "Data tidak tersedia saat ini; kami sedang mengatasinya", "error.id": "ID Kesalahan: {{eventId}}", "error.jupiterApi.execute.default.title": "Terjadi kesalahan dengan Jupiter API. Silakan coba lagi.", diff --git a/packages/uniswap/src/i18n/locales/translations/ja-JP.json b/packages/uniswap/src/i18n/locales/translations/ja-JP.json index 442c1009306..d9da5a16036 100644 --- a/packages/uniswap/src/i18n/locales/translations/ja-JP.json +++ b/packages/uniswap/src/i18n/locales/translations/ja-JP.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "ウォレットを切り替え", "common.connectTo": "{{platform}} に接続", "common.connectWallet.button": "ウォレットを接続", + "common.connecting": "接続中", "common.contactUs.button": "お問い合わせ", "common.copied": "コピーしました", "common.copy.address": "アドレスをコピー", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "すぐにスワップを始められます", "downloadApp.modal.getTheApp.title": "Uniswap を始めましょう", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap 製品はシームレスに連携して、最高のオンチェーンエクスペリエンスを実現します。", - "empty.swap.button.text": "資金を追加してスワップする", + "empty.swap.button.text": "トークンをスワップ", "error.dataUnavailable": "現在、データをご利用いただけません。問題を解決中です", "error.id": "エラーID:{{eventId}}", "error.jupiterApi.execute.default.title": "Jupiter API でエラーが発生しました。もう一度お試しください。", diff --git a/packages/uniswap/src/i18n/locales/translations/ko-KR.json b/packages/uniswap/src/i18n/locales/translations/ko-KR.json index fbfce56b879..6e6e06aa6a2 100644 --- a/packages/uniswap/src/i18n/locales/translations/ko-KR.json +++ b/packages/uniswap/src/i18n/locales/translations/ko-KR.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "지갑 전환", "common.connectTo": "{{platform}}에 연결", "common.connectWallet.button": "지갑 연결", + "common.connecting": "연결 중", "common.contactUs.button": "문의하기", "common.copied": "복사됨", "common.copy.address": "주소 복사", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "빠르게 스왑 시작", "downloadApp.modal.getTheApp.title": "Uniswap 시작하기", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap 제품은 서로 원활하게 작동하여 최고의 온체인 경험을 만들어냅니다.", - "empty.swap.button.text": "스왑을 위해 자금 추가", + "empty.swap.button.text": "토큰 교환", "error.dataUnavailable": "지금은 데이터를 사용할 수 없습니다. 우리는 문제를 해결하기 위해 노력하고 있습니다", "error.id": "오류 ID: {{eventId}}", "error.jupiterApi.execute.default.title": "Jupiter API에 문제가 발생했습니다. 다시 시도해 주세요.", diff --git a/packages/uniswap/src/i18n/locales/translations/nl-NL.json b/packages/uniswap/src/i18n/locales/translations/nl-NL.json index 33255752911..eab1cd5098a 100644 --- a/packages/uniswap/src/i18n/locales/translations/nl-NL.json +++ b/packages/uniswap/src/i18n/locales/translations/nl-NL.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Van wallet wisselen", "common.connectTo": "Verbinden met {{platform}}", "common.connectWallet.button": "Wallet koppelen", + "common.connecting": "Bezig met verbinden", "common.contactUs.button": "Neem contact met ons op", "common.copied": "Gekopieerd", "common.copy.address": "Adres kopiëren", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Begin in een paar seconden met swappen", "downloadApp.modal.getTheApp.title": "Aan de slag met Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap-producten werken naadloos samen om de beste onchain-ervaring te creëren.", - "empty.swap.button.text": "Geld toevoegen aan swap", + "empty.swap.button.text": "Tokens Wisselen", "error.dataUnavailable": "Gegevens zijn momenteel niet beschikbaar; we werken aan een oplossing", "error.id": "Fout-ID: {{eventId}}", "error.jupiterApi.execute.default.title": "Er is iets misgegaan met de Jupiter-API. Probeer het opnieuw.", diff --git a/packages/uniswap/src/i18n/locales/translations/pt-BR.json b/packages/uniswap/src/i18n/locales/translations/pt-BR.json index 1ac5f3bf105..0db6c35465b 100644 --- a/packages/uniswap/src/i18n/locales/translations/pt-BR.json +++ b/packages/uniswap/src/i18n/locales/translations/pt-BR.json @@ -288,6 +288,7 @@ "common.connectAWallet.button": "Conectar uma carteira", "common.connectingWallet": "Conectando carteira...", "common.connectWallet.button": "Conectar carteira", + "common.connecting": "Conectando", "common.contactUs.button": "Fale conosco", "common.contractInteraction": "Interação contratual", "common.copied": "Copiado", diff --git a/packages/uniswap/src/i18n/locales/translations/pt-PT.json b/packages/uniswap/src/i18n/locales/translations/pt-PT.json index 724c01edb8e..14061154213 100644 --- a/packages/uniswap/src/i18n/locales/translations/pt-PT.json +++ b/packages/uniswap/src/i18n/locales/translations/pt-PT.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Alterar carteira", "common.connectTo": "Conectar à {{platform}}", "common.connectWallet.button": "Conectar carteira", + "common.connecting": "Conectando", "common.contactUs.button": "Fale conosco", "common.copied": "Copiado", "common.copy.address": "Copiar endereço", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Comece a fazer swap em segundos", "downloadApp.modal.getTheApp.title": "Como começar a usar a Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Os produtos Uniswap funcionam muito bem juntos para criar a melhor experiência de on-chain.", - "empty.swap.button.text": "Adicionar fundos para fazer swap", + "empty.swap.button.text": "Trocar Tokens", "error.dataUnavailable": "Dados indisponíveis no momento; estamos trabalhando para resolver", "error.id": "ID do erro: {{eventId}}", "error.jupiterApi.execute.default.title": "Ocorreu um problema com a API Jupiter. Tente novamente.", diff --git a/packages/uniswap/src/i18n/locales/translations/ru-RU.json b/packages/uniswap/src/i18n/locales/translations/ru-RU.json index 554749095a5..4d78a21e839 100644 --- a/packages/uniswap/src/i18n/locales/translations/ru-RU.json +++ b/packages/uniswap/src/i18n/locales/translations/ru-RU.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Переключить кошелек", "common.connectTo": "Подключиться к {{platform}}", "common.connectWallet.button": "Подключить кошелек", + "common.connecting": "Подключение", "common.contactUs.button": "Свяжитесь с нами", "common.copied": "Скопировано", "common.copy.address": "Копировать адрес", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Начните выполнять своп токенов за считанные секунды", "downloadApp.modal.getTheApp.title": "Начало работы с Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Продукты Uniswap прекрасно взаимодействуют друг с другом, обеспечивая наилучший опыт работы ончейн.", - "empty.swap.button.text": "Добавить средства для свопа", + "empty.swap.button.text": "Обменять токены", "error.dataUnavailable": "Данные сейчас недоступны. Мы работаем над исправлением.", "error.id": "Идентификатор ошибки: {{eventId}}", "error.jupiterApi.execute.default.title": "Возникла проблема с Jupiter API. Попробуйте еще раз.", diff --git a/packages/uniswap/src/i18n/locales/translations/tr-TR.json b/packages/uniswap/src/i18n/locales/translations/tr-TR.json index eb506ef35a4..e2b7be115c8 100644 --- a/packages/uniswap/src/i18n/locales/translations/tr-TR.json +++ b/packages/uniswap/src/i18n/locales/translations/tr-TR.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Cüzdanı değiştir", "common.connectTo": "{{platform}} platformuna bağlan", "common.connectWallet.button": "Cüzdanı bağla", + "common.connecting": "Bağlanıyor", "common.contactUs.button": "Bize ulaş", "common.copied": "Kopyalandı", "common.copy.address": "Adresi kopyala", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Saniyeler içinde swap işlemlerine başla", "downloadApp.modal.getTheApp.title": "Uniswap'ı kullanmaya başla", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap ürünleri, en iyi zincir içi deneyimi sunmak için kusursuz bir şekilde birlikte çalışır.", - "empty.swap.button.text": "Swap'a fon ekle", + "empty.swap.button.text": "Token Takasla", "error.dataUnavailable": "Veriler şu anda kullanılamıyor; bir düzeltme üzerinde çalışıyoruz", "error.id": "Hata Kimliği: {{eventId}}", "error.jupiterApi.execute.default.title": "Jupiter API ile ilgili bir sorun oluştu. Lütfen tekrar dene.", diff --git a/packages/uniswap/src/i18n/locales/translations/vi-VN.json b/packages/uniswap/src/i18n/locales/translations/vi-VN.json index 2e1c12e3ad0..4f148ff4aed 100644 --- a/packages/uniswap/src/i18n/locales/translations/vi-VN.json +++ b/packages/uniswap/src/i18n/locales/translations/vi-VN.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "Chuyển ví", "common.connectTo": "Kết nối với {{platform}}", "common.connectWallet.button": "Kết nối ví", + "common.connecting": "Đang kết nối", "common.contactUs.button": "Liên hệ", "common.copied": "Đã sao chép", "common.copy.address": "Sao chép địa chỉ", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "Bắt đầu hoán đổi trong giây lát", "downloadApp.modal.getTheApp.title": "Bắt đầu với Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Các sản phẩm Uniswap hoạt động liền mạch cùng nhau để tạo trải nghiệm onchain tốt nhất.", - "empty.swap.button.text": "Nạp quỹ để hoán đổi", + "empty.swap.button.text": "Hoán đổi Token", "error.dataUnavailable": "Dữ liệu hiện không khả dụng; chúng tôi đang khắc phục", "error.id": "ID Lỗi: {{eventId}}", "error.jupiterApi.execute.default.title": "Đã xảy ra lỗi với API Jupiter. Vui lòng thử lại.", diff --git a/packages/uniswap/src/i18n/locales/translations/zh-CN.json b/packages/uniswap/src/i18n/locales/translations/zh-CN.json index 883b448d931..47bfad80127 100644 --- a/packages/uniswap/src/i18n/locales/translations/zh-CN.json +++ b/packages/uniswap/src/i18n/locales/translations/zh-CN.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "切换钱包", "common.connectTo": "连接到 {{platform}}", "common.connectWallet.button": "连接钱包", + "common.connecting": "连接中", "common.contactUs.button": "联系我们", "common.copied": "已复制", "common.copy.address": "复制地址", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "数秒内即可开始交换代币", "downloadApp.modal.getTheApp.title": "开始使用 Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap 产品无缝协作,创造最佳的链上体验。", - "empty.swap.button.text": "添加资金以进行交换", + "empty.swap.button.text": "交换代币", "error.dataUnavailable": "目前无法提供数据;我们正在努力修复", "error.id": "错误 ID:{{eventId}}", "error.jupiterApi.execute.default.title": "Jupiter API 出错了,请重试。", @@ -966,7 +967,7 @@ "home.upsell.receive.title": "接收加密货币", "home.warning.viewOnly": "这是仅供查看的钱包", "interface.metatags.description": "支持在以太坊、Base、Arbitrum、Polygon、Unichain 等链上交换加密货币。数百万用户信赖的 DeFi 平台。", - "interface.metatags.title": "Uniswap 界面", + "interface.metatags.title": "HSKSwap", "landing.api": "API", "landing.appsOverview": "专为各种交换方式而设计", "landing.blog.description": "了解最新的公司新闻、产品功能等", @@ -2222,7 +2223,7 @@ "testnet.modal.swapDeepLink.title.toTestnetMode": "启用测试网模式", "testnet.unsupported": "测试网模式不支持此功能。", "themeToggle.theme": "主题", - "title.buySellTradeEthereum": "在 Uniswap 上购买、出售和交易以太坊及其他顶级代币", + "title.buySellTradeEthereum": "在 HSKSwap 上购买、出售和交易以太坊及其他顶级代币", "title.createGovernanceOn": "在 Uniswap 上创建新的治理提案", "title.createGovernanceTo": "创建一个新的治理提案,供 UNI 持有者投票。UNI 代币代表 Uniswap 治理中的投票权份额。", "title.earnFees": "在他人通过向流动性池添加代币以在 Uniswap 上进行交换时赚取费用。", @@ -2241,9 +2242,9 @@ "title.removev3Liquidity": "从 v3 流动性资金池中移除你的代币。", "title.sendCrypto": "发送加密货币", "title.sendTokens": "在 Uniswap 上发送代币", - "title.swappingMadeSimple": "支持在以太坊、Base、Arbitrum、Polygon、Unichain 等链上即时买卖加密货币。数百万用户信赖的 DeFi 平台。", + "title.swappingMadeSimple": "支持在以太坊、Base、Arbitrum、Polygon、HashKey Chain 等链上即时买卖加密货币。数百万用户信赖的 DeFi 平台。", "title.tradeTokens": "交易代币并提供流动性。实时价格、图表、交易数据等。", - "title.uniswapTradeCrypto": "Uniswap | 在 DeFi 领先交易所交易加密货币 ", + "title.uniswapTradeCrypto": "HSKSwap | 在 DeFi 领先交易所交易加密货币", "title.uniToken": "UNI 代币代表 Uniswap 治理中的投票权份额。你可以自己对每个提案进行投票,也可以将你的投票委托给第三方。", "title.voteOnGov": "对 Uniswap 上的治理提案进行投票", "token.balances.main": "你的余额", diff --git a/packages/uniswap/src/i18n/locales/translations/zh-TW.json b/packages/uniswap/src/i18n/locales/translations/zh-TW.json index a802ae873ca..c209a1e16e8 100644 --- a/packages/uniswap/src/i18n/locales/translations/zh-TW.json +++ b/packages/uniswap/src/i18n/locales/translations/zh-TW.json @@ -301,6 +301,7 @@ "common.connectAWallet.button.switch": "切換錢包", "common.connectTo": "連接至 {{platform}}", "common.connectWallet.button": "連線錢包", + "common.connecting": "連線中", "common.contactUs.button": "聯絡我們", "common.copied": "已複製", "common.copy.address": "複製地址", @@ -717,7 +718,7 @@ "downloadApp.modal.getStarted.title": "幾秒內即可開始交換", "downloadApp.modal.getTheApp.title": "開始使用 Uniswap", "downloadApp.modal.uniswapProducts.subtitle": "Uniswap 產品無縫協作,創造最佳的鏈上體驗。", - "empty.swap.button.text": "新增資金以進行交換", + "empty.swap.button.text": "交換代幣", "error.dataUnavailable": "目前無法取得資料;我們正在努力修復問題", "error.id": "錯誤 ID:{{eventId}}", "error.jupiterApi.execute.default.title": "Jupiter API 發生錯誤,請重試。", @@ -2222,7 +2223,7 @@ "testnet.modal.swapDeepLink.title.toTestnetMode": "啟用測試網模式", "testnet.unsupported": "測試網模式不支援此功能。", "themeToggle.theme": "主題", - "title.buySellTradeEthereum": "在 Uniswap 上購買、出售和交易以太幣和其他頂級代幣", + "title.buySellTradeEthereum": "在 HSKSwap 上購買、出售和交易以太幣和其他頂級代幣", "title.createGovernanceOn": "在 Uniswap 上建立新的治理提案", "title.createGovernanceTo": "建立一個新的治理提案,供 UNI 持有者投票。UNI 代幣代表 Uniswap 治理中的投票份額。", "title.earnFees": "你可以在其他人將代幣新增到流動資產池,以在 Uniswap 上進行交換時賺取交易費用。", @@ -2241,9 +2242,9 @@ "title.removev3Liquidity": "從 v3 流動性資產池中移除你的代幣。", "title.sendCrypto": "傳送加密貨幣", "title.sendTokens": "在 Uniswap 上傳送代幣", - "title.swappingMadeSimple": "支援在以太坊、Base、Arbitrum、Polygon、Unichain 等鏈上即時買賣加密貨幣。深受數百萬用戶信賴的 DeFi 平台。", + "title.swappingMadeSimple": "支援在以太坊、Base、Arbitrum、Polygon、HashKey Chain 等鏈上即時買賣加密貨幣。深受數百萬用戶信賴的 DeFi 平台。", "title.tradeTokens": "交易代幣並提供流動資產。即時價格、圖表、交易資料和其他。", - "title.uniswapTradeCrypto": "Uniswap | 在 DeFi 領先交易所交易加密貨幣 ", + "title.uniswapTradeCrypto": "HSKSwap | 在 DeFi 領先交易所交易加密貨幣", "title.uniToken": "UNI 代幣代表 Uniswap 治理中的投票份額。你可以自己對每個提案進行投票,也可以將你的投票委託給第三方。", "title.voteOnGov": "對 Uniswap 治理提案進行投票", "token.balances.main": "你的餘額", diff --git a/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap b/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap index 0b525bc03cb..0c5c1a632cf 100644 --- a/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap +++ b/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap @@ -341,7 +341,7 @@ exports[`renders wallet preview card 1`] = ` "borderWidth": 0, }, { - "color": "#FF37C7", + "color": "#4177e2", "height": 20, "width": 20, }, @@ -356,7 +356,7 @@ exports[`renders wallet preview card 1`] = ` vbWidth={48} > { + // Wait for the transaction to be submitted before returning + // This ensures onSuccess is only called after the transaction is actually sent to the wallet + try { + await submitPromise + if (process.env.NODE_ENV === 'development') { + logger.debug('TransactionService', 'submitTransaction', 'Transaction submitted successfully:', transactionHash) + } + } catch (error) { logger.error(error, { tags: { file: 'TransactionService', function: 'submitTransaction' }, - extra: { context: 'Background submission failed' }, + extra: { context: 'Transaction submission failed' }, }) - }) + throw error + } - // Return the hash immediately + // Return the hash after transaction is submitted return { transactionHash } } diff --git a/packages/wallet/src/features/transactions/executeTransaction/services/TransactionSignerService/transactionSignerServiceImpl.ts b/packages/wallet/src/features/transactions/executeTransaction/services/TransactionSignerService/transactionSignerServiceImpl.ts index 78bcce0c630..408789d9ace 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/services/TransactionSignerService/transactionSignerServiceImpl.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/services/TransactionSignerService/transactionSignerServiceImpl.ts @@ -56,7 +56,17 @@ export function createTransactionSignerService(ctx: { const sendTransaction: TransactionSigner['sendTransaction'] = async (input) => { const provider = await ctx.getProvider() + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] Sending signed transaction to provider:', { + hasSignedTx: !!input.signedTx, + signedTxLength: input.signedTx?.length, + providerType: provider.constructor.name, + }) + } const transactionResponse = await provider.sendTransaction(input.signedTx) + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] Transaction sent, received hash:', transactionResponse.hash) + } return transactionResponse.hash } diff --git a/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts b/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts index d1b6273433d..c2897dce9e2 100644 --- a/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts @@ -170,10 +170,35 @@ function* executeTransactionStep(params: { } // Execute async (either because sync is not enabled or sync failed) + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] Executing transaction step asynchronously:', { + stepType: step.type, + chainId, + }) + } const asyncResult = yield* executor.executeStep(step) if (!asyncResult.success) { + const error = asyncResult.error instanceof Error + ? asyncResult.error + : new Error(asyncResult.error ? String(asyncResult.error) : 'Transaction failed') + if (process.env.NODE_ENV === 'development') { + console.error('[Swap] Error: Transaction step execution failed', { + error: error.message, + errorDetails: error, + stepType: step.type, + chainId, + }) + } yield* call(onFailure) - throw new Error('Transaction failed') + throw error + } + + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] Transaction step executed successfully:', { + stepType: step.type, + hash: asyncResult.hash, + chainId, + }) } return undefined // Async execution doesn't return a sync result @@ -326,9 +351,8 @@ export function createExecuteSwapSaga( if (isUniswapXPreSignedSwapTransaction(preSignedTransaction) || swapTxHasDelayedSubmission) { yield* call(onPending) - } else { - yield* call(onSuccess) } + // Note: onSuccess will be called after transaction is successfully submitted const gasFeeEstimation = swapTxContext.gasFeeEstimation @@ -437,6 +461,14 @@ export function createExecuteSwapSaga( ) } + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] About to execute swap transaction step:', { + stepType: swapStep.type, + hasParams: !!swapStep.params, + chainId, + }) + } + swapResult = yield* executeTransactionStep({ executor, step: swapStep, @@ -444,6 +476,14 @@ export function createExecuteSwapSaga( logger: dependencies.logger, onFailure: params.onFailure, }) + + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] Swap transaction step executed:', { + hasResult: !!swapResult, + resultType: swapResult ? 'sync' : 'async', + chainId, + }) + } } if (swapResult) { @@ -460,15 +500,22 @@ export function createExecuteSwapSaga( } } - // Call onSuccess now if it wasn't called earlier due to transaction spacing - if (swapTxHasDelayedSubmission) { + // Call onSuccess after transaction is successfully submitted + // For delayed submission, onSuccess will be called after all transactions are submitted yield* call(onSuccess) - } } catch (error) { dependencies.logger.error(error, { tags: { file: 'executeSwapSaga', function: 'executeSwap' }, extra: { analytics: params.analytics }, }) + if (process.env.NODE_ENV === 'development') { + console.error('[Swap] Error: executeSwapSaga failed', { + error: error instanceof Error ? error.message : String(error), + }) + } + // Call onFailure with the error so it can be displayed to the user + const swapError = error instanceof Error ? error : new Error(String(error)) + yield* call(params.onFailure, swapError) } } } diff --git a/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts b/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts index 698d06c190a..20837dc5dde 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts @@ -39,6 +39,43 @@ export function useSwapHandlers(): SwapHandlers { const execute: ExecuteSwapCallback = useCallback( async (params: ExecuteSwapParams) => { + if (process.env.NODE_ENV === 'development') { + console.log('[execute] ExecuteSwapCallback called with params:', params) + console.log('[execute] ExecuteSwapCallback - Detailed swapTxContext:', { + routing: params.swapTxContext?.routing, + hasTxRequests: !!params.swapTxContext?.txRequests, + txRequestCount: params.swapTxContext?.txRequests?.length || 0, + txRequests: params.swapTxContext?.txRequests?.map((tx, idx) => ({ + index: idx, + to: tx.to, + data: tx.data?.substring(0, 20) + '...', + value: tx.value?.toString(), + gasLimit: tx.gasLimit?.toString(), + gasPrice: tx.gasPrice?.toString(), + chainId: tx.chainId, + })), + hasTrade: !!params.swapTxContext?.trade, + trade: params.swapTxContext?.trade ? { + routing: params.swapTxContext.trade.routing, + inputAmount: params.swapTxContext.trade.inputAmount?.toExact(), + outputAmount: params.swapTxContext.trade.outputAmount?.toExact(), + deadline: params.swapTxContext.trade.deadline, + deadlineDate: params.swapTxContext.trade.deadline ? new Date(params.swapTxContext.trade.deadline * 1000).toLocaleString('zh-CN') : undefined, + } : undefined, + includesDelegation: params.swapTxContext?.includesDelegation, + hasSwapRequestArgs: 'swapRequestArgs' in (params.swapTxContext || {}), + swapRequestArgs: params.swapTxContext?.swapRequestArgs ? { + deadline: params.swapTxContext.swapRequestArgs.deadline, + deadlineDate: params.swapTxContext.swapRequestArgs.deadline ? new Date(params.swapTxContext.swapRequestArgs.deadline * 1000).toLocaleString('zh-CN') : undefined, + hasQuote: !!params.swapTxContext.swapRequestArgs.quote, + simulateTransaction: params.swapTxContext.swapRequestArgs.simulateTransaction, + allKeys: Object.keys(params.swapTxContext.swapRequestArgs), + fullSwapRequestArgs: params.swapTxContext.swapRequestArgs, + } : 'swapRequestArgs is undefined', + swapTxContextKeys: params.swapTxContext ? Object.keys(params.swapTxContext) : [], + }) + } + // Mark execution as called to prevent future prepareAndSign calls signing.markExecutionCalled() diff --git a/packages/wallet/src/features/transactions/swap/services/transactionExecutor.ts b/packages/wallet/src/features/transactions/swap/services/transactionExecutor.ts index fd04a21c66d..8149fe65ad9 100644 --- a/packages/wallet/src/features/transactions/swap/services/transactionExecutor.ts +++ b/packages/wallet/src/features/transactions/swap/services/transactionExecutor.ts @@ -39,9 +39,22 @@ export function createTransactionExecutor(transactionService: TransactionService try { const { params, shouldWait } = step + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] executeStep: Calling submitTransaction:', { + stepType: step.type, + hasParams: !!params, + hasRequest: !!params?.request, + hasSignedRequest: !!params?.request?.signedRequest, + }) + } + const result = yield* call(transactionService.submitTransaction, params) const hash = result.transactionHash + if (process.env.NODE_ENV === 'development') { + console.log('[Swap] executeStep: Transaction submitted, hash:', hash) + } + // Handle transaction spacing if required if (shouldWait) { const { success } = yield* call(waitForTransactionConfirmation, { hash }) @@ -57,6 +70,12 @@ export function createTransactionExecutor(transactionService: TransactionService tags: { file: 'transactionExecutor', function: 'executeStep' }, extra: { stepType: step.type }, }) + if (process.env.NODE_ENV === 'development') { + console.error('[Swap] Error: executeStep failed:', { + error: error instanceof Error ? error.message : String(error), + stepType: step.type, + }) + } return { error, success: false } } } diff --git a/prd.md b/prd.md new file mode 100644 index 00000000000..acff4734d9c --- /dev/null +++ b/prd.md @@ -0,0 +1,769 @@ +Uniswap V3 前端配池流程与调试指南 (全范围模式) +文档版本: 2.1 +适用场景: HashKey Chain (Mainnet & Testnet) V3 流动性添加,强制用户进行全范围 (Full Range) 流动性添加 +核心合约: NonfungiblePositionManager +实施状态: ✅ 已完成 + +## 概述 +本文档描述 HashKey Chain 上 Uniswap V3 流动性添加的特殊实现。为简化用户体验和降低风险,HashKey Chain 上的 V3 流动性添加**强制使用全范围模式**,隐藏价格区间选择功能。 + uniswap sdk 不支持 hashkey chain,不要使用 isBackendSupportedChainId 这类型错误!!! +### 适用链 +- **HashKey Chain Mainnet** (Chain ID: 177) +- **HashKey Chain Testnet** (Chain ID: 133) + + +Trading API 授权检查 本项目不支持, +1. useTokenAllowance - 基础链上授权检查 +位置:apps/web/src/hooks/useTokenAllowance.ts +功能: +使用 useReadContract 直接查询链上 ERC20 合约的 allowance 方法 +不依赖任何 API,纯链上查询 +支持自动刷新(当授权交易确认后) +export function useTokenAllowance({ token, owner, spender }: { token?: Token owner?: string // 用户地址 spender?: string // 授权给谁(比如 Position Manager)}): { tokenAllowance?: CurrencyAmount isSyncing: boolean} +2. usePermit2Allowance - Permit2 授权检查 +位置:apps/web/src/hooks/usePermit2Allowance.ts +功能: +检查 Permit2 合约的授权 +内部使用 useTokenAllowance 检查基础 ERC20 授权 +3. getApproveInfo - Gas 估算中的授权检查 +位置:apps/web/src/state/routing/gas.ts +功能: +使用合约的 callStatic.allowance 方法检查授权 +用于估算授权交易的 gas 费用 + +### 核心特性 +1. **仅支持 Uniswap V3**(不支持 V4) + - HashKey Chain 上的流动性添加功能**仅支持 V3 协议** + - V4 协议相关代码已从 HashKey Chain 支持中移除 + - 所有 V4 相关的 hooks、配置和逻辑都不适用于 HashKey Chain +2. 自动强制全范围流动性模式 +3. 隐藏价格区间选择 UI +4. 新建池子时需要用户输入初始价格 +5. 支持所有 V3 费率等级 (0.01%, 0.05%, 0.3%, 1%) +6. **默认费率等级:0.3%(最常用,适合主流代币对)** +7. **链上交易构建**:对于 HashKey Chain,不使用 Trading API,直接在链上构建交易 + - 使用 `NonfungiblePositionManager.multicall` 方法 + - 包含 `createAndInitializePoolIfNecessary` 和 `mint` 两个步骤 + +### 关键技术说明 + +#### 1. SDK 使用情况(重要 - 必读) + +⚠️ **关键信息**:本项目**正在迁移**到 HashKey 自定义 SDK + +**当前状态**: +- **目标 SDK**:`@hkdex-tmp/universal_router_sdk` (1.0.3) - HashKey 团队维护的自定义 SDK +- **当前状态**:部分功能还在使用官方 SDK,**正在逐步替换中** +- **原因**:官方 SDK 不支持 HashKey Chain,需要使用自定义版本 + +**已安装的 SDK 包**: + +**🔴 HashKey 自定义 SDK(核心 - 必须使用):** +- **@hkdex-tmp/universal_router_sdk**: 1.0.3 +- **用途**:应该用于**所有功能**(Swap、流动性添加、路由、价格计算等) +- **优先级**:⭐⭐⭐⭐⭐ **最高优先级** +- **原因**: + - HashKey 团队专门为 HashKey Chain 定制和维护 + - 包含 HashKey Chain 的所有合约地址 + - 已修复官方 SDK 在 HashKey Chain 上的兼容性问题 + - 针对 HashKey Chain 的特殊需求优化 + +**官方 Uniswap SDK(临时使用 - 计划替换):** +- **@uniswap/sdk-core**: 7.9.0 - 核心类型(Token、Currency) +- **@uniswap/v3-sdk**: 3.25.2 - V3 逻辑(Pool、Position、Tick) +- **@uniswap/v2-sdk**: 4.15.2 +- **@uniswap/v4-sdk**: 1.21.2 +- **@uniswap/router-sdk**: 2.0.2 - V3SwapRouter +- **状态**:部分功能还在使用,**正在逐步替换为 @hkdex-tmp/universal_router_sdk** + +**当前迁移状态**: +| 功能 | 当前使用的 SDK | 目标 SDK | 状态 | 说明 | +|------|--------------|---------|------|------| +| Swap 交易 | `@hkdex-tmp/universal_router_sdk` | `@hkdex-tmp/universal_router_sdk` | ✅ 已完成 | - | +| V3 流动性添加 | `@uniswap/v3-sdk` | `@hkdex-tmp/universal_router_sdk` | ⏳ 待迁移 | 还没来得及更换 | +| Pool 计算 | `@uniswap/v3-sdk` | `@hkdex-tmp/universal_router_sdk` | ⏳ 待迁移 | 还没来得及更换 | +| 价格计算 | `@uniswap/v3-sdk` | `@hkdex-tmp/universal_router_sdk` | ⏳ 待迁移 | 还没来得及更换 | +| 合约地址 | 手动配置 `v3Addresses.ts` | `@hkdex-tmp/universal_router_sdk` | ⏳ 待迁移 | 还没来得及更换 | + +**🔴 核心开发原则(必须遵守)**: + +**规则 1:遇到问题时,第一反应是检查 `@hkdex-tmp/universal_router_sdk`** +- 官方 SDK 报错?→ 检查 `@hkdex-tmp/universal_router_sdk` +- 缺少合约地址?→ 检查 `@hkdex-tmp/universal_router_sdk` +- 功能不支持?→ 检查 `@hkdex-tmp/universal_router_sdk` +- 计算结果异常?→ 检查 `@hkdex-tmp/universal_router_sdk` + +**规则 2:SDK 选择优先级** +``` +1️⃣ @hkdex-tmp/universal_router_sdk (1.0.3) ⭐ 最高优先级 + ↓ 如果确认该 SDK 没有所需功能 +2️⃣ 官方 @uniswap/*-sdk(临时方案) + ↓ 如果都不行 +3️⃣ 自行实现 +``` + +**规则 3:不要假设官方 SDK 可用** +- ❌ 错误:直接使用 `@uniswap/v3-sdk` 认为它支持 HashKey Chain +- ✅ 正确:先检查 `@hkdex-tmp/universal_router_sdk` 是否有对应功能 + +**当前项目状态**: +- ✅ `@hkdex-tmp/universal_router_sdk` 已安装在项目中 +- ⏳ 正在逐步迁移,还有很多功能使用官方 SDK +- 📝 本次流动性添加实现使用了官方 SDK(临时方案,后续需迁移) + +#### 2. HashKey Chain V3 合约部署 + +HashKey Chain 上部署了**自己的 Uniswap V3 合约克隆**,合约地址与官方 Ethereum 部署不同: + +**Testnet (Chain ID: 133) 和 Mainnet (Chain ID: 177) 合约地址:** +- **V3 Factory**: `0x2dC2c21D1049F786C535bF9d45F999dB5474f3A0` +- **NonfungiblePositionManager**: `0x3c8816a838966b8b0927546A1630113F612B1553` ⭐ **核心合约** +- **SwapRouter02**: `0x46cBccE3c74E95d1761435d52B0b9Abc9e2FEAC0` +- **QuoterV2**: `0x9576241e23629cF8ad3d8ad7b12993935b24fA9d` +- **Multicall2**: `0x47F625Ec29637445AA1570d7008Cf78692CdA096` +- **TickLens**: `0x73942976823088508a2C6c8055DF71107DB1d8db` +- **V3Migrator**: `0x0bb37eD33c163c46DEef0F6D14d262D0bc57B130` +- **V3Staker**: `0xF5A3fD7A48c574cB07fE79f679bb4DcC6EcA1205` +- **NFT Descriptor Library**: `0x04618B09C4bfa69768D07bA7479c19F40Aed06Ac` +- **NFT Descriptor**: `0x6EF5d83eC912C12F1b1c5ACBD6C565120aB6EC5c` +- **Descriptor Proxy**: `0x47438E3ee7B305fC7fd0e2cC3633002e65fFeaec` + +**说明**: +- 这些合约是 Uniswap V3 的标准部署克隆,但地址不同于官方 Ethereum 部署 +- 使用官方 SDK (@uniswap/v3-sdk) 可以与这些合约交互 +- **需要在代码中手动配置这些地址**(官方 SDK 默认不包含 HashKey Chain) +- 配置位置:`packages/uniswap/src/constants/v3Addresses.ts` +- 如果官方 SDK 不支持某些功能,检查 `@hkdex-tmp/universal_router_sdk` 是否提供 + +#### 3. 后端 API 支持情况 + +**关键问题**:Uniswap 官方后端不支持 HashKey Chain + +**表现**: +- `backendSupported: false` (在 chainInfo 配置中) +- REST API 查询池子信息返回 **404 错误** +- GraphQL API 不认识 HashKey Chain +- Trading API 不支持 HashKey Chain 的报价 + +**影响范围**: +1. **池子查询**:`useGetPoolsByTokens` 返回 404 +2. **价格数据**:无法获取历史价格和图表数据 +3. **TVL 数据**:无法显示池子的总锁仓量 +4. **交易路由**:Trading API 无法提供最优路由 + +**解决方案**: +- ✅ 使用本地 SDK 直接计算(不依赖后端) +- ✅ 检测 `backendSupported: false` 时,自动启用"创建新池子"模式 +- ✅ 使用链上 RPC 调用代替后端 API +- ⚠️ 缺少图表和历史数据(可接受的降级体验) + +**🔴 重要:自定义网关地址配置** + +本项目使用**自定义的 Uniswap Gateway DNS 地址**,而非官方默认地址: + +**环境变量配置**: +- **变量名**:`REACT_APP_UNISWAP_GATEWAY_DNS` +- **自定义地址**:`https://zy95c64c3c.execute-api.ap-southeast-1.amazonaws.com/prod/v2` +- **配置文件位置**:`apps/web/.env` + +**⚠️ 关键说明**: +- 这是**HashKey 团队自定义部署的网关服务**,专门为 HashKey Chain 优化 +- 与官方 Uniswap Gateway 不同,这是独立的 AWS API Gateway 部署 +- 该地址用于前端与后端服务的通信,包括池子查询、价格数据等 +- **不要使用官方默认地址**,必须使用此自定义地址 +- 如果修改此地址,需要确保新的网关服务支持 HashKey Chain 的相关功能 + +**配置示例**: +```bash +# apps/web/.env +REACT_APP_UNISWAP_GATEWAY_DNS=https://zy95c64c3c.execute-api.ap-southeast-1.amazonaws.com/prod/v2 +``` + +#### 4. 初始价格设置的关键问题 + +**问题现象**: +- 初始价格输入框没有显示 +- 用户无法设置新池子的初始价格 +- 导致数量计算异常(如 100 TT1 = 0.000000000000004799 WHSK) + +**根本原因**: +```typescript +// useDerivedPositionInfo.tsx +const creatingPoolOrPair = poolDataIsFetched && !poolOrPair +``` + +**问题分析**: +- `poolDataIsFetched`: 依赖后端 API 查询完成 +- 当后端返回 404 时,React Query 可能永远不会将 `isFetched` 设为 true +- 或者查询被禁用(`enabled: false`),导致 `poolDataIsFetched = false` +- 最终 `creatingPoolOrPair = false`,导致 `` 不显示 + +**问题定位**: +- 文件:`apps/web/src/components/Liquidity/Create/hooks/useDerivedPositionInfo.tsx` +- 第 299 行:`const creatingPoolOrPair = poolDataIsFetched && !poolOrPair` +- 当后端返回 404 时,`poolDataIsFetched` 可能为 `false`,导致 `creatingPoolOrPair = false` +- 结果:`` 组件不渲染 + +**需要修复**: +- ⚠️ **待确认正确的修复方案** +- 需要处理 HashKey Chain 后端不支持的情况 +- 确保初始价格输入框能正确显示 +- 修复时需要考虑: + 1. 如何检测后端不支持的情况 + 2. 如何正确设置 `creatingPoolOrPair` 标志 + 3. 不要破坏现有逻辑 + +#### 5. 当前实现的技术债务与后续优化 + +**⚠️ 重要提醒**:本次流动性添加功能使用了**临时技术方案** + +**临时方案详情**: +- 使用官方 `@uniswap/v3-sdk` 进行 Pool 计算、价格计算、Tick 处理 +- 使用官方 `@uniswap/sdk-core` 提供基础类型 +- 手动配置 HashKey Chain 的 V3 合约地址(`v3Addresses.ts`) +- 手动处理后端不支持的情况(`backendSupported: false`) + +**为什么使用临时方案**: +- ⏰ 时间紧急,还没来得及完全迁移到 `@hkdex-tmp/universal_router_sdk` +- ✅ 官方 SDK 的核心计算逻辑是通用的,可以工作 +- ⚠️ 但需要手动配置很多 HashKey Chain 特定的参数 + +**技术债务清单**: +1. [ ] **初始价格输入框不显示**:需要修复 `creatingPoolOrPair` 逻辑 +2. [ ] **合约地址配置**:应该从 `@hkdex-tmp/universal_router_sdk` 获取,而非手动配置 +3. [ ] **Pool 计算逻辑**:检查自定义 SDK 是否有优化版本 +4. [ ] **价格计算**:检查是否有 HashKey Chain 特定的处理 +5. [ ] **后端 fallback**:自定义 SDK 可能已经处理了后端不支持的情况 + +**后续优化步骤**: +1. 检查 `@hkdex-tmp/universal_router_sdk` 的完整 API 和类型定义 +2. 确认是否包含流动性相关的功能和合约地址 +3. 逐步替换官方 SDK 的使用 +4. 移除手动配置(如果 SDK 已包含) +5. 全面测试确保兼容性 + +**开发检查清单(每次实现新功能时)**: +- [ ] ⭐ 第一步:搜索 `@hkdex-tmp/universal_router_sdk` 的源码 +- [ ] 检查该 SDK 的 TypeScript 类型定义和导出 +- [ ] 如果没有所需功能,再考虑官方 SDK +- [ ] 记录选择的 SDK 和原因 +- [ ] 标记是否为技术债务(需要后续优化) + +## 实施细节 + +### 1. 代码修改文件 + +#### 1.1 `/apps/web/src/state/mint/v3/utils.ts` +添加全范围模式相关工具函数: +- `FULL_RANGE_TICKS`: 各费率等级的全范围 Tick 常量 +- `getFullRangeConfig(feeTier)`: 获取特定费率的全范围配置 +- `sortTokens(tokenA, tokenB)`: Token 地址排序 +- `isFullRangeModeChain(chainId)`: 判断链是否需要强制全范围模式 + +#### 1.2 `/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx` +修改价格区间选择组件: +- 检测 HashKey Chain,自动启用全范围模式 +- 隐藏全范围/自定义范围切换控件 +- 隐藏价格区间图表和输入框 +- 保留初始价格输入(新建池子时) + +### 2. 核心流程图解 +在开始写代码前,请确保逻辑遵循以下数据流。这一步最容易出问题的就是 Token 排序 导致的 价格倒置。 + +```mermaid +graph TD + Start[用户输入: Token A, Token B, 费率 Fee, 初始价格 P] --> Sort{地址排序 check}; + + Sort -- Token A < Token B --> Normal[顺序正常: token0=A, token1=B]; + Sort -- Token A > Token B --> Flip[顺序颠倒: token0=B, token1=A]; + + Normal --> CalcPrice[使用价格 P 计算 sqrtPriceX96]; + Flip --> CalcPriceInvert[使用 1/P 计算 sqrtPriceX96]; + + CalcPrice --> Ticks[读取全范围 Ticks 常量]; + CalcPriceInvert --> Ticks; + + Ticks --> CalcAmount[根据 P 和 输入数量A, 自动计算数量B]; + + CalcAmount --> Slippage[计算滑点 amountMin (例如 95%)]; + + Slippage --> Construct[构造 Multicall 数据]; + Construct --> Tx[发送交易 -> PositionManager]; +``` +2. 关键数据准备 (Step-by-Step) +2.1 Token 排序 (最重要) +Uniswap V3 强制要求 token0 地址必须小于 token1。 + +TypeScript +const isTokenA0 = tokenA.address.toLowerCase() < tokenB.address.toLowerCase(); +const token0 = isTokenA0 ? tokenA : tokenB; +const token1 = isTokenA0 ? tokenB : tokenA; + +// 价格处理 +const realPrice = isTokenA0 ? userInputPrice : (1 / userInputPrice); +2.2 获取全范围 Ticks (Hardcoded) +不要在运行时动态计算,直接使用根据 tickSpacing 预计算好的“最大整数倍对齐值”,防止 Revert。 + +费率 (Fee Tier) Spacing Min Tick (tickLower) Max Tick (tickUpper) +0.01% (100) 1 -887272 887272 +0.05% (500) 10 -887270 887270 +0.3% (3000) 60 -887220 887220 +1% (10000) 200 -887200 887200 +2.3 初始价格编码 +使用 SDK 将人类可读的价格转换为链上格式。 + +TypeScript +import { encodeSqrtRatioX96 } from '@uniswap/v3-sdk'; + +// 注意:这里需要处理 Decimals 精度差 +// 建议使用 SDK 的 Price 对象或 JSBI 进行预处理 +const sqrtPriceX96 = encodeSqrtRatioX96(amount1, amount0); +3. 合约交互参数构建 +我们需要向 NonfungiblePositionManager 发送一个 multicall 交易,包含两步:初始化池子 和 添加流动性。 + +步骤 A: createAndInitializePoolIfNecessary +如果池子已存在,此步骤会自动跳过(不消耗 Gas),但这保证了你的交易总是安全的。 + +token0: token0.address + +token1: token1.address + +fee: 3000 (对应 0.3%) + +sqrtPriceX96: (上一步计算的值) + +步骤 B: mint (添加流动性) +token0: token0.address + +token1: token1.address + +fee: 3000 + +tickLower: (从 2.2 表格中获取的常量) + +tickUpper: (从 2.2 表格中获取的常量) + +amount0Desired: 用户输入的 token0 数量 + +amount1Desired: 用户输入的 token1 数量 (全范围模式下,必须两边都存) + +amount0Min: amount0Desired * 0.95 (5% 滑点保护,新建池建议放宽一点) + +amount1Min: amount1Desired * 0.95 + +recipient: 用户钱包地址 + +deadline: Math.floor(Date.now() / 1000) + 60 * 20 + +4. 调试与排错清单 (Debugging Checklist) +如果你的交易失败 (Revert) 或模拟执行报错,请按以下顺序检查: + +🔴 错误 1: Transaction reverted: T / Tick +现象: 提示 Tick 无效或越界。 + +原因: 传入的 tickLower 或 tickUpper 不是 tickSpacing 的整数倍。 + +检查: 确认你是否正确读取了表格中的值。例如 0.3% 的池子,千万不要传 -887272,必须传 -887220。 + +🔴 错误 2: STF / TransferHelper: TRANSFER_FROM_FAILED +现象: 经典的转账失败。 + +原因: 用户没有授权 (Approve) 代币给 NonfungiblePositionManager。 + +检查: + +检查 Allowance 是否足够。 + +如果是原生代币 (ETH/BNB),需检查是否正确转换为了 WETH/WBNB (V3 Manager 只收 ERC20)。 + +检查用户钱包余额是否足够支付 amountDesired。 + +🔴 错误 3: 价格极其离谱 (如 1 ETH = 0.0005 USDC) +现象: 池子建成了,但价格是倒过来的。 + +原因: Token 没有排序。 + +检查: 打印 token0 和 token1 的地址。如果 token0 是 USDC (地址小) 而 token1 是 ETH (地址大),你的价格计算公式必须是 1 / 2000 而不是 2000。 + +🔴 错误 4: Gas Estimation Failed (Gas 预估失败) +原因 A: 池子虽然没显示,但在链上可能已经被别人建了(且价格和你设定的偏差巨大)。 + +原因 B: amountMin 设置得太高。对于新建池,如果计算精度有微小误差,过高的 min 会导致交易失败。调试时可先设为 0 试试。 + +🔴 错误 5: Trading API does not support creating LP positions on HashKey Chain +现象: 提示 Trading API 不支持 HashKey Chain。 + +原因: HashKey Chain 不支持 Trading API,需要使用链上交易构建。 + +解决方案: +- 代码已自动处理:对于 HashKey Chain,系统会自动在链上构建交易 +- 使用 `NonfungiblePositionManager.multicall` 方法 +- 包含 `createAndInitializePoolIfNecessary` 和 `mint` 两个步骤 +- 确保协议版本是 V3(不是 V4) + +🔴 错误 6: HashKey Chain only supports V3 protocol +现象: 提示 HashKey Chain 只支持 V3 协议。 + +原因: 尝试使用 V4 协议创建流动性,但 HashKey Chain 不支持 V4。 + +解决方案: +- 确保 `protocolVersion` 是 `ProtocolVersion.V3` +- 检查 `positionState.protocolVersion` 是否正确设置为 V3 +- 移除所有 V4 相关的配置和代码 + +--- + +## 7. HashKey Chain 链上交易构建实现 + +### 7.1 概述 + +对于 HashKey Chain,由于 Trading API 不支持,我们直接在链上构建交易,而不是调用 Trading API。 + +### 7.2 实现位置 + +**核心文件:** +- `/packages/uniswap/src/features/transactions/liquidity/steps/increasePosition.ts` + - `createCreatePositionAsyncStep` 函数 + - 检测 HashKey Chain + - 构建链上交易 + +**调用位置:** +- `/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx` + - `generateCreatePositionTxRequest` 函数 + - 禁用 Trading API 查询 + - 传递 `createPositionRequestArgs` 给异步步骤 + +### 7.3 交易构建流程 + +1. **检测 HashKey Chain** + ```typescript + const chainId = createPositionRequestArgs.chainId as number + const isHashKeyChain = chainId === UniverseChainId.HashKey || chainId === UniverseChainId.HashKeyTestnet + ``` + +2. **验证协议版本** + ```typescript + const protocol = createPositionRequestArgs.protocol + if (protocol !== TradingApi.ProtocolItems.V3) { + throw new Error(`HashKey Chain only supports V3 protocol, got ${protocol}`) + } + ``` + +3. **获取 Position Manager 地址** + ```typescript + const positionManagerAddress = getV3PositionManagerAddress(chainId) + ``` + +4. **构建 multicall 数据** + ```typescript + const multicallData: string[] = [] + + // 步骤 1: 创建并初始化池子(如果需要) + if (initialPrice) { + multicallData.push( + NFPMInterface.encodeFunctionData('createAndInitializePoolIfNecessary', [ + token0, + token1, + fee, + initialPrice, // sqrtPriceX96 + ]) + ) + } + + // 步骤 2: 添加流动性 + multicallData.push( + NFPMInterface.encodeFunctionData('mint', [ + { + token0, + token1, + fee, + tickLower, + tickUpper, + amount0Desired, + amount1Desired, + amount0Min, + amount1Min, + recipient: walletAddress, + deadline, + }, + ]) + ) + ``` + +5. **构建交易请求** + ```typescript + const txRequest: ValidatedTransactionRequest = { + to: positionManagerAddress, + data: NFPMInterface.encodeFunctionData('multicall', [multicallData]), + value: '0x0', + chainId, + } + ``` + +### 7.4 关键参数说明 + +- **token0, token1**: 代币地址(已排序,token0 < token1) +- **fee**: 费率等级(如 500 表示 0.05%,3000 表示 0.3%) +- **initialPrice**: 初始价格(sqrtPriceX96 格式),仅在创建新池子时需要 +- **tickLower, tickUpper**: 价格区间(全范围模式下使用预定义的常量值) +- **amount0Desired, amount1Desired**: 期望的代币数量 +- **amount0Min, amount1Min**: 最小代币数量(考虑滑点保护) +- **recipient**: 接收 NFT 的地址(用户钱包地址) +- **deadline**: 交易截止时间(Unix 时间戳,通常设置为当前时间 + 20 分钟) + +### 7.5 与 Trading API 的区别 + +| 特性 | Trading API | HashKey Chain 链上构建 | +|------|------------|----------------------| +| 协议支持 | V2, V3, V4 | 仅 V3 | +| 交易构建 | 后端 API | 前端链上构建 | +| 依赖 | Trading API 服务 | 仅需链上合约 | +| 授权检查 | Trading API | 链上检查(`useOnChainLpApproval`)| +| 错误处理 | API 错误消息 | 链上交易错误 | + +5. 工具函数 (Utils) +复制此代码块到你的项目中: + +TypeScript +import { FeeAmount } from '@uniswap/v3-sdk' + +// 全范围 Tick 常量表 +export const FULL_RANGE_TICKS = { + [FeeAmount.LOWEST]: { min: -887272, max: 887272 }, // 0.01% + [FeeAmount.LOW]: { min: -887270, max: 887270 }, // 0.05% + [FeeAmount.MEDIUM]: { min: -887220, max: 887220 }, // 0.3% + [FeeAmount.HIGH]: { min: -887200, max: 887200 }, // 1% +} + +/** + * 获取全范围配置 + * @param feeTier 费率枚举值 (e.g. 3000) + */ +export function getFullRangeConfig(feeTier: FeeAmount) { + const config = FULL_RANGE_TICKS[feeTier]; + if (!config) { + throw new Error(`Unsupported fee tier: ${feeTier}`); + } + return config; +} + +/** + * 简单的 Token 排序检查 + */ +export function sortTokens(tokenA: string, tokenB: string) { + return tokenA.toLowerCase() < tokenB.toLowerCase() + ? [tokenA, tokenB] + : [tokenB, tokenA]; +} + +6. 实施完成说明 + +本 PRD 已完成代码实施,具体修改如下: + +6.1 修改的文件 + +**核心功能文件:** + +1. `/apps/web/src/state/mint/v3/utils.ts` + - ✅ 添加 FULL_RANGE_TICKS 常量(支持所有费率等级) + - ✅ 添加 getFullRangeConfig() 工具函数 + - ✅ 添加 sortTokens() Token 地址排序函数 + - ✅ 添加 isFullRangeModeChain() 检测 HashKey Chain 的函数 + +2. `/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx` + - ✅ 检测当前链是否为 HashKey Chain (ID: 133 或 177) + - ✅ 自动强制启用全范围模式(设置 fullRange: true) + - ✅ 隐藏"Set Range"标题和说明 + - ✅ 隐藏全范围/自定义范围切换控件(SegmentedControl) + - ✅ 隐藏价格区间图表(LiquidityRangeInput / D3LiquidityRangeInput) + - ✅ 隐藏价格区间输入框(RangeAmountInput) + - ✅ 保留初始价格输入(新建池子时必需) + +3. `/apps/web/src/components/Liquidity/Create/hooks/useLiquidityUrlState.ts` + - ✅ 修改 `currencyA` parser 的默认值 + - ✅ 从空字符串 `''` 改为 `NATIVE_CHAIN_ID` + - ✅ 当用户访问 `/positions/create/v3` 时 + - ✅ URL 自动添加 `?currencyA=NATIVE` + - ✅ HSK 自动被选中为 Token A + +4. `/apps/web/src/pages/CreatePosition/CreatePosition.tsx` + - ✅ 添加 fallback 逻辑确保 tokenA 有值 + - ✅ 使用 `initialInputs.tokenA ?? initialInputs.defaultInitialToken` + - ✅ 监听 initialInputs 变化并更新 currencyInputs + - ✅ 确保 HSK 始终作为默认 Token A 显示 + +**默认链配置文件:** + +5. `/packages/uniswap/src/features/chains/utils.ts` + - ✅ 修改 `getDefaultChainId()` 函数 + - ✅ 测试模式默认链:HashKeyTestnet (133) + - ✅ 正式模式默认链:HashKey (177) + - ✅ 不再使用 Ethereum 或 Sepolia 作为默认链 + +**Token 配置文件:** + +6. `/packages/uniswap/src/constants/tokens.ts` + - ✅ 添加 HashKey Chain 和 HashKey Testnet 的导入 + - ✅ 在 `WRAPPED_NATIVE_CURRENCY` 中添加 WHSK 配置 + - ✅ HashKey Mainnet (177): WHSK at `0xCA8aAceEC5Db1e91B9Ed3a344bA026c4a2B3ebF6` + - ✅ HashKey Testnet (133): WHSK at `0xCA8aAceEC5Db1e91B9Ed3a344bA026c4a2B3ebF6` + - ✅ 解决 "Unsupported chain ID" 错误 + +7. `/apps/web/src/components/Liquidity/Create/types.ts` & `useLiquidityUrlState.ts` + - ✅ **设置默认费率等级为 0.3%(MEDIUM)** + - ✅ 修改 `DEFAULT_POSITION_STATE.fee` 从 `undefined` 为 `DEFAULT_FEE_DATA` + - ✅ 在 `useLiquidityUrlState` 中返回 `fee ?? DEFAULT_FEE_DATA` + - ✅ 提升用户体验:用户无需手动选择费率即可继续 + - ✅ 0.3% 是 Uniswap V3 最常用的费率,适合大多数代币对 + +8. `/packages/uniswap/src/features/chains/evm/info/hashkey.ts` + - ✅ **禁用 V4 支持**:设置 `supportsV4: false` + - ✅ HashKey Chain 仅支持 V3,不支持 V4 + - ✅ Mainnet 和 Testnet 都已更新 + +9. `/apps/web/src/components/Liquidity/DepositInputForm.tsx` + - ✅ **修复自定义代币显示问题** + - ✅ 手动构造 `CurrencyInfo` 对象,不依赖后端 API + - ✅ 使用 `currencyId()` 函数正确处理代币地址 + - ✅ 解决 "Select token" 按钮问题 + +10. `/apps/web/src/components/Liquidity/utils/getPoolIdOrAddressFromCreatePositionInfo.ts` + - ✅ **添加防御性检查** + - ✅ 当 Factory 地址未配置时返回 undefined + - ✅ 避免创建新池子时的地址错误 + - ✅ 使用 `getV3FactoryAddress()` 支持自定义链 + +11. `/packages/uniswap/src/constants/v3Addresses.ts` **(新文件)** + - ✅ **配置 HashKey Chain 的 V3 合约地址** + - ✅ V3 Factory: `0x2dC2c21D1049F786C535bF9d45F999dB5474f3A0` + - ✅ NonfungiblePositionManager: `0x3c8816a838966b8b0927546A1630113F612B1553` + - ✅ SwapRouter02: `0x46cBccE3c74E95d1761435d52B0b9Abc9e2FEAC0` + - ✅ QuoterV2: `0x9576241e23629cF8ad3d8ad7b12993935b24fA9d` + - ✅ Multicall2: `0x47F625Ec29637445AA1570d7008Cf78692CdA096` + - ✅ 支持 Mainnet (177) 和 Testnet (133) + +12. `/apps/web/src/pages/CreatePosition/CreatePositionTxContext.tsx` + - ✅ **修复 V3/V4 hooks 字段问题** + - ✅ 仅在 V4 时添加 hooks 字段 + - ✅ V3 不支持 hooks,移除该字段避免 API 错误 + - ✅ 添加 fee 必填验证,确保不会传递 undefined + - ✅ **移除 V4 支持**:HashKey Chain 仅支持 V3,所有 V4 相关代码已移除 + - ✅ **过滤 V4Pool**:从 `poolOrPair` 中过滤掉 V4Pool,只保留 V3Pool 或 Pair + - ✅ **禁用 Trading API 查询**:对于 HashKey Chain,禁用 `useCreateLpPositionCalldataQuery` + - ✅ **支持链上交易构建**:当 `txRequest` 为 undefined 时,使用异步步骤在链上构建交易 + +13. `/packages/uniswap/src/features/transactions/liquidity/steps/increasePosition.ts` + - ✅ **添加 HashKey Chain 链上交易构建支持** + - ✅ 检测 HashKey Chain,如果检测到则构建链上交易而非调用 Trading API + - ✅ **仅支持 V3 协议**:如果协议不是 V3,抛出错误 + - ✅ 使用 `NonfungiblePositionManager.multicall` 构建交易 + - ✅ 包含 `createAndInitializePoolIfNecessary`(如果需要创建池子) + - ✅ 包含 `mint`(添加流动性) + - ✅ 正确处理 `amount0Desired`、`amount1Desired`、`amount0Min`、`amount1Min` + - ✅ 计算 deadline(20 分钟) + +14. `/apps/web/src/components/Liquidity/Create/types.ts` + - ✅ **修改默认协议版本**:从 V4 改为 V3 + - ✅ 确保 HashKey Chain 默认使用 V3 + - ✅ 与链配置保持一致(HashKey Chain 不支持 V4) + +15. `/packages/uniswap/src/features/transactions/liquidity/utils.ts` + - ✅ **修复错误消息显示问题** + - ✅ 修复 "id: undefined" 错误消息 + - ✅ 只有当 `requestId` 存在时才在错误消息中包含 id + +**实现方式说明:** + +本实现采用**修改默认链配置**的方式,而非修改各个页面的链接。这样做的好处: +- ✅ 保持原有的链接形式(`/positions/create/v3`) +- ✅ 所有入口点自动生效,无需逐一修改 +- ✅ URL 参数自动带上 HashKey Chain 相关信息 +- ✅ 符合系统架构设计,集中管理默认配置 + +6.2 用户体验 + +在 HashKey Chain 上添加 V3 流动性时: +1. ✅ 用户选择 Token A 和 Token B(默认 Token A 为 HSK 原生代币) +2. ✅ 用户选择费率等级(**默认为 0.3%**,也可选择 0.01%, 0.05%, 1%) +3. ✅ 如果是新建池子,用户需要输入初始价格 +4. ✅ 系统自动使用全范围模式,无需用户选择价格区间 +5. ✅ 用户输入存款数量 +6. ✅ 确认并提交交易 + +6.3 技术要点 + +- 全范围 Tick 值已预先计算并硬编码,避免运行时计算错误 +- Token 自动按地址排序,确保 token0 < token1 +- 初始价格会根据 Token 排序自动调整(必要时取倒数) +- **HashKey Chain 仅支持 V3 协议**,不支持 V4 +- **链上交易构建**:对于 HashKey Chain,不使用 Trading API,直接在链上构建交易 + - 使用 `NonfungiblePositionManager.multicall` 方法 + - 包含 `createAndInitializePoolIfNecessary`(如果需要创建池子)和 `mint`(添加流动性)两个步骤 + - 正确处理滑点保护(slippage tolerance) + - 自动计算 deadline(20 分钟) +- **默认费率等级为 0.3%**,这是 Uniswap V3 中最常用且最平衡的费率选择 +- 用户仍可手动选择其他费率等级(0.01%, 0.05%, 1%),保留灵活性 +- **链上授权检查**:使用 `useOnChainLpApproval` hook 进行链上授权检查,不依赖 Trading API + +6.4 环境配置与默认链设置 + +**测试/开发环境:** +- 默认链:HashKey Testnet (Chain ID: 133) +- Testnet Mode 开启 + +**生产环境:** +- 默认链:HashKey Mainnet (Chain ID: 177) +- Testnet Mode 关闭 + +**其他链:** +- 不受影响,保持原有的价格区间选择功能 +- 用户可以手动切换到其他链 + +--- + +6.5 默认链配置实现 + +**核心修改:** + +在 `/packages/uniswap/src/features/chains/utils.ts` 中修改 `getDefaultChainId()` 函数: + +```typescript +function getDefaultChainId({ + platform, + isTestnetModeEnabled, +}: { + platform?: Platform + isTestnetModeEnabled: boolean +}): UniverseChainId { + if (platform === Platform.SVM) { + return UniverseChainId.Solana + } + + // 默认使用 HashKey Chain + // 开发/测试环境:HashKey Testnet (133) + // 生产环境:HashKey Mainnet (177) + return isTestnetModeEnabled ? UniverseChainId.HashKeyTestnet : UniverseChainId.HashKey +} +``` + +**生效范围:** + +所有使用 `useEnabledChains()` hook 的地方都会自动使用 HashKey Chain 作为默认链: +1. ✅ 导航栏 "Pool > Create Position" (`/positions/create/v3`) +2. ✅ Positions 页面的 "New" 按钮 +3. ✅ 空状态页面的 "New Position" 按钮 +4. ✅ 所有其他创建流动性的入口 +5. ✅ URL 自动生成正确的 chain 参数 +6. ✅ 默认选择 HSK 原生代币 + +**URL 效果:** + +用户访问 `/positions/create/v3` 时: +- 测试环境自动应用:`chain=hashkey_testnet`, `currencyA=NATIVE` +- 生产环境自动应用:`chain=hashkey`, `currencyA=NATIVE` + +**环境切换方式:** + +通过应用的 Testnet Mode 开关控制: +- Testnet Mode ON → HashKey Testnet (133) +- Testnet Mode OFF → HashKey Mainnet (177) \ No newline at end of file