diff --git a/config/abis/index.ts b/config/abis/index.ts index 6f35ed50..9456edee 100644 --- a/config/abis/index.ts +++ b/config/abis/index.ts @@ -10,4 +10,5 @@ export * from "./tokens"; export * from "./custom-pools"; export * from "./ve33"; export * from "./omegaQuoter"; -export * from "./prediction"; \ No newline at end of file +export * from "./prediction"; +export * from "./nav-hook"; diff --git a/config/abis/nav-hook/index.ts b/config/abis/nav-hook/index.ts new file mode 100644 index 00000000..100c5d1e --- /dev/null +++ b/config/abis/nav-hook/index.ts @@ -0,0 +1,2 @@ +export * from "./priceConvergenceVault"; +export * from "./priceConvergenceVaultDepositGuard"; diff --git a/config/abis/nav-hook/priceConvergenceVault.ts b/config/abis/nav-hook/priceConvergenceVault.ts new file mode 100644 index 00000000..649e09a1 --- /dev/null +++ b/config/abis/nav-hook/priceConvergenceVault.ts @@ -0,0 +1,1059 @@ +export const priceConvergenceVaultABI = [ + { + inputs: [ + { + internalType: "address", + name: "_pool", + type: "address", + }, + { + internalType: "address", + name: "_factory", + type: "address", + }, + { + internalType: "uint128", + name: "_fullRangeLiquidity", + type: "uint128", + }, + { + internalType: "address", + name: "_vaultMath", + type: "address", + }, + { + internalType: "uint32", + name: "_twapPeriod", + type: "uint32", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InvalidFactory", + type: "error", + }, + { + inputs: [], + name: "InvalidPosition", + type: "error", + }, + { + inputs: [], + name: "InvalidShares", + type: "error", + }, + { + inputs: [], + name: "InvalidTwapPeriod", + type: "error", + }, + { + inputs: [], + name: "OnlyPool", + type: "error", + }, + { + inputs: [], + name: "OnlyRebalanceEntrypoint", + type: "error", + }, + { + inputs: [], + name: "OnlyVaultManager", + type: "error", + }, + { + inputs: [], + name: "OracleNotConnected", + type: "error", + }, + { + inputs: [], + name: "PriceManipulation", + type: "error", + }, + { + inputs: [], + name: "ZeroAddress", + type: "error", + }, + { + inputs: [], + name: "ZeroValue", + type: "error", + }, + { + inputs: [], + name: "tickOutOfRange", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + name: "Deposit", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "int24", + name: "lower", + type: "int24", + }, + { + indexed: false, + internalType: "int24", + name: "upper", + type: "int24", + }, + { + indexed: false, + internalType: "uint128", + name: "liquidity", + type: "uint128", + }, + ], + name: "FullRangeInitialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "hysteresis", + type: "uint256", + }, + ], + name: "Hysteresis", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "int24", + name: "lower", + type: "int24", + }, + { + indexed: false, + internalType: "int24", + name: "upper", + type: "int24", + }, + { + indexed: false, + internalType: "uint128", + name: "liquidity", + type: "uint128", + }, + { + indexed: false, + internalType: "uint160", + name: "targetSqrtPriceX96", + type: "uint160", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + name: "Rebalance", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "rebalanceEntrypoint", + type: "address", + }, + ], + name: "RebalanceEntrypoint", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint32", + name: "twapPeriod", + type: "uint32", + }, + { + indexed: false, + internalType: "uint32", + name: "auxTwapPeriod", + type: "uint32", + }, + ], + name: "TwapPeriods", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + name: "Withdraw", + type: "event", + }, + { + inputs: [], + name: "DEFAULT_HYSTERESIS", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MIN_SHARES", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PRECISION", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PRICE_CONVERGENCE_VAULT_MANAGER", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TICK_SPACING", + outputs: [ + { + internalType: "int24", + name: "", + type: "int24", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount0Owed", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1Owed", + type: "uint256", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "algebraMintCallback", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "int256", + name: "amount0Delta", + type: "int256", + }, + { + internalType: "int256", + name: "amount1Delta", + type: "int256", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "algebraSwapCallback", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "address", + name: "spender", + type: "address", + }, + ], + name: "allowance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "approve", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "auxTwapPeriod", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "collectFees", + outputs: [ + { + internalType: "uint256", + name: "fees0", + type: "uint256", + }, + { + internalType: "uint256", + name: "fees1", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address", + }, + { + internalType: "uint256", + name: "subtractedValue", + type: "uint256", + }, + ], + name: "decreaseAllowance", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + name: "deposit", + outputs: [ + { + internalType: "uint256", + name: "shares", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "factory", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "fullRangeLiquidity", + outputs: [ + { + internalType: "uint128", + name: "", + type: "uint128", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getMainPosition", + outputs: [ + { + internalType: "uint128", + name: "liquidity", + type: "uint128", + }, + { + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getShareholderAmounts", + outputs: [ + { + internalType: "uint256", + name: "total0", + type: "uint256", + }, + { + internalType: "uint256", + name: "total1", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalAmounts", + outputs: [ + { + internalType: "uint256", + name: "total0", + type: "uint256", + }, + { + internalType: "uint256", + name: "total1", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "hysteresis", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address", + }, + { + internalType: "uint256", + name: "addedValue", + type: "uint256", + }, + ], + name: "increaseAllowance", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "mainPosition", + outputs: [ + { + internalType: "int24", + name: "lower", + type: "int24", + }, + { + internalType: "int24", + name: "upper", + type: "int24", + }, + { + internalType: "uint128", + name: "liquidity", + type: "uint128", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pool", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint160", + name: "targetSqrtPriceX96", + type: "uint160", + }, + ], + name: "rebalance", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rebalanceEntrypoint", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_hysteresis", + type: "uint256", + }, + ], + name: "setHysteresis", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_rebalanceEntrypoint", + type: "address", + }, + ], + name: "setRebalanceEntrypoint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint32", + name: "_twapPeriod", + type: "uint32", + }, + { + internalType: "uint32", + name: "_auxTwapPeriod", + type: "uint32", + }, + ], + name: "setTwapPeriods", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token0", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token1", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "transfer", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "transferFrom", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "twapPeriod", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "vaultMath", + outputs: [ + { + internalType: "contract IVaultMath", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + name: "withdraw", + outputs: [ + { + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/config/abis/nav-hook/priceConvergenceVaultDepositGuard.ts b/config/abis/nav-hook/priceConvergenceVaultDepositGuard.ts new file mode 100644 index 00000000..16e326cb --- /dev/null +++ b/config/abis/nav-hook/priceConvergenceVaultDepositGuard.ts @@ -0,0 +1,219 @@ +export const priceConvergenceVaultDepositGuardABI = [ + { + inputs: [ + { + internalType: "address", + name: "_vault", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InsufficientAmounts", + type: "error", + }, + { + inputs: [], + name: "InsufficientShares", + type: "error", + }, + { + inputs: [], + name: "InvalidRecipient", + type: "error", + }, + { + inputs: [], + name: "ZeroAddress", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "shares", + type: "uint256", + }, + ], + name: "DepositForwarded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + name: "WithdrawForwarded", + type: "event", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + { + internalType: "uint256", + name: "minimumShares", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + name: "deposit", + outputs: [ + { + internalType: "uint256", + name: "shares", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "token0", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token1", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "vault", + outputs: [ + { + internalType: "contract IPriceConvergenceVault", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "shares", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "minAmount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "minAmount1", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [ + { + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/config/app-modules.ts b/config/app-modules.ts index f4af23ce..b54208a1 100644 --- a/config/app-modules.ts +++ b/config/app-modules.ts @@ -5,6 +5,7 @@ export enum AppFeatureModule { Farming = "FarmingModule", LimitOrders = "LimitOrdersModule", ALM = "ALMModule", + NAVHook = "NAVHookModule", VE_33 = "Ve33Module", BoostedPools = "BoostedPoolsModule", Prediction = "PredictionModule", @@ -18,6 +19,7 @@ export const enabledModules: Record = { [AppFeatureModule.Farming]: true, [AppFeatureModule.LimitOrders]: true, [AppFeatureModule.ALM]: true, + [AppFeatureModule.NAVHook]: true, [AppFeatureModule.VE_33]: true, [AppFeatureModule.BoostedPools]: true, [AppFeatureModule.Prediction]: true, diff --git a/config/contract-addresses.ts b/config/contract-addresses.ts index edb5eafd..f2c4feaf 100644 --- a/config/contract-addresses.ts +++ b/config/contract-addresses.ts @@ -17,6 +17,7 @@ export const NONFUNGIBLE_POSITION_MANAGER: Record = { export const SECURITY_REGISTRY: Record = { [ChainId.BaseSepolia]: "0x6aa9481De990bC12F906C5e8DE70D8556ac5ba2e", }; + /* Farming */ export const ALGEBRA_ETERNAL_FARMING: Record = { [ChainId.BaseSepolia]: "0xB50E639E23C954546C75d9C15363FC0375E5E95E", @@ -59,3 +60,21 @@ export const REBASE_REWARD: Record = { export const BINARY_LMSR_MARKET_MANAGER: Record = { [ChainId.BaseSepolia]: "0xf04604de76eb31004F2331bd0701b6eBF8ffdCB1", }; + +/* NAV Hook */ +export const PRICE_CONVERGENCE_VAULT_BY_POOL: Record> = { + [ChainId.BaseSepolia]: { + // pool address -> vault address + // "0x25827078a91d0875376a8a9f1bcfb35827ce0d4f": "0x1A6EB0846DEaBf20eF5e11F07205D17518Ead9B1", + "0xd05e0dc6176f558439184795f6f865719b562720": "0xD936600f96593eA0212e6A13585077C225939b10", + "0x4A5856Ea3803aa06FF9d0ba63E18914C05773c94": "0xD0D27ED3b59D0E07A4889D765468a76660dd4dAA", + }, +}; +export const PRICE_CONVERGENCE_VAULT_DEPOSIT_GUARD_BY_POOL: Record> = { + [ChainId.BaseSepolia]: { + // pool address -> vault deposit guard address + // "0x25827078a91d0875376a8a9f1bcfb35827ce0d4f": "0xb9aA6adde7319c68D90879855D2bE28D3e235A2E", + "0xd05e0dc6176f558439184795f6f865719b562720": "0x2F3737B5b16E6fFEc8f2C805b3283159E7f5e6E3", + "0x4A5856Ea3803aa06FF9d0ba63E18914C05773c94": "0xf1f865Ee407Bfe9413e60Ee3a0daa4AAB76B83eC", + }, +}; diff --git a/config/custom-pool-deployer.ts b/config/custom-pool-deployer.ts index 98d0ff9b..458a2e29 100644 --- a/config/custom-pool-deployer.ts +++ b/config/custom-pool-deployer.ts @@ -1,7 +1,7 @@ import { ADDRESS_ZERO, ChainId } from "@cryptoalgebra/integral-sdk"; import { Address } from "viem"; -export type PoolDeployerType = "BASE_DYNAMIC" | "BASE_03" | "BASE_1"; +export type PoolDeployerType = "BASE_DYNAMIC" | "BASE_03" | "BASE_1" | "NAV_HOOK"; export const CUSTOM_POOL_DEPLOYER_ADDRESSES: Record> = { BASE_DYNAMIC: { @@ -13,12 +13,16 @@ export const CUSTOM_POOL_DEPLOYER_ADDRESSES: Record = { BASE_DYNAMIC: "Dynamic", BASE_03: "0.3%", BASE_1: "1%", + NAV_HOOK: "NAV Hook", } as const; export const customPoolDeployerTitleByAddress: Record = Object.fromEntries( diff --git a/config/wagmi.ts b/config/wagmi.ts index 92dc2a83..e3dc1097 100644 --- a/config/wagmi.ts +++ b/config/wagmi.ts @@ -19,6 +19,8 @@ import { votingEscrowABI, securityRegistryABI, binaryLMSRMarketManagerABI, + priceConvergenceVaultABI, + priceConvergenceVaultDepositGuardABI, } from "./abis"; import { ALGEBRA_ETERNAL_FARMING, @@ -90,6 +92,8 @@ const rawContracts = [ { name: "VotingEscrow", abi: votingEscrowABI }, { name: "SecurityRegistry", abi: securityRegistryABI }, { name: "BinaryLMSRMarketManager", abi: binaryLMSRMarketManagerABI }, + { name: "PriceConvergenceVault", abi: priceConvergenceVaultABI }, + { name: "PriceConvergenceVaultDepositGuard", abi: priceConvergenceVaultDepositGuardABI }, ]; const contractAddresses = { diff --git a/package.json b/package.json index 0a2b7e82..9e8e52fb 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "dependencies": { "@apollo/client": "^3.8.4", "@cryptoalgebra/alm-sdk": "^1.0.16", - "@cryptoalgebra/integral-omega-router-sdk": "1.0.1", - "@cryptoalgebra/integral-sdk": "1.1.2", + "@cryptoalgebra/integral-omega-router-sdk": "1.0.2", + "@cryptoalgebra/integral-sdk": "1.1.3", "@cryptoalgebra/router-custom-pools-and-sliding-fee": "npm:@cryptoalgebra/router-custom-pools@2.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", diff --git a/src/assets/tokens/m4626.png b/src/assets/tokens/m4626.png new file mode 100644 index 00000000..f442e11e Binary files /dev/null and b/src/assets/tokens/m4626.png differ diff --git a/src/assets/tokens/wtsgov.png b/src/assets/tokens/wtsgov.png new file mode 100644 index 00000000..7f0d2200 Binary files /dev/null and b/src/assets/tokens/wtsgov.png differ diff --git a/src/components/common/CurrencyLogo/index.tsx b/src/components/common/CurrencyLogo/index.tsx index 3a9cba8a..8c1c1dcf 100644 --- a/src/components/common/CurrencyLogo/index.tsx +++ b/src/components/common/CurrencyLogo/index.tsx @@ -4,6 +4,8 @@ import BTCLogo from "@/assets/tokens/wbtc.svg"; import USDCLogo from "@/assets/tokens/usdc.svg"; import USDCBlackLogo from "@/assets/tokens/usdc-black.png"; import USDTBlackLogo from "@/assets/tokens/usdt-black.png"; +import m4626Logo from "@/assets/tokens/m4626.png"; +import WTSGOVLogo from "@/assets/tokens/wtsgov.png"; import EtherLogo from "@/assets/tokens/ether.svg"; import ProjectXLogo from "@/assets/tokens/project-x.jpg"; import TOKENLogo from "@/assets/algebra-logo.svg"; @@ -70,6 +72,26 @@ export const specialTokens: { [key: Address]: { symbol: string; logo: string } } symbol: "AVETH", logo: EtherLogo, }, + ["0x4db3fba9958f7ee9715875e485ceae299714029c"]: { + symbol: "m4626", + logo: m4626Logo, + }, + ["0x0acae280cc7695e5bbbd6fb4b5b1b39c9594638d"]: { + symbol: "USDC", + logo: USDCLogo, + }, + ["0xdDC1FD535E7243f43465094f43Ee8a03A5189acd"]: { + symbol: "USDC", + logo: USDCLogo, + }, + ["0x980447AbF3B26B41c7f1777C2A8dF41cCd62ace6"]: { + symbol: "WTSGOV", + logo: WTSGOVLogo, + }, +}; + +const getSpecialToken = (address: Address): { symbol: string; logo: string } | undefined => { + return Object.entries(specialTokens).find(([key]) => key.toLowerCase() === address.toLowerCase())?.[1]; }; const CurrencyLogo = ({ currency, size, className, style = {} }: CurrencyLogoProps) => { @@ -85,17 +107,9 @@ const CurrencyLogo = ({ currency, size, className, style = {} }: CurrencyLogoPro const classString = cn(`w-[${size}px] h-[${size}px] min-w-[${size}px] min-h-[${size}px] bg-card-dark rounded-full`, className); - if (address in specialTokens) { - return ( - {specialTokens[address].symbol} - ); + const specialToken = getSpecialToken(address); + if (specialToken) { + return {specialToken.symbol}; } if (currency.isNative) { diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index 2c6ff215..8ae146e6 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -30,7 +30,7 @@ export const Algebra = () => (
- +
diff --git a/src/components/common/Table/poolsColumns.tsx b/src/components/common/Table/poolsColumns.tsx index 1fdd7695..7d18b853 100644 --- a/src/components/common/Table/poolsColumns.tsx +++ b/src/components/common/Table/poolsColumns.tsx @@ -19,7 +19,10 @@ import BoostedPoolsModule from "@/modules/BoostedPoolsModule"; const { BoostedTag, BoostedAPR } = BoostedPoolsModule.components; const { useBoostedTokenAPR } = BoostedPoolsModule.hooks; -const PoolPair = ({ pair, id, hasALM, hasActiveFarming }: FormattedPool) => { +import NAVHookModule from "@/modules/NAVHookModule"; +const { NAVHookTag } = NAVHookModule.components; + +const PoolPair = ({ pair, id, hasALM, hasActiveFarming, hasNAVHook }: FormattedPool) => { const token0 = pair.token0.id as Address; const token1 = pair.token1.id as Address; @@ -42,6 +45,7 @@ const PoolPair = ({ pair, id, hasALM, hasActiveFarming }: FormattedPool) => {
{hasActiveFarming && } {hasALM && } + {enabledModules.NAVHookModule && hasNAVHook && }
{/*
{`${fee}%`}
*/} diff --git a/src/components/common/Table/poolsTable.tsx b/src/components/common/Table/poolsTable.tsx index 949ac43d..ee82b3a3 100644 --- a/src/components/common/Table/poolsTable.tsx +++ b/src/components/common/Table/poolsTable.tsx @@ -2,25 +2,27 @@ import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ColumnDef, - OnChangeFn, - SortingState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + SortingState, useReactTable, } from "@tanstack/react-table"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { LoadingState } from "./loadingState"; import { Input } from "@/components/ui/input"; -import { Search, User, X, Zap } from "lucide-react"; +import { Filter, Search, User, X } from "lucide-react"; import { enabledModules } from "config/app-modules"; import { useNavigate } from "react-router-dom"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; type ActiveFilters = { hasActiveFarming?: boolean; hasALM?: boolean; + hasNAVHook?: boolean; isMyPool?: boolean; isBoosted?: boolean; }; @@ -35,7 +37,7 @@ interface PoolsTableProps { loading?: boolean; } -const SHOWCASE_SORT_ID = "__showcasePriority"; +// const SHOWCASE_SORT_ID = "__showcasePriority"; const PoolsTable = ({ columns, @@ -46,49 +48,46 @@ const PoolsTable = ({ showPagination = true, loading, }: PoolsTableProps) => { - const [sorting, setSorting] = useState(() => { - const defaultSort = defaultSortingID ? [{ id: defaultSortingID, desc: true }] : []; - return [{ id: SHOWCASE_SORT_ID, desc: true }, ...defaultSort]; - }); + const [sorting, setSorting] = useState(defaultSortingID ? [{ id: defaultSortingID, desc: true }] : []); const [columnFilters, setColumnFilters] = useState([]); const [activeFilters, setActiveFilters] = useState({}); - const columnsWithShowcaseSort = useMemo( - () => [ - ...columns, - { - id: SHOWCASE_SORT_ID, - accessorFn: (row: any) => (row?.isShowcase ? 1 : 0), - header: () => null, - cell: () => null, - } as ColumnDef, - ], - [columns] - ); + // const columnsWithShowcaseSort = useMemo( + // () => [ + // ...columns, + // { + // id: SHOWCASE_SORT_ID, + // accessorFn: (row: any) => (row?.isShowcase ? 1 : 0), + // header: () => null, + // cell: () => null, + // } as ColumnDef, + // ], + // [columns] + // ); - const handleSortingChange: OnChangeFn = (updater) => { - setSorting((prev) => { - const next = typeof updater === "function" ? updater(prev) : updater; - const withoutShowcase = next.filter((s) => s.id !== SHOWCASE_SORT_ID); - return [{ id: SHOWCASE_SORT_ID, desc: true }, ...withoutShowcase]; - }); - }; + // const handleSortingChange: OnChangeFn = (updater) => { + // setSorting((prev) => { + // const next = typeof updater === "function" ? updater(prev) : updater; + // const withoutShowcase = next.filter((s) => s.id !== SHOWCASE_SORT_ID); + // return [{ id: SHOWCASE_SORT_ID, desc: true }, ...withoutShowcase]; + // }); + // }; const table = useReactTable({ data, - columns: columnsWithShowcaseSort, + columns, initialState: { - columnVisibility: { - [SHOWCASE_SORT_ID]: false, - }, + // columnVisibility: { + // [SHOWCASE_SORT_ID]: false, + // }, }, state: { columnFilters, sorting, globalFilter: activeFilters, }, + onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, - onSortingChange: handleSortingChange, onGlobalFilterChange: setActiveFilters, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), @@ -99,6 +98,7 @@ const PoolsTable = ({ const f = filterValue as ActiveFilters; if (f.hasActiveFarming && !row.original.hasActiveFarming) return false; if (f.hasALM && !row.original.hasALM) return false; + if (f.hasNAVHook && !row.original.hasNAVHook) return false; if (f.isMyPool && !row.original.isMyPool) return false; if (f.isBoosted && !row.original.isBoosted) return false; return true; @@ -124,6 +124,13 @@ const PoolsTable = ({ return Boolean(activeFilters[filterId]); }; + const activePoolTypeFilters = [ + activeFilters.hasActiveFarming, + activeFilters.hasALM, + activeFilters.hasNAVHook, + activeFilters.isBoosted, + ].filter(Boolean).length; + if (loading) return ; return ( @@ -135,53 +142,86 @@ const PoolsTable = ({ placeholder="Search pool" value={(table.getColumn(searchID)?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn(searchID)?.setFilterValue(event.target.value)} - className="border border-border border-opacity-60 pl-12 h-10 max-w-80 md:w-64 lg:w-80 focus:border-opacity-100 focus:bg-primary-800 rounded-lg" + className="border-none pl-12 h-10 max-w-80 md:w-64 lg:w-80 focus:border-opacity-100 focus:bg-primary-800 rounded-full bg-card-light" />
- {enabledModules.FarmingModule && ( - - )} - {enabledModules.ALMModule && ( - - )} - {enabledModules.BoostedPoolsModule && ( - - )} + + + + + +
+ {enabledModules.FarmingModule && ( + + )} + {enabledModules.ALMModule && ( + + )} + {enabledModules.NAVHookModule && ( + + )} + {enabledModules.BoostedPoolsModule && ( + + )} +
+
+
- - )} +
+
+
+ + + {displayCurrency ? displayCurrency.symbol : "Select"} +
-
- handleInput(v)} - className={`text-right border-none text-xl font-bold w-9/12 p-0 ring-0!`} - placeholder={"0.0"} - maxDecimals={displayCurrency?.decimals} - /> - {valueUsd && ( -
-
${formatAmount(valueUsd, 2)}
-
- )} -
+ handleInput(v)} + className="w-full border-none bg-transparent p-0 text-right text-2xl font-semibold text-text-100 placeholder:text-text-300 focus:ring-0 focus-visible:ring-0" + placeholder="0.00" + maxDecimals={displayCurrency?.decimals} + /> +
+ +
+ {displayCurrency ? Balance: {balanceString} : } + {valueUsd !== undefined && valueUsd !== null && ${formatAmount(valueUsd, 2)}}
- + {showBoostedToggle && ( + + )}
); }; diff --git a/src/components/create-position/TokenRatio/index.tsx b/src/components/create-position/TokenRatio/index.tsx index 990ba444..396b079a 100644 --- a/src/components/create-position/TokenRatio/index.tsx +++ b/src/components/create-position/TokenRatio/index.tsx @@ -13,38 +13,42 @@ const TokenRatio = ({ mintInfo }: TokenRatioProps) => { } = mintInfo; const [token0Ratio, token1Ratio] = useMemo(() => { - const tickUpperAtLimit = - mintInfo.upperPrice && nearestUsableTick(TickMath.MAX_TICK, mintInfo.tickSpacing) === priceToClosestTick(mintInfo.upperPrice); - const currentPrice = mintInfo.price?.toSignificant(5); + try { + const tickUpperAtLimit = + mintInfo.upperPrice && + nearestUsableTick(TickMath.MAX_TICK, mintInfo.tickSpacing) === priceToClosestTick(mintInfo.upperPrice); + const currentPrice = mintInfo.price?.toSignificant(5); - const left = mintInfo.lowerPrice?.toSignificant(5); - const right = mintInfo.upperPrice?.toSignificant(5); + const left = mintInfo.lowerPrice?.toSignificant(5); + const right = mintInfo.upperPrice?.toSignificant(5); - if (tickUpperAtLimit) return ["50", "50"]; + if (tickUpperAtLimit) return ["50", "50"]; - if (!currentPrice) return ["0", "0"]; + if (!currentPrice) return ["0", "0"]; - if (!left && !right) return ["0", "0"]; + if (!left && !right) return ["0", "0"]; - if (!left && right) return ["0", "100"]; + if (!left && right) return ["0", "100"]; - if (!right && left) return ["100", "0"]; + if (!right && left) return ["100", "0"]; - if (left && right && currentPrice) { - const leftRange = +currentPrice - +left; - const rightRange = +right - +currentPrice; + if (left && right && currentPrice) { + const leftRange = +currentPrice - +left; + const rightRange = +right - +currentPrice; - const totalSum = +leftRange + +rightRange; + const totalSum = +leftRange + +rightRange; - const leftRate = (+leftRange * 100) / totalSum; - const rightRate = (+rightRange * 100) / totalSum; + const leftRate = (+leftRange * 100) / totalSum; + const rightRate = (+rightRange * 100) / totalSum; - if (!mintInfo.invertPrice) { - return [String(rightRate >= 100 ? 100 : rightRate), String(leftRate >= 100 ? 100 : leftRate)]; + if (!mintInfo.invertPrice) { + return [String(rightRate >= 100 ? 100 : rightRate), String(leftRate >= 100 ? 100 : leftRate)]; + } + return [String(leftRate >= 100 ? 100 : leftRate), String(rightRate >= 100 ? 100 : rightRate)]; } - return [String(leftRate >= 100 ? 100 : leftRate), String(rightRate >= 100 ? 100 : rightRate)]; + } catch (error) { + console.error(error); } - return [null, null]; }, [mintInfo.invertPrice, mintInfo.lowerPrice, mintInfo.price, mintInfo.tickSpacing, mintInfo.upperPrice]); diff --git a/src/components/modals/IncreaseLiquidityModal/index.tsx b/src/components/modals/IncreaseLiquidityModal/index.tsx index 0006928d..a9b2c0a4 100644 --- a/src/components/modals/IncreaseLiquidityModal/index.tsx +++ b/src/components/modals/IncreaseLiquidityModal/index.tsx @@ -26,7 +26,7 @@ export function IncreaseLiquidityModal({ tokenId, currencyA, currencyB, mintInfo Add Liquidity - + Enter Amounts diff --git a/src/components/pool/MyPositionsToolbar/index.tsx b/src/components/pool/MyPositionsToolbar/index.tsx index cbafd694..be34497d 100644 --- a/src/components/pool/MyPositionsToolbar/index.tsx +++ b/src/components/pool/MyPositionsToolbar/index.tsx @@ -1,55 +1,60 @@ import { FormattedPosition } from "@/types/formatted-position"; -import { formatPlural } from "@/utils/common/formatPlural"; import { formatAmount } from "@/utils/common/formatAmount"; -import { Currency } from "@cryptoalgebra/integral-sdk"; -import CurrencyLogo from "@/components/common/CurrencyLogo"; import FilterPopover from "../FilterPopover"; -import { Settings2 } from "lucide-react"; -import SecurityStatusTag from "@/components/pools/SecurityStatusTag"; +import { ListFilter } from "lucide-react"; interface MyPositionsToolbar { positionsData: FormattedPosition[]; - currencyA: Currency | undefined | null; - currencyB: Currency | undefined | null; - poolStatus: number | undefined | null; } -const MyPositionsToolbar = ({ positionsData, currencyA, currencyB, poolStatus }: MyPositionsToolbar) => { +const MyPositionsToolbar = ({ positionsData }: MyPositionsToolbar) => { const [myLiquidityUSD, myFeesUSD] = positionsData ? positionsData.reduce((acc, { liquidityUSD, feesUSD }) => [acc[0] + liquidityUSD, acc[1] + Number(feesUSD)], [0, 0]) : []; + const activePositions = positionsData.filter(({ isALM, isClosed, onFarming }) => !isALM && !isClosed && !onFarming).length; + const farmingPositions = positionsData.filter(({ isClosed, onFarming }) => !isClosed && onFarming).length; + const almPositions = positionsData.filter(({ isALM, isClosed, onFarming }) => isALM && !isClosed && !onFarming).length; + + const summaryItems = [ + { + label: "Positions", + value: (activePositions + farmingPositions + almPositions).toString(), + }, + { + label: "Liquidity", + value: `$${formatAmount(myLiquidityUSD || 0, 2)}`, + }, + { + label: "Fees", + value: `$${formatAmount(myFeesUSD || 0, 2)}`, + }, + ]; + + if (!positionsData.length) { + return null; + } + return ( -
-
-
- - -

- {currencyA?.symbol} / {currencyB?.symbol} -

- - - -
- - - -
-
- {myLiquidityUSD ? ( -
-
{`${positionsData?.length} ${formatPlural( - positionsData.length, - "position", - "positions" - )}`}
-
-
{`$${formatAmount(myLiquidityUSD || 0, 2)} TVL`}
-
-
{`$${formatAmount(myFeesUSD || 0, 2)} Fees`}
+
+
+

My Positions

+ + + + +
+ +
+ {summaryItems.map(({ label, value }) => ( +
+ {label} + {value}
- ) : null} + ))}
); diff --git a/src/components/pool/PoolHeader/index.tsx b/src/components/pool/PoolHeader/index.tsx index 950bf9bf..dcffcc49 100644 --- a/src/components/pool/PoolHeader/index.tsx +++ b/src/components/pool/PoolHeader/index.tsx @@ -1,26 +1,117 @@ -import PageTitle from "@/components/common/PageTitle"; +import CurrencyLogo from "@/components/common/CurrencyLogo"; +import SecurityStatusTag from "@/components/pools/SecurityStatusTag"; import { Button } from "@/components/ui/button"; -import { Plus } from "lucide-react"; +import { useBlockExplorerURL } from "@/hooks/common/useBlockExplorer"; +import { PoolStats } from "@/hooks/pools/usePoolStats"; +import NAVHookModule from "@/modules/NAVHookModule"; +import { formatAmount } from "@/utils/common/formatAmount"; +import { Currency } from "@cryptoalgebra/integral-sdk"; +import { enabledModules } from "config"; +import { ChevronLeft, ExternalLink, Plus } from "lucide-react"; import { Link } from "react-router-dom"; +import { Address } from "viem"; + +const { NAVHookHeaderInfo } = NAVHookModule.components; +const { useNAVHookPool } = NAVHookModule.hooks; + +interface PoolHeaderProps { + currencyA: Currency | undefined | null; + currencyB: Currency | undefined | null; + poolId: Address; + poolStatus: number | undefined | null; + stats: PoolStats; + showCreatePosition?: boolean; +} + +const PoolHeader = ({ currencyA, currencyB, poolId, poolStatus, stats, showCreatePosition = true }: PoolHeaderProps) => { + const blockExplorerURL = useBlockExplorerURL(); + const { isNAVHookPool } = useNAVHookPool(poolId); + const showNAVHookInfo = enabledModules.NAVHookModule && isNAVHookPool; + + const headerStats = [ + { + label: "TVL", + value: `$${formatAmount(stats.tvlUSD, 2)}`, + }, + { + label: "Volume 24h", + value: `$${formatAmount(stats.volume24USD, 2)}`, + }, + { + label: "Fees 24h", + value: `$${formatAmount(stats.fees24USD, 2)}`, + }, + { + label: "APR", + value: `${formatAmount(stats.avgApr, 2)}%`, + }, + ]; -const PoolHeader = ({ showCreatePosition = true }: { showCreatePosition?: boolean }) => { return ( -
-
- +
+ + + Pools + + +
+
+
+ + +
+
+
+

+ {currencyA?.symbol || "..."} - {currencyB?.symbol || "..."} +

+ +
+ + {showNAVHookInfo ? ( + + ) : ( +

Fee: {formatAmount(stats.fee, 4)}%

+ )} +
+
+ +
+ + + + + {showCreatePosition && ( + + )} +
- { showCreatePosition && - - } -
+
+ {headerStats.map(({ label, value }) => ( +
+ {label} + {stats.isLoading ? "..." : value} +
+ ))} +
+ ); }; diff --git a/src/components/position/PositionCard/index.tsx b/src/components/position/PositionCard/index.tsx index ace54bdd..1ff5c373 100644 --- a/src/components/position/PositionCard/index.tsx +++ b/src/components/position/PositionCard/index.tsx @@ -54,10 +54,10 @@ const PositionCard = ({ pool, selectedPosition, farming, closedFarmings, poolSta if (!selectedPosition) return; return ( -
+
-

{`Position #${selectedPosition?.id}`}

+

{`Position #${selectedPosition?.id}`}

diff --git a/src/components/swap/SwapButton/index.tsx b/src/components/swap/SwapButton/index.tsx index dac94746..84f3b397 100644 --- a/src/components/swap/SwapButton/index.tsx +++ b/src/components/swap/SwapButton/index.tsx @@ -8,7 +8,7 @@ import { warningSeverity } from "@/utils/swap/prices"; import { useCallback, useMemo } from "react"; import { useAccount, useChainId } from "wagmi"; import { SmartRouter } from "@cryptoalgebra/router-custom-pools-and-sliding-fee"; -import { tryParseAmount, BoostedRouteStepType } from "@cryptoalgebra/integral-sdk"; +import { tryParseAmount, BoostedRouteStepType, Currency } from "@cryptoalgebra/integral-sdk"; import { useAppKit, useAppKitNetwork } from "@reown/appkit/react"; import { useApproveCallbackFromTrade } from "@/hooks/common/useApprove"; import { ApprovalState } from "@/types/approve-state"; @@ -65,15 +65,14 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { return null; }, [trade, isSmartTrade]); + const tradeInputCurrency = trade?.inputAmount?.currency as Currency | undefined; + const tradeOutputCurrency = trade?.outputAmount?.currency as Currency | undefined; + const parsedAmountA = - independentField === SwapField.INPUT - ? parsedAmount - : tryParseAmount(trade?.inputAmount?.toSignificant(), trade?.inputAmount?.currency); + independentField === SwapField.INPUT ? parsedAmount : tryParseAmount(trade?.inputAmount?.toSignificant(), tradeInputCurrency); const parsedAmountB = - independentField === SwapField.OUTPUT - ? parsedAmount - : tryParseAmount(trade?.outputAmount?.toSignificant(), trade?.outputAmount?.currency); + independentField === SwapField.OUTPUT ? parsedAmount : tryParseAmount(trade?.outputAmount?.toSignificant(), tradeOutputCurrency); const parsedAmounts = useMemo( () => ({ @@ -160,8 +159,8 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { const showWrap = wrapType !== WrapType.NOT_APPLICABLE; const { callback: smartSwapCallback, isLoading: smartSwapLoading } = useSmartRouterCallback( - trade?.inputAmount?.currency, - trade?.outputAmount?.currency, + tradeInputCurrency, + tradeOutputCurrency, trade?.inputAmount?.toFixed(), smartTradeCallOptions.calldata, smartTradeCallOptions.value, @@ -170,7 +169,7 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { // Use OmegaRouter callback for boosted routes and Permit2-signed swaps const { callback: omegaSwapCallback, isLoading: omegaSwapLoading, error: omegaSwapError } = useOmegaSwapCallback( - shouldUseOmegaRouter && !isSmartTrade ? trade : null, + shouldUseOmegaRouter && !isSmartTrade ? (!needsApprovalOrPermit ? trade : null) : null, allowedSlippage, permitSignature, onTransactionSuccess, @@ -181,6 +180,7 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { !isSmartTrade && !shouldUseOmegaRouter ? trade : null, allowedSlippage, onTransactionSuccess, + approvalState !== ApprovalState.APPROVED, ); const isSwapLoading = swapLoading || smartSwapLoading || omegaSwapLoading; @@ -198,7 +198,6 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { permit2Allowance.removePermitSign(); } } else { - console.log("Executing regular swap callback", { swapCallback }); await swapCallback?.(); } } catch (error) { @@ -209,6 +208,7 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { const isValid = !swapInputError && !activeSwapError; const hasLargePriceDifference = priceImpactSeverity > 2; + const approvalTokenSymbol = trade?.inputAmount.currency.symbol ?? "token"; const largePriceDifferencePercent = useMemo(() => { if (!priceImpact) return "0.00"; @@ -220,7 +220,9 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { }, [priceImpact]); // Check if we need standard ERC20 approval (for native/smart router) - const needsClassicApproval = !shouldUseOmegaRouter && approvalState === ApprovalState.NOT_APPROVED; + const isResetApprovalRequired = approvalState === ApprovalState.RESET_REQUIRED; + const needsClassicApproval = + !shouldUseOmegaRouter && (approvalState === ApprovalState.NOT_APPROVED || approvalState === ApprovalState.RESET_REQUIRED); const isApproving = approvalState === ApprovalState.PENDING; const isWrongChain = !userChainId || appChainId !== userChainId; @@ -263,7 +265,13 @@ const SwapButton = ({ derivedSwap }: { derivedSwap: IDerivedSwapInfo }) => { if (needsClassicApproval || isApproving) { return ( ); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 5197db31..a70e76a1 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -26,7 +26,7 @@ const buttonVariants = cva( size: { default: "h-10 px-4 py-2 ", sm: "h-9 rounded-lg px-3", - md: "h-6 rounded-lg p-4 py-6 rounded-full", + md: "h-12 rounded-lg p-4 rounded-full", lg: "text-md p-5 rounded-full", icon: "h-10 w-10 rounded-xl", }, diff --git a/src/graphql/queries/pools.ts b/src/graphql/queries/pools.ts index 184992e0..4e654148 100644 --- a/src/graphql/queries/pools.ts +++ b/src/graphql/queries/pools.ts @@ -24,6 +24,8 @@ export const POOL_FRAGMENT = gql` feesToken0 feesToken1 deployer + txCount + createdAtTimestamp } `; export const TICK_FRAGMENT = gql` @@ -149,4 +151,4 @@ export const POOLS_HOUR_DATAS = gql` ...PoolHourDataFields } } -`; \ No newline at end of file +`; diff --git a/src/hooks/common/useApprove.ts b/src/hooks/common/useApprove.ts index d4b1fd33..c598d7a1 100644 --- a/src/hooks/common/useApprove.ts +++ b/src/hooks/common/useApprove.ts @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { Currency, CurrencyAmount, Percent, Trade, TradeType } from "@cryptoalgebra/integral-sdk"; import { SmartRouter, SmartRouterTrade } from "@cryptoalgebra/router-custom-pools-and-sliding-fee"; -import { DEFAULT_CHAIN_ID, SWAP_ROUTER } from "config"; +import { DEFAULT_CHAIN_ID, SWAP_ROUTER, TOKENS } from "config"; import { ApprovalState, ApprovalStateType } from "@/types/approve-state"; import { useNeedAllowance } from "./useNeedAllowance"; @@ -12,32 +12,52 @@ import { Address, erc20Abi } from "viem"; import { useWriteContract } from "wagmi"; import { formatAmount } from "@/utils"; +const TOKENS_REQUIRING_APPROVAL_RESET = new Set([TOKENS[DEFAULT_CHAIN_ID].USDT.address.toLowerCase()]); + export function useApprove(amountToApprove: CurrencyAmount | undefined, spender: Address) { const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined; const [shouldPolling, setShouldPolling] = useState(false); + const [pendingApprovalTitle, setPendingApprovalTitle] = useState(); + const amountToApproveValue = amountToApprove?.quotient.toString(); + + const { needAllowance, allowance, refetchAllowance } = useNeedAllowance(token, amountToApprove, spender, true); - const { needAllowance, refetchAllowance } = useNeedAllowance(token, amountToApprove, spender, true); + const targetApprovalAmount = amountToApprove ? BigInt(amountToApprove.quotient.toString()) : undefined; + + const shouldResetApprovalBeforeIncrease = Boolean( + token && + typeof allowance === "bigint" && + allowance > 0n && + targetApprovalAmount && + targetApprovalAmount > allowance && + TOKENS_REQUIRING_APPROVAL_RESET.has(token.address.toLowerCase()), + ); const approvalState: ApprovalStateType = useMemo(() => { if (!amountToApprove || !spender) return ApprovalState.UNKNOWN; if (amountToApprove.currency.isNative) return ApprovalState.APPROVED; + if (shouldResetApprovalBeforeIncrease) return ApprovalState.RESET_REQUIRED; return needAllowance ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED; - }, [amountToApprove?.quotient.toString(), needAllowance, spender]); + }, [amountToApprove, needAllowance, shouldResetApprovalBeforeIncrease, spender]); const config = amountToApprove ? { address: amountToApprove.currency.wrapped.address as Address, abi: erc20Abi, functionName: "approve" as const, - args: [spender, BigInt(amountToApprove.quotient.toString())] as [Address, bigint], + args: [spender, targetApprovalAmount as bigint] as [Address, bigint], } : undefined; const { data: approvalData, writeContract: approve, isPending } = useWriteContract(); + const currentApprovalTitle = shouldResetApprovalBeforeIncrease + ? `Reset ${amountToApprove?.currency.symbol} approval` + : `Approve ${formatAmount(amountToApprove?.toSignificant(24) as string)} ${amountToApprove?.currency.symbol}`; + const { isLoading, isSuccess } = useTransactionAwait(approvalData, { - title: `Approve ${formatAmount(amountToApprove?.toSignificant() as string)} ${amountToApprove?.currency.symbol}`, + title: pendingApprovalTitle ?? currentApprovalTitle, tokenA: token?.address as Address, type: TransactionType.SWAP, callback: refetchAllowance, @@ -45,7 +65,7 @@ export function useApprove(amountToApprove: CurrencyAmount | undefined useEffect(() => { setShouldPolling(true); - }, [amountToApprove?.quotient.toString()]); + }, [amountToApproveValue]); useEffect(() => { if (!needAllowance && shouldPolling) { @@ -56,6 +76,16 @@ export function useApprove(amountToApprove: CurrencyAmount | undefined const approvalCallback = () => { if (config) { setShouldPolling(true); + setPendingApprovalTitle(currentApprovalTitle); + + if (shouldResetApprovalBeforeIncrease) { + approve({ + ...config, + args: [spender, 0n], + }); + return; + } + approve(config); } }; @@ -87,7 +117,10 @@ export function useApproveCallbackFromTrade( [trade, allowedSlippage, isSmartTrade], ); - return useApprove(amountToApprove, SWAP_ROUTER[amountToApprove?.currency.chainId || DEFAULT_CHAIN_ID]); + return useApprove( + amountToApprove as CurrencyAmount | undefined, + SWAP_ROUTER[amountToApprove?.currency.chainId || DEFAULT_CHAIN_ID], + ); } export function useRevokeApprove(token: Currency | undefined, spender: Address) { diff --git a/src/hooks/common/useNeedAllowance.ts b/src/hooks/common/useNeedAllowance.ts index 4dee42f5..a161dde1 100644 --- a/src/hooks/common/useNeedAllowance.ts +++ b/src/hooks/common/useNeedAllowance.ts @@ -24,5 +24,5 @@ export function useNeedAllowance( !currency?.isNative && typeof allowance === "bigint" && amount && amount.greaterThan(allowance.toString()) ); - return { needAllowance, refetchAllowance: refetch }; + return { needAllowance, allowance, refetchAllowance: refetch }; } diff --git a/src/hooks/common/useTransactionAwait.tsx b/src/hooks/common/useTransactionAwait.tsx index f334d4cb..2440c64d 100644 --- a/src/hooks/common/useTransactionAwait.tsx +++ b/src/hooks/common/useTransactionAwait.tsx @@ -3,7 +3,7 @@ import { useToast } from "@/components/ui/use-toast"; import { TransactionInfo, usePendingTransactionsStore } from "@/state/pendingTransactionsStore"; import { useAppKitNetwork } from "@reown/appkit/react"; import { ExternalLinkIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Address } from "viem"; import { useAccount, useWaitForTransactionReceipt } from "wagmi"; @@ -29,6 +29,7 @@ export const ViewTxOnExplorer = ({ hash }: { hash: Address | undefined }) => { export function useTransactionAwait(hash: Address | undefined, transactionInfo: TransactionInfo, redirectPath?: string) { const { toast } = useToast(); + const transactionInfoRef = useRef<{ hash?: Address; info: TransactionInfo }>({ info: transactionInfo }); const navigate = useNavigate(); @@ -42,8 +43,11 @@ export function useTransactionAwait(hash: Address | undefined, transactionInfo: hash, }); + const currentTransactionInfo = hash && transactionInfoRef.current.hash === hash ? transactionInfoRef.current.info : transactionInfo; + useEffect(() => { if (isLoading && hash && account) { + transactionInfoRef.current = { hash, info: transactionInfo }; toast({ title: transactionInfo.title, description: transactionInfo.description || "Transaction was sent", @@ -57,8 +61,8 @@ export function useTransactionAwait(hash: Address | undefined, transactionInfo: useEffect(() => { if (isError && hash) { toast({ - title: transactionInfo.title, - description: transactionInfo.description || "Transaction failed", + title: currentTransactionInfo.title, + description: currentTransactionInfo.description || "Transaction failed", action: , }); } @@ -67,12 +71,12 @@ export function useTransactionAwait(hash: Address | undefined, transactionInfo: useEffect(() => { if (isSuccess && hash) { toast({ - title: transactionInfo.title, - description: transactionInfo.description || "Transaction confirmed", + title: currentTransactionInfo.title, + description: currentTransactionInfo.description || "Transaction confirmed", action: , }); - if (transactionInfo.callback) { - transactionInfo.callback(); + if (currentTransactionInfo.callback) { + currentTransactionInfo.callback(); } if (redirectPath) { navigate(redirectPath); diff --git a/src/hooks/pools/useFormattedPools.ts b/src/hooks/pools/useFormattedPools.ts index a0af769e..4993b1c3 100644 --- a/src/hooks/pools/useFormattedPools.ts +++ b/src/hooks/pools/useFormattedPools.ts @@ -9,7 +9,8 @@ import { usePositions } from "../positions/usePositions"; import ALMModule from "@/modules/ALMModule"; import { Address } from "viem"; import { BOOSTED_TOKENS } from "config/tokens"; -import { DEFAULT_CHAIN_ID } from "config"; +import { DEFAULT_CHAIN_ID, PRICE_CONVERGENCE_VAULT_BY_POOL, PRICE_CONVERGENCE_VAULT_DEPOSIT_GUARD_BY_POOL } from "config"; +import { enabledModules } from "config/app-modules"; const { useAllUserALMAmounts, useAllALMVaults } = ALMModule.hooks; interface Pair { @@ -30,11 +31,18 @@ export interface FormattedPool { isMyPool: boolean; hasActiveFarming: boolean; hasALM: boolean; + hasNAVHook: boolean; deployer: string; isBoostedPool: boolean; isBoostedToken0: boolean; isBoostedToken1: boolean; - isShowcase: boolean; + // isShowcase: boolean; +} + +function hasPoolMapping(mapping: Record | undefined, poolId: string): boolean { + if (!mapping) return false; + + return Object.keys(mapping).some((address) => address.toLowerCase() === poolId.toLowerCase()); } export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPool[]; isLoading: boolean } { @@ -79,7 +87,7 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo } return true; }) - .map(({ id, token0, token1, overrideFee, totalValueLockedUSD, deployer, poolDayData }) => { + .map(({ id, token0, token1, fee: baseFee, overrideFee, totalValueLockedUSD, deployer, poolDayData }) => { const currentPool = poolDayData[0]; const lastDate = currentPool ? currentPool.date * 1000 : 0; const currentDate = new Date().getTime(); @@ -89,7 +97,7 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo const msIn24Hours = 24 * 60 * 60 * 1000; const openPositions = positions?.filter( - (position) => position.pool.toLowerCase() === id.toLowerCase() && position.liquidity > 0n + (position) => position.pool.toLowerCase() === id.toLowerCase() && position.liquidity > 0n, ); const activeFarming = activeFarmings?.eternalFarmings.find((farming) => farming.pool === id); @@ -102,13 +110,20 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo const avgApr = farmApr + poolAvgApr; - const isBoostedToken0 = Object.values(BOOSTED_TOKENS[chainId || DEFAULT_CHAIN_ID]).find( - (bt) => bt.address.toLowerCase() === token0.id.toLowerCase() + const activeChainId = chainId || DEFAULT_CHAIN_ID; + const isBoostedToken0 = Object.values(BOOSTED_TOKENS[activeChainId]).find( + (bt) => bt.address.toLowerCase() === token0.id.toLowerCase(), ); - const isBoostedToken1 = Object.values(BOOSTED_TOKENS[chainId || DEFAULT_CHAIN_ID]).find( - (bt) => bt.address.toLowerCase() === token1.id.toLowerCase() + const isBoostedToken1 = Object.values(BOOSTED_TOKENS[activeChainId]).find( + (bt) => bt.address.toLowerCase() === token1.id.toLowerCase(), ); const isBoosted = isBoostedToken0 || isBoostedToken1; + const hasNAVHook = + enabledModules.NAVHookModule && + hasPoolMapping(PRICE_CONVERGENCE_VAULT_BY_POOL[activeChainId], id) && + hasPoolMapping(PRICE_CONVERGENCE_VAULT_DEPOSIT_GUARD_BY_POOL[activeChainId], id); + + const fee = (Number(overrideFee) || Number(baseFee)) / 10_000; return { id: id as Address, @@ -116,7 +131,7 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo token0, token1, }, - fee: Number(overrideFee) / 10_000, + fee, tvlUSD: Number(totalValueLockedUSD), volume24USD: timeDifference <= msIn24Hours ? Number(currentPool.volumeUSD) : 0, fees24USD: timeDifference <= msIn24Hours ? Number(currentPool.feesUSD) : 0, @@ -126,12 +141,13 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo avgApr, isMyPool: Boolean(openPositions?.length || openAlmPositions?.length), hasALM: Boolean(openVaults?.length), + hasNAVHook, hasActiveFarming: Boolean(activeFarming), isBoostedPool: Boolean(isBoosted), isBoostedToken0: Boolean(isBoostedToken0), isBoostedToken1: Boolean(isBoostedToken1), deployer: deployer.toLowerCase(), - isShowcase: token0.id === '0x4200000000000000000000000000000000000006' && token1.id === '0xabac6f23fdf1313fc2e9c9244f666157ccd32990' + // isShowcase: token0.id === '0x4200000000000000000000000000000000000006' && token1.id === '0xabac6f23fdf1313fc2e9c9244f666157ccd32990' }; }); }, [ @@ -145,6 +161,7 @@ export function useFormattedPools(tokenAddress?: Address): { pools: FormattedPoo poolsMaxApr, poolsAvgApr, farmingsAPR, + chainId, ]); return { pools: formattedPools, isLoading }; diff --git a/src/hooks/pools/usePoolStats.ts b/src/hooks/pools/usePoolStats.ts new file mode 100644 index 00000000..dd27763a --- /dev/null +++ b/src/hooks/pools/usePoolStats.ts @@ -0,0 +1,63 @@ +import { usePoolDayDatasQuery, useSinglePoolQuery } from "@/graphql/generated/graphql"; +import { useClients } from "@/hooks/graphql/useClients"; +import { getPoolAPR } from "@/utils/pool/getPoolAPR"; +import { useMemo } from "react"; +import useSWR from "swr"; +import { Address } from "viem"; + +export interface PoolStats { + fee: number; + tvlUSD: number; + volume24USD: number; + fees24USD: number; + avgApr: number; + txCount: string; + createdAtTimestamp: string | undefined; + isLoading: boolean; +} + +export function usePoolStats(poolId: Address): PoolStats { + const { infoClient } = useClients(); + + const poolDayDataRange = useMemo(() => { + const to = Math.floor(Date.now() / 1000); + const from = to - 3 * 24 * 60 * 60; + + return { from, to }; + }, []); + + const { data: poolInfoData, loading: isPoolInfoLoading } = useSinglePoolQuery({ + client: infoClient, + variables: { poolId: poolId.toLowerCase() }, + }); + + const { data: poolDayData, loading: isPoolDayDataLoading } = usePoolDayDatasQuery({ + client: infoClient, + variables: { + poolId: poolId.toLowerCase(), + from: poolDayDataRange.from, + to: poolDayDataRange.to, + }, + }); + + const { data: poolAvgApr, isLoading: isPoolAprLoading } = useSWR(["poolAPR", poolId], () => getPoolAPR(poolId)); + + return useMemo(() => { + const latestDayData = poolDayData?.poolDayDatas?.at(-1); + const latestDayDataDate = latestDayData ? latestDayData.date * 1000 : 0; + const isLatestDayDataFresh = Date.now() - latestDayDataDate <= 24 * 60 * 60 * 1000; + + const fee = (Number(poolInfoData?.pool?.overrideFee || 0) || Number(poolInfoData?.pool?.fee || 0)) / 10_000; + + return { + fee, + tvlUSD: Number(poolInfoData?.pool?.totalValueLockedUSD || 0), + volume24USD: isLatestDayDataFresh ? Number(latestDayData?.volumeUSD || 0) : 0, + fees24USD: isLatestDayDataFresh ? Number(latestDayData?.feesUSD || 0) : 0, + avgApr: Number(poolAvgApr || 0), + txCount: poolInfoData?.pool?.txCount || "0", + createdAtTimestamp: poolInfoData?.pool?.createdAtTimestamp, + isLoading: isPoolInfoLoading || isPoolDayDataLoading || isPoolAprLoading, + }; + }, [isPoolAprLoading, isPoolDayDataLoading, isPoolInfoLoading, poolAvgApr, poolDayData?.poolDayDatas, poolInfoData?.pool]); +} diff --git a/src/hooks/swap/useAllRoutes.ts b/src/hooks/swap/useAllRoutes.ts index 7f9c402c..9610551f 100644 --- a/src/hooks/swap/useAllRoutes.ts +++ b/src/hooks/swap/useAllRoutes.ts @@ -6,7 +6,7 @@ import { computeRegularRoutes } from "@/utils/swap/computeRegularRoutes"; export function useAllRoutes( currencyIn?: Currency, - currencyOut?: Currency + currencyOut?: Currency, ): { loading: boolean; boostedRoutes: BoostedRoute[]; diff --git a/src/hooks/swap/useBestTrade.ts b/src/hooks/swap/useBestTrade.ts index fe55a740..051a035b 100644 --- a/src/hooks/swap/useBestTrade.ts +++ b/src/hooks/swap/useBestTrade.ts @@ -116,7 +116,7 @@ export function useBestTradeExactIn(amountIn?: CurrencyAmount, currenc amountOut: null, fee: null, priceAfterSwap: null, - } + }, ); if (!bestRoute || !amountOut) { @@ -250,7 +250,7 @@ export function useBestTradeExactOut(currencyIn?: Currency, amountOut?: Currency amountIn: null, fee: null, priceAfterSwap: null, - } + }, ); if (!bestRoute || !amountIn) { diff --git a/src/hooks/swap/useOverrideFee.ts b/src/hooks/swap/useOverrideFee.ts index c1cfe392..b76f993f 100644 --- a/src/hooks/swap/useOverrideFee.ts +++ b/src/hooks/swap/useOverrideFee.ts @@ -7,6 +7,42 @@ import { useEffect, useState } from "react"; import { useChainId } from "wagmi"; import { Address, maxUint128 } from "viem"; +type PoolFeeParams = { + chainId: number; + poolAddress: Address; + isZeroToOne: boolean; + amount: bigint; + baseFee: number; +}; + +const getPoolFee = (overrideFee: number, baseFee: number, pluginFee: number) => { + return (overrideFee === 0 ? baseFee : overrideFee) + pluginFee; +}; + +const getSimulatedPoolFee = async ({ chainId, poolAddress, isZeroToOne, amount, baseFee }: PoolFeeParams) => { + const plugin = await readAlgebraPoolPlugin(wagmiConfig, { + address: poolAddress, + }); + + let beforeSwap: [string, number, number]; + + try { + const { result } = await simulateAlgebraBasePluginV1BeforeSwap(wagmiConfig, { + address: plugin, + args: [SWAP_ROUTER[chainId], ADDRESS_ZERO, isZeroToOne, amount, maxUint128, false, "0x"] as const, + account: poolAddress, + }); + + beforeSwap = result as [string, number, number]; + } catch (error) { + beforeSwap = ["", 0, 0]; + } + + const [, overrideFee, pluginFee] = beforeSwap; + + return getPoolFee(overrideFee, baseFee, pluginFee); +}; + export function useOverrideFee(trade: SmartRouterTrade | Trade | null | undefined) { const [overrideFees, setOverrideFees] = useState<{ fee: number | undefined; @@ -34,48 +70,21 @@ export function useOverrideFee(trade: SmartRouterTrade | Trade | Trade 0) { diff --git a/src/hooks/swap/useSwapCallback.ts b/src/hooks/swap/useSwapCallback.ts index 3869bbe7..8f9a8b02 100644 --- a/src/hooks/swap/useSwapCallback.ts +++ b/src/hooks/swap/useSwapCallback.ts @@ -33,6 +33,7 @@ export function useSwapCallback( trade: Trade | null | undefined, allowedSlippage: Percent, onTransactionSuccess?: () => void, + disabled?: boolean, ) { const { address: account } = useAccount(); @@ -46,6 +47,7 @@ export function useSwapCallback( useEffect(() => { async function findBestCall() { + if (disabled) return; if (!swapCalldata || swapCalldata.length === 0 || swapCalldata.every((call) => call.calldata.length === 0)) return; if (!account || !client) return; @@ -88,7 +90,7 @@ export function useSwapCallback( } findBestCall(); - }, [swapCalldata, account, chainId, client]); + }, [swapCalldata, account, chainId, client, disabled]); const swapConfig = useMemo( () => diff --git a/src/hooks/swap/useSwapPools.ts b/src/hooks/swap/useSwapPools.ts index 913b2f53..1cc2cb4d 100644 --- a/src/hooks/swap/useSwapPools.ts +++ b/src/hooks/swap/useSwapPools.ts @@ -16,7 +16,7 @@ import { isDefined } from "@/utils"; */ export function useSwapPools( currencyIn?: Currency, - currencyOut?: Currency + currencyOut?: Currency, ): { pools: Pool[]; isLoading: boolean; @@ -40,8 +40,8 @@ export function useSwapPools( tokenA, tokenB, customPoolDeployer, - }) - ) + }), + ), ); return [...basePoolAddresses, ...customPoolAddresses]; @@ -66,14 +66,14 @@ export function useSwapPools( pool.token0.id, Number(pool.token0.decimals), pool.token0.symbol, - pool.token0.name + pool.token0.name, ); const token1 = tryCreateBoostedToken( chainId, pool.token1.id, Number(pool.token1.decimals), pool.token1.symbol, - pool.token1.name + pool.token1.name, ); return new Pool( @@ -84,7 +84,7 @@ export function useSwapPools( pool.deployer, pool.liquidity, Number(pool.tick), - Number(pool.tickSpacing) + Number(pool.tickSpacing), ); }) .filter(isDefined); diff --git a/src/modules/ALMModule/components/CreateAutomatedPosition/index.tsx b/src/modules/ALMModule/components/CreateAutomatedPosition/index.tsx index 883d92d0..ae18d02e 100644 --- a/src/modules/ALMModule/components/CreateAutomatedPosition/index.tsx +++ b/src/modules/ALMModule/components/CreateAutomatedPosition/index.tsx @@ -3,16 +3,17 @@ import EnterAmountCard from "@/components/create-position/EnterAmountsCard"; import { useMintActionHandlers, useMintState } from "@/state/mintStore"; import { formatAmount } from "@/utils/common/formatAmount"; import { tryParseAmount } from "@cryptoalgebra/integral-sdk"; -import { useState, useEffect } from "react"; +import { ReactNode, useState, useEffect } from "react"; import { ExtendedVault } from "../../hooks"; import AddAutomatedLiquidityButton from "../AddAutomatedLiquidityButton"; interface CreateAutomatedPositionProps { vaults?: ExtendedVault[]; poolId?: string; + modeSwitch?: ReactNode; } -export function CreateAutomatedPosition({ vaults, poolId }: CreateAutomatedPositionProps) { +export function CreateAutomatedPosition({ vaults, poolId, modeSwitch }: CreateAutomatedPositionProps) { const [selectedVault, setSelectedVault] = useState(); const { typedValue } = useMintState(); @@ -78,6 +79,7 @@ export function CreateAutomatedPosition({ vaults, poolId }: CreateAutomatedPosit
+ {modeSwitch} -
+

Pool Liquidity

${formatAmount(statistics?.tvlUSD || 0, 4)}

@@ -80,7 +80,7 @@ const LiquidityStats = ({
-
+

Statistics

@@ -193,7 +193,7 @@ export function AnalyticsPoolPage() {
-
+
@@ -214,25 +214,27 @@ export function AnalyticsPoolPage() { />
- { enableActions &&
- - - - - - -
} + {enableActions && ( +
+ + + + + + +
+ )}
-
+
diff --git a/src/modules/AnalyticsModule/components/AnalyticsTokenPage/index.tsx b/src/modules/AnalyticsModule/components/AnalyticsTokenPage/index.tsx index 6d31ce90..0288eedd 100644 --- a/src/modules/AnalyticsModule/components/AnalyticsTokenPage/index.tsx +++ b/src/modules/AnalyticsModule/components/AnalyticsTokenPage/index.tsx @@ -38,7 +38,7 @@ const LiquidityStats = ({ }) => { return (
-
+

Price

${formatAmount(statistics?.priceUSD || 0, 4)}

Liquidity

@@ -55,7 +55,7 @@ const LiquidityStats = ({
-
+

Statistics

@@ -161,7 +161,7 @@ export function AnalyticsTokenPage() {
-
+
@@ -182,13 +182,13 @@ export function AnalyticsTokenPage() {
- - diff --git a/src/modules/BoostedPoolsModule/hooks/usePermit.ts b/src/modules/BoostedPoolsModule/hooks/usePermit.ts index ba420596..1d2db047 100644 --- a/src/modules/BoostedPoolsModule/hooks/usePermit.ts +++ b/src/modules/BoostedPoolsModule/hooks/usePermit.ts @@ -17,13 +17,20 @@ function toDeadline(expiration: number): number { export type PermitStateType = PermitState.LOADING | PermitState.NOT_PERMITTED | PermitState.PERMITTED; +type SignedPermitState = { + signature: PermitSignature; + address: Address; + chainId: number; +}; + export function usePermit(amount: CurrencyAmount | undefined, spender: string | undefined) { const { address, chainId } = useAccount(); const token = amount?.currency.wrapped; + const tokenAddress = token?.address; const permit2Address = chainId ? (PERMIT2[chainId] as Address) : undefined; // Signature state - const [signature, setSignature] = useState(); + const [signedPermit, setSignedPermit] = useState(); // Check Permit2 allowance const queryEnabled = !!address && !!token?.address && !!spender && !!permit2Address; @@ -50,11 +57,21 @@ export function usePermit(amount: CurrencyAmount | undefined, spender: }, [permitData, token]); // Check if signature is valid - const now = useMemo(() => Math.floor(Date.now() / 1000), []); + const now = Math.floor(Date.now() / 1000); const isSigned = useMemo(() => { - if (!amount || !signature) return false; - return signature.details.token === token?.address && signature.spender === spender && signature.sigDeadline >= now; - }, [amount, now, signature, spender, token?.address]); + if (!amount || !signedPermit) return false; + + const { signature, address: signedAddress, chainId: signedChainId } = signedPermit; + + return ( + signedAddress === address && + signedChainId === chainId && + signature.details.token.toLowerCase() === tokenAddress?.toLowerCase() && + signature.spender === spender && + BigInt(signature.details.amount.toString()) >= BigInt(amount.quotient.toString()) && + signature.sigDeadline >= now + ); + }, [address, amount, chainId, now, signedPermit, spender, tokenAddress]); // Check if permit is valid const isPermitted = useMemo(() => { @@ -86,6 +103,9 @@ export function usePermit(amount: CurrencyAmount | undefined, spender: if (!token) { throw new Error("missing token"); } + if (!amount) { + throw new Error("missing amount"); + } if (!spender) { throw new Error("missing spender"); } @@ -125,7 +145,7 @@ export function usePermit(amount: CurrencyAmount | undefined, spender: }); const permitSignature = { ...permit, signature: signatureResult }; - setSignature(permitSignature); + setSignedPermit({ signature: permitSignature, address, chainId }); // Show success toast toast({ @@ -148,21 +168,43 @@ export function usePermit(amount: CurrencyAmount | undefined, spender: }); throw error; } - }, [address, chainId, nonce, signTypedDataAsync, spender, token, toast, permit2Address]); + }, [address, amount, chainId, nonce, signTypedDataAsync, spender, token, toast, permit2Address]); + + const removePermitSign = useCallback(() => { + setSignedPermit(undefined); + }, []); + + useEffect(() => { + if (!signedPermit) return; + + const signatureExpiredIn = signedPermit.signature.sigDeadline * 1000 - Date.now(); + if (signatureExpiredIn <= 0) { + removePermitSign(); + return; + } - const removePermitSign = () => { - setSignature(undefined) - } + const timeout = setTimeout(removePermitSign, signatureExpiredIn); + return () => clearTimeout(timeout); + }, [removePermitSign, signedPermit]); useEffect(() => { - removePermitSign() - }, [amount?.quotient.toString(), spender]) + if (!signedPermit) return; + + const { signature, address: signedAddress, chainId: signedChainId } = signedPermit; + const contextChanged = signedAddress !== address || signedChainId !== chainId; + const tokenChanged = !!tokenAddress && signature.details.token.toLowerCase() !== tokenAddress.toLowerCase(); + const spenderChanged = !!spender && signature.spender.toLowerCase() !== spender.toLowerCase(); + + if (contextChanged || tokenChanged || spenderChanged) { + removePermitSign(); + } + }, [address, chainId, removePermitSign, signedPermit, spender, tokenAddress]); return { permitState, permitCallback, - permitSignature: isSigned ? signature : undefined, + permitSignature: isSigned ? signedPermit?.signature : undefined, refetchPermit, - removePermitSign + removePermitSign, }; } diff --git a/src/modules/BoostedPoolsModule/hooks/usePermit2.ts b/src/modules/BoostedPoolsModule/hooks/usePermit2.ts index fce9d896..428db4a2 100644 --- a/src/modules/BoostedPoolsModule/hooks/usePermit2.ts +++ b/src/modules/BoostedPoolsModule/hooks/usePermit2.ts @@ -129,5 +129,6 @@ export function usePermit2({ amount, spender }: { amount?: CurrencyAmount { @@ -67,7 +67,7 @@ export const ActiveFarming = ({ farming, deposits, positionsData }: ActiveFarmin }; return ( -
+

Active Farming

@@ -88,7 +88,7 @@ export const ActiveFarming = ({ farming, deposits, positionsData }: ActiveFarmin ? `${formatAmount(formattedRewardEarned + formattedBonusRewardEarned, 2)} ${farming.rewardToken.symbol}` : `${formatAmount(formattedRewardEarned, 2)} ${farming.rewardToken.symbol} + ${formatAmount( formattedBonusRewardEarned, - 2 + 2, )} ${farming.bonusRewardToken?.symbol}` } className="w-full" @@ -126,12 +126,12 @@ export const ActiveFarming = ({ farming, deposits, positionsData }: ActiveFarmin
-
+
diff --git a/src/modules/FarmingModule/components/CardInfo/index.tsx b/src/modules/FarmingModule/components/CardInfo/index.tsx index ec500408..eb284886 100644 --- a/src/modules/FarmingModule/components/CardInfo/index.tsx +++ b/src/modules/FarmingModule/components/CardInfo/index.tsx @@ -10,7 +10,7 @@ interface CardInfoProps { export const CardInfo: FC = ({ title, children, additional, className }) => { return ( -
+

{title}

{children}
diff --git a/src/modules/FarmingModule/components/SelectPositionFarmModal/index.tsx b/src/modules/FarmingModule/components/SelectPositionFarmModal/index.tsx index 8d932389..7bb61738 100644 --- a/src/modules/FarmingModule/components/SelectPositionFarmModal/index.tsx +++ b/src/modules/FarmingModule/components/SelectPositionFarmModal/index.tsx @@ -47,7 +47,7 @@ export function SelectPositionFarmModal({ farming, positionsData, isHarvestLoadi const availablePositions = positionsData.filter( (position) => - !position.onFarming && (Number(position.almShares || 0) > 0 || BigInt(position.position?.liquidity.toString() || 0) > 0n) + !position.onFarming && (Number(position.almShares || 0) > 0 || BigInt(position.position?.liquidity.toString() || 0) > 0n), ); const handleApprove = async () => { @@ -78,7 +78,7 @@ export function SelectPositionFarmModal({ farming, positionsData, isHarvestLoadi return ( - @@ -97,7 +97,7 @@ export function SelectPositionFarmModal({ farming, positionsData, isHarvestLoadi key={position.id} className={cn( "w-full row-span-1 col-span-1", - selectedPosition?.id === position.id ? "border-primary-button hover:border-primary-button" : "" + selectedPosition?.id === position.id ? "border-primary-button hover:border-primary-button" : "", )} onClick={() => handleSelectPosition(position)} positionId={position.id} @@ -114,20 +114,20 @@ export function SelectPositionFarmModal({ farming, positionsData, isHarvestLoadi
{isApproveVerifying ? ( - ) : selectedPosition && availablePositions.length > 0 ? ( <> - - ) : ( - )} diff --git a/src/modules/NAVHookModule/components/NAVHookAddLiquidityModal.tsx b/src/modules/NAVHookModule/components/NAVHookAddLiquidityModal.tsx new file mode 100644 index 00000000..7dba9a56 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookAddLiquidityModal.tsx @@ -0,0 +1,143 @@ +import Loader from "@/components/common/Loader"; +import EnterAmountCard from "@/components/create-position/EnterAmountsCard"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { DEFAULT_CHAIN_NAME } from "config"; +import { ApprovalState } from "@/types/approve-state"; +import { Field } from "@cryptoalgebra/integral-sdk"; +import { useAppKit, useAppKitNetwork } from "@reown/appkit/react"; +import { useState } from "react"; +import { Address } from "viem"; +import { useAccount, useChainId } from "wagmi"; +import { useNAVHookDeposit, useNAVHookDepositForm, useNAVHookPool } from "../hooks"; + +interface NAVHookAddLiquidityModalProps { + poolId: Address | undefined; + onSuccess?: () => void; + triggerClassName?: string; +} + +export function NAVHookAddLiquidityModal({ poolId, onSuccess, triggerClassName }: NAVHookAddLiquidityModalProps) { + const [isOpen, setIsOpen] = useState(false); + const { address: account } = useAccount(); + const appChainId = useChainId(); + const { chainId: userChainId } = useAppKitNetwork(); + const { open } = useAppKit(); + const { token0, token1 } = useNAVHookPool(poolId); + const depositForm = useNAVHookDepositForm(poolId); + + const { + deposit, + approveToken0, + approveToken1, + token0ApprovalState, + token1ApprovalState, + isToken0Approved, + isToken1Approved, + isPending, + error, + } = useNAVHookDeposit({ + poolId, + amount0: depositForm.amount0, + amount1: depositForm.amount1, + onSuccess: () => { + setIsOpen(false); + depositForm.reset(); + onSuccess?.(); + }, + }); + + const isWrongChain = !userChainId || appChainId !== userChainId; + + const actionButton = () => { + if (!account) { + return ( + + ); + } + + if (isWrongChain) { + return ( + + ); + } + + if (!depositForm.hasAmounts) { + return ( + + ); + } + + if (depositForm.errorMessage) { + return ( + + ); + } + + if (!isToken0Approved) { + return ( + + ); + } + + if (!isToken1Approved) { + return ( + + ); + } + + return ( + + ); + }; + + return ( + + + + + + + Add NAV Liquidity + + +
+ + + {error &&
{error}
} + {actionButton()} +
+
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookHeaderInfo.tsx b/src/modules/NAVHookModule/components/NAVHookHeaderInfo.tsx new file mode 100644 index 00000000..023dca55 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookHeaderInfo.tsx @@ -0,0 +1,22 @@ +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; + +export function NAVHookHeaderInfo() { + return ( + + +
+ +
Managed by Price Convergence + +
+ + +

Price Convergence plugin

+

+ This pool does not use manual LP ranges. You deposit into a vault, receive share tokens, and the plugin manages liquidity + around the oracle price. +

+
+ + ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookMyLiquidity.tsx b/src/modules/NAVHookModule/components/NAVHookMyLiquidity.tsx new file mode 100644 index 00000000..f6590f62 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookMyLiquidity.tsx @@ -0,0 +1,108 @@ +import LiquidityChart from "@/components/create-position/LiquidityChart"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SecurityState } from "@/hooks/pools/usePool"; +import { Currency, CurrencyAmount, Pool } from "@cryptoalgebra/integral-sdk"; +import { useAppKit } from "@reown/appkit/react"; +import { Address } from "viem"; +import { NAVHookVaultState } from "../types"; +import { NAVHookAddLiquidityModal } from "./NAVHookAddLiquidityModal"; +import { NAVHookTokenValueCard } from "./NAVHookTokenValueCard"; +import { NAVHookWithdrawLiquidityModal } from "./NAVHookWithdrawLiquidityModal"; +import { useUSDCValue } from "@/hooks/common/useUSDCValue"; +import { formatAmount } from "@/utils/common/formatAmount"; + +interface NAVHookMyLiquidityProps { + poolId: Address | undefined; + pool: Pool | null; + token0: Currency | undefined; + token1: Currency | undefined; + vaultState: NAVHookVaultState; + account: Address | undefined; + poolStatus: number | undefined | null; +} + +export function NAVHookMyLiquidity({ poolId, pool, token0, token1, vaultState, account, poolStatus }: NAVHookMyLiquidityProps) { + const { open } = useAppKit(); + const currentPrice = pool ? Number(pool.token0Price.toSignificant(8)) : undefined; + const enableActions = poolStatus === SecurityState.ENABLED; + + const { userAmount0, userAmount1 } = vaultState; + const { formatted: amount0Usd } = useUSDCValue(token0 && CurrencyAmount.fromRawAmount(token0, userAmount0.toString())); + const { formatted: amount1Usd } = useUSDCValue(token1 && CurrencyAmount.fromRawAmount(token1, userAmount1.toString())); + const depositAmounUsd = Number(amount0Usd || 0) + Number(amount1Usd || 0); + + const renderActions = () => { + if (!account) { + return ( + + ); + } + + if (!enableActions) { + return ( + + ); + } + + if (!vaultState.hasPosition) { + return ; + } + + return ( +
+ + +
+ ); + }; + + return ( +
+
+

Pool Liquidity

+ +
+ {pool && token0 && token1 ? ( + + ) : ( + + )} +
+
+ +
+
+

Deposit Balance

+ +
+ ${formatAmount(depositAmounUsd, 2)} +
+
+ {vaultState.hasPosition ? ( +
+ + + + +
+ ) : ( +
+ You have no liquidity in this pool +
+ )} + {renderActions()} +
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookPoolAttributesPanel.tsx b/src/modules/NAVHookModule/components/NAVHookPoolAttributesPanel.tsx new file mode 100644 index 00000000..dce40b67 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookPoolAttributesPanel.tsx @@ -0,0 +1,146 @@ +import CurrencyLogo from "@/components/common/CurrencyLogo"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; +import { useBlockExplorerURL } from "@/hooks/common/useBlockExplorer"; +import { PoolStats } from "@/hooks/pools/usePoolStats"; +import { formatAmount } from "@/utils"; +import { truncateHash } from "@/utils/common/truncateHash"; +import { Currency, Pool } from "@cryptoalgebra/integral-sdk"; +import { ArrowUpDown, Copy, ExternalLink } from "lucide-react"; +import { ReactNode, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { Address } from "viem"; + +interface NAVHookPoolAttributesPanelProps { + pool: Pool | null; + token0: Currency | undefined; + token1: Currency | undefined; + poolStats: PoolStats; +} + +interface PoolPriceDetails { + direct: { + baseSymbol: string; + quoteSymbol: string; + value: string; + }; + inverse: { + baseSymbol: string; + quoteSymbol: string; + value: string; + }; +} + +function OverviewAttribute({ label, value }: { label: string; value: ReactNode }) { + return ( +
+ {label} +
{value}
+
+ ); +} + +function AssetValue({ token }: { token: Currency | undefined }) { + const { toast } = useToast(); + const blockExplorerUrl = useBlockExplorerURL(); + + if (!token) return -; + + const address = token.wrapped.address as Address; + const tokenExplorePath = `/analytics/tokens/${address}`; + const explorerUrl = blockExplorerUrl ? `${blockExplorerUrl}/address/${address}` : undefined; + + const handleCopy = () => { + navigator.clipboard.writeText(address); + toast({ + title: "Copied", + description: "Address copied to clipboard", + }); + }; + + return ( +
+ + + + {token.symbol} + + {truncateHash(address)} + + + {explorerUrl && ( + + + + )} +
+ ); +} + +function CurrentPriceValue({ priceDetails }: { priceDetails: PoolPriceDetails | null }) { + const [isInverted, setIsInverted] = useState(false); + + if (!priceDetails) return -; + + const activePrice = isInverted ? priceDetails.inverse : priceDetails.direct; + + return ( +
+ + 1 {activePrice.baseSymbol} = {activePrice.value} {activePrice.quoteSymbol} + + +
+ ); +} + +function formatCreatedOn(createdAtTimestamp: string | undefined) { + if (!createdAtTimestamp) return "-"; + + return new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(Number(createdAtTimestamp) * 1000)); +} + +export function NAVHookPoolAttributesPanel({ pool, token0, token1, poolStats }: NAVHookPoolAttributesPanelProps) { + const priceDetails = useMemo(() => { + if (!pool || !token0 || !token1) return null; + + return { + direct: { + baseSymbol: token0.symbol || "Token", + quoteSymbol: token1.symbol || "Token", + value: pool.token0Price.toSignificant(8), + }, + inverse: { + baseSymbol: token1.symbol || "Token", + quoteSymbol: token0.symbol || "Token", + value: pool.token1Price.toSignificant(8), + }, + }; + }, [pool, token0, token1]); + + return ( +
+

Pool Attributes

+
+ } /> + } /> + } /> + + +
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookPoolLayout.tsx b/src/modules/NAVHookModule/components/NAVHookPoolLayout.tsx new file mode 100644 index 00000000..17192dc7 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookPoolLayout.tsx @@ -0,0 +1,39 @@ +import { Pool } from "@cryptoalgebra/integral-sdk"; +import { PoolStats } from "@/hooks/pools/usePoolStats"; +import { Address } from "viem"; +import { useAccount } from "wagmi"; +import { useNAVHookPool, useNAVHookVaultState } from "../hooks"; +import { NAVHookMyLiquidity } from "./NAVHookMyLiquidity"; +import { NAVHookPoolAttributesPanel } from "./NAVHookPoolAttributesPanel"; +import { NAVHookReservesPanel } from "./NAVHookReservesPanel"; + +interface NAVHookPoolLayoutProps { + poolId: Address | undefined; + pool: Pool | null; + poolStatus: number | undefined | null; + poolStats: PoolStats; +} + +export function NAVHookPoolLayout({ poolId, pool, poolStatus, poolStats }: NAVHookPoolLayoutProps) { + const { address: account } = useAccount(); + const { token0, token1 } = useNAVHookPool(poolId); + const vaultState = useNAVHookVaultState(poolId, account); + + return ( +
+ +
+ + +
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookReservesPanel.tsx b/src/modules/NAVHookModule/components/NAVHookReservesPanel.tsx new file mode 100644 index 00000000..c8526607 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookReservesPanel.tsx @@ -0,0 +1,136 @@ +import CurrencyLogo from "@/components/common/CurrencyLogo"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useUSDCValue } from "@/hooks/common/useUSDCValue"; +import { formatAmount } from "@/utils"; +import { Currency, CurrencyAmount } from "@cryptoalgebra/integral-sdk"; +import { useMemo } from "react"; +import { Link } from "react-router-dom"; +import { formatUnits } from "viem"; +import { NAVHookVaultState } from "../types"; + +interface NAVHookReservesPanelProps { + token0: Currency | undefined; + token1: Currency | undefined; + vaultState: NAVHookVaultState; +} + +function ReserveRatioBar({ + token0, + token1, + token0Share, + token1Share, +}: { + token0: Currency | undefined; + token1: Currency | undefined; + token0Share: number; + token1Share: number; +}) { + return ( +
+
+
+
+
+
+
+ + {formatAmount(token0Share, 2)}% +
+
+ + {formatAmount(token1Share, 2)}% +
+
+
+ ); +} + +function ReserveRow({ token, amountRaw }: { token: Currency | undefined; amountRaw: bigint }) { + const amount = useMemo(() => (token ? CurrencyAmount.fromRawAmount(token, amountRaw.toString()) : undefined), [amountRaw, token]); + const { formatted: amountUSD } = useUSDCValue(amount); + const tokenExplorePath = token?.wrapped.address ? `/analytics/tokens/${token.wrapped.address}` : undefined; + + return ( +
+
+ + {tokenExplorePath ? ( + + {token?.symbol} + + ) : ( + {token?.symbol || "Token"} + )} +
+ + {token ? formatAmount(formatUnits(amountRaw, token.decimals), 4) : }{" "} + ${formatAmount(amountUSD || 0, 2)} + +
+ ); +} + +export function NAVHookReservesPanel({ token0, token1, vaultState }: NAVHookReservesPanelProps) { + const amount0 = useMemo(() => (token0 ? CurrencyAmount.fromRawAmount(token0, vaultState.total0.toString()) : undefined), [ + token0, + vaultState.total0, + ]); + const amount1 = useMemo(() => (token1 ? CurrencyAmount.fromRawAmount(token1, vaultState.total1.toString()) : undefined), [ + token1, + vaultState.total1, + ]); + const { formatted: amount0USD } = useUSDCValue(amount0); + const { formatted: amount1USD } = useUSDCValue(amount1); + + const { token0Share, token1Share, totalUSD } = useMemo(() => { + const reserve0USD = Number(amount0USD || 0); + const reserve1USD = Number(amount1USD || 0); + const totalReserveUSD = reserve0USD + reserve1USD; + + if (totalReserveUSD > 0) { + const nextToken0Share = (reserve0USD / totalReserveUSD) * 100; + return { + token0Share: nextToken0Share, + token1Share: 100 - nextToken0Share, + totalUSD: totalReserveUSD, + }; + } + + const reserve0Amount = token0 ? Number(formatUnits(vaultState.total0, token0.decimals)) : 0; + const reserve1Amount = token1 ? Number(formatUnits(vaultState.total1, token1.decimals)) : 0; + const totalReserveAmount = reserve0Amount + reserve1Amount; + + if (!Number.isFinite(totalReserveAmount) || totalReserveAmount <= 0) { + return { token0Share: 0, token1Share: 0, totalUSD: 0 }; + } + + const nextToken0Share = (reserve0Amount / totalReserveAmount) * 100; + return { + token0Share: nextToken0Share, + token1Share: 100 - nextToken0Share, + totalUSD: 0, + }; + }, [amount0USD, amount1USD, token0, token1, vaultState.total0, vaultState.total1]); + + return ( +
+
+

Vault Reserves

+
${formatAmount(totalUSD, 2)}
+
+ +
+ + +
+ +
+

Token Ratio

+ +
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookTag.tsx b/src/modules/NAVHookModule/components/NAVHookTag.tsx new file mode 100644 index 00000000..b3026bcf --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookTag.tsx @@ -0,0 +1,19 @@ +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; + +export function NAVHookTag() { + return ( + + +
+ NAV +
+
+ +

Price Convergence plugin

+

+ This pool uses the Price Convergence Plugin which enhances AMM price discovery by applying external price data. +

+
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookTokenValueCard.tsx b/src/modules/NAVHookModule/components/NAVHookTokenValueCard.tsx new file mode 100644 index 00000000..880776d9 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookTokenValueCard.tsx @@ -0,0 +1,41 @@ +import CurrencyLogo from "@/components/common/CurrencyLogo"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useUSDCValue } from "@/hooks/common/useUSDCValue"; +import { formatAmount } from "@/utils"; +import { Currency, CurrencyAmount } from "@cryptoalgebra/integral-sdk"; +import { useMemo } from "react"; +import { formatUnits } from "viem"; + +interface NAVHookTokenValueCardProps { + currency: Currency | undefined; + amountRaw: bigint; +} + +export function NAVHookTokenValueCard({ currency, amountRaw }: NAVHookTokenValueCardProps) { + const amount = useMemo(() => (currency ? CurrencyAmount.fromRawAmount(currency, amountRaw.toString()) : undefined), [ + amountRaw, + currency, + ]); + const { formatted: amountUSD } = useUSDCValue(amount); + + return ( +
+ +
+
+ {currency ? ( + <> + + {formatAmount(formatUnits(amountRaw, currency.decimals), 6)} + + {currency.symbol} + + ) : ( + + )} +
+
${formatAmount(amountUSD || 0, 2)}
+
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/NAVHookWithdrawLiquidityModal.tsx b/src/modules/NAVHookModule/components/NAVHookWithdrawLiquidityModal.tsx new file mode 100644 index 00000000..7306e270 --- /dev/null +++ b/src/modules/NAVHookModule/components/NAVHookWithdrawLiquidityModal.tsx @@ -0,0 +1,131 @@ +import { NAVHookTokenValueCard } from "./NAVHookTokenValueCard"; +import Loader from "@/components/common/Loader"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Slider } from "@/components/ui/slider"; +import { DEFAULT_CHAIN_NAME } from "config"; +import { ApprovalState } from "@/types/approve-state"; +import { useAppKit, useAppKitNetwork } from "@reown/appkit/react"; +import { useState } from "react"; +import { Address } from "viem"; +import { useAccount, useChainId } from "wagmi"; +import { useNAVHookPool, useNAVHookVaultState, useNAVHookWithdraw } from "../hooks"; +import { getPercentAmount } from "../utils"; + +interface NAVHookWithdrawLiquidityModalProps { + poolId: Address | undefined; + onSuccess?: () => void; + triggerClassName?: string; +} + +export function NAVHookWithdrawLiquidityModal({ poolId, onSuccess, triggerClassName }: NAVHookWithdrawLiquidityModalProps) { + const [isOpen, setIsOpen] = useState(false); + const [sliderValue, setSliderValue] = useState([50]); + const percent = sliderValue[0] ?? 0; + const { address: account } = useAccount(); + const appChainId = useChainId(); + const { chainId: userChainId } = useAppKitNetwork(); + const { open } = useAppKit(); + const { token0, token1 } = useNAVHookPool(poolId); + const vaultState = useNAVHookVaultState(poolId, account); + + const { withdraw, approveShares, approvalState, isApproved, isPending, error, sharesToWithdrawRaw } = useNAVHookWithdraw({ + poolId, + percent, + onSuccess: () => { + setIsOpen(false); + onSuccess?.(); + }, + }); + + const isWrongChain = !userChainId || appChainId !== userChainId; + const amount0Raw = getPercentAmount(vaultState.userAmount0, percent); + const amount1Raw = getPercentAmount(vaultState.userAmount1, percent); + + const actionButton = () => { + if (!account) { + return ( + + ); + } + + if (isWrongChain) { + return ( + + ); + } + + if (sharesToWithdrawRaw === 0n) { + return ( + + ); + } + + if (!isApproved) { + return ( + + ); + } + + return ( + + ); + }; + + return ( + + + + + + + Withdraw NAV Liquidity + + +
+

{percent}%

+
+ {[25, 50, 75, 100].map((value) => ( + + ))} +
+ +
+ + +
+ {error &&
{error}
} + {actionButton()} +
+
+
+ ); +} diff --git a/src/modules/NAVHookModule/components/index.ts b/src/modules/NAVHookModule/components/index.ts new file mode 100644 index 00000000..5be74777 --- /dev/null +++ b/src/modules/NAVHookModule/components/index.ts @@ -0,0 +1,8 @@ +export * from "./NAVHookAddLiquidityModal"; +export * from "./NAVHookHeaderInfo"; +export * from "./NAVHookMyLiquidity"; +export * from "./NAVHookPoolAttributesPanel"; +export * from "./NAVHookPoolLayout"; +export * from "./NAVHookReservesPanel"; +export * from "./NAVHookTag"; +export * from "./NAVHookWithdrawLiquidityModal"; diff --git a/src/modules/NAVHookModule/hooks/index.ts b/src/modules/NAVHookModule/hooks/index.ts new file mode 100644 index 00000000..309a4869 --- /dev/null +++ b/src/modules/NAVHookModule/hooks/index.ts @@ -0,0 +1,5 @@ +export * from "./useNAVHookDeposit"; +export * from "./useNAVHookDepositForm"; +export * from "./useNAVHookPool"; +export * from "./useNAVHookVaultState"; +export * from "./useNAVHookWithdraw"; diff --git a/src/modules/NAVHookModule/hooks/useNAVHookDeposit.ts b/src/modules/NAVHookModule/hooks/useNAVHookDeposit.ts new file mode 100644 index 00000000..4514e6ca --- /dev/null +++ b/src/modules/NAVHookModule/hooks/useNAVHookDeposit.ts @@ -0,0 +1,85 @@ +import { priceConvergenceVaultDepositGuardAbi } from "@/generated"; +import { useApprove } from "@/hooks/common/useApprove"; +import { useTransactionAwait } from "@/hooks/common/useTransactionAwait"; +import { TransactionType } from "@/state/pendingTransactionsStore"; +import { useUserSlippageToleranceWithDefault } from "@/state/userStore"; +import { ApprovalState } from "@/types/approve-state"; +import { Currency, CurrencyAmount } from "@cryptoalgebra/integral-sdk"; +import { useCallback, useMemo, useState } from "react"; +import { Address, zeroAddress } from "viem"; +import { useAccount, usePublicClient, useWriteContract } from "wagmi"; +import { applySlippage, DEFAULT_NAV_HOOK_SLIPPAGE } from "../utils"; +import { useNAVHookPool } from "./useNAVHookPool"; + +interface UseNAVHookDepositArgs { + poolId: Address | undefined; + amount0: CurrencyAmount | undefined; + amount1: CurrencyAmount | undefined; + onSuccess?: () => void; +} + +export function useNAVHookDeposit({ poolId, amount0, amount1, onSuccess }: UseNAVHookDepositArgs) { + const { address: account } = useAccount(); + const { guardAddress, token0, token1 } = useNAVHookPool(poolId); + const publicClient = usePublicClient(); + const slippage = useUserSlippageToleranceWithDefault(DEFAULT_NAV_HOOK_SLIPPAGE); + const [error, setError] = useState(null); + const amount0Key = amount0?.quotient.toString(); + const amount1Key = amount1?.quotient.toString(); + const amount0Raw = useMemo(() => (amount0Key ? BigInt(amount0Key) : 0n), [amount0Key]); + const amount1Raw = useMemo(() => (amount1Key ? BigInt(amount1Key) : 0n), [amount1Key]); + + const { approvalState: token0ApprovalState, approvalCallback: approveToken0 } = useApprove(amount0, guardAddress ?? zeroAddress); + const { approvalState: token1ApprovalState, approvalCallback: approveToken1 } = useApprove(amount1, guardAddress ?? zeroAddress); + + const { data: hash, writeContractAsync, isPending } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useTransactionAwait(hash, { + title: "Add NAV liquidity", + tokenA: token0?.wrapped.address as Address, + tokenB: token1?.wrapped.address as Address, + type: TransactionType.POOL, + callback: onSuccess, + }); + + const deposit = useCallback(async () => { + if (!guardAddress || !account || !publicClient) return; + if (amount0Raw + amount1Raw === 0n) return; + + setError(null); + + try { + const simulation = await publicClient.simulateContract({ + account, + address: guardAddress, + abi: priceConvergenceVaultDepositGuardAbi, + functionName: "deposit", + args: [amount0Raw, amount1Raw, 0n, account], + }); + const minimumShares = applySlippage(simulation.result, slippage); + + await writeContractAsync({ + address: guardAddress, + abi: priceConvergenceVaultDepositGuardAbi, + functionName: "deposit", + args: [amount0Raw, amount1Raw, minimumShares, account], + }); + } catch (depositError) { + console.error("NAV Hook deposit failed", depositError); + setError("Deposit failed. Please check the entered amounts and try again."); + } + }, [account, amount0Raw, amount1Raw, guardAddress, publicClient, slippage, writeContractAsync]); + + return { + deposit, + approveToken0, + approveToken1, + token0ApprovalState, + token1ApprovalState, + isToken0Approved: amount0Raw === 0n || token0ApprovalState === ApprovalState.APPROVED, + isToken1Approved: amount1Raw === 0n || token1ApprovalState === ApprovalState.APPROVED, + isPending: isPending || isConfirming, + isSuccess, + error, + }; +} diff --git a/src/modules/NAVHookModule/hooks/useNAVHookDepositForm.ts b/src/modules/NAVHookModule/hooks/useNAVHookDepositForm.ts new file mode 100644 index 00000000..db7a2185 --- /dev/null +++ b/src/modules/NAVHookModule/hooks/useNAVHookDepositForm.ts @@ -0,0 +1,85 @@ +import { useUSDCValue } from "@/hooks/common/useUSDCValue"; +import { Currency, CurrencyAmount, tryParseAmount } from "@cryptoalgebra/integral-sdk"; +import { useCallback, useMemo, useState } from "react"; +import { Address } from "viem"; +import { useAccount, useBalance } from "wagmi"; +import { useNAVHookPool } from "./useNAVHookPool"; + +interface UseNAVHookDepositFormResult { + amount0Value: string; + amount1Value: string; + amount0: CurrencyAmount | undefined; + amount1: CurrencyAmount | undefined; + amount0Usd: number | null; + amount1Usd: number | null; + hasAmounts: boolean; + errorMessage: string | undefined; + onAmount0Change: (value: string) => void; + onAmount1Change: (value: string) => void; + reset: () => void; +} + +function currencyBalance(currency: Currency | undefined, balance: { value: bigint } | undefined) { + if (!currency || !balance) return undefined; + + return CurrencyAmount.fromRawAmount(currency, balance.value.toString()); +} + +export function useNAVHookDepositForm(poolId: Address | undefined): UseNAVHookDepositFormResult { + const { address: account } = useAccount(); + const { token0, token1 } = useNAVHookPool(poolId); + const [amount0Value, setAmount0Value] = useState(""); + const [amount1Value, setAmount1Value] = useState(""); + + const amount0 = useMemo(() => tryParseAmount(amount0Value, token0), [amount0Value, token0]); + const amount1 = useMemo(() => tryParseAmount(amount1Value, token1), [amount1Value, token1]); + const { formatted: amount0Usd } = useUSDCValue(amount0); + const { formatted: amount1Usd } = useUSDCValue(amount1); + + const { data: token0Balance } = useBalance({ + address: account, + token: token0?.isNative ? undefined : (token0?.wrapped.address as Address | undefined), + }); + const { data: token1Balance } = useBalance({ + address: account, + token: token1?.isNative ? undefined : (token1?.wrapped.address as Address | undefined), + }); + + const parsedToken0Balance = useMemo(() => currencyBalance(token0, token0Balance), [token0, token0Balance]); + const parsedToken1Balance = useMemo(() => currencyBalance(token1, token1Balance), [token1, token1Balance]); + + const amount0Raw = amount0 ? BigInt(amount0.quotient.toString()) : 0n; + const amount1Raw = amount1 ? BigInt(amount1.quotient.toString()) : 0n; + const hasAmounts = amount0Raw + amount1Raw > 0n; + + const errorMessage = useMemo(() => { + if (amount0 && parsedToken0Balance && parsedToken0Balance.lessThan(amount0)) { + return `Insufficient ${token0?.symbol} balance`; + } + + if (amount1 && parsedToken1Balance && parsedToken1Balance.lessThan(amount1)) { + return `Insufficient ${token1?.symbol} balance`; + } + + return undefined; + }, [amount0, amount1, parsedToken0Balance, parsedToken1Balance, token0?.symbol, token1?.symbol]); + + const reset = useCallback(() => { + setAmount0Value(""); + setAmount1Value(""); + }, []); + + return { + amount0Value, + amount1Value, + amount0, + amount1, + amount0Usd, + amount1Usd, + hasAmounts, + errorMessage, + onAmount0Change: setAmount0Value, + onAmount1Change: setAmount1Value, + reset, + }; +} diff --git a/src/modules/NAVHookModule/hooks/useNAVHookPool.ts b/src/modules/NAVHookModule/hooks/useNAVHookPool.ts new file mode 100644 index 00000000..021cbf25 --- /dev/null +++ b/src/modules/NAVHookModule/hooks/useNAVHookPool.ts @@ -0,0 +1,50 @@ +import { useCurrency } from "@/hooks/common/useCurrency"; +import { priceConvergenceVaultAbi } from "@/generated"; +import { PRICE_CONVERGENCE_VAULT_BY_POOL, PRICE_CONVERGENCE_VAULT_DEPOSIT_GUARD_BY_POOL } from "config"; +import { useMemo } from "react"; +import { Address } from "viem"; +import { useChainId, useReadContract } from "wagmi"; +import { NAVHookPoolInfo } from "../types"; +import { getAddressByPool } from "../utils"; + +export function useNAVHookPool(poolId: Address | undefined): NAVHookPoolInfo { + const chainId = useChainId(); + + const vaultAddress = useMemo( + () => getAddressByPool(PRICE_CONVERGENCE_VAULT_BY_POOL[chainId], poolId), + [chainId, poolId], + ); + const guardAddress = useMemo( + () => getAddressByPool(PRICE_CONVERGENCE_VAULT_DEPOSIT_GUARD_BY_POOL[chainId], poolId), + [chainId, poolId], + ); + + const { data: token0Address } = useReadContract({ + address: vaultAddress, + abi: priceConvergenceVaultAbi, + functionName: "token0", + query: { + enabled: Boolean(vaultAddress), + }, + }); + + const { data: token1Address } = useReadContract({ + address: vaultAddress, + abi: priceConvergenceVaultAbi, + functionName: "token1", + query: { + enabled: Boolean(vaultAddress), + }, + }); + + const token0 = useCurrency(token0Address, true); + const token1 = useCurrency(token1Address, true); + + return { + isNAVHookPool: Boolean(vaultAddress && guardAddress), + vaultAddress, + guardAddress, + token0, + token1, + }; +} diff --git a/src/modules/NAVHookModule/hooks/useNAVHookVaultState.ts b/src/modules/NAVHookModule/hooks/useNAVHookVaultState.ts new file mode 100644 index 00000000..30a71154 --- /dev/null +++ b/src/modules/NAVHookModule/hooks/useNAVHookVaultState.ts @@ -0,0 +1,62 @@ +import { priceConvergenceVaultAbi } from "@/generated"; +import { Address } from "viem"; +import { useReadContracts } from "wagmi"; +import { NAVHookVaultState } from "../types"; +import { getProportionalAmount } from "../utils"; +import { useNAVHookPool } from "./useNAVHookPool"; + +export function useNAVHookVaultState(poolId: Address | undefined, account: Address | undefined): NAVHookVaultState { + const { vaultAddress } = useNAVHookPool(poolId); + + const { data: vaultData, isLoading: isVaultDataLoading, refetch: refetchVaultData } = useReadContracts({ + allowFailure: false, + contracts: vaultAddress + ? [ + { address: vaultAddress, abi: priceConvergenceVaultAbi, functionName: "totalSupply" }, + { address: vaultAddress, abi: priceConvergenceVaultAbi, functionName: "getTotalAmounts" }, + { address: vaultAddress, abi: priceConvergenceVaultAbi, functionName: "decimals" }, + ] + : [], + query: { + enabled: Boolean(vaultAddress), + refetchInterval: 15_000, + }, + }); + + const { data: userData, isLoading: isUserDataLoading, refetch: refetchUserData } = useReadContracts({ + allowFailure: false, + contracts: + vaultAddress && account + ? [{ address: vaultAddress, abi: priceConvergenceVaultAbi, functionName: "balanceOf", args: [account] }] + : [], + query: { + enabled: Boolean(vaultAddress && account), + refetchInterval: 15_000, + }, + }); + + const totalSupply = vaultData?.[0] ?? 0n; + const totalAmounts = vaultData?.[1] ?? [0n, 0n]; + const shareDecimals = vaultData?.[2] ?? 18; + const userShares = userData?.[0] ?? 0n; + const total0 = totalAmounts[0] ?? 0n; + const total1 = totalAmounts[1] ?? 0n; + const userAmount0 = getProportionalAmount(total0, userShares, totalSupply); + const userAmount1 = getProportionalAmount(total1, userShares, totalSupply); + + return { + userShares, + totalSupply, + total0, + total1, + userAmount0, + userAmount1, + shareDecimals: Number(shareDecimals), + isLoading: isVaultDataLoading || isUserDataLoading, + hasPosition: userShares > 0n, + refetch: () => { + refetchVaultData(); + refetchUserData(); + }, + }; +} diff --git a/src/modules/NAVHookModule/hooks/useNAVHookWithdraw.ts b/src/modules/NAVHookModule/hooks/useNAVHookWithdraw.ts new file mode 100644 index 00000000..ddfc7fc4 --- /dev/null +++ b/src/modules/NAVHookModule/hooks/useNAVHookWithdraw.ts @@ -0,0 +1,89 @@ +import { priceConvergenceVaultDepositGuardAbi } from "@/generated"; +import { useApprove } from "@/hooks/common/useApprove"; +import { useCurrency } from "@/hooks/common/useCurrency"; +import { useTransactionAwait } from "@/hooks/common/useTransactionAwait"; +import { TransactionType } from "@/state/pendingTransactionsStore"; +import { useUserSlippageToleranceWithDefault } from "@/state/userStore"; +import { ApprovalState } from "@/types/approve-state"; +import { CurrencyAmount } from "@cryptoalgebra/integral-sdk"; +import { useCallback, useMemo, useState } from "react"; +import { Address, zeroAddress } from "viem"; +import { useAccount, usePublicClient, useWriteContract } from "wagmi"; +import { applySlippage, DEFAULT_NAV_HOOK_SLIPPAGE, getPercentAmount } from "../utils"; +import { useNAVHookPool } from "./useNAVHookPool"; +import { useNAVHookVaultState } from "./useNAVHookVaultState"; + +interface UseNAVHookWithdrawArgs { + poolId: Address | undefined; + percent: number; + onSuccess?: () => void; +} + +export function useNAVHookWithdraw({ poolId, percent, onSuccess }: UseNAVHookWithdrawArgs) { + const { address: account } = useAccount(); + const { vaultAddress, guardAddress, token0, token1 } = useNAVHookPool(poolId); + const vaultState = useNAVHookVaultState(poolId, account); + const vaultShareToken = useCurrency(vaultAddress, false); + const publicClient = usePublicClient(); + const slippage = useUserSlippageToleranceWithDefault(DEFAULT_NAV_HOOK_SLIPPAGE); + const [error, setError] = useState(null); + + const sharesToWithdrawRaw = useMemo(() => getPercentAmount(vaultState.userShares, percent), [percent, vaultState.userShares]); + const sharesToApprove = useMemo( + () => (vaultShareToken ? CurrencyAmount.fromRawAmount(vaultShareToken, sharesToWithdrawRaw.toString()) : undefined), + [sharesToWithdrawRaw, vaultShareToken], + ); + + const { approvalState, approvalCallback } = useApprove(sharesToApprove, guardAddress ?? zeroAddress); + const { data: hash, writeContractAsync, isPending } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useTransactionAwait(hash, { + title: "Withdraw NAV liquidity", + tokenA: token0?.wrapped.address as Address, + tokenB: token1?.wrapped.address as Address, + type: TransactionType.POOL, + callback: onSuccess, + }); + + const withdraw = useCallback(async () => { + if (!guardAddress || !account || !publicClient || sharesToWithdrawRaw === 0n) return; + + setError(null); + + try { + const simulation = await publicClient.simulateContract({ + account, + address: guardAddress, + abi: priceConvergenceVaultDepositGuardAbi, + functionName: "withdraw", + args: [sharesToWithdrawRaw, account, 0n, 0n], + }); + + await writeContractAsync({ + address: guardAddress, + abi: priceConvergenceVaultDepositGuardAbi, + functionName: "withdraw", + args: [ + sharesToWithdrawRaw, + account, + applySlippage(simulation.result[0], slippage), + applySlippage(simulation.result[1], slippage), + ], + }); + } catch (withdrawError) { + console.error("NAV Hook withdraw failed", withdrawError); + setError("Withdrawal failed. Please check the selected amount and try again."); + } + }, [account, guardAddress, publicClient, sharesToWithdrawRaw, slippage, writeContractAsync]); + + return { + withdraw, + approveShares: approvalCallback, + approvalState, + isApproved: approvalState === ApprovalState.APPROVED, + isPending: isPending || isConfirming, + isSuccess, + error, + sharesToWithdrawRaw, + }; +} diff --git a/src/modules/NAVHookModule/index.ts b/src/modules/NAVHookModule/index.ts new file mode 100644 index 00000000..16216ff8 --- /dev/null +++ b/src/modules/NAVHookModule/index.ts @@ -0,0 +1,9 @@ +import * as NAVHookComponents from "./components"; +import * as NAVHookHooks from "./hooks"; + +const NAVHookModule = { + hooks: NAVHookHooks, + components: NAVHookComponents, +}; + +export default NAVHookModule; diff --git a/src/modules/NAVHookModule/types/index.ts b/src/modules/NAVHookModule/types/index.ts new file mode 100644 index 00000000..3eae3fc6 --- /dev/null +++ b/src/modules/NAVHookModule/types/index.ts @@ -0,0 +1,23 @@ +import { Currency } from "@cryptoalgebra/integral-sdk"; +import { Address } from "viem"; + +export interface NAVHookPoolInfo { + isNAVHookPool: boolean; + vaultAddress?: Address; + guardAddress?: Address; + token0?: Currency; + token1?: Currency; +} + +export interface NAVHookVaultState { + userShares: bigint; + totalSupply: bigint; + total0: bigint; + total1: bigint; + userAmount0: bigint; + userAmount1: bigint; + shareDecimals: number; + isLoading: boolean; + hasPosition: boolean; + refetch: () => void; +} diff --git a/src/modules/NAVHookModule/utils/index.ts b/src/modules/NAVHookModule/utils/index.ts new file mode 100644 index 00000000..db87847b --- /dev/null +++ b/src/modules/NAVHookModule/utils/index.ts @@ -0,0 +1,31 @@ +import { Percent } from "@cryptoalgebra/integral-sdk"; +import { Address } from "viem"; + +export const DEFAULT_NAV_HOOK_SLIPPAGE = new Percent(10, 10_000); + +export function getAddressByPool(mapping: Record | undefined, poolId: Address | undefined): Address | undefined { + if (!mapping || !poolId) return undefined; + + const matched = Object.entries(mapping).find(([address]) => address.toLowerCase() === poolId.toLowerCase()); + return matched?.[1]; +} + +export function getProportionalAmount(total: bigint, shares: bigint, totalSupply: bigint): bigint { + if (totalSupply === 0n || shares === 0n) return 0n; + return (total * shares) / totalSupply; +} + +export function applySlippage(amount: bigint, slippage: Percent): bigint { + if (amount === 0n) return 0n; + + const numerator = BigInt(slippage.numerator.toString()); + const denominator = BigInt(slippage.denominator.toString()); + + if (denominator === 0n || numerator >= denominator) return 0n; + return (amount * (denominator - numerator)) / denominator; +} + +export function getPercentAmount(amount: bigint, percent: number): bigint { + if (amount === 0n || percent <= 0) return 0n; + return (amount * BigInt(percent)) / 100n; +} diff --git a/src/modules/__empty_module.ts b/src/modules/__empty_module.ts index 898355c3..3a14ab72 100644 --- a/src/modules/__empty_module.ts +++ b/src/modules/__empty_module.ts @@ -36,9 +36,13 @@ export default { usePositionInFarming: () => ({}), useUnclaimedRewards: () => ({}), useLimitOrderInfo: () => ({}), + useNAVHookDeposit: () => ({}), + useNAVHookDepositForm: () => ({}), + useNAVHookPool: () => ({}), + useNAVHookVaultState: () => ({}), + useNAVHookWithdraw: () => ({}), useAllOpenMarkets: () => ({}), useCountdown: () => ({}), - useLivePoolPrice: () => ({}), useMarketFiveMinuteData: () => ({}), useMarketStats: () => ({}), useMarketsByTokens: () => ({}), @@ -100,14 +104,31 @@ export default { LimitPriceCard: () => null, limitOrderColumns: () => null, LimitOrdersTable: () => null, - LiveChip: () => null, + NAVHookAddLiquidityModal: () => null, + NAVHookHeaderInfo: () => null, + NAVHookMyLiquidity: () => null, + NAVHookPoolAttributesPanel: () => null, + NAVHookPoolLayout: () => null, + NAVHookReservesPanel: () => null, + NAVHookTag: () => null, + NAVHookTokenValueCard: () => null, + NAVHookWithdrawLiquidityModal: () => null, + FeaturedMarketCard: () => null, + LivePriceChart: () => null, + MarketLadder: () => null, + OpportunityStage: () => null, PredictionButton: () => null, PredictionChart: () => null, - PredictionInfo: () => null, + PredictionForm: () => null, PredictionMarketCard: () => null, + PredictionMarketDetails: () => null, PredictionMarkets: () => null, + PredictionParams: () => null, + PredictionQuestion: () => null, PredictionSideSelector: () => null, PredictionTradesTable: () => null, + UserActivitySection: () => null, + UserMarkets: () => null, ClaimRebaseRewardsButton: () => null, ClaimVotingRewardsModal: () => null, CreateLockModal: () => null, @@ -129,6 +150,11 @@ export default { getRewardsCalldata: () => undefined, getUnclaimedRewardsCalldata: () => undefined, isSameRewards: () => undefined, + DEFAULT_NAV_HOOK_SLIPPAGE: () => undefined, + getAddressByPool: () => undefined, + getProportionalAmount: () => undefined, + applySlippage: () => undefined, + getPercentAmount: () => undefined, getTimeUntilTimestamp: () => undefined, optimizeVotes: () => undefined, }, diff --git a/src/pages/NewPosition/CreateManualPosition.tsx b/src/pages/NewPosition/CreateManualPosition.tsx index 35e6bc2f..a0b5b629 100644 --- a/src/pages/NewPosition/CreateManualPosition.tsx +++ b/src/pages/NewPosition/CreateManualPosition.tsx @@ -7,14 +7,15 @@ import { useReadAlgebraPoolToken0, useReadAlgebraPoolToken1 } from "@/generated" import { useCurrency } from "@/hooks/common/useCurrency"; import { useDerivedMintInfo, useRangeHopCallbacks, useMintActionHandlers, useMintState } from "@/state/mintStore"; import { INITIAL_POOL_FEE, Bound, nearestUsableTick, TickMath } from "@cryptoalgebra/integral-sdk"; -import { useState, useMemo, useEffect } from "react"; +import { ReactNode, useState, useMemo, useEffect } from "react"; import { Address } from "viem"; interface ManualProps { poolAddress?: Address; + modeSwitch?: ReactNode; } -export function CreateManualPosition({ poolAddress }: ManualProps) { +export function CreateManualPosition({ poolAddress, modeSwitch }: ManualProps) { const { data: token0 } = useReadAlgebraPoolToken0({ address: poolAddress, }); @@ -42,7 +43,7 @@ export function CreateManualPosition({ poolAddress }: ManualProps) { poolAddress, INITIAL_POOL_FEE, currencyA ?? undefined, - undefined + undefined, ); const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = mintInfo.pricesAtTicks; @@ -78,7 +79,7 @@ export function CreateManualPosition({ poolAddress }: ManualProps) { mintInfo.tickSpacing, tickLower, tickUpper, - mintInfo.pool + mintInfo.pool, ); const { onLeftRangeInput, onRightRangeInput } = useMintActionHandlers(mintInfo.noLiquidity); @@ -104,21 +105,23 @@ export function CreateManualPosition({ poolAddress }: ManualProps) {
-
+

Select Range

@@ -160,7 +163,8 @@ export function CreateManualPosition({ poolAddress }: ManualProps) {
{/*

2. Enter Amounts

*/} -
+
+ {modeSwitch}
diff --git a/src/pages/NewPosition/index.tsx b/src/pages/NewPosition/index.tsx index 43a22ecd..3325ee42 100644 --- a/src/pages/NewPosition/index.tsx +++ b/src/pages/NewPosition/index.tsx @@ -1,12 +1,12 @@ import PageContainer from "@/components/common/PageContainer"; import PageTitle from "@/components/common/PageTitle"; import { useParams } from "react-router-dom"; -import { Button } from "@/components/ui/button"; import { useEffect, useState } from "react"; import { CreateManualPosition } from "./CreateManualPosition"; import { Address } from "viem"; import { enabledModules } from "config/app-modules"; import ALMModule from "@/modules/ALMModule"; +import { cn } from "@/utils"; const { useALMVaultsByPool } = ALMModule.hooks; const { CreateAutomatedPosition } = ALMModule.components; @@ -37,35 +37,44 @@ const NewPositionPage = () => { } }, [vaults]); + const modeSwitch = isALMPool && enabledModules.ALMModule && ( +
+ + +
+ ); + return (
- {isALMPool && enabledModules.ALMModule && ( -
- - -
- )}
- {isALM ? : } + {isALM ? ( + + ) : ( + + )}
); }; diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index 84845f91..f4c38d94 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -5,6 +5,7 @@ import PositionCard from "@/components/position/PositionCard"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { usePool, SecurityState } from "@/hooks/pools/usePool"; +import { usePoolStats } from "@/hooks/pools/usePoolStats"; import { usePositions } from "@/hooks/positions/usePositions"; import { FormattedPosition } from "@/types/formatted-position"; import { getPositionAPR } from "@/utils/positions/getPositionAPR"; @@ -18,6 +19,7 @@ import { useAccount } from "wagmi"; import JSBI from "jsbi"; import { Address, parseUnits } from "viem"; import ALMModule from "@/modules/ALMModule"; +import NAVHookModule from "@/modules/NAVHookModule"; import FarmingModule from "@/modules/FarmingModule"; import MyPositionsToolbar from "@/components/pool/MyPositionsToolbar"; import { useAppKit } from "@reown/appkit/react"; @@ -26,10 +28,14 @@ import { useUSDCPrice } from "@/hooks/common/useUSDCValue"; import useSWR from "swr"; import { Deposit, useSinglePositionLazyQuery } from "@/graphql/generated/graphql"; import { useReadSecurityRegistryGlobalStatus } from "@/generated"; +import { enabledModules } from "config"; const { ALMPositionCard } = ALMModule.components; const { useUserALMVaultsByPool } = ALMModule.hooks; +const { NAVHookPoolLayout } = NAVHookModule.components; +const { useNAVHookPool } = NAVHookModule.hooks; + const { ActiveFarming, UnclaimedRewards } = FarmingModule.components; const { useActiveFarming, useClosedFarmings, useUnclaimedRewards } = FarmingModule.hooks; @@ -61,6 +67,10 @@ const PoolPage = () => { const effectiveStatus = globalStatus !== SecurityState.ENABLED ? globalStatus : poolSecurityStatus; + const poolStats = usePoolStats(poolId); + const { isNAVHookPool } = useNAVHookPool(poolId); + const showNAVHookPool = enabledModules.NAVHookModule && isNAVHookPool; + const filteredPositions = useMemo(() => { if (!positions || !poolEntity) return []; @@ -132,6 +142,9 @@ const PoolPage = () => { return positionsAPRs; }, + { + keepPreviousData: true, + }, ); const positionsData = useMemo(() => { @@ -210,78 +223,86 @@ const PoolPage = () => { return ( - - -
-
- - {!account ? ( - - ) : isLoading ? ( - - ) : noPositions ? ( - effectiveStatus === SecurityState.ENABLED ? ( - - ) : null - ) : ( - <> - setSelectedPosition(position)} + + + {showNAVHookPool ? ( + + ) : ( +
+
+ + {!account ? ( + + ) : isLoading ? ( + + ) : noPositions ? ( + effectiveStatus === SecurityState.ENABLED ? ( + + ) : null + ) : ( + <> + setSelectedPosition(position)} + /> + {unclaimedRewards && + Boolean(unclaimedRewards?.rewards?.length) && + effectiveStatus === SecurityState.ENABLED && ( + + )} + + )} + {farmingInfo && !isFarmingLoading && !areDepositsLoading && effectiveStatus === SecurityState.ENABLED && ( + - {unclaimedRewards && - Boolean(unclaimedRewards?.rewards?.length) && - effectiveStatus === SecurityState.ENABLED && ( - - )} - - )} - {farmingInfo && !isFarmingLoading && !areDepositsLoading && effectiveStatus === SecurityState.ENABLED && ( - + +
+ - )} -
- -
- - v.vault.id === selectedPosition?.almVaultAddress && v.shares === selectedPosition?.almShares, - )} - poolStatus={effectiveStatus} - /> + v.vault.id === selectedPosition?.almVaultAddress && v.shares === selectedPosition?.almShares, + )} + poolStatus={effectiveStatus} + /> +
-
+ )} ); }; const NoPositions = ({ poolId }: { poolId: Address }) => ( -
-

You don't have positions for this pool

-

Let's create one!

-
@@ -291,10 +312,12 @@ const NoAccount = () => { const { open } = useAppKit(); return ( -
-

Connect Wallet

-

Connect your account to view or create positions

-
@@ -302,7 +325,7 @@ const NoAccount = () => { }; const LoadingState = () => ( -
+
{[1, 2, 3, 4].map((v) => ( ))} diff --git a/src/state/mintStore.ts b/src/state/mintStore.ts index c9563e94..e5945206 100644 --- a/src/state/mintStore.ts +++ b/src/state/mintStore.ts @@ -116,8 +116,8 @@ const initialState = { initialUSDPrices: { [Field.CURRENCY_A]: "", [Field.CURRENCY_B]: "" }, initialTokenPrice: "", currentStep: 0, - token0InputMode: enabledModules.BoostedPoolsModule ? ("underlying" as const) : ("boosted" as const), - token1InputMode: enabledModules.BoostedPoolsModule ? ("underlying" as const) : ("boosted" as const), + token0InputMode: "boosted" as const, + token1InputMode: "boosted" as const, }; export const useMintState = create((set, get) => ({ @@ -150,7 +150,7 @@ export const useMintState = create((set, get) => ({ })); export function useMintActionHandlers( - noLiquidity: boolean | undefined + noLiquidity: boolean | undefined, ): { onFieldAInput: (typedValue: string) => void; onFieldBInput: (typedValue: string) => void; @@ -187,7 +187,7 @@ export function useDerivedMintInfo( poolAddress?: Address, feeAmount?: number, baseCurrency?: Currency, - existingPosition?: Position + existingPosition?: Position, ): IDerivedMintInfo { const { address: account } = useAccount(); @@ -209,7 +209,7 @@ export function useDerivedMintInfo( [Field.CURRENCY_A]: currencyA, [Field.CURRENCY_B]: currencyB, }), - [currencyA, currencyB] + [currencyA, currencyB], ); // formatted with tokens @@ -226,7 +226,7 @@ export function useDerivedMintInfo( ? [tokenA, tokenB] : [tokenB, tokenA] : [undefined, undefined], - [tokenA, tokenB] + [tokenA, tokenB], ); const [addressA, addressB] = [ @@ -349,7 +349,7 @@ export function useDerivedMintInfo( [Bound.LOWER]: tickSpacing ? nearestUsableTick(TickMath.MIN_TICK, tickSpacing) : undefined, [Bound.UPPER]: tickSpacing ? nearestUsableTick(TickMath.MAX_TICK, tickSpacing) : undefined, }), - [tickSpacing] + [tickSpacing], ); // parse typed range values and determine closest ticks @@ -387,7 +387,7 @@ export function useDerivedMintInfo( [Bound.LOWER]: Boolean(feeAmount) && tickLower === tickSpaceLimits.LOWER, [Bound.UPPER]: Boolean(feeAmount) && tickUpper === tickSpaceLimits.UPPER, }), - [tickSpaceLimits, tickLower, tickUpper, feeAmount] + [tickSpaceLimits, tickLower, tickUpper, feeAmount], ); // mark invalid range @@ -404,7 +404,7 @@ export function useDerivedMintInfo( // liquidity range warning const outOfRange = Boolean( - !invalidRange && price && lowerPrice && upperPrice && (price.lessThan(lowerPrice) || price.greaterThan(upperPrice)) + !invalidRange && price && lowerPrice && upperPrice && (price.lessThan(lowerPrice) || price.greaterThan(upperPrice)), ); const independentInputMode = independentField === Field.CURRENCY_A ? token0InputMode : token1InputMode; @@ -423,7 +423,7 @@ export function useDerivedMintInfo( const debouncedIndependentAmount: CurrencyAmount | undefined = tryParseAmount( debouncedTypedValue, - independentDisplayCurrency + independentDisplayCurrency, ); const needsConversion = enabledModules.BoostedPoolsModule && independentCurrency?.isBoosted && independentInputMode === "underlying"; @@ -431,7 +431,7 @@ export function useDerivedMintInfo( const { outputAmount: convertedAmount } = useBoostedConversion( needsConversion ? debouncedIndependentAmount : undefined, independentCurrency, - "underlying-to-boosted" + "underlying-to-boosted", ); const independentAmount = needsConversion ? convertedAmount : debouncedIndependentAmount; @@ -485,7 +485,7 @@ export function useDerivedMintInfo( const { outputAmount: convertedDependentAmount } = useBoostedConversion( needsDependentConversion ? dependentAmount : undefined, dependentCurrency, - "boosted-to-underlying" + "boosted-to-underlying", ); const displayDependentAmount = needsDependentConversion ? convertedDependentAmount : dependentAmount; @@ -517,13 +517,13 @@ export function useDerivedMintInfo( invalidRange || Boolean( (deposit0Disabled && poolForPosition && tokenA && poolForPosition.token0.equals(tokenA)) || - (deposit1Disabled && poolForPosition && tokenA && poolForPosition.token1.equals(tokenA)) + (deposit1Disabled && poolForPosition && tokenA && poolForPosition.token1.equals(tokenA)), ); const depositBDisabled = invalidRange || Boolean( (deposit0Disabled && poolForPosition && tokenB && poolForPosition.token0.equals(tokenB)) || - (deposit1Disabled && poolForPosition && tokenB && poolForPosition.token1.equals(tokenB)) + (deposit1Disabled && poolForPosition && tokenB && poolForPosition.token1.equals(tokenB)), ); // create position entity based on users selection @@ -655,7 +655,7 @@ export function useRangeHopCallbacks( tickSpacing: number, tickLower: number | undefined, tickUpper: number | undefined, - pool?: Pool | undefined | null + pool?: Pool | undefined | null, ) { const { actions: { setFullRange }, @@ -678,7 +678,7 @@ export function useRangeHopCallbacks( } return ""; }, - [baseToken, quoteToken, tickLower, tickSpacing, pool] + [baseToken, quoteToken, tickLower, tickSpacing, pool], ); const getIncrementLower = useCallback( @@ -694,7 +694,7 @@ export function useRangeHopCallbacks( } return ""; }, - [baseToken, quoteToken, tickLower, tickSpacing, pool] + [baseToken, quoteToken, tickLower, tickSpacing, pool], ); const getDecrementUpper = useCallback( @@ -710,7 +710,7 @@ export function useRangeHopCallbacks( } return ""; }, - [baseToken, quoteToken, tickUpper, tickSpacing, pool] + [baseToken, quoteToken, tickUpper, tickSpacing, pool], ); const getIncrementUpper = useCallback( @@ -726,7 +726,7 @@ export function useRangeHopCallbacks( } return ""; }, - [baseToken, quoteToken, tickUpper, tickSpacing, pool] + [baseToken, quoteToken, tickUpper, tickSpacing, pool], ); const getSetRange = useCallback( @@ -743,7 +743,7 @@ export function useRangeHopCallbacks( } return ["", ""]; }, - [baseToken, quoteToken, tickSpacing, pool] + [baseToken, quoteToken, tickSpacing, pool], ); const getSetFullRange = useCallback(() => setFullRange(), []); diff --git a/src/types/approve-state.ts b/src/types/approve-state.ts index 600b080a..fa3f74cf 100644 --- a/src/types/approve-state.ts +++ b/src/types/approve-state.ts @@ -1,6 +1,7 @@ export const ApprovalState = { UNKNOWN: "UNKNOWN", NOT_APPROVED: "NOT_APPROVED", + RESET_REQUIRED: "RESET_REQUIRED", PENDING: "PENDING", APPROVED: "APPROVED", }; diff --git a/src/types/formatted-position.ts b/src/types/formatted-position.ts index b886d98d..19765e06 100644 --- a/src/types/formatted-position.ts +++ b/src/types/formatted-position.ts @@ -3,6 +3,7 @@ import { Position } from "@cryptoalgebra/integral-sdk"; export interface FormattedPosition { id: string; outOfRange: boolean; + isClosed: boolean; range: string; liquidityUSD: number; feesUSD: number | null; diff --git a/yarn.lock b/yarn.lock index f1736290..74be881d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1475,22 +1475,22 @@ tiny-warning "^1.0.3" toformat "^2.0.0" -"@cryptoalgebra/integral-omega-router-sdk@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@cryptoalgebra/integral-omega-router-sdk/-/integral-omega-router-sdk-1.0.1.tgz#67d355087d14521bed82ae871d77ccd819383a66" - integrity sha512-1bsNLX01yEO+M4qVHLDppw/c1p8i6aB/KgCfXO3uMmmCtaGBoZFBXsa13haytw2Rl5iQ2drOsA9CBiSaTURhag== +"@cryptoalgebra/integral-omega-router-sdk@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@cryptoalgebra/integral-omega-router-sdk/-/integral-omega-router-sdk-1.0.2.tgz#4fac626439af93de32591d8bbbf70ff30d610507" + integrity sha512-E3WYszMSiJxER0Y0u4Pk1B1YW31+Gu9EjP6VUBTgDTx3kiSRD6MHQYymhV/8i7R6E76ZkowtNu5XBfPv4WHGKw== dependencies: - "@cryptoalgebra/integral-sdk" "npm:@cryptoalgebra/integral-sdk@1.1.2" + "@cryptoalgebra/integral-sdk" "^1.1.3" "@uniswap/permit2-sdk" "^1.4.0" "@uniswap/sdk-core" "^7.10.0" "@uniswap/v2-sdk" "^4.16.0" "@uniswap/v3-sdk" "^3.26.0" viem "^2.31.3" -"@cryptoalgebra/integral-sdk@1.1.2", "@cryptoalgebra/integral-sdk@npm:@cryptoalgebra/integral-sdk@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@cryptoalgebra/integral-sdk/-/integral-sdk-1.1.2.tgz#9d036532829fc4c76df1c52971e2fd0e8c882b4c" - integrity sha512-f0yYtK2oeM/8VBSNkJEwdtYdbpMsJjQkmtbHbW7nqjaqpdEwk/NKkekXCliSqew+P9VvQ71+71pGt3YfKgBkMQ== +"@cryptoalgebra/integral-sdk@1.1.3", "@cryptoalgebra/integral-sdk@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@cryptoalgebra/integral-sdk/-/integral-sdk-1.1.3.tgz#4c58e3818a4351152ccf91dd9fa4a98a8ee2cb5c" + integrity sha512-Xf6mnPPQIf4BWp5HhbV2KPnUbBQe4gHu9k6qNKv3h6TDJdZAyYdT6DRqBADareI0LAjq3ojbPQBQWM+8CMNqeA== dependencies: "@ethersproject/abi" "^5.7.0" "@ethersproject/address" "^5.7.0"