diff --git a/package.json b/package.json index c43fdfe..0007a1f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "lodash.uniqby": "^4.7.0" }, "devDependencies": { + "@date-fns/utc": "^1.1.1", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", "@nx/devkit": "^17.1.3", diff --git a/packages/core-ui-kit/package.json b/packages/core-ui-kit/package.json index dc1b8e6..4546472 100644 --- a/packages/core-ui-kit/package.json +++ b/packages/core-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@dhedge/core-ui-kit", - "version": "2.0.8", + "version": "2.0.9", "description": "Core UI Kit", "type": "module", "main": "dist/index.js", diff --git a/packages/core-ui-kit/src/hooks/pool/index.ts b/packages/core-ui-kit/src/hooks/pool/index.ts index f2806b1..339c27c 100644 --- a/packages/core-ui-kit/src/hooks/pool/index.ts +++ b/packages/core-ui-kit/src/hooks/pool/index.ts @@ -11,3 +11,4 @@ export { usePoolFees } from './use-pool-fees' export { useSynthetixV3AssetBalance } from './synthetixV3/use-synthetix-v3-asset-balance' export { useTotalFundValueMutable } from './synthetixV3/use-total-funds-value-mutable' export { useInvalidatePoolContractData } from './use-invalidate-pool-contract-data' +export { usePoolsDynamic } from './multicall' diff --git a/packages/core-ui-kit/src/hooks/pool/multicall/index.ts b/packages/core-ui-kit/src/hooks/pool/multicall/index.ts index 3c90d07..9d53c34 100644 --- a/packages/core-ui-kit/src/hooks/pool/multicall/index.ts +++ b/packages/core-ui-kit/src/hooks/pool/multicall/index.ts @@ -1,4 +1,4 @@ export { usePoolStatic } from './use-pool.static' -export { usePoolDynamic } from './use-pool.dynamic' export { usePoolManagerStatic } from './use-pool-manager.static' export { usePoolManagerDynamic } from './use-pool-manager.dynamic' +export { usePoolsDynamic } from './use-pools.dynamic' diff --git a/packages/core-ui-kit/src/hooks/pool/multicall/use-pool-manager.dynamic.ts b/packages/core-ui-kit/src/hooks/pool/multicall/use-pool-manager.dynamic.ts index 663bb77..7e3f4ec 100644 --- a/packages/core-ui-kit/src/hooks/pool/multicall/use-pool-manager.dynamic.ts +++ b/packages/core-ui-kit/src/hooks/pool/multicall/use-pool-manager.dynamic.ts @@ -1,36 +1,65 @@ -import { PoolManagerLogicAbi } from 'abi' +import { PoolLogicAbi, PoolManagerLogicAbi } from 'abi' import { AddressZero } from 'const' import { useManagerLogicAddress } from 'hooks/pool/use-manager-logic-address' -import { useContractReadErrorLogging, useReadContracts } from 'hooks/web3' -import type { MulticallReturnType, PoolContractCallParams } from 'types' +import { + useAccount, + useContractReadErrorLogging, + useReadContracts, +} from 'hooks/web3' +import type { + Address, + MulticallReturnType, + PoolContractCallParams, +} from 'types' import { isZeroAddress } from 'utils' -const getContracts = ({ address, chainId }: PoolContractCallParams) => +type GetContractsParams = PoolContractCallParams & { + managerLogicAddress: Address + account: Address +} + +const getContracts = ({ + address, + chainId, + managerLogicAddress, + account, +}: GetContractsParams) => [ { - address, + address: managerLogicAddress, abi: PoolManagerLogicAbi, functionName: 'getFundComposition', chainId, }, + { + address, + abi: PoolLogicAbi, + functionName: 'getExitRemainingCooldown', + chainId, + args: [account], + }, ] as const type Data = MulticallReturnType> const selector = (data: Data) => ({ getFundComposition: data[0].result, + getExitRemainingCooldown: data[1].result, }) export const usePoolManagerDynamic = ({ address, chainId, }: PoolContractCallParams) => { + const { account = AddressZero } = useAccount() const managerLogicAddress = useManagerLogicAddress({ address, chainId }) const result = useReadContracts({ contracts: getContracts({ - address: managerLogicAddress ?? AddressZero, + address, chainId, + managerLogicAddress: managerLogicAddress ?? AddressZero, + account, }), query: { enabled: !!managerLogicAddress && !isZeroAddress(managerLogicAddress), diff --git a/packages/core-ui-kit/src/hooks/pool/multicall/use-pool.dynamic.ts b/packages/core-ui-kit/src/hooks/pool/multicall/use-pool.dynamic.ts deleted file mode 100644 index bcc432f..0000000 --- a/packages/core-ui-kit/src/hooks/pool/multicall/use-pool.dynamic.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PoolLogicAbi } from 'abi' -import { AddressZero } from 'const' -import { - useAccount, - useContractReadErrorLogging, - useReadContracts, -} from 'hooks/web3' -import type { - MulticallReturnType, - PoolContractAccountCallParams, - PoolContractCallParams, -} from 'types' -import { isZeroAddress } from 'utils' - -const getContracts = ({ - address, - chainId, - account, -}: PoolContractAccountCallParams) => - [ - { - address, - abi: PoolLogicAbi, - functionName: 'getFundSummary', - chainId, - }, - { - address, - abi: PoolLogicAbi, - functionName: 'tokenPrice', - chainId, - }, - { - address, - abi: PoolLogicAbi, - functionName: 'isMemberAllowed', - args: [account ?? AddressZero], - chainId, - }, - { - address, - abi: PoolLogicAbi, - functionName: 'getExitRemainingCooldown', - chainId, - args: [account ?? AddressZero], - }, - ] as const - -type Data = MulticallReturnType> - -const selector = (data: Data) => ({ - getFundSummary: data[0].result, - tokenPrice: data[1].result, - isMemberAllowed: data[2].result, - getExitRemainingCooldown: data[3].result, -}) - -export const usePoolDynamic = ({ - address, - chainId, -}: PoolContractCallParams) => { - const { account = AddressZero } = useAccount() - - const result = useReadContracts({ - contracts: getContracts({ address, chainId, account }), - query: { - enabled: !!address && !isZeroAddress(address), - select: selector, - }, - }) - - useContractReadErrorLogging({ error: result.error, status: result.status }) - - return result -} diff --git a/packages/core-ui-kit/src/hooks/pool/multicall/use-pools.dynamic.ts b/packages/core-ui-kit/src/hooks/pool/multicall/use-pools.dynamic.ts new file mode 100644 index 0000000..4df2e3c --- /dev/null +++ b/packages/core-ui-kit/src/hooks/pool/multicall/use-pools.dynamic.ts @@ -0,0 +1,98 @@ +import chunk from 'lodash.chunk' + +import { PoolLogicAbi } from 'abi' +import { AddressZero, DEFAULT_CHAIN_ID } from 'const' +import { useTradingPanelPoolConfigs } from 'hooks/state' +import { + useAccount, + useContractReadErrorLogging, + useReadContracts, +} from 'hooks/web3' +import type { + Address, + ContractFunctionReturnType, + DynamicPoolContractData, + PoolContractAccountCallParams, +} from 'types' + +const getPoolContracts = ({ + account, + chainId, + address, +}: PoolContractAccountCallParams) => + [ + { + address, + abi: PoolLogicAbi, + functionName: 'balanceOf', + chainId, + args: [account ?? AddressZero], + }, + { + address, + abi: PoolLogicAbi, + functionName: 'tokenPrice', + chainId, + args: [], + }, + { + address, + abi: PoolLogicAbi, + functionName: 'getFundSummary', + chainId, + }, + ] as const + +type GetFundSummary = ContractFunctionReturnType< + typeof PoolLogicAbi, + 'view', + 'getFundSummary' +> + +type PoolsMap = Record + +const getContracts = (pools: PoolContractAccountCallParams[]) => + pools.flatMap(getPoolContracts) + +const POOL_CHUNK_SIZE = getPoolContracts({ + account: AddressZero, + chainId: DEFAULT_CHAIN_ID, + address: AddressZero, +}).length + +export const usePoolsDynamic = () => { + const { account = AddressZero } = useAccount() + const pools = useTradingPanelPoolConfigs() + + const result = useReadContracts({ + contracts: getContracts(pools.map((pool) => ({ ...pool, account }))), + query: { + select: (data) => + chunk(data, POOL_CHUNK_SIZE).reduce( + (acc, [balanceOf, tokenPrice, getFundSummary], index) => { + const poolAddress = pools?.[index]?.address ?? AddressZero + const summary = getFundSummary?.result as GetFundSummary + + return { + ...acc, + [poolAddress]: { + userBalance: balanceOf?.result?.toString(), + tokenPrice: tokenPrice?.result?.toString(), + totalValue: summary?.totalFundValue?.toString(), + totalSupply: summary?.totalSupply?.toString(), + isPrivateVault: summary?.privatePool, + performanceFee: summary?.performanceFeeNumerator?.toString(), + streamingFee: summary?.managerFeeNumerator?.toString(), + entryFee: summary?.entryFeeNumerator?.toString(), + }, + } + }, + {}, + ), + }, + }) + + useContractReadErrorLogging({ error: result.error, status: result.status }) + + return result +} diff --git a/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.test.ts b/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.test.ts index 2606158..3bf719e 100644 --- a/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.test.ts +++ b/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.test.ts @@ -81,6 +81,6 @@ describe('usePoolTokenPriceMutable', () => { chainId, disabled: false, }) - expect(result.current).toEqual(1000000000000000000n) + expect(result.current).toEqual('1000000000000000000') }) }) diff --git a/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.ts b/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.ts index 7d0b0e1..1699b82 100644 --- a/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.ts +++ b/packages/core-ui-kit/src/hooks/pool/synthetixV3/use-pool-token-price-mutable.ts @@ -18,7 +18,7 @@ export const usePoolTokenPriceMutable = ({ address, chainId, disabled, -}: PoolTokenPriceParams): bigint | undefined => { +}: PoolTokenPriceParams): string | undefined => { const { data: [poolManagerLogic, totalSupply] = [] } = useReadContracts({ contracts: [ { @@ -62,11 +62,9 @@ export const usePoolTokenPriceMutable = ({ : null return totalFundValueMutable && totalSupplyWithManagerFee - ? BigInt( - new BigNumber(totalFundValueMutable) - .dividedBy(totalSupplyWithManagerFee.toFixed()) - .shiftedBy(DEFAULT_PRECISION) - .toFixed(0), - ) + ? new BigNumber(totalFundValueMutable) + .dividedBy(totalSupplyWithManagerFee.toFixed()) + .shiftedBy(DEFAULT_PRECISION) + .toFixed(0) : undefined } diff --git a/packages/core-ui-kit/src/hooks/pool/use-invalidate-pool-contract-data.ts b/packages/core-ui-kit/src/hooks/pool/use-invalidate-pool-contract-data.ts index d8e82ce..8407b01 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-invalidate-pool-contract-data.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-invalidate-pool-contract-data.ts @@ -1,19 +1,14 @@ -import { usePoolDynamic, usePoolManagerDynamic } from './multicall' +import { usePoolManagerDynamic } from './multicall' import { useTradingPanelPoolConfig } from '../state' import { useInvalidateOnBlock } from '../web3' export const useInvalidatePoolContractData = () => { const { address, chainId } = useTradingPanelPoolConfig() - const { queryKey: poolDynamicKey } = usePoolDynamic({ - address, - chainId, - }) const { queryKey: poolManagerDynamicKey } = usePoolManagerDynamic({ address, chainId, }) - useInvalidateOnBlock({ queryKey: poolDynamicKey }) useInvalidateOnBlock({ queryKey: poolManagerDynamicKey }) } diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-composition-with-fraction.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-composition-with-fraction.ts index f845062..5de4b52 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-composition-with-fraction.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-composition-with-fraction.ts @@ -100,7 +100,7 @@ export const usePoolCompositionWithFraction = ({ return formatPoolComposition({ composition: poolComposition, vaultTokensAmount: shiftBy(new BigNumber(vaultTokensAmount || 0)), - totalSupply: totalSupply.toString(), + totalSupply, }) }, [vaultTokensAmount, poolComposition, totalSupply]) } diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.test.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.test.ts index 7956e00..b47cee1 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.test.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.test.ts @@ -10,76 +10,34 @@ import { TEST_ADDRESS } from 'tests/mocks' import type { Address } from 'types' -import { - getDataFromSummary, - usePoolDynamicContractData, -} from './use-pool-dynamic-contract-data' +import { usePoolDynamicContractData } from './use-pool-dynamic-contract-data' vi.mock('hooks/pool/multicall', () => ({ - usePoolDynamic: vi.fn(), + usePoolsDynamic: vi.fn(), + usePoolManagerDynamic: vi.fn(), })) vi.mock('hooks/pool', () => ({ useManagerLogicAddress: vi.fn(), useTotalFundValueMutable: vi.fn(), })) -describe('getDataFromSummary', () => { - it('should parse summary data', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } - expect(getDataFromSummary(summary)).toEqual({ - isPrivate: summary.privatePool, - performanceFee: summary.performanceFeeNumerator.toString(), - streamingFee: summary.managerFeeNumerator.toString(), - totalSupply: summary.totalSupply.toString(), - totalValue: summary.totalFundValue.toString(), - entryFee: summary.entryFeeNumerator.toString(), - }) - }) -}) - describe('usePoolDynamicContractData', () => { - it('should call getExitRemainingCooldown and getFundSummary methods on PoolLogicAbi', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } - const exitCooldown = BigInt(1) + it('should call usePoolsDynamic and usePoolManagerDynamic', () => { const chainId = optimism.id - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(poolMulticallHooks.usePoolsDynamic).mockImplementation( () => ({ - data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: summary, - }, - isFetched: true, - }) as ReturnType, + data: {}, + }) as unknown as ReturnType, + ) + vi.mocked(poolMulticallHooks.usePoolManagerDynamic).mockImplementation( + () => + ({ + data: {}, + }) as unknown as ReturnType< + typeof poolMulticallHooks.usePoolManagerDynamic + >, ) renderHook(() => @@ -89,43 +47,46 @@ describe('usePoolDynamicContractData', () => { }), ) - expect(vi.mocked(poolMulticallHooks.usePoolDynamic)).toHaveBeenCalledTimes( + expect(vi.mocked(poolMulticallHooks.usePoolsDynamic)).toHaveBeenCalledTimes( 1, ) - expect(vi.mocked(poolMulticallHooks.usePoolDynamic)).toHaveBeenCalledWith({ - address: TEST_ADDRESS, - chainId, - }) + expect( + vi.mocked(poolMulticallHooks.usePoolManagerDynamic), + ).toHaveBeenCalledTimes(1) + expect( + vi.mocked(poolMulticallHooks.usePoolManagerDynamic), + ).toHaveBeenCalledWith({ address: TEST_ADDRESS, chainId }) }) it('should resolve positive cooldown data', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } const exitCooldown = BigInt(1) const chainId = optimism.id - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(poolMulticallHooks.usePoolsDynamic).mockImplementation( () => ({ data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: summary, + [TEST_ADDRESS]: { + userBalance: '1', + tokenPrice: '2', + totalValue: '3', + totalSupply: '4', + isPrivateVault: true, + performanceFee: '5', + streamingFee: '6', + entryFee: '7', + }, }, - isFetched: true, - }) as ReturnType, + }) as ReturnType, + ) + + vi.mocked(poolMulticallHooks.usePoolManagerDynamic).mockImplementation( + () => + ({ + data: { getExitRemainingCooldown: exitCooldown }, + }) as unknown as ReturnType< + typeof poolMulticallHooks.usePoolManagerDynamic + >, ) const { result } = renderHook(() => @@ -141,86 +102,36 @@ describe('usePoolDynamicContractData', () => { cooldownEndsInTime: formatDuration( intervalToDuration({ start: 0, end: Number(exitCooldown) * 1000 }), ), + userBalance: '1', + tokenPrice: '2', + totalValue: '3', + totalSupply: '4', + isPrivateVault: true, + performanceFee: '5', + streamingFee: '6', + entryFee: '7', }), ) }) it('should resolve zero cooldown data', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } const exitCooldown = undefined const chainId = optimism.id - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(poolMulticallHooks.usePoolsDynamic).mockImplementation( () => ({ - data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: summary, - }, - isFetched: true, - }) as ReturnType, + data: {}, + }) as ReturnType, ) - const { result } = renderHook(() => - usePoolDynamicContractData({ - address: TEST_ADDRESS, - chainId, - }), - ) - - expect(result.current).toEqual( - expect.objectContaining({ - cooldownActive: false, - cooldownEndsInTime: formatDuration( - intervalToDuration({ start: 0, end: 0 }), - ), - }), - ) - }) - - it('should return parsed fund summary data for non synthetix v3 vault', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } - const exitCooldown = undefined - const chainId = optimism.id - const isFetched = true - - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(poolMulticallHooks.usePoolManagerDynamic).mockImplementation( () => ({ - data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: summary, - }, - isFetched, - }) as ReturnType, + data: { getExitRemainingCooldown: exitCooldown }, + }) as unknown as ReturnType< + typeof poolMulticallHooks.usePoolManagerDynamic + >, ) const { result } = renderHook(() => @@ -230,39 +141,17 @@ describe('usePoolDynamicContractData', () => { }), ) - expect(poolHooks.useManagerLogicAddress).toHaveBeenCalledWith({ - address: TEST_ADDRESS, - chainId, - }) - - expect(poolHooks.useTotalFundValueMutable).toHaveBeenCalledWith( - expect.objectContaining({ disabled: true }), - ) - expect(result.current).toEqual( expect.objectContaining({ - ...getDataFromSummary(summary), - isFetched, + cooldownActive: false, + cooldownEndsInTime: formatDuration( + intervalToDuration({ start: 0, end: 0 }), + ), }), ) }) it('should return parsed fund summary data for synthetix v3 vault', () => { - const summary = { - creationTime: BigInt(0), - exitFeeDenominator: BigInt(0), - exitFeeNumerator: BigInt(0), - manager: TEST_ADDRESS, - managerFeeDenominator: BigInt(0), - managerFeeNumerator: BigInt(0), - managerName: 'managerName', - name: 'name', - performanceFeeNumerator: BigInt(0), - privatePool: true, - totalFundValue: BigInt(0), - totalSupply: BigInt(0), - entryFeeNumerator: BigInt(0), - } const exitCooldown = undefined const chainId = optimism.id const isFetched = true @@ -270,16 +159,34 @@ describe('usePoolDynamicContractData', () => { const managerLogicAddress = '0x123' as Address const customTotalFundValue = '1111111' - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(poolMulticallHooks.usePoolsDynamic).mockImplementation( () => ({ data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: summary, + [address]: { + userBalance: '1', + tokenPrice: '2', + totalValue: '3', + totalSupply: '4', + isPrivateVault: true, + performanceFee: '5', + streamingFee: '6', + entryFee: '7', + }, }, + }) as ReturnType, + ) + + vi.mocked(poolMulticallHooks.usePoolManagerDynamic).mockImplementation( + () => + ({ + data: { getExitRemainingCooldown: exitCooldown }, isFetched, - }) as ReturnType, + }) as unknown as ReturnType< + typeof poolMulticallHooks.usePoolManagerDynamic + >, ) + vi.mocked(poolHooks.useManagerLogicAddress).mockImplementationOnce( () => managerLogicAddress, ) @@ -305,7 +212,6 @@ describe('usePoolDynamicContractData', () => { expect(result.current).toEqual( expect.objectContaining({ - ...getDataFromSummary(summary), totalValue: customTotalFundValue, isFetched, }), diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.ts index ea171d5..8075e17 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-dynamic-contract-data.ts @@ -1,44 +1,10 @@ import { formatDuration, intervalToDuration } from 'date-fns' import { useManagerLogicAddress, useTotalFundValueMutable } from 'hooks/pool' -import { usePoolDynamic } from 'hooks/pool/multicall' +import { usePoolManagerDynamic, usePoolsDynamic } from 'hooks/pool/multicall' import type { Address, ChainId } from 'types/web3.types' import { isSynthetixV3Vault } from 'utils' -interface FundSummary { - creationTime: bigint - exitFeeDenominator: bigint - exitFeeNumerator: bigint - manager: Address - managerFeeDenominator: bigint - managerFeeNumerator: bigint - managerName: string - name: string - performanceFeeNumerator: bigint - privatePool: boolean - totalFundValue: bigint - totalSupply: bigint - entryFeeNumerator: bigint -} - -export const getDataFromSummary = (summary?: FundSummary) => { - const totalSupply = summary?.totalSupply?.toString() ?? '' - const totalValue = summary?.totalFundValue?.toString() ?? '' - const isPrivate = summary?.privatePool - const performanceFee = summary?.performanceFeeNumerator?.toString() ?? '' - const streamingFee = summary?.managerFeeNumerator?.toString() ?? '' - const entryFee = summary?.entryFeeNumerator?.toString() ?? '' - - return { - isPrivate, - performanceFee, - streamingFee, - totalSupply, - totalValue, - entryFee, - } -} - interface PoolDynamicContractDataParams { address: Address chainId: ChainId @@ -49,6 +15,12 @@ export const usePoolDynamicContractData = ({ chainId, }: PoolDynamicContractDataParams) => { const isSynthetixVault = isSynthetixV3Vault(address) + const { data: { getExitRemainingCooldown: exitCooldown } = {}, isFetched } = + usePoolManagerDynamic({ address, chainId }) + const { data: poolsMap } = usePoolsDynamic() + const dynamicPoolData = poolsMap?.[address] + + // logic related to Synthetix V3 vault const managerLogicAddress = useManagerLogicAddress({ address, chainId, @@ -59,16 +31,6 @@ export const usePoolDynamicContractData = ({ disabled: !isSynthetixVault, }) - const { - data: { - getExitRemainingCooldown: exitCooldown, - getFundSummary: fundSummary, - } = {}, - isFetched, - } = usePoolDynamic({ address, chainId }) - - const summary = getDataFromSummary(fundSummary) - const cooldown = exitCooldown ? Number(exitCooldown) * 1000 : 0 const cooldownEndsInTime = formatDuration( intervalToDuration({ start: 0, end: cooldown }), @@ -77,10 +39,10 @@ export const usePoolDynamicContractData = ({ return { cooldownActive: cooldown > 0, cooldownEndsInTime, - ...summary, + ...dynamicPoolData, totalValue: isSynthetixVault - ? totalFundValueMutable ?? summary.totalValue - : summary.totalValue, + ? totalFundValueMutable ?? dynamicPoolData?.totalValue + : dynamicPoolData?.totalValue, isFetched, } } diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-fees.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-fees.ts index ea185eb..112b2e5 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-fees.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-fees.ts @@ -13,8 +13,8 @@ interface PoolFeesParams { export const usePoolFees = ({ address, chainId }: PoolFeesParams) => { const { - performanceFee, - streamingFee, + performanceFee = '0', + streamingFee = '0', entryFee: poolEntryFee, } = usePoolDynamicContractData({ address, chainId }) const [easySwapperEntryFee] = useTradingPanelEntryFee() diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.test.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.test.ts index 985c21c..9227df2 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.test.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.test.ts @@ -1,5 +1,4 @@ import { DHEDGE_SYNTHETIX_V3_VAULT_ADDRESSES, optimism } from 'const' -import * as poolMulticallHooks from 'hooks/pool/multicall' import * as stateHooks from 'hooks/state' import { renderHook } from 'test-utils' import { TEST_ADDRESS } from 'tests/mocks' @@ -7,14 +6,16 @@ import { TEST_ADDRESS } from 'tests/mocks' import type { Address } from 'types' import { usePoolTokenPriceMutable } from './synthetixV3/use-pool-token-price-mutable' +import { usePoolDynamicContractData } from './use-pool-dynamic-contract-data' import { usePoolTokenPrice } from './use-pool-token-price' vi.mock('hooks/state', () => ({ useTradingPanelPoolFallbackData: vi.fn(), + useTradingPanelPoolConfigs: vi.fn(), })) -vi.mock('hooks/pool/multicall', () => ({ - usePoolDynamic: vi.fn(), +vi.mock('./use-pool-dynamic-contract-data', () => ({ + usePoolDynamicContractData: vi.fn(), })) vi.mock('./synthetixV3/use-pool-token-price-mutable', () => ({ usePoolTokenPriceMutable: vi.fn(), @@ -27,13 +28,15 @@ describe('usePoolTokenPrice', () => { const tokenPrice = BigInt(1) const poolData = { tokenPrice: '1' } - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(usePoolDynamicContractData).mockImplementationOnce( () => - ({ data: { tokenPrice } }) as ReturnType< - typeof poolMulticallHooks.usePoolDynamic + ({ data: { tokenPrice } }) as unknown as ReturnType< + typeof usePoolDynamicContractData >, ) - vi.mocked(stateHooks.useTradingPanelPoolFallbackData).mockImplementation( + vi.mocked( + stateHooks.useTradingPanelPoolFallbackData, + ).mockImplementationOnce( () => [poolData, vi.fn()] as unknown as ReturnType< typeof stateHooks.useTradingPanelPoolFallbackData @@ -47,8 +50,8 @@ describe('usePoolTokenPrice', () => { }), ) - expect(poolMulticallHooks.usePoolDynamic).toHaveBeenCalledTimes(1) - expect(poolMulticallHooks.usePoolDynamic).toHaveBeenCalledWith({ + expect(usePoolDynamicContractData).toHaveBeenCalledTimes(1) + expect(usePoolDynamicContractData).toHaveBeenCalledWith({ address, chainId, }) @@ -57,18 +60,20 @@ describe('usePoolTokenPrice', () => { it('should call usePoolTokenPriceMutable hook for synthetix v3 vault', () => { const address = DHEDGE_SYNTHETIX_V3_VAULT_ADDRESSES[0] as Address const chainId = optimism.id - const tokenPrice = BigInt(123) + const tokenPrice = '123' const poolData = { tokenPrice: '1' } const formatter = (price: bigint) => price.toString() - vi.mocked(usePoolTokenPriceMutable).mockImplementation(() => tokenPrice) - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(usePoolTokenPriceMutable).mockImplementationOnce(() => tokenPrice) + vi.mocked(usePoolDynamicContractData).mockImplementationOnce( () => - ({ data: { tokenPrice: undefined } }) as ReturnType< - typeof poolMulticallHooks.usePoolDynamic + ({ data: { tokenPrice: undefined } }) as unknown as ReturnType< + typeof usePoolDynamicContractData >, ) - vi.mocked(stateHooks.useTradingPanelPoolFallbackData).mockImplementation( + vi.mocked( + stateHooks.useTradingPanelPoolFallbackData, + ).mockImplementationOnce( () => [poolData, vi.fn()] as unknown as ReturnType< typeof stateHooks.useTradingPanelPoolFallbackData @@ -84,8 +89,8 @@ describe('usePoolTokenPrice', () => { }), ) - expect(poolMulticallHooks.usePoolDynamic).toHaveBeenCalledTimes(1) - expect(poolMulticallHooks.usePoolDynamic).toHaveBeenCalledWith({ + expect(usePoolDynamicContractData).toHaveBeenCalledTimes(1) + expect(usePoolDynamicContractData).toHaveBeenCalledWith({ address, chainId, }) @@ -94,23 +99,25 @@ describe('usePoolTokenPrice', () => { chainId, disabled: false, }) - expect(result.current).toEqual(formatter(tokenPrice)) + expect(result.current).toEqual(formatter(BigInt(tokenPrice))) }) it('should format contract token price', () => { const address = TEST_ADDRESS const chainId = optimism.id - const tokenPrice = BigInt(1) + const tokenPrice = '1' const poolData = { tokenPrice: '2' } const formatterMock = vi.fn() - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(usePoolDynamicContractData).mockImplementationOnce( () => - ({ data: { tokenPrice } }) as ReturnType< - typeof poolMulticallHooks.usePoolDynamic - >, + ({ + tokenPrice, + }) as unknown as ReturnType, ) - vi.mocked(stateHooks.useTradingPanelPoolFallbackData).mockImplementation( + vi.mocked( + stateHooks.useTradingPanelPoolFallbackData, + ).mockImplementationOnce( () => [poolData, vi.fn()] as unknown as ReturnType< typeof stateHooks.useTradingPanelPoolFallbackData @@ -125,7 +132,7 @@ describe('usePoolTokenPrice', () => { }), ) - expect(formatterMock).toHaveBeenCalledWith(tokenPrice) + expect(formatterMock).toHaveBeenCalledWith(BigInt(tokenPrice)) }) it('should format fallback poolData.tokenPrice', () => { @@ -135,13 +142,15 @@ describe('usePoolTokenPrice', () => { const poolData = { tokenPrice: BigInt(1) } const formatterMock = vi.fn() - vi.mocked(poolMulticallHooks.usePoolDynamic).mockImplementation( + vi.mocked(usePoolDynamicContractData).mockImplementationOnce( () => - ({ data: { tokenPrice } }) as ReturnType< - typeof poolMulticallHooks.usePoolDynamic + ({ data: { tokenPrice } }) as unknown as ReturnType< + typeof usePoolDynamicContractData >, ) - vi.mocked(stateHooks.useTradingPanelPoolFallbackData).mockImplementation( + vi.mocked( + stateHooks.useTradingPanelPoolFallbackData, + ).mockImplementationOnce( () => [poolData, vi.fn()] as unknown as ReturnType< typeof stateHooks.useTradingPanelPoolFallbackData diff --git a/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.ts b/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.ts index 435b67f..746f6fb 100644 --- a/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.ts +++ b/packages/core-ui-kit/src/hooks/pool/use-pool-token-price.ts @@ -1,9 +1,9 @@ -import { usePoolDynamic } from 'hooks/pool/multicall' import { useTradingPanelPoolFallbackData } from 'hooks/state' import type { Address, ChainId } from 'types/web3.types' import { formatEther, isSynthetixV3Vault } from 'utils' import { usePoolTokenPriceMutable } from './synthetixV3/use-pool-token-price-mutable' +import { usePoolDynamicContractData } from './use-pool-dynamic-contract-data' interface PoolTokenPriceParams { address: Address @@ -21,7 +21,7 @@ export const usePoolTokenPrice = ({ const [poolData] = useTradingPanelPoolFallbackData() const isSynthetixVault = isSynthetixV3Vault(address) - const { data: { tokenPrice } = {} } = usePoolDynamic({ address, chainId }) + const { tokenPrice } = usePoolDynamicContractData({ address, chainId }) const mutableTokenPrice = usePoolTokenPriceMutable({ address, @@ -31,5 +31,5 @@ export const usePoolTokenPrice = ({ const contractTokenPrice = isSynthetixVault ? mutableTokenPrice : tokenPrice - return formatter(contractTokenPrice ?? BigInt(poolData?.tokenPrice ?? '0')) + return formatter(BigInt(contractTokenPrice ?? poolData?.tokenPrice ?? '0')) } diff --git a/packages/core-ui-kit/src/hooks/trading/deposit/use-deposit-method-handler.ts b/packages/core-ui-kit/src/hooks/trading/deposit/use-deposit-method-handler.ts index 0a05326..1cb7bf7 100644 --- a/packages/core-ui-kit/src/hooks/trading/deposit/use-deposit-method-handler.ts +++ b/packages/core-ui-kit/src/hooks/trading/deposit/use-deposit-method-handler.ts @@ -23,7 +23,7 @@ export const useDepositMethodHandler = (): [ address, chainId, }) - const { entryFee } = usePoolDynamicContractData({ + const { entryFee = '0' } = usePoolDynamicContractData({ address, chainId, }) diff --git a/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.test.ts b/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.test.ts index 1d73816..5747f05 100644 --- a/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.test.ts +++ b/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.test.ts @@ -15,11 +15,11 @@ vi.mock('hooks/state', () => ({ useTradingPanelPoolConfig: vi.fn() })) describe('useShouldBeWhitelisted', () => { it('should not check whitelisting if vault is not private or deprecated', () => { - const isPrivate = false + const isPrivateVault = false const deprecated = false vi.mocked(poolHooks.usePoolDynamicContractData).mockImplementationOnce( () => - ({ isPrivate }) as ReturnType< + ({ isPrivateVault }) as ReturnType< typeof poolHooks.usePoolDynamicContractData >, ) @@ -53,12 +53,12 @@ describe('useShouldBeWhitelisted', () => { }) it('should check whitelisting if vault is private', () => { - const isPrivate = true + const isPrivateVault = true const deprecated = false const chainId = optimism.id vi.mocked(poolHooks.usePoolDynamicContractData).mockImplementationOnce( () => - ({ isPrivate }) as ReturnType< + ({ isPrivateVault }) as ReturnType< typeof poolHooks.usePoolDynamicContractData >, ) @@ -87,12 +87,12 @@ describe('useShouldBeWhitelisted', () => { }) it('should check whitelisting if vault is deprecated', () => { - const isPrivate = false + const isPrivateVault = false const deprecated = true const chainId = optimism.id vi.mocked(poolHooks.usePoolDynamicContractData).mockImplementationOnce( () => - ({ isPrivate }) as ReturnType< + ({ isPrivateVault }) as ReturnType< typeof poolHooks.usePoolDynamicContractData >, ) @@ -121,12 +121,12 @@ describe('useShouldBeWhitelisted', () => { }) it('should check whitelisting if vault is deprecated and private', () => { - const isPrivate = true + const isPrivateVault = true const deprecated = true const chainId = optimism.id vi.mocked(poolHooks.usePoolDynamicContractData).mockImplementationOnce( () => - ({ isPrivate }) as ReturnType< + ({ isPrivateVault }) as ReturnType< typeof poolHooks.usePoolDynamicContractData >, ) diff --git a/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.ts b/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.ts index 39c4f93..9fb15e0 100644 --- a/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.ts +++ b/packages/core-ui-kit/src/hooks/trading/deposit/use-should-be-whitelisted.ts @@ -3,9 +3,12 @@ import { useTradingPanelPoolConfig } from 'hooks/state' export const useShouldBeWhitelisted = () => { const { chainId, address, deprecated } = useTradingPanelPoolConfig() - const { isPrivate } = usePoolDynamicContractData({ address, chainId }) + const { isPrivateVault = false } = usePoolDynamicContractData({ + address, + chainId, + }) - const shouldBeWhitelisted = isPrivate || deprecated + const shouldBeWhitelisted = isPrivateVault || deprecated const isAccountWhitelisted = useCheckWhitelist({ address, chainId, diff --git a/packages/core-ui-kit/src/hooks/user/use-user-token-balance.ts b/packages/core-ui-kit/src/hooks/user/use-user-token-balance.ts index 060d72c..104e785 100644 --- a/packages/core-ui-kit/src/hooks/user/use-user-token-balance.ts +++ b/packages/core-ui-kit/src/hooks/user/use-user-token-balance.ts @@ -48,11 +48,13 @@ export const useUserTokenBalance = ({ address, abi: erc20Abi, functionName: 'balanceOf', + chainId: poolConfig.chainId, args: [account ?? AddressZero], }, { address, abi: erc20Abi, + chainId: poolConfig.chainId, functionName: 'decimals', }, ], diff --git a/packages/core-ui-kit/src/tests/test-utils.tsx b/packages/core-ui-kit/src/tests/test-utils.tsx index 71f1298..7ca12be 100644 --- a/packages/core-ui-kit/src/tests/test-utils.tsx +++ b/packages/core-ui-kit/src/tests/test-utils.tsx @@ -1,16 +1,17 @@ import { - dehydrate, + HydrationBoundary, QueryClient, QueryClientProvider, - HydrationBoundary, + dehydrate, } from '@tanstack/react-query' import type { Queries, queries } from '@testing-library/dom' import type { RenderHookOptions, RenderOptions } from '@testing-library/react' import { render, renderHook } from '@testing-library/react' import type { ReactElement, ReactNode } from 'react' -import { WagmiProvider } from 'providers/wagmi-provider' import { TradingPanelProvider } from 'providers' +import { WagmiProvider } from 'providers/wagmi-provider' + import type { TradingPanelContextConfig } from 'types' import { CALLBACK_CONFIG_MOCK, POOL_CONFIG_MAP_MOCK } from './mocks' diff --git a/packages/core-ui-kit/src/types/web3.types.ts b/packages/core-ui-kit/src/types/web3.types.ts index 9634a42..0d5381f 100644 --- a/packages/core-ui-kit/src/types/web3.types.ts +++ b/packages/core-ui-kit/src/types/web3.types.ts @@ -16,6 +16,7 @@ export type { WaitForTransactionReceiptReturnType, CallExecutionError, MulticallReturnType, + ContractFunctionReturnType, } from 'viem' export type { Address, Chain } export type ChainId = Chain['id'] @@ -63,3 +64,14 @@ export interface PoolContractCallParams { export interface PoolContractAccountCallParams extends PoolContractCallParams { account: Address } + +export interface DynamicPoolContractData { + userBalance: string | undefined + tokenPrice: string | undefined + totalValue: string | undefined + totalSupply: string | undefined + isPrivateVault: boolean | undefined + performanceFee: string | undefined + streamingFee: string | undefined + entryFee: string | undefined +} diff --git a/packages/trading-widget/src/components/common/button/action-button/action-button.tsx b/packages/trading-widget/src/components/common/button/action-button/action-button.tsx new file mode 100644 index 0000000..4767b85 --- /dev/null +++ b/packages/trading-widget/src/components/common/button/action-button/action-button.tsx @@ -0,0 +1,62 @@ +import classNames from 'classnames' +import type { FC, PropsWithChildren } from 'react' + +interface ActionButtonProps { + onClick?: () => void + highlighted?: boolean + disabled?: boolean + className?: string + type?: 'submit' | 'button' +} + +const NON_HIGHLIGHTED_CLASSNAMES = [ + 'dtw-bg-transparent', + 'dtw-border-[var(--panel-action-outline-button-border-color,var(--panel-border-color))]', + 'dtw-text-[color:var(--panel-action-outline-button-color,var(--panel-content-color))]', + 'active:dtw-border-opacity-100', + 'hover:enabled:dtw-border-[var(--panel-action-outline-button-border-hover-color)]', +] + +const HIGHLIGHTED_CLASSNAMES = [ + 'dtw-text-[color:var(--panel-action-accent-button-color,var(--panel-accent-content-color))]', + 'dtw-bg-gradient-to-r', + 'dtw-from-[var(--panel-action-accent-button-bg-from,var(--panel-accent-from-color))]', + 'dtw-to-[var(--panel-action-accent-button-bg-to,var(--panel-accent-to-color))]', + 'dtw-border-[color:var(--panel-action-accent-button-border-color)]', + 'dtw-border-[length:var(--panel-action-accent-button-border-width)]', + 'hover:enabled:dtw-from-[var(--panel-action-accent-button-hover-bg-from,var(--panel-accent-hover-from-color))]', + 'hover:enabled:dtw-to-[var(--panel-action-accent-button-hover-bg-to,var(--panel-accent-hover-to-color))]', +] + +export const ActionButton: FC> = ({ + children, + onClick, + highlighted = false, + disabled = false, + className, + type, +}) => ( + +) diff --git a/packages/trading-widget/src/components/common/button/disabled-button-with-prompt/disabled-button-with-prompt.tsx b/packages/trading-widget/src/components/common/button/disabled-button-with-prompt/disabled-button-with-prompt.tsx new file mode 100644 index 0000000..d94a797 --- /dev/null +++ b/packages/trading-widget/src/components/common/button/disabled-button-with-prompt/disabled-button-with-prompt.tsx @@ -0,0 +1,34 @@ +import { + ExclamationCircleIcon, + LockClosedIcon, +} from '@heroicons/react/24/outline' + +import type { FC, PropsWithChildren } from 'react' + +import { InfoTooltip } from '../../tooltip/info-tooltip/info-tooltip' +import { ActionButton } from '../action-button/action-button' + +interface DisabledButtonWithPromptProps { + promptText: string +} + +export const DisabledButtonWithPrompt: FC< + PropsWithChildren +> = ({ children, promptText }) => { + return ( +
+
+ +
{promptText}
+
+ + +
+ + {children} +
+
+
+
+ ) +} diff --git a/packages/trading-widget/src/components/common/index.ts b/packages/trading-widget/src/components/common/index.ts index e288959..03cb05f 100644 --- a/packages/trading-widget/src/components/common/index.ts +++ b/packages/trading-widget/src/components/common/index.ts @@ -6,3 +6,5 @@ export { TransactionOverviewDisclosure } from './meta/transaction-disclosure/tra export type { TransactionDisclosureItemProps } from './meta/transaction-disclosure/transaction-disclosure-item/transaction-disclosure-item' export { Spinner } from './spinner/spinner' export { Layout } from './layout' +export { ActionButton } from './button/action-button/action-button' +export { DisabledButtonWithPrompt } from './button/disabled-button-with-prompt/disabled-button-with-prompt' diff --git a/packages/trading-widget/src/components/common/meta/transaction-disclosure/transaction-disclosure.tsx b/packages/trading-widget/src/components/common/meta/transaction-disclosure/transaction-disclosure.tsx index ac44c0c..1d4b6fd 100644 --- a/packages/trading-widget/src/components/common/meta/transaction-disclosure/transaction-disclosure.tsx +++ b/packages/trading-widget/src/components/common/meta/transaction-disclosure/transaction-disclosure.tsx @@ -3,6 +3,8 @@ import { ChevronDownIcon } from '@heroicons/react/24/solid' import classNames from 'classnames' import type { FC, PropsWithChildren } from 'react' +import { useTranslationContext } from 'providers/translation-provider' + import { THEME_TYPE } from 'types' import type { ThemeType } from 'types' @@ -23,48 +25,52 @@ export const TransactionOverviewDisclosure: FC< staticItems, collapseItems, themeType = THEME_TYPE.DEFAULT, -}) => ( -
- - {({ open }) => ( - <> - {staticItems?.map((props) => ( - - ))} - - { + const t = useTranslationContext() + + return ( +
+ + {({ open }) => ( + <> + {staticItems?.map((props) => ( + + ))} + + + + + + + - - - - - - - {collapseItems?.map((props) => ( - - ))} - {children} - - - - )} - -
-) + + {collapseItems?.map((props) => ( + + ))} + {children} + + + + )} +
+
+ ) +} diff --git a/packages/trading-widget/src/components/deposit/button/trade-button/trade-button.tsx b/packages/trading-widget/src/components/deposit/button/trade-button/trade-button.tsx new file mode 100644 index 0000000..ed2ff73 --- /dev/null +++ b/packages/trading-widget/src/components/deposit/button/trade-button/trade-button.tsx @@ -0,0 +1,16 @@ +import { useHandleTrade } from '@dhedge/core-ui-kit/hooks/trading' +import { useDeposit } from '@dhedge/core-ui-kit/hooks/trading/deposit' +import type { FC } from 'react' + +import { ActionButton } from 'components/common' + +export const DepositTradeButton: FC = () => { + const deposit = useDeposit() + const { disabled, label, handleTrade } = useHandleTrade(deposit) + + return ( + + {label} + + ) +} diff --git a/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.hooks.ts b/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.hooks.ts new file mode 100644 index 0000000..f467057 --- /dev/null +++ b/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.hooks.ts @@ -0,0 +1,68 @@ +import { + usePoolDynamicContractData, + usePoolManagerLogicData, + usePoolTokenPrice, +} from '@dhedge/core-ui-kit/hooks/pool' +import { + useReceiveTokenInput, + useSendTokenInput, + useTradingPanelPoolConfig, +} from '@dhedge/core-ui-kit/hooks/state' +import { useSynthetixV3OraclesUpdate } from '@dhedge/core-ui-kit/hooks/trading' +import { + useDepositAllowance, + useShouldBeWhitelisted, +} from '@dhedge/core-ui-kit/hooks/trading/deposit' +import { normalizeNumber } from '@dhedge/core-ui-kit/utils' + +import BigNumber from 'bignumber.js' + +import { useHighSlippageCheck } from 'hooks' + +export const useValidDepositButton = () => { + const { address, chainId, deprecated, symbol } = useTradingPanelPoolConfig() + const [receiveToken] = useReceiveTokenInput() + const [sendToken] = useSendTokenInput() + + const { shouldBeWhitelisted, isAccountWhitelisted } = useShouldBeWhitelisted() + const poolTokenPrice = usePoolTokenPrice({ address, chainId }) + const { minDepositUSD } = usePoolManagerLogicData(address, chainId) + const { userBalance } = usePoolDynamicContractData({ + address, + chainId, + }) + const { approve, canSpend } = useDepositAllowance() + const { needToBeUpdated, updateOracles } = useSynthetixV3OraclesUpdate({ + disabled: !canSpend, + }) + const { requiresHighSlippageConfirm, confirmHighSlippage, slippageToBeUsed } = + useHighSlippageCheck() + + const depositValueInUsd = new BigNumber( + receiveToken.value || '0', + ).multipliedBy(poolTokenPrice || '0') + + const poolBalanceInUsdNumber = + normalizeNumber(userBalance ?? 0) * normalizeNumber(poolTokenPrice ?? 0) + + const isLowerThanMinDeposit = + poolBalanceInUsdNumber < minDepositUSD || + depositValueInUsd.lt(minDepositUSD) + + return { + requiresMinDeposit: + receiveToken.value === '0' ? false : isLowerThanMinDeposit, + requiresWhitelist: shouldBeWhitelisted && !isAccountWhitelisted, + requiresApprove: !canSpend, + requiresUpdate: needToBeUpdated && !!sendToken.value, + requiresHighSlippageConfirm, + sendTokenSymbol: sendToken.symbol, + poolSymbol: symbol, + minDepositUSD, + deprecated, + approve, + confirmHighSlippage, + updateOracles, + slippageToBeUsed, + } +} diff --git a/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.tsx b/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.tsx new file mode 100644 index 0000000..e774de1 --- /dev/null +++ b/packages/trading-widget/src/components/deposit/button/valid-deposit-button/valid-deposit-button.tsx @@ -0,0 +1,69 @@ +import { commify } from '@dhedge/core-ui-kit/utils' +import type { FC, PropsWithChildren } from 'react' + +import { ActionButton, DisabledButtonWithPrompt } from 'components/common' +import { ApproveButton } from 'components/widget/widget-buttons' + +import { useValidDepositButton } from './valid-deposit-button.hooks' + +export const ValidDepositButton: FC = ({ children }) => { + const { + requiresMinDeposit, + minDepositUSD, + requiresApprove, + requiresWhitelist, + requiresUpdate, + requiresHighSlippageConfirm, + deprecated = false, + poolSymbol, + sendTokenSymbol, + slippageToBeUsed, + updateOracles, + approve, + confirmHighSlippage, + } = useValidDepositButton() + + if (requiresMinDeposit) { + return ( + {`Minimum purchase is $${commify( + minDepositUSD.toString() ?? '', + )}`} + ) + } + + if (requiresWhitelist) { + return ( + + Buy + + ) + } + + if (requiresApprove) { + return + } + + if (requiresUpdate) { + return ( + + Update Oracles + + ) + } + + if (requiresHighSlippageConfirm) { + return ( + + {`Confirm ${Math.abs(slippageToBeUsed)}% max slippage`} + + ) + } + + return children +} diff --git a/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.hooks.ts b/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.hooks.ts new file mode 100644 index 0000000..516be1f --- /dev/null +++ b/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.hooks.ts @@ -0,0 +1,98 @@ +import { + usePoolFees, + usePoolManagerLogicData, +} from '@dhedge/core-ui-kit/hooks/pool' +import { + useReceiveTokenInput, + useSendTokenInput, + useTradingPanelApprovingStatus, + useTradingPanelLockTime, + useTradingPanelPoolConfig, + useTradingPanelSettings, +} from '@dhedge/core-ui-kit/hooks/state' +import { useProjectedEarnings } from '@dhedge/core-ui-kit/hooks/trading/deposit' +import { formatToUsd } from '@dhedge/core-ui-kit/utils' +import BigNumber from 'bignumber.js' + +import { useGetSlippagePlaceholder, useGetThemeTypeBySlippage } from 'hooks' +import { useConfigContextParams } from 'providers/config-provider' +import { useTranslationContext } from 'providers/translation-provider' + +import { THEME_TYPE } from 'types' + +export const useDepositTransactionDisclosure = () => { + const t = useTranslationContext() + const { customLockTime } = useConfigContextParams() + const [approvingStatus] = useTradingPanelApprovingStatus() + const [{ slippage, minSlippage, isInfiniteAllowance, isMaxSlippageLoading }] = + useTradingPanelSettings() + const [receiveToken] = useReceiveTokenInput() + const [sendToken] = useSendTokenInput() + const { address, chainId } = useTradingPanelPoolConfig() + + const { entryFee, hasPoolEntryFee } = usePoolFees({ address, chainId }) + const { minDepositUSD } = usePoolManagerLogicData(address, chainId) + const projectedEarnings = useProjectedEarnings() + const lockTime = useTradingPanelLockTime() + + const minDeposit = minDepositUSD + ? formatToUsd({ value: minDepositUSD, minimumFractionDigits: 0 }) + : '' + + const isAutoSlippage = slippage === 'auto' + + const themeType = useGetThemeTypeBySlippage( + isAutoSlippage ? minSlippage ?? 0 : slippage, + ) + + const slippagePlaceholder = useGetSlippagePlaceholder( + 'deposit', + slippage, + minSlippage, + ) + const slippageTooltipText = + themeType === THEME_TYPE.DEFAULT ? t.slippageWarning : t.highSlippageWarning + + const getMinReceiveText = () => { + if (isAutoSlippage) { + return `${new BigNumber(receiveToken.value ?? 0).toFixed( + 4, + )} ${receiveToken.symbol.toUpperCase()}` + } + if (receiveToken.symbol === 'all') { + return t.estimatedMultiAssetFractions + } + + const receiveBalance = new BigNumber(receiveToken.value ?? 0) + const receiveValueAfterSlippage = + receiveToken.value && receiveBalance.isFinite() + ? receiveBalance.times(1 - slippage / 100).toFixed(4) + : '0' + + return `${receiveValueAfterSlippage} ${receiveToken.symbol.toUpperCase()}` + } + + const tokenAllowance = isInfiniteAllowance + ? t.infinite + : `${new BigNumber(sendToken.value || '0').toFixed(4)} ${sendToken.symbol}` + + const entryFeeTooltipText = hasPoolEntryFee + ? t.entryFeeExplanation + : t.easySwapperEntryFee.replace('{time}', customLockTime) + + return { + projectedEarnings, + themeType, + slippageTooltipText, + isMaxSlippageLoading, + slippagePlaceholder, + minReceive: getMinReceiveText(), + allowanceRequired: !approvingStatus, + tokenAllowance, + sendTokenSymbol: sendToken.symbol, + entryFee, + entryFeeTooltipText, + minDeposit, + lockTime, + } +} diff --git a/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.tsx b/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.tsx index b56a51f..08f168a 100644 --- a/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.tsx +++ b/packages/trading-widget/src/components/deposit/meta/transaction-disclosure/transaction-disclosure.tsx @@ -1,38 +1,125 @@ +import classNames from 'classnames' + import { useMemo } from 'react' import type { TransactionDisclosureItemProps } from 'components/common' -import { TransactionOverviewDisclosure } from 'components/common' +import { Spinner, TransactionOverviewDisclosure } from 'components/common' +import { useTranslationContext } from 'providers/translation-provider' + +import { THEME_TYPE } from 'types' + +import { useDepositTransactionDisclosure } from './transaction-disclosure.hooks' export const DepositTransactionOverviewDisclosure = () => { - const staticItems = useMemo( - () => [ + const t = useTranslationContext() + const { + projectedEarnings: { yearlyEarnings, dailyEarnings }, + slippageTooltipText, + slippagePlaceholder, + isMaxSlippageLoading, + minReceive, + themeType, + allowanceRequired, + tokenAllowance, + sendTokenSymbol, + entryFee, + entryFeeTooltipText, + minDeposit, + lockTime, + } = useDepositTransactionDisclosure() + + const staticItems: TransactionDisclosureItemProps[] = [ + { + tooltipText: t.projectedDailyEarningsTooltip, + label: t.dailyEarnings, + value: dailyEarnings, + emphasised: true, + }, + { + tooltipText: t.projectedYearlyEarningsTooltip, + label: t.yearlyEarnings, + value: yearlyEarnings, + emphasised: true, + }, + ] + + const collapseItems = useMemo(() => { + const items: TransactionDisclosureItemProps[] = [ { - tooltipText: 'Deposit static tooltip text', - label: 'Deposit static label', - value: 'Deposit static value', - emphasised: true, + tooltipText: slippageTooltipText, + label: t.maxSlippage, + value: ( +
+ {isMaxSlippageLoading && ( + + )} + + {slippagePlaceholder}% + +
+ ), }, - ], - [], - ) - - const collapseItems = useMemo( - () => [ { - tooltipText: 'Deposit collapse tooltip text', - label: 'Deposit collapse label', - value: 'Deposit collapse value', + tooltipText: t.minReceiveAmount, + label: t.minReceived, + value: minReceive, }, - ], - [], - ) + ] + + if (allowanceRequired) { + items.push({ + tooltipText: t.amountToBeApproved.replace('{symbol}', sendTokenSymbol), + label: t.tokenAllowance, + value: tokenAllowance, + }) + } + + items.push({ + tooltipText: entryFeeTooltipText, + label: t.entryFee, + value: entryFee, + }) + + if (minDeposit) { + items.push({ + tooltipText: t.minDepositUsd, + label: t.minDeposit, + value: minDeposit, + }) + } + + return items + }, [ + slippageTooltipText, + t, + isMaxSlippageLoading, + slippagePlaceholder, + minReceive, + allowanceRequired, + sendTokenSymbol, + tokenAllowance, + entryFeeTooltipText, + entryFee, + minDeposit, + ]) return ( - Deposit TransactionOverviewDisclosure Children +

+ {t.tokensLockTime.replace('{lockTime}', lockTime)} +

) } diff --git a/packages/trading-widget/src/components/deposit/tab/tab-panel/tab-panel.tsx b/packages/trading-widget/src/components/deposit/tab/tab-panel/tab-panel.tsx index 83301cc..4ab3949 100644 --- a/packages/trading-widget/src/components/deposit/tab/tab-panel/tab-panel.tsx +++ b/packages/trading-widget/src/components/deposit/tab/tab-panel/tab-panel.tsx @@ -1,13 +1,16 @@ import type { FC } from 'react' import { Layout } from 'components/common' +import { ValidNetworkButton } from 'components/widget/widget-buttons' import { useComponentContext } from 'providers/component-provider' -import { useConfigContext } from 'providers/config-provider' +import { useConfigContextParams } from 'providers/config-provider' +import { DepositTradeButton } from '../../button/trade-button/trade-button' +import { ValidDepositButton } from '../../button/valid-deposit-button/valid-deposit-button' import { DepositMeta } from '../../meta/meta' export const DepositTabPanel: FC = () => { - const { isGeoBlocked = false } = useConfigContext() + const { isGeoBlocked } = useConfigContextParams() const { GeoBlockAlert } = useComponentContext() return ( @@ -16,7 +19,11 @@ export const DepositTabPanel: FC = () => { {isGeoBlocked && GeoBlockAlert ? ( ) : ( - <>Deposit Action Buttons + + + + + )} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.hooks.ts b/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.hooks.ts new file mode 100644 index 0000000..8e7ca64 --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.hooks.ts @@ -0,0 +1,18 @@ +import { useTradingPanelApprovingStatus } from '@dhedge/core-ui-kit/hooks/state' +import { useIsTradingEnabled } from '@dhedge/core-ui-kit/hooks/trading' + +export interface ApproveButtonProps { + symbol: string + onApprove: () => void +} + +export const useApproveButton = () => { + const tradingEnabled = useIsTradingEnabled() + const [approvingStatus] = useTradingPanelApprovingStatus() + const isLoading = approvingStatus === 'success' + + return { + disabled: !tradingEnabled || isLoading, + isLoading, + } +} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.tsx b/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.tsx new file mode 100644 index 0000000..1368f26 --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/approve-button/approve-button.tsx @@ -0,0 +1,27 @@ +import type { FC } from 'react' + +import { ActionButton, Spinner } from 'components/common' +import { THEME_TYPE } from 'types' + +import type { ApproveButtonProps } from './approve-button.hooks' +import { useApproveButton } from './approve-button.hooks' + +export const ApproveButton: FC = ({ + onApprove, + symbol, +}) => { + const { disabled, isLoading } = useApproveButton() + + return ( + +
+ Approve {symbol} + {isLoading && ( + + + + )} +
+
+ ) +} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/connect-wallet-button/connect-wallet-button.tsx b/packages/trading-widget/src/components/widget/widget-buttons/connect-wallet-button/connect-wallet-button.tsx new file mode 100644 index 0000000..94084f7 --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/connect-wallet-button/connect-wallet-button.tsx @@ -0,0 +1,18 @@ +import type { FC } from 'react' + +import { ActionButton } from 'components/common' +import { useConfigContextActions } from 'providers/config-provider' + +export const ConnectWalletButton: FC = () => { + const { onConnect } = useConfigContextActions() + + return ( + + Connect Wallet + + ) +} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/index.ts b/packages/trading-widget/src/components/widget/widget-buttons/index.ts new file mode 100644 index 0000000..26334c7 --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/index.ts @@ -0,0 +1,3 @@ +export { ValidNetworkButton } from './valid-network-button/valid-network-button' +export { ApproveButton } from './approve-button/approve-button' +export type { ApproveButtonProps } from './approve-button/approve-button.hooks' diff --git a/packages/trading-widget/src/components/widget/widget-buttons/switch-network-button/switch-network-button.tsx b/packages/trading-widget/src/components/widget/widget-buttons/switch-network-button/switch-network-button.tsx new file mode 100644 index 0000000..ebc6283 --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/switch-network-button/switch-network-button.tsx @@ -0,0 +1,15 @@ +import { useTradingPanelPoolConfig } from '@dhedge/core-ui-kit/hooks/state' +import { useNetwork } from '@dhedge/core-ui-kit/hooks/web3' + +import { ActionButton } from 'components/common' + +export const SwitchNetworkButton = () => { + const { switchNetwork } = useNetwork() + const { chainId } = useTradingPanelPoolConfig() + + const handleSwitch = () => { + switchNetwork?.({ chainId }) + } + + return Switch Network +} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.hooks.ts b/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.hooks.ts new file mode 100644 index 0000000..88b24eb --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.hooks.ts @@ -0,0 +1,13 @@ +import { useTradingPanelPoolConfig } from '@dhedge/core-ui-kit/hooks/state' +import { useAccount, useNetwork } from '@dhedge/core-ui-kit/hooks/web3' + +export const useValidNetworkButton = () => { + const { account } = useAccount() + const { chainId } = useNetwork() + const poolConfig = useTradingPanelPoolConfig() + + return { + isDisconnected: !account, + isWrongNetwork: chainId !== poolConfig.chainId, + } +} diff --git a/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.tsx b/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.tsx new file mode 100644 index 0000000..74de46c --- /dev/null +++ b/packages/trading-widget/src/components/widget/widget-buttons/valid-network-button/valid-network-button.tsx @@ -0,0 +1,19 @@ +import type { FC, PropsWithChildren } from 'react' + +import { useValidNetworkButton } from './valid-network-button.hooks' +import { ConnectWalletButton } from '../connect-wallet-button/connect-wallet-button' +import { SwitchNetworkButton } from '../switch-network-button/switch-network-button' + +export const ValidNetworkButton: FC = ({ children }) => { + const { isDisconnected, isWrongNetwork } = useValidNetworkButton() + + if (!isDisconnected) { + return + } + + if (!isWrongNetwork) { + return + } + + return children +} diff --git a/packages/trading-widget/src/components/withdraw/button/trade-button/trade-button.tsx b/packages/trading-widget/src/components/withdraw/button/trade-button/trade-button.tsx new file mode 100644 index 0000000..2faf262 --- /dev/null +++ b/packages/trading-widget/src/components/withdraw/button/trade-button/trade-button.tsx @@ -0,0 +1,16 @@ +import { useHandleTrade } from '@dhedge/core-ui-kit/hooks/trading' +import { useWithdraw } from '@dhedge/core-ui-kit/hooks/trading/withdraw' +import type { FC } from 'react' + +import { ActionButton } from 'components/common' + +export const WithdrawTradeButton: FC = () => { + const withdraw = useWithdraw() + const { disabled, label, handleTrade } = useHandleTrade(withdraw) + + return ( + + {label} + + ) +} diff --git a/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.hooks.ts b/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.hooks.ts new file mode 100644 index 0000000..3bc5984 --- /dev/null +++ b/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.hooks.ts @@ -0,0 +1,42 @@ +import { usePoolDynamicContractData } from '@dhedge/core-ui-kit/hooks/pool' +import { + useSendTokenInput, + useTradingPanelPoolConfig, +} from '@dhedge/core-ui-kit/hooks/state' +import { useSynthetixV3OraclesUpdate } from '@dhedge/core-ui-kit/hooks/trading' +import { useWithdrawAllowance } from '@dhedge/core-ui-kit/hooks/trading/withdraw' +import { isSynthetixV3Vault } from '@dhedge/core-ui-kit/utils' + +import { useHighSlippageCheck, useSynthetixWithdrawalWindow } from 'hooks' + +export const useValidWithdrawButton = () => { + const { address, chainId } = useTradingPanelPoolConfig() + const [sendToken] = useSendTokenInput() + const { isWithdrawal, startTime } = useSynthetixWithdrawalWindow() + + const { cooldownActive, cooldownEndsInTime } = usePoolDynamicContractData({ + address, + chainId, + }) + const { approve, canSpend } = useWithdrawAllowance() + const { needToBeUpdated, updateOracles } = useSynthetixV3OraclesUpdate({ + disabled: !canSpend || cooldownActive, + }) + const { requiresHighSlippageConfirm, confirmHighSlippage, slippageToBeUsed } = + useHighSlippageCheck() + + return { + requiresWithdrawalWindow: isSynthetixV3Vault(address) && !isWithdrawal, + requiresEndOfCooldown: cooldownActive, + requiresApprove: !canSpend, + requiresHighSlippageConfirm, + requiresUpdate: needToBeUpdated && !!sendToken.value, + sendTokenSymbol: sendToken.symbol, + slippageToBeUsed, + cooldownEndsInTime, + withdrawalWindowStartTime: startTime, + approve, + updateOracles, + confirmHighSlippage, + } +} diff --git a/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.tsx b/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.tsx new file mode 100644 index 0000000..e37a024 --- /dev/null +++ b/packages/trading-widget/src/components/withdraw/button/valid-withdraw-button/valid-withdraw-button.tsx @@ -0,0 +1,65 @@ +import type { FC, PropsWithChildren } from 'react' + +import { ActionButton, DisabledButtonWithPrompt } from 'components/common' +import { ApproveButton } from 'components/widget/widget-buttons' + +import { useValidWithdrawButton } from './valid-withdraw-button.hooks' + +export const ValidWithdrawButton: FC = ({ children }) => { + const { + requiresWithdrawalWindow, + requiresEndOfCooldown, + requiresApprove, + requiresHighSlippageConfirm, + requiresUpdate, + sendTokenSymbol, + slippageToBeUsed, + cooldownEndsInTime, + withdrawalWindowStartTime, + approve, + updateOracles, + confirmHighSlippage, + } = useValidWithdrawButton() + + if (requiresWithdrawalWindow) { + return ( + + Sell + + ) + } + + if (requiresEndOfCooldown) { + return ( + + Sell + + ) + } + + if (requiresApprove) { + return + } + + if (requiresHighSlippageConfirm) { + return ( + + {`Confirm ${Math.abs(slippageToBeUsed)}% max slippage`} + + ) + } + + if (requiresUpdate) { + return ( + + Update Oracles + + ) + } + + return children +} diff --git a/packages/trading-widget/src/components/withdraw/tab/tab-panel/tab-panel.tsx b/packages/trading-widget/src/components/withdraw/tab/tab-panel/tab-panel.tsx index 603beed..09bf876 100644 --- a/packages/trading-widget/src/components/withdraw/tab/tab-panel/tab-panel.tsx +++ b/packages/trading-widget/src/components/withdraw/tab/tab-panel/tab-panel.tsx @@ -1,13 +1,21 @@ import type { FC } from 'react' import { Layout } from 'components/common' +import { ValidNetworkButton } from 'components/widget/widget-buttons' + +import { WithdrawTradeButton } from '../../button/trade-button/trade-button' +import { ValidWithdrawButton } from '../../button/valid-withdraw-button/valid-withdraw-button' import { WithdrawMeta } from '../../meta/meta' -export const WithdrawTabPanel: FC = () => { - return ( - - Withdraw Action Buttons - - ) -} +export const WithdrawTabPanel: FC = () => ( + + + + + + + + + +) diff --git a/packages/trading-widget/src/constants/synthetix-v3.ts b/packages/trading-widget/src/constants/synthetix-v3.ts new file mode 100644 index 0000000..5f741db --- /dev/null +++ b/packages/trading-widget/src/constants/synthetix-v3.ts @@ -0,0 +1,138 @@ +import { COLORS } from 'theme/colors' +import type { + PeriodConfig, + SynthetixV3PeriodType, +} from 'types/synthetix-v3.types' +import { SYNTHETIX_V3_PERIOD } from 'types/synthetix-v3.types' + +export const SYNTHETIX_V3_PERIOD_RANGE_CONFIG: Record< + SynthetixV3PeriodType, + PeriodConfig +> = { + DELEGATION: { + id: SYNTHETIX_V3_PERIOD.DELEGATION, + name: 'Delegation', + description: + 'Automated strategy picks up new deposits, compounds fees and rewards, monitors debt', + gradient: [COLORS.GRAY.DEFAULT, COLORS.GRAY['600']], + start: { + dayOfWeek: 2, + hour: 0, + }, + end: { + dayOfWeek: 4, + hour: 12, + }, + intraWeekRanges: [ + { + start: { + dayOfWeek: 2, + hour: 0, + }, + end: { + dayOfWeek: 4, + hour: 12, + }, + }, + ], + }, + UNDELEGATION: { + id: SYNTHETIX_V3_PERIOD.UNDELEGATION, + name: 'Undelegation', + description: + 'Automated strategy frees up a portion of locked funds to make it available as withdrawal liquidity', + gradient: [COLORS.GRAY.DEFAULT, COLORS.GRAY['600']], + start: { + dayOfWeek: 4, + hour: 12, + }, + end: { + dayOfWeek: 5, + hour: 0, + }, + intraWeekRanges: [ + { + start: { + dayOfWeek: 4, + hour: 12, + }, + end: { + dayOfWeek: 5, + hour: 0, + }, + }, + ], + }, + WAIT: { + id: SYNTHETIX_V3_PERIOD.WAIT, + name: 'Wait', + gradient: [COLORS.GRAY.DEFAULT, COLORS.GRAY['600']], + description: 'Please wait until the withdrawal window to redeem assets', + start: { + dayOfWeek: 5, + hour: 0, + }, + end: { + dayOfWeek: 6, + hour: 0, + }, + intraWeekRanges: [ + { + start: { + dayOfWeek: 5, + hour: 0, + }, + end: { + dayOfWeek: 6, + hour: 0, + }, + }, + ], + }, + WITHDRAWAL: { + id: SYNTHETIX_V3_PERIOD.WITHDRAWAL, + name: 'Withdrawal', + description: 'Withdrawal window open. Token can be redeemed at this time', + gradient: [COLORS.GREEN.DEFAULT, COLORS.GREEN['700']], + start: { + dayOfWeek: 6, + hour: 0, + }, + end: { + dayOfWeek: 9, + hour: 0, + }, + intraWeekRanges: [ + { + start: { + dayOfWeek: 1, + hour: 0, + }, + end: { + dayOfWeek: 1, + hour: 24, + }, + }, + { + start: { + dayOfWeek: 6, + hour: 0, + }, + end: { + dayOfWeek: 7, + hour: 24, + }, + }, + { + start: { + dayOfWeek: 8, + hour: 0, + }, + end: { + dayOfWeek: 8, + hour: 24, + }, + }, + ], + }, +} diff --git a/packages/trading-widget/src/hooks/index.ts b/packages/trading-widget/src/hooks/index.ts new file mode 100644 index 0000000..26f610a --- /dev/null +++ b/packages/trading-widget/src/hooks/index.ts @@ -0,0 +1,4 @@ +export { useHighSlippageCheck } from './use-high-slippage-check' +export { useSynthetixWithdrawalWindow } from './use-synthetix-withdrawal-window' +export { useGetThemeTypeBySlippage } from './use-get-theme-type-by-slippage' +export { useGetSlippagePlaceholder } from './use-get-slippage-placeholder' diff --git a/packages/trading-widget/src/hooks/use-get-slippage-placeholder.ts b/packages/trading-widget/src/hooks/use-get-slippage-placeholder.ts new file mode 100644 index 0000000..3aaa1f7 --- /dev/null +++ b/packages/trading-widget/src/hooks/use-get-slippage-placeholder.ts @@ -0,0 +1,27 @@ +import type { TradingPanelType } from '@dhedge/core-ui-kit/types' +import isNumber from 'lodash.isnumber' + +import { useConfigContextParams } from 'providers/config-provider' + +export const useGetSlippagePlaceholder = ( + tradingType: TradingPanelType, + slippage: number | 'auto', + minSlippage?: number, +) => { + const { defaultDepositSlippage, defaultWithdrawSlippageScale } = + useConfigContextParams() + + if (slippage !== 'auto') { + return slippage.toString() + } + + if (isNumber(minSlippage)) { + return minSlippage.toString() + } + + return tradingType === 'deposit' + ? defaultDepositSlippage.toString() + : `auto ${defaultWithdrawSlippageScale[0]}-${ + defaultWithdrawSlippageScale[defaultWithdrawSlippageScale.length - 1] + }` +} diff --git a/packages/trading-widget/src/hooks/use-get-theme-type-by-slippage.ts b/packages/trading-widget/src/hooks/use-get-theme-type-by-slippage.ts new file mode 100644 index 0000000..1ff619f --- /dev/null +++ b/packages/trading-widget/src/hooks/use-get-theme-type-by-slippage.ts @@ -0,0 +1,20 @@ +import { useConfigContextParams } from 'providers/config-provider' +import type { ThemeType } from 'types' +import { THEME_TYPE } from 'types' + +export const useGetThemeTypeBySlippage = (slippage: number): ThemeType => { + const { depositQuoteDiffErrorThreshold, depositQuoteDiffWarningThreshold } = + useConfigContextParams() + + const value = Math.abs(slippage) + + if (value > depositQuoteDiffErrorThreshold) { + return THEME_TYPE.ERROR + } + + if (value > depositQuoteDiffWarningThreshold) { + return THEME_TYPE.WARNING + } + + return THEME_TYPE.DEFAULT +} diff --git a/packages/trading-widget/src/hooks/use-high-slippage-check.ts b/packages/trading-widget/src/hooks/use-high-slippage-check.ts new file mode 100644 index 0000000..cb3d5cb --- /dev/null +++ b/packages/trading-widget/src/hooks/use-high-slippage-check.ts @@ -0,0 +1,24 @@ +import { useTradingPanelSettings } from '@dhedge/core-ui-kit/hooks/state' +import { useIsInsufficientBalance } from '@dhedge/core-ui-kit/hooks/user' +import { useCallback, useState } from 'react' + +import { useConfigContextParams } from 'providers/config-provider' + +export const useHighSlippageCheck = () => { + const { depositQuoteDiffErrorThreshold } = useConfigContextParams() + const [isHighSlippageConfirmed, setIsHighSlippageConfirmed] = useState(false) + const [{ slippage, minSlippage }] = useTradingPanelSettings() + const slippageToBeUsed = slippage === 'auto' ? minSlippage ?? 0 : slippage + const insufficientBalance = useIsInsufficientBalance() + + const requiresHighSlippageConfirm = + !insufficientBalance && + slippageToBeUsed > depositQuoteDiffErrorThreshold && + !isHighSlippageConfirmed + + const confirmHighSlippage = useCallback(() => { + setIsHighSlippageConfirmed(true) + }, []) + + return { requiresHighSlippageConfirm, confirmHighSlippage, slippageToBeUsed } +} diff --git a/packages/trading-widget/src/hooks/use-synthetix-withdrawal-window.ts b/packages/trading-widget/src/hooks/use-synthetix-withdrawal-window.ts new file mode 100644 index 0000000..3a2e770 --- /dev/null +++ b/packages/trading-widget/src/hooks/use-synthetix-withdrawal-window.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react' + +import { SYNTHETIX_V3_PERIOD } from 'types/synthetix-v3.types' +import { + getCurrentRangeIndex, + getRangeData, + getWithdrawalWindowStart, +} from 'utils/synthetix-v3' + +export const useSynthetixWithdrawalWindow = () => + useMemo(() => { + const currentIndex = getCurrentRangeIndex() + const data = getRangeData() + + return { + isWithdrawal: data[currentIndex]?.id === SYNTHETIX_V3_PERIOD.WITHDRAWAL, + startTime: getWithdrawalWindowStart(), + } + }, []) diff --git a/packages/trading-widget/src/providers/config-provider/config-provider.hooks.tsx b/packages/trading-widget/src/providers/config-provider/config-provider.hooks.tsx index d7817a7..fba86e2 100644 --- a/packages/trading-widget/src/providers/config-provider/config-provider.hooks.tsx +++ b/packages/trading-widget/src/providers/config-provider/config-provider.hooks.tsx @@ -11,3 +11,6 @@ export const useConfigContext = () => { return context } + +export const useConfigContextParams = () => useConfigContext().params +export const useConfigContextActions = () => useConfigContext().actions diff --git a/packages/trading-widget/src/providers/config-provider/config-provider.tsx b/packages/trading-widget/src/providers/config-provider/config-provider.tsx index d869cd6..69acf2d 100644 --- a/packages/trading-widget/src/providers/config-provider/config-provider.tsx +++ b/packages/trading-widget/src/providers/config-provider/config-provider.tsx @@ -1,22 +1,82 @@ +import { + DEFAULT_DEPOSIT_SLIPPAGE, + DEFAULT_DEPOSIT_SLIPPAGE_SCALE, + DEFAULT_WITHDRAW_SLIPPAGE_SCALE, +} from '@dhedge/core-ui-kit/const' +import { formatDuration } from 'date-fns' import type { FC, PropsWithChildren } from 'react' -import { createContext } from 'react' +import { createContext, useMemo } from 'react' + +interface ConfigProviderParams { + isGeoBlocked: boolean + depositQuoteDiffWarningThreshold: number + depositQuoteDiffErrorThreshold: number + defaultDepositSlippage: number + defaultDepositSlippageScale: number[] + defaultWithdrawSlippageScale: number[] + customLockTime: string +} + +interface ConfigProviderActions { + onConnect: () => void +} export interface ConfigProviderProps { config?: { - isGeoBlocked?: boolean + params: Partial + actions: Partial } } -export const ConfigProviderContext = createContext< - Partial ->({}) +type ConfigProviderState = { + params: ConfigProviderParams + actions: ConfigProviderActions +} + +export const DEFAULT_CONFIG_PARAMS: Required< + Required['config']['params'] +> = { + isGeoBlocked: false, + depositQuoteDiffWarningThreshold: 1, + depositQuoteDiffErrorThreshold: 3, + defaultDepositSlippage: DEFAULT_DEPOSIT_SLIPPAGE, + defaultDepositSlippageScale: DEFAULT_DEPOSIT_SLIPPAGE_SCALE, + defaultWithdrawSlippageScale: DEFAULT_WITHDRAW_SLIPPAGE_SCALE, + customLockTime: formatDuration({ minutes: 15 }), +} + +const defaultValue: ConfigProviderState = { + params: { + ...DEFAULT_CONFIG_PARAMS, + }, + actions: { + onConnect: () => {}, + }, +} + +export const ConfigProviderContext = + createContext(defaultValue) export const ConfigProvider: FC> = ({ children, - config = {}, + config = defaultValue, }) => { + const value = useMemo( + () => ({ + params: { + ...defaultValue.params, + ...config.params, + }, + actions: { + ...defaultValue.actions, + ...config.actions, + }, + }), + [], + ) + return ( - + {children} ) diff --git a/packages/trading-widget/src/providers/config-provider/index.ts b/packages/trading-widget/src/providers/config-provider/index.ts index 8026649..10101b0 100644 --- a/packages/trading-widget/src/providers/config-provider/index.ts +++ b/packages/trading-widget/src/providers/config-provider/index.ts @@ -1 +1,6 @@ -export { useConfigContext } from './config-provider.hooks' +export { + useConfigContext, + useConfigContextParams, + useConfigContextActions, +} from './config-provider.hooks' +export { DEFAULT_CONFIG_PARAMS } from './config-provider' diff --git a/packages/trading-widget/src/providers/index.tsx b/packages/trading-widget/src/providers/index.tsx index 4bb5c89..a1ec466 100644 --- a/packages/trading-widget/src/providers/index.tsx +++ b/packages/trading-widget/src/providers/index.tsx @@ -6,11 +6,14 @@ import type { ConfigProviderProps } from './config-provider/config-provider' import { ConfigProvider } from './config-provider/config-provider' import type { ThemeProviderProps } from './theme-provider/theme-provider' import { ThemeProvider } from './theme-provider/theme-provider' +import type { TranslationProviderProps } from './translation-provider/translation-provider' +import { TranslationProvider } from './translation-provider/translation-provider' export interface ProvidersProps { theme?: ThemeProviderProps['config'] config?: ConfigProviderProps['config'] components?: ComponentProviderProps['config'] + translation?: TranslationProviderProps['config'] } export const Providers: FC> = ({ @@ -18,10 +21,13 @@ export const Providers: FC> = ({ config, theme, components, + translation, }) => ( - - - {children} - - + + + + {children} + + + ) diff --git a/packages/trading-widget/src/providers/translation-provider/index.ts b/packages/trading-widget/src/providers/translation-provider/index.ts new file mode 100644 index 0000000..ef6dab4 --- /dev/null +++ b/packages/trading-widget/src/providers/translation-provider/index.ts @@ -0,0 +1 @@ +export { useTranslationContext } from './translation-provider.hooks' diff --git a/packages/trading-widget/src/providers/translation-provider/translation-default-data.ts b/packages/trading-widget/src/providers/translation-provider/translation-default-data.ts new file mode 100644 index 0000000..a20ed5f --- /dev/null +++ b/packages/trading-widget/src/providers/translation-provider/translation-default-data.ts @@ -0,0 +1,54 @@ +export interface TranslationMap { + slippageWarning: string + highSlippageWarning: string + projectedDailyEarningsTooltip: string + dailyEarnings: string + projectedYearlyEarningsTooltip: string + yearlyEarnings: string + fullReceiveDetails: string + tradeDetails: string + maxSlippage: string + minReceiveAmount: string + minReceived: string + estimatedMultiAssetFractions: string + infinite: string + tokenAllowance: string + entryFee: string + entryFeeExplanation: string + easySwapperEntryFee: string + amountToBeApproved: string + minDepositUsd: string + minDeposit: string + tokensLockTime: string +} + +export const DEFAULT_TRANSLATION_DATA: TranslationMap = { + slippageWarning: + 'Includes entry fee. We recommend 2-3%, but usually it will be < 1%. Slippage may be amplified by the leverage. See the docs for more info.', + highSlippageWarning: + 'We recommend using another asset to trade with lower slippage.', + projectedDailyEarningsTooltip: + 'Projected daily earnings are based on the current APY and may differ from actual earnings.', + dailyEarnings: 'Daily Earnings', + projectedYearlyEarningsTooltip: + 'Projected yearly earnings are based on the current APY and may differ from actual earnings.', + yearlyEarnings: 'Yearly Earnings', + fullReceiveDetails: 'See full details influencing what you will receive.', + tradeDetails: 'Trade details', + maxSlippage: 'Max slippage', + minReceiveAmount: 'You will receive no less than this amount.', + minReceived: 'Minimum Received', + estimatedMultiAssetFractions: 'Estimated multi asset fractions', + infinite: 'Infinite', + tokenAllowance: 'Token Allowance', + entryFee: 'Entry Fee', + entryFeeExplanation: + "When you deposit, the token takes a small entry fee. This fee helps cover the costs when we rebalance the underlying funds, and it's shared among all token holders.", + easySwapperEntryFee: + 'Entry fee is charged when a cooldown of {time} is selected. Bypass Entry Fee at trading settings.', + amountToBeApproved: + 'Amount of {symbol} tokens to be approved. Can be customized in settings.', + minDepositUsd: 'Minimum deposit in USD.', + minDeposit: 'Minimum Deposit', + tokensLockTime: 'Purchased tokens will have a {lockTime} lock.', +} diff --git a/packages/trading-widget/src/providers/translation-provider/translation-provider.hooks.ts b/packages/trading-widget/src/providers/translation-provider/translation-provider.hooks.ts new file mode 100644 index 0000000..460e503 --- /dev/null +++ b/packages/trading-widget/src/providers/translation-provider/translation-provider.hooks.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { TranslationProviderContext } from './translation-provider' + +export const useTranslationContext = () => { + const context = useContext(TranslationProviderContext) + + if (!context) { + throw new Error('TranslationContext is used out of Provider') + } + + return context +} diff --git a/packages/trading-widget/src/providers/translation-provider/translation-provider.tsx b/packages/trading-widget/src/providers/translation-provider/translation-provider.tsx new file mode 100644 index 0000000..7aa432d --- /dev/null +++ b/packages/trading-widget/src/providers/translation-provider/translation-provider.tsx @@ -0,0 +1,31 @@ +import type { FC, PropsWithChildren } from 'react' +import { createContext, useMemo } from 'react' + +import type { TranslationMap } from './translation-default-data' +import { DEFAULT_TRANSLATION_DATA } from './translation-default-data' + +export interface TranslationProviderProps { + config?: Partial +} + +export const TranslationProviderContext = createContext< + Required +>(DEFAULT_TRANSLATION_DATA) + +export const TranslationProvider: FC< + PropsWithChildren +> = ({ children, config }) => { + const value = useMemo( + () => ({ + ...DEFAULT_TRANSLATION_DATA, + ...config, + }), + [config], + ) + + return ( + + {children} + + ) +} diff --git a/packages/trading-widget/src/types/synthetix-v3.types.ts b/packages/trading-widget/src/types/synthetix-v3.types.ts new file mode 100644 index 0000000..31cf1a4 --- /dev/null +++ b/packages/trading-widget/src/types/synthetix-v3.types.ts @@ -0,0 +1,29 @@ +export const SYNTHETIX_V3_PERIOD = { + DELEGATION: 'DELEGATION', + UNDELEGATION: 'UNDELEGATION', + WAIT: 'WAIT', + WITHDRAWAL: 'WITHDRAWAL', +} as const + +export type SynthetixV3PeriodType = + (typeof SYNTHETIX_V3_PERIOD)[keyof typeof SYNTHETIX_V3_PERIOD] + +interface DayParams { + dayOfWeek: number + hour: number +} + +interface WeekPeriod { + start: DayParams + end: DayParams +} + +export interface PeriodConfig { + id: SynthetixV3PeriodType + name: string + description?: string + gradient: [string, string] + start: DayParams + end: DayParams + intraWeekRanges: WeekPeriod[] +} diff --git a/packages/trading-widget/src/utils/synthetix-v3.ts b/packages/trading-widget/src/utils/synthetix-v3.ts new file mode 100644 index 0000000..5978a1c --- /dev/null +++ b/packages/trading-widget/src/utils/synthetix-v3.ts @@ -0,0 +1,70 @@ +import { UTCDate } from '@date-fns/utc' + +import { + differenceInHours, + format, + isWithinInterval, + setDay, + setHours, + startOfWeek, +} from 'date-fns' + +import { SYNTHETIX_V3_PERIOD_RANGE_CONFIG } from 'constants/synthetix-v3' +import type { SynthetixV3PeriodType } from 'types/synthetix-v3.types' +import { SYNTHETIX_V3_PERIOD } from 'types/synthetix-v3.types' + +const WEEK_IN_HOURS = 24 * 7 + +export const getDateRangeByPeriod = (period: SynthetixV3PeriodType) => { + const config = SYNTHETIX_V3_PERIOD_RANGE_CONFIG[period] + const date = startOfWeek(new UTCDate(), { weekStartsOn: 1 }) + + const from = setHours(setDay(date, config.start.dayOfWeek), config.start.hour) + const to = setHours(setDay(date, config.end.dayOfWeek), config.end.hour) + + const durationHours = differenceInHours(to, from) + + return { + from, + to, + intraWeekRanges: config.intraWeekRanges.map((range) => ({ + from: setHours(setDay(date, range.start.dayOfWeek), range.start.hour), + to: setHours(setDay(date, range.end.dayOfWeek), range.end.hour), + })), + duration: { + percentage: (durationHours / WEEK_IN_HOURS) * 100, + }, + format: { + from: format(new Date(from.toString()), 'iii HH:mm'), + to: format(new Date(to.toString()), 'iii HH:mm'), + }, + } +} + +export const getRangeData = () => + Object.values(SYNTHETIX_V3_PERIOD_RANGE_CONFIG).map((config) => ({ + ...config, + ...getDateRangeByPeriod(config.id), + })) + +export const getCurrentRangeIndex = () => { + const now = new UTCDate() + + return Object.values(SYNTHETIX_V3_PERIOD_RANGE_CONFIG).reduce( + (acc, config, index) => { + const range = getDateRangeByPeriod(config.id) + return range.intraWeekRanges.some((segment) => + isWithinInterval(now, { start: segment.from, end: segment.to }), + ) + ? index + : acc + }, + 0, + ) +} + +export const getWithdrawalWindowStart = () => { + const config = getDateRangeByPeriod(SYNTHETIX_V3_PERIOD.WITHDRAWAL) + + return format(new Date(config.from.toString()), 'iii, MMM do, HH:mm') +} diff --git a/packages/trading-widget/tsconfig.json b/packages/trading-widget/tsconfig.json index d21ad5f..1a6fec8 100644 --- a/packages/trading-widget/tsconfig.json +++ b/packages/trading-widget/tsconfig.json @@ -46,8 +46,10 @@ "@dhedge/core-ui-kit/hooks/user": ["../../core-ui-kit/hooks/user"], "@dhedge/core-ui-kit/hooks/utils": ["../../core-ui-kit/hooks/utils"], "@dhedge/core-ui-kit/hooks/web3": ["../../core-ui-kit/hooks/web3"], + "@dhedge/core-ui-kit/hooks/pool": ["../../core-ui-kit/hooks/pool"], "@dhedge/core-ui-kit/models": ["../../core-ui-kit/models"], - "@dhedge/core-ui-kit/providers": ["../../core-ui-kit/providers"] + "@dhedge/core-ui-kit/providers": ["../../core-ui-kit/providers"], + "@dhedge/core-ui-kit/utils": ["../../core-ui-kit/utils"] } }, "include": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddfbe00..fdd0013 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@tanstack/react-query-devtools': specifier: ^5.24.0 @@ -31,6 +27,9 @@ dependencies: version: 4.7.0 devDependencies: + '@date-fns/utc': + specifier: ^1.1.1 + version: 1.1.1 '@headlessui/react': specifier: ^1.7.18 version: 1.7.18(react-dom@18.2.0)(react@18.2.0) @@ -2955,6 +2954,10 @@ packages: dependencies: '@jridgewell/trace-mapping': 0.3.9 + /@date-fns/utc@1.1.1: + resolution: {integrity: sha512-xHqw5SkB6z2OpouO/6BqLSL1nEI2jLC6WIXT01TNFfffU0Os8U/Gk2yxoKB/qbY44tfnGbqUF+nkST5QqLJpDA==} + dev: true + /@discoveryjs/json-ext@0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -19948,3 +19951,7 @@ packages: react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false