diff --git a/README.md b/README.md index 20826336..4e73f5d1 100644 --- a/README.md +++ b/README.md @@ -176,17 +176,18 @@ UI configuration provider. Manages params to configure custom styling, component Custom components to be injected into widget layout -> | name | type | default value | description | -> | ------------------- | ----------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- | -> | `GeoBlockAlert` | ComponentType | `` | Component replaces deposit button while `isGeoBlocked` config param is set to `true` | -> | `SanctionedAlert` | ComponentType | `` | Component replaces deposit button while `isSanctioned` config param is set to `true` | -> | `DepositMetaInfo` | ComponentType | `undefined` | Component is injected into deposit meta part of widget layout nearby TransactionOverviewDisclosure | -> | `WithdrawMetaInfo` | ComponentType | `undefined` | Component is injected into withdraw meta part of widget layout nearby WithdrawTransactionOverviewDisclosure | -> | `CustomDepositMeta` | ComponentType | `undefined` | Custom extra component injected above deposit meta section in the deposit tab panel (e.g., chart, info, etc.) | -> | `Image` | ComponentType | `` | Component optionally can be used to pass `nextjs` Image component to be used for assets rendering | -> | `LogoSpinner` | ComponentType> | `` | Component is injected into widget pending transaction overlay. Assume using of spinning animation | -> | `DepositTermsOfUse` | ComponentType | `undefined` | Component is injected into `TermsOfUseOverlay` to extend default terms of use statement points | -> | `ActionButton` | ComponentType | `` | Component overrides default `ActionButton` and has `ButtonProps` API | +> | name | type | default value | description | +> | ----------------------- | ----------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +> | `GeoBlockAlert` | ComponentType | `` | Component replaces deposit button while `isGeoBlocked` config param is set to `true` | +> | `SanctionedAlert` | ComponentType | `` | Component replaces deposit button while `isSanctioned` config param is set to `true` | +> | `MaxSupplyReachedAlert` | ComponentType | `` | Component rendered in the deposit meta when the expected total supply exceeds the max cap; warns that the deposit will not go through | +> | `DepositMetaInfo` | ComponentType | `undefined` | Component is injected into deposit meta part of widget layout nearby TransactionOverviewDisclosure | +> | `WithdrawMetaInfo` | ComponentType | `undefined` | Component is injected into withdraw meta part of widget layout nearby WithdrawTransactionOverviewDisclosure | +> | `CustomDepositMeta` | ComponentType | `undefined` | Custom extra component injected above deposit meta section in the deposit tab panel (e.g., chart, info, etc.) | +> | `Image` | ComponentType | `` | Component optionally can be used to pass `nextjs` Image component to be used for assets rendering | +> | `LogoSpinner` | ComponentType> | `` | Component is injected into widget pending transaction overlay. Assume using of spinning animation | +> | `DepositTermsOfUse` | ComponentType | `undefined` | Component is injected into `TermsOfUseOverlay` to extend default terms of use statement points | +> | `ActionButton` | ComponentType | `` | Component overrides default `ActionButton` and has `ButtonProps` API | ###### Source: `packages/trading-widget/src/trading-widget/providers/component-provider/component-provider.tsx` diff --git a/package.json b/package.json index c21e09f3..a31e2468 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dhedge/trading-widget", - "version": "4.3.0", + "version": "4.4.0", "license": "MIT", "type": "module", "main": "dist/index.cjs", diff --git a/src/core-kit/abi/pool-manager-logic.ts b/src/core-kit/abi/pool-manager-logic.ts index 0f242ca8..14380674 100644 --- a/src/core-kit/abi/pool-manager-logic.ts +++ b/src/core-kit/abi/pool-manager-logic.ts @@ -137,4 +137,30 @@ export const PoolManagerLogicAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'maxSupplyCap', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'poolFeeShareNumerator', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, ] as const diff --git a/src/core-kit/hooks/pool/multicall/use-pool-manager.static.ts b/src/core-kit/hooks/pool/multicall/use-pool-manager.static.ts index 2e0fb2c0..450b7b4b 100644 --- a/src/core-kit/hooks/pool/multicall/use-pool-manager.static.ts +++ b/src/core-kit/hooks/pool/multicall/use-pool-manager.static.ts @@ -38,6 +38,13 @@ const getContracts = ({ args: [account ?? AddressZero], chainId, }, + { + address, + abi: PoolManagerLogicAbi, + functionName: 'maxSupplyCap', + args: [], + chainId, + }, ] as const type Data = MulticallReturnType> @@ -46,6 +53,7 @@ const selector = (data: Data) => ({ getFeeIncreaseInfo: data[0].result, minDepositUSD: data[1].result, isMemberAllowed: data[2].result, + maxSupplyCapD18: data[3].result, }) export const usePoolManagerStatic = ({ diff --git a/src/core-kit/hooks/pool/multicall/use-pool.dynamic.ts b/src/core-kit/hooks/pool/multicall/use-pool.dynamic.ts index cd01a5e6..1ad738d3 100644 --- a/src/core-kit/hooks/pool/multicall/use-pool.dynamic.ts +++ b/src/core-kit/hooks/pool/multicall/use-pool.dynamic.ts @@ -35,8 +35,8 @@ const selector = ([tokenPrice, getFundSummary]: Data) => { return { tokenPrice: tokenPrice?.result?.toString(), - totalValue: summary?.totalFundValue?.toString(), - totalSupply: summary?.totalSupply?.toString(), + totalValueD18: summary?.totalFundValue?.toString(), + totalSupplyD18: summary?.totalSupply?.toString(), isPrivateVault: summary?.privatePool, performanceFee: summary?.performanceFeeNumerator?.toString(), streamingFee: summary?.managerFeeNumerator?.toString(), diff --git a/src/core-kit/hooks/pool/use-available-manager-fee.test.ts b/src/core-kit/hooks/pool/use-available-manager-fee.test.ts new file mode 100644 index 00000000..4c6ba2fd --- /dev/null +++ b/src/core-kit/hooks/pool/use-available-manager-fee.test.ts @@ -0,0 +1,82 @@ +import * as wagmi from 'wagmi' + +import { DEFAULT_PRECISION, optimism } from 'core-kit/const' +import * as multicallHooks from 'core-kit/hooks/pool/multicall' +import { useAvailableManagerFee } from 'core-kit/hooks/pool/use-available-manager-fee' +import * as stateHooks from 'core-kit/hooks/state' +import { shiftBy } from 'core-kit/utils' +import { TEST_ADDRESS } from 'tests/mocks' +import { renderHook } from 'tests/test-utils' + +const toD18String = (value: string | number) => + shiftBy(value, DEFAULT_PRECISION) + +vi.mock('core-kit/hooks/state', () => ({ + useTradingPanelPoolConfig: vi.fn(), +})) + +vi.mock('core-kit/hooks/pool/multicall', () => ({ + usePoolManagerStatic: vi.fn(), + usePoolDynamic: vi.fn(), +})) + +vi.mock('wagmi', async () => { + const actual = await vi.importActual>('wagmi') + return { + ...actual, + useReadContract: vi.fn(), + } +}) + +describe('useAvailableManagerFee', () => { + beforeEach(() => { + vi.mocked(stateHooks.useTradingPanelPoolConfig).mockReturnValue({ + address: TEST_ADDRESS, + chainId: optimism.id, + } as unknown as ReturnType) + vi.mocked(wagmi.useReadContract).mockReset() + }) + + it('disables query when no cap or total value', () => { + vi.mocked(multicallHooks.usePoolManagerStatic).mockReturnValue({ + data: { maxSupplyCapD18: 0n }, + } as unknown as ReturnType) + vi.mocked(multicallHooks.usePoolDynamic).mockReturnValue({ + data: { totalValueD18: undefined }, + } as unknown as ReturnType) + vi.mocked(wagmi.useReadContract).mockImplementationOnce((args: any) => { + expect(args.query?.enabled).toBe(false) + return { + data: undefined, + } as unknown as ReturnType + }) + + const { result } = renderHook(() => useAvailableManagerFee()) + expect(result.current.data).toBeUndefined() + }) + + it('calls with totalValue and rounds the result up', () => { + vi.mocked(multicallHooks.usePoolManagerStatic).mockReturnValue({ + data: { maxSupplyCapD18: BigInt(toD18String(1000)) }, + } as unknown as ReturnType) + vi.mocked(multicallHooks.usePoolDynamic).mockReturnValue({ + data: { totalValueD18: toD18String(123.4567) }, + } as unknown as ReturnType) + + let capturedArgs: any + vi.mocked(wagmi.useReadContract).mockImplementationOnce((args: any) => { + capturedArgs = args + const raw = BigInt(toD18String(9.01)) + const selected = args.query?.select ? args.query.select(raw) : raw + return { + data: selected, + } as unknown as ReturnType + }) + + const { result } = renderHook(() => useAvailableManagerFee()) + expect(result.current.data).toBe(10) + expect(capturedArgs.functionName).toBe('calculateAvailableManagerFee') + expect(capturedArgs.args?.[0]).toBe(BigInt(toD18String(123.4567))) + expect(capturedArgs.query?.enabled).toBe(true) + }) +}) diff --git a/src/core-kit/hooks/pool/use-available-manager-fee.ts b/src/core-kit/hooks/pool/use-available-manager-fee.ts new file mode 100644 index 00000000..fce49e85 --- /dev/null +++ b/src/core-kit/hooks/pool/use-available-manager-fee.ts @@ -0,0 +1,32 @@ +import { useReadContract } from 'wagmi' + +import { PoolLogicAbi } from 'core-kit/abi' +import { + usePoolDynamic, + usePoolManagerStatic, +} from 'core-kit/hooks/pool/multicall' +import { useTradingPanelPoolConfig } from 'core-kit/hooks/state' +import { normalizeNumber } from 'core-kit/utils' + +const select = (data: bigint) => Math.ceil(normalizeNumber(data)) + +export const useAvailableManagerFee = () => { + const { address, chainId } = useTradingPanelPoolConfig() + const { data: { totalValueD18 } = {} } = usePoolDynamic({ address, chainId }) + const { data: { maxSupplyCapD18 } = {} } = usePoolManagerStatic({ + address, + chainId, + }) + + return useReadContract({ + address, + chainId, + abi: PoolLogicAbi, + functionName: 'calculateAvailableManagerFee', + args: [BigInt(totalValueD18 ?? '0')], + query: { + enabled: !!totalValueD18 && !!maxSupplyCapD18 && maxSupplyCapD18 > 0n, + select, + }, + }) +} diff --git a/src/core-kit/hooks/pool/use-pool-composition-with-fraction.test.ts b/src/core-kit/hooks/pool/use-pool-composition-with-fraction.test.ts index c74275db..66f3d30c 100644 --- a/src/core-kit/hooks/pool/use-pool-composition-with-fraction.test.ts +++ b/src/core-kit/hooks/pool/use-pool-composition-with-fraction.test.ts @@ -50,7 +50,7 @@ describe('formatPoolComposition', () => { formatPoolComposition({ composition: [poolComposition], vaultTokensAmount: 'assetAmount', - totalSupply: 'totalSupply', + totalSupplyD18: 'totalSupply', }), ).toEqual([ { @@ -95,7 +95,7 @@ describe('formatPoolComposition', () => { formatPoolComposition({ composition: [poolComposition, zeroAmountPoolComposition], vaultTokensAmount: 'assetAmount', - totalSupply: 'totalSupply', + totalSupplyD18: 'totalSupply', }), ).toEqual([ { @@ -128,7 +128,7 @@ describe('usePoolCompositionWithFraction', () => { }, } const vaultTokensAmount = '2' - const totalSupply = undefined + const totalSupplyD18 = undefined vi.mocked(poolHooks.usePoolComposition).mockImplementation(() => [ poolComposition, @@ -136,7 +136,7 @@ describe('usePoolCompositionWithFraction', () => { vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( () => ({ - data: { totalSupply }, + data: { totalSupplyD18 }, }) as unknown as ReturnType, ) @@ -164,7 +164,7 @@ describe('usePoolCompositionWithFraction', () => { }, } const vaultTokensAmount = '2' - const totalSupply = '10' + const totalSupplyD18 = '10' const fraction = 1 const fractionUsd = '2' @@ -176,7 +176,7 @@ describe('usePoolCompositionWithFraction', () => { vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( () => ({ - data: { totalSupply }, + data: { totalSupplyD18 }, }) as unknown as ReturnType, ) @@ -192,7 +192,7 @@ describe('usePoolCompositionWithFraction', () => { formatPoolComposition({ composition: [poolComposition], vaultTokensAmount: shiftBy(vaultTokensAmount || 0), - totalSupply: totalSupply.toString(), + totalSupplyD18: totalSupplyD18.toString(), }), ) }) diff --git a/src/core-kit/hooks/pool/use-pool-composition-with-fraction.ts b/src/core-kit/hooks/pool/use-pool-composition-with-fraction.ts index 1c94825f..eef04023 100644 --- a/src/core-kit/hooks/pool/use-pool-composition-with-fraction.ts +++ b/src/core-kit/hooks/pool/use-pool-composition-with-fraction.ts @@ -19,7 +19,7 @@ import { interface PoolCompositionParams { composition: PoolComposition[] vaultTokensAmount: string - totalSupply: string + totalSupplyD18: string } type PoolCompositionWithFractionParams = PoolContractCallParams & { @@ -30,7 +30,7 @@ type PoolCompositionWithFractionParams = PoolContractCallParams & { export const formatPoolComposition = ({ composition, vaultTokensAmount, - totalSupply, + totalSupplyD18, }: PoolCompositionParams): PoolCompositionWithFraction[] => composition .reduce((acc, asset) => { @@ -58,7 +58,7 @@ export const formatPoolComposition = ({ const fraction = getPoolFraction( asset.amount, vaultTokensAmount, - totalSupply, + totalSupplyD18, asset.precision, ) const fractionUsd = getPoolFraction( @@ -67,7 +67,7 @@ export const formatPoolComposition = ({ .shiftedBy(-asset.precision) .toFixed(), vaultTokensAmount, - totalSupply, + totalSupplyD18, ) return { ...asset, @@ -85,16 +85,16 @@ export const usePoolCompositionWithFraction = ({ chainId, }: PoolCompositionWithFractionParams) => { const poolComposition = usePoolComposition({ address, chainId }) - const { data: { totalSupply } = {} } = usePoolDynamic({ address, chainId }) + const { data: { totalSupplyD18 } = {} } = usePoolDynamic({ address, chainId }) return useMemo(() => { - if (!totalSupply) { + if (!totalSupplyD18) { return [] } return formatPoolComposition({ composition: poolComposition, vaultTokensAmount: shiftBy(vaultTokensAmount || 0), - totalSupply, + totalSupplyD18, }) - }, [vaultTokensAmount, poolComposition, totalSupply]) + }, [vaultTokensAmount, poolComposition, totalSupplyD18]) } diff --git a/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.test.ts b/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.test.ts new file mode 100644 index 00000000..8df17998 --- /dev/null +++ b/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.test.ts @@ -0,0 +1,191 @@ +import { DEFAULT_PRECISION, optimism } from 'core-kit/const' +import * as poolHooks from 'core-kit/hooks/pool' +import * as multicallHooks from 'core-kit/hooks/pool/multicall' +import * as availableFeeHook from 'core-kit/hooks/pool/use-available-manager-fee' +import * as stateHooks from 'core-kit/hooks/state' +import { useIsMaxSupplyCapReached } from 'core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached' +import { formatToUsd, shiftBy } from 'core-kit/utils' +import { TEST_ADDRESS } from 'tests/mocks' +import { renderHook } from 'tests/test-utils' + +vi.mock('core-kit/hooks/state', () => ({ + useTradingPanelPoolConfig: vi.fn(), + useReceiveTokenInput: vi.fn(), +})) + +vi.mock('core-kit/hooks/pool/multicall', () => ({ + usePoolManagerStatic: vi.fn(), + usePoolDynamic: vi.fn(), +})) + +vi.mock('core-kit/hooks/pool', () => ({ + usePoolTokenPrice: vi.fn(), +})) + +vi.mock('core-kit/hooks/pool/use-available-manager-fee', () => ({ + useAvailableManagerFee: vi.fn(), +})) + +const toD18String = (value: string | number) => + shiftBy(value, DEFAULT_PRECISION) + +describe('useIsMaxSupplyCapReached', () => { + beforeEach(() => { + vi.mocked(stateHooks.useTradingPanelPoolConfig).mockImplementation( + () => + ({ address: TEST_ADDRESS, chainId: optimism.id }) as ReturnType< + typeof stateHooks.useTradingPanelPoolConfig + >, + ) + + // default token price = $1 + vi.mocked(poolHooks.usePoolTokenPrice).mockReturnValue('1') + + // default available manager fee = 100 tokens + vi.mocked(availableFeeHook.useAvailableManagerFee).mockReturnValue({ + data: 100, + } as unknown as ReturnType) + }) + + it('returns false when maxSupplyCapD18 is zero', () => { + vi.mocked(multicallHooks.usePoolManagerStatic).mockImplementation( + () => + ({ data: { maxSupplyCapD18: 0n } }) as ReturnType< + typeof multicallHooks.usePoolManagerStatic + >, + ) + vi.mocked(multicallHooks.usePoolDynamic).mockImplementation( + () => + ({ data: { totalSupplyD18: '0' } }) as ReturnType< + typeof multicallHooks.usePoolDynamic + >, + ) + vi.mocked(stateHooks.useReceiveTokenInput).mockImplementation( + () => + [{ value: '0' }, vi.fn()] as unknown as ReturnType< + typeof stateHooks.useReceiveTokenInput + >, + ) + + const { result } = renderHook(() => useIsMaxSupplyCapReached()) + expect(result.current.isMaxSupplyCapReached).toBe(false) + expect(result.current.supplyCapInUsd).toBe('') + }) + + it('returns false when expected total supply is below cap', () => { + // total = 1, deposit = 0.5, cap = 101.6 (effective 1.6) + vi.mocked(multicallHooks.usePoolManagerStatic).mockImplementation( + () => + ({ + data: { maxSupplyCapD18: BigInt(toD18String(101.6)) }, + }) as ReturnType, + ) + vi.mocked(multicallHooks.usePoolDynamic).mockImplementation( + () => + ({ data: { totalSupplyD18: toD18String(1) } }) as ReturnType< + typeof multicallHooks.usePoolDynamic + >, + ) + vi.mocked(stateHooks.useReceiveTokenInput).mockImplementation( + () => + [{ value: '0.5' }, vi.fn()] as unknown as ReturnType< + typeof stateHooks.useReceiveTokenInput + >, + ) + + const { result } = renderHook(() => useIsMaxSupplyCapReached()) + expect(result.current.isMaxSupplyCapReached).toBe(false) + }) + + it('computes supplyCapInUsd based on remaining cap and token price', () => { + // remaining tokens = (101.5 - 100) - 1.0 = 0.5; price = $2000 → $1,000 + vi.mocked(multicallHooks.usePoolManagerStatic).mockImplementation( + () => + ({ + data: { maxSupplyCapD18: BigInt(toD18String(101.5)) }, + }) as ReturnType, + ) + vi.mocked(multicallHooks.usePoolDynamic).mockImplementation( + () => + ({ data: { totalSupplyD18: toD18String(1) } }) as ReturnType< + typeof multicallHooks.usePoolDynamic + >, + ) + vi.mocked(stateHooks.useReceiveTokenInput).mockImplementation( + () => + [{ value: '0' }, vi.fn()] as unknown as ReturnType< + typeof stateHooks.useReceiveTokenInput + >, + ) + vi.mocked(poolHooks.usePoolTokenPrice).mockReturnValue('2000') + + const { result } = renderHook(() => useIsMaxSupplyCapReached()) + expect(result.current.isMaxSupplyCapReached).toBe(false) + expect(result.current.supplyCapInUsd).toBe( + formatToUsd({ + value: 1000, + maximumFractionDigits: 0, + minimumFractionDigits: 0, + }), + ) + }) + + it('returns $0 when total supply already exceeds cap', () => { + // remaining tokens = max(0, (101.0 - 100) - 1.2) = 0 → $0 + vi.mocked(multicallHooks.usePoolManagerStatic).mockImplementation( + () => + ({ data: { maxSupplyCapD18: BigInt(toD18String(101)) } }) as ReturnType< + typeof multicallHooks.usePoolManagerStatic + >, + ) + vi.mocked(multicallHooks.usePoolDynamic).mockImplementation( + () => + ({ data: { totalSupplyD18: toD18String(1.2) } }) as ReturnType< + typeof multicallHooks.usePoolDynamic + >, + ) + vi.mocked(stateHooks.useReceiveTokenInput).mockImplementation( + () => + [{ value: '0' }, vi.fn()] as unknown as ReturnType< + typeof stateHooks.useReceiveTokenInput + >, + ) + vi.mocked(poolHooks.usePoolTokenPrice).mockReturnValue('123.45') + + const { result } = renderHook(() => useIsMaxSupplyCapReached()) + expect(result.current.isMaxSupplyCapReached).toBe(true) + expect(result.current.supplyCapInUsd).toBe( + formatToUsd({ + value: 0, + maximumFractionDigits: 0, + minimumFractionDigits: 0, + }), + ) + }) + + it('returns true when expected total supply exceeds cap', () => { + // total = 1, deposit = 0.7, cap = 101.6 (effective 1.6) → 1.7 > 1.6 + vi.mocked(multicallHooks.usePoolManagerStatic).mockImplementation( + () => + ({ + data: { maxSupplyCapD18: BigInt(toD18String(101.6)) }, + }) as ReturnType, + ) + vi.mocked(multicallHooks.usePoolDynamic).mockImplementation( + () => + ({ data: { totalSupplyD18: toD18String(1) } }) as ReturnType< + typeof multicallHooks.usePoolDynamic + >, + ) + vi.mocked(stateHooks.useReceiveTokenInput).mockImplementation( + () => + [{ value: '0.7' }, vi.fn()] as unknown as ReturnType< + typeof stateHooks.useReceiveTokenInput + >, + ) + vi.mocked(poolHooks.usePoolTokenPrice).mockReturnValue('1') + + const { result } = renderHook(() => useIsMaxSupplyCapReached()) + expect(result.current.isMaxSupplyCapReached).toBe(true) + }) +}) diff --git a/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.ts b/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.ts new file mode 100644 index 00000000..1c97c00b --- /dev/null +++ b/src/core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached.ts @@ -0,0 +1,47 @@ +import { usePoolTokenPrice } from 'core-kit/hooks/pool' +import { + usePoolDynamic, + usePoolManagerStatic, +} from 'core-kit/hooks/pool/multicall' +import { useAvailableManagerFee } from 'core-kit/hooks/pool/use-available-manager-fee' +import { + useReceiveTokenInput, + useTradingPanelPoolConfig, +} from 'core-kit/hooks/state' +import { formatToUsd, normalizeNumber } from 'core-kit/utils' + +export const useIsMaxSupplyCapReached = () => { + const { address, chainId } = useTradingPanelPoolConfig() + const { data: { maxSupplyCapD18 } = {} } = usePoolManagerStatic({ + address, + chainId, + }) + const { data: { totalSupplyD18 } = {} } = usePoolDynamic({ address, chainId }) + const [receiveToken] = useReceiveTokenInput() + const tokenPrice = usePoolTokenPrice({ address, chainId }) + const { data: availableManagerFee = 0 } = useAvailableManagerFee() + + if (!maxSupplyCapD18 || maxSupplyCapD18 === 0n) { + return { isMaxSupplyCapReached: false, supplyCapInUsd: '' } + } + + const totalSupply = Math.ceil(normalizeNumber(totalSupplyD18 ?? 0)) + + const depositAmount = Number(receiveToken.value || '0') + const maxSupplyCap = normalizeNumber(maxSupplyCapD18) + + const effectiveMaxSupplyCap = Math.max(0, maxSupplyCap - availableManagerFee) + const remainingCapInTokens = Math.max(0, effectiveMaxSupplyCap - totalSupply) + const remainingCapInUsdNumber = remainingCapInTokens * Number(tokenPrice) + const supplyCapInUsd = formatToUsd({ + value: remainingCapInUsdNumber, + maximumFractionDigits: 0, + minimumFractionDigits: 0, + }) + + return { + isMaxSupplyCapReached: + Number(totalSupply) + Number(depositAmount) > effectiveMaxSupplyCap, + supplyCapInUsd, + } +} diff --git a/src/core-kit/hooks/user/use-flatmoney-points-user-balances.ts b/src/core-kit/hooks/user/use-flatmoney-points-user-balances.ts index 3fef9950..2ecec413 100644 --- a/src/core-kit/hooks/user/use-flatmoney-points-user-balances.ts +++ b/src/core-kit/hooks/user/use-flatmoney-points-user-balances.ts @@ -30,7 +30,7 @@ export const useFlatmoneyPointsUserBalances = } = useTradingPanelPoolConfig() const { account = AddressZero } = useAccount() const balance = useUserTokenBalance({ symbol, address: vaultAddress }) - const { data: { totalSupply } = {} } = usePoolDynamic({ + const { data: { totalSupplyD18 } = {} } = usePoolDynamic({ address: vaultAddress, chainId, }) @@ -48,10 +48,10 @@ export const useFlatmoneyPointsUserBalances = }) return useMemo(() => { - const userVaultPortionInPercents = totalSupply + const userVaultPortionInPercents = totalSupplyD18 ? new BigNumber(balance) .shiftedBy(DEFAULT_PRECISION) - .dividedBy(totalSupply) + .dividedBy(totalSupplyD18) : new BigNumber(0) const userPortionOfLockedPointsBalance = lockedVaultPointsBalance ? new BigNumber(lockedVaultPointsBalance.toString()) @@ -78,7 +78,7 @@ export const useFlatmoneyPointsUserBalances = isLoading, } }, [ - totalSupply, + totalSupplyD18, balance, lockedVaultPointsBalance, unlockTaxInPercents, diff --git a/src/core-kit/hooks/web3/use-invalidate-trading-queries.ts b/src/core-kit/hooks/web3/use-invalidate-trading-queries.ts index a8a43485..b8841313 100644 --- a/src/core-kit/hooks/web3/use-invalidate-trading-queries.ts +++ b/src/core-kit/hooks/web3/use-invalidate-trading-queries.ts @@ -18,6 +18,7 @@ const tradingContractCalls = [ 'getExitRemainingCooldown', 'tokenPrice', 'limitOrders', + 'calculateAvailableManagerFee', ] const allowanceContractCalls = ['allowance'] diff --git a/src/core-kit/utils/number.ts b/src/core-kit/utils/number.ts index 219420c2..d18a4f95 100644 --- a/src/core-kit/utils/number.ts +++ b/src/core-kit/utils/number.ts @@ -6,7 +6,7 @@ export const getPercent = (numerator: number, denominator: number) => (numerator / denominator) * 100 export const normalizeNumber = ( - value: string | number | BigNumber, + value: string | number | BigNumber | bigint, precision = DEFAULT_PRECISION, ): number => new BigNumber(value).shiftedBy(-precision).toNumber() diff --git a/src/trading-widget/components/common/alert/alert.tsx b/src/trading-widget/components/common/alert/alert.tsx index 05588b8f..1175a603 100644 --- a/src/trading-widget/components/common/alert/alert.tsx +++ b/src/trading-widget/components/common/alert/alert.tsx @@ -3,15 +3,25 @@ import type { FC, PropsWithChildren } from 'react' interface AlertProps { className?: string + type?: 'error' | 'warning' | 'info' } export const Alert: FC> = ({ className, children, + type = 'error', }) => (
diff --git a/src/trading-widget/components/deposit/meta/meta.tsx b/src/trading-widget/components/deposit/meta/meta.tsx index e1d06c24..205c2be1 100644 --- a/src/trading-widget/components/deposit/meta/meta.tsx +++ b/src/trading-widget/components/deposit/meta/meta.tsx @@ -1,15 +1,20 @@ import type { FC, PropsWithChildren } from 'react' +import { useIsMaxSupplyCapReached } from 'core-kit/hooks/trading/deposit-v2/use-is-max-supply-cap-reached' import { Layout } from 'trading-widget/components/common/layout' import { DepositTransactionOverviewDisclosure } from 'trading-widget/components/deposit/meta/transaction-disclosure/transaction-disclosure' import { useComponentContext } from 'trading-widget/providers/component-provider' export const DepositMeta: FC = ({ children }) => { - const { DepositMetaInfo } = useComponentContext() + const { DepositMetaInfo, MaxSupplyReachedAlert } = useComponentContext() + const { isMaxSupplyCapReached, supplyCapInUsd } = useIsMaxSupplyCapReached() return ( + {isMaxSupplyCapReached && !!MaxSupplyReachedAlert && ( + + )} {DepositMetaInfo && }
diff --git a/src/trading-widget/providers/component-provider/component-provider.defaults.tsx b/src/trading-widget/providers/component-provider/component-provider.defaults.tsx index 65e95d99..a6a6f943 100644 --- a/src/trading-widget/providers/component-provider/component-provider.defaults.tsx +++ b/src/trading-widget/providers/component-provider/component-provider.defaults.tsx @@ -5,28 +5,33 @@ import type { ComponentProviderProps } from 'trading-widget/providers/component- export const DEFAULT_COMPONENT_PROVIDER_COMPONENTS: ComponentProviderProps['config'] = { GeoBlockAlert: () => ( - - - Depositing is geo-blocked. - + + Depositing is geo-blocked. ), SanctionedAlert: () => ( - - + + Your address has been found on a sanctions list. Deposits are disabled. ), AvailableLiquidityAlert: ({ liquidityAmount }) => ( - -

- Liquidity is running low{' '} - {liquidityAmount ? `(${liquidityAmount})` : null} + +

+ {liquidityAmount + ? `${liquidityAmount} withdrawal liquidity` + : 'Liquidity is running low'}

-

- There is not enough capital to withdraw +

Sell a smaller amount

+
+ ), + MaxSupplyReachedAlert: ({ supplyCapInUsd }) => ( + +

Supply caps reached

+

+ Can only buy {supplyCapInUsd} more

), diff --git a/src/trading-widget/providers/component-provider/component-provider.types.ts b/src/trading-widget/providers/component-provider/component-provider.types.ts index 3d4eef0c..8f125b2e 100644 --- a/src/trading-widget/providers/component-provider/component-provider.types.ts +++ b/src/trading-widget/providers/component-provider/component-provider.types.ts @@ -23,6 +23,10 @@ export interface AvailableLiquidityAlertProps { liquidityAmount: string } +export interface MaxSupplyCapProps { + supplyCapInUsd: string +} + export interface ComponentProviderProps { config?: { GeoBlockAlert?: ComponentType @@ -35,5 +39,6 @@ export interface ComponentProviderProps { DepositTermsOfUse?: ComponentType ActionButton?: ComponentType> AvailableLiquidityAlert?: ComponentType + MaxSupplyReachedAlert?: ComponentType } } diff --git a/src/trading-widget/providers/theme-provider/theme-provider.tsx b/src/trading-widget/providers/theme-provider/theme-provider.tsx index 7be19fb9..1404967f 100644 --- a/src/trading-widget/providers/theme-provider/theme-provider.tsx +++ b/src/trading-widget/providers/theme-provider/theme-provider.tsx @@ -84,7 +84,7 @@ export const ThemeProvider: FC> = ({ }`, '--panel-warning-content-color': `${ - config?.global?.color?.colorTextWarning ?? COLORS.AMBER['400'] + config?.global?.color?.colorTextWarning ?? COLORS.WARNING }`, '--panel-success-content-color': `${ @@ -610,6 +610,29 @@ export const ThemeProvider: FC> = ({ config?.component?.tooltip?.color?.colorText ?? COLORS.WHITE.DEFAULT }`, + // alert + '--panel-alert-error-bg': `${ + config?.component?.alert?.color?.errorBg ?? `${COLORS.ERROR}4D` + }`, + '--panel-alert-warning-bg': `${ + config?.component?.alert?.color?.warningBg ?? `${COLORS.WARNING}4D` + }`, + '--panel-alert-info-bg': `${ + config?.component?.alert?.color?.infoBg ?? `${COLORS.INFO}4D` + }`, + '--panel-alert-error-color': `${ + config?.component?.alert?.color?.errorTextColor ?? + 'var(--panel-error-content-color)' + }`, + '--panel-alert-warning-color': `${ + config?.component?.alert?.color?.warningTextColor ?? + 'var(--panel-warning-content-color)' + }`, + '--panel-alert-info-color': `${ + config?.component?.alert?.color?.infoTextColor ?? + 'var(--panel-content-color)' + }`, + //switch //switch-color '--panel-switch-bg-checked': `${ diff --git a/src/trading-widget/providers/theme-provider/theme-provider.types.ts b/src/trading-widget/providers/theme-provider/theme-provider.types.ts index 11d43be4..cbed28df 100644 --- a/src/trading-widget/providers/theme-provider/theme-provider.types.ts +++ b/src/trading-widget/providers/theme-provider/theme-provider.types.ts @@ -67,6 +67,16 @@ export interface ThemeProviderConfigProps { radiusMd?: string } } + alert?: { + color?: { + errorBg?: string + warningBg?: string + infoBg?: string + errorTextColor?: string + warningTextColor?: string + infoTextColor?: string + } + } notification?: { color?: { colorBg?: string