diff --git a/next.config.mjs b/next.config.mjs index a276705..6e59593 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', + // output: 'standalone', images: { remotePatterns: [ { diff --git a/package.json b/package.json index 76dc55e..582941e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "format": "prettier --write ." + "format": "prettier --write ." }, "dependencies": { "@bbachain/buffer-layout": "^1.0.0", @@ -35,9 +35,12 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.0", + "@radix-ui/react-visually-hidden": "^1.2.3", "@tabler/icons-react": "^3.11.0", + "@tanstack/query-async-storage-persister": "^5.89.0", "@tanstack/react-query": "^5.51.11", "@tanstack/react-query-next-experimental": "^5.51.11", + "@tanstack/react-query-persist-client": "^5.89.0", "@tanstack/react-table": "^8.21.3", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", @@ -50,6 +53,7 @@ "jotai": "^2.9.1", "lucide-react": "^0.487.0", "moment": "^2.30.1", + "motion": "^12.23.24", "next": "14.2.5", "next-themes": "^0.4.4", "react": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2400aa..3ec9263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,15 +86,24 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tabler/icons-react': specifier: ^3.11.0 version: 3.11.0(react@18.3.1) + '@tanstack/query-async-storage-persister': + specifier: ^5.89.0 + version: 5.89.0 '@tanstack/react-query': specifier: ^5.51.11 version: 5.51.11(react@18.3.1) '@tanstack/react-query-next-experimental': specifier: ^5.51.11 version: 5.51.11(@tanstack/react-query@5.51.11(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@tanstack/react-query-persist-client': + specifier: ^5.89.0 + version: 5.89.0(@tanstack/react-query@5.51.11(react@18.3.1))(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -131,6 +140,9 @@ importers: moment: specifier: ^2.30.1 version: 2.30.1 + motion: + specifier: ^12.23.24 + version: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: 14.2.5 version: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1703,9 +1715,18 @@ packages: '@tabler/icons@3.11.0': resolution: {integrity: sha512-/vZinJNvCYhdAB+RUsyCpanSPuOEKHHIZi4Uu0Bw7ilewHnQhCWUPrT704uHCRli2ROl7spADPmWzAqOganA5A==} + '@tanstack/query-async-storage-persister@5.89.0': + resolution: {integrity: sha512-/l7JVKvQ6/oad0+ncvBcJOqRL4lIJ5E3UjfB+xgTfVeqga8wMnH1cgOlJEpvBFEDCw88hA/jpGb6Cp9my9Vc+w==} + '@tanstack/query-core@5.51.9': resolution: {integrity: sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==} + '@tanstack/query-core@5.89.0': + resolution: {integrity: sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==} + + '@tanstack/query-persist-client-core@5.89.0': + resolution: {integrity: sha512-kxZgQGgD7VqSFTDA/JyajywixHGGhzjMTtkENeVcS6BoTW6CGOkvoZH3L4/ROsaCZ4ibDfrmPzfUCpghID5ENg==} + '@tanstack/react-query-next-experimental@5.51.11': resolution: {integrity: sha512-gUYTz9KYUbyxSek7U3o1J4efqzfNZICd3XvBhfz5i0rrxx7hKghx18fL87vH2N9UxYS5V5DjjX3dIp4m/u+Nvg==} peerDependencies: @@ -1713,6 +1734,12 @@ packages: next: ^13 || ^14 || ^15 react: ^18 || ^19 + '@tanstack/react-query-persist-client@5.89.0': + resolution: {integrity: sha512-c1RaSID8DPzr7HnO2kfah5ON/lEtN/g0gN4nRsxWPi8gjWQRMfOh9av/KJWxxqWnBMPZ+tMV5Lb1OS38GAIRrw==} + peerDependencies: + '@tanstack/react-query': ^5.89.0 + react: ^18 || ^19 + '@tanstack/react-query@5.51.11': resolution: {integrity: sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==} peerDependencies: @@ -2678,6 +2705,20 @@ packages: react-dom: optional: true + framer-motion@12.23.24: + resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3378,9 +3419,26 @@ packages: motion-dom@12.23.12: resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + motion@12.23.24: + resolution: {integrity: sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -6081,14 +6139,31 @@ snapshots: '@tabler/icons@3.11.0': {} + '@tanstack/query-async-storage-persister@5.89.0': + dependencies: + '@tanstack/query-core': 5.89.0 + '@tanstack/query-persist-client-core': 5.89.0 + '@tanstack/query-core@5.51.9': {} + '@tanstack/query-core@5.89.0': {} + + '@tanstack/query-persist-client-core@5.89.0': + dependencies: + '@tanstack/query-core': 5.89.0 + '@tanstack/react-query-next-experimental@5.51.11(@tanstack/react-query@5.51.11(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/react-query': 5.51.11(react@18.3.1) next: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 + '@tanstack/react-query-persist-client@5.89.0(@tanstack/react-query@5.51.11(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-persist-client-core': 5.89.0 + '@tanstack/react-query': 5.51.11(react@18.3.1) + react: 18.3.1 + '@tanstack/react-query@5.51.11(react@18.3.1)': dependencies: '@tanstack/query-core': 5.51.9 @@ -6953,7 +7028,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -6981,7 +7056,7 @@ snapshots: enhanced-resolve: 5.17.0 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -7003,7 +7078,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -7300,6 +7375,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + framer-motion@12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8165,8 +8249,20 @@ snapshots: dependencies: motion-utils: 12.23.6 + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + motion-utils@12.23.6: {} + motion@12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + ms@2.1.2: {} ms@2.1.3: {} diff --git a/src/app/(walletConnected)/layout.tsx b/src/app/(walletConnected)/layout.tsx index d1c08f2..ee6302b 100644 --- a/src/app/(walletConnected)/layout.tsx +++ b/src/app/(walletConnected)/layout.tsx @@ -1,8 +1,6 @@ 'use client' import { useWallet } from '@bbachain/wallet-adapter-react' -import { Loader2 } from 'lucide-react' -import { useState, useEffect } from 'react' import { CiWallet } from 'react-icons/ci' import ThemeImage from '@/components/common/ThemeImage' @@ -13,22 +11,11 @@ import { useWalletListDialog } from '@/stores/walletDialog' export default function WalletConnectedLayout({ children }: { children: React.ReactNode }) { const { publicKey: address } = useWallet() const { openWalletList } = useWalletListDialog() - const [mounted, setMounted] = useState(false) - useEffect(() => setMounted(true), []) - - if (!mounted) { - return ( -
- -

Please wait...

-
- ) - } if (!address) return ( -
- +
+ Wallet Not Connected @@ -36,12 +23,15 @@ export default function WalletConnectedLayout({ children }: { children: React.Re

- Please connect your wallet to use the Quick Token
Generator and access features. + Please connect your wallet to use the Quick Token
Generator and access + features.

@@ -58,5 +48,5 @@ export default function WalletConnectedLayout({ children }: { children: React.Re
) - return
{children}
+ return
{children}
} diff --git a/src/app/(walletConnected)/liquidity-pools/create-pool/page.tsx b/src/app/(walletConnected)/liquidity-pools/create-pool/page.tsx index 92b8c35..e487700 100644 --- a/src/app/(walletConnected)/liquidity-pools/create-pool/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/create-pool/page.tsx @@ -1,15 +1,7 @@ 'use client' import { zodResolver } from '@hookform/resolvers/zod' -import { - ChevronDown, - ArrowLeft, - ArrowRight, - Info, - Loader2, - CheckCircle, - AlertCircle -} from 'lucide-react' +import { ChevronDown, ArrowLeft, ArrowRight, Info, Loader2, AlertCircle } from 'lucide-react' import Image from 'next/image' import { useEffect, useState, useCallback } from 'react' import { useForm } from 'react-hook-form' @@ -49,22 +41,24 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import LPSuccessDialog from '@/features/liquidityPool/components/LPSuccessDialog' import { useCreatePool } from '@/features/liquidityPool/services' import { TCreatePoolPayload, MintInfo } from '@/features/liquidityPool/types' +import { + isBBAPool, + getBBAPositionInPool, + requiresBBAWrapping +} from '@/features/liquidityPool/utils' import { createPoolValidation } from '@/features/liquidityPool/validation' import { LoadingDialog } from '@/features/nfts/components/StatusDialog' import SwapItem from '@/features/swap/components/SwapItem' -import TokenListDialog from '@/features/swap/components/TokenListDialog' -import { - useGetTokensFromAPI, - useGetTokenPrice, - useGetUserBalanceByMint, - useGetCoinGeckoTokenPrice -} from '@/features/swap/services' -import { TTokenProps } from '@/features/swap/types' -import { getCoinGeckoId } from '@/features/swap/utils' import FormProgressLine from '@/features/tokens/components/form/FormProgressLine' -import { cn, formatTokenBalance } from '@/lib/utils' -import { useGetBalance } from '@/services/wallet' -import { isBBAPool, getBBAPositionInPool, requiresBBAWrapping } from '@/staticData/tokens' +import TradeableTokenListDialog from '@/features/tokens/components/TradeableTokenListDialog' +import { useGetTokenPriceByCoinGeckoId } from '@/features/tokens/services' +import { useGetTradeableTokens } from '@/features/tokens/services' +import { TTradeableTokenProps } from '@/features/tokens/types' +import { getCoinGeckoId } from '@/lib/token' +import { formatTokenBalance } from '@/lib/token' +import { cn } from '@/lib/utils' +import { useGetTokenBalanceByMint } from '@/services/wallet' +import { useGetBBABalance } from '@/services/wallet' import { useErrorDialog } from '@/stores/errorDialog' // Enhanced step configuration @@ -167,9 +161,9 @@ function TokenSelectionCard({ export default function CreatePool() { // Hooks - const getTokensQuery = useGetTokensFromAPI() + const getTokensQuery = useGetTradeableTokens() const createPoolMutation = useCreatePool() - const getBalanceQuery = useGetBalance() + const getBalanceQuery = useGetBBABalance() const { openErrorDialog } = useErrorDialog() // Form setup @@ -197,6 +191,7 @@ export default function CreatePool() { const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false) // Computed values + const createPoolResponse = createPoolMutation.data?.data const selectedBaseToken = form.watch('baseToken') const selectedQuoteToken = form.watch('quoteToken') const isNoBalance = getBalanceQuery.isError || !getBalanceQuery.data || getBalanceQuery.data === 0 @@ -210,33 +205,27 @@ export default function CreatePool() { selectedBaseToken && selectedQuoteToken ? getBBAPositionInPool(selectedBaseToken.address, selectedQuoteToken.address) : null - const requiresWrapping = - selectedBaseToken && selectedQuoteToken - ? requiresBBAWrapping(selectedBaseToken.address, selectedQuoteToken.address) - : false - const bbaToken = - bbaPosition === 'base' ? selectedBaseToken : bbaPosition === 'quote' ? selectedQuoteToken : null const nonBBAToken = bbaPosition === 'base' ? selectedQuoteToken : bbaPosition === 'quote' ? selectedBaseToken : null // Balance queries - const getMintABalance = useGetUserBalanceByMint({ + const getMintABalance = useGetTokenBalanceByMint({ mintAddress: selectedBaseToken?.address || '' }) - const getMintBBalance = useGetUserBalanceByMint({ + const getMintBBalance = useGetTokenBalanceByMint({ mintAddress: selectedQuoteToken?.address || '' }) - const getMintATokenPrice = useGetCoinGeckoTokenPrice({ + const getMintATokenPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getCoinGeckoId(selectedBaseToken?.address) }) - const getMintBTokenPrice = useGetCoinGeckoTokenPrice({ + const getMintBTokenPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getCoinGeckoId(selectedQuoteToken?.address) }) - const mintABalance = getMintABalance.data?.balance || 0 - const mintBBalance = getMintBBalance.data?.balance || 0 - const mintAInitialPrice = getMintATokenPrice.data || 0 - const mintBInitialPrice = getMintBTokenPrice.data || 0 + const mintABalance = getMintABalance.data ?? 0 + const mintBBalance = getMintBBalance.data ?? 0 + const mintAInitialPrice = getMintATokenPrice.data ?? 0 + const mintBInitialPrice = getMintBTokenPrice.data ?? 0 // Format balances with proper decimals for display const formattedMintABalance = selectedBaseToken @@ -348,7 +337,7 @@ export default function CreatePool() { ) // Default token props for TokenListDialog fallback - const defaultTokenProps: TTokenProps = { + const defaultTokenProps: TTradeableTokenProps = { address: '', logoURI: '', symbol: '', @@ -358,7 +347,7 @@ export default function CreatePool() { } // Type conversion functions for TokenListDialog compatibility - const convertTTokenPropsToMintInfo = (token: TTokenProps): MintInfo => ({ + const convertTTokenPropsToMintInfo = (token: TTradeableTokenProps): MintInfo => ({ chainId: token.chainId, address: token.address, programId: token.programId, @@ -370,7 +359,7 @@ export default function CreatePool() { extensions: token.extensions }) - const convertMintInfoToTTokenProps = (token: MintInfo): TTokenProps => ({ + const convertMintInfoToTTokenProps = (token: MintInfo): TTradeableTokenProps => ({ chainId: token.chainId, address: token.address, programId: token.programId, @@ -384,7 +373,7 @@ export default function CreatePool() { // Wrapper functions for TokenListDialog compatibility const onSelectBaseTokenWrapper = useCallback( - (token: TTokenProps) => { + (token: TTradeableTokenProps) => { const mintInfo = convertTTokenPropsToMintInfo(token) onSelectBaseToken(mintInfo) }, @@ -392,7 +381,7 @@ export default function CreatePool() { ) const onSelectQuoteTokenWrapper = useCallback( - (token: TTokenProps) => { + (token: TTradeableTokenProps) => { const mintInfo = convertTTokenPropsToMintInfo(token) onSelectQuoteToken(mintInfo) }, @@ -1077,7 +1066,7 @@ export default function CreatePool() { {/* Token Selection Dialog */} - {/* Loading Overlay */} diff --git a/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx index d8c9bc6..8a0a6ef 100644 --- a/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx @@ -11,21 +11,27 @@ import { IoCheckmarkCircle } from 'react-icons/io5' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import DepositDetailSkeleton from '@/features/liquidityPool/components/DepositDetailSkeleton' import LPSlippageDialog from '@/features/liquidityPool/components/LPSlippageDialog' import LPSuccessDialog from '@/features/liquidityPool/components/LPSuccessDialog' import { useDepositToPool, useGetPoolById, - useGetUserPoolStatsById + useGetUserPoolStats } from '@/features/liquidityPool/services' -import { PoolData } from '@/features/liquidityPool/types' +import { MintInfo } from '@/features/liquidityPool/types' +import { + calculateOptimalAmountADeposit, + calculateOptimalAmountBDeposit +} from '@/features/liquidityPool/utils' import SwapItem from '@/features/swap/components/SwapItem' -import { useGetUserBalanceByMint, useGetCoinGeckoTokenPrice } from '@/features/swap/services' -import { TTokenProps } from '@/features/swap/types' -import { getCoinGeckoId } from '@/features/swap/utils' +import { useGetTokenPriceByCoinGeckoId } from '@/features/tokens/services' +import { formatTokenBalance, getCoinGeckoId } from '@/lib/token' import { cn } from '@/lib/utils' +import { useGetTokenBalanceByMint } from '@/services/wallet' -const initialTokeProps: TTokenProps = { +const initialTokeProps: MintInfo = { chainId: 101, address: '', programId: '', @@ -37,50 +43,15 @@ const initialTokeProps: TTokenProps = { extensions: {} } -// Helper function to calculate optimal amount B from amount A -const calculateOptimalAmountB = ( - amountA: string, - reserveA: bigint, - reserveB: bigint, - decimalsA: number, - decimalsB: number -): string => { - if (!amountA || Number(amountA) === 0 || reserveA === BigInt(0)) return '' - - const amountADaltons = BigInt(Math.floor(Number(amountA) * Math.pow(10, decimalsA))) - const amountBDaltons = (amountADaltons * reserveB) / reserveA - const amountB = Number(amountBDaltons) / Math.pow(10, decimalsB) - - return amountB.toFixed(6) -} - -// Helper function to calculate optimal amount A from amount B -const calculateOptimalAmountA = ( - amountB: string, - reserveA: bigint, - reserveB: bigint, - decimalsA: number, - decimalsB: number -): string => { - if (!amountB || Number(amountB) === 0 || reserveB === BigInt(0)) return '' - - const amountBDaltons = BigInt(Math.floor(Number(amountB) * Math.pow(10, decimalsB))) - const amountADaltons = (amountBDaltons * reserveA) / reserveB - const amountA = Number(amountADaltons) / Math.pow(10, decimalsA) - - return amountA.toFixed(6) -} - -// Helper function to check if data is PoolData type -const isPoolData = (data: any): data is PoolData => { - return data && typeof data === 'object' && 'week' in data -} - export default function LiquidityPoolDeposit({ params }: { params: { poolId: string } }) { const poolId = params.poolId + const router = useRouter() + const getPoolByIdQuery = useGetPoolById({ poolId }) const poolDetailData = getPoolByIdQuery.data?.data - const router = useRouter() + + const getUserPoolStats = useGetUserPoolStats({ pool: poolDetailData }) + const userStatsData = getUserPoolStats.data?.data const [fromAmount, setFromAmount] = useState('') const [toAmount, setToAmount] = useState('') @@ -96,97 +67,48 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str totalValue: string } | null>(null) - const getMintABalance = useGetUserBalanceByMint({ + const getMintABalance = useGetTokenBalanceByMint({ mintAddress: poolDetailData?.mintA.address ?? '' }) - const getMintBBalance = useGetUserBalanceByMint({ + const getMintBBalance = useGetTokenBalanceByMint({ mintAddress: poolDetailData?.mintB.address ?? '' }) - // Get token prices from CoinGecko - const getMintATokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: poolDetailData?.mintA.address - ? getCoinGeckoId(poolDetailData.mintA.address) - : undefined - }) - const getMintBTokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: poolDetailData?.mintB.address - ? getCoinGeckoId(poolDetailData.mintB.address) - : undefined - }) // Convert balance from daltons to UI units using token decimals - const mintABalance = - getMintABalance.data && poolDetailData - ? getMintABalance.data.balance / Math.pow(10, poolDetailData.mintA.decimals) - : 0 - const mintBBalance = - getMintBBalance.data && poolDetailData - ? getMintBBalance.data.balance / Math.pow(10, poolDetailData.mintB.decimals) - : 0 + const mintABalance = formatTokenBalance( + getMintABalance.data ?? 0, + poolDetailData?.mintA.decimals ?? 0 + ) + const mintBBalance = formatTokenBalance( + getMintBBalance.data ?? 0, + poolDetailData?.mintB.decimals ?? 0 + ) // Get unit prices from CoinGecko API - const mintAUnitPrice = getMintATokenPrice.data ?? 0 - const mintBUnitPrice = getMintBTokenPrice.data ?? 0 - - // Calculate total USD values for current input amounts - const baseTokenPrice = Number(fromAmount) > 0 ? Number(fromAmount) * mintAUnitPrice : 0 - const quoteTokenPrice = Number(toAmount) > 0 ? Number(toAmount) * mintBUnitPrice : 0 + const getMintACoinGeckoId = getCoinGeckoId(poolDetailData?.mintA.address ?? '') + const getMintBCoinGeckoId = getCoinGeckoId(poolDetailData?.mintB.address ?? '') + const getMintAPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getMintACoinGeckoId }) + const getMintBPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getMintBCoinGeckoId }) + const mintAPrice = getMintAPrice.data ?? 0 + const mintBPrice = getMintBPrice.data ?? 0 + const inputAmountPrice = mintAPrice * Number(fromAmount) + const outputAmountPrice = mintBPrice * Number(toAmount) const depositMutation = useDepositToPool() - const mintAPoolAmount = poolDetailData - ? isPoolData(poolDetailData) - ? poolDetailData.mintAmountA - : Number(poolDetailData.reserveA) / Math.pow(10, poolDetailData.mintA.decimals) - : 0 - - const mintBPoolAmount = poolDetailData - ? isPoolData(poolDetailData) - ? poolDetailData.mintAmountB - : Number(poolDetailData.reserveB) / Math.pow(10, poolDetailData.mintB.decimals) - : 0 - - const getUserStats = useGetUserPoolStatsById({ - poolId, - reserveA: mintAPoolAmount, - reserveB: mintBPoolAmount, - mintA: poolDetailData?.mintA, - mintB: poolDetailData?.mintB, - feeRate: poolDetailData?.feeRate ?? 0, - volume24h: !poolDetailData - ? 0 - : isPoolData(poolDetailData) - ? poolDetailData.day.volume - : poolDetailData.volume24h, - isUserStats: true - }) - // Auto-calculate optimal ratio useEffect(() => { if (!poolDetailData || isCalculating) return - const pool = isPoolData(poolDetailData) ? poolDetailData : (poolDetailData as any) - - let reserveA: bigint, reserveB: bigint - - if (isPoolData(poolDetailData)) { - reserveA = BigInt( - Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals)) - ) - reserveB = BigInt( - Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals)) - ) - } else { - reserveA = pool.reserveA || BigInt(0) - reserveB = pool.reserveB || BigInt(0) - } + const reserveA = poolDetailData.reserveA + const reserveB = poolDetailData.reserveB if (reserveA === BigInt(0) || reserveB === BigInt(0)) return setIsCalculating(true) if (lastChangedField === 'from' && fromAmount) { - const optimalB = calculateOptimalAmountB( + const optimalB = calculateOptimalAmountBDeposit( fromAmount, reserveA, reserveB, @@ -197,7 +119,7 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str setToAmount(optimalB) } } else if (lastChangedField === 'to' && toAmount) { - const optimalA = calculateOptimalAmountA( + const optimalA = calculateOptimalAmountADeposit( toAmount, reserveA, reserveB, @@ -241,39 +163,18 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str try { const result = await depositMutation.mutateAsync({ - pool: isPoolData(poolDetailData) - ? { - address: (poolDetailData as any).id || '', - programId: poolDetailData.programId, - swapData: {} as any, - mintA: poolDetailData.mintA, - mintB: poolDetailData.mintB, - tokenAccountA: '', - tokenAccountB: '', - reserveA: BigInt( - Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals)) - ), - reserveB: BigInt( - Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals)) - ), - feeRate: poolDetailData.feeRate, - tvl: poolDetailData.tvl, - volume24h: poolDetailData.day?.volume ?? 0, - fees24h: poolDetailData.day?.volumeFee ?? 0, - apr24h: poolDetailData.day?.apr ?? 0 - } - : poolDetailData, - amountA: fromAmount, - amountB: toAmount, + pool: poolDetailData, + mintAAmount: parseFloat(fromAmount), + mintBAmount: parseFloat(toAmount), slippage }) // Show success dialog with transaction details setSuccessData({ - signature: result.signature, + signature: result.data.signature, amountA: fromAmount, amountB: toAmount, - totalValue: (baseTokenPrice + quoteTokenPrice).toFixed(2) + totalValue: (inputAmountPrice + outputAmountPrice).toFixed(2) }) setFromAmount('') @@ -294,6 +195,8 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str } }, [depositMutation.data, depositMutation.isSuccess]) + if (getPoolByIdQuery.isLoading) return + return (
-
-
-
- {`${baseMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> - {`${quoteMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> -

{`${baseMint?.symbol}-${quoteMint?.symbol}`}

+ {getPoolById.isLoading ? ( +
+
+ + + + +
+
+ + +
+
+ ) : ( +
+
+
+ {`${pool?.mintA?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> + {`${pool?.mintB?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> +

{`${pool?.mintA?.symbol}-${pool?.mintB?.symbol}`}

+
+
+

+ {(pool?.feeRate ?? 0 * 100).toFixed(2)}% +

+
+
-
-

+ - {(poolDetailData?.feeRate ?? 0 * 100).toFixed(2)}% -

+ + Swap + + + + Add Liquidity +
- -
-
- - - Swap - - - - Add Liquidity - -
-
+
+ )} +
setIsMyStats(value === 'my-stats')} + onValueChange={(value) => setIsOwnerTab(value === 'my-stats')} > - + Pool Stats - + My Stats @@ -365,6 +320,7 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) {
@@ -392,28 +348,28 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { isLoading={getPoolById.isLoading} title="APR(7 days)" info="Weekly Percentage Rate earned by liquidity providers from trading fees and rewards." - content={apr7Day} + content={`${pool?.apr24h.toFixed(2) ?? 0.0}%`} />


@@ -424,53 +380,56 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) {
{`${baseMint?.name} { e.currentTarget.src = '/icon-placeholder.svg' }} /> -

{baseMint?.symbol}

+

{pool?.mintA?.symbol}

- {shortenAddress(baseMint?.address ?? '', 7)} + {shortenAddress(pool?.mintA?.address ?? '', 7)} - +
{`${quoteMint?.name} { e.currentTarget.src = '/icon-placeholder.svg' }} /> -

{quoteMint?.symbol}

+

{pool?.mintB?.symbol}

- {shortenAddress(quoteMint?.address ?? '', 7)} + {shortenAddress(pool?.mintB?.address ?? '', 7)} - +
@@ -483,6 +442,7 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { @@ -128,10 +136,16 @@ export default function NFTDetail({ params }: { params: { address: string } }) { component: (

- {NFTDetailData?.metadata.creators ? NFTDetailData.metadata.creators[0].address.toBase58() : '-'} + {NFTDetailData?.metadata.creators + ? NFTDetailData.metadata.creators[0].address.toBase58() + : '-'}

) @@ -165,6 +179,19 @@ export default function NFTDetail({ params }: { params: { address: string } }) { if (getNFTDetailData.isLoading) { return (
+
+ +

+ {NFTDetailData?.metadata?.name ?? ''} +

+
@@ -205,7 +232,10 @@ export default function NFTDetail({ params }: { params: { address: string } }) {
{NFTAboutData.map((about) => ( -
+

{about.label}

:

{about.value}

@@ -214,8 +244,12 @@ export default function NFTDetail({ params }: { params: { address: string } }) {
{NFTDetailData?.metadata.name setIsImageOpen(true)} variant="outline" + aria-label="maximize icon" className="rounded-full text-white bg-transparent border-white" > @@ -233,6 +268,7 @@ export default function NFTDetail({ params }: { params: { address: string } }) { size="icon" variant="outline" className={'rounded-full text-white bg-transparent border-white'} + aria-label="download icon" onClick={() => downloadImage( NFTDetailData?.metadata?.metadataOffChain.data.image ?? '', @@ -249,7 +285,10 @@ export default function NFTDetail({ params }: { params: { address: string } }) {
{NFTBlockchainInfo.map((info) => ( -
+

{info.label}

: {info.component} @@ -261,7 +300,10 @@ export default function NFTDetail({ params }: { params: { address: string } }) {
{metaDataInfo.map((info) => ( -
+

{info.label}

: {info.component} diff --git a/src/app/(walletConnected)/nfts/collection/create/page.tsx b/src/app/(walletConnected)/nfts/collection/create/page.tsx index 0572429..067e370 100644 --- a/src/app/(walletConnected)/nfts/collection/create/page.tsx +++ b/src/app/(walletConnected)/nfts/collection/create/page.tsx @@ -8,14 +8,32 @@ import { useForm } from 'react-hook-form' import { NoBalanceAlert } from '@/components/common/Alert' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '@/components/ui/card' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' import { Input } from '@/components/ui/input' -import { LoadingDialog, SuccessDialogCollection, SuccessDialogNFT } from '@/features/nfts/components/StatusDialog' +import { + LoadingDialog, + SuccessDialogCollection, + SuccessDialogNFT +} from '@/features/nfts/components/StatusDialog' import { useCreateCollection, useValidateOffChainMetadata } from '@/features/nfts/services' import { TCreateCollectionPayload, TCreateNFTDialogProps } from '@/features/nfts/types' import { CreateCollectionValidation } from '@/features/nfts/validation' -import { useGetBalance } from '@/services/wallet' +import { useGetBBABalance } from '@/services/wallet' import { useErrorDialog } from '@/stores/errorDialog' type FieldName = keyof TCreateCollectionPayload @@ -33,7 +51,7 @@ export default function CreateCollection() { const createCollectionMutation = useCreateCollection() const validateMetadataMutation = useValidateOffChainMetadata() - const getBalanceQuery = useGetBalance() + const getBalanceQuery = useGetBBABalance() const isNoBalance = getBalanceQuery.isError || !getBalanceQuery.data || getBalanceQuery.data === 0 const [step, setStep] = useState(0) @@ -54,7 +72,8 @@ export default function CreateCollection() { validateMetadataMutation.mutate({ uri: form.getValues('uri') }) } - const onCreateCollection = async (payload: TCreateCollectionPayload) => createCollectionMutation.mutate(payload) + const onCreateCollection = async (payload: TCreateCollectionPayload) => + createCollectionMutation.mutate(payload) const { openErrorDialog } = useErrorDialog() @@ -75,7 +94,10 @@ export default function CreateCollection() { setIsSuccessDialogMetadata(true) form.setValue('name', validateMetadataMutation.data.data.name) form.setValue('symbol', validateMetadataMutation.data.data.symbol) - form.setValue('sellerFeeBasisPoints', validateMetadataMutation.data.data.seller_fee_basis_points.toString()) + form.setValue( + 'sellerFeeBasisPoints', + validateMetadataMutation.data.data.seller_fee_basis_points.toString() + ) setStep(1) } }, [form, validateMetadataMutation.data, validateMetadataMutation.isSuccess]) @@ -120,14 +142,6 @@ export default function CreateCollection() { } }, [createCollectionMutation.error, createCollectionMutation.isError, openErrorDialog]) - if (getBalanceQuery.isLoading) - return ( -
- -

Please wait...

-
- ) - return (
- Upload Metadata + + Upload Metadata + - Upload Metadata + + Upload Metadata + Upload or link to your JSON metadata files hosted on IPFS or Arweave. diff --git a/src/app/(walletConnected)/nfts/create/page.tsx b/src/app/(walletConnected)/nfts/create/page.tsx index 690f1ee..48b08a8 100644 --- a/src/app/(walletConnected)/nfts/create/page.tsx +++ b/src/app/(walletConnected)/nfts/create/page.tsx @@ -11,17 +11,35 @@ import { capitalCase } from 'text-case' import { NoBalanceAlert } from '@/components/common/Alert' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '@/components/ui/card' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import CreateCollectionDialog from '@/features/nfts/components/CreateCollectionDialog' import SelectCollection, { SelectedItem } from '@/features/nfts/components/SelectCollection' import { LoadingDialog, SuccessDialogNFT } from '@/features/nfts/components/StatusDialog' -import { useGetCollections, useCreateNFT, useValidateOffChainMetadata } from '@/features/nfts/services' +import { + useGetCollections, + useCreateNFT, + useValidateOffChainMetadata +} from '@/features/nfts/services' import { TCreateNFTPayload, TCreateNFTDialogProps } from '@/features/nfts/types' import { CreateNFTValidation } from '@/features/nfts/validation' -import { useGetBalance } from '@/services/wallet' +import { useGetBBABalance } from '@/services/wallet' import { useErrorDialog } from '@/stores/errorDialog' const initialDialogContent: TCreateNFTDialogProps = { @@ -38,22 +56,23 @@ export default function CreateNFT() { } }) - const router = useRouter() const query = useSearchParams() const collectionKey = query.get('collectionKey') const createNFTMutation = useCreateNFT() const getCollectionQuery = useGetCollections() const validateMetadataMutation = useValidateOffChainMetadata() - const getBalanceQuery = useGetBalance() + const getBalanceQuery = useGetBBABalance() const isNoBalance = getBalanceQuery.isError || !getBalanceQuery.data || getBalanceQuery.data === 0 const [step, setStep] = useState<'upload' | 'preview'>('upload') const [isCreateCollection, setIsCreateCollection] = useState(false) const [isSuccessDialog, setIsSuccessDialog] = useState(false) - const [successDialogProps, setSuccessDialogProps] = useState(initialDialogContent) + const [successDialogProps, setSuccessDialogProps] = + useState(initialDialogContent) const [isLoadingDialog, setIsLoadingDialog] = useState(false) - const [loadingDialogProps, setLoadingDialogProps] = useState(initialDialogContent) + const [loadingDialogProps, setLoadingDialogProps] = + useState(initialDialogContent) const [selectedCollection, setSelectedCollection] = useState(null) const collectionListData = useMemo(() => { @@ -157,14 +176,6 @@ export default function CreateNFT() { const onCreateNFT = (payload: TCreateNFTPayload) => createNFTMutation.mutate(payload) - if (getBalanceQuery.isLoading) - return ( -
- -

Please wait...

-
- ) - return (
-

Metadata Preview

+

+ Metadata Preview +

{/* eslint-disable-next-line @next/next/no-img-element*/} @@ -226,7 +239,9 @@ export default function CreateNFT() { key={attribute.trait_type} className="bg-box w-auto text-sm px-3 h-8 py-1.5 rounded-[8px] flex items-center space-x-1.5" > -

{attribute.trait_type}

+

+ {attribute.trait_type} +

:

{attribute.value}

@@ -237,7 +252,9 @@ export default function CreateNFT() {
- + -

Mint New NFT

+

+ Mint New NFT +

- Upload Metadata + + Upload Metadata + Upload or link to your JSON metadata files hosted on IPFS or Arweave. diff --git a/src/app/(walletConnected)/nfts/page.tsx b/src/app/(walletConnected)/nfts/page.tsx index 3d81feb..305db26 100644 --- a/src/app/(walletConnected)/nfts/page.tsx +++ b/src/app/(walletConnected)/nfts/page.tsx @@ -1,7 +1,6 @@ 'use client' import { ColumnDef } from '@tanstack/react-table' -import { Loader2 } from 'lucide-react' import { useEffect } from 'react' import toast from 'react-hot-toast' @@ -46,19 +45,12 @@ export default function NFTs() { if (getNFTQuery.isError && getNFTQuery.error) toast.error(getNFTQuery.error.message) }, [getNFTQuery.error, getNFTQuery.isError]) - if (getNFTQuery.isPending) { - return ( -
- -

Please wait...

-
- ) - } - return (
-

My NFTs

- +

+ My NFTs +

+
) } diff --git a/src/app/(walletConnected)/swap/page.tsx b/src/app/(walletConnected)/swap/page.tsx index e4c27a3..b17b779 100644 --- a/src/app/(walletConnected)/swap/page.tsx +++ b/src/app/(walletConnected)/swap/page.tsx @@ -14,51 +14,39 @@ import REGEX from '@/constants/regex' import ExpertModeWarningDialog from '@/features/swap/components/ExpertModeWarningDialog' import SettingDialog from '@/features/swap/components/SettingDialog' import SwapItem from '@/features/swap/components/SwapItem' -import TokenListDialog from '@/features/swap/components/TokenListDialog' -import { - useGetSwapQuote, - useGetUserBalanceByMint, - useExecuteSwap, - useCanSwap, - useGetSwapRoute, - useGetTokensFromAPI, - useGetCoinGeckoTokenPrice -} from '@/features/swap/services' -import { TTokenProps } from '@/features/swap/types' -import { getCoinGeckoId } from '@/features/swap/utils' +import { useGetSwapQuote, useExecuteSwap, useGetSwapRoute } from '@/features/swap/services' +import TradeableTokenListDialog from '@/features/tokens/components/TradeableTokenListDialog' +import { useGetTokenPriceByCoinGeckoId, useGetTradeableTokens } from '@/features/tokens/services' +import { TTradeableTokenProps } from '@/features/tokens/types' +import { formatTokenBalance, getCoinGeckoId } from '@/lib/token' import { cn } from '@/lib/utils' -import StaticTokens from '@/staticData/tokens' - -const initialBaseToken = StaticTokens[1] -const initialQuoteToken = StaticTokens[2] +import { useGetTokenBalanceByMint } from '@/services/wallet' +import { USDT_MINT } from '@/staticData/tokens' + +const initialTradeableToken: TTradeableTokenProps = { + address: '', + symbol: '', + name: '', + decimals: 0 +} export default function Swap() { - /** - * Enhanced Swap Page with URL Parameter Support - * - * Features: - * - URL parameters: /swap?from=&to= - * - Dynamic token fetching from /api/tokens endpoint - * - Automatic URL sync when tokens are selected - * - Fallback to hardcoded tokens if URL params not found - * - * Example: /swap?from=LUGhbMWAWsMCmNDRivANNg1adxw2Bgqz6sAm8QYA1Qq&to=GyWmvShQr9QGGYsqpVJtMHsyLAng4QtZRgDmwWvYTMaR - */ const searchParams = useSearchParams() const router = useRouter() const [amountIn, setAmountIn] = useState('') - const [fromTokenProps, setFromTokenProps] = useState(initialBaseToken) - const [toTokenProps, setToTokenProps] = useState(initialQuoteToken) - const [isTokenDialogOpen, setIsTokenDialogOpen] = useState(false) + const [fromTokenProps, setFromTokenProps] = useState(initialTradeableToken) + const [toTokenProps, setToTokenProps] = useState(initialTradeableToken) const [maxSlippage, setMaxSlippage] = useState(0.5) const [timeLimit, setTimeLimit] = useState('0') const [isExpertMode, setIsExpertMode] = useState(false) + const [isTokenDialogOpen, setIsTokenDialogOpen] = useState(false) const [isSettingDialogOpen, setIsSettingDialogOpen] = useState(false) const [isExpertModeDialogOpen, setIsExpertModeDialogOpen] = useState(false) - const [typeItem, setTypeItem] = useState<'from' | 'to'>('from') + const [tokenDialogType, setTokenDialogType] = useState<'from' | 'to'>('from') + const [inputType, setInputType] = useState<'from' | 'to'>('from') // Get all tokens from API for lookup - const { data: allTokens } = useGetTokensFromAPI('') + const { data: allTokens, isLoading: isTokensLoading } = useGetTradeableTokens('') // Handle URL parameters for token initialization useEffect(() => { @@ -66,7 +54,7 @@ export default function Swap() { const toParam = searchParams.get('to') // Function to find token by address from API - const findTokenByAddress = (address: string): TTokenProps | null => { + const findTokenByAddress = (address: string): TTradeableTokenProps | null => { if (!allTokens?.data) return null const token = allTokens.data.find((token) => token.address === address) @@ -82,30 +70,18 @@ export default function Swap() { } } - if (fromParam && allTokens?.data) { - const fromToken = findTokenByAddress(fromParam) - if (fromToken) { - console.log('🔄 Setting from token from URL:', fromToken) - setFromTokenProps(fromToken) - } - } - - if (toParam && allTokens?.data) { - const toToken = findTokenByAddress(toParam) - if (toToken) { - console.log('🔄 Setting to token from URL:', toToken) - setToTokenProps(toToken) - } - } + const fromToken = findTokenByAddress(fromParam ?? '') + const toToken = findTokenByAddress(toParam ?? '') + if (fromToken) setFromTokenProps(fromToken) + if (toToken) setToTokenProps(toToken) }, [searchParams, allTokens]) // Function to update URL with current token selection const updateURLParams = useCallback( - (fromToken: TTokenProps, toToken: TTokenProps) => { + (fromToken: TTradeableTokenProps, toToken: TTradeableTokenProps) => { const params = new URLSearchParams() - params.set('from', fromToken.address) - params.set('to', toToken.address) - + params.set('from', fromToken.address ? fromToken.address : NATIVE_MINT.toBase58()) + params.set('to', toToken.address ? toToken.address : USDT_MINT.toBase58()) // Update URL without page reload router.push(`/swap?${params.toString()}`, { scroll: false }) }, @@ -116,128 +92,110 @@ export default function Swap() { updateURLParams(fromTokenProps, toTokenProps) }, [fromTokenProps, toTokenProps, updateURLParams]) + // Get swap route information + const getSwapRouteQuery = useGetSwapRoute({ + inputMint: fromTokenProps.address, + outputMint: toTokenProps.address + }) + const swapRouteData = getSwapRouteQuery?.data + // Enhanced swap quote using onchain pools - const swapQuoteQuery = useGetSwapQuote({ + const getSwapQuoteQuery = useGetSwapQuote({ + pool: swapRouteData?.pools[0], inputMint: fromTokenProps.address, outputMint: toTokenProps.address, inputAmount: amountIn, - slippage: maxSlippage + slippage: maxSlippage, + inputType }) + const swapQuoteData = getSwapQuoteQuery.data - const isSwapQuoteLoading = swapQuoteQuery.isLoading || swapQuoteQuery.isRefetching - - // Check if swap is possible - const canSwapQuery = useCanSwap(fromTokenProps.address, toTokenProps.address) - - // Get swap route information - const swapRouteQuery = useGetSwapRoute(fromTokenProps.address, toTokenProps.address) - - // Get user balances - const getMintABalance = useGetUserBalanceByMint({ mintAddress: fromTokenProps.address }) - const getMintBBalance = useGetUserBalanceByMint({ mintAddress: toTokenProps.address }) - - // Get token prices (fallback to external API if needed) - const getMintATokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: getCoinGeckoId(fromTokenProps.address) - }) - const getMintBTokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: getCoinGeckoId(toTokenProps.address) - }) + const isSwapQuoteLoading = + getSwapRouteQuery.isLoading || + getSwapRouteQuery.isRefetching || + getSwapQuoteQuery.isLoading || + getSwapQuoteQuery.isRefetching // Swap execution const executeSwapMutation = useExecuteSwap() - // Debug logging - useEffect(() => { - console.log('🔍 Swap Debug Info:', { - inputAmount: amountIn, - inputAmountNumber: Number(amountIn), - inputAmountValid: amountIn && Number(amountIn) > 0, - fromToken: fromTokenProps.symbol, - toToken: toTokenProps.symbol, - swapQuoteLoading: swapQuoteQuery.isLoading || swapQuoteQuery.isRefetching, - swapQuoteError: swapQuoteQuery.error, - swapQuoteData: swapQuoteQuery.data, - canSwap: canSwapQuery.data, - canSwapLoading: canSwapQuery.isLoading, - route: swapRouteQuery.data - }) - }, [ - amountIn, - fromTokenProps.symbol, - toTokenProps.symbol, - swapQuoteQuery, - canSwapQuery, - swapRouteQuery - ]) - - // Computed values - const swapQuote = swapQuoteQuery.data - const mintABalance = getMintABalance.data?.balance ?? 0 - const mintBBalance = getMintBBalance.data?.balance ?? 0 - const mintAInitialPrice = getMintATokenPrice.data ?? 0 - const mintBInitialPrice = getMintBTokenPrice.data ?? 0 - - // Calculate USD values - const inputAmount = swapQuote?.inputAmount ?? 0 - const outputAmount = swapQuote?.outputAmount ?? 0 - const baseTokenPrice = inputAmount * mintAInitialPrice - const quoteTokenPrice = outputAmount * mintBInitialPrice + // Get user balances + const getMintABalance = useGetTokenBalanceByMint({ mintAddress: fromTokenProps.address }) + const getMintBBalance = useGetTokenBalanceByMint({ mintAddress: toTokenProps.address }) + const mintABalance = getMintABalance.data ?? 0 + const mintBBalance = getMintBBalance.data ?? 0 + const mintABalanceFormatted = formatTokenBalance(mintABalance, fromTokenProps.decimals) + const mintBBalanceFormatted = formatTokenBalance(mintBBalance, toTokenProps.decimals) - useEffect(() => { - console.log('base token price ', baseTokenPrice) - }, [baseTokenPrice]) + // get input/output amounts + const outputAmount = parseFloat(swapQuoteData?.outputAmount.toFixed(6) ?? '').toString() ?? '' + const inputAmount = parseFloat(swapQuoteData?.inputAmount.toFixed(6) ?? '').toString() ?? '' // Improved validation - const userInputAmount = Number(amountIn) || 0 - const userTokenBalance = mintABalance / Math.pow(10, fromTokenProps.decimals) - const isBaseTokenBalanceNotEnough = userInputAmount > userTokenBalance - // Allow positive decimals for swap input - const isAmountPositive = REGEX.POSITIVE_DECIMAL.test(amountIn) && userInputAmount > 0 - const hasValidTokenPair = fromTokenProps.address !== toTokenProps.address - const isValid = - !isBaseTokenBalanceNotEnough && - isAmountPositive && - hasValidTokenPair && - canSwapQuery.data === true && - swapQuote - - // Exchange rate and other computed values - const exchangeRate = swapQuote?.exchangeRate - ? `1 ${fromTokenProps.symbol} = ${swapQuote.exchangeRate.toFixed(6)} ${toTokenProps.symbol}` + const userInputAmount = Number(inputAmount) || 0 + const userMaximumInputAmount = Number(swapQuoteData?.maximumInput ?? 0) + const userTokenBalance = mintABalanceFormatted + const isBalanceEnough = + inputType === 'from' + ? userInputAmount <= userTokenBalance + : userMaximumInputAmount <= userTokenBalance + const isInputPositive = REGEX.POSITIVE_DECIMAL.test(userInputAmount.toString()) + const isTokenPairValid = fromTokenProps.address !== toTokenProps.address + const isQuoteError = getSwapQuoteQuery.isError && getSwapQuoteQuery.error + const isValid = isBalanceEnough && isInputPositive && isTokenPairValid && swapQuoteData + + // get mint prices + const getMintACoinGeckoId = getCoinGeckoId(fromTokenProps.address) + const getMintBCoinGeckoId = getCoinGeckoId(toTokenProps.address) + const getMintAPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getMintACoinGeckoId }) + const getMintBPrice = useGetTokenPriceByCoinGeckoId({ coinGeckoId: getMintBCoinGeckoId }) + const mintAPrice = getMintAPrice.data ?? 0 + const mintBPrice = getMintBPrice.data ?? 0 + const inputAmountPrice = mintAPrice * userInputAmount + const outputAmountPrice = mintBPrice * (Number(outputAmount) || 0) + + // Exchange rate and other computed values for UI + const exchangeRate = swapQuoteData?.exchangeRate + ? `1 ${fromTokenProps.symbol} = ${swapQuoteData.exchangeRate.toFixed(6)} ${toTokenProps.symbol}` + : '-' + const minimumReceived = swapQuoteData?.minimumReceived + ? `${swapQuoteData.minimumReceived.toFixed(6)} ${toTokenProps.symbol}` : '-' - const minimumReceived = swapQuote?.minimumReceived - ? `${swapQuote.minimumReceived.toFixed(6)} ${toTokenProps.symbol}` + const maximumInput = swapQuoteData?.maximumInput + ? `${swapQuoteData.maximumInput.toFixed(6)} ${fromTokenProps.symbol}` : '-' - const priceImpact = swapQuote?.priceImpact ? `${swapQuote.priceImpact.toFixed(2)}%` : '-' + const priceImpact = swapQuoteData?.priceImpact ? `${swapQuoteData.priceImpact.toFixed(2)}%` : '-' const onSelectTokenFrom = () => { - setTypeItem('from') + setTokenDialogType('from') setIsTokenDialogOpen(true) } const onSelectTokenTo = () => { - setTypeItem('to') + setTokenDialogType('to') setIsTokenDialogOpen(true) } - const handleInputChange = (val: string) => { - console.log('💰 Input amount changed:', val) + const handleBaseInputChange = (val: string) => { + setInputType('from') + setAmountIn(val) + } + + const handleQuoteInputChange = (val: string) => { + setInputType('to') setAmountIn(val) } // Enhanced token setters that update URL - const setFromTokenWithURL = (token: TTokenProps) => { + const setFromTokenWithURL = (token: TTradeableTokenProps) => { setFromTokenProps(token) } - const setToTokenWithURL = (token: TTokenProps) => { + const setToTokenWithURL = (token: TTradeableTokenProps) => { setToTokenProps(token) } const onReverseSwap = () => { - console.log('🔄 Reversing swap tokens') - setAmountIn('') const newFromToken = toTokenProps const newToToken = fromTokenProps setFromTokenProps(newFromToken) @@ -245,8 +203,8 @@ export default function Swap() { } const handleSwap = async () => { - if (!swapQuote || !isValid) { - console.error('❌ Cannot execute swap:', { swapQuote: !!swapQuote, isValid }) + if (!swapQuoteData || !isValid) { + console.error('❌ Cannot execute swap:', { swapQuote: !!swapQuoteData, isValid }) return } @@ -254,17 +212,14 @@ export default function Swap() { const result = await executeSwapMutation.mutateAsync({ inputMint: fromTokenProps.address, outputMint: toTokenProps.address, - inputAmount: amountIn, + inputAmount, slippage: maxSlippage, - poolAddress: swapQuote.poolAddress + pool: swapRouteData?.pools[0] }) - toast.success( - `Swap successful! Received ${result.actualOutputAmount.toFixed(6)} ${toTokenProps.symbol}`, - { - duration: 5000 - } - ) + toast.success(result.message, { + duration: 5000 + }) // Reset form setAmountIn('') @@ -276,6 +231,25 @@ export default function Swap() { } } + const buttonDisplay = useCallback(() => { + if (executeSwapMutation.isPending) return 'Swapping...' + else if (isSwapQuoteLoading) return 'Computing...' + else if (!isTokenPairValid) return 'Select Different Tokens' + else if (!isInputPositive) return 'Enter Valid Amount' + else if (!isBalanceEnough) return 'Insufficient Balance' + else if (!swapRouteData) return 'No Pool Available' + else if (isQuoteError) return 'Failed to Get Quote' + else return 'Swap' + }, [ + executeSwapMutation.isPending, + isBalanceEnough, + isInputPositive, + isQuoteError, + isSwapQuoteLoading, + isTokenPairValid, + swapRouteData + ]) + return (

@@ -301,23 +275,23 @@ export default function Swap() {
0 ? outputAmount.toString() : ''} - setInputAmount={() => {}} // Output is read-only - disable={true} + inputAmount={inputType === 'to' ? amountIn : outputAmount} + setInputAmount={handleQuoteInputChange} />
@@ -347,44 +321,43 @@ export default function Swap() {

)} - {/* Error state */} - {swapQuoteQuery.error && ( -

- Error:{' '} - {swapQuoteQuery.error instanceof Error - ? swapQuoteQuery.error.message - : 'Failed to get quote'} -

- )} - {/* Pool information */} - {swapQuote && ( + {swapQuoteData && (

Pool TVL:{' '} - ${swapQuote.poolTvl.toLocaleString()} + ${swapQuoteData.poolTvl.toLocaleString()}

)} {/* Routing information */} - {swapRouteQuery.data && ( + {swapRouteData && (

- Route: {swapRouteQuery.data.type} + Route: {swapRouteData.type}

)} {/* Warning for no liquidity */} - {canSwapQuery.data === false && ( + {swapRouteData && !swapRouteData?.pools[0] && (

No liquidity pool available for this token pair

)} + {/* Error state */} + {isQuoteError && ( +

+ Error:{' '} + {getSwapQuoteQuery.error instanceof Error + ? getSwapQuoteQuery.error.message + : 'Failed to get quote'} +

+ )} + {/* Input validation warnings */} - {amountIn && !isAmountPositive && ( + {isInputPositive === false && (

Please enter a valid positive amount

)} - - {amountIn && isBaseTokenBalanceNotEnough && ( + {isBalanceEnough === false && (

Insufficient {fromTokenProps.symbol} balance

)}
@@ -394,29 +367,37 @@ export default function Swap() {

Rate

{exchangeRate}

-
-

Minimum Received

-

{minimumReceived}

-
+ {inputType === 'from' && ( +
+

Minimum Received

+

{minimumReceived}

+
+ )} + {inputType === 'to' && ( +
+

Maximum Input

+

{maximumInput}

+
+ )}

Price Impact

5 && 'text-red-500', - swapQuote?.priceImpact && - swapQuote.priceImpact > 1 && - swapQuote.priceImpact <= 5 && + swapQuoteData?.priceImpact && swapQuoteData.priceImpact > 5 && 'text-red-500', + swapQuoteData?.priceImpact && + swapQuoteData.priceImpact > 1 && + swapQuoteData.priceImpact <= 5 && 'text-yellow-500' )} > {priceImpact}

- {swapQuote && ( + {swapQuoteData && (

Trading Fee

-

{swapQuote.feeRate.toFixed(2)}%

+

{swapQuoteData.feeRate.toFixed(2)}%

)}
@@ -435,34 +416,21 @@ export default function Swap() { {(isSwapQuoteLoading || executeSwapMutation.isPending) && ( )} - {executeSwapMutation.isPending - ? 'Swapping...' - : isSwapQuoteLoading - ? 'Computing...' - : !hasValidTokenPair - ? 'Select Different Tokens' - : canSwapQuery.data === false - ? 'No Pool Available' - : !isAmountPositive - ? 'Enter Amount' - : isBaseTokenBalanceNotEnough - ? 'Insufficient Balance' - : 'Swap'} + {buttonDisplay()} {/* Enhanced Token Selection Dialog */} - - - - burnTokensMutation.mutate({ amount: Number(burnTokenAmount), decimals: tokenDetailData?.decimals! }), + burnTokensMutation.mutate({ + amount: Number(burnTokenAmount), + decimals: tokenDetailData?.decimals! + }), onChange: (e: ChangeEvent) => setBurnTokenAmount(e.target.value) }, { @@ -247,14 +251,18 @@ export default function TokenDetail({ params }: { params: { address: string } }) type: 'input', tip: 'Add more token supply to your account', pending: mintTokensMutation.isPending, - disabled: isDisabled || mintTokenAmount === '' || Number(mintTokenAmount) <= 0 || isMintRevoked, + disabled: + isDisabled || mintTokenAmount === '' || Number(mintTokenAmount) <= 0 || isMintRevoked, errorMessages: () => { if (mintTokenAmount !== '' && Number(mintTokenAmount) <= 0) { return { message: 'The value should be greater than 0' } } }, action: () => - mintTokensMutation.mutate({ amount: Number(mintTokenAmount), decimals: tokenDetailData?.decimals! }), + mintTokensMutation.mutate({ + amount: Number(mintTokenAmount), + decimals: tokenDetailData?.decimals! + }), onChange: (e: ChangeEvent) => setMintTokenAmount(e.target.value) } ] @@ -324,7 +332,8 @@ export default function TokenDetail({ params }: { params: { address: string } }) }, [revokeFreezeAuthoritiMutation.data, revokeFreezeAuthoritiMutation.isSuccess]) useEffect(() => { - if (lockMetadataMutation.isSuccess && lockMetadataMutation.data) toast.success(lockMetadataMutation.data.message) + if (lockMetadataMutation.isSuccess && lockMetadataMutation.data) + toast.success(lockMetadataMutation.data.message) }, [lockMetadataMutation.data, lockMetadataMutation.isSuccess]) useEffect(() => { @@ -367,17 +376,26 @@ export default function TokenDetail({ params }: { params: { address: string } }) useEffect(() => { if (lockMetadataMutation.isError && lockMetadataMutation.error) - openErrorDialog({ title: 'We can not proceed your transaction', description: lockMetadataMutation.error.message }) + openErrorDialog({ + title: 'We can not proceed your transaction', + description: lockMetadataMutation.error.message + }) }, [openErrorDialog, lockMetadataMutation.error, lockMetadataMutation.isError]) useEffect(() => { if (burnTokensMutation.isError && burnTokensMutation.error) - openErrorDialog({ title: 'We can not proceed your transaction', description: burnTokensMutation.error.message }) + openErrorDialog({ + title: 'We can not proceed your transaction', + description: burnTokensMutation.error.message + }) }, [burnTokensMutation.error, burnTokensMutation.isError, openErrorDialog]) useEffect(() => { if (mintTokensMutation.isError && mintTokensMutation.error) - openErrorDialog({ title: 'We can not proceed your transaction', description: mintTokensMutation.error.message }) + openErrorDialog({ + title: 'We can not proceed your transaction', + description: mintTokensMutation.error.message + }) }, [mintTokensMutation.error, mintTokensMutation.isError, openErrorDialog]) if (getBalanceQuery.isLoading || getTokenDetailQuery.isLoading) { @@ -405,7 +423,7 @@ export default function TokenDetail({ params }: { params: { address: string } }) className={'md:flex hidden w-32 mb-3 text-main-black items-center space-x-2.5 text-xl'} > -

Tokens

+ Tokens

Manage Token - {pageTitle} @@ -427,7 +445,10 @@ export default function TokenDetail({ params }: { params: { address: string } }) value={overviewData.value} type={overviewData.type} onChange={(e) => - onUpdateInputPayload(overviewData.name as keyof TUpdateTokenMetadataPayload, e.target.value) + onUpdateInputPayload( + overviewData.name as keyof TUpdateTokenMetadataPayload, + e.target.value + ) } />

@@ -472,7 +493,7 @@ export default function TokenDetail({ params }: { params: { address: string } })
-
{optionData.label}
+

{optionData.label}

@@ -483,7 +504,9 @@ export default function TokenDetail({ params }: { params: { address: string } }) placeholder="Enter amount" type="number" /> -

{optionData.errorMessages?.()?.message}

+

+ {optionData.errorMessages?.()?.message} +

@@ -519,7 +542,9 @@ export default function TokenDetail({ params }: { params: { address: string } }) className="bg-main-green md:ml-0 ml-9 rounded-[8px] text-sm w-auto md:max-w-none max-w-[171px] min-w-[114px] h-7 md:px-3 px-0 py-1.5 font-medium" > {optionData.pending && } - {optionData.label !== 'Lock Metadata' ? `Revoke ${optionData.label}` : optionData.label} + {optionData.label !== 'Lock Metadata' + ? `Revoke ${optionData.label}` + : optionData.label} )}
diff --git a/src/app/(walletConnected)/tokens/create/page.tsx b/src/app/(walletConnected)/tokens/create/page.tsx index bff7927..1f37294 100644 --- a/src/app/(walletConnected)/tokens/create/page.tsx +++ b/src/app/(walletConnected)/tokens/create/page.tsx @@ -8,18 +8,28 @@ import { useForm } from 'react-hook-form' import { NoBalanceAlert } from '@/components/common/Alert' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import FileInput from '@/features/tokens/components/form/FileInput' -import FormProgressLine, { type CreateTokenStepProps } from '@/features/tokens/components/form/FormProgressLine' +import FormProgressLine, { + type CreateTokenStepProps +} from '@/features/tokens/components/form/FormProgressLine' import SuccessDialog from '@/features/tokens/components/form/SuccessDialog' import CreateTokenOverview from '@/features/tokens/components/form/TokenOverview' import { useCreateToken } from '@/features/tokens/services' import { TCreateTokenDataResponse, TCreateTokenPayload } from '@/features/tokens/types' import { CreateTokenValidation } from '@/features/tokens/validation' -import { useGetBalance } from '@/services/wallet' +import { useGetBBABalance } from '@/services/wallet' import { useErrorDialog } from '@/stores/errorDialog' type FieldName = keyof TCreateTokenPayload @@ -65,7 +75,7 @@ export default function CreateToken() { const watchTokenSupply = form.watch('supply') const createTokenMutation = useCreateToken() - const getBalanceQuery = useGetBalance() + const getBalanceQuery = useGetBBABalance() const isNoBalance = getBalanceQuery.isError || !getBalanceQuery.data || getBalanceQuery.data === 0 const [currentStep, setCurrentStep] = useState(0) @@ -180,17 +190,9 @@ export default function CreateToken() { if (supply > LIMIT_OF_SIXTH_DECIMALS) form.setValue('decimals', '6') }, [form, watchTokenSupply]) - if (getBalanceQuery.isLoading) - return ( -
- -

Please wait...

-
- ) - if (createTokenMutation.isPending) return ( -
+

Creating your token...

@@ -198,28 +200,32 @@ export default function CreateToken() { return (
- {responseData && } + {responseData && ( + + )} {isNoBalance && } -

+

QUICK TOKEN GENERATOR

{currentStep === 0 && ( - + - Token Details + + Token Details + Basic details about your token - -
+ +
Token Symbol - Token Icon + + Token Icon + Enhance your token with a stunning icon! @@ -337,7 +345,9 @@ export default function CreateToken() { {currentStep === 2 && ( - Features + + Features + Extra feature for your token @@ -349,9 +359,12 @@ export default function CreateToken() { render={({ field }) => (
- Revoke Freeze + + Revoke Freeze + - To create a liquidity pool, it's necessary to Revoke Freeze Authority of the Token. + To create a liquidity pool, it's necessary to Revoke Freeze Authority + of the Token.
@@ -370,9 +383,12 @@ export default function CreateToken() { render={({ field }) => (
- Revoke Mint + + Revoke Mint + - Another essential step to ensure reliability among users is revoking the mint authority. + Another essential step to ensure reliability among users is revoking the + mint authority.
@@ -391,7 +407,9 @@ export default function CreateToken() { render={({ field }) => (
- Immutable Metadata + + Immutable Metadata + Enhance security and trust by locking token metadata. diff --git a/src/app/(walletConnected)/tokens/page.tsx b/src/app/(walletConnected)/tokens/page.tsx index 0d97b19..139f783 100644 --- a/src/app/(walletConnected)/tokens/page.tsx +++ b/src/app/(walletConnected)/tokens/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Loader2, Plus } from 'lucide-react' +import { Plus } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' @@ -34,55 +34,49 @@ export default function Tokens() { const getTokenDataQuery = useGetTokens() const getLPTokenDataQuery = useGetLPTokens() - const tokenData = getTokenDataQuery.data ? mapToTokenListPropsList(getTokenDataQuery.data.data) : [] - const lpTokenData = getLPTokenDataQuery.data ? mapToTokenListPropsList(getLPTokenDataQuery.data.data) : [] + const tokenData = getTokenDataQuery.data + ? mapToTokenListPropsList(getTokenDataQuery.data.data) + : [] + const lpTokenData = getLPTokenDataQuery.data + ? mapToTokenListPropsList(getLPTokenDataQuery.data.data) + : [] - const isLoading = getTokenDataQuery.isPending || (activeTab === 'lp-tokens' && getLPTokenDataQuery.isPending) - - if (isLoading) { - return ( -
- -

Please wait...

-
- ) - } + const isLoading = + getTokenDataQuery.isPending || (activeTab === 'lp-tokens' && getLPTokenDataQuery.isPending) return ( -
-
-
-

My Tokens

+
+
+
+

My Tokens

View and manage all tokens you've created using Quick Token Generator.

- {/* Create Token Button */} - + Create Token
- - + Regular Tokens - LP Tokens ({lpTokenData.length}) + LP Tokens @@ -90,6 +84,7 @@ export default function Tokens() { getTokenDataQuery.refetch()} isRefreshing={getTokenDataQuery.isRefetching} + isLoading={isLoading} columns={TokenListColumns} data={tokenData} /> @@ -100,13 +95,16 @@ export default function Tokens() { withoutAction onRefresh={() => getLPTokenDataQuery.refetch()} isRefreshing={getLPTokenDataQuery.isRefetching} + isLoading={isLoading} columns={TokenListColumns} data={lpTokenData} /> ) : (

No LP tokens found

-

LP tokens are earned when you provide liquidity to pools

+

+ LP tokens are earned when you provide liquidity to pools +

)} diff --git a/src/app/(walletConnected)/wrapping/page.tsx b/src/app/(walletConnected)/wrapping/page.tsx index 03a98a8..450905f 100644 --- a/src/app/(walletConnected)/wrapping/page.tsx +++ b/src/app/(walletConnected)/wrapping/page.tsx @@ -3,22 +3,21 @@ import { useEffect, useState } from 'react' import toast from 'react-hot-toast' -import { balanceFormater } from '@/components/common/WalletButton' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import WrapBalanceItem from '@/features/wrapping/components/WrapBalanceItem' import WrapContent from '@/features/wrapping/components/WrapContentCard' -import { useGetWBBABalance, useUnwrapBBA, useWrapBBA } from '@/features/wrapping/services' -import { useGetBalance } from '@/services/wallet' -import StaticTokens from '@/staticData/tokens' +import { useUnwrapBBA, useWrapBBA } from '@/features/wrapping/services' +import { getBBAFromDaltons } from '@/lib/token' +import { useGetBBABalance, useGetWBBABalance } from '@/services/wallet' import { useErrorDialog } from '@/stores/errorDialog' export default function Wrapping() { const wrapBBAMutation = useWrapBBA() const unwrapWBBAMutation = useUnwrapBBA() - const getBBABalance = useGetBalance() + const getBBABalance = useGetBBABalance() const getWBBABalance = useGetWBBABalance() - const BBABalance = balanceFormater(getBBABalance.data ?? 0) - const WBBABalance = (getWBBABalance.data?.balance ?? 0) / Math.pow(10, StaticTokens[0].decimals) + const BBABalance = getBBAFromDaltons(getBBABalance.data ?? 0) + const WBBABalance = getBBAFromDaltons(getBBABalance.data ?? 0) const [inputAmount, setInputAmount] = useState('') const isAmountPositive = Number(inputAmount) >= 0 @@ -71,9 +70,9 @@ export default function Wrapping() {

BBA Wrapping

-
+

Easily convert between BBA and WBBA. Use WBBA for swaps and liquidity pools. -

+

> { +export async function GET(request: NextRequest): Promise> { try { // Get search query parameter for filtering const { searchParams } = new URL(request.url) @@ -72,7 +68,7 @@ export async function POST(request: NextRequest): Promise { } const tokens = StaticTokens.filter((token) => addresses.includes(token.address)) - + return NextResponse.json( { message: `Successfully retrieved ${tokens.length} tokens`, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dfc049d..7df3be1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,7 +10,8 @@ import { TooltipProvider } from '@/components/ui/tooltip' const roboto = Roboto({ weight: ['100', '300', '400', '500', '700', '900'], - subsets: ['latin'] + subsets: ['latin'], + display: 'swap' }) export const metadata = { diff --git a/src/app/page.tsx b/src/app/page.tsx index be18f03..01ada46 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,154 +1,219 @@ 'use client' +import { motion, Variants } from 'motion/react' import Image from 'next/image' -import { useRouter } from 'next/navigation' +import Link from 'next/link' import FAQItem from '@/components/common/FaqItem' -import { Button, buttonVariants } from '@/components/ui/button' +import ThemeImage from '@/components/common/ThemeImage' +import { buttonVariants } from '@/components/ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' import FAQData from '@/staticData/faq' +const StepsData = [ + { + value: 'connect_wallet', + title: 'Connect Wallet', + description: + 'Link your wallet to the platform for seamless management of funds during the token creation process.', + image: '/connect_wallet.svg' + }, + { + value: 'token_details', + title: 'Add Token Details', + description: + 'Customize your token by providing essential information such as its name, symbol, and total supply.', + image: '/token_details.svg' + }, + { + value: 'add_features', + title: 'Add Features', + description: + "Tailor your token's functionality by choosing from a variety of features such as smart contract conditions and governance structures.", + image: '/add_features.svg' + }, + { + value: 'deploy_token', + title: 'Deploy Token', + description: + 'Finalize and launch your token onto the blockchain network, making it accessible for users within your ecosystem.', + image: '/deploy_token.svg' + } +] + +const containerVariants: Variants = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.2, + when: 'beforeChildren' + } + } +} + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 30 }, + show: { + opacity: 1, + y: 0, + transition: { + type: 'spring', + stiffness: 60, + damping: 12 + } + } +} + export default function Home() { - const router = useRouter() return ( <> -
-
-

- QUICK TOKEN GENERATOR -

-

- Effortlessly streamline token deployment across BBA network with our adaptable platform, engineered for - seamless integration -

-
- -
-
-
-

- Easy Steps to create your token -

-

- Create your token swiftly, in less than a minute. Choose the fastest, most secure
way to bring your - token to life, without any concessions -

-
- + + - - +

+ QUICK TOKEN GENERATOR +

+

+ Effortlessly streamline token deployment across BBA network with our adaptable + platform, engineered for seamless integration +

+
+ + -

Connect Wallet

-

- Link your wallet to the platform for seamless management of funds during the token creation process. -

- - -

Add Token Details

-

- Customize your token by providing essential information such as its name, symbol, and total supply. -

-
- -

Add Features

-

- Tailor your token's functionality by choosing from a variety of features such as smart contract - conditions and governance structures. -

-
- +
+ + +
+ + +

+ Easy Steps to create your token +

+

+ Create your token swiftly, in less than a minute. Choose the fastest, most secure{' '} +
way to bring your token to life, without any concessions +

+
+ + -

Deploy Token

-

- Finalize and launch your token onto the blockchain network, making it accessible for users within your - ecosystem. -

- - -
- - connect wallet image - - - connect wallet image - - - connect wallet image - - - connect wallet image - -
-
+ + {StepsData.map((step) => ( + +

{step.title}

+

+ {step.description} +

+
+ ))} +
+
+ {StepsData.map((step) => ( + + {step.title + + ))} +
+ +
+
-
-

+ + Frequently Asked Questions -

-
Ask us anything
-

+ + + Ask us anything + + Have any questions? We're here to assist you. -

- + Contact - -
-
+ + + {FAQData.map((value) => ( - + + + ))} -
+ ) } diff --git a/src/components/common/CopyButton.tsx b/src/components/common/CopyButton.tsx index 9155c78..edb85f7 100644 --- a/src/components/common/CopyButton.tsx +++ b/src/components/common/CopyButton.tsx @@ -44,7 +44,14 @@ export function CopyButton({ return ( - diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 5ef21ae..6c8b326 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,56 +1,77 @@ -'use client' - import Link from 'next/link' +import ThemeImage from '@/components/common/ThemeImage' import FooterMenu from '@/staticData/footer' import SocialMedia from '@/staticData/socialMedia' -import ThemeImage from '../common/ThemeImage' - export default function Footer() { const topMobileMenu = FooterMenu.slice(0, 3) const bottomMobileMenu = FooterMenu.slice(3) return ( -
-
- -
- {FooterMenu.map((menu) => ( - - {menu.name} - - ))} -
-
- {topMobileMenu.map((menu) => ( - - {menu.name} - - ))} -
-
- {bottomMobileMenu.map((menu) => ( - - {menu.name} - - ))} -
-
- {SocialMedia.map((media, index) => ( - - - - ))} +
+ +
+
+ +
+ {FooterMenu.map((menu) => ( + + {menu.name} + + ))} +
+
+ {topMobileMenu.map((menu) => ( + + {menu.name} + + ))} +
+
+ {bottomMobileMenu.map((menu) => ( + + {menu.name} + + ))} +
+
+ {SocialMedia.map((media, index) => ( + + + + ))} +
+

Š 2024 BTI Group OÜ

-

Š 2024 BTI Group OÜ

) } diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 65c8c65..da0013c 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -1,5 +1,6 @@ 'use client' +import { VisuallyHidden } from '@radix-ui/react-visually-hidden' import { ChevronRight } from 'lucide-react' import Link from 'next/link' import { useTheme } from 'next-themes' @@ -8,15 +9,17 @@ import { IoMdClose } from 'react-icons/io' import { IoSunnySharp, IoMoonSharp } from 'react-icons/io5' import { RxHamburgerMenu } from 'react-icons/rx' -import { useIsMobile } from '@/hooks/isMobile' -import NavMenu from '@/staticData/navbar' - -import SelectCluster from '../common/SelectCluster' -import ThemeImage from '../common/ThemeImage' -import CustomWalletButton from '../common/WalletButton' -import { useCluster } from '../providers/ClusterProvider' -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion' -import { Button } from '../ui/button' +import SelectCluster from '@/components/common/SelectCluster' +import ThemeImage from '@/components/common/ThemeImage' +import CustomWalletButton from '@/components/common/WalletButton' +import { useCluster } from '@/components/providers/ClusterProvider' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from '@/components/ui/accordion' +import { Button } from '@/components/ui/button' import { Drawer, DrawerClose, @@ -24,7 +27,7 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger -} from '../ui/drawer' +} from '@/components/ui/drawer' import { NavigationMenu, NavigationMenuContent, @@ -32,34 +35,43 @@ import { NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger -} from '../ui/navigation-menu' -import { Skeleton } from '../ui/skeleton' +} from '@/components/ui/navigation-menu' +import { Skeleton } from '@/components/ui/skeleton' +import { useIsMobile } from '@/hooks/isMobile' +import NavMenu from '@/staticData/navbar' function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme() + const isDark = resolvedTheme === 'dark' return ( ) } function MobileMenuDrawer() { const { clusters, setCluster, cluster } = useCluster() - const handleSelect = (item: typeof cluster) => setCluster(item) return ( - @@ -68,9 +80,16 @@ function MobileMenuDrawer() { className="h-screen rounded-none top-0 mt-0 right-0 left-auto w-4/6" > - + + Mobile Menu Title + - @@ -164,7 +183,7 @@ export default function Navbar() { useEffect(() => setMounted(true), []) return ( -
-
+
+ {isLoading && + Array.from({ length: 5 }).map((_, index) => ( + + ))} {table.getRowModel().rows?.length > 0 && table.getRowModel().rows.map((row) => { const original = row.original as NftCardProps return })}
- {table.getRowModel().rows?.length === 0 &&

No Result

} + {!isLoading && table.getRowModel().rows?.length === 0 && ( +

No Result

+ )} ) } diff --git a/src/features/nfts/services.ts b/src/features/nfts/services.ts index 28068a3..80adcab 100644 --- a/src/features/nfts/services.ts +++ b/src/features/nfts/services.ts @@ -20,7 +20,7 @@ import BN from 'bn.js' import { ZodError } from 'zod' import SERVICES_KEY from '@/constants/service' -import { getTokenAccounts } from '@/lib/tokenAccount' +import { getTokenAccounts } from '@/lib/token' import { TCreateCollectionPayload, @@ -107,8 +107,19 @@ export const useCreateNFT = () => { daltons, programId: TOKEN_PROGRAM_ID }), - createInitializeMintInstruction(mintKeypair.publicKey, 0, ownerAddress, ownerAddress, TOKEN_PROGRAM_ID), - createAssociatedTokenAccountInstruction(ownerAddress, tokenATA, ownerAddress, mintKeypair.publicKey), + createInitializeMintInstruction( + mintKeypair.publicKey, + 0, + ownerAddress, + ownerAddress, + TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + ownerAddress, + tokenATA, + ownerAddress, + mintKeypair.publicKey + ), createMintToInstruction( mintKeypair.publicKey, tokenATA, @@ -122,7 +133,10 @@ export const useCreateNFT = () => { createAccountTx.partialSign(mintKeypair) const accountSignature = await sendTransaction(createAccountTx, connection) - await connection.confirmTransaction({ signature: accountSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: accountSignature, ...latestBlockhash }, + 'confirmed' + ) // Create the metadata account @@ -163,7 +177,10 @@ export const useCreateNFT = () => { const createdMetadataTx = new Transaction().add(createdMetadataIx) const metadataSignature = await sendTransaction(createdMetadataTx, connection) - await connection.confirmTransaction({ signature: metadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: metadataSignature, ...latestBlockhash }, + 'confirmed' + ) const dataResponse = { ownerAddress: ownerAddress.toBase58(), @@ -176,9 +193,11 @@ export const useCreateNFT = () => { }, onSuccess: () => Promise.all([ - client.invalidateQueries({ queryKey: [SERVICES_KEY.NFT.GET_NFT, ownerAddress?.toBase58()] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.NFT.GET_NFT, ownerAddress?.toBase58()] + }), + client.invalidateQueries({ + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -232,8 +251,19 @@ export const useCreateCollection = () => { daltons, programId: TOKEN_PROGRAM_ID }), - createInitializeMintInstruction(mintKeypair.publicKey, 0, ownerAddress, ownerAddress, TOKEN_PROGRAM_ID), - createAssociatedTokenAccountInstruction(ownerAddress, tokenATA, ownerAddress, mintKeypair.publicKey), + createInitializeMintInstruction( + mintKeypair.publicKey, + 0, + ownerAddress, + ownerAddress, + TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + ownerAddress, + tokenATA, + ownerAddress, + mintKeypair.publicKey + ), createMintToInstruction( mintKeypair.publicKey, tokenATA, @@ -247,7 +277,10 @@ export const useCreateCollection = () => { createAccountTx.partialSign(mintKeypair) const accountSignature = await sendTransaction(createAccountTx, connection) - await connection.confirmTransaction({ signature: accountSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: accountSignature, ...latestBlockhash }, + 'confirmed' + ) // Create the metadata account @@ -291,7 +324,10 @@ export const useCreateCollection = () => { const createdMetadataTx = new Transaction().add(createdMetadataIx) const metadataSignature = await sendTransaction(createdMetadataTx, connection) - await connection.confirmTransaction({ signature: metadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: metadataSignature, ...latestBlockhash }, + 'confirmed' + ) const dataResponse = { ownerAddress: ownerAddress.toBase58(), @@ -304,16 +340,22 @@ export const useCreateCollection = () => { }, onSuccess: () => Promise.all([ - client.invalidateQueries({ queryKey: [SERVICES_KEY.NFT.GET_COLLECTION, ownerAddress?.toBase58()] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.NFT.GET_COLLECTION, ownerAddress?.toBase58()] + }), + client.invalidateQueries({ + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) } export const useValidateOffChainMetadata = () => - useMutation({ + useMutation< + TValidateMetadataSuccessResponse, + TValidateMetadataErrorResponse, + TValidateMetadataOffChainPayload + >({ mutationKey: ['validate-metadata'], mutationFn: async (payload) => { try { diff --git a/src/features/swap/components/SwapItem.tsx b/src/features/swap/components/SwapItem.tsx index 61e700c..ddd0928 100644 --- a/src/features/swap/components/SwapItem.tsx +++ b/src/features/swap/components/SwapItem.tsx @@ -6,27 +6,26 @@ import { capitalCase } from 'text-case' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' - -import { TTokenProps } from '../types' +import { Skeleton } from '@/components/ui/skeleton' +import type { TTradeableTokenProps } from '@/features/tokens/types' interface SwapItemProps { type: 'from' | 'to' - tokenProps: TTokenProps + tokenProps: TTradeableTokenProps inputAmount: string balance: number price: number setInputAmount: (inputAmount: string) => void setTokenProps?: () => void + isTokenLoading?: boolean noTitle?: boolean - noCheckBalance?: boolean disable?: boolean } export default function SwapItem({ type, + isTokenLoading, noTitle = false, - noCheckBalance = false, disable = false, tokenProps, inputAmount, @@ -35,17 +34,15 @@ export default function SwapItem({ setTokenProps, setInputAmount }: SwapItemProps) { - const isBalanceNotEnough = !noCheckBalance && Number(inputAmount) > balance - const isAmountPositive = Number(inputAmount) >= 0 - const isInValid = isBalanceNotEnough || !isAmountPositive - const onMaxClick = () => setInputAmount(balance.toString()) return (
{!noTitle &&
{capitalCase(type)}
}
- {setTokenProps ? ( + {isTokenLoading ? ( + + ) : setTokenProps ? ( )} - {isBalanceNotEnough && ( -

Balance is not enough

- )} - {!isAmountPositive && ( -

- Amount can not be negative -

- )}
-

- Balance:{' '} - {balance.toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 6 - })}{' '} - {tokenProps.symbol} -

+
+

Balance:

+ {isTokenLoading ? ( + + ) : ( +

+ {balance.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 6 + })}{' '} + {tokenProps.symbol} +

+ )} +

≈${' '} {price.toLocaleString(undefined, { minimumFractionDigits: 2, - maximumFractionDigits: 12 + maximumFractionDigits: 6 })}

diff --git a/src/features/swap/services.ts b/src/features/swap/services.ts index 57f76f9..639a5d2 100644 --- a/src/features/swap/services.ts +++ b/src/features/swap/services.ts @@ -1,4 +1,3 @@ -import * as BufferLayout from '@bbachain/buffer-layout' import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, @@ -20,690 +19,204 @@ import { type TransactionInstruction } from '@bbachain/web3.js' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import axios from 'axios' -import { TGetTokensResponse } from '@/app/api/tokens/route' -import ENDPOINTS from '@/constants/endpoint' +import REGEX from '@/constants/regex' import SERVICES_KEY from '@/constants/service' -import { bbaTodaltons, daltonsToBBA } from '@/lib/bbaWrapping' -import { getTokenAccounts2 } from '@/lib/tokenAccount' -import { ExtendedMintInfo, isNativeBBA } from '@/staticData/tokens' - -import { getAllPoolsFromOnchain, OnchainPoolData } from '../liquidityPool/onchain' -import { TGetTokenDataResponse, TGetTokenResponse } from '../tokens/types' -import { getTokenData } from '../tokens/utils' - +import { useGetPools } from '@/features/liquidityPool/services' +import type { + TExecuteSwapPayload, + TExecuteSwapResponse, + TExecuteSwapResponseData, + TGetSwapQuotePayload, + TGetSwapQuoteResponse, + TGetSwapRoutePayload, + TGetSwapRouteResponse, + TSwapValidationPayload, + TSwapValidationResponse +} from '@/features/swap/types' import { - TGetSwappableTokensResponse, - TGetTokenPriceData, - TGetUserBalanceData, - TGetSwapTransactionPayload, - TTokenProps, - TGetSwapTransactionData -} from './types' - -interface RawTokenSwap { - version: number - isInitialized: boolean - bumpSeed: number - poolTokenProgramId: PublicKey - tokenAccountA: PublicKey - tokenAccountB: PublicKey - tokenPool: PublicKey - mintA: PublicKey - mintB: PublicKey - feeAccount: PublicKey - tradeFeeNumerator: bigint - tradeFeeDenominator: bigint - ownerTradeFeeNumerator: bigint - ownerTradeFeeDenominator: bigint - ownerWithdrawFeeNumerator: bigint - ownerWithdrawFeeDenominator: bigint - hostFeeNumerator: bigint - hostFeeDenominator: bigint - curveType: number - curveParameters: Uint8Array -} - -export const publicKey = (property: string = 'publicKey') => { - return BufferLayout.blob(32, property) -} - -/** - * Layout for a 64bit unsigned value - */ -export const uint64 = (property: string = 'uint64') => { - return BufferLayout.blob(8, property) -} - -export const TokenSwapLayout = BufferLayout.struct([ - BufferLayout.u8('version'), - BufferLayout.u8('isInitialized'), - BufferLayout.u8('bumpSeed'), - publicKey('tokenProgramId'), - publicKey('tokenAccountA'), - publicKey('tokenAccountB'), - publicKey('tokenPool'), - publicKey('mintA'), - publicKey('mintB'), - publicKey('feeAccount'), - uint64('tradeFeeNumerator'), - uint64('tradeFeeDenominator'), - uint64('ownerTradeFeeNumerator'), - uint64('ownerTradeFeeDenominator'), - uint64('ownerWithdrawFeeNumerator'), - uint64('ownerWithdrawFeeDenominator'), - uint64('hostFeeNumerator'), - uint64('hostFeeDenominator'), - BufferLayout.u8('curveType'), - BufferLayout.blob(32, 'curveParameters') -]) - -export const useGetSwappableTokens = () => - useQuery({ - queryKey: [SERVICES_KEY.SWAP.GET_SWAPPABLE_TOKEN], - queryFn: async () => { - const res = await axios.get(ENDPOINTS.RAYDIUM.GET_MINT_LIST) - const swappableTokensData = res.data.data.mintList as TTokenProps[] - return { message: 'Successfully get swappable tokens data', data: swappableTokensData } - } - }) - -/** - * Hook to fetch tokens from internal API endpoint - * This replaces the onchain token fetching for better performance and reliability - */ -export const useGetTokensFromAPI = (searchQuery?: string) => - useQuery({ - queryKey: [SERVICES_KEY.SWAP.GET_SWAPPABLE_TOKEN + '_api', searchQuery], - queryFn: async () => { - const params = new URLSearchParams() - if (searchQuery) { - params.append('search', searchQuery) - params.append('includeAddress', 'true') - } - - const url = searchQuery - ? `${ENDPOINTS.API.GET_TOKENS}?${params.toString()}` - : ENDPOINTS.API.GET_TOKENS - - const res = await axios.get(url) - return res.data as TGetTokensResponse - }, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 3 - }) - -export const useGetSwappableTokens2 = () => { - const { connection } = useConnection() - return useQuery({ - queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN], - queryFn: async () => { - const tokenAccounts = await getTokenAccounts2(connection) - const tokenData = await Promise.all( - tokenAccounts.map(async (account) => { - const mintKey = new PublicKey(account.mintAddress) - return await getTokenData(connection, mintKey) - }) - ) - - const filteredTokenData = tokenData.filter( - (token): token is TGetTokenDataResponse => token !== null - ) - - return { - message: `Successfully get token`, - data: filteredTokenData - } - } - }) -} - -export const useGetSwappableTokens3 = () => { - const { connection } = useConnection() - const { publicKey: ownerAddress } = useWallet() - return useQuery({ - queryKey: ['get-swappable-token-3'], - queryFn: async () => { - if (!ownerAddress) throw new Error('No wallet connected') - const accountInfo = await connection.getAccountInfo(ownerAddress) - console.log('account info, ', accountInfo!.data) - if (accountInfo === null) { - throw new Error('Failed to find account') - } - - if (!accountInfo.owner.equals(TOKEN_SWAP_PROGRAM_ID)) { - throw new Error(`Invalid owner: ${JSON.stringify(accountInfo.owner)}`) - } - - const data = new Uint8Array( - accountInfo.data.buffer, - accountInfo.data.byteOffset, - accountInfo.data.byteLength - ) - return { data: TokenSwapLayout.decode(data) } - } - }) -} - -export const useGetUserBalanceByMint = ({ mintAddress }: { mintAddress: string }) => { - const { publicKey: ownerAddress } = useWallet() - const { connection } = useConnection() - return useQuery({ - queryKey: [SERVICES_KEY.SWAP.GET_USER_BALANCE_BY_MINT, mintAddress], - queryFn: async () => { - if (!ownerAddress) throw new Error('No wallet connected') + calculateInputAmount, + calculateOutputAmount, + calculatePriceImpact, + findBestPool +} from '@/features/swap/utils' +import { isNativeBBA, getBBAFromDaltons, getDaltonsFromBBA, formatTokenBalance } from '@/lib/token' - try { - // Import the native BBA detection function - const { isNativeBBA } = await import('@/staticData/tokens') - - // Handle native BBA token differently - if (isNativeBBA(mintAddress)) { - console.log('đŸĒ™ Fetching native BBA balance...') - const balance = await connection.getBalance(ownerAddress) - console.log('💰 Native BBA balance:', balance, 'daltons') - return { balance } - } - - // Handle SPL tokens (existing logic) - console.log('đŸĒ™ Fetching SPL token balance for:', mintAddress) - const mint = new PublicKey(mintAddress) - const ata = await getAssociatedTokenAddress(mint, ownerAddress) - const balanceAmount = await connection.getTokenAccountBalance(ata) - console.log('💰 SPL token balance:', balanceAmount.value.amount, 'base units') - - return { balance: Number(balanceAmount.value.amount) } - } catch (e) { - console.error('Balance fetch error:', e) - return { balance: 0 } - } - }, - enabled: !!mintAddress && !!ownerAddress - }) -} - -export const useGetTokenPrice = ({ mintAddress }: { mintAddress: string }) => - useQuery({ - queryKey: [SERVICES_KEY.SWAP.GET_TOKEN_PRICE, mintAddress], - queryFn: async () => { - const res = await axios.get(ENDPOINTS.RAYDIUM.GET_TOKEN_PRICE_BY_MINT, { - params: { - mints: mintAddress - } - }) - const usdRate = res.data.data[mintAddress] ?? 0 - return { usdRate } - }, - enabled: !!mintAddress, - refetchInterval: 60000 - }) - -export const useGetCoinGeckoTokenPrice = ({ coinGeckoId }: { coinGeckoId?: string }) => - useQuery({ - queryKey: [SERVICES_KEY.SWAP.GET_COIN_GECKO_TOKEN_PRICE, coinGeckoId], - queryFn: async () => { - if (!coinGeckoId) return 0 - const res = await axios.get(ENDPOINTS.COIN_GECKO.GET_SIMPLE_PRICE, { - params: { - ids: coinGeckoId, - vs_currencies: 'usd' - } - }) - const usdRate = res.data[coinGeckoId].usd ?? 0 - // Only log price if it's actually different from previous value to reduce spam - console.log(`💰 ${coinGeckoId} price: $${usdRate}`) - return usdRate - }, - enabled: !!coinGeckoId, - refetchInterval: 300000, // Reduced to 5 minutes to avoid spam - staleTime: 60000 // Cache for 1 minute - }) - -export const useGetSwapTransactionByMint = ({ - swapType, +export const useGetSwapQuote = ({ + pool, inputMint, outputMint, - amount, - decimals, - slippage -}: TGetSwapTransactionPayload) => { - const amountPayload = Number(amount) * 10 ** decimals - const slippageBps = slippage * 100 - return useQuery({ + inputAmount, + slippage = 0.5, + inputType = 'from' +}: TGetSwapQuotePayload) => { + return useQuery({ queryKey: [ - SERVICES_KEY.SWAP.GET_SWAP_TRANSACTION, - swapType, + SERVICES_KEY.SWAP.GET_SWAP_QUOTE, + pool?.address, inputMint, outputMint, - amount, - decimals, - slippage + inputAmount, + slippage, + inputType ], queryFn: async () => { - const baseType = swapType === 'BaseIn' ? 'in' : 'out' - const res = await axios.get( - ENDPOINTS.RAYDIUM.GET_SWAP_TRANSACTION_BY_MINT + `/swap-base-${baseType}`, - { - params: { - inputMint, - outputMint, - amount: amountPayload, - slippageBps, - txVersion: 'V0' - } - } - ) - return res.data - }, - enabled: !!amount || amount === '0', - refetchInterval: 60000 - }) -} - -/** - * Enhanced swap services for BBAChain using onchain liquidity pools - */ - -/** - * Calculate output amount using constant product formula (x * y = k) - * @param inputAmount - Amount to swap in - * @param inputReserve - Reserve of input token in pool - * @param outputReserve - Reserve of output token in pool - * @param feeRate - Pool fee rate (e.g., 0.003 for 0.3%) - * @returns Output amount after fees - */ -export function calculateOutputAmount( - inputAmount: number, - inputReserve: number, - outputReserve: number, - feeRate: number -): number { - if (inputAmount <= 0 || inputReserve <= 0 || outputReserve <= 0) { - console.log('❌ Invalid input parameters for calculation') - return 0 - } - - // Apply fee to input amount - const inputAmountWithFee = inputAmount * (1 - feeRate) - - // Constant product formula: (x + dx) * (y - dy) = x * y - // Solving for dy: dy = (y * dx) / (x + dx) - const outputAmount = (outputReserve * inputAmountWithFee) / (inputReserve + inputAmountWithFee) - - const result = Math.max(0, outputAmount) - - return result -} - -/** - * Calculate price impact for a swap - * @param inputAmount - Amount being swapped - * @param inputReserve - Input token reserve - * @param outputReserve - Output token reserve - * @param feeRate - Pool fee rate - * @returns Price impact as percentage - */ -export function calculatePriceImpact( - inputAmount: number, - inputReserve: number, - outputReserve: number, - feeRate: number -): number { - if (inputAmount <= 0 || inputReserve <= 0 || outputReserve <= 0) return 0 - - // Current price (no slippage) - const currentPrice = outputReserve / inputReserve - - // Price after swap - const outputAmount = calculateOutputAmount(inputAmount, inputReserve, outputReserve, feeRate) - const effectivePrice = outputAmount / inputAmount - - // Price impact percentage - const priceImpact = Math.abs((currentPrice - effectivePrice) / currentPrice) * 100 - - return priceImpact -} - -/** - * Find the best pool for a token pair - * @param pools - Array of available pools - * @param inputMint - Input token mint address - * @param outputMint - Output token mint address - * @returns Best pool for the swap or null if no pool found - */ -export function findBestPool( - pools: OnchainPoolData[], - inputMint: string, - outputMint: string -): OnchainPoolData | null { - const availablePools = pools.filter((pool) => { - const hasInputToken = pool.mintA.address === inputMint || pool.mintB.address === inputMint - const hasOutputToken = pool.mintA.address === outputMint || pool.mintB.address === outputMint - return hasInputToken && hasOutputToken && pool.tvl > 0 - }) - - if (availablePools.length === 0) return null - - // Sort by TVL (Total Value Locked) to find the most liquid pool - availablePools.sort((a, b) => b.tvl - a.tvl) - - return availablePools[0] -} - -/** - * Enhanced hook to get swap quote using onchain pools - */ -export const useGetSwapQuote = ({ - inputMint, - outputMint, - inputAmount, - slippage = 0.5 -}: { - inputMint: string - outputMint: string - inputAmount: string - slippage?: number -}) => { - const { connection } = useConnection() - - return useQuery({ - queryKey: ['swap-quote', inputMint, outputMint, inputAmount, slippage], - queryFn: async () => { - console.log('🔄 Starting swap quote calculation:', { - inputMint, - outputMint, - inputAmount, - inputAmountNumber: Number(inputAmount), - slippage - }) - - if (!inputAmount || Number(inputAmount) <= 0) { - console.log('❌ Invalid input amount:', inputAmount) - return null - } - - if (inputMint === outputMint) { - console.log('❌ Same input and output mint') - return null - } + if (!pool) throw new Error('No pool found') + if (!inputAmount || Number(inputAmount) <= 0) return null + if (inputMint === outputMint) return null try { - // Get all pools from onchain - console.log('📊 Fetching pools from onchain...') - const pools = await getAllPoolsFromOnchain(connection) - console.log('📊 Pools fetched:', { - totalPools: pools.length, - poolAddresses: pools.map((p) => p.address).slice(0, 5) // First 5 for debugging - }) - - // Find the best pool for this token pair - const bestPool = findBestPool(pools, inputMint, outputMint) - console.log('🔍 Pool search result:', { - found: !!bestPool, - poolAddress: bestPool?.address, - poolTvl: bestPool?.tvl, - mintA: bestPool?.mintA?.symbol, - mintB: bestPool?.mintB?.symbol - }) - - if (!bestPool) { - console.log('❌ No liquidity pool found for pair:', { - inputMint, - outputMint, - availablePools: pools.map((p) => ({ - address: p.address, - mintA: p.mintA.address, - mintB: p.mintB.address, - symbols: `${p.mintA.symbol}/${p.mintB.symbol}` - })) - }) - throw new Error(`No liquidity pool found for this token pair`) - } - - // Determine which token is A and which is B - const isInputTokenA = bestPool.mintA.address === inputMint - console.log('🔄 Token direction:', { - isInputTokenA, - inputToken: isInputTokenA ? bestPool.mintA.symbol : bestPool.mintB.symbol, - outputToken: isInputTokenA ? bestPool.mintB.symbol : bestPool.mintA.symbol - }) + // determine which side is A vs B + const isInputTokenA = pool.mintA.address === inputMint const inputReserve = isInputTokenA - ? Number(bestPool.reserveA) / Math.pow(10, bestPool.mintA.decimals) - : Number(bestPool.reserveB) / Math.pow(10, bestPool.mintB.decimals) + ? formatTokenBalance(Number(pool.reserveA), pool.mintA.decimals) + : formatTokenBalance(Number(pool.reserveB), pool.mintB.decimals) const outputReserve = isInputTokenA - ? Number(bestPool.reserveB) / Math.pow(10, bestPool.mintB.decimals) - : Number(bestPool.reserveA) / Math.pow(10, bestPool.mintA.decimals) - - console.log('💰 Pool reserves:', { - inputReserve, - outputReserve, - feeRate: bestPool.feeRate, - inputReserveRaw: isInputTokenA - ? bestPool.reserveA.toString() - : bestPool.reserveB.toString(), - outputReserveRaw: isInputTokenA - ? bestPool.reserveB.toString() - : bestPool.reserveA.toString() - }) + ? formatTokenBalance(Number(pool.reserveB), pool.mintB.decimals) + : formatTokenBalance(Number(pool.reserveA), pool.mintA.decimals) - // Validate reserves - if (inputReserve <= 0 || outputReserve <= 0) { - console.error('❌ Invalid pool reserves:', { inputReserve, outputReserve }) - throw new Error('Pool has invalid reserves') - } + if (inputReserve <= 0 || outputReserve <= 0) throw new Error('Pool has invalid reserves') - const inputAmountNumber = Number(inputAmount) + const amount = Number(inputAmount) + const feeRateDecimal = pool.feeRate > 1 ? pool.feeRate / 100 : pool.feeRate - // Convert fee rate from percentage to decimal (1.0% -> 0.01) - const feeRateDecimal = bestPool.feeRate > 1 ? bestPool.feeRate / 100 : bestPool.feeRate + let baseAmount: number // always "from" side + let quoteAmount: number // always "to" side - console.log('🧮 Calculating swap amounts for:', { - inputAmountNumber, - inputReserve, - outputReserve, - feeRate: bestPool.feeRate, - feeRateDecimal, - feeRateDisplay: `${bestPool.feeRate}% -> ${(feeRateDecimal * 100).toFixed(2)}%` - }) + if (inputType === 'from') { + // user typed in base → normal forward calculation + baseAmount = amount + quoteAmount = calculateOutputAmount( + baseAmount, + inputReserve, + outputReserve, + feeRateDecimal + ) + } else { + // user typed in quote → back-calc base amount + quoteAmount = amount + baseAmount = calculateInputAmount( + quoteAmount, + inputReserve, + outputReserve, + feeRateDecimal + ) + } - const outputAmount = calculateOutputAmount( - inputAmountNumber, - inputReserve, - outputReserve, - feeRateDecimal - ) const priceImpact = calculatePriceImpact( - inputAmountNumber, + baseAmount, inputReserve, outputReserve, feeRateDecimal ) - console.log('📈 Calculation results:', { - inputAmount: inputAmountNumber, - outputAmount, - priceImpact - }) - - // Validate calculation results - if (outputAmount <= 0) { - console.error('❌ Invalid output amount:', outputAmount) - throw new Error('Cannot calculate valid output amount') - } - - // Calculate minimum received with slippage + // slippage handling const slippageMultiplier = 1 - slippage / 100 - const minimumReceived = outputAmount * slippageMultiplier + const minimumReceived = quoteAmount * slippageMultiplier + const maximumInput = baseAmount / slippageMultiplier - // Calculate exchange rate - const exchangeRate = outputAmount / inputAmountNumber + // exchange rate always from → to + const exchangeRate = quoteAmount / baseAmount - const result = { - inputAmount: inputAmountNumber, - outputAmount, + return { + inputAmount: baseAmount, // base side + outputAmount: quoteAmount, // quote side minimumReceived, + maximumInput, priceImpact, exchangeRate, - feeRate: bestPool.feeRate * 100, // Convert to percentage - poolAddress: bestPool.address, - poolTvl: bestPool.tvl, - inputToken: isInputTokenA ? bestPool.mintA : bestPool.mintB, - outputToken: isInputTokenA ? bestPool.mintB : bestPool.mintA - } - - console.log('✅ Swap quote calculation successful:', result) - return result + feeRate: pool.feeRate * 100, + poolAddress: pool.address, + poolTvl: pool.tvl, + inputToken: isInputTokenA ? pool.mintA : pool.mintB, + outputToken: isInputTokenA ? pool.mintB : pool.mintA + } as TGetSwapQuoteResponse } catch (error) { console.error('❌ Error in swap quote calculation:', error) throw error } }, enabled: + !!pool && !!inputMint && !!outputMint && !!inputAmount && Number(inputAmount) > 0 && inputMint !== outputMint, - staleTime: 10000, // 10 seconds - refetchInterval: 15000, // Refresh every 15 seconds + staleTime: 10000, + refetchInterval: 15000, retry: (failureCount, error) => { console.log('🔄 Retrying swap quote:', { failureCount, error: error?.message }) - return failureCount < 2 // Retry up to 2 times + return failureCount < 2 } }) } /** - * Enhanced hook to get available tokens from API with search - */ -export const useGetAvailableTokens = (searchQuery?: string) => { - return useQuery({ - queryKey: ['available-tokens', searchQuery], - queryFn: async () => { - console.log('🔍 Fetching available tokens:', { searchQuery }) - - const params = new URLSearchParams() - if (searchQuery) { - params.append('search', searchQuery) - params.append('includeAddress', 'true') - } - - const url = searchQuery - ? `${ENDPOINTS.API.GET_TOKENS}?${params.toString()}` - : ENDPOINTS.API.GET_TOKENS - - const response = await axios.get(url) - console.log('✅ Tokens fetched:', { - count: response.data?.data?.length || 0, - hasData: !!response.data?.data - }) - - const data = response.data.data as ExtendedMintInfo[] - return data - }, - staleTime: 60000, // 1 minute - enabled: true - }) -} - -/** - * Enhanced hook to get pools that contain a specific token + * Hook to get swap route information */ -export const useGetPoolsForToken = (tokenAddress: string) => { - const { connection } = useConnection() +export const useGetSwapRoute = ({ inputMint, outputMint }: TGetSwapRoutePayload) => { + const getPoolsQuery = useGetPools() + const pools = getPoolsQuery.data?.data - return useQuery({ - queryKey: ['pools-for-token', tokenAddress], + const getSwapRoute = useQuery({ + queryKey: [ + SERVICES_KEY.SWAP.GET_SWAP_ROUTE, + pools?.map((pool) => pool.address), + inputMint, + outputMint + ], queryFn: async () => { - if (!tokenAddress) return [] - - const allPools = await getAllPoolsFromOnchain(connection) - - return allPools.filter( - (pool) => pool.mintA.address === tokenAddress || pool.mintB.address === tokenAddress - ) + if (!pools) return null + const directPool = findBestPool(pools, inputMint, outputMint) + if (directPool) { + return { + type: 'direct', + pools: [directPool], + route: [inputMint, outputMint], + totalFeeRate: directPool.feeRate + } + } + // TODO: Implement multi-hop routing for indirect swaps + // For now, return null if no direct route exists + return null }, - enabled: !!tokenAddress, - staleTime: 60000 // 1 minute + enabled: !!inputMint && !!outputMint && inputMint !== outputMint, + staleTime: 10000, // 10 seconds + refetchInterval: 15000 // Refresh every 15 seconds }) -} - -/** - * Enhanced swap execution using BBAChain liquidity pools - */ - -export interface SwapExecutionParams { - inputMint: string - outputMint: string - inputAmount: string - slippage: number - poolAddress: string -} -export interface SwapExecutionResult { - signature: string - inputAmount: number - outputAmount: number - actualOutputAmount: number - priceImpact: number - executionTime: number - poolDetail?: OnchainPoolData + return { + ...getSwapRoute, + isLoading: getPoolsQuery.isLoading || getSwapRoute.isLoading, + isRefetching: getPoolsQuery.isRefetching || getSwapRoute.isRefetching + } } -/** - * Hook for executing swaps through BBAChain liquidity pools - */ export const useExecuteSwap = () => { const { connection } = useConnection() const { publicKey, sendTransaction } = useWallet() const queryClient = useQueryClient() - return useMutation({ + return useMutation({ + mutationKey: [SERVICES_KEY.SWAP.EXECUTE_SWAP, publicKey?.toBase58()], mutationFn: async (params) => { const startTime = Date.now() console.log('🔄 Swap execution started:', params) - if (!publicKey) { - throw new Error('Wallet not connected') - } + if (!publicKey) throw new Error('Wallet not connected') + if (!params.pool) throw new Error('No pool found') - const { inputMint, outputMint, inputAmount, slippage, poolAddress } = params + const { inputMint, outputMint, inputAmount, slippage, pool } = params // === BBA Detection (must be early) === const isInputBBA = isNativeBBA(inputMint) const isOutputBBA = isNativeBBA(outputMint) const isBBASwap = isInputBBA || isOutputBBA - // Get pool data to access swap authority and token accounts - const pools = await getAllPoolsFromOnchain(connection) - const pool = pools.find((p) => p.address === poolAddress) - if (!pool) { - throw new Error('Pool not found') - } - - // Debug: Check createSwapInstruction interface - console.log('🔍 Debug createSwapInstruction function:', { - name: createSwapInstruction.name, - length: createSwapInstruction.length - }) - - // === Handle BBA/WBBA Mint Mapping for Pool Matching === - // For pool matching, we need to use WBBA address when BBA is involved - const effectiveInputMint = isInputBBA ? NATIVE_MINT.toBase58() : inputMint - const effectiveOutputMint = isOutputBBA ? NATIVE_MINT.toBase58() : outputMint - - console.log('🔄 Effective mints for pool matching:', { - originalInputMint: inputMint, - originalOutputMint: outputMint, - effectiveInputMint, - effectiveOutputMint, - poolMintA: pool.mintA.address, - poolMintB: pool.mintB.address - }) - // Calculate all required values for debugging + const effectiveInputMint = isInputBBA ? NATIVE_MINT.toBase58() : inputMint const isInputTokenA = pool.mintA.address === effectiveInputMint const inputAmountNumber = Number(inputAmount) @@ -731,59 +244,27 @@ export const useExecuteSwap = () => { const slippageMultiplier = 1 - slippage / 100 const minimumOutputDaltons = Math.floor(expectedOutputDaltons * slippageMultiplier) - console.log('💰 All swap parameters ready:', { - inputAmount: inputAmountNumber, - inputAmountDaltons, - expectedOutput, - expectedOutputDaltons, - minimumOutputDaltons, - slippage: slippage + '%', - poolAddress, - isInputTokenA - }) - - // === BBA-AWARE Token Account Preparation === - console.log('🔍 Swap Analysis:', { - inputMint, - outputMint, - isInputBBA, - isOutputBBA, - isBBASwap, - requiresWrapping: isBBASwap - }) - let userInputTokenAccount: PublicKey let userOutputTokenAccount: PublicKey - let needsUnwrapping = false - // Track whether we created a temporary WBBA ATA for BBA → Token swaps let createdWBBAInputAccount = false let preTxInstructions: TransactionInstruction[] = [] let postTxInstructions: TransactionInstruction[] = [] if (isBBASwap) { - console.log('đŸĒ™ BBA swap detected - using WBBA for native BBA') - if (isInputBBA) { - // BBA → Token: Use WBBA account for input - console.log('💰 BBA → Token swap: Using WBBA account for input') - - // Check BBA balance const userBBABalance = await connection.getBalance(publicKey) - const requiredBBA = bbaTodaltons(inputAmountNumber) + const requiredBBA = getDaltonsFromBBA(inputAmountNumber) - if (userBBABalance < requiredBBA) { + if (userBBABalance < requiredBBA) throw new Error( - `Insufficient BBA balance. Required: ${inputAmountNumber} BBA, Available: ${daltonsToBBA(userBBABalance)} BBA` + `Insufficient BBA balance. Required: ${inputAmountNumber} BBA, Available: ${getBBAFromDaltons(userBBABalance)} BBA` ) - } - // Use WBBA (NATIVE_MINT) associated token account userInputTokenAccount = await getAssociatedTokenAddress(NATIVE_MINT, publicKey) // Create WBBA account if it doesn't exist and fund it const wbbaAccountInfo = await connection.getAccountInfo(userInputTokenAccount) if (!wbbaAccountInfo) { - console.log('📝 Creating WBBA account and funding with BBA...') const createWBBAIx = createAssociatedTokenAccountInstruction( publicKey, userInputTokenAccount, @@ -819,16 +300,12 @@ export const useExecuteSwap = () => { postTxInstructions.push(closeWbbaInputIx) } } else if (isOutputBBA) { - // Token → BBA: Swap to WBBA then unwrap - console.log('💰 Token → BBA swap: Will receive WBBA and unwrap to BBA') - // For input, use standard token account userInputTokenAccount = await getAssociatedTokenAddress( new PublicKey(inputMint), publicKey ) - // For output, use WBBA (NATIVE_MINT) associated token account userOutputTokenAccount = await getAssociatedTokenAddress(NATIVE_MINT, publicKey) // Create WBBA account if it doesn't exist @@ -844,8 +321,6 @@ export const useExecuteSwap = () => { preTxInstructions.push(createWBBAIx) } - // Add instruction to unwrap WBBA to BBA after swap - needsUnwrapping = true const closeWBBAIx = createCloseAccountInstruction( userOutputTokenAccount, publicKey, @@ -870,14 +345,12 @@ export const useExecuteSwap = () => { // Derive swap authority from pool account const [swapAuthority] = PublicKey.findProgramAddressSync( - [new PublicKey(poolAddress).toBuffer()], + [new PublicKey(pool.address).toBuffer()], TOKEN_SWAP_PROGRAM_ID ) - // Prepare accounts object for createSwapInstruction const accounts = { - // Required accounts based on @bbachain/spl-token-swap - tokenSwap: new PublicKey(poolAddress), + tokenSwap: new PublicKey(pool.address), authority: swapAuthority, userTransferAuthority: publicKey, source: userInputTokenAccount, @@ -932,7 +405,16 @@ export const useExecuteSwap = () => { userInputTokenAccount, publicKey, publicKey, - bbaTodaltons(inputAmountNumber) + getDaltonsFromBBA(inputAmountNumber) + ) + transaction.add(approveIx) + } else { + // for other spl tokens + const approveIx = createApproveInstruction( + userInputTokenAccount, + publicKey, + publicKey, + inputAmountDaltons ) transaction.add(approveIx) } @@ -950,20 +432,17 @@ export const useExecuteSwap = () => { // Send transaction const signature = await sendTransaction(transaction, connection) - console.log('📤 Transaction sent with signature:', signature) // Wait for confirmation const confirmation = await connection.confirmTransaction(signature, 'confirmed') - if (confirmation.value.err) { throw new Error(`Transaction failed: ${confirmation.value.err}`) } console.log('✅ Swap transaction confirmed!') - // Return execution result - return { + const responseData = { signature, inputAmount: inputAmountNumber, outputAmount: expectedOutput, @@ -976,99 +455,20 @@ export const useExecuteSwap = () => { ), executionTime: Date.now() - startTime, poolDetail: pool - } as SwapExecutionResult - }, - onSuccess: (result) => { - console.log('✅ Swap completed successfully:', result) - const poolId = result.poolDetail?.address - const baseMint = result.poolDetail?.mintA - const quoteMint = result.poolDetail?.mintB - - // Invalidate relevant queries to refresh balances and pool data - queryClient.invalidateQueries({ queryKey: ['swap-quote'] }) - queryClient.invalidateQueries({ queryKey: ['user-balance'] }) - queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.SWAP.GET_USER_BALANCE_BY_MINT] }) - queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.POOL.GET_POOLS] }) - queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.POOL.GET_POOL_BY_ID, poolId] }) - queryClient.invalidateQueries({ - queryKey: [ - SERVICES_KEY.POOL.GET_TRANSACTIONS_BY_POOL_ID, - poolId, - baseMint?.address, - quoteMint?.address - ] - }) - queryClient.invalidateQueries({ - queryKey: [ - SERVICES_KEY.POOL.GET_TRANSACTIONS_BY_POOL_ID, - poolId, - quoteMint?.address, - baseMint?.address - ] - }) - queryClient.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, publicKey?.toBase58()] - }) - }, - onError: (error) => { - console.error('❌ Swap failed:', error) - } - }) -} + } as TExecuteSwapResponseData -/** - * Hook to check if a swap is possible between two tokens - */ -export const useCanSwap = (inputMint: string, outputMint: string) => { - const { connection } = useConnection() + const outputSymbol = isInputTokenA ? pool.mintB.symbol : pool.mintA.symbol - return useQuery({ - queryKey: ['can-swap', inputMint, outputMint], - queryFn: async () => { - if (!inputMint || !outputMint || inputMint === outputMint) { - return false - } - - const pools = await getAllPoolsFromOnchain(connection) - const availablePool = findBestPool(pools, inputMint, outputMint) - - return !!availablePool - }, - enabled: !!inputMint && !!outputMint && inputMint !== outputMint, - staleTime: 30000 // 30 seconds - }) -} - -/** - * Hook to get swap route information - */ -export const useGetSwapRoute = (inputMint: string, outputMint: string) => { - const { connection } = useConnection() - - return useQuery({ - queryKey: ['swap-route', inputMint, outputMint], - queryFn: async () => { - if (!inputMint || !outputMint || inputMint === outputMint) { - return null - } - - const pools = await getAllPoolsFromOnchain(connection) - const directPool = findBestPool(pools, inputMint, outputMint) - - if (directPool) { - return { - type: 'direct', - pools: [directPool], - route: [inputMint, outputMint], - totalFeeRate: directPool.feeRate - } + // Return execution result + return { + message: `Swap successful! Received ${responseData.actualOutputAmount.toFixed(6)} ${outputSymbol}`, + data: responseData } - - // TODO: Implement multi-hop routing for indirect swaps - // For now, return null if no direct route exists - return null }, - enabled: !!inputMint && !!outputMint && inputMint !== outputMint, - staleTime: 60000 // 1 minute + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.SWAP.GET_SWAP_QUOTE] }) + queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE] }) + queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.WALLET.GET_TOKEN_BALANCE_BY_MINT] }) + } }) } diff --git a/src/features/swap/types.ts b/src/features/swap/types.ts index 26baa69..e86fff9 100644 --- a/src/features/swap/types.ts +++ b/src/features/swap/types.ts @@ -1,8 +1,33 @@ -import { MintInfo } from '@/features/liquidityPool/types' +import { PublicKey } from '@bbachain/web3.js' + +import { TOnchainPoolData } from '@/features/liquidityPool/types' +import { TTradeableTokenProps } from '@/features/tokens/types' import { TSuccessMessage } from '@/types' -export type SwapType = 'BaseIn' | 'BaseOut' +export type RawTokenSwap = { + version: number + isInitialized: boolean + bumpSeed: number + poolTokenProgramId: PublicKey + tokenAccountA: PublicKey + tokenAccountB: PublicKey + tokenPool: PublicKey + mintA: PublicKey + mintB: PublicKey + feeAccount: PublicKey + tradeFeeNumerator: bigint + tradeFeeDenominator: bigint + ownerTradeFeeNumerator: bigint + ownerTradeFeeDenominator: bigint + ownerWithdrawFeeNumerator: bigint + ownerWithdrawFeeDenominator: bigint + hostFeeNumerator: bigint + hostFeeDenominator: bigint + curveType: number + curveParameters: Uint8Array +} +export type SwapType = 'BaseIn' | 'BaseOut' export type RoutePlanStep = { poolId: string @@ -28,39 +53,82 @@ export type SwapData = { routePlan: RoutePlanStep[] } -export type TTokenProps = MintInfo - -export type TGetUserBalanceData = { - balance: number +export type TGetSwappableTokensResponse = TSuccessMessage & { + data: TTradeableTokenProps[] } -export type TGetTokenPriceData = { - usdRate: number +export type TPostSwapTokensResponse = TSuccessMessage & { + txId: string } +export type TGetSwapRoutePayload = { + inputMint: string + outputMint: string +} +export type TSwapValidationPayload = { + pool: TOnchainPoolData | undefined + inputMint: string + outputMint: string + inputAmount: number + userBalance: number +} -export type TGetSwapTransactionPayload = { - swapType: SwapType +export type TGetSwapQuotePayload = { + pool: TOnchainPoolData | undefined inputMint: string outputMint: string - amount: string - decimals: number - slippage: number + inputAmount: string + slippage?: number + inputType?: 'from' | 'to' } -export type TGetSwappableTokensResponse = TSuccessMessage & { - data: TTokenProps[] +export type TGetSwapQuoteResponse = { + inputAmount: number + outputAmount: number + minimumReceived?: number + maximumInput?: number + priceImpact: number + exchangeRate: number + feeRate: number + poolAddress: string + poolTvl: number + inputToken: TTradeableTokenProps + outputToken: TTradeableTokenProps } -export type TGetSwapTransactionData = { - id: string - success: boolean - version: string - data: SwapData +export type TGetSwapRouteResponse = { + type: string + pools: TOnchainPoolData[] + route: string[] + totalFeeRate: number } -export type TPostSwapTokensResponse = TSuccessMessage & { - txId: string +export type TSwapValidationResponse = { + isBalanceEnough: boolean + isInputPositive: boolean + isTokenPairValid: boolean } +export type TExecuteSwapPayload = { + pool: TOnchainPoolData | undefined + inputMint: string + outputMint: string + inputAmount: string + slippage: number +} + +export type TExecuteSwapResponseData = { + signature: string + inputAmount: number + outputAmount: number + actualOutputAmount: number + priceImpact: number + executionTime: number + poolDetail?: TOnchainPoolData +} + + +export type TExecuteSwapResponse = TSuccessMessage & { + data: TExecuteSwapResponseData +} diff --git a/src/features/swap/utils.ts b/src/features/swap/utils.ts index 0a2dfda..80db931 100644 --- a/src/features/swap/utils.ts +++ b/src/features/swap/utils.ts @@ -1,6 +1,125 @@ -import StaticTokens from '@/staticData/tokens' +import { TOnchainPoolData } from '../liquidityPool/types' -export function getCoinGeckoId(address: string) { - const coinGeckoId = StaticTokens.find((token) => address === token.address)?.coinGeckoId - return coinGeckoId +/** + * Calculate output amount using constant product formula (x * y = k) + * @param inputAmount - Amount to swap in + * @param inputReserve - Reserve of input token in pool + * @param outputReserve - Reserve of output token in pool + * @param feeRate - Pool fee rate (e.g., 0.003 for 0.3%) + * @returns Output amount after fees + */ +export function calculateOutputAmount( + inputAmount: number, + inputReserve: number, + outputReserve: number, + feeRate: number +): number { + if (inputAmount <= 0 || inputReserve <= 0 || outputReserve <= 0) { + console.log('❌ Invalid input parameters for calculation') + return 0 + } + + // Apply fee to input amount + const inputAmountWithFee = inputAmount * (1 - feeRate) + + // Constant product formula: (x + dx) * (y - dy) = x * y + // Solving for dy: dy = (y * dx) / (x + dx) + const outputAmount = (outputReserve * inputAmountWithFee) / (inputReserve + inputAmountWithFee) + const result = Math.max(0, outputAmount) + if (result >= outputReserve) throw new Error('Not enough liquidity for minimum output') + return result +} + +/** + * Calculate required input amount to get a desired output + * using constant product formula (x * y = k) + * @param desiredOutput - Target output amount + * @param inputReserve - Reserve of input token in pool + * @param outputReserve - Reserve of output token in pool + * @param feeRate - Pool fee rate (e.g., 0.003 for 0.3%) + * @returns Required input amount after accounting for fees + */ +export function calculateInputAmount( + desiredOutput: number, + inputReserve: number, + outputReserve: number, + feeRate: number +): number { + if (desiredOutput <= 0 || inputReserve <= 0 || outputReserve <= 0) { + console.log('❌ Invalid input parameters for inverse calculation') + return 0 + } + + // 🚨 Hard guard: user cannot request more than pool liquidity + if (desiredOutput >= outputReserve) throw new Error('Not enough liquidity for desired output') + + // Inverse formula (safe since desiredOutput < outputReserve) + const numerator = inputReserve * desiredOutput + const denominator = (outputReserve - desiredOutput) * (1 - feeRate) + + const inputAmount = numerator / denominator + return Math.max(0, inputAmount) +} + +/** + * Calculate price impact for a swap + * @param inputAmount - Amount being swapped + * @param inputReserve - Input token reserve + * @param outputReserve - Output token reserve + * @param feeRate - Pool fee rate + * @returns Price impact as percentage + */ +export function calculatePriceImpact( + inputAmount: number, + inputReserve: number, + outputReserve: number, + feeRate: number +): number { + if (inputAmount <= 0 || inputReserve <= 0 || outputReserve <= 0) return 0 + + // Current price (no slippage) + const currentPrice = outputReserve / inputReserve + + // Price after swap + const outputAmount = calculateOutputAmount(inputAmount, inputReserve, outputReserve, feeRate) + const effectivePrice = outputAmount / inputAmount + + // Price impact percentage + const priceImpact = Math.abs((currentPrice - effectivePrice) / currentPrice) * 100 + + return priceImpact +} + +/** + * Find the best pool for a token pair + * @param pools - Array of available pools + * @param inputMint - Input token mint address + * @param outputMint - Output token mint address + * @returns Best pool for the swap or null if no pool found + */ +export function findBestPool( + pools: TOnchainPoolData[], + inputMint: string, + outputMint: string +): TOnchainPoolData | null { + const availablePools = pools + .filter((pool) => { + const hasInputToken = pool.mintA.address === inputMint || pool.mintB.address === inputMint + const hasOutputToken = pool.mintA.address === outputMint || pool.mintB.address === outputMint + return hasInputToken && hasOutputToken && pool.tvl > 0 + }) + .map((pool) => { + const tvlScore = pool.tvl + const feeScore = 1 - pool.feeRate + const volumeScore = pool.volume24h + // (50% tvl, 30% fee, 20% volume) + const score = tvlScore * 0.5 + feeScore * 0.3 + volumeScore * 0.2 + return { ...pool, score } + }) + + if (availablePools.length === 0) return null + availablePools.sort((a, b) => b.score - a.score) + + // Return pool terbaik + return availablePools[0] } diff --git a/src/features/tokens/components/DataTable.tsx b/src/features/tokens/components/DataTable.tsx index d056a62..2c48297 100644 --- a/src/features/tokens/components/DataTable.tsx +++ b/src/features/tokens/components/DataTable.tsx @@ -28,8 +28,22 @@ import { DropdownMenuLabel } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { Skeleton } from '@/components/ui/skeleton' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { type TokenListProps } from '@/features/tokens/components/Columns' import { cn } from '@/lib/utils' @@ -38,6 +52,7 @@ interface DataTableProps { columns: ColumnDef[] data: TData[] onRefresh?: () => void + isLoading?: boolean isRefreshing?: boolean withoutAction?: boolean } @@ -46,6 +61,7 @@ export function DataTable({ columns, data, onRefresh, + isLoading, isRefreshing, withoutAction }: DataTableProps) { @@ -80,7 +96,8 @@ export function DataTable({ const filteredPools = table.getFilteredRowModel().rows.length const currentPage = table.getState().pagination.pageIndex + 1 const totalPages = table.getPageCount() - const startIndex = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 + const startIndex = + table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 const endIndex = Math.min(startIndex + table.getState().pagination.pageSize - 1, filteredPools) return ( @@ -92,13 +109,20 @@ export function DataTable({ placeholder="Search" value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)} + disabled={isLoading} className="max-w-sm rounded-[10px] pl-9 md:h-12 h-9 border-[1.4px] border-[#989898] dark:border-[#C6C6C6]" />
- @@ -123,7 +147,14 @@ export function DataTable({ - @@ -134,10 +165,16 @@ export function DataTable({
-
-

- {filteredPools === totalPools ? `${totalPools} total pools` : `${filteredPools} of ${totalPools} pools`} -

+
+ {isLoading ? ( + + ) : ( +

+ {filteredPools === totalPools + ? `${totalPools} total pools` + : `${filteredPools} of ${totalPools} pools`} +

+ )} {filteredPools > 0 && (

Showing{' '} @@ -156,6 +193,7 @@ export function DataTable({ onValueChange={(value) => { table.setPageSize(Number(value)) }} + disabled={isLoading} > @@ -172,66 +210,96 @@ export function DataTable({

- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender(header.column.columnDef.header, header.getContext())} - - ) - })} - + {isLoading ? ( + <> +
+ {['Token', 'Symbol', 'Token Supply', 'USDC', 'Created Date'].map((col) => ( + + ))} +
+ {/* Table rows */} + {[...Array(5)].map((_, row) => ( +
+ {[...Array(5)].map((_, col) => ( + + ))} +
))} -
- - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - const mintAddress = (row.original as TokenListProps).id + + ) : ( +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => { + const mintAddress = (row.original as TokenListProps).id - return ( - - {row.getVisibleCells().map((cell, index, arr) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - {!withoutAction && ( - - + {row.getVisibleCells().map((cell, index, arr) => ( + - - - - )} - - ) - }) - ) : ( - - - No results. - - - )} - -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + {!withoutAction && ( + + + + + + )} + + ) + }) + ) : ( + + + No results. + + + )} + + + )}
- {filteredPools > 0 && ( + {isLoading && } + {!isLoading && filteredPools > 0 && (
diff --git a/src/features/swap/components/TokenListDialog.tsx b/src/features/tokens/components/TradeableTokenListDialog.tsx similarity index 87% rename from src/features/swap/components/TokenListDialog.tsx rename to src/features/tokens/components/TradeableTokenListDialog.tsx index 5876123..9114661 100644 --- a/src/features/swap/components/TokenListDialog.tsx +++ b/src/features/tokens/components/TradeableTokenListDialog.tsx @@ -16,20 +16,19 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useGetTradeableTokens } from '@/features/tokens/services' +import { type TTradeableTokenProps } from '@/features/tokens/types' +import { TExtendedTradeableTokenProps } from '@/features/tokens/types' import { cn } from '@/lib/utils' -import { ExtendedMintInfo } from '@/staticData/tokens' - -import { useGetAvailableTokens } from '../services' -import { TTokenProps } from '../types' interface TokenListDialogProps { type: 'from' | 'to' isOpen: boolean setIsOpen: Dispatch> - selectedFrom: TTokenProps - selectedTo: TTokenProps - setSelectedFrom: (token: TTokenProps) => void - setSelectedTo: (token: TTokenProps) => void + selectedFrom: TTradeableTokenProps + selectedTo: TTradeableTokenProps + setSelectedFrom: (token: TTradeableTokenProps) => void + setSelectedTo: (token: TTradeableTokenProps) => void } const SelectTokenTips = { @@ -39,7 +38,7 @@ const SelectTokenTips = { noPoolTip: 'No liquidity pool available for this token pair' } as const -export default function TokenListDialog({ +export default function TradeableTokenListDialog({ type, isOpen, setIsOpen, @@ -65,9 +64,9 @@ export default function TokenListDialog({ data: tokensResponse, isLoading, error - } = useGetAvailableTokens(debouncedSearch.trim() || undefined) + } = useGetTradeableTokens(debouncedSearch.trim() || undefined) - const tokens: ExtendedMintInfo[] = tokensResponse ?? [] + const tokens = tokensResponse?.data ?? [] const selectedToken = () => { switch (type) { case 'from': @@ -77,14 +76,12 @@ export default function TokenListDialog({ } } - const onSelectToken = (token: TTokenProps) => { - // Prevent selecting the same token for both from and to - if (type === 'from' && selectedTo && token.address === selectedTo.address) { - return - } - if (type === 'to' && selectedFrom && token.address === selectedFrom.address) { - return - } + const onSelectToken = (token: TTradeableTokenProps) => { + const isSameAsSelected = + (type === 'from' && selectedTo?.address === token.address) || + (type === 'to' && selectedFrom?.address === token.address) + + if (isSameAsSelected) return switch (type) { case 'from': @@ -95,16 +92,14 @@ export default function TokenListDialog({ } // Filter out already selected token from the opposite side - const filteredTokens = tokens.filter((token: ExtendedMintInfo) => { + const filteredTokens = tokens.filter((token: TExtendedTradeableTokenProps) => { if (token.isNative) return false - if (type === 'from' && selectedTo) { - return token.address !== selectedTo.address - } - if (type === 'to' && selectedFrom) { - return token.address !== selectedFrom.address - } - return true + const isSameAsSelected = + (type === 'from' && selectedTo?.address === token.address) || + (type === 'to' && selectedFrom?.address === token.address) + + return !isSameAsSelected }) const handleClearSearch = () => { @@ -178,7 +173,7 @@ export default function TokenListDialog({ {!isLoading && !error && filteredTokens.length > 0 && (
- {filteredTokens.map((token: TTokenProps) => { + {filteredTokens.map((token: TTradeableTokenProps) => { const isSelected = selectedToken()?.address === token.address const isDisabled = (type === 'from' && selectedTo?.address === token.address) || diff --git a/src/features/tokens/components/form/FileInput.tsx b/src/features/tokens/components/form/FileInput.tsx index c1f20c6..ec4887e 100644 --- a/src/features/tokens/components/form/FileInput.tsx +++ b/src/features/tokens/components/form/FileInput.tsx @@ -36,7 +36,7 @@ export default function FileInput(props: FileInputProps) {
{preview ? ( <> - Uploaded preview + Uploaded preview
) } diff --git a/src/features/tokens/services.ts b/src/features/tokens/services.ts index 2b47f3e..2cd94a1 100644 --- a/src/features/tokens/services.ts +++ b/src/features/tokens/services.ts @@ -20,13 +20,11 @@ import { } from '@bbachain/spl-token-metadata' import { useConnection, useWallet } from '@bbachain/wallet-adapter-react' import { Keypair, PublicKey, SystemProgram, Transaction } from '@bbachain/web3.js' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query' +import axios from 'axios' +import ENDPOINTS from '@/constants/endpoint' import SERVICES_KEY from '@/constants/service' -import { uploadIconToPinata, uploadMetadataToPinata } from '@/lib/pinata' -import { getTokenAccounts } from '@/lib/tokenAccount' -import { TSuccessMessage } from '@/types' - import { TCreateTokenPayload, TCreateTokenResponse, @@ -37,9 +35,41 @@ import { TUpdateTokenMetadataPayload, TUpdateTokenMetadataResponse, TBurnTokenPayload, - TMintTokenPayload -} from './types' -import { getTokenData, getTokenMetadata, getLPTokenData } from './utils' + TMintTokenPayload, + TGetTradeableTokenResponse, + TExtendedTradeableTokenProps, + TGetAllTokenPrices +} from '@/features/tokens/types' +import { + getTokenData, + getTokenMetadata, + getLPTokenData, + getTokenPriceByCoinGeckoId +} from '@/features/tokens/utils' +import { uploadIconToPinata, uploadMetadataToPinata } from '@/lib/pinata' +import { getTokenAccounts } from '@/lib/token' +import { TSuccessMessage } from '@/types' + +export const useGetTradeableTokens = (searchQuery?: string) => + useQuery({ + queryKey: [SERVICES_KEY.TOKEN.GET_TRADEABLE_TOKEN, searchQuery], + queryFn: async () => { + const params = new URLSearchParams() + if (searchQuery) { + params.append('search', searchQuery) + params.append('includeAddress', 'true') + } + + const url = searchQuery + ? `${ENDPOINTS.API.GET_TRADEABLE_TOKENS}?${params.toString()}` + : ENDPOINTS.API.GET_TRADEABLE_TOKENS + + const res = await axios.get(url) + return res.data as TGetTradeableTokenResponse + }, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: 3 + }) export const useGetTokens = () => { const { publicKey: ownerAddress } = useWallet() @@ -57,7 +87,9 @@ export const useGetTokens = () => { }) ) - const filteredTokenData = tokenData.filter((token): token is TGetTokenDataResponse => token !== null) + const filteredTokenData = tokenData.filter( + (token): token is TGetTokenDataResponse => token !== null + ) return { message: `Successfully get token with address ${ownerAddress.toBase58()}`, @@ -89,6 +121,111 @@ export const useGetTokenDetail = ({ mintAddress }: { mintAddress: string }) => { }) } +export const useGetLPTokens = () => { + const { publicKey: ownerAddress } = useWallet() + const { connection } = useConnection() + return useQuery({ + queryKey: [SERVICES_KEY.TOKEN.GET_LP_TOKEN, ownerAddress?.toBase58()], + queryFn: async () => { + if (!ownerAddress) throw new Error('No wallet connected') + + const tokenAccounts = await getTokenAccounts(connection, ownerAddress) + const lpTokenData = await Promise.all( + tokenAccounts.map(async (account) => { + const mintKey = new PublicKey(account.mintAddress) + return await getLPTokenData(connection, mintKey) + }) + ) + + const filteredLPTokenData = lpTokenData.filter( + (token): token is TGetTokenDataResponse => token !== null + ) + + return { + message: `Successfully get LP tokens with address ${ownerAddress.toBase58()}`, + data: filteredLPTokenData + } + }, + enabled: !!ownerAddress, + staleTime: 30000, // 30 seconds + gcTime: 5 * 60 * 1000 // 5 minutes + }) +} + +export const useGetTokenPriceByCoinGeckoId = ({ coinGeckoId }: { coinGeckoId?: string }) => { + return useQuery({ + queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_PRICE_BY_COIN_GECKO_ID, coinGeckoId], + queryFn: async () => { + if (!coinGeckoId) return 0 + try { + const tokenPriceRes = await getTokenPriceByCoinGeckoId(coinGeckoId) + const usdRate = tokenPriceRes[coinGeckoId].usd + return usdRate + } catch (e) { + console.error('failed to get price ', e) + return 0 + } + }, + refetchInterval: 1000 * 60, + staleTime: 1000 * 60, + meta: { persist: true } + }) +} + +export const useGetMultipleTokenPricesByCoinGeckoIds = ({ + coinGeckoIds +}: { + coinGeckoIds: (string | undefined)[] +}) => { + return useQueries({ + queries: coinGeckoIds.map((id) => ({ + queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_PRICE_BY_COIN_GECKO_ID, id], + queryFn: async () => { + try { + if (!id) return 0 + const tokenPriceRes = await getTokenPriceByCoinGeckoId(id) + return tokenPriceRes[id]?.usd ?? 0 // ✅ fallback if missing + } catch (e) { + console.warn(`Failed to fetch price for ${id}, defaulting to 0`) + return 0 // ✅ ensure query resolves, not rejects + } + }, + enabled: !!id, + refetchInterval: 300000, + staleTime: 60000 + })) + }) +} + +// really useful to prevent coin gecko api price from 429 too many requests +export const useGetAllTokenPrices = () => + useQuery({ + queryKey: [SERVICES_KEY.TOKEN.GET_ALL_TOKEN_PRICES], + queryFn: async () => { + const getTradeableTokens = await axios.get(ENDPOINTS.API.GET_TRADEABLE_TOKENS) + const tradeableTokens = getTradeableTokens.data.data as TExtendedTradeableTokenProps[] + const filteredTokens = tradeableTokens.filter((token) => !token.isNative) + + const tradeableTokensWithPrices = await Promise.all( + filteredTokens.map(async (token) => { + if (!token.coinGeckoId) return { [token.symbol]: 0 } + const getPrice = await getTokenPriceByCoinGeckoId(token.coinGeckoId) + const price = getPrice[token.coinGeckoId].usd + return { [token.symbol]: price } + }) + ) + + const prices = tradeableTokensWithPrices.reduce>( + (acc, curr) => ({ ...acc, ...curr }), + {} + ) + + return prices + }, + meta: { persist: true }, + refetchInterval: 1000 * 60 + }) + export const useCreateToken = () => { const { connection } = useConnection() const { publicKey: ownerAddress, sendTransaction } = useWallet() @@ -114,8 +251,19 @@ export const useCreateToken = () => { daltons, programId: TOKEN_PROGRAM_ID }), - createInitializeMintInstruction(mintKeypair.publicKey, decimals, ownerAddress, ownerAddress, TOKEN_PROGRAM_ID), - createAssociatedTokenAccountInstruction(ownerAddress, tokenATA, ownerAddress, mintKeypair.publicKey), + createInitializeMintInstruction( + mintKeypair.publicKey, + decimals, + ownerAddress, + ownerAddress, + TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + ownerAddress, + tokenATA, + ownerAddress, + mintKeypair.publicKey + ), createMintToInstruction( mintKeypair.publicKey, tokenATA, @@ -129,7 +277,10 @@ export const useCreateToken = () => { createAccountTx.partialSign(mintKeypair) const accountSignature = await sendTransaction(createAccountTx, connection) - await connection.confirmTransaction({ signature: accountSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: accountSignature, ...latestBlockhash }, + 'confirmed' + ) // Create the metadata account @@ -180,18 +331,33 @@ export const useCreateToken = () => { createdMetadataTx.recentBlockhash = latestBlockhash.blockhash createdMetadataTx.feePayer = ownerAddress const metadataSignature = await sendTransaction(createdMetadataTx, connection) - await connection.confirmTransaction({ signature: metadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: metadataSignature, ...latestBlockhash }, + 'confirmed' + ) // Revoke authorities (mint/freeze) transaction if requested const revokeTx = new Transaction() if (payload.revoke_mint) { - revokeTx.add(createSetAuthorityInstruction(mintKeypair.publicKey, ownerAddress, AuthorityType.MintTokens, null)) + revokeTx.add( + createSetAuthorityInstruction( + mintKeypair.publicKey, + ownerAddress, + AuthorityType.MintTokens, + null + ) + ) } if (payload.revoke_freeze) { revokeTx.add( - createSetAuthorityInstruction(mintKeypair.publicKey, ownerAddress, AuthorityType.FreezeAccount, null) + createSetAuthorityInstruction( + mintKeypair.publicKey, + ownerAddress, + AuthorityType.FreezeAccount, + null + ) ) } @@ -199,7 +365,10 @@ export const useCreateToken = () => { revokeTx.recentBlockhash = latestBlockhash.blockhash revokeTx.feePayer = ownerAddress const revokeSignature = await sendTransaction(revokeTx, connection) - await connection.confirmTransaction({ signature: revokeSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: revokeSignature, ...latestBlockhash }, + 'confirmed' + ) } if (payload.immutable_metadata) { @@ -221,7 +390,10 @@ export const useCreateToken = () => { lockMetadataTx.recentBlockhash = latestBlockhash.blockhash lockMetadataTx.feePayer = ownerAddress const lockSignature = await sendTransaction(lockMetadataTx, connection) - await connection.confirmTransaction({ signature: lockSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: lockSignature, ...latestBlockhash }, + 'confirmed' + ) } const data = { @@ -239,7 +411,7 @@ export const useCreateToken = () => { queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN, ownerAddress?.toBase58()] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -310,7 +482,10 @@ export const useUpdateTokenMetadata = ({ mintAddress }: { mintAddress: string }) createdMetadataTx.recentBlockhash = latestBlockhash.blockhash createdMetadataTx.feePayer = ownerAddress const createMetadataSignature = await sendTransaction(createdMetadataTx, connection) - await connection.confirmTransaction({ signature: createMetadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: createMetadataSignature, ...latestBlockhash }, + 'confirmed' + ) } else { const updateMetadataAccountArgs: UpdateMetadataAccountArgs = { data: updatedOnChainMetadata, @@ -330,7 +505,10 @@ export const useUpdateTokenMetadata = ({ mintAddress }: { mintAddress: string }) updatedMetadataTx.recentBlockhash = latestBlockhash.blockhash updatedMetadataTx.feePayer = ownerAddress const updateMetadataSignature = await sendTransaction(updatedMetadataTx, connection) - await connection.confirmTransaction({ signature: updateMetadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: updateMetadataSignature, ...latestBlockhash }, + 'confirmed' + ) } const data = { @@ -351,7 +529,7 @@ export const useUpdateTokenMetadata = ({ mintAddress }: { mintAddress: string }) queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -376,7 +554,9 @@ export const useLockMetadata = ({ mintAddress }: { mintAddress: string }) => { const accountInfo = await connection.getAccountInfo(metadataPda) if (!accountInfo || !accountInfo?.data) - throw new Error('This address does not have metadata account, please update your metadata first.') + throw new Error( + 'This address does not have metadata account, please update your metadata first.' + ) const [metadata] = Metadata.deserialize(accountInfo?.data) @@ -399,7 +579,10 @@ export const useLockMetadata = ({ mintAddress }: { mintAddress: string }) => { updatedMetadataTx.recentBlockhash = latestBlockhash.blockhash updatedMetadataTx.feePayer = ownerAddress const updateMetadataSignature = await sendTransaction(updatedMetadataTx, connection) - await connection.confirmTransaction({ signature: updateMetadataSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: updateMetadataSignature, ...latestBlockhash }, + 'confirmed' + ) return { message: 'Successfully lock your metadata' } }, @@ -412,7 +595,7 @@ export const useLockMetadata = ({ mintAddress }: { mintAddress: string }) => { queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -428,10 +611,18 @@ export const useRevokeMintAuthority = ({ mintAddress }: { mintAddress: string }) if (!ownerAddress) throw new Error('Wallet not connected') const mintKey = new PublicKey(mintAddress) const latestBlockhash = await connection.getLatestBlockhash() - const revokeMintIx = createSetAuthorityInstruction(mintKey, ownerAddress, AuthorityType.MintTokens, null) + const revokeMintIx = createSetAuthorityInstruction( + mintKey, + ownerAddress, + AuthorityType.MintTokens, + null + ) const revokeMintTx = new Transaction().add(revokeMintIx) const revokeMintSignature = await sendTransaction(revokeMintTx, connection) - await connection.confirmTransaction({ signature: revokeMintSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: revokeMintSignature, ...latestBlockhash }, + 'confirmed' + ) return { message: 'Successfully revoked mint authority' } }, onSuccess: () => @@ -443,7 +634,7 @@ export const useRevokeMintAuthority = ({ mintAddress }: { mintAddress: string }) queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -459,10 +650,18 @@ export const useRevokeFreezeAuthority = ({ mintAddress }: { mintAddress: string if (!ownerAddress) throw new Error('Wallet not connected') const mintKey = new PublicKey(mintAddress) const latestBlockhash = await connection.getLatestBlockhash() - const revokeMintIx = createSetAuthorityInstruction(mintKey, ownerAddress, AuthorityType.FreezeAccount, null) + const revokeMintIx = createSetAuthorityInstruction( + mintKey, + ownerAddress, + AuthorityType.FreezeAccount, + null + ) const revokeMintTx = new Transaction().add(revokeMintIx) const revokeMintSignature = await sendTransaction(revokeMintTx, connection) - await connection.confirmTransaction({ signature: revokeMintSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: revokeMintSignature, ...latestBlockhash }, + 'confirmed' + ) return { message: 'Successfully revoked freeze authority' } }, onSuccess: () => @@ -474,7 +673,7 @@ export const useRevokeFreezeAuthority = ({ mintAddress }: { mintAddress: string queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -493,10 +692,20 @@ export const useBurnTokenSupply = ({ mintAddress }: { mintAddress: string }) => const amount = BigInt(Math.floor(payload.amount) * Math.pow(10, payload.decimals)) const tokenAccount = await getAssociatedTokenAddress(mintKey, ownerAddress) - const burnTokenIx = createBurnInstruction(tokenAccount, mintKey, ownerAddress, amount, [], TOKEN_PROGRAM_ID) + const burnTokenIx = createBurnInstruction( + tokenAccount, + mintKey, + ownerAddress, + amount, + [], + TOKEN_PROGRAM_ID + ) const burnTokenTx = new Transaction().add(burnTokenIx) const burnTokenSignature = await sendTransaction(burnTokenTx, connection) - await connection.confirmTransaction({ signature: burnTokenSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: burnTokenSignature, ...latestBlockhash }, + 'confirmed' + ) return { message: `Successfully burn ${payload.amount} tokens from your account` } }, onSuccess: () => @@ -508,7 +717,7 @@ export const useBurnTokenSupply = ({ mintAddress }: { mintAddress: string }) => queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) @@ -527,10 +736,20 @@ export const useMintTokenSupply = ({ mintAddress }: { mintAddress: string }) => const amount = BigInt(Math.floor(payload.amount) * Math.pow(10, payload.decimals)) const tokenAccount = await getAssociatedTokenAddress(mintKey, ownerAddress) - const mintTokenIx = createMintToInstruction(mintKey, tokenAccount, ownerAddress, amount, [], TOKEN_PROGRAM_ID) + const mintTokenIx = createMintToInstruction( + mintKey, + tokenAccount, + ownerAddress, + amount, + [], + TOKEN_PROGRAM_ID + ) const mintTokenTx = new Transaction().add(mintTokenIx) const mintTokenSignature = await sendTransaction(mintTokenTx, connection) - await connection.confirmTransaction({ signature: mintTokenSignature, ...latestBlockhash }, 'confirmed') + await connection.confirmTransaction( + { signature: mintTokenSignature, ...latestBlockhash }, + 'confirmed' + ) return { message: `Successfully mint ${payload.amount} tokens to your account` } }, onSuccess: () => @@ -542,37 +761,8 @@ export const useMintTokenSupply = ({ mintAddress }: { mintAddress: string }) => queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN_DETAIL, ownerAddress?.toBase58(), mintAddress] }), client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) ]) }) } - -export const useGetLPTokens = () => { - const { publicKey: ownerAddress } = useWallet() - const { connection } = useConnection() - return useQuery({ - queryKey: [SERVICES_KEY.TOKEN.GET_TOKEN, 'LP_TOKENS', ownerAddress?.toBase58()], - queryFn: async () => { - if (!ownerAddress) throw new Error('No wallet connected') - - const tokenAccounts = await getTokenAccounts(connection, ownerAddress) - const lpTokenData = await Promise.all( - tokenAccounts.map(async (account) => { - const mintKey = new PublicKey(account.mintAddress) - return await getLPTokenData(connection, mintKey) - }) - ) - - const filteredLPTokenData = lpTokenData.filter((token): token is TGetTokenDataResponse => token !== null) - - return { - message: `Successfully get LP tokens with address ${ownerAddress.toBase58()}`, - data: filteredLPTokenData - } - }, - enabled: !!ownerAddress, - staleTime: 30000, // 30 seconds - gcTime: 5 * 60 * 1000 // 5 minutes - }) -} diff --git a/src/features/tokens/types.ts b/src/features/tokens/types.ts index b44076e..06c0c2e 100644 --- a/src/features/tokens/types.ts +++ b/src/features/tokens/types.ts @@ -2,7 +2,24 @@ import { Collection, Creator, Uses } from '@bbachain/spl-token-metadata' import { z } from 'zod' import { type CreateTokenValidation } from '@/features/tokens/validation' -import { TSuccessMessage } from '@/types' +import { type TSuccessMessage } from '@/types' + +export type TTradeableTokenProps = { + chainId?: number + address: string + programId?: string + logoURI?: string + symbol: string + name: string + decimals: number + tags?: string[] + extensions?: Record +} + +export type TExtendedTradeableTokenProps = TTradeableTokenProps & { + coinGeckoId?: string + isNative: boolean +} export type UploadToMetadataPayload = { name: string @@ -56,6 +73,20 @@ export type TCreateTokenDataResponse = { metadata: UploadToMetadataPayload } +export type TGetTokenPriceByCoinGeckoIdData = { + [key: string]: { + usd: number + } +} + +export type TGetAllTokenPrices = { + [key: string]: number +} + +export type TGetTradeableTokenResponse = TSuccessMessage & { + data: TExtendedTradeableTokenProps[] +} + export type TGetTokenResponse = TSuccessMessage & { data: TGetTokenDataResponse[] } diff --git a/src/features/tokens/utils.ts b/src/features/tokens/utils.ts index 3b60570..cf7f55e 100644 --- a/src/features/tokens/utils.ts +++ b/src/features/tokens/utils.ts @@ -3,9 +3,35 @@ import { Metadata, PROGRAM_ID } from '@bbachain/spl-token-metadata' import { Connection, PublicKey } from '@bbachain/web3.js' import axios from 'axios' -import { isLikelyLPToken } from '@/staticData/tokens' - -import { TGetTokenDataResponse, TTokenMetadata, TTokenMetadataOffChain, TTokenMetadataOffChainData } from './types' +import ENDPOINTS from '@/constants/endpoint' +import SERVICES_KEY from '@/constants/service' +import { + TGetTokenDataResponse, + TTokenMetadata, + TTokenMetadataOffChain, + TTokenMetadataOffChainData, + TGetTokenPriceByCoinGeckoIdData +} from '@/features/tokens/types' + +const isLikelyLPToken = (decimals: number, mintAuthorityAddress: string | null): boolean => { + // Basic check: LP tokens typically have 2 decimals and no mint authority + if (decimals !== 2 || !mintAuthorityAddress) return false + + // Common non-LP addresses to exclude + const commonAddresses = [ + '11111111111111111111111111111111', // System Program + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program + 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' // Associated Token Program + ] + + // If mint authority is a common system address, it's not an LP token + if (commonAddresses.includes(mintAuthorityAddress)) return false + + // Additional heuristic: LP tokens usually have PDA authorities + // Most user tokens don't use complex PDA structures + // This is a reasonable heuristic for filtering + return true +} export const getTokenMetadata = async (connection: Connection, metadataAddress: PublicKey) => { const initialMetadataOffChainData: TTokenMetadataOffChainData = { @@ -167,3 +193,21 @@ export const getLPTokenData = async ( return null } } + +export const getTokenPriceByCoinGeckoId = async ( + coinGeckoId: string +): Promise => { + const res = await axios.get( + ENDPOINTS.COIN_GECKO.GET_SIMPLE_PRICE, + { + params: { + ids: coinGeckoId, + vs_currencies: 'usd' + } + } + ) + + if (!res.data) throw new Error('Failed to get token price') + + return res.data +} diff --git a/src/features/wrapping/services.ts b/src/features/wrapping/services.ts index 4d1b48b..2d04140 100644 --- a/src/features/wrapping/services.ts +++ b/src/features/wrapping/services.ts @@ -6,38 +6,14 @@ import { NATIVE_MINT } from '@bbachain/spl-token' import { useConnection, useWallet } from '@bbachain/wallet-adapter-react' -import { PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@bbachain/web3.js' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { SystemProgram, Transaction, TransactionInstruction } from '@bbachain/web3.js' +import { useMutation, useQueryClient } from '@tanstack/react-query' import SERVICES_KEY from '@/constants/service' -import { bbaTodaltons } from '@/lib/bbaWrapping' +import { TUnwrapResponse, TWrapPayload, TWrapResponse } from '@/features/wrapping/types' +import { getDaltonsFromBBA } from '@/lib/token' import StaticTokens from '@/staticData/tokens' -import { TGetUserBalanceData } from '../swap/types' - -import { TUnwrapResponse, TWrapPayload, TWrapResponse } from './types' - -export function useGetWBBABalance() { - const { publicKey: ownerAddress } = useWallet() - const { connection } = useConnection() - return useQuery({ - queryKey: [SERVICES_KEY.WRAPPING.GET_WBBA_BALANCE, ownerAddress?.toBase58()], - queryFn: async () => { - if (!ownerAddress) throw new Error('No wallet connected') - - try { - const mint = new PublicKey(NATIVE_MINT.toBase58()) - const ata = await getAssociatedTokenAddress(mint, ownerAddress) - const balanceAmount = await connection.getTokenAccountBalance(ata) - return { balance: Number(balanceAmount.value.amount) } - } catch (e) { - console.error('Balance fetch error:', e) - return { balance: 0 } - } - } - }) -} - export function useWrapBBA() { const { publicKey: ownerAddress, sendTransaction } = useWallet() const { connection } = useConnection() @@ -49,7 +25,7 @@ export function useWrapBBA() { try { const wrapTxInstructions: TransactionInstruction[] = [] - const bbaAmountDaltons = bbaTodaltons(payload.amount) + const bbaAmountDaltons = getDaltonsFromBBA(payload.amount) const wbbaAccount = await getAssociatedTokenAddress(NATIVE_MINT, ownerAddress) const wbbaAccountInfo = await connection.getAccountInfo(wbbaAccount) @@ -86,7 +62,7 @@ export function useWrapBBA() { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.WRAPPING.GET_WBBA_BALANCE, ownerAddress?.toBase58()] @@ -137,7 +113,7 @@ export function useUnwrapBBA() { if (!isUnwrapAll) { // 🔹 CASE Unwrap partial const remainingWBBAAmounts = Number(formattedWBBABalance) - payload.amount - const remainingWBBADaltons = bbaTodaltons(remainingWBBAAmounts) + const remainingWBBADaltons = getDaltonsFromBBA(remainingWBBAAmounts) const createWBBAIx = createAssociatedTokenAccountInstruction( ownerAddress, @@ -172,7 +148,7 @@ export function useUnwrapBBA() { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, ownerAddress?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()] }) queryClient.invalidateQueries({ queryKey: [SERVICES_KEY.WRAPPING.GET_WBBA_BALANCE, ownerAddress?.toBase58()] diff --git a/src/lib/bbaWrapping.ts b/src/lib/bbaWrapping.ts deleted file mode 100644 index 327fd5f..0000000 --- a/src/lib/bbaWrapping.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * BBA Wrapping/Unwrapping Utilities for BBAChain - * - * This module provides utilities for wrapping/unwrapping BBA tokens - * for use in liquidity pools and swaps, based on the patterns from examples. - */ - -import { - TOKEN_PROGRAM_ID, - NATIVE_MINT, - createInitializeAccountInstruction, - createSyncNativeInstruction, - createCloseAccountInstruction, - getAccount -} from '@bbachain/spl-token' -import { - Connection, - Keypair, - PublicKey, - Transaction, - SystemProgram, - sendAndConfirmTransaction, - BBA_DALTON_UNIT -} from '@bbachain/web3.js' - -/** - * Create token account manually (BBAChain compatible) - * Based on pattern from examples/01-create-lp-with-native.ts - */ -export async function createTokenAccountManual( - connection: Connection, - payer: Keypair, - mint: PublicKey, - owner: PublicKey -): Promise { - const tokenAccount = Keypair.generate() - - // Get minimum balance for token account - const tokenAccountSpace = 165 // Standard token account size - const rentExemptAmount = await connection.getMinimumBalanceForRentExemption(tokenAccountSpace) - - // Create account instruction - const createAccountIx = SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: tokenAccount.publicKey, - daltons: rentExemptAmount, - space: tokenAccountSpace, - programId: TOKEN_PROGRAM_ID - }) - - // Initialize token account instruction - const initAccountIx = createInitializeAccountInstruction(tokenAccount.publicKey, mint, owner) - - // Send transaction - const transaction = new Transaction().add(createAccountIx, initAccountIx) - await sendAndConfirmTransaction(connection, transaction, [payer, tokenAccount], { - commitment: 'confirmed' - }) - - return tokenAccount.publicKey -} - -/** - * Wrap BBA to WBBA (BBAChain compatible) - * Based on pattern from examples/04-swap-bba-usdt.ts - */ -export async function wrapBBAtoWBBA( - connection: Connection, - payer: Keypair, - owner: PublicKey, - amount: number // Amount in daltons -): Promise { - console.log(`🔄 Wrapping ${amount / BBA_DALTON_UNIT} BBA to WBBA...`) - - // Create WBBA token account manually (BBAChain compatible) - const wbbaAccount = await createTokenAccountManual(connection, payer, NATIVE_MINT, owner) - - // Transfer BBA daltons to token account - const transferIx = SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: wbbaAccount, - daltons: amount - }) - - // Sync native instruction - const syncIx = createSyncNativeInstruction(wbbaAccount) - - // Send transaction - const wrapTransaction = new Transaction().add(transferIx, syncIx) - await sendAndConfirmTransaction(connection, wrapTransaction, [payer], { - commitment: 'confirmed' - }) - - console.log(`✅ Wrapped ${amount / BBA_DALTON_UNIT} BBA to WBBA`) - console.log(`✅ WBBA Account: ${wbbaAccount.toBase58()}`) - - return wbbaAccount -} - -/** - * Unwrap WBBA to BBA (BBAChain working solution) - * Based on pattern from examples/04-swap-bba-usdt.ts - */ -export async function unwrapWBBAtoBBA( - connection: Connection, - payer: Keypair, - wbbaAccount: PublicKey -): Promise { - console.log('🔄 Unwrapping WBBA to BBA...') - - try { - // Check account state - const accountInfo = await getAccount(connection, wbbaAccount) - const balance = Number(accountInfo.amount) - - if (balance === 0) { - console.log('✅ WBBA account is empty, unwrap complete') - return true - } - - console.log(`💰 WBBA balance: ${balance / BBA_DALTON_UNIT} BBA`) - - // BBAChain method: Simply close account to unwrap - const closeAccountIx = createCloseAccountInstruction( - wbbaAccount, // account to close - payer.publicKey, // destination for daltons - payer.publicKey, // owner authority - [], // multiSigners - TOKEN_PROGRAM_ID // programId - ) - - const unwrapTransaction = new Transaction().add(closeAccountIx) - await sendAndConfirmTransaction(connection, unwrapTransaction, [payer], { - commitment: 'confirmed' - }) - - console.log('✅ Successfully unwrapped WBBA to BBA') - return true - } catch (error) { - console.log('â„šī¸ Unwrap not available on BBAChain, but WBBA = BBA') - - // Get final balance for user info - try { - const accountInfo = await getAccount(connection, wbbaAccount) - const balance = Number(accountInfo.amount) - - console.log(`💰 Final balance: ${(balance / BBA_DALTON_UNIT).toFixed(9)} WBBA`) - console.log('💡 WBBA can be used as BBA (1:1 equivalent)') - console.log('💡 Transfer to wallet or use in DeFi applications') - } catch { - console.log('💰 WBBA processed successfully') - } - - return false // Unwrap failed but it's OK - } -} - -/** - * Add BBA liquidity directly to pool account - * Used for pool initialization - transfers BBA directly to pool and syncs - */ -export async function addBBAToPoolAccount( - connection: Connection, - payer: Keypair, - poolAccount: PublicKey, - amount: number // Amount in daltons -): Promise { - console.log(`🔄 Adding ${amount / BBA_DALTON_UNIT} BBA to pool account...`) - - // Transfer BBA daltons directly to pool account - const transferToPoolIx = SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: poolAccount, - daltons: amount - }) - - // Sync native instruction to wrap BBA in the pool - const syncPoolIx = createSyncNativeInstruction(poolAccount) - - // Send transaction to fund pool - const fundPoolTransaction = new Transaction().add(transferToPoolIx, syncPoolIx) - await sendAndConfirmTransaction(connection, fundPoolTransaction, [payer], { - commitment: 'confirmed' - }) - - console.log(`✅ Added ${amount / BBA_DALTON_UNIT} BBA to pool account`) -} - -/** - * Calculate expected output using constant product formula - * Helper function from examples - */ -export function calculateExpectedOutput(amountIn: number, reserveIn: number, reserveOut: number): number { - return (amountIn * reserveOut) / (reserveIn + amountIn) -} - -/** - * Convert BBA amount to daltons - */ -export function bbaTodaltons(bbaAmount: number): number { - return Math.floor(bbaAmount * BBA_DALTON_UNIT) -} - -/** - * Convert daltons to BBA amount - */ -export function daltonsToBBA(daltons: number): number { - return daltons / BBA_DALTON_UNIT -} diff --git a/src/lib/token.ts b/src/lib/token.ts new file mode 100644 index 0000000..35706ed --- /dev/null +++ b/src/lib/token.ts @@ -0,0 +1,116 @@ +import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@bbachain/spl-token' +import { BBA_DALTON_UNIT, Connection, PublicKey } from '@bbachain/web3.js' + +import { TTradeableTokenProps } from '@/features/tokens/types' +import StaticTradeableTokens from '@/staticData/tokens' +import { TGetTokenAccountsData } from '@/types' + +export const getTokenAccounts = async ( + connection: Connection, + ownerAddress: PublicKey +): Promise => { + try { + const [tokenAccounts, token2022Accounts] = await Promise.all([ + connection.getParsedTokenAccountsByOwner(ownerAddress, { + programId: TOKEN_PROGRAM_ID + }), + connection.getParsedTokenAccountsByOwner(ownerAddress, { + programId: TOKEN_2022_PROGRAM_ID + }) + ]) + + const tokenAccountsData = [...tokenAccounts.value, ...token2022Accounts.value] + + return tokenAccountsData.map((account) => { + const parsedInfo = account.account.data.parsed.info + const mintAddress = parsedInfo.mint as string + const owner = parsedInfo.owner as string + const supply = parsedInfo.tokenAmount.uiAmount as number + const decimals = parsedInfo.tokenAmount.decimals as number + + console.log({ + pubKey: account.pubkey.toBase58(), + mintAddress, + ownerAddress: owner, + supply, + decimals + }) + + return { + pubKey: account.pubkey.toBase58(), + mintAddress, + ownerAddress: owner, + supply, + decimals + } + }) + } catch (error) { + console.error('Error fetching token accounts:', error) + throw new Error('Error fetching token accounts') + } +} + +const TradeableTokenRegistry = new Map() +StaticTradeableTokens.forEach((token) => { + TradeableTokenRegistry.set(token.address, token) +}) + +export function getTradeableTokenByAddress(address: string): TTradeableTokenProps | null { + return TradeableTokenRegistry.get(address) || null +} + +export function getKnownTokenAddresses(): string[] { + return Array.from(TradeableTokenRegistry.keys()) +} + +export function isKnownTradeableToken(address: string): boolean { + return TradeableTokenRegistry.has(address) +} + +export function isNativeBBA(address: string): boolean { + return address === NATIVE_MINT.toBase58() +} + +export function isBBAToken(address: string): boolean { + const token = getTradeableTokenByAddress(address) + return token?.symbol === 'BBA' || isNativeBBA(address) +} + +export function getNativeBBAToken(): TTradeableTokenProps { + return StaticTradeableTokens[0] // First token is always BBA +} + +export function getBBAFromDaltons(daltons: number): number { + return daltons / BBA_DALTON_UNIT +} + +export function getDaltonsFromBBA(bbaAmount: number): number { + return Math.floor(bbaAmount * BBA_DALTON_UNIT) +} + +/** + * Format token balance from daltons to human-readable format + * @param balance - Balance in daltons (smallest unit) + * @param decimals - Number of decimals for the token + * @returns Formatted balance as number + */ +export function formatTokenBalance(balance: number, decimals: number): number { + if (!balance || !decimals) return 0 + return balance / Math.pow(10, decimals) +} + +/** + * Format token amount to daltons + * @param amount - Amount in human-readable format + * @param decimals - Number of decimals for the token + * @returns Amount in daltons + */ +export function formatTokenToDaltons(amount: number, decimals: number): number { + if (!amount || !decimals) return 0 + return amount * Math.pow(10, decimals) +} + +export function getCoinGeckoId(address: string) { + const coinGeckoId = StaticTradeableTokens.find((token) => address === token.address)?.coinGeckoId + return coinGeckoId +} diff --git a/src/lib/tokenAccount.ts b/src/lib/tokenAccount.ts deleted file mode 100644 index da47768..0000000 --- a/src/lib/tokenAccount.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { MintLayout, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@bbachain/spl-token' -import { Metadata } from '@bbachain/spl-token-metadata' -import { Connection, PublicKey } from '@bbachain/web3.js' - -import { TGetTokenAccountsData } from '@/types' - -export const getTokenAccounts = async ( - connection: Connection, - ownerAddress: PublicKey -): Promise => { - try { - const [tokenAccounts, token2022Accounts] = await Promise.all([ - connection.getParsedTokenAccountsByOwner(ownerAddress, { - programId: TOKEN_PROGRAM_ID - }), - connection.getParsedTokenAccountsByOwner(ownerAddress, { - programId: TOKEN_2022_PROGRAM_ID - }) - ]) - - const tokenAccountsData = [...tokenAccounts.value, ...token2022Accounts.value] - - return tokenAccountsData.map((account) => { - const parsedInfo = account.account.data.parsed.info - const mintAddress = parsedInfo.mint as string - const owner = parsedInfo.owner as string - const supply = parsedInfo.tokenAmount.uiAmount as number - const decimals = parsedInfo.tokenAmount.decimals as number - - console.log({ - pubKey: account.pubkey.toBase58(), - mintAddress, - ownerAddress: owner, - supply, - decimals - }) - - return { - pubKey: account.pubkey.toBase58(), - mintAddress, - ownerAddress: owner, - supply, - decimals - } - }) - } catch (error) { - console.error('Error fetching token accounts:', error) - throw new Error('Error fetching token accounts') - } -} - -export const getTokenAccounts2 = async (connection: Connection): Promise => { - try { - const [tokenAccounts, token2022Accounts] = await Promise.all([ - connection.getProgramAccounts(TOKEN_PROGRAM_ID, { - filters: [{ dataSize: MintLayout.span }] - }), - connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, { - filters: [{ dataSize: MintLayout.span }] - }) - ]) - - const tokenAccountsData = [...tokenAccounts, ...token2022Accounts] - - return tokenAccountsData.map((account) => { - const data = new Uint8Array( - account.account.data.buffer, - account.account.data.byteOffset, - account.account.data.byteLength - ) - const mintInfo = MintLayout.decode(data) - const mintAddress = account.pubkey.toBase58() - const owner = account.pubkey.toBase58() - const supply = Number(mintInfo.supply) - const decimals = Number(mintInfo.decimals) - - console.log({ - pubKey: account.pubkey.toBase58(), - mintAddress, - ownerAddress: owner, - supply, - decimals - }) - - return { - pubKey: account.pubkey.toBase58(), - mintAddress, - ownerAddress: owner, - supply, - decimals - } - }) - } catch (error) { - console.error('Error fetching token accounts:', error) - throw new Error('Error fetching token accounts') - } -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9dfcf2f..615b280 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,43 +5,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -/** - * Format token balance from daltons to human-readable format - * @param balance - Balance in daltons (smallest unit) - * @param decimals - Number of decimals for the token - * @returns Formatted balance as number - */ -export function formatTokenBalance(balance: number, decimals: number): number { - if (!balance || !decimals) return 0 - return balance / Math.pow(10, decimals) -} - -/** - * Format token amount to daltons - * @param amount - Amount in human-readable format - * @param decimals - Number of decimals for the token - * @returns Amount in daltons - */ -export function formatTokenToDaltons(amount: number, decimals: number): number { - if (!amount || !decimals) return 0 - return amount * Math.pow(10, decimals) -} - -/** - * Format balance for display with proper number formatting - * @param balance - Balance in daltons - * @param decimals - Number of decimals for the token - * @param maxDecimals - Maximum decimals to show in display (default: 6) - * @returns Formatted balance string - */ -export function formatBalanceDisplay(balance: number, decimals: number, maxDecimals: number = 6): string { - const formatted = formatTokenBalance(balance, decimals) - return formatted.toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: maxDecimals - }) -} - export function getExplorerAddress(address: string) { return `https://explorer.bbachain.com/address/${address}?cluster=testnet` } diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index dd64a9f..36a3ca3 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -2,9 +2,11 @@ import { BBA_DALTON_UNIT, + Blockhash, Connection, PublicKey, SystemProgram, + Transaction, TransactionMessage, VersionedTransaction } from '@bbachain/web3.js' @@ -50,3 +52,72 @@ export const createTransaction = async ({ latestBlockhash } } + +// Enhanced retry configuration +const RETRY_CONFIG = { + attempts: 3, + delay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff + retryCondition: (error: any) => { + // Retry on network errors, timeouts, and 5xx errors + return !error.response || error.response.status >= 500 || error.code === 'NETWORK_ERROR' + } +} + +// Transaction helper with retry and delay +export const sendTransactionWithRetry = async ( + transaction: Transaction, + connection: Connection, + sendTransaction: any, + options?: { signers?: any[]; skipPreflight?: boolean; preflightCommitment?: string } +) => { + for (let attempt = 1; attempt <= RETRY_CONFIG.attempts; attempt++) { + try { + console.log(`📤 Sending transaction (attempt ${attempt}/${RETRY_CONFIG.attempts})...`) + const signature = await sendTransaction(transaction, connection, options) + console.log(`✅ Transaction sent successfully:`, signature) + return signature + } catch (error: any) { + console.error(`❌ Transaction attempt ${attempt} failed:`, error) + + if (attempt === RETRY_CONFIG.attempts || !RETRY_CONFIG.retryCondition(error)) { + throw error + } + + // Wait before retry + const delay = RETRY_CONFIG.delay(attempt - 1) + console.log(`âŗ Waiting ${delay}ms before retry...`) + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } +} + +// Confirmation helper with timeout +export const confirmTransactionWithTimeout = async ( + connection: Connection, + signature: string, + latestBlockhash: Readonly<{ + blockhash: Blockhash + lastValidBlockHeight: number + }>, + timeoutMs = 30000 +) => { + console.log(`âŗ Confirming transaction: ${signature}`) + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Transaction confirmation timeout')), timeoutMs) + }) + + const confirmPromise = connection.confirmTransaction( + { signature, ...latestBlockhash }, + 'confirmed' + ) + + const confirmation: any = await Promise.race([confirmPromise, timeoutPromise]) + + if (confirmation.value?.err) { + throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`) + } + + console.log(`✅ Transaction confirmed: ${signature}`) + return confirmation +} diff --git a/src/services/wallet.tsx b/src/services/wallet.tsx index dada707..b03ad3a 100644 --- a/src/services/wallet.tsx +++ b/src/services/wallet.tsx @@ -1,25 +1,76 @@ 'use client' +import { getAssociatedTokenAddress, NATIVE_MINT } from '@bbachain/spl-token' import { useConnection, useWallet } from '@bbachain/wallet-adapter-react' -import { BBA_DALTON_UNIT } from '@bbachain/web3.js' +import { BBA_DALTON_UNIT, PublicKey } from '@bbachain/web3.js' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import SERVICES_KEY from '@/constants/service' +import { isNativeBBA } from '@/lib/token' import { TSuccessMessage } from '@/types' import { TRequestAirdropPayload } from '@/types/wallet' -export function useGetBalance() { - const { publicKey: address } = useWallet() +export function useGetBBABalance() { + const { publicKey: ownerAddress } = useWallet() const { connection } = useConnection() return useQuery({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, address?.toBase58()], + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, ownerAddress?.toBase58()], queryFn: async () => { - if (!address) throw new Error('No wallet connected') - const balance = await connection.getBalance(address) + if (!ownerAddress) throw new Error('No wallet connected') + const balance = await connection.getBalance(ownerAddress) return balance }, - enabled: !!address + enabled: !!ownerAddress + }) +} + +export function useGetWBBABalance() { + const { publicKey: ownerAddress } = useWallet() + const { connection } = useConnection() + return useQuery({ + queryKey: [SERVICES_KEY.WALLET.GET_WBBA_BALANCE, ownerAddress?.toBase58()], + queryFn: async () => { + if (!ownerAddress) throw new Error('No wallet connected') + + try { + const mint = new PublicKey(NATIVE_MINT.toBase58()) + const ata = await getAssociatedTokenAddress(mint, ownerAddress) + const balanceAmount = await connection.getTokenAccountBalance(ata) + return Number(balanceAmount.value.amount) + } catch (e) { + console.error('WBBA balance fetch error:', e) + return 0 + } + }, + enabled: !!ownerAddress + }) +} + +export function useGetTokenBalanceByMint({ mintAddress }: { mintAddress: string }) { + const { publicKey: ownerAddress } = useWallet() + const { connection } = useConnection() + return useQuery({ + queryKey: [SERVICES_KEY.WALLET.GET_TOKEN_BALANCE_BY_MINT, mintAddress], + queryFn: async () => { + if (!ownerAddress) throw new Error('No wallet connected') + + try { + // Handle native BBA token differently + if (isNativeBBA(mintAddress)) { + const balance = await connection.getBalance(ownerAddress) + return balance + } + const mint = new PublicKey(mintAddress) + const ata = await getAssociatedTokenAddress(mint, ownerAddress) + const balanceAmount = await connection.getTokenAccountBalance(ata) + return Number(balanceAmount.value.amount) + } catch (e) { + console.error('Balance fetch error:', e) + return 0 + } + }, + enabled: !!mintAddress && !!ownerAddress }) } @@ -51,7 +102,7 @@ export function useRequestAirdrop() { }, onSuccess: () => { client.invalidateQueries({ - queryKey: [SERVICES_KEY.WALLET.GET_BALANCE, address?.toBase58()] + queryKey: [SERVICES_KEY.WALLET.GET_BBA_BALANCE, address?.toBase58()] }) } }) diff --git a/src/staticData/socialMedia.ts b/src/staticData/socialMedia.ts index cf6fae2..aeab054 100644 --- a/src/staticData/socialMedia.ts +++ b/src/staticData/socialMedia.ts @@ -4,48 +4,59 @@ import { IconType } from 'react-icons/lib' type SocialMediaProps = { icon: IconType + label: string href: string } const SocialMedia: SocialMediaProps[] = [ { icon: FaXTwitter, + label: 'bbachain twitter', href: 'https://twitter.com/bbachain_com' }, { icon: FaFacebook, + label: 'bbachain facebook', href: 'https://www.facebook.com/bbachain' }, { icon: FaInstagram, + label: 'bbachain instagram', href: 'https://www.instagram.com/bbachaincom/' }, { icon: FaLinkedin, + label: 'bbachain linkedin', href: 'https://www.linkedin.com/company/bti-group-bbachain/?viewAsMember=true' }, { icon: FaYoutube, + label: 'bbachain youtube', href: 'https://www.youtube.com/@BBAChain' }, { icon: FaTiktok, + label: 'bbachain tiktok', href: 'https://www.tiktok.com/@bbachain' }, { icon: FaDiscord, + label: 'bbachain discord', href: 'https://www.tiktok.com/@bbachain' }, { icon: FaTelegram, + label: 'bbachain telegram global', href: 'https://t.me/bbachain_Global' }, { icon: FaTelegram, + label: 'bbachain telegram', href: 'https://t.me/bbachain' }, { icon: FaMedium, + label: 'bbachain medium', href: 'https://medium.com/@bbachain' } ] diff --git a/src/staticData/tokens.ts b/src/staticData/tokens.ts index cfa89c8..b335cce 100644 --- a/src/staticData/tokens.ts +++ b/src/staticData/tokens.ts @@ -1,14 +1,11 @@ import { NATIVE_MINT } from '@bbachain/spl-token' -import { BBA_DALTON_UNIT } from '@bbachain/web3.js' +import { PublicKey } from '@bbachain/web3.js' -import { MintInfo } from '@/features/liquidityPool/types' +import { type TExtendedTradeableTokenProps } from '@/features/tokens/types' -export type ExtendedMintInfo = MintInfo & { - coinGeckoId?: string; - isNative: boolean; -} +export const USDT_MINT = new PublicKey('C5CpKwRY2Q5kPYhx78XimCg2eRT3YUgPFAoocFF7Vgf') -const StaticTokens: ExtendedMintInfo[] = [ +const StaticTradeableTokens: TExtendedTradeableTokenProps[] = [ { name: 'BBA Coin', symbol: 'BBA', @@ -33,7 +30,7 @@ const StaticTokens: ExtendedMintInfo[] = [ name: 'Tether USD', symbol: 'USDT', coinGeckoId: 'tether', - address: 'C5CpKwRY2Q5kPYhx78XimCg2eRT3YUgPFAoocFF7Vgf', + address: USDT_MINT.toBase58(), logoURI: 'https://assets.coingecko.com/coins/images/325/small/Tether.png', decimals: 6, isNative: false, @@ -41,176 +38,4 @@ const StaticTokens: ExtendedMintInfo[] = [ } ] -// Create a lookup map for faster access -const TokenRegistry = new Map() -StaticTokens.forEach((token) => { - TokenRegistry.set(token.address, token) -}) - -/** - * Get token info by mint address from the registry - */ -export function getTokenByAddress(address: string): MintInfo | null { - return TokenRegistry.get(address) || null -} - -/** - * Get a list of known token addresses - */ -export function getKnownTokenAddresses(): string[] { - return Array.from(TokenRegistry.keys()) -} - -/** - * Check if a token is known in our registry - */ -export function isKnownToken(address: string): boolean { - return TokenRegistry.has(address) -} - -/** - * Generate a user-friendly display name for unknown tokens - */ -export function generateTokenDisplayName(address: string): { symbol: string; name: string } { - // Create a shorter, more readable format - const shortSymbol = address.slice(0, 6) // First 6 characters - - // Try to make it look like a token symbol - const cleanSymbol = shortSymbol.toUpperCase() - - return { - symbol: cleanSymbol, - name: `Token ${cleanSymbol}` - } -} - -/** - * Check if a token is the native BBA token - */ -export function isNativeBBA(address: string): boolean { - return address === NATIVE_MINT.toBase58() -} - -/** - * Check if a token is a BBA token (native or wrapped) - */ -export function isBBAToken(address: string): boolean { - const token = getTokenByAddress(address) - return token?.symbol === 'BBA' || isNativeBBA(address) -} - -/** - * Get the native BBA token info - */ -export function getNativeBBAToken(): MintInfo { - return StaticTokens[0] // First token is always BBA -} - -export function getBBAFromDaltons(daltons: number): number { - return daltons / BBA_DALTON_UNIT -} - -/** - * ======================================== - * BBA LIQUIDITY POOL UTILITIES - * ======================================== - */ - -/** - * Check if a pool pair involves the native BBA token - */ -export function isBBAPool(baseTokenAddress: string, quoteTokenAddress: string): boolean { - return isNativeBBA(baseTokenAddress) || isNativeBBA(quoteTokenAddress) -} - -/** - * Get the BBA token position in a pool pair - * @returns 'base' if BBA is base token, 'quote' if BBA is quote token, null if no BBA - */ -export function getBBAPositionInPool(baseTokenAddress: string, quoteTokenAddress: string): 'base' | 'quote' | null { - if (isNativeBBA(baseTokenAddress)) return 'base' - if (isNativeBBA(quoteTokenAddress)) return 'quote' - return null -} - -/** - * Get the non-BBA token in a BBA pool pair - */ -export function getNonBBATokenFromPool(baseTokenAddress: string, quoteTokenAddress: string): string | null { - if (isNativeBBA(baseTokenAddress)) return quoteTokenAddress - if (isNativeBBA(quoteTokenAddress)) return baseTokenAddress - return null -} - -/** - * Check if pool creation requires BBA wrapping - */ -export function requiresBBAWrapping(baseTokenAddress: string, quoteTokenAddress: string): boolean { - return isBBAPool(baseTokenAddress, quoteTokenAddress) -} - -/** - * Get the WBBA (Wrapped BBA) mint address for pool operations - * BBA uses NATIVE_MINT when wrapped - */ -export function getWBBAMintAddress(): string { - return NATIVE_MINT.toBase58() -} - -/** - * Check if an address is the WBBA mint (same as NATIVE_MINT) - */ -export function isWBBAMint(address: string): boolean { - return address === NATIVE_MINT.toBase58() -} - -/** - * ======================================== - * LP TOKEN IDENTIFICATION UTILITIES - * ======================================== - */ - -/** - * Check if a token is likely a Liquidity Pool (LP) token - * LP tokens typically have: - * - 2 decimals (standard for LP tokens) - * - Mint authority controlled by a Program Derived Address (PDA) - * - Authority that is not the common system addresses - */ -export function isLikelyLPToken(decimals: number, mintAuthorityAddress: string | null): boolean { - // Basic check: LP tokens typically have 2 decimals - if (decimals !== 2) { - return false - } - - // If no mint authority, it's not an LP token (those are usually revoked tokens) - if (!mintAuthorityAddress) { - return false - } - - // Common non-LP addresses to exclude - const commonAddresses = [ - '11111111111111111111111111111111', // System Program - 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program - 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' // Associated Token Program - ] - - // If mint authority is a common system address, it's not an LP token - if (commonAddresses.includes(mintAuthorityAddress)) { - return false - } - - // Additional heuristic: LP tokens usually have PDA authorities - // Most user tokens don't use complex PDA structures - // This is a reasonable heuristic for filtering - return true -} - -/** - * Get display name for LP tokens - */ -export function getLPTokenDisplayName(mintAddress: string): string { - return `LP-${mintAddress.slice(0, 6)}...${mintAddress.slice(-4)}` -} - -export default StaticTokens +export default StaticTradeableTokens diff --git a/src/types/index.ts b/src/types/index.ts index 9710e3d..e6bf5c8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -21,7 +21,3 @@ export type TGetTokenAccountsData = { supply: number decimals: number } - -export type TGetTokenAccountsData2 = { - mintAddress: string -}