From c49d22b0ca096e9fd5dad30bc1a674b098844d19 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Sat, 31 May 2025 14:51:51 -0400 Subject: [PATCH 1/5] Add withdrawal destination experience --- .../Lend/solanaWithdraw/1-ConfirmTransfer.tsx | 26 ++++++- .../Lend/solanaWithdraw/2-Transfer.tsx | 73 ++++++++++++++++++- .../components/Lend/solanaWithdraw/index.tsx | 1 + .../huma-widget/src/store/widgets.reducers.ts | 5 ++ .../huma-widget/src/store/widgets.store.ts | 1 + 5 files changed, 99 insertions(+), 7 deletions(-) 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..6f34b892 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)) } @@ -59,6 +65,14 @@ export function ConfirmTransfer({ subTitle='Withdraw all the available amount' > + + Destination Address + setWithdrawDestinationValue(e.target.value)} + /> + Price Per Share @@ -73,7 +87,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..1d707b9c 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 { @@ -140,12 +204,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..99871406 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx @@ -149,6 +149,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 = {} From 20cbd8f338f9a9fedbbcfaf7ecb4679c8a177b79 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Sat, 31 May 2025 18:19:06 -0400 Subject: [PATCH 2/5] Force dev false --- packages/huma-web-shared/src/utils/checkIsDev.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 From 7913826d4de221fea078c6380d0f0d949db0249c Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Mon, 2 Jun 2025 10:19:05 -0400 Subject: [PATCH 3/5] Fix isZero check for tranche supply num --- .../src/components/Lend/solanaWithdraw/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx b/packages/huma-widget/src/components/Lend/solanaWithdraw/index.tsx index ebd87ab7..0fe58b1c 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) From cc2f77f32d8cd75cec0b29d1ed0626853a0f2ea8 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 10 Jun 2025 19:02:18 -0400 Subject: [PATCH 4/5] Hide share price if 0 --- .../Lend/solanaWithdraw/1-ConfirmTransfer.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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..1d52b8f6 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/1-ConfirmTransfer.tsx @@ -59,13 +59,17 @@ export function ConfirmTransfer({ subTitle='Withdraw all the available amount' > - - Price Per Share - - {sharePrice.toFixed(1)} {symbol} - - - + {sharePrice !== 0 && ( + <> + + Price Per Share + + {sharePrice.toFixed(1)} {symbol} + + + + + )} Available to withdraw From 4074fb50d0d8ec62aaee6fbb12ad003edf01f0db Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Thu, 19 Jun 2025 10:21:11 -0400 Subject: [PATCH 5/5] Add transfer to close instruction --- .../Lend/solanaWithdraw/2-Transfer.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 1d707b9c..883135f1 100644 --- a/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx +++ b/packages/huma-widget/src/components/Lend/solanaWithdraw/2-Transfer.tsx @@ -191,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, }), ) }