diff --git a/Strategies/components/DepositModal.tsx b/Strategies/components/DepositModal.tsx index 3ac535c1a1..f8c23c3da4 100644 --- a/Strategies/components/DepositModal.tsx +++ b/Strategies/components/DepositModal.tsx @@ -4,6 +4,7 @@ import SolendModalContent from './SolendModalContent' import MangoDeposit from './MangoDepositComponent' import BigNumber from 'bignumber.js' import { SolendStrategy } from 'Strategies/types/types' +import EverlendModalContent from './EverlendModalContent' const DepositModal = ({ onClose, @@ -49,6 +50,14 @@ const DepositModal = ({ createProposalFcn={createProposalFcn} > ) : null} + {protocolName === 'Everlend' ? ( + + ) : null} ) } diff --git a/Strategies/components/EverlendModalContent.tsx b/Strategies/components/EverlendModalContent.tsx new file mode 100644 index 0000000000..5bed1195ec --- /dev/null +++ b/Strategies/components/EverlendModalContent.tsx @@ -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 ( +
+
+ setSelectedTab(tab)} + values={tabs} + /> +
+ {selectedTab === Tabs.DEPOSIT && ( + + )} + {selectedTab === Tabs.WITHDRAW && ( + + )} +
+ ) +} + +export default EverlendModalContent diff --git a/Strategies/components/everlend/EverlendDeposit.tsx b/Strategies/components/everlend/EverlendDeposit.tsx new file mode 100644 index 0000000000..9b1565e283 --- /dev/null +++ b/Strategies/components/everlend/EverlendDeposit.tsx @@ -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 ( +
+
+ Amount +
+ Bal:{' '} + {Number(maxAmountFormatted)} + { + setAmount(Number(maxAmountFormatted)) + }} + className="font-bold ml-2 text-primary-light" + > + Max + +
+
+ setAmount(e.target.value)} + value={amount} + onBlur={validateAmountOnBlur} + error={formErrors['amount']} + /> + setProposalInfo((prev) => ({ ...prev, title: evt }))} + setDescription={(evt) => + setProposalInfo((prev) => ({ ...prev, description: evt })) + } + voteByCouncil={voteByCouncil} + setVoteByCouncil={setVoteByCouncil} + /> +
+
+ Current Deposits + + {depositedAmount}{' '} + {tokenSymbol} + +
+
+ Proposed Deposit + + {amount?.toLocaleString() || ( + Enter an amount + )}{' '} + + {amount && tokenSymbol} + + +
+
+
+ +
+
+ ) +} + +export default EverlendDeposit diff --git a/Strategies/components/everlend/EverlendWithdraw.tsx b/Strategies/components/everlend/EverlendWithdraw.tsx new file mode 100644 index 0000000000..9592af7875 --- /dev/null +++ b/Strategies/components/everlend/EverlendWithdraw.tsx @@ -0,0 +1,231 @@ +import { useState } from 'react' +import Button, { LinkButton } from '@components/Button' +import Input from '@components/inputs/Input' +import Tooltip from '@components/Tooltip' +import { RpcContext } from '@solana/spl-governance' +import { PublicKey } from '@solana/web3.js' +import { getProgramVersionForRealm } from '@models/registry/api' +import { + getMintMinAmountAsDecimal, + getMintNaturalAmountFromDecimalAsBN, +} from '@tools/sdk/units' +import { CreateEverlendProposal } from '../../protocols/everlend/tools' +import { AssetAccount } from '@utils/uiTypes/assets' +import useRealm from '@hooks/useRealm' +import useWalletStore from 'stores/useWalletStore' +import useVotePluginsClientStore from 'stores/useVotePluginsClientStore' +import useQueryContext from '@hooks/useQueryContext' +import { useRouter } from 'next/router' +import AdditionalProposalOptions from '@components/AdditionalProposalOptions' +import tokenService from '@utils/services/token' +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 EverlendWithdraw = ({ + proposedInvestment, + createProposalFcn, + governedTokenAccount, + depositedAmount, +}: IProps) => { + const [amount, setAmount] = useState(0) + const [isWithdrawing, setIsWithdrawing] = useState(false) + const [proposalInfo, setProposalInfo] = useState({ + title: '', + description: '', + }) + const [formErrors, setFormErrors] = useState({}) + + const { + realmInfo, + realm, + mint, + councilMint, + ownVoterWeight, + symbol, + } = useRealm() + const { canUseTransferInstruction } = useGovernanceAssets() + const [voteByCouncil, setVoteByCouncil] = useState(false) + const client = useVotePluginsClientStore( + (s) => s.state.currentRealmVotingClient + ) + const { fmtUrlWithCluster } = useQueryContext() + const connection = useWalletStore((s) => s.connection) + const wallet = useWalletStore((s) => s.current) + const router = useRouter() + + const tokenSymbol = tokenService.getTokenInfo( + governedTokenAccount.extensions.mint!.publicKey.toBase58() + )?.symbol + + const proposalTitle = `Withdraw ${amount} ${ + tokenSymbol || 'tokens' + } from the Everlend pool` + + const mintInfo = governedTokenAccount.extensions?.mint?.account + const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1 + const currentPrecision = precision(mintMinAmount) + + const handleWithdraw = async () => { + const isValid = await validateInstruction({ + schema, + form: { amount }, + setFormErrors, + }) + if (!isValid) { + return + } + try { + setIsWithdrawing(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: 'Withdraw', + 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) + } + setIsWithdrawing(false) + } + + const schema = yup.object().shape({ + amount: yup.number().required('Amount is required').max(depositedAmount), + }) + + const validateAmountOnBlur = () => { + setAmount( + parseFloat( + Math.max( + Number(mintMinAmount), + Math.min(Number(Number.MAX_SAFE_INTEGER), Number(amount)) + ).toFixed(currentPrecision) + ) + ) + } + + return ( +
+
+ Amount +
+ Bal: {depositedAmount} + setAmount(depositedAmount)} + className="font-bold ml-2 text-primary-light" + > + Max + +
+
+ + setAmount(e.target.value)} + value={amount} + onBlur={validateAmountOnBlur} + error={formErrors['amount']} + /> + + setProposalInfo((prev) => ({ ...prev, title: evt }))} + setDescription={(evt) => + setProposalInfo((prev) => ({ ...prev, description: evt })) + } + voteByCouncil={voteByCouncil} + setVoteByCouncil={setVoteByCouncil} + /> +
+
+ Current Deposits + + {depositedAmount}{' '} + {tokenSymbol} + +
+
+ Proposed Deposit + + {amount?.toLocaleString() || ( + Enter an amount + )}{' '} + + {amount && tokenSymbol} + + +
+
+ +
+ +
+
+ ) +} + +export default EverlendWithdraw diff --git a/Strategies/protocols/everlend/preparedSolDepositTx.ts b/Strategies/protocols/everlend/preparedSolDepositTx.ts new file mode 100644 index 0000000000..7b32b72973 --- /dev/null +++ b/Strategies/protocols/everlend/preparedSolDepositTx.ts @@ -0,0 +1,136 @@ +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, +} from '@solana/web3.js' +import BN from 'bn.js' +import { DepositTx, Pool } from '@everlend/general-pool' +import { GeneralPoolsProgram } from '@everlend/general-pool' +import { + CreateAssociatedTokenAccount, + findAssociatedTokenAccount, + findRegistryPoolConfigAccount, +} from '@everlend/common' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + NATIVE_MINT, + Token, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { syncNative } from '@solendprotocol/solend-sdk' + +export type ActionOptions = { + /** the JSON RPC connection instance. */ + connection: Connection + /** the fee payer public key, can be user's SOL address (owner address). */ + payerPublicKey: PublicKey +} + +export type ActionResult = { + /** the prepared transaction, ready for signing and sending. */ + tx: Transaction + /** the additional key pairs which may be needed for signing and sending transactions. */ + keypairs?: Record +} + +export const prepareSolDepositTx = async ( + { connection, payerPublicKey }: ActionOptions, + pool: PublicKey, + registry: PublicKey, + amount: BN, + source: PublicKey, + destination: PublicKey +): Promise => { + const { + data: { poolMarket, tokenAccount, poolMint, tokenMint }, + } = await Pool.load(connection, pool) + + const poolMarketAuthority = await GeneralPoolsProgram.findProgramAddress([ + poolMarket.toBuffer(), + ]) + + const tx = new Transaction() + const registryPoolConfig = await findRegistryPoolConfigAccount(registry, pool) + + console.log('source (ctoken)', source.toString()) + console.log('dest (liquidity)', destination.toString()) + + // Wrapping SOL + const depositAccountInfo = await connection.getAccountInfo(source) + console.log({ depositAccountInfo }) + if (!depositAccountInfo) { + // generate the instruction for creating the ATA + const createAtaInst = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + new PublicKey(tokenMint), + source, + payerPublicKey, + payerPublicKey + ) + tx.add(createAtaInst) + } + + const userWSOLAccountInfo = await connection.getAccountInfo(destination) + + const rentExempt = await Token.getMinBalanceRentForExemptAccount(connection) + + const transferLamportsIx = SystemProgram.transfer({ + fromPubkey: payerPublicKey, + toPubkey: source, + lamports: (userWSOLAccountInfo ? 0 : rentExempt) + amount.toNumber(), + }) + + tx.add(transferLamportsIx) + + if (!userWSOLAccountInfo) { + const createUserWSOLAccountIx = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + NATIVE_MINT, + source, + payerPublicKey, + payerPublicKey + ) + tx.add(createUserWSOLAccountIx) + } else { + const syncIx = syncNative(source) + tx.add(syncIx) + } + + // Create destination account for pool mint if doesn't exist + destination = + destination ?? (await findAssociatedTokenAccount(payerPublicKey, poolMint)) + !(await connection.getAccountInfo(destination)) && + tx.add( + new CreateAssociatedTokenAccount( + { feePayer: payerPublicKey }, + { + associatedTokenAddress: destination, + tokenMint: poolMint, + } + ) + ) + + tx.add( + new DepositTx( + { feePayer: payerPublicKey }, + { + registryPoolConfig, + registry, + poolMarket, + pool, + source, + destination, + tokenAccount, + poolMint, + poolMarketAuthority, + amount, + } + ) + ) + + return { tx } +} diff --git a/Strategies/protocols/everlend/tools.ts b/Strategies/protocols/everlend/tools.ts new file mode 100644 index 0000000000..cf64c7254e --- /dev/null +++ b/Strategies/protocols/everlend/tools.ts @@ -0,0 +1,313 @@ +import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' +import { + getInstructionDataFromBase64, + ProgramAccount, + Realm, + RpcContext, + serializeInstructionToBase64, + TokenOwnerRecord, +} from '@solana/spl-governance' +import { BN } from '@project-serum/anchor' +import { AssetAccount } from '@utils/uiTypes/assets' +import { ConnectionContext } from '@utils/connection' +import { VotingClient } from '@utils/uiTypes/VotePlugin' +import { + createProposal, + InstructionDataWithHoldUpTime, +} from 'actions/createProposal' +import tokenService from '@utils/services/token' +import { + prepareDepositTx, + prepareWithdrawalRequestTx, + Pool, +} from '@everlend/general-pool' +import axios from 'axios' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { prepareSolDepositTx } from './preparedSolDepositTx' + +const MARKET_MAIN = 'DzGDoJHdzUANM7P7V25t5nxqbvzRcHDmdhY51V6WNiXC' +const MARKET_DEV = '4yC3cUWXQmoyyybfnENpxo33hiNxUNa1YAmmuxz93WAJ' +const REGISTRY_DEV = '6KCHtgSGR2WDE3aqrqSJppHRGVPgy9fHDX5XD8VZgb61' +const REGISTRY_MAIN = 'UaqUGgMvVzUZLthLHC9uuuBzgw5Ldesich94Wu5pMJg' +const ENDPOINT_MAIN = 'https://api.everlend.finance/api/v1/' +const ENDPOINT_DEV = 'https://dev-api.everlend.finance/api/v1/' +export const EVERLEND = 'Everlend' + +async function getAPYs(isDev = false) { + const api = axios.create({ + baseURL: isDev ? ENDPOINT_DEV : ENDPOINT_MAIN, + timeout: 30000, + }) + + return api.get('apy') +} + +async function getStrategies(connection: ConnectionContext) { + const isDev = connection.cluster === 'devnet' + const POOL_MARKET_PUBKEY = new PublicKey(isDev ? MARKET_DEV : MARKET_MAIN) + + try { + const response = await Pool.findMany(connection.current, { + poolMarket: POOL_MARKET_PUBKEY, + }) + + const apys = await getAPYs(isDev) + + const strategies = response.map((pool) => { + const { tokenMint, poolMint } = pool.data + const tokenInfo = tokenService.getTokenInfo(tokenMint.toString()) + const apy = + apys.data.find((apy) => apy.token === tokenInfo?.symbol)?.supply_apy * + 100 ?? 0 + return { + handledMint: tokenMint.toString(), + createProposalFcn: handleEverlendAction, + protocolLogoSrc: '/realms/Everlend/img/logo.png', + protocolName: 'Everlend', + protocolSymbol: 'evd', + isGenericItem: false, + poolMint: poolMint.toString(), + poolPubKey: pool.publicKey.toString(), + strategyDescription: '', + strategyName: 'Deposit', + handledTokenSymbol: tokenInfo?.symbol, + handledTokenImgSrc: tokenInfo?.logoURI, + apy: apy.toFixed(2).concat('%'), + } + }) + + return strategies + } catch (e) { + console.error(e) + } +} + +export async function handleEverlendAction( + rpcContext: RpcContext, + form: { + action: 'Deposit' | 'Withdraw' + title: string + description: string + bnAmount: BN + poolPubKey: string + tokenMint: string + poolMint: string + }, + realm: ProgramAccount, + matchedTreasury: AssetAccount, + tokenOwnerRecord: ProgramAccount, + governingTokenMint: PublicKey, + proposalIndex: number, + isDraft: boolean, + connection: ConnectionContext, + client?: VotingClient +) { + const isSol = matchedTreasury.isSol + const insts: InstructionDataWithHoldUpTime[] = [] + const owner = isSol + ? matchedTreasury!.pubkey + : matchedTreasury!.extensions!.token!.account.owner + const REGISTRY = new PublicKey( + connection.cluster === 'mainnet' ? REGISTRY_MAIN : REGISTRY_DEV + ) + + const ctokenATA = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + new PublicKey(form.tokenMint), + owner, + true + ) + + const liquidityATA = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + new PublicKey(form.poolMint), + owner, + true + ) + + const setupInsts: InstructionDataWithHoldUpTime[] = [] + const cleanupInsts: InstructionDataWithHoldUpTime[] = [] + + if (form.action === 'Deposit') { + const actionTx = await handleEverlendDeposit( + Boolean(isSol), + connection, + owner, + REGISTRY, + form.poolPubKey, + form.bnAmount, + ctokenATA, + liquidityATA + ) + actionTx.instructions.map((instruction) => { + insts.push({ + data: getInstructionDataFromBase64( + serializeInstructionToBase64(instruction) + ), + holdUpTime: matchedTreasury.governance!.account!.config + .minInstructionHoldUpTime, + prerequisiteInstructions: [], + }) + }) + } else if (form.action === 'Withdraw') { + const { withdrawTx, closeIx } = await handleEverlendWithdraw( + Boolean(isSol), + connection, + owner, + REGISTRY, + form.poolPubKey, + form.bnAmount, + liquidityATA, + ctokenATA + ) + + withdrawTx.instructions.map((instruction) => { + insts.push({ + data: getInstructionDataFromBase64( + serializeInstructionToBase64(instruction) + ), + holdUpTime: matchedTreasury.governance!.account!.config + .minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkSplitByDefault: true, + }) + }) + + if (closeIx) { + cleanupInsts.push({ + data: getInstructionDataFromBase64( + serializeInstructionToBase64(closeIx) + ), + holdUpTime: matchedTreasury.governance!.account!.config + .minInstructionHoldUpTime, + prerequisiteInstructions: [], + chunkSplitByDefault: true, + }) + } + } + + const proposalAddress = await createProposal( + rpcContext, + realm, + matchedTreasury.governance!.pubkey, + tokenOwnerRecord, + form.title, + form.description, + governingTokenMint, + proposalIndex, + [...setupInsts, ...insts, ...cleanupInsts], + isDraft, + client + ) + return proposalAddress +} + +async function handleEverlendDeposit( + isSol: boolean, + connection: ConnectionContext, + owner: PublicKey, + REGISTRY: PublicKey, + poolPubKey: string, + amount: BN, + source: PublicKey, + destination: PublicKey +) { + let actionTx: Transaction + if (isSol) { + const { tx: depositTx } = await prepareSolDepositTx( + { connection: connection.current, payerPublicKey: owner }, + new PublicKey(poolPubKey), + REGISTRY, + amount, + source, + destination + ) + actionTx = depositTx + } else { + const { tx: depositTx } = await prepareDepositTx( + { connection: connection.current, payerPublicKey: owner }, + new PublicKey(poolPubKey), + REGISTRY, + amount, + source + ) + actionTx = depositTx + } + return actionTx +} + +async function handleEverlendWithdraw( + isSol: boolean, + connection: ConnectionContext, + owner: PublicKey, + REGISTRY: PublicKey, + poolPubKey: string, + amount: BN, + source: PublicKey, + destination: PublicKey +) { + const { tx: withdrawslTx } = await prepareWithdrawalRequestTx( + { + connection: connection.current, + payerPublicKey: owner, + }, + new PublicKey(poolPubKey), + REGISTRY, + amount, + source, + isSol ? owner : undefined + ) + const withdrawTx = withdrawslTx + let closeIx: TransactionInstruction | undefined + if (isSol) { + const closeWSOLAccountIx = Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + destination, + owner, + owner, + [] + ) + closeIx = closeWSOLAccountIx + } + + return { + withdrawTx, + closeIx: closeIx ?? null, + } +} + +export async function getEverlendStrategies( + connection: ConnectionContext +): Promise { + const strategies = await getStrategies(connection) + + return strategies +} + +export type CreateEverlendProposal = ( + rpcContext: RpcContext, + form: { + action: 'Deposit' | 'Withdraw' + title: string + description: string + bnAmount: BN + amountFmt: string + poolPubKey: string + tokenMint: string + poolMint: string + }, + realm: ProgramAccount, + matchedTreasury: AssetAccount, + tokenOwnerRecord: ProgramAccount, + governingTokenMint: PublicKey, + proposalIndex: number, + isDraft: boolean, + connection: ConnectionContext, + client?: VotingClient +) => Promise diff --git a/Strategies/store/useStrategiesStore.tsx b/Strategies/store/useStrategiesStore.tsx index cb466bf3df..2e408b0d4d 100644 --- a/Strategies/store/useStrategiesStore.tsx +++ b/Strategies/store/useStrategiesStore.tsx @@ -4,6 +4,7 @@ import { tvl } from 'Strategies/protocols/mango/tools' import { getSolendStrategies } from 'Strategies/protocols/solend' import { TreasuryStrategy } from 'Strategies/types/types' import create, { State } from 'zustand' +import { getEverlendStrategies } from '../protocols/everlend/tools' interface StrategiesStore extends State { strategies: TreasuryStrategy[] @@ -21,9 +22,10 @@ const useStrategiesStore = create((set, _get) => ({ try { const mango = await tvl(Date.now() / 1000, connection) const solend = await getSolendStrategies() + const everlend = await getEverlendStrategies(connection) //add fetch functions for your protocol in promise.all - const strategies: TreasuryStrategy[] = [...solend, ...mango] + const strategies: TreasuryStrategy[] = [...solend, ...mango, ...everlend] set((s) => { s.strategies = strategies diff --git a/components/TreasuryAccount/AccountOverview.tsx b/components/TreasuryAccount/AccountOverview.tsx index 3c683e6870..4790e22546 100644 --- a/components/TreasuryAccount/AccountOverview.tsx +++ b/components/TreasuryAccount/AccountOverview.tsx @@ -43,6 +43,8 @@ import { SOLEND, } from 'Strategies/protocols/solend' import tokenService from '@utils/services/token' +import { EVERLEND } from '../../Strategies/protocols/everlend/tools' +import { findAssociatedTokenAccount } from '@everlend/common' type InvestmentType = TreasuryStrategy & { investedAmount: number @@ -179,6 +181,32 @@ const AccountOverview = () => { return [] } + const handleEverlendAccounts = async (): Promise => { + const everlendStrategy = visibleInvestments.filter( + (strat) => strat.protocolName === EVERLEND + )[0] + + const tokenMintATA = await findAssociatedTokenAccount( + isSol + ? currentAccount!.pubkey + : currentAccount!.extensions!.token!.account.owner, + + new PublicKey( + (everlendStrategy as TreasuryStrategy & { poolMint: string }).poolMint + ) + ) + const tokenMintATABalance = await connection.current.getTokenAccountBalance( + tokenMintATA + ) + + return [ + { + ...everlendStrategy, + investedAmount: Number(tokenMintATABalance.value.uiAmount), + }, + ].filter((strat) => strat.investedAmount !== 0) + } + const loadData = async () => { const requests = [] as Array>> if (visibleInvestments.filter((x) => x.protocolName === MANGO).length) { @@ -188,6 +216,12 @@ const AccountOverview = () => { requests.push(getSlndCTokens()) } + if ( + visibleInvestments.filter((x) => x.protocolName === EVERLEND).length + ) { + requests.push(handleEverlendAccounts()) + } + const results = await Promise.all(requests) setLoading(false) diff --git a/package.json b/package.json index 2f5ed27ebf..5045550d78 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@dialectlabs/react-ui": "0.8.2", "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", + "@everlend/general-pool": "^0.0.19", "@foresight-tmp/foresight-sdk": "^0.1.46", "@friktion-labs/friktion-sdk": "^1.1.118", "@headlessui/react": "^1.6.4", diff --git a/public/realms/Everlend/img/logo.png b/public/realms/Everlend/img/logo.png new file mode 100644 index 0000000000..2c9dd216e2 Binary files /dev/null and b/public/realms/Everlend/img/logo.png differ diff --git a/yarn.lock b/yarn.lock index 9ad20efeff..c6c69ceb71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -972,6 +972,29 @@ "@ethersproject/logger" "^5.5.0" hash.js "1.1.7" +"@everlend/common@workspace:^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@everlend/common/-/common-0.0.3.tgz#b18d7e025592a6f863dc84abf563ca2b25df0ac2" + integrity sha512-hj729mHUw250kJNKgdhfK/ZLiUJOSze2IquGyNs6qooMe856Au9E/vi3RlkuCzDMe3nkvvC8mpRzjzRmkwpJWw== + dependencies: + "@solana/spl-token" "^0.1.5" + "@solana/web3.js" "^1.16.1" + borsh "^0.6.0" + bs58 "^4.0.1" + buffer "^6.0.3" + +"@everlend/general-pool@^0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@everlend/general-pool/-/general-pool-0.0.19.tgz#35c449cee0dfd9f9d3c49f1f3dbbd7cc59927634" + integrity sha512-GoEntJ9fchb4QUd/cLF1unpovlaWZEcEMcMiavClQfM49HR9U3VglUYGhQJ/6uRABnr43qH3YfMTccnZ7y/lvw== + dependencies: + "@everlend/common" "workspace:^0.0.3" + "@solana/spl-token" "^0.1.5" + "@solana/web3.js" "^1.16.1" + borsh "^0.6.0" + bs58 "^4.0.1" + buffer "^6.0.3" + "@fast-csv/format@4.3.5": version "4.3.5" resolved "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz" @@ -2784,7 +2807,7 @@ buffer-layout "^1.2.0" dotenv "10.0.0" -"@solana/spl-token@0.1.8", "@solana/spl-token@^0.1.6", "@solana/spl-token@^0.1.8": +"@solana/spl-token@0.1.8", "@solana/spl-token@^0.1.5", "@solana/spl-token@^0.1.6", "@solana/spl-token@^0.1.8": version "0.1.8" resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.8.tgz#f06e746341ef8d04165e21fc7f555492a2a0faa6" integrity sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ== @@ -2977,6 +3000,28 @@ superstruct "^0.14.2" tweetnacl "^1.0.0" +"@solana/web3.js@^1.16.1": + version "1.44.2" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.44.2.tgz#5303efd94a7f2d6054a1483a4b4db4a26eb2a392" + integrity sha512-DvrJMoKonLuaX0/KyyJXcP/+w+9q8mve4gN3hC2Ptg51K/Gi1/cx6oQN2lbRZb4wYPBd2s2GDAJAJUAwZGsEug== + dependencies: + "@babel/runtime" "^7.12.5" + "@ethersproject/sha2" "^5.5.0" + "@solana/buffer-layout" "^4.0.0" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.1" + fast-stable-stringify "^1.0.0" + jayson "^3.4.4" + js-sha3 "^0.8.0" + node-fetch "2" + rpc-websockets "^7.4.2" + secp256k1 "^4.0.2" + superstruct "^0.14.2" + tweetnacl "^1.0.0" + "@solana/web3.js@^1.32.0": version "1.41.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.41.4.tgz#595aa29a4a61c181b8c8f5cf0bbef80b4739cfab"