diff --git a/components/Pages/Trade/Migrate/Migrate.tsx b/components/Pages/Trade/Migrate/Migrate.tsx index c5197c6d..88290059 100644 --- a/components/Pages/Trade/Migrate/Migrate.tsx +++ b/components/Pages/Trade/Migrate/Migrate.tsx @@ -1,17 +1,11 @@ import { useEffect, useMemo, useState } from 'react' -import { - VStack, - Text, - useMediaQuery, - Heading, - Box, - Divider, -} from '@chakra-ui/react' -import { Button } from '@chakra-ui/react' // Ensure you import Button if not already +import { Box, Button, Divider, Heading, Text, useMediaQuery, VStack } from '@chakra-ui/react' // Ensure you import Button if not already import { useChain } from '@cosmos-kit/react-lite' import DepositForm from 'components/Pages/Trade/Liquidity/DepositForm' import useProvideLP from 'components/Pages/Trade/Liquidity/hooks/useProvideLP' +import { useFetchStaked } from 'components/Pages/Trade/Migrate/hooks/useFetchStaked' +import useMigrateTx, { MigrateAction } from 'components/Pages/Trade/Migrate/hooks/useMigrateTx' import { usePoolsListQuery } from 'components/Pages/Trade/Pools/hooks/usePoolsListQuery' import { PoolEntityTypeWithLiquidity, @@ -28,20 +22,17 @@ import { getDecimals } from 'util/conversion/index' const Migrate = () => { const { walletChainName } = useRecoilValue(chainState) - const { isWalletConnected, address } = useChain(walletChainName) + const { isWalletConnected, address, openView } = useChain(walletChainName) const [isMobile] = useMediaQuery('(max-width: 720px)') - - // Mock states and values for demonstration - const [oldPoolLiquidity, setOldPoolLiquidity] = useState(500) // Hardcoded for demonstration - const [hasWithdrawn, setHasWithdrawn] = useState(false) - const [hasDeposited, setHasDeposited] = useState(false) + const data = useFetchStaked(address) + const { submit } = useMigrateTx() + const isAmpLp = useMemo(() => data?.denom.includes('amplp'), [data]) + const amount = useMemo(() => Number(data?.amount ?? 0) * (10 ** -6), [data]) // States required for DepositForm: const [reverse, setReverse] = useState(false) const clearForm = () => {} const chainId = 'phoenix-1' - const openView = () => {} - const incentivesEnabled = true const { data: poolData } = usePoolsListQuery() const { cosmWasmClient } = useClients(walletChainName) @@ -144,9 +135,13 @@ const Migrate = () => { {/* Step 1 */} Step 1: Check Old Pool Liquidity - {oldPoolLiquidity !== null && oldPoolLiquidity > 0 ? ( - You have {oldPoolLiquidity} LP tokens in the old USDC/USDT XYK pool. - ) : ( + { isAmpLp && amount > 0 && ( + You have {amount.toFixed(6)} ampLP tokens in the old USDC/USDT XYK pool. + )} + {!isAmpLp && amount > 0 && ( + You have {amount.toFixed(6)} LP tokens in the old USDC/USDT XYK pool. + )} + {Number(data?.amount ?? 0) === 0 && ( You have no liquidity in the old pool. )} @@ -158,8 +153,11 @@ const Migrate = () => { Step 2: Withdraw Liquidity Withdraw all liquidity from the old XYK pool before migrating. setHasWithdrawn(true)} + isDisabled={!(isWalletConnected && (amount > 0)) } + label={isWalletConnected && (amount > 0) ? 'Withdraw' : 'Nothing to withdraw'} + onClick={() => submit( + MigrateAction.Withdraw, amount, data.denom, + )} /> diff --git a/components/Pages/Trade/Migrate/hooks/deposit.ts b/components/Pages/Trade/Migrate/hooks/deposit.ts new file mode 100644 index 00000000..4ece7183 --- /dev/null +++ b/components/Pages/Trade/Migrate/hooks/deposit.ts @@ -0,0 +1,61 @@ +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient' +import { ADV_MEMO } from 'constants/index' +import { createGasFee } from 'services/treasuryService' +import { createExecuteMessage } from 'util/messages/createExecuteMessage' + +export const deposit: any = async ( + signingClient: SigningCosmWasmClient, + address: string, + denom: string, + amount: number, +) => { + const ampLpPostMessage = { + liquid_stake: { + compounder: 'terra1zly98gvcec54m3caxlqexce7rus6rzgplz7eketsdz7nh750h2rqvu8uzx', + receiver: null, + gauge: 'bluechip', + }, + } + const lpPostMessage = { + stake: { + asset_staking: 'terra14mmvqn0kthw6sre75vku263lafn5655mkjdejqjedjga4cw0qx2qlf4arv', + receiver: null, + }, + } + + const handleMsg1 = { + create_lp: { + min_received: '6307', + assets: [ + { + native: 'ibc/2C962DAB9F57FE0921435426AE75196009FAA1981BF86991203C8411F8980FDB', + }, + { + native: 'ibc/9B19062D46CAB50361CE9B0A3E6D0A7A53AC9E7CB361F32A73CC733144A9A9E5', + }, + ], + stage: { + white_whale: { + pair: 'terra17vas9rhxhc6j6f5wrup9cqapxn74jvpft069py7l7l9kr7wx3tnsxrazux', + }, + }, + post_action: { + stake: { + asset_staking: 'terra14mmvqn0kthw6sre75vku263lafn5655mkjdejqjedjga4cw0qx2qlf4arv', + receiver: null, + }, + }, + }, + } + + const execMsg = createExecuteMessage({ senderAddress: address, + contractAddress: 'terra1zly98gvcec54m3caxlqexce7rus6rzgplz7eketsdz7nh750h2rqvu8uzx', + message: handleMsg1, + funds: [{ amount: Math.ceil(amount).toString(), + denom }] }) + return await signingClient.signAndBroadcast( + address, [execMsg], await createGasFee( + signingClient, address, [execMsg], + ), ADV_MEMO, + ) +} diff --git a/components/Pages/Trade/Migrate/hooks/useFetchStaked.ts b/components/Pages/Trade/Migrate/hooks/useFetchStaked.ts new file mode 100644 index 00000000..8d931c51 --- /dev/null +++ b/components/Pages/Trade/Migrate/hooks/useFetchStaked.ts @@ -0,0 +1,37 @@ +import { useQuery } from 'react-query' + +import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import { WalletChainName } from 'constants/index' +import { useClients } from 'hooks/useClients' + +const fetchStaked = async (client: CosmWasmClient, + address: string) => { + const lpToken = await client.getBalance(address, 'factory/terra17vas9rhxhc6j6f5wrup9cqapxn74jvpft069py7l7l9kr7wx3tnsxrazux/uLP') + const ampLpToken = await client.getBalance(address, 'factory/terra1zly98gvcec54m3caxlqexce7rus6rzgplz7eketsdz7nh750h2rqvu8uzx/7/bluechip/amplp') + + /* + * Const result: JsonObject = await client.queryContractSmart('terra14mmvqn0kthw6sre75vku263lafn5655mkjdejqjedjga4cw0qx2qlf4arv', + * { + * staked_balance: { + * address, + * asset: { + * native: 'factory/terra17vas9rhxhc6j6f5wrup9cqapxn74jvpft069py7l7l9kr7wx3tnsxrazux/uLP', + * }, + * }, + * }) + */ + return Number(lpToken.amount) > 0 ? lpToken : ampLpToken +} +export const useFetchStaked = (address: string): {amount: string, denom: string } => { + const { cosmWasmClient } = useClients(WalletChainName.terra) + const { data } = useQuery({ + queryKey: ['fetchStaked', address], + queryFn: () => fetchStaked(cosmWasmClient, address), + enabled: Boolean(cosmWasmClient) && Boolean(address), + staleTime: 30_000, // 30 seconds + cacheTime: 1 * 60 * 1_000, // 5 minutes + }); + + // Ensure the hook always returns an object with defaults + return data +} diff --git a/components/Pages/Trade/Migrate/hooks/useMigrateTx.tsx b/components/Pages/Trade/Migrate/hooks/useMigrateTx.tsx new file mode 100644 index 00000000..0e7f4fd4 --- /dev/null +++ b/components/Pages/Trade/Migrate/hooks/useMigrateTx.tsx @@ -0,0 +1,254 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useMutation, useQuery } from 'react-query' + +import { useToast } from '@chakra-ui/react' +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient' +import { useChain } from '@cosmos-kit/react-lite' +import Finder from 'components/Finder' +import { deposit } from 'components/Pages/Trade/Migrate/hooks/deposit' +import { withdraw } from 'components/Pages/Trade/Migrate/hooks/withdraw' +import { useClients } from 'hooks/useClients' +import { useRecoilValue } from 'recoil' +import { chainState } from 'state/chainState' +import { TxStep } from 'types/index' + +export enum MigrateAction { Withdraw, Deposit } +export const useMigrateTx = () => { + const toast = useToast() + const { chainId, walletChainName } = useRecoilValue(chainState) + const { address } = useChain(walletChainName) + const { signingClient } = useClients(walletChainName) + const [txStep, setTxStep] = useState(TxStep.Idle) + const [migrateAction, setMigrateAction] = useState(null) + const [txHash, setTxHash] = useState(null) + const [error, setError] = useState(null) + const [buttonLabel, setButtonLabel] = useState(null) + + const { data: fee } = useQuery( + ['fee', error], + () => { + setError(null) + setTxStep(TxStep.Estimating) + try { + const response = 0 // Await client.simulate(senderAddress, debouncedMsgs, '') + if (buttonLabel) { + setButtonLabel(null) + } + setTxStep(TxStep.Ready) + return response + } catch (error) { + if ( + (/insufficient funds/u).test(error.toString()) || + (/Overflow: Cannot Sub with/u).test(error.toString()) + ) { + console.error(error.toString()) + setTxStep(TxStep.Idle) + setError('Insufficient Funds') + setButtonLabel('Insufficient Funds') + throw new Error('Insufficient Funds') + } else if ((/account sequence mismatch/u).test(error?.toString())) { + setError('You have pending transaction') + setButtonLabel('You have pending transaction') + throw new Error('You have pending transaction') + } else { + console.error(error.toString()) + setTxStep(TxStep.Idle) + setError(error?.message) + throw Error(error?.message) + /* + * SetTxStep(TxStep.Idle) + * setError("Failed to execute transaction.") + * throw Error("Failed to execute transaction.") + */ + } + } + }, + { + enabled: txStep === TxStep.Idle && !error && Boolean(signingClient), + refetchOnWindowFocus: false, + retry: false, + staleTime: 0, + onSuccess: () => { + setTxStep(TxStep.Ready) + }, + onError: () => { + setTxStep(TxStep.Idle) + }, + }, + ) + + const { mutate } = useMutation(async (data: { signingClient: SigningCosmWasmClient, migrateAction: MigrateAction, address: string, denom: string, amount: number }) => { + console.log({ data }) + if (data.migrateAction === MigrateAction.Withdraw) { + return withdraw( + signingClient, address, data.denom, (data.amount * (10 ** 6)), + ) + } else { + return deposit( + signingClient, address, data.denom, (data.amount * (10 ** 6)), + ) + } + }, + { + onMutate: () => { + setTxStep(TxStep.Posting) + }, + onError: (e) => { + let message: any = '' + console.error(e?.toString()) + setTxStep(TxStep.Failed) + if ( + (/insufficient funds/u).test(e?.toString()) || + (/Overflow: Cannot Sub with/u).test(e?.toString()) + ) { + setError('Insufficient Funds') + message = 'Insufficient Funds' + } else if ((/Request rejected/u).test(e?.toString())) { + setError('User Denied') + message = 'User Denied' + } else if ((/account sequence mismatch/u).test(e?.toString())) { + setError('You have pending transaction') + message = 'You have pending transaction' + } else if ((/out of gas/u).test(e?.toString())) { + setError('Out of gas, try increasing gas limit on wallet.') + message = 'Out of gas, try increasing gas limit on wallet.' + } else if ((/before the new\/latest epoch/u).test(e?.toString())) { + setError('Epoch not yet created.') + message = 'Please force the pending epoch on the bonding page.' + } else if ( + (/There are unclaimed rewards available./u).test(e?.toString()) + ) { + setError('There are unclaimed rewards available.') + message = + 'There are unclaimed rewards available. Claim them before attempting to bond/unbond.' + } else if ( + (/was submitted but was not yet found on the chain/u).test(e?.toString()) + ) { + setError(e?.toString()) + message = ( + + {' '} + + ) + } else { + setError('Failed to execute transaction.') + message = 'Failed to execute transaction.' + } + + toast({ + title: (() => { + switch (migrateAction) { + case MigrateAction.Withdraw: + return 'Bonding Failed.' + case MigrateAction.Deposit: + return 'Unbonding Failed' + default: + return '' + } + })(), + description: message, + status: 'error', + duration: 9000, + position: 'top-right', + isClosable: true, + }) + }, + onSuccess: (data: any) => { + setTxStep(TxStep.Broadcasting) + setTxHash(data?.transactionHash) + toast({ + title: (() => { + switch (migrateAction) { + case MigrateAction.Deposit: + return 'Deposit Successful.' + case MigrateAction.Withdraw: + return 'Withdrawal Successful.' + default: + return '' + } + })(), + description: ( + + {' '} + + ), + status: 'success', + duration: 9000, + position: 'top-right', + isClosable: true, + }) + }, + }) + + const { data: txInfo } = useQuery( + ['txInfo', txHash], + () => { + if (!txHash) { + return null + } + return signingClient?.getTx(txHash) + }, + { + enabled: Boolean(txHash), + retry: true, + }, + ) + + const reset = () => { + setError(null) + setTxHash(null) + setTxStep(TxStep.Idle) + } + + const submit = useCallback(( + migrateAction: MigrateAction, + amount: number | null, + denom: string | null, + ) => { + setMigrateAction(migrateAction) + console.log(amount, denom) + mutate({ + fee, + address, + migrateAction, + denom, + amount, + }) + }, + [address, fee, mutate]) + + useEffect(() => { + if (txInfo && txHash) { + if (txInfo?.code) { + setTxStep(TxStep.Failed) + } else { + setTxStep(TxStep.Success) + } + } + }, [txInfo, txHash, error]) + + useEffect(() => { + if (error) { + setError(null) + } + + if (txStep !== TxStep.Idle) { + setTxStep(TxStep.Idle) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // DebouncedMsgs + + return useMemo(() => ({ + fee, + buttonLabel, + submit, + txStep, + txInfo, + txHash, + error, + reset, + }), + [txStep, txInfo, txHash, error, fee]) +} + +export default useMigrateTx diff --git a/components/Pages/Trade/Migrate/hooks/withdraw.ts b/components/Pages/Trade/Migrate/hooks/withdraw.ts new file mode 100644 index 00000000..9aed9789 --- /dev/null +++ b/components/Pages/Trade/Migrate/hooks/withdraw.ts @@ -0,0 +1,44 @@ +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient' +import { ADV_MEMO } from 'constants/index' +import { createGasFee } from 'services/treasuryService' +import { createExecuteMessage } from 'util/messages/createExecuteMessage' + +export const withdraw: any = async ( + signingClient: SigningCosmWasmClient, + address: string, + denom: string, + amount: number, +) => { + const handleMsg1 = { + unstake: { + recipient: 'terra1qdjsxsv96aagrdxz83gwtjk8qvf2mrg4y8y3dqjxg556lm79pg5qdgmaxl', + }, + } + const handleMsg2 = { + withdraw_lp: { + stage: { + white_whale: { + pair: 'terra17vas9rhxhc6j6f5wrup9cqapxn74jvpft069py7l7l9kr7wx3tnsxrazux', + }, + }, + }, + } + console.log(denom, amount) + + const execMsg1 = createExecuteMessage({ senderAddress: address, + contractAddress: 'terra1zly98gvcec54m3caxlqexce7rus6rzgplz7eketsdz7nh750h2rqvu8uzx', + message: handleMsg1, + funds: [{ amount: Math.ceil(amount).toString(), + denom }] }) + + const execMsg2 = createExecuteMessage({ senderAddress: address, + contractAddress: 'terra1qdjsxsv96aagrdxz83gwtjk8qvf2mrg4y8y3dqjxg556lm79pg5qdgmaxl', + message: handleMsg2, + funds: [] }) + const messages = denom.includes('amplp') ? [execMsg1, execMsg2] : [execMsg2] + return await signingClient.signAndBroadcast( + address, messages, await createGasFee( + signingClient, address, messages, + ), ADV_MEMO, + ) +}