diff --git a/packages/chains/src/types/protocol.ts b/packages/chains/src/types/protocol.ts index 94666d6e8..d5296f43b 100644 --- a/packages/chains/src/types/protocol.ts +++ b/packages/chains/src/types/protocol.ts @@ -6,6 +6,7 @@ import { SupportedLiquityV2, TargetType, } from "@metrom-xyz/sdk"; +import { SupportedChain } from "@metrom-xyz/contracts"; export enum ProtocolType { Dex = "dex", @@ -34,6 +35,10 @@ export interface DexProtocol extends ProtocolBase { supportsFetchAllPools: boolean; } +export type WithChain = T & { + chainId: SupportedChain; +}; + export interface LiquityV2Protocol extends ProtocolBase { type: ProtocolType.LiquityV2; debtToken: Erc20Token; diff --git a/packages/frontend/SUPPORTED_PROTOCOLS.md b/packages/frontend/SUPPORTED_PROTOCOLS.md index 978fa55d9..416c74e4e 100644 --- a/packages/frontend/SUPPORTED_PROTOCOLS.md +++ b/packages/frontend/SUPPORTED_PROTOCOLS.md @@ -1,12 +1,12 @@ -||Telos|Gnosis|Sonic|Lens|Sei Network|LightLink|Swell|Base|Hemi|Taiko|Scroll|Lumia| -|---|---|---|---|---|---|---|---|---|---|---|---|--- -|[Uniswap v3](https://app.uniswap.org/)|✅|✅|✅|✅|-|✅|-|✅|✅|✅|✅|-| -|[Swapr](https://swapr.eth.link/)|-|✅|-|-|-|-|-|-|-|-|-|-| -|[SilverSwap](https://silverswap.io/)|-|-|✅|-|-|-|-|-|-|-|-|-| -|[Carbon DeFi](https://carbondefi.xyz/)|-|-|-|-|✅|-|-|-|-|-|-|-| -|[Velodrome](https://velodrome.finance/)|-|-|-|-|-|-|✅|-|-|-|-|-| -|[Kim](https://www.kim.exchange)|-|-|-|-|-|-|-|✅|-|-|-|-| -|[BaseSwap](https://baseswap.fi/)|-|-|-|-|-|-|-|✅|-|-|-|-| -|[Unagi](https://unagiswap.xyz/)|-|-|-|-|-|-|-|-|-|✅|-|-| -|[Scribe](https://scribe.exchange)|-|-|-|-|-|-|-|-|-|-|✅|-| -|[Morphex](https://morphex.exchange/)|-|-|-|-|-|-|-|-|-|-|-|✅| +| | Telos | Gnosis | Sonic | Lens | Sei Network | LightLink | Swell | Base | Hemi | Taiko | Scroll | Lumia | +| --------------------------------------- | ----- | ------ | ----- | ---- | ----------- | --------- | ----- | ---- | ---- | ----- | ------ | ----- | +| [Uniswap v3](https://app.uniswap.org/) | ✅ | ✅ | ✅ | ✅ | - | ✅ | - | ✅ | ✅ | ✅ | ✅ | - | +| [Swapr](https://swapr.eth.link/) | - | ✅ | - | - | - | - | - | - | - | - | - | - | +| [SilverSwap](https://silverswap.io/) | - | - | ✅ | - | - | - | - | - | - | - | - | - | +| [Carbon DeFi](https://carbondefi.xyz/) | - | - | - | - | ✅ | - | - | - | - | - | - | - | +| [Velodrome](https://velodrome.finance/) | - | - | - | - | - | - | ✅ | - | - | - | - | - | +| [Kim](https://www.kim.exchange) | - | - | - | - | - | - | - | ✅ | - | - | - | - | +| [BaseSwap](https://baseswap.fi/) | - | - | - | - | - | - | - | ✅ | - | - | - | - | +| [Unagi](https://unagiswap.xyz/) | - | - | - | - | - | - | - | - | - | ✅ | - | - | +| [Scribe](https://scribe.exchange) | - | - | - | - | - | - | - | - | - | - | ✅ | - | +| [Morphex](https://morphex.exchange/) | - | - | - | - | - | - | - | - | - | - | - | ✅ | diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 58da2322c..ba3ddfbc9 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -340,6 +340,16 @@ "approve": "Approve {amount} {symbol} ({currentIndex}/{totalAmount})" }, "preview": "Campaign preview" + }, + "notifications": { + "setupFail": { + "title": "Setup failed", + "message": "Something went wrong" + }, + "setupSuccess": { + "title": "Setup successful", + "message": "Campaign imported" + } } }, "campaignPreview": { @@ -395,8 +405,10 @@ "allows": "Allows {count, plural, =0 {} =1 {# address} other {# adresses}}" }, "errors": { - "specification": "An error occurred while processing the campaign specification" + "specification": "An error occurred while processing the campaign specification", + "setup": "An error occurred while processing the campaign setup" }, + "share": "Share campaign setup", "deploy": "Launch campaign", "connectWallet": "Connect wallet", "congratulations": "Congratulations!", @@ -408,7 +420,17 @@ } }, "allCampaigns": "All campaigns", - "newCampaign": "New campaign" + "newCampaign": "New campaign", + "notifications": { + "linkCopied": { + "title": "Link copied to clipboard", + "message": "Expires in 1 day" + }, + "linkError": { + "title": "Link generation failed", + "message": "Something went wrong" + } + } }, "campaignDuration": { "startDate": "Start date", diff --git a/packages/frontend/src/app/[locale]/campaigns/create/[type]/page.tsx b/packages/frontend/src/app/[locale]/campaigns/create/[type]/page.tsx index e114b3afa..76aed084d 100644 --- a/packages/frontend/src/app/[locale]/campaigns/create/[type]/page.tsx +++ b/packages/frontend/src/app/[locale]/campaigns/create/[type]/page.tsx @@ -2,6 +2,7 @@ import { CreateCampaignForm } from "@/src/components/create-campaign/form"; import { routing, type Locale } from "@/src/i18n/routing"; import { CampaignType } from "@/src/types/campaign"; import { setRequestLocale } from "next-intl/server"; +import { Suspense } from "react"; interface Params { type: CampaignType; @@ -26,7 +27,11 @@ export default async function CampaignFormPage({ setRequestLocale(locale); - return ; + return ( + + + + ); } export async function generateStaticParams() { diff --git a/packages/frontend/src/components/campaigns/index.tsx b/packages/frontend/src/components/campaigns/index.tsx index f2b202abd..e43cddc52 100644 --- a/packages/frontend/src/components/campaigns/index.tsx +++ b/packages/frontend/src/components/campaigns/index.tsx @@ -304,7 +304,9 @@ export function Campaigns() { const params = new URLSearchParams(searchParams.toString()); if (!chain.query) params.delete("chain"); else params.set("chain", chain.query); - router.replace(`${pathname}?${params.toString()}`); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); }, [pathname, router, searchParams], ); @@ -329,7 +331,7 @@ export function Campaigns() { const params = new URLSearchParams(searchParams.toString()); params.delete("chain"); - router.replace(`${pathname}?${params.toString()}`); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); }, [pathname, searchParams, router]); function handlePreviousPage() { diff --git a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx index 412cd094a..2068bfa4e 100644 --- a/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/amm-pool-liquidity-form/index.tsx @@ -4,10 +4,17 @@ import { AmmPoolLiquidityCampaignPreviewPayload, type AmmPoolLiquidityCampaignPayloadPart, type CampaignPreviewDistributables, + CampaignKind, } from "@/src/types/campaign"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useChainId } from "wagmi"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { useChainId, useSwitchChain } from "wagmi"; import { AmmPoolLiquidityType, DistributablesType, @@ -28,8 +35,16 @@ import { AMM_SUPPORTS_TOKENS_RATIO, } from "@/src/commons"; import { WeightingStep } from "../../steps/weighting"; +import { usePathname } from "@/i18n/routing"; +import dayjs from "dayjs"; +import { useCampaignSetup } from "@/src/hooks/useCampaignSetup"; +import { toast } from "sonner"; +import { SetupFail } from "../notifications/setup-fail"; +import { SetupSuccess } from "../notifications/setup-success"; +import { useRouter, useSearchParams } from "next/navigation"; import styles from "./styles.module.css"; +import { decodeCampaignSetup } from "@/src/utils/campaign"; function validatePayload( payload: AmmPoolLiquidityCampaignPayload, @@ -88,11 +103,79 @@ export function AmmPoolLiquidityForm({ onPreviewClick, }: AmmPoolLiquidityFormProps) { const t = useTranslations("newCampaign"); + const chainId = useChainId(); + const router = useRouter(); + const pathname = usePathname(); + const { switchChainAsync, isPending: switchingChain } = useSwitchChain(); + const searchParams = useSearchParams(); + const { + loading: loadingSetup, + error: setupError, + setup, + } = useCampaignSetup({ + hash: searchParams.get("setup"), + enabled: !!searchParams.get("setup"), + }); const [payload, setPayload] = useState(initialPayload); const [errors, setErrors] = useState({}); + // This hook auto fills the form state when a campaign setup is available. + useLayoutEffect(() => { + // Remove the 'setup' parameter from the URL after parsing. + // This ensures the form behaves correctly after autocomplete completes. + const params = new URLSearchParams(searchParams.toString()); + + if (setupError) { + params.delete("setup"); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); + } + + if (!setup) return; + + const autocompletePayload = async () => { + const decodedSetup = decodeCampaignSetup(setup); + if (decodedSetup.kind !== CampaignKind.AmmPoolLiquidity) return; + + const payload = decodedSetup as AmmPoolLiquidityCampaignPayload; + if (!payload.dex?.chainId) return; + + const { dex } = payload; + + if (dex.chainId !== chainId) + await switchChainAsync({ chainId: dex.chainId }); + + params.delete("setup"); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); + + setPayload(payload); + }; + + autocompletePayload(); + }, [ + loadingSetup, + setupError, + searchParams, + chainId, + setup, + pathname, + router, + switchChainAsync, + ]); + + useEffect(() => { + if (!loadingSetup && setupError) + toast.custom((toastId) => ); + + if (!loadingSetup && !setupError && !!setup) + toast.custom((toastId) => ); + }, [loadingSetup, setupError, setup]); + const previewPayload = useMemo(() => { if (Object.values(errors).some((error) => !!error)) return null; return validatePayload(payload); @@ -155,15 +238,19 @@ export function AmmPoolLiquidityForm({ onPreviewClick(previewPayload); } + const loading = loadingSetup || switchingChain; + return (
)} )} ({ const [view, setView] = useState(View.Form); const [payload, setPayload] = useState(null); + const unsupportedChain = useMemo(() => { + return ( + isConnected && + (!connectedChain || + !chains.some((chain) => chain.id === selectedChain)) + ); + }, [chains, connectedChain, isConnected, selectedChain]); + function handlePreviewOnClick(payload: CampaignPreviewPayload | null) { setPayload(payload); setView(View.Preview); @@ -62,14 +70,6 @@ export function CreateCampaignForm({ router.push("/campaigns/create"); } - const unsupportedChain = useMemo(() => { - return ( - isConnected && - (!connectedChain || - !chains.some((chain) => chain.id === selectedChain)) - ); - }, [chains, connectedChain, isConnected, selectedChain]); - return (
{dexesProtocols.length > 0 && liquityV2Protocols.length > 0 && ( diff --git a/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx b/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx index 8c7d3d90e..58716d1db 100644 --- a/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/liquity-v2-forks-form/index.tsx @@ -4,10 +4,17 @@ import { type LiquityV2CampaignPayload, type LiquityV2CampaignPayloadPart, type CampaignPreviewDistributables, + CampaignKind, } from "@/src/types/campaign"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useChainId } from "wagmi"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { useChainId, useSwitchChain } from "wagmi"; import { DistributablesType } from "@metrom-xyz/sdk"; import { StartDateStep } from "../../steps/start-date-step"; import { EndDateStep } from "../../steps/end-date-step"; @@ -18,6 +25,13 @@ import { ArrowRightIcon } from "@/src/assets/arrow-right-icon"; import { LiquityV2BrandStep } from "../../steps/liquity-v2-brand-step"; import { LiquityV2ActionStep } from "../../steps/liquity-v2-action-step"; import { LiquityV2CollateralStep } from "../../steps/liquity-v2-collateral-step"; +import { useRouter, useSearchParams } from "next/navigation"; +import { usePathname } from "@/src/i18n/routing"; +import { useCampaignSetup } from "@/src/hooks/useCampaignSetup"; +import { decodeCampaignSetup } from "@/src/utils/campaign"; +import { toast } from "sonner"; +import { SetupFail } from "../notifications/setup-fail"; +import { SetupSuccess } from "../notifications/setup-success"; import styles from "./styles.module.css"; @@ -83,10 +97,81 @@ export function LiquityV2ForksForm({ }: LiquityV2ForksFormProps) { const t = useTranslations("newCampaign"); const chainId = useChainId(); + const router = useRouter(); + const pathname = usePathname(); + const { switchChainAsync, isPending: switchingChain } = useSwitchChain(); + const searchParams = useSearchParams(); + const { + loading: loadingSetup, + error: setupError, + setup, + } = useCampaignSetup({ + hash: searchParams.get("setup"), + enabled: !!searchParams.get("setup"), + }); const [payload, setPayload] = useState(initialPayload); const [errors, setErrors] = useState({}); + // This hook auto fills the form state when a campaign setup is available. + useLayoutEffect(() => { + // Remove the 'setup' parameter from the URL after parsing. + // This ensures the form behaves correctly after autocomplete completes. + const params = new URLSearchParams(searchParams.toString()); + + if (setupError) { + params.delete("setup"); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); + } + + if (!setup) return; + + const autocompletePayload = async () => { + const decodedSetup = decodeCampaignSetup(setup); + if ( + decodedSetup.kind !== CampaignKind.LiquityV2Debt && + decodedSetup.kind !== CampaignKind.LiquityV2StabilityPool + ) + return; + + const payload = decodedSetup as LiquityV2CampaignPayload; + if (!payload.brand?.chainId) return; + + const { brand } = payload; + + if (brand.chainId !== chainId) + await switchChainAsync({ chainId: brand.chainId }); + + params.delete("setup"); + router.replace(`${pathname}?${params.toString()}`, { + scroll: false, + }); + + setPayload(payload); + }; + + autocompletePayload(); + }, [ + loadingSetup, + setupError, + searchParams, + chainId, + setup, + pathname, + router, + switchChainAsync, + ]); + + useEffect(() => { + if (!loadingSetup && !!setupError) + toast.custom((toastId) => ); + + if (!loadingSetup && !setupError && !!setup) + toast.custom((toastId) => ); + }, [loadingSetup, setupError, setup]); + const previewPayload = useMemo(() => { if (Object.values(errors).some((error) => !!error)) return null; return validatePayload(payload); @@ -132,21 +217,27 @@ export function LiquityV2ForksForm({ onPreviewClick(previewPayload); } + const loading = loadingSetup || switchingChain; + return (
*/}
{tokensApproved && (