diff --git a/packages/huma-web-shared/src/utils/checkIsDev.ts b/packages/huma-web-shared/src/utils/checkIsDev.ts index a90f1074..46abd9bc 100644 --- a/packages/huma-web-shared/src/utils/checkIsDev.ts +++ b/packages/huma-web-shared/src/utils/checkIsDev.ts @@ -1,8 +1 @@ -export const checkIsDev = () => - import.meta.env.VITE_FORCE_IS_DEV_FALSE !== 'true' && - !!( - window.location.hostname.startsWith('dev') || - window.location.hostname.startsWith('pr-') || - window.location.hostname.startsWith('localhost') || - window.location.hostname.startsWith('testnet') - ) +export const checkIsDev = () => false diff --git a/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx b/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx index 692f2db3..4cdf2eba 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx @@ -1,8 +1,11 @@ import { UnderlyingTokenInfo } from '@huma-finance/shared' -import { Box, Divider, css, useTheme } from '@mui/material' -import React from 'react' +import { Box, Divider, Input, css, useTheme } from '@mui/material' +import React, { useState } from 'react' import { useDispatch } from 'react-redux' -import { setStep } from '../../../store/widgets.reducers' +import { + setStep, + setWithdrawDestination, +} from '../../../store/widgets.reducers' import { WIDGET_STEP } from '../../../store/widgets.store' import { BottomButton } from '../../BottomButton' import { WrapperModal } from '../../WrapperModal' @@ -23,8 +26,11 @@ export function ConfirmTransfer({ const theme = useTheme() const dispatch = useDispatch() const { symbol } = poolUnderlyingToken + const [withdrawDestinationValue, setWithdrawDestinationValue] = + useState('') const goToWithdraw = () => { + dispatch(setWithdrawDestination(withdrawDestinationValue)) dispatch(setStep(WIDGET_STEP.Transfer)) } @@ -60,11 +66,24 @@ export function ConfirmTransfer({ > - Price Per Share - - {sharePrice.toFixed(1)} {symbol} - + Destination Address + setWithdrawDestinationValue(e.target.value)} + /> + {sharePrice !== 0 && ( + <> + + Price Per Share + + {sharePrice.toFixed(1)} {symbol} + + + + + )} Available to withdraw @@ -73,7 +92,11 @@ export function ConfirmTransfer({ - + WITHDRAW diff --git a/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx b/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx index e05cf88a..883135f1 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx @@ -7,7 +7,9 @@ import { useHumaProgram } from '@huma-finance/web-shared' import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, + createTransferCheckedInstruction, getAccount, + getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, TokenAccountNotFoundError, @@ -16,33 +18,39 @@ import { import { useConnection, useWallet } from '@solana/wallet-adapter-react' import { ComputeBudgetProgram, PublicKey, Transaction } from '@solana/web3.js' import React, { useCallback, useEffect, useState } from 'react' +import { BN } from '@coral-xyz/anchor' import useLogOnFirstMount from '../../../hooks/useLogOnFirstMount' -import { useAppDispatch } from '../../../hooks/useRedux' +import { useAppDispatch, useAppSelector } from '../../../hooks/useRedux' import { setStep } from '../../../store/widgets.reducers' import { WIDGET_STEP } from '../../../store/widgets.store' import { SolanaTxSendModal } from '../../SolanaTxSendModal' +import { selectWidgetState } from '../../../store/widgets.selectors' type Props = { poolInfo: SolanaPoolInfo + withdrawableAmount: BN selectedTranche: TrancheType poolIsClosed: boolean } export function Transfer({ poolInfo, + withdrawableAmount, selectedTranche, poolIsClosed, }: Props): React.ReactElement | null { useLogOnFirstMount('Transaction') + const { decimals } = poolInfo.underlyingMint const { publicKey } = useWallet() const dispatch = useAppDispatch() const { connection } = useConnection() const program = useHumaProgram(poolInfo.chainId) const [transaction, setTransaction] = useState() + const { withdrawDestination } = useAppSelector(selectWidgetState) useEffect(() => { async function getTx() { - if (!publicKey || transaction || !connection) { + if (!publicKey || transaction || !connection || !withdrawDestination) { return } @@ -89,6 +97,45 @@ export function Transfer({ } } + const withdrawalDestinationTokenATA = getAssociatedTokenAddressSync( + new PublicKey(poolInfo.underlyingMint.address), + new PublicKey(withdrawDestination), + true, // allowOwnerOffCurve + TOKEN_PROGRAM_ID, + ) + // Create user token account if it doesn't exist + let createdWithdrawalTokenAccounts = false + try { + await getAccount( + connection, + withdrawalDestinationTokenATA, + undefined, + TOKEN_PROGRAM_ID, + ) + } catch (error: unknown) { + // TokenAccountNotFoundError can be possible if the associated address has already received some lamports, + // becoming a system account. Assuming program derived addressing is safe, this is the only case for the + // TokenInvalidAccountOwnerError in this code path. + // Source: https://solana.stackexchange.com/questions/802/checking-to-see-if-a-token-account-exists-using-anchor-ts + if ( + error instanceof TokenAccountNotFoundError || + error instanceof TokenInvalidAccountOwnerError + ) { + // As this isn't atomic, it's possible others can create associated accounts meanwhile. + createdWithdrawalTokenAccounts = true + tx.add( + createAssociatedTokenAccountInstruction( + publicKey, + withdrawalDestinationTokenATA, + publicKey, + new PublicKey(poolInfo.underlyingMint.address), + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + ) + } + } + if (!poolIsClosed) { const disburseTx = await program.methods .disburse() @@ -104,10 +151,27 @@ export function Transfer({ }) .transaction() tx.add(disburseTx) + const transferTx = await createTransferCheckedInstruction( + underlyingTokenATA, + new PublicKey(poolInfo.underlyingMint.address), + withdrawalDestinationTokenATA, + publicKey, + BigInt(withdrawableAmount.toString()), + decimals, + ) + tx.add(transferTx) + + let txFee = 70_000 + if (createdAccounts) { + txFee += 25_000 + } + if (createdWithdrawalTokenAccounts) { + txFee += 25_000 + } tx.instructions.unshift( ComputeBudgetProgram.setComputeUnitLimit({ - units: createdAccounts ? 85_000 : 60_000, + units: txFee, }), ) } else { @@ -127,10 +191,27 @@ export function Transfer({ }) .transaction() tx.add(withdrawAfterPoolClosureTx) + const transferTx = await createTransferCheckedInstruction( + underlyingTokenATA, + new PublicKey(poolInfo.underlyingMint.address), + withdrawalDestinationTokenATA, + publicKey, + BigInt(withdrawableAmount.toString()), + decimals, + ) + tx.add(transferTx) + + let txFee = 120_000 + if (createdAccounts) { + txFee += 25_000 + } + if (createdWithdrawalTokenAccounts) { + txFee += 25_000 + } tx.instructions.unshift( ComputeBudgetProgram.setComputeUnitLimit({ - units: createdAccounts ? 145_000 : 120_000, + units: txFee, }), ) } @@ -140,12 +221,15 @@ export function Transfer({ getTx() }, [ connection, + decimals, poolInfo, poolIsClosed, program.methods, publicKey, selectedTranche, transaction, + withdrawDestination, + withdrawableAmount, ]) const handleSuccess = useCallback(() => { diff --git a/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx b/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx index ebd87ab7..5f9dda13 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx @@ -113,7 +113,11 @@ export function SolanaLendWithdraw({ : poolState.juniorTrancheAssets const trancheAssetsVal = new BN(trancheAssets ?? 1) - const trancheSupplyVal = new BN(trancheMintAccount.supply.toString() ?? 1) + const trancheSupplyVal = new BN( + trancheMintAccount.supply.toString(), + ).isZero() + ? new BN(1) + : new BN(trancheMintAccount.supply.toString()) const sharePrice = trancheAssetsVal.muln(100000).div(trancheSupplyVal).toNumber() / 100000 setSharePrice(sharePrice) @@ -149,6 +153,7 @@ export function SolanaLendWithdraw({ )} {step === WIDGET_STEP.Transfer && ( ) => { state.step = payload @@ -131,6 +132,9 @@ export const widgetSlice = createSlice({ state.errorReason = payload.errorReason state.step = WIDGET_STEP.Error }, + setWithdrawDestination: (state, { payload }: PayloadAction) => { + state.withdrawDestination = payload + }, }, }) @@ -153,6 +157,7 @@ export const { setTxHash, setPointsAccumulated, setLoggingContext, + setWithdrawDestination, } = widgetSlice.actions export default widgetSlice.reducer diff --git a/packages/huma-widget/src/store/widgets.store.ts b/packages/huma-widget/src/store/widgets.store.ts index cc58de7b..7fe6d4cb 100644 --- a/packages/huma-widget/src/store/widgets.store.ts +++ b/packages/huma-widget/src/store/widgets.store.ts @@ -64,6 +64,7 @@ export type WidgetState = { pointsAccumulated?: number txHash?: string loggingContext?: LoggingContext + withdrawDestination?: string } export const initialWidgetState: WidgetState = {}