diff --git a/packages/chains/src/chains/base.ts b/packages/chains/src/chains/base.ts index bb384372e..a7f88df5e 100644 --- a/packages/chains/src/chains/base.ts +++ b/packages/chains/src/chains/base.ts @@ -3,6 +3,8 @@ import { ChainType, PartnerCampaignType, SupportedDex, + SupportedOdyssey, + SupportedOdysseyStrategy, SupportedYieldSeeker, } from "@metrom-xyz/sdk"; import { SupportedChain, ADDRESS } from "@metrom-xyz/contracts"; @@ -15,6 +17,7 @@ import { HydrexLogo } from "../assets/logos/dexes/hydrex"; import { BalancerLogo } from "../assets/logos/dexes/balancer"; import { QuickswapLogo } from "../assets/logos/dexes/quickswap"; import { YieldSeekerLogo } from "../assets/logos/yield-seeker"; +import { OdysseyLogo } from "../assets/logos/odyssey"; export const baseData: ChainData = { active: true, @@ -31,6 +34,11 @@ export const baseData: ChainData = { partner: false, type: BaseCampaignType.AmmPoolLiquidity, }, + { + active: true, + partner: false, + type: BaseCampaignType.Odyssey, + }, { active: false, partner: true, @@ -87,6 +95,18 @@ export const baseData: ChainData = { }, supportsFetchAllPools: true, }, + { + active: true, + type: ProtocolType.Odyssey, + slug: SupportedOdyssey.Odyssey, + logo: OdysseyLogo, + name: "Odyssey", + strategies: [ + SupportedOdysseyStrategy.SynthStrategy, + SupportedOdysseyStrategy.CompoundV3BorrowStrategy, + SupportedOdysseyStrategy.CompoundV3VesperStrategy, + ], + }, { active: false, type: ProtocolType.YieldSeeker, diff --git a/packages/frontend/messages/en.json b/packages/frontend/messages/en.json index 4abb40c56..9f96d47b9 100644 --- a/packages/frontend/messages/en.json +++ b/packages/frontend/messages/en.json @@ -156,7 +156,7 @@ "description": "Incentivize users holding any ERC20 token." }, "odyssey": { - "title": "Odyssey strategy", + "title": "ODYSSEY", "description": "Incentivize users opting for a particular strategy on Odyssey." }, "partnerAction": { @@ -348,6 +348,19 @@ "apply": "Apply" } }, + "odyssey": { + "brand": { "title": "Platform" }, + "strategy": { "title": "Strategy" }, + "assets": { + "title": "Asset", + "list": { + "token": "Token", + "empty": "Nothing here", + "allocated": "Allocated", + "deposited": "Deposited" + } + } + }, "aaveV3": { "brand": { "title": "Platform" }, "actions": { @@ -376,8 +389,7 @@ "empty": "Nothing here", "debt": "Debt", "deposits": "Deposits" - }, - "apply": "Apply" + } }, "blacklistedCrossBorrowCollaterals": { "title": "Block cross borrow", diff --git a/packages/frontend/src/commons/abi.ts b/packages/frontend/src/commons/abi.ts index 47c016a82..6bf753531 100644 --- a/packages/frontend/src/commons/abi.ts +++ b/packages/frontend/src/commons/abi.ts @@ -1,964 +1,3 @@ -export const metromAptosAbi = { - address: - "0xe1c55a37640ff4582a47bacd396e3409056f2e931b5dd8dc6ef6ef131cf5cc9e", - name: "metrom", - friends: [], - exposed_functions: [ - { - name: "owner", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["address"], - }, - { - name: "distribute_rewards", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "vector", "vector"], - return: [], - }, - { - name: "fee", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["u32"], - }, - { - name: "accept_campaign_ownership", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "vector"], - return: [], - }, - { - name: "accept_ownership", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer"], - return: [], - }, - { - name: "updater", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["address"], - }, - { - name: "campaign_reward", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["vector", "address"], - return: ["u64"], - }, - { - name: "claim_fees", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address", "address"], - return: [], - }, - { - name: "claim_rewards", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: [ - "&signer", - "vector", - "vector>", - "address", - "u64", - "address", - ], - return: [], - }, - { - name: "claimable_fees", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["address"], - return: ["u64"], - }, - { - name: "claimed_campaign_reward", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["vector", "address", "address"], - return: ["u64"], - }, - { - name: "create_points_campaign", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: [ - "&signer", - "u64", - "u64", - "u32", - "vector", - "vector", - "u64", - "address", - ], - return: [], - }, - { - name: "create_reward_campaign", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: [ - "&signer", - "u64", - "u64", - "u32", - "vector", - "vector", - "vector
", - "vector", - ], - return: [], - }, - { - name: "fee_rebate", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["address"], - return: ["u32"], - }, - { - name: "init_module", - visibility: "private", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer"], - return: [], - }, - { - name: "init_state", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address", "address", "u32", "u64", "u64"], - return: [], - }, - { - name: "minimum_campaign_duration", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["u64"], - }, - { - name: "maximum_campaign_duration", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["u64"], - }, - { - name: "minimum_fee_token_rate", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["address"], - return: ["u64"], - }, - { - name: "minimum_reward_token_rate", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["address"], - return: ["u64"], - }, - { - name: "pending_owner", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [], - return: ["0x1::option::Option
"], - }, - { - name: "points_campaign_by_id", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["vector"], - return: [ - "0xe1c55a37640ff4582a47bacd396e3409056f2e931b5dd8dc6ef6ef131cf5cc9e::metrom::PointsCampaign", - ], - }, - { - name: "points_campaign_id", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [ - "u64", - "u64", - "u32", - "vector", - "vector", - "u64", - "address", - ], - return: ["vector"], - }, - { - name: "recover_rewards", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: [ - "&signer", - "vector", - "vector>", - "address", - "u64", - "address", - ], - return: [], - }, - { - name: "rewards_campaign_by_id", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: ["vector"], - return: [ - "0xe1c55a37640ff4582a47bacd396e3409056f2e931b5dd8dc6ef6ef131cf5cc9e::metrom::ReadonlyRewardsCampaign", - ], - }, - { - name: "rewards_campaign_id", - visibility: "public", - is_entry: false, - is_view: true, - generic_type_params: [], - params: [ - "u64", - "u64", - "u32", - "vector", - "vector", - "vector
", - "vector", - ], - return: ["vector"], - }, - { - name: "set_fee", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "u32"], - return: [], - }, - { - name: "set_fee_rebate", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address", "u32"], - return: [], - }, - { - name: "set_maximum_campaign_duration", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "u64"], - return: [], - }, - { - name: "set_minimum_campaign_duration", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "u64"], - return: [], - }, - { - name: "set_minimum_fee_token_rate", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address", "u64"], - return: [], - }, - { - name: "set_minimum_reward_token_rate", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address", "u64"], - return: [], - }, - { - name: "set_updater", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address"], - return: [], - }, - { - name: "transfer_campaign_ownership", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "vector", "address"], - return: [], - }, - { - name: "transfer_ownership", - visibility: "public", - is_entry: true, - is_view: false, - generic_type_params: [], - params: ["&signer", "address"], - return: [], - }, - ], - structs: [ - { - name: "State", - is_native: false, - is_event: false, - abilities: ["key"], - generic_type_params: [], - fields: [ - { - name: "treasury", - type: "0x1::account::SignerCapability", - }, - { - name: "owner", - type: "address", - }, - { - name: "pending_owner", - type: "0x1::option::Option
", - }, - { - name: "updater", - type: "address", - }, - { - name: "fee", - type: "u32", - }, - { - name: "minimum_campaign_duration", - type: "u64", - }, - { - name: "maximum_campaign_duration", - type: "u64", - }, - { - name: "fee_rebate", - type: "0x1::smart_table::SmartTable", - }, - { - name: "claimable_fees", - type: "0x1::smart_table::SmartTable", - }, - { - name: "minimum_reward_token_rate", - type: "0x1::smart_table::SmartTable", - }, - { - name: "minimum_fee_token_rate", - type: "0x1::smart_table::SmartTable", - }, - { - name: "points_campaign", - type: "0x1::smart_table::SmartTable, 0xe1c55a37640ff4582a47bacd396e3409056f2e931b5dd8dc6ef6ef131cf5cc9e::metrom::PointsCampaign>", - }, - { - name: "rewards_campaign", - type: "0x1::smart_table::SmartTable, 0xe1c55a37640ff4582a47bacd396e3409056f2e931b5dd8dc6ef6ef131cf5cc9e::metrom::RewardsCampaign>", - }, - ], - }, - { - name: "Initialize", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - { - name: "updater", - type: "address", - }, - { - name: "fee", - type: "u32", - }, - { - name: "minimum_campaign_duration", - type: "u64", - }, - { - name: "maximum_campaign_duration", - type: "u64", - }, - ], - }, - { - name: "AcceptCampaignOwnership", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "campaign_id", - type: "vector", - }, - { - name: "owner", - type: "address", - }, - ], - }, - { - name: "AcceptOwnership", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - ], - }, - { - name: "ClaimFee", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "token", - type: "address", - }, - { - name: "amount", - type: "u64", - }, - { - name: "receiver", - type: "address", - }, - ], - }, - { - name: "ClaimReward", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "campaign_id", - type: "vector", - }, - { - name: "token", - type: "address", - }, - { - name: "amount", - type: "u64", - }, - { - name: "receiver", - type: "address", - }, - ], - }, - { - name: "CreatePointsCampaign", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "id", - type: "vector", - }, - { - name: "owner", - type: "address", - }, - { - name: "from", - type: "u64", - }, - { - name: "to", - type: "u64", - }, - { - name: "kind", - type: "u32", - }, - { - name: "data", - type: "vector", - }, - { - name: "specification_hash", - type: "vector", - }, - { - name: "points", - type: "u64", - }, - { - name: "fee_token", - type: "address", - }, - { - name: "fee", - type: "u64", - }, - ], - }, - { - name: "CreateRewardsCampaign", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "id", - type: "vector", - }, - { - name: "owner", - type: "address", - }, - { - name: "from", - type: "u64", - }, - { - name: "to", - type: "u64", - }, - { - name: "kind", - type: "u32", - }, - { - name: "data", - type: "vector", - }, - { - name: "specification_hash", - type: "vector", - }, - { - name: "reward_tokens", - type: "vector
", - }, - { - name: "reward_amounts", - type: "vector", - }, - { - name: "reward_fees", - type: "vector", - }, - ], - }, - { - name: "DistributeReward", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "campaign_id", - type: "vector", - }, - { - name: "root", - type: "vector", - }, - ], - }, - { - name: "PointsCampaign", - is_native: false, - is_event: false, - abilities: ["copy", "store", "key"], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - { - name: "pending_owner", - type: "0x1::option::Option
", - }, - { - name: "from", - type: "u64", - }, - { - name: "to", - type: "u64", - }, - { - name: "kind", - type: "u32", - }, - { - name: "data", - type: "vector", - }, - { - name: "specification_hash", - type: "vector", - }, - { - name: "points", - type: "u64", - }, - ], - }, - { - name: "ReadonlyRewardsCampaign", - is_native: false, - is_event: false, - abilities: [], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - { - name: "pending_owner", - type: "0x1::option::Option
", - }, - { - name: "from", - type: "u64", - }, - { - name: "to", - type: "u64", - }, - { - name: "kind", - type: "u32", - }, - { - name: "data", - type: "vector", - }, - { - name: "specification_hash", - type: "vector", - }, - { - name: "root", - type: "vector", - }, - ], - }, - { - name: "RecoverReward", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "campaign_id", - type: "vector", - }, - { - name: "token", - type: "address", - }, - { - name: "amount", - type: "u64", - }, - { - name: "receiver", - type: "address", - }, - ], - }, - { - name: "Reward", - is_native: false, - is_event: false, - abilities: ["store", "key"], - generic_type_params: [], - fields: [ - { - name: "amount", - type: "u64", - }, - { - name: "claimed", - type: "0x1::smart_table::SmartTable", - }, - ], - }, - { - name: "RewardsCampaign", - is_native: false, - is_event: false, - abilities: ["store", "key"], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - { - name: "pending_owner", - type: "0x1::option::Option
", - }, - { - name: "from", - type: "u64", - }, - { - name: "to", - type: "u64", - }, - { - name: "kind", - type: "u32", - }, - { - name: "data", - type: "vector", - }, - { - name: "specification_hash", - type: "vector", - }, - { - name: "root", - type: "vector", - }, - { - name: "reward", - type: "0x1::smart_table::SmartTable", - }, - ], - }, - { - name: "SetFee", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "fee", - type: "u32", - }, - ], - }, - { - name: "SetFeeRebate", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "account", - type: "address", - }, - { - name: "rebate", - type: "u32", - }, - ], - }, - { - name: "SetMaximumCampaignDuration", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "maximum_campaign_duration", - type: "u64", - }, - ], - }, - { - name: "SetMinimumCampaignDuration", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "minimum_campaign_duration", - type: "u64", - }, - ], - }, - { - name: "SetMinimumFeeTokenRate", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "token", - type: "address", - }, - { - name: "minimum_rate", - type: "u64", - }, - ], - }, - { - name: "SetMinimumRewardTokenRate", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "token", - type: "address", - }, - { - name: "minimum_rate", - type: "u64", - }, - ], - }, - { - name: "SetUpdater", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "updater", - type: "address", - }, - ], - }, - { - name: "TransferCampaignOwnership", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "campaign_id", - type: "vector", - }, - { - name: "owner", - type: "address", - }, - ], - }, - { - name: "TransferOwnership", - is_native: false, - is_event: true, - abilities: ["drop", "store"], - generic_type_params: [], - fields: [ - { - name: "owner", - type: "address", - }, - ], - }, - ], -} as const; - export const velodromPoolAbi = [ { anonymous: false, diff --git a/packages/frontend/src/commons/odyssey.ts b/packages/frontend/src/commons/odyssey.ts index fc6834a43..63e5df94f 100644 --- a/packages/frontend/src/commons/odyssey.ts +++ b/packages/frontend/src/commons/odyssey.ts @@ -1,19 +1,70 @@ import { SupportedOdysseyStrategy } from "@metrom-xyz/sdk"; +import type { FunctionComponent } from "react"; +import type { SVGIcon } from "../types/common"; -export const ODYSSEY_STRATEGIES_NAME: Record = - { - [SupportedOdysseyStrategy.AaveV3BorrowStrategy]: "Aave V3 borrow", - [SupportedOdysseyStrategy.AjnaBorrowStrategy]: "Ajna borrow", - [SupportedOdysseyStrategy.CompoundV2BorrowStrategy]: - "Compound V2 borrow", - [SupportedOdysseyStrategy.CompoundV2VesperStrategy]: - "Compound V2 vesper", - [SupportedOdysseyStrategy.CompoundV3BorrowStrategy]: - "Compound V3 borrow", - [SupportedOdysseyStrategy.CompoundV3VesperStrategy]: - "Compound V3 vesper", - [SupportedOdysseyStrategy.SynthStrategy]: "Synth", - [SupportedOdysseyStrategy.ERC4626Strategy]: "ERC4626", - [SupportedOdysseyStrategy.EulerV2BorrowStrategy]: "Euler V2 borrow", - [SupportedOdysseyStrategy.MorphoBorrowStrategy]: "Morpho borrow", - }; +export interface OdysseyStrategyData { + id: SupportedOdysseyStrategy; + name: string; + icon?: FunctionComponent; + docs?: string; +} + +export const ODYSSEY_STRATEGIES: Record< + SupportedOdysseyStrategy, + OdysseyStrategyData +> = { + [SupportedOdysseyStrategy.AaveV2BorrowStrategy]: { + id: SupportedOdysseyStrategy.AaveV2BorrowStrategy, + name: "Aave V2 borrow", + }, + [SupportedOdysseyStrategy.AaveV3BorrowStrategy]: { + id: SupportedOdysseyStrategy.AaveV3BorrowStrategy, + name: "Aave V3 borrow", + }, + [SupportedOdysseyStrategy.AjnaBorrowStrategy]: { + id: SupportedOdysseyStrategy.AjnaBorrowStrategy, + name: "Ajna borrow", + }, + [SupportedOdysseyStrategy.CompoundV2BorrowStrategy]: { + id: SupportedOdysseyStrategy.CompoundV2BorrowStrategy, + name: "Compound V2 borrow", + }, + [SupportedOdysseyStrategy.CompoundV2VesperStrategy]: { + id: SupportedOdysseyStrategy.CompoundV2VesperStrategy, + name: "Compound V2 vesper", + }, + [SupportedOdysseyStrategy.CompoundV3BorrowStrategy]: { + id: SupportedOdysseyStrategy.CompoundV3BorrowStrategy, + name: "Compound V3 borrow", + }, + [SupportedOdysseyStrategy.CompoundV3VesperStrategy]: { + id: SupportedOdysseyStrategy.CompoundV3VesperStrategy, + name: "Compound V3 vesper", + }, + [SupportedOdysseyStrategy.SynthStrategy]: { + id: SupportedOdysseyStrategy.SynthStrategy, + name: "Metronome Synth", + }, + [SupportedOdysseyStrategy.ERC4626Strategy]: { + id: SupportedOdysseyStrategy.ERC4626Strategy, + name: "ERC4626", + }, + [SupportedOdysseyStrategy.EulerV2BorrowStrategy]: { + id: SupportedOdysseyStrategy.EulerV2BorrowStrategy, + name: "Euler V2 borrow", + }, + [SupportedOdysseyStrategy.MorphoBorrowStrategy]: { + id: SupportedOdysseyStrategy.MorphoBorrowStrategy, + name: "Morpho borrow", + }, +}; + +export const ODYSSEY_BORROW_STRATEGIES: SupportedOdysseyStrategy[] = [ + SupportedOdysseyStrategy.AaveV2BorrowStrategy, + SupportedOdysseyStrategy.AaveV3BorrowStrategy, + SupportedOdysseyStrategy.AjnaBorrowStrategy, + SupportedOdysseyStrategy.CompoundV2BorrowStrategy, + SupportedOdysseyStrategy.CompoundV3BorrowStrategy, + SupportedOdysseyStrategy.EulerV2BorrowStrategy, + SupportedOdysseyStrategy.MorphoBorrowStrategy, +]; diff --git a/packages/frontend/src/components/create-campaign/form/index.tsx b/packages/frontend/src/components/create-campaign/form/index.tsx index d1be8c138..fbdbdeb9f 100644 --- a/packages/frontend/src/components/create-campaign/form/index.tsx +++ b/packages/frontend/src/components/create-campaign/form/index.tsx @@ -23,6 +23,7 @@ import { AaveV3BridgeAndSupplyForm } from "./aave-v3-bridge-and-supply-form"; import { useForms } from "@/src/hooks/useForms"; import { FormNotSupported } from "../form-not-supported"; import { HoldFungibleAssetForm } from "./hold-fungible-asset-form"; +import { OdysseyForm } from "./odyssey-form"; import styles from "./styles.module.css"; @@ -106,6 +107,12 @@ export function CreateCampaignForm({ onPreviewClick={handlePreviewOnClick} /> )} + {type === BaseCampaignType.Odyssey && ( + + )} {type === PartnerCampaignType.AaveV3BridgeAndSupply && ( void; +} + +const initialPayload: OdysseyCampaignPayload = { + distributables: { type: DistributablesType.Tokens }, +}; + +export function OdysseyForm({ + unsupportedChain, + onPreviewClick, +}: OdysseyFormProps) { + const t = useTranslations("newCampaign"); + const { id: chainId } = useChainWithType(); + + const [payload, setPayload] = useState(initialPayload); + const [errors, setErrors] = useState({}); + + const previewPayload = useMemo(() => { + if (Object.values(errors).some((error) => !!error)) return null; + return validatePayload(chainId, payload); + }, [chainId, payload, errors]); + + const noDistributables = useMemo(() => { + if (!payload.distributables) return true; + + const { type } = payload.distributables; + + if (type === DistributablesType.FixedPoints) + return ( + !payload.distributables.fee || !payload.distributables.points + ); + if (type === DistributablesType.Tokens) + return ( + !payload.distributables.tokens || + payload.distributables.tokens.length === 0 + ); + + return true; + }, [payload.distributables]); + + useEffect(() => { + setPayload(initialPayload); + }, [chainId]); + + const handlePayloadOnChange = useCallback( + (part: OdysseyCampaignPayloadPart) => { + setPayload((prev) => ({ ...prev, ...part })); + }, + [], + ); + + const handlePayloadOnError = useCallback( + (errors: CampaignPayloadErrors) => { + setErrors((state) => ({ ...state, ...errors })); + }, + [], + ); + + function handlePreviewOnClick() { + onPreviewClick(previewPayload); + } + + const usdTvl = getOdysseyUsdTarget({ + asset: payload.asset, + strategy: payload.strategy?.id, + }); + + return ( +
+
+ + + + + + + + +
+ +
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/form/odyssey-form/styles.module.css b/packages/frontend/src/components/create-campaign/form/odyssey-form/styles.module.css new file mode 100644 index 000000000..a2b64825b --- /dev/null +++ b/packages/frontend/src/components/create-campaign/form/odyssey-form/styles.module.css @@ -0,0 +1,13 @@ +@reference "../../../../app.css"; + +.root { + @apply flex flex-col gap-5 w-full; +} + +.stepsWrapper { + @apply flex flex-col gap-2; +} + +.button { + @apply w-full h-16; +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/index.tsx new file mode 100644 index 000000000..80b8db4e9 --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/index.tsx @@ -0,0 +1,118 @@ +import { useCallback, useEffect, useState } from "react"; +import { useChainWithType } from "@/src/hooks/useChainWithType"; +import { Step } from "@/src/components/step"; +import { StepPreview } from "@/src/components/step/preview"; +import { StepContent } from "@/src/components/step/content"; +import { useTranslations } from "next-intl"; +import { AssetsList } from "./list"; +import { Typography } from "@metrom-xyz/ui"; +import type { OdysseyAsset } from "@metrom-xyz/sdk"; +import { RemoteLogo } from "@/src/components/remote-logo"; +import type { OdysseyStrategyData } from "@/src/commons/odyssey"; +import type { OdysseyCampaignPayloadPart } from "@/src/types/campaign"; +import { useOdysseyAssets } from "@/src/hooks/useOdysseyAssets"; +import type { OdysseyProtocol } from "@metrom-xyz/chains"; + +import styles from "./styles.module.css"; + +interface OdysseyStepProps { + disabled?: boolean; + brand?: OdysseyProtocol; + strategy?: OdysseyStrategyData; + asset?: OdysseyAsset; + onAssetChange: (value: OdysseyCampaignPayloadPart) => void; +} + +export function OdysseyAssetsStep({ + disabled, + brand, + strategy, + asset, + onAssetChange, +}: OdysseyStepProps) { + const t = useTranslations("newCampaign.form.odyssey.assets"); + + const [open, setOpen] = useState(false); + + const { id: chainId, type: chainType } = useChainWithType(); + const { loading, assets } = useOdysseyAssets({ + chainId, + chainType, + brand: brand?.slug, + strategy: strategy?.id, + }); + + useEffect(() => { + setOpen(false); + }, [chainId]); + + useEffect(() => { + if (disabled || !!assets) return; + setOpen(true); + }, [assets, disabled, strategy]); + + useEffect(() => { + onAssetChange({ asset: undefined }); + }, [brand, strategy, onAssetChange]); + + const handleAssetChange = useCallback( + (asset: OdysseyAsset) => { + onAssetChange({ asset }); + setOpen(false); + }, + [onAssetChange], + ); + + function handleStepOnClick() { + setOpen((open) => !open); + } + + return ( + + +
+ + {t("title")} + +
+ + } + > + {asset && ( +
+
+ +
+ + {asset.symbol} + +
+ )} +
+ + + +
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/index.tsx b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/index.tsx new file mode 100644 index 000000000..d9658823d --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/index.tsx @@ -0,0 +1,81 @@ +import { type OdysseyAsset } from "@metrom-xyz/sdk"; +import { Typography } from "@metrom-xyz/ui"; +import { useTranslations } from "next-intl"; +import { Row, RowSkeleton } from "./row"; +import { + ODYSSEY_BORROW_STRATEGIES, + type OdysseyStrategyData, +} from "@/src/commons/odyssey"; + +import styles from "./styles.module.css"; + +interface AssetsListProps { + loading?: boolean; + strategy?: OdysseyStrategyData; + selected?: OdysseyAsset; + assets?: OdysseyAsset[]; + onChange: (asset: OdysseyAsset) => void; +} + +export function AssetsList({ + loading, + strategy, + selected, + assets, + onChange, +}: AssetsListProps) { + const t = useTranslations("newCampaign.form.odyssey.assets"); + + return ( +
+
+ + {t("list.token")} + + {strategy && ( + + {t( + ODYSSEY_BORROW_STRATEGIES.includes(strategy.id) + ? "list.deposited" + : "list.allocated", + )} + + )} +
+ {loading ? ( + <> + + + + + ) : assets && assets.length > 0 ? ( + assets.map((asset) => { + return ( + + ); + }) + ) : ( +
+ {/* TODO: add illustration */} + {t("list.empty")} +
+ )} +
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/index.tsx b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/index.tsx new file mode 100644 index 000000000..fb746eae8 --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/index.tsx @@ -0,0 +1,60 @@ +import { type OdysseyAsset } from "@metrom-xyz/sdk"; +import { RemoteLogo } from "@/src/components/remote-logo"; +import { Skeleton, Typography } from "@metrom-xyz/ui"; +import { useCallback } from "react"; +import { useChainWithType } from "@/src/hooks/useChainWithType"; +import classNames from "classnames"; +import { formatUsdAmount } from "@/src/utils/format"; +import type { OdysseyStrategyData } from "@/src/commons/odyssey"; +import { getOdysseyUsdTarget } from "@/src/utils/odyssey"; + +import styles from "./styles.module.css"; + +interface RowProps { + strategy?: OdysseyStrategyData; + selected?: boolean; + asset: OdysseyAsset; + onChange: (asset: OdysseyAsset) => void; +} + +export function Row({ strategy, selected, asset, onChange }: RowProps) { + const { id: chainId } = useChainWithType(); + + const handleOnClick = useCallback(() => { + onChange(asset); + }, [asset, onChange]); + + return ( +
+
+ + + {asset.symbol} + +
+ + {formatUsdAmount({ + amount: getOdysseyUsdTarget({ + strategy: strategy?.id, + asset, + }), + })} + +
+ ); +} + +export function RowSkeleton() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/styles.module.css b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/styles.module.css new file mode 100644 index 000000000..7e08f018c --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/row/styles.module.css @@ -0,0 +1,23 @@ +@reference "../../../../../../app.css"; + +.root { + @apply h-14 + flex + items-center + gap-3 + px-4 + select-none + transition-colors + duration-200 + ease-in-out + hover:cursor-pointer + surface-primary-hover; +} + +.collateral { + @apply w-full flex items-center gap-3; +} + +.active { + @apply bg-gray-150 dark:bg-neutral-800; +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/styles.module.css b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/styles.module.css new file mode 100644 index 000000000..2739fa023 --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/list/styles.module.css @@ -0,0 +1,17 @@ +@reference "../../../../../app.css"; + +.root { + @apply flex + flex-col + gap-1 + w-full + overflow-hidden; +} + +.listHeader { + @apply flex justify-between items-center mb-1 p-4; +} + +.empty { + @apply flex justify-center pt-4; +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/styles.module.css b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/styles.module.css new file mode 100644 index 000000000..e993fde5c --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-assets-step/styles.module.css @@ -0,0 +1,38 @@ +@reference "../../../../app.css"; + +.preview { + @apply flex gap-2 items-center; +} + +.previewLabelWrapper { + @apply w-full flex justify-between items-center; +} + +.previewTextWrapper { + @apply flex items-center gap-2; +} + +.previewLabel { + font-size: inherit; + color: inherit; +} + +.previewWrapper { + @apply flex items-center gap-2; +} + +.collateralWrapper { + @apply flex; +} + +.collateralLogo:not(:first-child) { + @apply -ml-2; +} + +.contentWrapper { + @apply flex flex-col gap-4 p-4; +} + +.applyButton { + @apply w-full; +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/index.tsx new file mode 100644 index 000000000..8cf890c8c --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/index.tsx @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Step } from "@/src/components/step"; +import { StepPreview } from "@/src/components/step/preview"; +import { StepContent } from "@/src/components/step/content"; +import classNames from "classnames"; +import { Typography } from "@metrom-xyz/ui"; +import { type OdysseyCampaignPayloadPart } from "@/src/types/campaign"; +import { ProtocolType, type OdysseyProtocol } from "@metrom-xyz/chains"; +import { useProtocolsInChain } from "@/src/hooks/useProtocolsInChain"; +import { ProtocolLogo } from "@/src/components/protocol-logo"; +import { useChainWithType } from "@/src/hooks/useChainWithType"; + +import styles from "./styles.module.css"; + +interface OdysseyBrandStepProps { + disabled?: boolean; + brand?: OdysseyProtocol; + onBrandChange: (value: OdysseyCampaignPayloadPart) => void; +} + +export function OdysseyBrandStep({ + disabled, + brand, + onBrandChange, +}: OdysseyBrandStepProps) { + const t = useTranslations("newCampaign.form.odyssey.brand"); + const [open, setOpen] = useState(true); + + const { id: chainId, type: chainType } = useChainWithType(); + const supportedBrands = useProtocolsInChain({ + chainId, + chainType, + type: ProtocolType.Odyssey, + active: true, + }); + + const selected = useMemo(() => { + if (!brand) return undefined; + return supportedBrands.find(({ slug }) => slug === brand.slug); + }, [supportedBrands, brand]); + + useEffect(() => { + setOpen(false); + }, [chainId]); + + useEffect(() => { + if (!!brand || supportedBrands.length !== 1) return; + onBrandChange({ + brand: supportedBrands[0], + }); + setOpen(false); + }, [supportedBrands, brand, onBrandChange]); + + const getPlatformChangeHandler = useCallback( + (newPlatform: OdysseyProtocol) => { + return () => { + if (brand && brand.slug === newPlatform.slug) return; + onBrandChange({ + brand: newPlatform, + }); + setOpen(false); + }; + }, + [brand, onBrandChange], + ); + + function handleStepOnClick() { + setOpen((open) => !open); + } + + return ( + + + {!!selected && ( +
+ + + {selected.name} + +
+ )} +
+ +
+ {supportedBrands.map((availablePlatform) => ( +
+ + + {availablePlatform.name} + +
+ ))} +
+
+
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/styles.module.css b/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/styles.module.css new file mode 100644 index 000000000..1dadc206e --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-brand-step/styles.module.css @@ -0,0 +1,39 @@ +@reference "../../../../app.css"; + +.preview { + @apply flex gap-2 items-center; +} + +.brandWrapper { + @apply flex + flex-col + gap-1 + max-h-96 + overflow-y-auto + rounded-b-2xl + scroll-pt-0.5 + snap-y; +} + +.brandRow { + @apply flex + h-[3.375rem] + gap-2 + items-center + p-4 + select-none + snap-start + transition-colors + duration-200 + ease-in-out + hover:cursor-pointer + surface-primary-hover; +} + +.brandRowSelected { + @apply bg-gray-150 dark:bg-neutral-800; +} + +.brandRow > * { + @apply pointer-events-none; +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/index.tsx b/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/index.tsx new file mode 100644 index 000000000..f392e4fbf --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/index.tsx @@ -0,0 +1,111 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Step } from "@/src/components/step"; +import { StepPreview } from "@/src/components/step/preview"; +import { StepContent } from "@/src/components/step/content"; +import { Typography } from "@metrom-xyz/ui"; +import { type OdysseyCampaignPayloadPart } from "@/src/types/campaign"; +import { useChainWithType } from "@/src/hooks/useChainWithType"; +import { useOdysseyStrategies } from "@/src/hooks/useOdysseyStrategies"; +import { + ODYSSEY_STRATEGIES, + type OdysseyStrategyData, +} from "@/src/commons/odyssey"; + +import styles from "./styles.module.css"; +import classNames from "classnames"; + +interface OdysseyStrategyStepProps { + disabled?: boolean; + strategy?: OdysseyStrategyData; + onStrategyChange: (value: OdysseyCampaignPayloadPart) => void; +} + +export function OdysseyStrategyStep({ + disabled, + strategy, + onStrategyChange, +}: OdysseyStrategyStepProps) { + const t = useTranslations("newCampaign.form.odyssey.strategy"); + const [open, setOpen] = useState(true); + + const { id: chainId, type: chainType } = useChainWithType(); + const supportedStrategies = useOdysseyStrategies({ + chainId, + chainType, + crossVm: true, + enabled: !!strategy, + }); + + const supportedStrategiesData: OdysseyStrategyData[] = + supportedStrategies.map((supported) => ODYSSEY_STRATEGIES[supported]); + + const selected: OdysseyStrategyData | undefined = useMemo(() => { + if (!strategy) return undefined; + + const selected = supportedStrategies.find( + (supported) => supported === strategy.id, + ); + if (!selected) return undefined; + + return { ...strategy, id: selected }; + }, [supportedStrategies, strategy]); + + useEffect(() => { + setOpen(false); + }, [chainId]); + + const getStrategyChangeHandler = useCallback( + (newStrategy: OdysseyStrategyData) => { + return () => { + if (strategy && strategy.id === newStrategy.id) return; + onStrategyChange({ + strategy: newStrategy, + }); + setOpen(false); + }; + }, + [strategy, onStrategyChange], + ); + + function handleStepOnClick() { + setOpen((open) => !open); + } + + return ( + + + {!!selected && ( +
+ + {selected.name} + +
+ )} +
+ +
+ {supportedStrategiesData.map((data) => ( +
+ + {data.name} + +
+ ))} +
+
+
+ ); +} diff --git a/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/styles.module.css b/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/styles.module.css new file mode 100644 index 000000000..755adcb7b --- /dev/null +++ b/packages/frontend/src/components/create-campaign/steps/odyssey-strategy-step/styles.module.css @@ -0,0 +1,26 @@ +@reference "../../../../app.css"; + +.preview { + @apply flex gap-2 items-center; +} + +.strategies { + @apply flex flex-col; +} + +.strategy { + @apply w-full + flex + gap-6 + items-center + p-4 + transition-colors + duration-200 + ease-in-out + hover:cursor-pointer + surface-primary-hover; +} + +.active { + @apply bg-gray-150 dark:bg-neutral-800; +} diff --git a/packages/frontend/src/hooks/useCampaignsFiltersOptions.ts b/packages/frontend/src/hooks/useCampaignsFiltersOptions.ts index 53336e0d4..b7a03ebee 100644 --- a/packages/frontend/src/hooks/useCampaignsFiltersOptions.ts +++ b/packages/frontend/src/hooks/useCampaignsFiltersOptions.ts @@ -42,11 +42,13 @@ export function useCampaignsFiltersOptions() { ); const protocolOptions: SelectOption[] = useMemo(() => { - return supportedProtocols.map((protocol) => ({ - label: protocol.name, - protocol, - value: protocol.slug, - })); + return supportedProtocols + .map((protocol) => ({ + label: protocol.name, + protocol, + value: protocol.slug, + })) + .sort((a, b) => a.label.localeCompare(b.label, "en")); }, [supportedProtocols]); const chainOptions: ChainFilterOption[] = useMemo(() => { @@ -62,7 +64,7 @@ export function useCampaignsFiltersOptions() { query: chainData.name.toLowerCase().replaceAll(" ", "_"), }); } - return options; + return options.sort((a, b) => a.label.localeCompare(b.label, "en")); }, [supportedChains]); return { statusOptions, protocolOptions, chainOptions }; diff --git a/packages/frontend/src/hooks/useOdysseyAssets.ts b/packages/frontend/src/hooks/useOdysseyAssets.ts new file mode 100644 index 000000000..eb20828d4 --- /dev/null +++ b/packages/frontend/src/hooks/useOdysseyAssets.ts @@ -0,0 +1,70 @@ +import type { SupportedChain } from "@metrom-xyz/contracts"; +import { + ChainType, + SupportedOdyssey, + SupportedOdysseyStrategy, + type OdysseyAsset, +} from "@metrom-xyz/sdk"; +import { useQuery } from "@tanstack/react-query"; +import type { HookBaseParams } from "../types/hooks"; +import { METROM_API_CLIENT } from "../commons"; + +interface UseOdysseyAssetsParams extends HookBaseParams { + chainId: SupportedChain; + chainType: ChainType; + brand?: SupportedOdyssey; + strategy?: SupportedOdysseyStrategy; +} + +type QueryKey = [ + string, + SupportedOdyssey | undefined, + SupportedOdysseyStrategy | undefined, + SupportedChain, + ChainType, +]; + +export function useOdysseyAssets({ + chainId, + chainType, + brand, + strategy, + enabled = true, +}: UseOdysseyAssetsParams): { + loading: boolean; + assets?: OdysseyAsset[]; +} { + const { data: assets, isPending: loading } = useQuery({ + queryKey: ["odyssey-assets", brand, strategy, chainId, chainType], + queryFn: async ({ queryKey }) => { + const [, brand, strategy, chainId, chainType] = + queryKey as QueryKey; + if (!brand || !strategy) return null; + + try { + const collaterals = await METROM_API_CLIENT.fetchOdysseyAssets({ + chainId, + chainType, + brand, + strategy, + }); + + return collaterals.sort((a, b) => + a.name.localeCompare(b.name, "en"), + ); + } catch (error) { + console.error( + `Could not fetch odyssey assets for brand ${brand} and strategy ${strategy}, in chain with id ${chainId} and type ${chainType}: ${error}`, + ); + throw error; + } + }, + refetchOnMount: false, + enabled: enabled && !!brand, + }); + + return { + loading, + assets: assets || undefined, + }; +} diff --git a/packages/frontend/src/hooks/useOdysseyStrategies.ts b/packages/frontend/src/hooks/useOdysseyStrategies.ts new file mode 100644 index 000000000..6fcbc21a5 --- /dev/null +++ b/packages/frontend/src/hooks/useOdysseyStrategies.ts @@ -0,0 +1,26 @@ +import { useChainData } from "./useChainData"; +import type { HookBaseParams, HookCrossVmParams } from "../types/hooks"; +import type { ChainType, SupportedOdysseyStrategy } from "@metrom-xyz/sdk"; +import { ProtocolType } from "@metrom-xyz/chains"; + +interface UseOdysseyStrategiesProps extends HookBaseParams, HookCrossVmParams { + chainId: number; + chainType?: ChainType; +} + +// TODO: add brand filter if we will have multiple brands for odyssey +export function useOdysseyStrategies({ + chainId, + chainType, + crossVm = false, +}: UseOdysseyStrategiesProps): SupportedOdysseyStrategy[] { + const chainData = useChainData({ chainId, chainType, crossVm }); + if (!chainData) return []; + + const odysseyProtocol = chainData.protocols.find( + (protocol) => protocol.type === ProtocolType.Odyssey, + ); + if (!odysseyProtocol) return []; + + return odysseyProtocol.strategies; +} diff --git a/packages/frontend/src/types/campaign.ts b/packages/frontend/src/types/campaign.ts index 7ac7d5bcb..f8e3098c6 100644 --- a/packages/frontend/src/types/campaign.ts +++ b/packages/frontend/src/types/campaign.ts @@ -20,6 +20,7 @@ import { type FixedPointDistributables, type DynamicPointDistributables, type AmmPool, + type OdysseyAsset, } from "@metrom-xyz/sdk"; import type { Dayjs } from "dayjs"; import type { Address } from "viem"; @@ -31,8 +32,13 @@ import { type ChainData, type DexProtocol, type LiquityV2Protocol, + type OdysseyProtocol, } from "@metrom-xyz/chains"; import type { PropertyUnion } from "./utils"; +import { + ODYSSEY_BORROW_STRATEGIES, + type OdysseyStrategyData, +} from "../commons/odyssey"; export interface ClaimWithRemaining extends Claim { remaining: UsdPricedOnChainAmount; @@ -101,6 +107,12 @@ export interface HoldFungibleAssetCampaignPayload extends BaseCampaignPayload { stakingAssets: FungibleAssetInfo[]; } +export interface OdysseyCampaignPayload extends BaseCampaignPayload { + brand?: OdysseyProtocol; + strategy?: OdysseyStrategyData; + asset?: OdysseyAsset; +} + export interface CampaignPayloadTokenDistributables { type: DistributablesType.Tokens; tokens?: WhitelistedErc20TokenAmount[]; @@ -293,6 +305,31 @@ export class HoldFungibleAssetCampaignPreviewPayload extends BaseCampaignPreview } } +export class OdysseyCampaignPreviewPayload extends BaseCampaignPreviewPayload { + public readonly kind: CampaignKind = CampaignKind.OdysseyStrategy; + constructor( + public readonly brand: OdysseyProtocol, + public readonly strategy: OdysseyStrategyData, + public readonly asset: OdysseyAsset, + ...baseArgs: ConstructorParameters + ) { + super(...baseArgs); + } + + getTargetValue(): TargetValue | undefined { + if (ODYSSEY_BORROW_STRATEGIES.includes(this.strategy.id)) + return { + usd: this.asset.usdTotalDeposited, + raw: this.asset.totalDeposited, + }; + + return { + usd: this.asset.usdTotalAllocated, + raw: this.asset.totalAllocated, + }; + } +} + export class EmptyTargetCampaignPreviewPayload extends BaseCampaignPreviewPayload { public readonly kind: CampaignKind = CampaignKind.EmptyTarget; constructor( @@ -310,6 +347,7 @@ export type CampaignPreviewPayload = | AmmPoolLiquidityCampaignPreviewPayload | LiquityV2CampaignPreviewPayload | AaveV3CampaignPreviewPayload + | OdysseyCampaignPreviewPayload | EmptyTargetCampaignPreviewPayload; export interface DistributablesCampaignPreviewPayload< @@ -347,7 +385,9 @@ export type CampaignPayloadPart = ? Partial : T extends ProtocolType.AaveV3 ? Partial - : never; + : T extends ProtocolType.Odyssey + ? Partial + : never; export type AmmPoolLiquidityCampaignPayloadPart = PropertyUnion; @@ -360,6 +400,8 @@ export type AaveV3CampaignPayloadPart = PropertyUnion; export type HoldFungibleAssetCampaignPayloadPart = PropertyUnion; +export type OdysseyCampaignPayloadPart = PropertyUnion; + export class Campaign extends SdkCampaign { constructor( campaign: SdkCampaign, diff --git a/packages/frontend/src/utils/campaign-bundle.ts b/packages/frontend/src/utils/campaign-bundle.ts index 4f357d94e..9eb59c75b 100644 --- a/packages/frontend/src/utils/campaign-bundle.ts +++ b/packages/frontend/src/utils/campaign-bundle.ts @@ -13,6 +13,7 @@ import { EmptyTargetCampaignPreviewPayload, HoldFungibleAssetCampaignPreviewPayload, LiquityV2CampaignPreviewPayload, + OdysseyCampaignPreviewPayload, type CampaignPreviewPayload, } from "../types/campaign"; import { @@ -64,6 +65,19 @@ export function buildCampaignDataBundleEvm(payload: CampaignPreviewPayload) { blacklistedCollaterals, ], ); + } else if (payload instanceof OdysseyCampaignPreviewPayload) { + return encodeAbiParameters( + [ + { name: "brand", type: "bytes32" }, + { name: "strategy", type: "uint32" }, + { name: "collateral", type: "address" }, + ], + [ + stringToHex(payload.brand.slug).padEnd(66, "0") as Hex, + payload.strategy.id, + payload.asset.address, + ], + ); } else if (payload instanceof EmptyTargetCampaignPreviewPayload) { return "0x"; } else return null; diff --git a/packages/frontend/src/utils/campaign.ts b/packages/frontend/src/utils/campaign.ts index aab5f0d75..593486fb8 100644 --- a/packages/frontend/src/utils/campaign.ts +++ b/packages/frontend/src/utils/campaign.ts @@ -24,7 +24,7 @@ import { SECONDS_IN_YEAR } from "../commons"; import { type LiquityV2Protocol } from "@metrom-xyz/chains"; import { getTranslations } from "next-intl/server"; import { getChainData, getCrossVmChainData } from "./chain"; -import { ODYSSEY_STRATEGIES_NAME } from "../commons/odyssey"; +import { ODYSSEY_STRATEGIES } from "../commons/odyssey"; // TODO: Should maybe avoid passing the t function as a parameter https://github.com/amannn/next-intl/issues/1704#issuecomment-2643211585. export function getCampaignName( @@ -114,9 +114,9 @@ export function getCampaignName( case TargetType.Odyssey: { return t("campaignActions.odysseyStrategy", { strategy: - ODYSSEY_STRATEGIES_NAME[ + ODYSSEY_STRATEGIES[ campaign.target.strategyId as SupportedOdysseyStrategy - ], + ].name, asset: campaign.target.asset.symbol, }); } diff --git a/packages/frontend/src/utils/odyssey.ts b/packages/frontend/src/utils/odyssey.ts new file mode 100644 index 000000000..54930871c --- /dev/null +++ b/packages/frontend/src/utils/odyssey.ts @@ -0,0 +1,16 @@ +import { SupportedOdysseyStrategy, type OdysseyAsset } from "@metrom-xyz/sdk"; +import { ODYSSEY_BORROW_STRATEGIES } from "../commons/odyssey"; + +interface GetUsdTvlParams { + strategy?: SupportedOdysseyStrategy; + asset?: OdysseyAsset; +} + +export function getOdysseyUsdTarget({ strategy, asset }: GetUsdTvlParams) { + if (!strategy || !asset) return undefined; + + if (ODYSSEY_BORROW_STRATEGIES.includes(strategy)) + return asset.usdTotalDeposited; + + return asset.usdTotalAllocated; +} diff --git a/packages/sdk/src/client/backend.ts b/packages/sdk/src/client/backend.ts index 2210e4f14..ddcab6920 100644 --- a/packages/sdk/src/client/backend.ts +++ b/packages/sdk/src/client/backend.ts @@ -8,6 +8,8 @@ import { SupportedBridge, type SupportedProtocol, SupportedGmxV1, + SupportedOdyssey, + SupportedOdysseyStrategy, } from "../commons"; import type { BackendCampaignOrderBy, @@ -101,6 +103,8 @@ import type { BackendFungibleAssetResponse } from "./types/fungible-asset"; import type { AmmPool, CampaignAmmPool } from "src/types/pools"; import type { BackendProjectsResponse } from "./types/projects"; import type { Project } from "src/types/projects"; +import type { OdysseyAsset } from "src/types/odyssey"; +import type { BackendOdysseyAssetsResponse } from "./types/odyssey"; const MIN_TICK = -887272; const MAX_TICK = -MIN_TICK; @@ -258,6 +262,11 @@ export interface FetchAaveV3CollateralUsdNetSupplyParams extends ChainParams { blacklistedCrossBorrowCollaterals?: Address[]; } +export interface FetchOdysseyAssetsParams extends ChainParams { + brand: SupportedOdyssey; + strategy: SupportedOdysseyStrategy; +} + interface InitializedTick { idx: number; liquidityNet: bigint; @@ -1071,6 +1080,35 @@ export class MetromApiClient { return b.campaigns.total - a.campaigns.total; }); } + + async fetchOdysseyAssets( + params: FetchOdysseyAssetsParams, + ): Promise { + const url = new URL( + `v2/odyssey/${params.chainType}/${params.chainId}/${params.brand}/${params.strategy}/assets`, + this.baseUrl, + ); + + const response = await fetch(url); + if (!response.ok) + throw new Error( + `Response not ok while fetching odyssey assets: ${await response.text()}`, + ); + + const parsedResponse = + (await response.json()) as BackendOdysseyAssetsResponse; + + return parsedResponse.assets.map((asset) => { + return { + ...asset, + // FIXME: it's probably better to have chain id and chainType in the response + chainId: params.chainId, + chainType: params.chainType, + totalAllocated: BigInt(asset.totalAllocated), + totalDeposited: BigInt(asset.totalDeposited), + }; + }); + } } function processCampaignsResponse( diff --git a/packages/sdk/src/client/types/odyssey.ts b/packages/sdk/src/client/types/odyssey.ts new file mode 100644 index 000000000..a900b2cf5 --- /dev/null +++ b/packages/sdk/src/client/types/odyssey.ts @@ -0,0 +1,12 @@ +import type { BackendErc20Token } from "./commons"; + +export interface BackendOdysseyAsset extends BackendErc20Token { + totalAllocated: string; + totalDeposited: string; + usdTotalAllocated: number; + usdTotalDeposited: number; +} + +export interface BackendOdysseyAssetsResponse { + assets: BackendOdysseyAsset[]; +} diff --git a/packages/sdk/src/commons.ts b/packages/sdk/src/commons.ts index 970bedd10..b089c114e 100644 --- a/packages/sdk/src/commons.ts +++ b/packages/sdk/src/commons.ts @@ -91,7 +91,7 @@ export type SupportedProtocol = | SupportedYieldSeeker; export enum SupportedOdysseyStrategy { - // TODO: strategy wity id 1 is missing the name + AaveV2BorrowStrategy = 1, AaveV3BorrowStrategy = 2, AjnaBorrowStrategy = 3, CompoundV2BorrowStrategy = 4, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e4656445b..31da151d1 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -22,6 +22,7 @@ export * from "./types/kpi-measurements"; export * from "./types/leaderboards"; export * from "./types/aave-v3"; export * from "./types/liquity-v2"; +export * from "./types/odyssey"; export * from "./types/pools"; export * from "./types/projects"; export * from "./types/reward-tokens"; diff --git a/packages/sdk/src/types/odyssey.ts b/packages/sdk/src/types/odyssey.ts new file mode 100644 index 000000000..45e3802d6 --- /dev/null +++ b/packages/sdk/src/types/odyssey.ts @@ -0,0 +1,10 @@ +import type { ChainType, Erc20Token } from "./commons"; + +export interface OdysseyAsset extends Erc20Token { + chainId: number; + chainType: ChainType; + totalAllocated: bigint; + totalDeposited: bigint; + usdTotalAllocated: number; + usdTotalDeposited: number; +}