Skip to content

[do not merge] Add withdrawal destination experience #487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions packages/huma-web-shared/src/utils/checkIsDev.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,8 +26,11 @@ export function ConfirmTransfer({
const theme = useTheme()
const dispatch = useDispatch()
const { symbol } = poolUnderlyingToken
const [withdrawDestinationValue, setWithdrawDestinationValue] =
useState<string>('')

const goToWithdraw = () => {
dispatch(setWithdrawDestination(withdrawDestinationValue))
dispatch(setStep(WIDGET_STEP.Transfer))
}

Expand Down Expand Up @@ -60,11 +66,24 @@ export function ConfirmTransfer({
>
<Box css={styles.itemWrapper}>
<Box css={styles.item}>
<Box>Price Per Share</Box>
<Box css={styles.itemValue}>
{sharePrice.toFixed(1)} {symbol}
</Box>
<Box>Destination Address</Box>
<Input
placeholder='Enter your destination address'
value={withdrawDestinationValue}
onChange={(e) => setWithdrawDestinationValue(e.target.value)}
/>
</Box>
{sharePrice !== 0 && (
<>
<Box css={styles.item}>
<Box>Price Per Share</Box>
<Box css={styles.itemValue}>
{sharePrice.toFixed(1)} {symbol}
</Box>
</Box>
<Divider css={styles.divider} orientation='horizontal' />
</>
)}
<Divider css={styles.divider} orientation='horizontal' />
<Box css={styles.item}>
<Box fontWeight={700}>Available to withdraw</Box>
Expand All @@ -73,7 +92,11 @@ export function ConfirmTransfer({
</Box>
</Box>
</Box>
<BottomButton variant='contained' onClick={goToWithdraw}>
<BottomButton
variant='contained'
onClick={goToWithdraw}
disabled={withdrawDestinationValue === ''}
>
WITHDRAW
</BottomButton>
</WrapperModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Transaction>()
const { withdrawDestination } = useAppSelector(selectWidgetState)

useEffect(() => {
async function getTx() {
if (!publicKey || transaction || !connection) {
if (!publicKey || transaction || !connection || !withdrawDestination) {
return
}

Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand All @@ -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,
}),
)
}
Expand All @@ -140,12 +221,15 @@ export function Transfer({
getTx()
}, [
connection,
decimals,
poolInfo,
poolIsClosed,
program.methods,
publicKey,
selectedTranche,
transaction,
withdrawDestination,
withdrawableAmount,
])

const handleSuccess = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -149,6 +153,7 @@ export function SolanaLendWithdraw({
)}
{step === WIDGET_STEP.Transfer && (
<Transfer
withdrawableAmount={withdrawableAmount}
poolInfo={poolInfo}
selectedTranche={trancheType}
poolIsClosed={poolIsClosed}
Expand Down
5 changes: 5 additions & 0 deletions packages/huma-widget/src/store/widgets.reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const widgetSlice = createSlice({
state.solanaSignature = undefined
state.txHash = undefined
state.loggingContext = undefined
state.withdrawDestination = undefined
},
setStep: (state, { payload }: PayloadAction<WIDGET_STEP>) => {
state.step = payload
Expand Down Expand Up @@ -131,6 +132,9 @@ export const widgetSlice = createSlice({
state.errorReason = payload.errorReason
state.step = WIDGET_STEP.Error
},
setWithdrawDestination: (state, { payload }: PayloadAction<string>) => {
state.withdrawDestination = payload
},
},
})

Expand All @@ -153,6 +157,7 @@ export const {
setTxHash,
setPointsAccumulated,
setLoggingContext,
setWithdrawDestination,
} = widgetSlice.actions

export default widgetSlice.reducer
1 change: 1 addition & 0 deletions packages/huma-widget/src/store/widgets.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type WidgetState = {
pointsAccumulated?: number
txHash?: string
loggingContext?: LoggingContext
withdrawDestination?: string
}

export const initialWidgetState: WidgetState = {}