forked from solana-labs/governance-ui
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
11 changed files
with
1,102 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import ButtonGroup from '@components/ButtonGroup' | ||
import { useEffect, useState } from 'react' | ||
import { TreasuryStrategy } from 'Strategies/types/types' | ||
import { CreateEverlendProposal } from 'Strategies/protocols/everlend/tools' | ||
import { AssetAccount } from '@utils/uiTypes/assets' | ||
import EverlendDeposit from './everlend/EverlendDeposit' | ||
import EverlendWithdraw from './everlend/EverlendWithdraw' | ||
import { findAssociatedTokenAccount } from '@everlend/common' | ||
import { PublicKey } from '@solana/web3.js' | ||
import useWalletStore from 'stores/useWalletStore' | ||
|
||
enum Tabs { | ||
DEPOSIT = 'Deposit', | ||
WITHDRAW = 'Withdraw', | ||
} | ||
|
||
interface IProps { | ||
proposedInvestment: TreasuryStrategy & { poolMint: string } | ||
handledMint: string | ||
createProposalFcn: CreateEverlendProposal | ||
governedTokenAccount: AssetAccount | ||
} | ||
|
||
const EverlendModalContent = ({ | ||
proposedInvestment, | ||
handledMint, | ||
createProposalFcn, | ||
governedTokenAccount, | ||
}: IProps) => { | ||
const [selectedTab, setSelectedTab] = useState(Tabs.DEPOSIT) | ||
const [depositedAmount, setDepositedAmount] = useState(0) | ||
const tabs = Object.values(Tabs) | ||
const connection = useWalletStore((s) => s.connection) | ||
|
||
const isSol = governedTokenAccount.isSol | ||
const owner = isSol | ||
? governedTokenAccount!.pubkey | ||
: governedTokenAccount!.extensions!.token!.account.owner | ||
|
||
useEffect(() => { | ||
const loadMaxAmount = async () => { | ||
const tokenMintATA = await findAssociatedTokenAccount( | ||
owner, | ||
new PublicKey(proposedInvestment.poolMint) | ||
) | ||
const tokenMintATABalance = await connection.current.getTokenAccountBalance( | ||
tokenMintATA | ||
) | ||
setDepositedAmount(Number(tokenMintATABalance.value.uiAmount)) | ||
} | ||
loadMaxAmount() | ||
}, [proposedInvestment, handledMint]) | ||
|
||
return ( | ||
<div> | ||
<div className="pb-4"> | ||
<ButtonGroup | ||
activeValue={selectedTab} | ||
onChange={(tab) => setSelectedTab(tab)} | ||
values={tabs} | ||
/> | ||
</div> | ||
{selectedTab === Tabs.DEPOSIT && ( | ||
<EverlendDeposit | ||
proposedInvestment={proposedInvestment} | ||
createProposalFcn={createProposalFcn} | ||
governedTokenAccount={governedTokenAccount} | ||
handledMint={handledMint} | ||
depositedAmount={depositedAmount} | ||
/> | ||
)} | ||
{selectedTab === Tabs.WITHDRAW && ( | ||
<EverlendWithdraw | ||
proposedInvestment={proposedInvestment} | ||
createProposalFcn={createProposalFcn} | ||
governedTokenAccount={governedTokenAccount} | ||
handledMint={handledMint} | ||
depositedAmount={depositedAmount} | ||
/> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
export default EverlendModalContent |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
import Button, { LinkButton } from '@components/Button' | ||
import Tooltip from '@components/Tooltip' | ||
import Input from '@components/inputs/Input' | ||
import { useState } from 'react' | ||
import { useRouter } from 'next/router' | ||
import useQueryContext from '@hooks/useQueryContext' | ||
import useRealm from '@hooks/useRealm' | ||
import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' | ||
import useWalletStore from 'stores/useWalletStore' | ||
import tokenService from '@utils/services/token' | ||
import BN from 'bn.js' | ||
import { | ||
fmtMintAmount, | ||
getMintMinAmountAsDecimal, | ||
getMintNaturalAmountFromDecimalAsBN, | ||
} from '@tools/sdk/units' | ||
import { RpcContext } from '@solana/spl-governance' | ||
import { PublicKey } from '@solana/web3.js' | ||
import { getProgramVersionForRealm } from '@models/registry/api' | ||
import { AssetAccount } from '@utils/uiTypes/assets' | ||
import { CreateEverlendProposal } from '../../protocols/everlend/tools' | ||
import AdditionalProposalOptions from '@components/AdditionalProposalOptions' | ||
import * as yup from 'yup' | ||
import { precision } from '@utils/formatting' | ||
import { validateInstruction } from '@utils/instructionTools' | ||
import useGovernanceAssets from '@hooks/useGovernanceAssets' | ||
import Loading from '@components/Loading' | ||
|
||
interface IProps { | ||
proposedInvestment | ||
handledMint: string | ||
createProposalFcn: CreateEverlendProposal | ||
governedTokenAccount: AssetAccount | ||
depositedAmount: number | ||
} | ||
|
||
const EverlendDeposit = ({ | ||
proposedInvestment, | ||
createProposalFcn, | ||
governedTokenAccount, | ||
depositedAmount, | ||
}: IProps) => { | ||
const [amount, setAmount] = useState(0) | ||
const tokenSymbol = tokenService.getTokenInfo( | ||
governedTokenAccount.extensions.mint!.publicKey.toBase58() | ||
)?.symbol | ||
|
||
const proposalTitle = `Deposit ${amount} ${ | ||
tokenSymbol || 'tokens' | ||
} to the Everlend pool` | ||
|
||
const [proposalInfo, setProposalInfo] = useState({ | ||
title: '', | ||
description: '', | ||
}) | ||
const [formErrors, setFormErrors] = useState({}) | ||
const [isDepositing, setIsDepositing] = useState(false) | ||
const router = useRouter() | ||
const { fmtUrlWithCluster } = useQueryContext() | ||
const { | ||
realmInfo, | ||
realm, | ||
mint, | ||
councilMint, | ||
ownVoterWeight, | ||
symbol, | ||
} = useRealm() | ||
const [voteByCouncil, setVoteByCouncil] = useState(false) | ||
const client = useVotePluginsClientStore( | ||
(s) => s.state.currentRealmVotingClient | ||
) | ||
const connection = useWalletStore((s) => s.connection) | ||
const wallet = useWalletStore((s) => s.current) | ||
|
||
const { canUseTransferInstruction } = useGovernanceAssets() | ||
|
||
const treasuryAmount = new BN( | ||
governedTokenAccount.isSol | ||
? governedTokenAccount.extensions.amount!.toNumber() | ||
: governedTokenAccount.extensions.token!.account.amount | ||
) | ||
|
||
const mintInfo = governedTokenAccount.extensions?.mint?.account | ||
|
||
const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1 | ||
const currentPrecision = precision(mintMinAmount) | ||
const maxAmountFormatted = fmtMintAmount(mintInfo, treasuryAmount) | ||
|
||
const handleDeposit = async () => { | ||
const isValid = await validateInstruction({ | ||
schema, | ||
form: { amount }, | ||
setFormErrors, | ||
}) | ||
if (!isValid) { | ||
return | ||
} | ||
try { | ||
setIsDepositing(true) | ||
const rpcContext = new RpcContext( | ||
new PublicKey(realm!.owner), | ||
getProgramVersionForRealm(realmInfo!), | ||
wallet!, | ||
connection.current, | ||
connection.endpoint | ||
) | ||
const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal( | ||
governedTokenAccount!.governance!.account.config, | ||
voteByCouncil | ||
) | ||
const defaultProposalMint = voteByCouncil | ||
? realm?.account.config.councilMint | ||
: !mint?.supply.isZero() || | ||
realm?.account.config.useMaxCommunityVoterWeightAddin | ||
? realm!.account.communityMint | ||
: !councilMint?.supply.isZero() | ||
? realm!.account.config.councilMint | ||
: undefined | ||
|
||
const proposalAddress = await createProposalFcn( | ||
rpcContext, | ||
{ | ||
title: proposalInfo.title || proposalTitle, | ||
description: proposalInfo.description, | ||
amountFmt: String(amount), | ||
bnAmount: getMintNaturalAmountFromDecimalAsBN( | ||
amount as number, | ||
governedTokenAccount.extensions.mint!.account.decimals | ||
), | ||
action: 'Deposit', | ||
poolPubKey: proposedInvestment.poolPubKey, | ||
tokenMint: proposedInvestment.handledMint, | ||
poolMint: proposedInvestment.poolMint, | ||
}, | ||
realm!, | ||
governedTokenAccount!, | ||
ownTokenRecord, | ||
defaultProposalMint!, | ||
governedTokenAccount!.governance!.account!.proposalCount, | ||
false, | ||
connection, | ||
client | ||
) | ||
const url = fmtUrlWithCluster( | ||
`/dao/${symbol}/proposal/${proposalAddress}` | ||
) | ||
router.push(url) | ||
} catch (e) { | ||
console.error(e) | ||
} | ||
setIsDepositing(false) | ||
} | ||
|
||
const schema = yup.object().shape({ | ||
amount: yup | ||
.number() | ||
.required('Amount is required') | ||
.max(Number(maxAmountFormatted)), | ||
}) | ||
|
||
const validateAmountOnBlur = () => { | ||
setAmount( | ||
parseFloat( | ||
Math.max( | ||
Number(mintMinAmount), | ||
Math.min(Number(Number.MAX_SAFE_INTEGER), Number(amount)) | ||
).toFixed(currentPrecision) | ||
) | ||
) | ||
} | ||
|
||
return ( | ||
<div> | ||
<div className="flex my-1.5 text-sm"> | ||
Amount | ||
<div className="ml-auto flex items-center text-xs"> | ||
<span className="text-fgd-3 mr-1">Bal:</span>{' '} | ||
{Number(maxAmountFormatted)} | ||
<LinkButton | ||
onClick={() => { | ||
setAmount(Number(maxAmountFormatted)) | ||
}} | ||
className="font-bold ml-2 text-primary-light" | ||
> | ||
Max | ||
</LinkButton> | ||
</div> | ||
</div> | ||
<Input | ||
type="number" | ||
onChange={(e) => setAmount(e.target.value)} | ||
value={amount} | ||
onBlur={validateAmountOnBlur} | ||
error={formErrors['amount']} | ||
/> | ||
<AdditionalProposalOptions | ||
title={proposalInfo.title} | ||
description={proposalInfo.description} | ||
defaultTitle={proposalTitle} | ||
defaultDescription={`Deposit ${tokenSymbol} into Everlend to mint cTokens and earn interest`} | ||
setTitle={(evt) => setProposalInfo((prev) => ({ ...prev, title: evt }))} | ||
setDescription={(evt) => | ||
setProposalInfo((prev) => ({ ...prev, description: evt })) | ||
} | ||
voteByCouncil={voteByCouncil} | ||
setVoteByCouncil={setVoteByCouncil} | ||
/> | ||
<div className="border border-fgd-4 p-4 rounded-md mb-6 mt-4 space-y-1 text-sm"> | ||
<div className="flex justify-between"> | ||
<span className="text-fgd-3">Current Deposits</span> | ||
<span className="font-bold text-fgd-1"> | ||
{depositedAmount}{' '} | ||
<span className="font-normal text-fgd-3">{tokenSymbol}</span> | ||
</span> | ||
</div> | ||
<div className="flex justify-between"> | ||
<span className="text-fgd-3">Proposed Deposit</span> | ||
<span className="font-bold text-fgd-1"> | ||
{amount?.toLocaleString() || ( | ||
<span className="font-normal text-red">Enter an amount</span> | ||
)}{' '} | ||
<span className="font-normal text-fgd-3"> | ||
{amount && tokenSymbol} | ||
</span> | ||
</span> | ||
</div> | ||
</div> | ||
<div className="mt-4"> | ||
<Button | ||
disabled={!amount || !canUseTransferInstruction || isDepositing} | ||
onClick={() => handleDeposit()} | ||
className="w-full" | ||
> | ||
<Tooltip content={''}> | ||
{' '} | ||
{!isDepositing ? 'Propose deposit' : <Loading></Loading>} | ||
</Tooltip> | ||
</Button> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default EverlendDeposit |
Oops, something went wrong.