Skip to content

Commit

Permalink
Automatic Redemption updates (#351)
Browse files Browse the repository at this point in the history
* Automatic Redemption updates

* Use pool authority instead of sentinel

* Address comments

* lint

---------

Co-authored-by: shan-57blocks <[email protected]>
  • Loading branch information
mliu and shan-57blocks authored Nov 4, 2024
1 parent 109f130 commit d4756b7
Show file tree
Hide file tree
Showing 16 changed files with 547 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/huma-web-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './hooks'
export * from './solana'
export * from './utils'
1 change: 1 addition & 0 deletions packages/huma-web-shared/src/solana/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './hooks'
export * from './types'
export * from './utils'
1 change: 1 addition & 0 deletions packages/huma-web-shared/src/solana/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notEnabledAutoRedeem'
14 changes: 14 additions & 0 deletions packages/huma-web-shared/src/solana/utils/notEnabledAutoRedeem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Account } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'

export const notEnabledAutoRedeem = (
tokenAccount: Account | undefined,
poolAuthorityPubkey: PublicKey,
amount: bigint | undefined,
): boolean =>
!!tokenAccount &&
!!amount &&
tokenAccount.amount > 0 &&
(tokenAccount.delegate == null ||
!poolAuthorityPubkey.equals(tokenAccount.delegate) ||
tokenAccount.delegatedAmount < amount)
21 changes: 21 additions & 0 deletions packages/huma-web-shared/src/utils/getLenderLockupDates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const getLenderLockupDates = (
withdrawalLockupPeriodDays: number,
): { lockupEndTimeUnix: number; withdrawTimeUnix: number } => {
const now = new Date()
const lockupEndTime = new Date(now.getFullYear(), now.getMonth(), 1)
lockupEndTime.setDate(
lockupEndTime.getDate() + (withdrawalLockupPeriodDays ?? 0),
)

const withdrawTime = new Date(
lockupEndTime.getFullYear(),
lockupEndTime.getMonth() + 1,
lockupEndTime.getDate(),
)

// Get Unix timestamps in seconds
const lockupEndTimeUnix = Math.floor(lockupEndTime.getTime() / 1000)
const withdrawTimeUnix = Math.floor(withdrawTime.getTime() / 1000)

return { lockupEndTimeUnix, withdrawTimeUnix }
}
1 change: 1 addition & 0 deletions packages/huma-web-shared/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getLenderLockupDates'
38 changes: 38 additions & 0 deletions packages/huma-widget/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ To be used when re-enabling autopay and other pool actions that require allowanc
<dd><p>Lend pool supply widget for Solana pools</p></dd>
<dt><a href="#SolanaPaymentWidget">SolanaPaymentWidget(props)</a></dt>
<dd><p>Lend pool supply widget for Solana pools</p></dd>
<dt><a href="#SolanaEnableAutoRedemptionWidget">SolanaEnableAutoRedemptionWidget(props)</a></dt>
<dd><p>Lend pool supply widget for Solana pools</p></dd>
</dl>

## Typedefs
Expand Down Expand Up @@ -83,6 +85,8 @@ To be used when re-enabling autopay and other pool actions that require allowanc
<dd><p>Lend pool supply props</p></dd>
<dt><a href="#SolanaLendCancelRedemptionProps">SolanaLendCancelRedemptionProps</a> : <code>Object</code></dt>
<dd><p>Lend pool supply props</p></dd>
<dt><a href="#SolanaEnableAutoRedemptionProps">SolanaEnableAutoRedemptionProps</a> : <code>Object</code></dt>
<dd><p>Lend pool supply props</p></dd>
<dt><a href="#SolanaLendSupplyProps">SolanaLendSupplyProps</a> : <code>Object</code></dt>
<dd><p>Lend pool supply props</p></dd>
<dt><a href="#LendSupplyProps">LendSupplyProps</a> : <code>Object</code></dt>
Expand Down Expand Up @@ -147,6 +151,8 @@ To be used when re-enabling autopay and other pool actions that require allowanc
<dd><p>Lend pool supply widget props for Solana pools</p></dd>
<dt><a href="#SolanaPaymentWidgetProps">SolanaPaymentWidgetProps</a> : <code>Object</code></dt>
<dd><p>Lend pool supply widget props for Solana pools</p></dd>
<dt><a href="#SolanaEnableAutoRedemptionWidgetProps">SolanaEnableAutoRedemptionWidgetProps</a> : <code>Object</code></dt>
<dd><p>Lend pool supply widget props for Solana pools</p></dd>
</dl>

<a name="InvoiceFactoringBorrowWidget"></a>
Expand Down Expand Up @@ -421,6 +427,17 @@ To be used when re-enabling autopay and other pool actions that require allowanc
| --- | --- | --- |
| props | [<code>SolanaPaymentWidgetProps</code>](#SolanaPaymentWidgetProps) | <p>Widget props</p> |

<a name="SolanaEnableAutoRedemptionWidget"></a>

## SolanaEnableAutoRedemptionWidget(props)
<p>Lend pool supply widget for Solana pools</p>

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| props | [<code>SolanaEnableAutoRedemptionWidgetProps</code>](#SolanaEnableAutoRedemptionWidgetProps) | <p>Widget props</p> |

<a name="CreditLineApproveProps"></a>

## CreditLineApproveProps : <code>Object</code>
Expand Down Expand Up @@ -641,6 +658,21 @@ To be used when re-enabling autopay and other pool actions that require allowanc
| poolState | <code>SolanaPoolState</code> | <p>The current state config of the pool. * @property {function():void} handleClose Function to notify to close the widget modal when user clicks the 'x' close button.</p> |
| handleSuccess | <code>function</code> | <p>Optional function to notify that the lending pool supply action is successful.</p> |

<a name="SolanaEnableAutoRedemptionProps"></a>

## SolanaEnableAutoRedemptionProps : <code>Object</code>
<p>Lend pool supply props</p>

**Kind**: global typedef
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| poolInfo | <code>SolanaPoolInfo</code> | <p>The metadata of the pool.</p> |
| poolState | <code>SolanaPoolState</code> | <p>The current state config of the pool.</p> |
| handleClose | <code>function</code> | <p>Function to notify to close the widget modal when user clicks the 'x' close button.</p> |
| handleSuccess | <code>function</code> | <p>Optional function to notify that the lending pool supply action is successful.</p> |

<a name="SolanaLendSupplyProps"></a>

## SolanaLendSupplyProps : <code>Object</code>
Expand Down Expand Up @@ -1031,3 +1063,9 @@ To be used when re-enabling autopay and other pool actions that require allowanc
<p>Lend pool supply widget props for Solana pools</p>

**Kind**: global typedef
<a name="SolanaEnableAutoRedemptionWidgetProps"></a>

## SolanaEnableAutoRedemptionWidgetProps : <code>Object</code>
<p>Lend pool supply widget props for Solana pools</p>

**Kind**: global typedef
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { timeUtil } from '@huma-finance/shared'
import { SolanaPoolState } from '@huma-finance/web-shared'
import dayjs from 'dayjs'
import React, { useCallback } from 'react'
import { Box, css, useTheme } from '@mui/material'
import { useAppDispatch } from '../../../hooks/useRedux'
import { WrapperModal } from '../../WrapperModal'
import { WIDGET_STEP } from '../../../store/widgets.store'
import { setStep } from '../../../store/widgets.reducers'
import { BottomButton } from '../../BottomButton'
import { AutoPaybackImg } from '../../images'

type Props = {
poolState: SolanaPoolState
}

export function ApproveAllowance({ poolState }: Props): React.ReactElement {
const theme = useTheme()
const dispatch = useAppDispatch()
const handleNext = useCallback(() => {
dispatch(setStep(WIDGET_STEP.Transfer))
}, [dispatch])

const styles = {
iconWrapper: css`
${theme.cssMixins.rowCentered};
margin-top: ${theme.spacing(6)};
& > img {
width: 220px;
}
`,
description: css`
margin-top: ${theme.spacing(4)};
font-weight: 400;
font-size: 16px;
color: ${theme.palette.text.secondary};
padding: ${theme.spacing(0, 1)};
`,
}

const lockupEndTime = dayjs()
.add(poolState.withdrawalLockupPeriodDays ?? 0, 'day')
.date(1)
const withdrawTime = lockupEndTime.add(1, 'month')

return (
<WrapperModal title='Auto-Redemption'>
<Box css={styles.iconWrapper}>
<img src={AutoPaybackImg} alt='auto-payback' />
</Box>
<Box css={styles.description}>
This allowance transaction will enable auto-redemption for your existing
tranche shares. Redemption requests will be automatically submitted on{' '}
{timeUtil.timestampToLL(lockupEndTime.unix())}. Your deposit can be
redeemed and yield rewards will stop on{' '}
{timeUtil.timestampToLL(withdrawTime.unix())}.
</Box>
<BottomButton variant='contained' onClick={handleNext}>
APPROVE ALLOWANCE
</BottomButton>
</WrapperModal>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
getSentinelAddress,
getTokenAccounts,
SolanaPoolInfo,
} from '@huma-finance/shared'
import React, { useCallback, useEffect, useState } from 'react'

import {
notEnabledAutoRedeem,
SolanaPoolState,
useHumaProgram,
useLenderAccounts,
useTrancheTokenAccounts,
} from '@huma-finance/web-shared'
import {
createApproveCheckedInstruction,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token'
import { useWallet } from '@solana/wallet-adapter-react'
import { PublicKey, Transaction } from '@solana/web3.js'
import { useAppDispatch } from '../../../hooks/useRedux'
import { setError, setStep } from '../../../store/widgets.reducers'
import { WIDGET_STEP } from '../../../store/widgets.store'
import { LoadingModal } from '../../LoadingModal'
import { SolanaTxSendModal } from '../../SolanaTxSendModal'

type Props = {
poolInfo: SolanaPoolInfo
poolState: SolanaPoolState
}

export function Transfer({
poolInfo,
poolState,
}: Props): React.ReactElement | null {
const dispatch = useAppDispatch()
const { publicKey } = useWallet()
const sentinel = getSentinelAddress(poolInfo.chainId)
const [transaction, setTransaction] = useState<Transaction>()
const {
juniorLenderApprovedAccountPDA,
seniorLenderApprovedAccountPDA,
seniorLenderStateAccount,
juniorLenderStateAccount,
seniorTrancheMintSupply,
juniorTrancheMintSupply,
loading: isLoadingLenderAccounts,
} = useLenderAccounts(poolInfo.chainId, poolInfo.poolName)
const {
seniorTokenAccount,
juniorTokenAccount,
loading: isLoadingTrancheTokenAccounts,
} = useTrancheTokenAccounts(poolInfo)
const program = useHumaProgram(poolInfo.chainId)

const handleSuccess = useCallback(() => {
dispatch(setStep(WIDGET_STEP.Done))
}, [dispatch])

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

const tx = new Transaction()

const { seniorTrancheATA, juniorTrancheATA } = getTokenAccounts(
poolInfo,
publicKey,
)
const poolAuthorityPubkey = new PublicKey(poolInfo.poolAuthority)

if (!seniorTokenAccount?.amount && !juniorTokenAccount?.amount) {
dispatch(
setError({ errorMessage: 'Error reading tranche token balance' }),
)
return
}
if (
notEnabledAutoRedeem(
seniorTokenAccount,
poolAuthorityPubkey,
seniorTokenAccount?.amount,
)
) {
tx.add(
createApproveCheckedInstruction(
seniorTrancheATA,
new PublicKey(poolInfo.seniorTrancheMint),
poolAuthorityPubkey, // delegate
publicKey, // owner of the wallet
BigInt(seniorTokenAccount?.amount.toString() ?? 0), // amount
poolInfo.trancheDecimals,
undefined, // multiSigners
TOKEN_2022_PROGRAM_ID,
),
)
}
if (
notEnabledAutoRedeem(
juniorTokenAccount,
poolAuthorityPubkey,
juniorTokenAccount?.amount,
)
) {
tx.add(
createApproveCheckedInstruction(
juniorTrancheATA,
new PublicKey(poolInfo.juniorTrancheMint),
poolAuthorityPubkey, // delegate
publicKey, // owner of the wallet
BigInt(juniorTokenAccount?.amount.toString() ?? 0), // amount
poolInfo.trancheDecimals,
undefined, // multiSigners
TOKEN_2022_PROGRAM_ID,
),
)
}
if (!tx.instructions.length) {
dispatch(
setError({ errorMessage: 'No tranches require Auto-Redemption' }),
)
return
}

setTransaction(tx)
}
getTx()
}, [
dispatch,
isLoadingLenderAccounts,
isLoadingTrancheTokenAccounts,
juniorLenderApprovedAccountPDA,
juniorLenderStateAccount,
juniorTokenAccount,
juniorTrancheMintSupply,
poolInfo,
poolState.juniorTrancheAssets,
poolState.seniorTrancheAssets,
program.methods,
publicKey,
seniorLenderApprovedAccountPDA,
seniorLenderStateAccount,
seniorTokenAccount,
seniorTrancheMintSupply,
sentinel,
transaction,
])

if (isLoadingLenderAccounts || isLoadingTrancheTokenAccounts) {
return <LoadingModal title='Auto-Redeem' />
}

return (
<SolanaTxSendModal
tx={transaction}
chainId={poolInfo.chainId}
handleSuccess={handleSuccess}
/>
)
}
Loading

0 comments on commit d4756b7

Please sign in to comment.