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"