diff --git a/README.md b/README.md index 3b1b3b63..50332bba 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,13 @@ The widget is responsive, so you can change the width and height in any way you - chain: default chain (eg `chain=ethereum`). This parameter is required - from: token to sell, to use the gas token for the chain use 0x0000000000000000000000000000000000000000 (eg `from=0x0000000000000000000000000000000000000000`) - to: token to buy, to use the gas token for the chain use 0x0000000000000000000000000000000000000000 (eg `to=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`) +- fromAmount: amount to sell (eg `fromAmount=0.1`). Only one of `fromAmount` or `toAmount` can be specified +- toAmount: amount to buy (eg `toAmount=400`). Only one of `fromAmount` or `toAmount` can be specified - background: color of the background (eg `background=rgb(10,20,30)`) -Note: only tokens that are part of our token lists are accepted in `from` and `to`, this is to prevent scammers linking to llamaswap with fake tokens loaded (eg a fake USDC) +**Important notes:** +- Only tokens that are part of our token lists are accepted in `from` and `to`, this is to prevent scammers linking to llamaswap with fake tokens loaded (eg a fake USDC) +- If both `fromAmount` and `toAmount` are provided, only `fromAmount` will be used and `toAmount` will be ignored #### API integration diff --git a/src/components/Aggregator/index.tsx b/src/components/Aggregator/index.tsx index e31b8f4e..78938baa 100644 --- a/src/components/Aggregator/index.tsx +++ b/src/components/Aggregator/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, Fragment, useEffect } from 'react'; +import { useRef, useState, Fragment, useEffect, useCallback } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useAccount, useSignTypedData, useCapabilities, useSwitchChain, useBytecode } from 'wagmi'; import { useAddRecentTransaction, useConnectModal } from '@rainbow-me/rainbowkit'; @@ -340,7 +340,56 @@ export function AggregatorContainer() { // swap input fields and selected aggregator states const [aggregator, setAggregator] = useState(null); const [isPrivacyEnabled, setIsPrivacyEnabled] = useLocalStorage('llamaswap-isprivacyenabled', false); - const [[amount, amountOut], setAmount] = useState<[number | string, number | string]>(['', '']); + const [[amount, amountOut], setAmountState] = useState<[number | string, number | string]>(['', '']); + + // Get router early since it's needed for URL updates + const router = useRouter(); + + // Wrapper to update both state and URL params + const setAmount = useCallback>>((newAmounts) => { + setAmountState(newAmounts); + + // Also update URL params when amounts change + if (typeof newAmounts === 'function') { + // Handle function updates + setAmountState((prev) => { + const updated = newAmounts(prev); + const [fromAmt, toAmt] = updated; + const query = { ...router.query }; + + // Clear both first + delete query.fromAmount; + delete query.toAmount; + + // Set the appropriate one + if (fromAmt && fromAmt !== '' && fromAmt !== '0') { + query.fromAmount = String(fromAmt); + } else if (toAmt && toAmt !== '' && toAmt !== '0') { + query.toAmount = String(toAmt); + } + + router.push({ pathname: router.pathname, query }, undefined, { shallow: true }); + return updated; + }); + } else { + // Handle direct value updates + const [fromAmt, toAmt] = newAmounts; + const query = { ...router.query }; + + // Clear both first + delete query.fromAmount; + delete query.toAmount; + + // Set the appropriate one + if (fromAmt && fromAmt !== '' && fromAmt !== '0') { + query.fromAmount = String(fromAmt); + } else if (toAmt && toAmt !== '' && toAmt !== '0') { + query.toAmount = String(toAmt); + } + + router.push({ pathname: router.pathname, query }, undefined, { shallow: true }); + } + }, [router]); const [slippage, setSlippage] = useLocalStorage('llamaswap-slippage', '0.3'); const [lastOutputValue, setLastOutputValue] = useState<{ aggregator: string; amount: number } | null>(null); @@ -366,9 +415,8 @@ export function AggregatorContainer() { // get selected chain and tokens from URL query params const routesRef = useRef(null); - const router = useRouter(); - const { toTokenAddress } = useQueryParams(); + const { toTokenAddress, fromAmount: fromAmountQuery, toAmount: toAmountQuery } = useQueryParams(); const { selectedChain, selectedToToken, finalSelectedFromToken, finalSelectedToToken } = useSelectedChainAndTokens(); const isValidSelectedChain = selectedChain && chainOnWallet ? selectedChain.id === chainOnWallet.id : false; const isOutputTrade = amountOut && amountOut !== ''; @@ -539,7 +587,7 @@ export function AggregatorContainer() { .push( { pathname: '/', - query: { ...router.query, chain: newChain.value, from: zeroAddress, to: undefined } + query: { ...router.query, chain: newChain.value, from: zeroAddress, to: undefined, fromAmount: '10', toAmount: undefined } }, undefined, { shallow: true } @@ -574,6 +622,15 @@ export function AggregatorContainer() { } }, [router?.query, savedTokens]); + // Initialize amounts from query parameters only + useEffect(() => { + if (fromAmountQuery) { + setAmountState([fromAmountQuery, '']); + } else if (toAmountQuery) { + setAmountState(['', toAmountQuery]); + } + }, [fromAmountQuery, toAmountQuery]); + useEffect(() => { if (selectedRoute?.amount && aggregator) { if ( @@ -1174,7 +1231,7 @@ export function AggregatorContainer() { router.push( { pathname: router.pathname, - query: { ...router.query, to: finalSelectedFromToken?.address, from: finalSelectedToToken?.address } + query: { ...router.query, to: finalSelectedFromToken?.address, from: finalSelectedToToken?.address, fromAmount: amountOut || undefined, toAmount: amount || undefined } }, undefined, { shallow: true } diff --git a/src/hooks/useQueryParams.tsx b/src/hooks/useQueryParams.tsx index 8561d9a5..ba12bf8b 100644 --- a/src/hooks/useQueryParams.tsx +++ b/src/hooks/useQueryParams.tsx @@ -14,12 +14,27 @@ export function useQueryParams() { const toToken = urlParams.get('to'); const fromToken = urlParams.get('from'); const chainOnURL = urlParams.get('chain'); + const fromAmountOnURL = urlParams.get('fromAmount'); + const toAmountOnURL = urlParams.get('toAmount'); const { ...query } = router.query; const chainName = typeof chainOnURL === 'string' ? chainOnURL.toLowerCase() : 'ethereum'; const fromTokenAddress = typeof fromToken === 'string' ? fromToken.toLowerCase() : null; const toTokenAddress = typeof toToken === 'string' ? toToken.toLowerCase() : null; + + // Only allow one amount parameter to be set - prioritize fromAmount if both exist + const fromAmount = fromAmountOnURL || null; + const toAmount = fromAmountOnURL ? null : toAmountOnURL || null; + + // Clean up URL if both amount params exist (keep only fromAmount) + useEffect(() => { + if (router.isReady && fromAmountOnURL && toAmountOnURL) { + const query = { ...router.query }; + delete query.toAmount; + router.push({ pathname: router.pathname, query }, undefined, { shallow: true }); + } + }, [router, fromAmountOnURL, toAmountOnURL]); useEffect(() => { if (router.isReady && !chainOnURL) { @@ -30,7 +45,7 @@ export function useQueryParams() { router.push( { pathname: '/', - query: { ...query, chain: chain.value, from: zeroAddress, tab: 'swap' } + query: { ...query, chain: chain.value, from: zeroAddress, fromAmount: "0.1", tab: 'swap' } }, undefined, { shallow: true } @@ -40,7 +55,7 @@ export function useQueryParams() { router.push( { pathname: '/', - query: { ...query, chain: 'ethereum', from: zeroAddress, tab: 'swap' } + query: { ...query, chain: 'ethereum', from: zeroAddress, fromAmount: "0.1", tab: 'swap' } }, undefined, { shallow: true } @@ -49,5 +64,5 @@ export function useQueryParams() { } }, [chainOnURL, chainOnWallet, isConnected, router]); - return { chainName, fromTokenAddress, toTokenAddress }; + return { chainName, fromTokenAddress, toTokenAddress, fromAmount, toAmount }; }