From e95676c77804f4c5af9d6977ad8d76ea2417486c Mon Sep 17 00:00:00 2001
From: 0xodia <89805726+0xodia@users.noreply.github.com>
Date: Tue, 14 Jun 2022 12:54:14 -0400
Subject: [PATCH] Add Solend to treasury investments UI (#749)
* add withdraw
* complete solend investment integration
* DRY investment mint
* remove console.log
* fix case where token has no strategy in main market
* fix case where token has no strategy in main market for withdraw too
---
Strategies/components/DepositModal.tsx | 16 +-
Strategies/components/SolendModalContent.tsx | 57 ++
.../components/solend/SolendDeposit.tsx | 376 +++++++++++++
.../components/solend/SolendWithdraw.tsx | 381 +++++++++++++
Strategies/protocols/solend/index.ts | 511 ++++++++++++++++++
Strategies/store/useStrategiesStore.tsx | 6 +-
Strategies/types/types.ts | 14 +
components/AdditionalProposalOptions.tsx | 3 +
.../TreasuryAccount/AccountOverview.tsx | 195 +++++--
.../programs/associatedTokenAccount.tsx | 2 +-
10 files changed, 1514 insertions(+), 47 deletions(-)
create mode 100644 Strategies/components/SolendModalContent.tsx
create mode 100644 Strategies/components/solend/SolendDeposit.tsx
create mode 100644 Strategies/components/solend/SolendWithdraw.tsx
create mode 100644 Strategies/protocols/solend/index.ts
diff --git a/Strategies/components/DepositModal.tsx b/Strategies/components/DepositModal.tsx
index 7d3943adfc..3ac535c1a1 100644
--- a/Strategies/components/DepositModal.tsx
+++ b/Strategies/components/DepositModal.tsx
@@ -1,11 +1,13 @@
import Modal from '@components/Modal'
import ModalHeader from './ModalHeader'
+import SolendModalContent from './SolendModalContent'
import MangoDeposit from './MangoDepositComponent'
import BigNumber from 'bignumber.js'
+import { SolendStrategy } from 'Strategies/types/types'
const DepositModal = ({
onClose,
- isOpen,
+ proposedInvestment,
handledMint,
apy,
protocolName,
@@ -20,8 +22,9 @@ const DepositModal = ({
const currentPositionFtm = new BigNumber(
currentPosition.toFixed(0)
).toFormat()
+
return (
-
+
-
+ {protocolName === 'Solend' ? (
+
+ ) : null}
{protocolName === 'Mango' ? (
{
+ const [proposalType, setProposalType] = useState('Deposit')
+
+ const tabs = [DEPOSIT, WITHDRAW]
+
+ return (
+
+
+ setProposalType(v)}
+ values={tabs}
+ />
+
+ {proposalType === WITHDRAW && (
+
+ )}
+ {proposalType === DEPOSIT && (
+
+ )}
+
+ )
+}
+
+export default SolendDepositComponent
diff --git a/Strategies/components/solend/SolendDeposit.tsx b/Strategies/components/solend/SolendDeposit.tsx
new file mode 100644
index 0000000000..62ef146836
--- /dev/null
+++ b/Strategies/components/solend/SolendDeposit.tsx
@@ -0,0 +1,376 @@
+import { PublicKey } from '@blockworks-foundation/mango-client'
+import Button, { LinkButton } from '@components/Button'
+import Input from '@components/inputs/Input'
+import Loading from '@components/Loading'
+import Tooltip from '@components/Tooltip'
+import useGovernanceAssets from '@hooks/useGovernanceAssets'
+import useQueryContext from '@hooks/useQueryContext'
+import useRealm from '@hooks/useRealm'
+import { getProgramVersionForRealm } from '@models/registry/api'
+import { BN } from '@project-serum/anchor'
+import { RpcContext } from '@solana/spl-governance'
+import {
+ getMintDecimalAmount,
+ getMintMinAmountAsDecimal,
+ getMintNaturalAmountFromDecimalAsBN,
+} from '@tools/sdk/units'
+import { precision } from '@utils/formatting'
+import tokenService from '@utils/services/token'
+import BigNumber from 'bignumber.js'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import useWalletStore from 'stores/useWalletStore'
+import { SolendStrategy } from 'Strategies/types/types'
+import AdditionalProposalOptions from '@components/AdditionalProposalOptions'
+import { validateInstruction } from '@utils/instructionTools'
+import * as yup from 'yup'
+import { AssetAccount } from '@utils/uiTypes/assets'
+import Select from '@components/inputs/Select'
+import {
+ CreateSolendStrategyParams,
+ cTokenExchangeRate,
+ getReserveData,
+ SolendSubStrategy,
+} from 'Strategies/protocols/solend'
+import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
+
+const SOL_BUFFER = 0.02
+
+const SolendDeposit = ({
+ proposedInvestment,
+ handledMint,
+ createProposalFcn,
+ governedTokenAccount,
+}: {
+ proposedInvestment: SolendStrategy
+ handledMint: string
+ createProposalFcn: CreateSolendStrategyParams
+ governedTokenAccount: AssetAccount
+}) => {
+ const router = useRouter()
+ const { fmtUrlWithCluster } = useQueryContext()
+ const {
+ proposals,
+ realmInfo,
+ realm,
+ ownVoterWeight,
+ mint,
+ councilMint,
+ symbol,
+ } = useRealm()
+ const [isDepositing, setIsDepositing] = useState(false)
+ const [deposits, setDeposits] = useState<{
+ [reserveAddress: string]: number
+ }>({})
+ 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 tokenInfo = tokenService.getTokenInfo(handledMint)
+ const {
+ governedTokenAccountsWithoutNfts,
+ auxiliaryTokenAccounts,
+ canUseTransferInstruction,
+ } = useGovernanceAssets()
+
+ const treasuryAmount = new BN(
+ governedTokenAccount.isSol
+ ? governedTokenAccount.extensions.amount!.toNumber()
+ : governedTokenAccount.extensions.token!.account.amount
+ )
+ const mintInfo = governedTokenAccount.extensions?.mint?.account
+ const tokenSymbol = tokenService.getTokenInfo(
+ governedTokenAccount.extensions.mint!.publicKey.toBase58()
+ )?.symbol
+ const [form, setForm] = useState<{
+ title: string
+ description: string
+ amount?: number
+ reserve: SolendSubStrategy
+ }>({
+ title: '',
+ description: '',
+ amount: undefined,
+ reserve:
+ proposedInvestment.reserves.find((reserve) => reserve.isPrimary) ??
+ proposedInvestment.reserves[0]!,
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const proposalTitle = `Deposit ${form.amount} ${
+ tokenSymbol || 'tokens'
+ } to the Solend ${form.reserve.marketName} pool`
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({ ...form, [propertyName]: value })
+ }
+ const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1
+ let maxAmount = mintInfo
+ ? getMintDecimalAmount(mintInfo, treasuryAmount)
+ : new BigNumber(0)
+ if (governedTokenAccount.isSol) {
+ maxAmount = maxAmount.minus(SOL_BUFFER)
+ }
+ const maxAmountFtm = maxAmount.toNumber().toFixed(4)
+ const currentPrecision = precision(mintMinAmount)
+
+ const validateAmountOnBlur = () => {
+ handleSetForm({
+ propertyName: 'amount',
+ value: parseFloat(
+ Math.max(
+ Number(mintMinAmount),
+ Math.min(Number(Number.MAX_SAFE_INTEGER), Number(form.amount))
+ ).toFixed(currentPrecision)
+ ),
+ })
+ }
+
+ // Solend
+ useEffect(() => {
+ const getSlndCTokens = async () => {
+ const accounts = [
+ ...governedTokenAccountsWithoutNfts,
+ ...auxiliaryTokenAccounts,
+ ]
+
+ const relevantAccs = accounts
+ .map((acc) => {
+ const reserve = (proposedInvestment as SolendStrategy)?.reserves.find(
+ (reserve) =>
+ reserve.mintAddress === handledMint &&
+ reserve.collateralMintAddress ===
+ acc.extensions.mint?.publicKey.toBase58()
+ )
+ if (!reserve || !proposedInvestment) return null
+
+ return {
+ acc,
+ reserve,
+ }
+ })
+ .filter(Boolean)
+
+ const reserveStats = await getReserveData(
+ relevantAccs.map((data) => data!.reserve.reserveAddress)
+ )
+
+ const results = Object.fromEntries(
+ relevantAccs.map((data) => {
+ const reserve = data!.reserve
+ const account = data!.acc
+
+ const stat = reserveStats.find(
+ (stat) => stat.reserve.lendingMarket === reserve.marketAddress
+ )!
+
+ return [
+ reserve.reserveAddress,
+ ((account.extensions.amount?.toNumber() ?? 0) *
+ cTokenExchangeRate(stat)) /
+ 10 ** reserve.decimals,
+ ]
+ })
+ )
+ setDeposits(results)
+ }
+ getSlndCTokens()
+ }, [])
+
+ const handleDeposit = async () => {
+ const isValid = await validateInstruction({ schema, form, setFormErrors })
+ if (!isValid) {
+ return
+ }
+ try {
+ setIsDepositing(true)
+ const rpcContext = new RpcContext(
+ new PublicKey(realm!.owner.toString()),
+ getProgramVersionForRealm(realmInfo!),
+ wallet!,
+ connection.current,
+ connection.endpoint
+ )
+ const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal(
+ governedTokenAccount!.governance!.account.config
+ )
+ 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,
+ {
+ ...form,
+ amountFmt: form.amount!.toFixed(4),
+ bnAmount: getMintNaturalAmountFromDecimalAsBN(
+ form.amount as number,
+ governedTokenAccount.extensions.mint!.account.decimals
+ ),
+ proposalCount: Object.keys(proposals).length,
+ action: 'Deposit',
+ },
+ realm!,
+ governedTokenAccount!,
+ ownTokenRecord,
+ defaultProposalMint!,
+ governedTokenAccount!.governance!.account!.proposalCount,
+ false,
+ connection,
+ client
+ )
+ const url = fmtUrlWithCluster(
+ `/dao/${symbol}/proposal/${proposalAddress}`
+ )
+ router.push(url)
+ } catch (e) {
+ console.log(e)
+ throw e
+ }
+ setIsDepositing(false)
+ }
+ const schema = yup.object().shape({
+ amount: yup
+ .number()
+ .required('Amount is required')
+ .min(mintMinAmount)
+ .max(maxAmount.toNumber()),
+ reserve: yup.object().required('Lending market address is required'),
+ })
+
+ return (
+
+
+
+ Amount
+
+ Bal: {maxAmountFtm}
+
+ handleSetForm({
+ propertyName: 'amount',
+ value: maxAmount.toNumber(),
+ })
+ }
+ className="font-bold ml-2 text-primary-light"
+ >
+ Max
+
+
+
+
+ handleSetForm({ propertyName: 'amount', value: e.target.value })
+ }
+ step={mintMinAmount}
+ onBlur={validateAmountOnBlur}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'title',
+ })
+ }
+ setDescription={(evt) =>
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'description',
+ })
+ }
+ voteByCouncil={voteByCouncil}
+ setVoteByCouncil={setVoteByCouncil}
+ />
+
+
+ Current Deposits
+
+ {deposits[form.reserve.reserveAddress]?.toFixed(4) || 0}{' '}
+ {tokenInfo?.symbol}
+
+
+
+ Proposed Deposit
+
+ {form.amount?.toLocaleString() || (
+ Enter an amount
+ )}{' '}
+
+ {form.amount && tokenInfo?.symbol}
+
+
+
+
+
+
+ )
+}
+SolendDeposit.whyDidYouRender = true
+
+export default SolendDeposit
diff --git a/Strategies/components/solend/SolendWithdraw.tsx b/Strategies/components/solend/SolendWithdraw.tsx
new file mode 100644
index 0000000000..786056c77b
--- /dev/null
+++ b/Strategies/components/solend/SolendWithdraw.tsx
@@ -0,0 +1,381 @@
+import { PublicKey } from '@blockworks-foundation/mango-client'
+import Button, { LinkButton } from '@components/Button'
+import Input from '@components/inputs/Input'
+import Loading from '@components/Loading'
+import Tooltip from '@components/Tooltip'
+import useGovernanceAssets from '@hooks/useGovernanceAssets'
+import useQueryContext from '@hooks/useQueryContext'
+import useRealm from '@hooks/useRealm'
+import { getProgramVersionForRealm } from '@models/registry/api'
+import { BN } from '@project-serum/anchor'
+import { RpcContext } from '@solana/spl-governance'
+import {
+ getMintMinAmountAsDecimal,
+ getMintNaturalAmountFromDecimalAsBN,
+} from '@tools/sdk/units'
+import { precision } from '@utils/formatting'
+import tokenService from '@utils/services/token'
+import BigNumber from 'bignumber.js'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import useWalletStore from 'stores/useWalletStore'
+import { SolendStrategy } from 'Strategies/types/types'
+import useVotePluginsClientStore from 'stores/useVotePluginsClientStore'
+import AdditionalProposalOptions from '@components/AdditionalProposalOptions'
+import { validateInstruction } from '@utils/instructionTools'
+import * as yup from 'yup'
+import { AssetAccount } from '@utils/uiTypes/assets'
+import Select from '@components/inputs/Select'
+import {
+ CreateSolendStrategyParams,
+ cTokenExchangeRate,
+ getReserveData,
+ SolendSubStrategy,
+} from 'Strategies/protocols/solend'
+
+const SolendWithdraw = ({
+ proposedInvestment,
+ handledMint,
+ createProposalFcn,
+ governedTokenAccount,
+}: {
+ proposedInvestment: SolendStrategy
+ handledMint: string
+ createProposalFcn: CreateSolendStrategyParams
+ governedTokenAccount: AssetAccount
+}) => {
+ const {
+ governedTokenAccountsWithoutNfts,
+ auxiliaryTokenAccounts,
+ canUseTransferInstruction,
+ } = useGovernanceAssets()
+ const router = useRouter()
+ const { fmtUrlWithCluster } = useQueryContext()
+ const {
+ proposals,
+ realmInfo,
+ realm,
+ ownVoterWeight,
+ mint,
+ councilMint,
+ symbol,
+ } = useRealm()
+ const [isWithdrawing, setIsWithdrawing] = useState(false)
+ const [voteByCouncil, setVoteByCouncil] = useState(false)
+ const [deposits, setDeposits] = useState<{
+ [reserveAddress: string]: { amount: number; amountExact: number }
+ }>({})
+ const client = useVotePluginsClientStore(
+ (s) => s.state.currentRealmVotingClient
+ )
+ const connection = useWalletStore((s) => s.connection)
+ const wallet = useWalletStore((s) => s.current)
+ const tokenInfo = tokenService.getTokenInfo(handledMint)
+ const mintInfo = governedTokenAccount.extensions?.mint?.account
+ const tokenSymbol = tokenService.getTokenInfo(
+ governedTokenAccount.extensions.mint!.publicKey.toBase58()
+ )?.symbol
+ const [form, setForm] = useState<{
+ title: string
+ description: string
+ amount?: number
+ reserve: SolendSubStrategy
+ max: boolean
+ }>({
+ title: '',
+ description: '',
+ amount: undefined,
+ reserve:
+ proposedInvestment.reserves.find((reserve) => reserve.isPrimary) ??
+ proposedInvestment.reserves[0]!,
+ max: false,
+ })
+ const [formErrors, setFormErrors] = useState({})
+ const proposalTitle = `Withdraw ${form.amount} ${
+ tokenSymbol || 'tokens'
+ } from the Solend ${form.reserve.marketName} pool`
+ const handleSetForm = ({ propertyName, value }) => {
+ setFormErrors({})
+ setForm({
+ ...form,
+ max: propertyName === 'amount' ? false : form.max,
+ [propertyName]: value,
+ })
+ }
+ const mintMinAmount = mintInfo ? getMintMinAmountAsDecimal(mintInfo) : 1
+ const maxAmount = new BigNumber(
+ deposits[form.reserve.reserveAddress]?.amount ?? 0
+ )
+ const maxAmountFtm = maxAmount.toFixed(4)
+ const currentPrecision = precision(mintMinAmount)
+
+ // Solend
+ useEffect(() => {
+ const getSlndCTokens = async () => {
+ const accounts = [
+ ...governedTokenAccountsWithoutNfts,
+ ...auxiliaryTokenAccounts,
+ ]
+
+ const relevantAccs = accounts
+ .map((acc) => {
+ const reserve = (proposedInvestment as SolendStrategy)?.reserves.find(
+ (reserve) =>
+ reserve.mintAddress === handledMint &&
+ reserve.collateralMintAddress ===
+ acc.extensions.mint?.publicKey.toBase58()
+ )
+ if (!reserve || !proposedInvestment) return null
+
+ return {
+ acc,
+ reserve,
+ }
+ })
+ .filter(Boolean)
+
+ const reserveStats = await getReserveData(
+ relevantAccs.map((data) => data!.reserve.reserveAddress)
+ )
+
+ setDeposits(
+ Object.fromEntries(
+ relevantAccs.map((data) => {
+ const reserve = data!.reserve
+ const account = data!.acc
+
+ const stat = reserveStats.find(
+ (stat) => stat.reserve.lendingMarket === reserve.marketAddress
+ )!
+
+ return [
+ reserve.reserveAddress,
+ {
+ amount:
+ ((account.extensions.amount?.toNumber() ?? 0) *
+ cTokenExchangeRate(stat)) /
+ 10 ** reserve.decimals,
+ amountExact: account.extensions.amount?.toNumber() ?? 0,
+ },
+ ]
+ })
+ )
+ )
+ }
+ getSlndCTokens()
+ }, [])
+
+ const validateAmountOnBlur = () => {
+ handleSetForm({
+ propertyName: 'amount',
+ value: parseFloat(
+ Math.max(
+ Number(mintMinAmount),
+ Math.min(Number(Number.MAX_SAFE_INTEGER), Number(form.amount))
+ ).toFixed(currentPrecision)
+ ),
+ })
+ }
+
+ const handleWithdraw = async () => {
+ const isValid = await validateInstruction({ schema, form, setFormErrors })
+ if (!isValid) {
+ return
+ }
+ try {
+ setIsWithdrawing(true)
+ const rpcContext = new RpcContext(
+ new PublicKey(realm!.owner.toString()),
+ getProgramVersionForRealm(realmInfo!),
+ wallet!,
+ connection.current,
+ connection.endpoint
+ )
+ const ownTokenRecord = ownVoterWeight.getTokenRecordToCreateProposal(
+ governedTokenAccount!.governance!.account.config
+ )
+ 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 reserveStat = await getReserveData([form.reserve.reserveAddress])
+
+ const proposalAddress = await createProposalFcn(
+ rpcContext,
+ {
+ ...form,
+ bnAmount: form.max
+ ? new BN(deposits[form.reserve.reserveAddress].amountExact)
+ : getMintNaturalAmountFromDecimalAsBN(
+ (form.amount as number) / cTokenExchangeRate(reserveStat[0]),
+ governedTokenAccount.extensions.mint!.account.decimals
+ ),
+ amountFmt: (
+ (form.amount as number) / cTokenExchangeRate(reserveStat[0])
+ ).toFixed(4),
+ proposalCount: Object.keys(proposals).length,
+ action: 'Withdraw',
+ },
+ realm!,
+ governedTokenAccount!,
+ ownTokenRecord,
+ defaultProposalMint!,
+ governedTokenAccount!.governance!.account!.proposalCount,
+ false,
+ connection,
+ client
+ )
+ const url = fmtUrlWithCluster(
+ `/dao/${symbol}/proposal/${proposalAddress}`
+ )
+ router.push(url)
+ } catch (e) {
+ console.log(e)
+ throw e
+ }
+ setIsWithdrawing(false)
+ }
+ const schema = yup.object().shape({
+ amount: yup
+ .number()
+ .required('Amount is required')
+ .max(deposits[form.reserve.reserveAddress]?.amount),
+ reserve: yup.object().required('Lending market address is required'),
+ })
+
+ return (
+
+
+
+ Amount
+
+ Bal: {maxAmountFtm}
+ {
+ setFormErrors({})
+ setForm({
+ ...form,
+ amount: maxAmount.toNumber(),
+ max: true,
+ })
+ }}
+ className="font-bold ml-2 text-primary-light"
+ >
+ Max
+
+
+
+
+ handleSetForm({ propertyName: 'amount', value: e.target.value })
+ }
+ step={mintMinAmount}
+ onBlur={validateAmountOnBlur}
+ />
+
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'title',
+ })
+ }
+ setDescription={(evt) =>
+ handleSetForm({
+ value: evt.target.value,
+ propertyName: 'description',
+ })
+ }
+ voteByCouncil={voteByCouncil}
+ setVoteByCouncil={setVoteByCouncil}
+ />
+
+
+ Current Deposits
+
+ {deposits[form.reserve.reserveAddress]?.amount.toFixed(4) || 0}{' '}
+ {tokenInfo?.symbol}
+
+
+
+ Proposed Withdraw
+
+ {form.amount?.toLocaleString() || (
+ Enter an amount
+ )}{' '}
+
+ {form.amount && tokenInfo?.symbol}
+
+
+
+
+
+
+ )
+}
+
+export default SolendWithdraw
diff --git a/Strategies/protocols/solend/index.ts b/Strategies/protocols/solend/index.ts
new file mode 100644
index 0000000000..3289dd0e29
--- /dev/null
+++ b/Strategies/protocols/solend/index.ts
@@ -0,0 +1,511 @@
+import { BN } from '@project-serum/anchor'
+import {
+ ProgramAccount,
+ Realm,
+ getInstructionDataFromBase64,
+ RpcContext,
+ serializeInstructionToBase64,
+ TokenOwnerRecord,
+} from '@solana/spl-governance'
+import {
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ NATIVE_MINT,
+ Token,
+ TOKEN_PROGRAM_ID,
+} from '@solana/spl-token'
+import { PublicKey, SystemProgram } from '@solana/web3.js'
+import {
+ depositReserveLiquidityInstruction,
+ redeemReserveCollateralInstruction,
+ syncNative,
+} from '@solendprotocol/solend-sdk'
+import tokenService from '@utils/services/token'
+import {
+ createProposal,
+ InstructionDataWithHoldUpTime,
+} from 'actions/createProposal'
+import axios from 'axios'
+import { SolendStrategy } from 'Strategies/types/types'
+
+import { VotingClient } from '@utils/uiTypes/VotePlugin'
+import { AssetAccount } from '@utils/uiTypes/assets'
+import { ConnectionContext } from '@utils/connection'
+import BigNumber from 'bignumber.js'
+
+const MAINNET_PROGRAM = 'So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo'
+const DEVNET_PROGRAM = 'ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx'
+
+export const SOLEND = 'Solend'
+const SOLEND_SYMBOL = 'SLND'
+const SOLEND_PROTOCOL_LOGO_URI =
+ 'https://solend-image-assets.s3.us-east-2.amazonaws.com/1280-circle.png'
+
+const SOLEND_ENDPOINT = 'https://api.solend.fi'
+
+export type CreateSolendStrategyParams = (
+ rpcContext: RpcContext,
+ form: {
+ title: string
+ description: string
+ action: 'Deposit' | 'Withdraw'
+ bnAmount: BN
+ amountFmt: string
+ proposalCount: number
+ reserve: SolendSubStrategy
+ },
+ realm: ProgramAccount,
+ treasuaryAccount: AssetAccount,
+ tokenOwnerRecord: ProgramAccount,
+ governingTokenMint: PublicKey,
+ proposalIndex: number,
+ isDraft: boolean,
+ connection: ConnectionContext,
+ client?: VotingClient
+) => Promise
+
+type Config = Array
+
+type MarketConfig = {
+ name: string
+ isPrimary: boolean
+ description: string
+ creator: string
+ address: string
+ authorityAddress: string
+ reserves: Array
+}
+
+type ReserveConfig = {
+ liquidityToken: {
+ coingeckoID: string
+ decimals: number
+ logo: string
+ mint: string
+ name: string
+ symbol: string
+ volume24h: number
+ }
+ pythOracle: string
+ switchboardOracle: string
+ address: string
+ collateralMintAddress: string
+ collateralSupplyAddress: string
+ liquidityAddress: string
+ liquidityFeeReceiverAddress: string
+ userSupplyCap: number
+}
+
+type ReserveStat = {
+ reserve: {
+ lendingMarket: string
+ liquidity: {
+ mintPubkey: string
+ mintDecimals: number
+ supplyPubkey: string
+ pythOracle: string
+ switchboardOracle: string
+ availableAmount: string
+ borrowedAmountWads: string
+ cumulativeBorrowRateWads: string
+ marketPrice: string
+ }
+ collateral: {
+ mintPubkey: string
+ mintTotalSupply: string
+ supplyPubkey: string
+ }
+ }
+ rates: {
+ supplyInterest: string
+ borrowInterest: string
+ }
+}
+
+export type SolendSubStrategy = {
+ marketAddress: string
+ marketName: string
+ reserveAddress: string
+ mintAddress: string
+ logo: string
+ symbol: string
+ decimals: number
+ liquidity: number
+ supplyApy: number
+ isPrimary: boolean
+ liquidityAddress: string
+ collateralMintAddress: string
+ marketAuthorityAddress: string
+}
+
+export async function getReserveData(
+ reserveIds: Array
+): Promise> {
+ if (!reserveIds.length) return []
+ const stats = (
+ await (
+ await axios.get(
+ `${SOLEND_ENDPOINT}/v1/reserves?ids=${reserveIds.join(',')}`
+ )
+ ).data
+ ).results as Array
+
+ return stats
+}
+
+export function cTokenExchangeRate(reserve: ReserveStat) {
+ return new BigNumber(reserve.reserve.liquidity.availableAmount ?? '0')
+ .plus(
+ new BigNumber(reserve.reserve.liquidity.borrowedAmountWads).shiftedBy(-18)
+ )
+ .dividedBy(new BigNumber(reserve.reserve.collateral.mintTotalSupply))
+ .toNumber()
+}
+
+export async function getReserve(): Promise {
+ return await (
+ await axios.get(`${SOLEND_ENDPOINT}/v1/markets/configs?scope=solend`)
+ ).data
+}
+
+export async function getConfig(): Promise {
+ return await (
+ await axios.get(`${SOLEND_ENDPOINT}/v1/markets/configs?scope=solend`)
+ ).data
+}
+
+export async function getReserves(): Promise {
+ const config = await getConfig()
+ const reserves = config.flatMap((market) =>
+ market.reserves.map((reserve) => ({
+ marketName: market.name,
+ marketDescription: market.description,
+ marketAddress: market.address,
+ marketPrimary: market.isPrimary,
+ marketAuthorityAddress: market.authorityAddress,
+ ...reserve,
+ }))
+ )
+
+ return reserves
+}
+
+export async function getSolendStrategies() {
+ const strats: SolendStrategy[] = []
+
+ // method to fetch solend strategies
+ const config = await getConfig()
+ const reserves = config.flatMap((market) =>
+ market.reserves.map((reserve) => ({
+ marketName: market.name,
+ marketDescription: market.description,
+ marketAddress: market.address,
+ marketPrimary: market.isPrimary,
+ marketAuthorityAddress: market.authorityAddress,
+ ...reserve,
+ }))
+ )
+
+ const stats = await getReserveData(reserves.map((reserve) => reserve.address))
+
+ const mergedData = reserves.map((reserve, index) => ({
+ marketName:
+ reserve.marketName.charAt(0).toUpperCase() + reserve.marketName.slice(1),
+ marketAddress: reserve.marketAddress,
+ reserveAddress: reserve.address,
+ mintAddress: reserve.liquidityToken.mint,
+ decimals: reserve.liquidityToken.decimals,
+ liquidityAddress: reserve.liquidityAddress,
+ collateralMintAddress: reserve.collateralMintAddress,
+ marketAuthorityAddress: reserve.marketAuthorityAddress,
+ isPrimary: reserve.marketPrimary,
+ logo: reserve.liquidityToken.logo,
+ symbol: reserve.liquidityToken.symbol,
+ liquidity:
+ (Number(stats[index].reserve.liquidity.availableAmount) /
+ 10 ** stats[index].reserve.liquidity.mintDecimals) *
+ (Number(stats[index].reserve.liquidity.marketPrice) / 10 ** 18),
+ supplyApy: Number(stats[index].rates.supplyInterest),
+ })) as Array
+
+ const aggregatedData = mergedData.reduce(
+ (acc, reserve) => ({
+ ...acc,
+ [reserve.symbol]: (acc[reserve.symbol] ?? []).concat(reserve),
+ }),
+ {} as {
+ [symbol: string]: typeof mergedData
+ }
+ )
+
+ for (const [symbol, reserves] of Object.entries(aggregatedData)) {
+ const tokenData = reserves[0]
+ const maxApy = Math.max(...reserves.map((reserve) => reserve.supplyApy))
+ const totalLiquidity = reserves.reduce(
+ (acc, reserve) => acc + reserve.liquidity,
+ 0
+ )
+
+ strats.push({
+ liquidity: totalLiquidity,
+ handledTokenSymbol: symbol,
+ apy:
+ reserves.length > 1
+ ? `Up to ${maxApy.toFixed(2)}%`
+ : `${maxApy.toFixed(2)}%`,
+ protocolName: SOLEND,
+ protocolSymbol: SOLEND_SYMBOL,
+ handledMint: tokenData.mintAddress,
+ handledTokenImgSrc: tokenData.logo,
+ protocolLogoSrc: SOLEND_PROTOCOL_LOGO_URI,
+ strategyName: 'Deposit',
+ strategyDescription:
+ 'Earn interest on your treasury assets by depositing into Solend.',
+ isGenericItem: false,
+ reserves: reserves,
+ createProposalFcn: handleSolendAction,
+ })
+ }
+
+ return strats
+}
+
+async function handleSolendAction(
+ rpcContext: RpcContext,
+ form: {
+ action: 'Deposit' | 'Withdraw'
+ title: string
+ description: string
+ bnAmount: BN
+ reserve: SolendSubStrategy
+ amountFmt: 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 slndProgramAddress =
+ connection.cluster === 'mainnet' ? MAINNET_PROGRAM : DEVNET_PROGRAM
+
+ const ctokenATA = await Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ new PublicKey(form.reserve.collateralMintAddress),
+ owner,
+ true
+ )
+
+ const liquidityATA = await Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ new PublicKey(form.reserve.mintAddress),
+ owner,
+ true
+ )
+
+ let createAtaInst
+
+ if (form.action === 'Deposit') {
+ const depositAccountInfo = await connection.current.getAccountInfo(
+ ctokenATA
+ )
+ if (!depositAccountInfo) {
+ // generate the instruction for creating the ATA
+ createAtaInst = Token.createAssociatedTokenAccountInstruction(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ new PublicKey(form.reserve.collateralMintAddress),
+ ctokenATA,
+ owner,
+ owner
+ )
+ }
+ } else {
+ const withdrawAccountInfo = await connection.current.getAccountInfo(
+ liquidityATA
+ )
+ if (!withdrawAccountInfo) {
+ // generate the instruction for creating the ATA
+ createAtaInst = Token.createAssociatedTokenAccountInstruction(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ matchedTreasury.extensions.token!.publicKey,
+ liquidityATA,
+ owner,
+ owner
+ )
+ }
+ }
+
+ if (createAtaInst) {
+ const createAtaInstObj = {
+ data: getInstructionDataFromBase64(
+ serializeInstructionToBase64(createAtaInst)
+ ),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+ insts.push(createAtaInstObj)
+ }
+
+ const setupInsts: InstructionDataWithHoldUpTime[] = []
+ const cleanupInsts: InstructionDataWithHoldUpTime[] = []
+
+ if (isSol) {
+ const userWSOLAccountInfo = await connection.current.getAccountInfo(
+ liquidityATA
+ )
+
+ const rentExempt = await Token.getMinBalanceRentForExemptAccount(
+ connection.current
+ )
+
+ const sendAction = form.action === 'Deposit'
+
+ const transferLamportsIx = SystemProgram.transfer({
+ fromPubkey: owner,
+ toPubkey: liquidityATA,
+ lamports:
+ (userWSOLAccountInfo ? 0 : rentExempt) +
+ (sendAction ? form.bnAmount.toNumber() : 0),
+ })
+
+ const transferLamportInst = {
+ data: getInstructionDataFromBase64(
+ serializeInstructionToBase64(transferLamportsIx)
+ ),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+
+ setupInsts.push(transferLamportInst)
+
+ const closeWSOLAccountIx = Token.createCloseAccountInstruction(
+ TOKEN_PROGRAM_ID,
+ liquidityATA,
+ owner,
+ owner,
+ []
+ )
+
+ const closeWSOLInst = {
+ data: getInstructionDataFromBase64(
+ serializeInstructionToBase64(closeWSOLAccountIx)
+ ),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+
+ if (userWSOLAccountInfo) {
+ const syncIx = syncNative(liquidityATA)
+ const syncInst = {
+ data: getInstructionDataFromBase64(
+ serializeInstructionToBase64(syncIx)
+ ),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+ if (sendAction) {
+ setupInsts.push(syncInst)
+ } else {
+ cleanupInsts.push(closeWSOLInst)
+ }
+ } else {
+ const createUserWSOLAccountIx = Token.createAssociatedTokenAccountInstruction(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ NATIVE_MINT,
+ liquidityATA,
+ owner,
+ owner
+ )
+ const createUserWSOLAccountInst = {
+ data: getInstructionDataFromBase64(
+ serializeInstructionToBase64(createUserWSOLAccountIx)
+ ),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+ setupInsts.push(createUserWSOLAccountInst)
+ cleanupInsts.push(closeWSOLInst)
+ }
+ }
+
+ const actionIx =
+ form.action === 'Deposit'
+ ? depositReserveLiquidityInstruction(
+ form.bnAmount,
+ liquidityATA,
+ ctokenATA,
+ new PublicKey(form.reserve.reserveAddress),
+ new PublicKey(form.reserve.liquidityAddress),
+ new PublicKey(form.reserve.collateralMintAddress),
+ new PublicKey(form.reserve.marketAddress),
+ new PublicKey(form.reserve.marketAuthorityAddress),
+ owner,
+ new PublicKey(slndProgramAddress)
+ )
+ : redeemReserveCollateralInstruction(
+ form.bnAmount,
+ ctokenATA,
+ liquidityATA,
+ new PublicKey(form.reserve.reserveAddress),
+ new PublicKey(form.reserve.collateralMintAddress),
+ new PublicKey(form.reserve.liquidityAddress),
+ new PublicKey(form.reserve.marketAddress),
+ new PublicKey(form.reserve.marketAuthorityAddress),
+ owner,
+ new PublicKey(slndProgramAddress)
+ )
+
+ const depositSolendInsObj = {
+ data: getInstructionDataFromBase64(serializeInstructionToBase64(actionIx)),
+ holdUpTime: matchedTreasury.governance!.account!.config
+ .minInstructionHoldUpTime,
+ prerequisiteInstructions: [],
+ chunkSplitByDefault: true,
+ }
+ insts.push(depositSolendInsObj)
+
+ const proposalAddress = await createProposal(
+ rpcContext,
+ realm,
+ matchedTreasury.governance!.pubkey,
+ tokenOwnerRecord,
+ form.title ||
+ `${form.action} ${form.amountFmt} ${
+ tokenService.getTokenInfo(
+ matchedTreasury.extensions.mint!.publicKey.toBase58()
+ )?.symbol || 'tokens'
+ } ${form.action === 'Deposit' ? 'into' : 'from'} the Solend ${
+ form.reserve.marketName
+ } pool`,
+ form.description,
+ governingTokenMint,
+ proposalIndex,
+ [...setupInsts, ...insts, ...cleanupInsts],
+ isDraft,
+ client
+ )
+ return proposalAddress
+}
diff --git a/Strategies/store/useStrategiesStore.tsx b/Strategies/store/useStrategiesStore.tsx
index e3499d6803..cb466bf3df 100644
--- a/Strategies/store/useStrategiesStore.tsx
+++ b/Strategies/store/useStrategiesStore.tsx
@@ -1,6 +1,7 @@
import { ConnectionContext } from '@utils/connection'
import { notify } from '@utils/notifications'
import { tvl } from 'Strategies/protocols/mango/tools'
+import { getSolendStrategies } from 'Strategies/protocols/solend'
import { TreasuryStrategy } from 'Strategies/types/types'
import create, { State } from 'zustand'
@@ -19,8 +20,11 @@ const useStrategiesStore = create((set, _get) => ({
})
try {
const mango = await tvl(Date.now() / 1000, connection)
+ const solend = await getSolendStrategies()
+
//add fetch functions for your protocol in promise.all
- const strategies: TreasuryStrategy[] = [...mango]
+ const strategies: TreasuryStrategy[] = [...solend, ...mango]
+
set((s) => {
s.strategies = strategies
})
diff --git a/Strategies/types/types.ts b/Strategies/types/types.ts
index 177036ae7a..fcec9d199c 100644
--- a/Strategies/types/types.ts
+++ b/Strategies/types/types.ts
@@ -9,6 +9,10 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { VotingClient } from '@utils/uiTypes/VotePlugin'
import { AssetAccount } from '@utils/uiTypes/assets'
import { MarketStore } from 'Strategies/store/marketStore'
+import {
+ CreateSolendStrategyParams,
+ SolendSubStrategy,
+} from 'Strategies/protocols/solend'
export interface TreasuryStrategy {
//liquidity in $
@@ -16,6 +20,7 @@ export interface TreasuryStrategy {
protocolSymbol: string
apy: string
protocolName: string
+ strategySubtext?: string
handledMint: string
handledTokenSymbol: string
handledTokenImgSrc: string
@@ -25,11 +30,20 @@ export interface TreasuryStrategy {
//if you want to use custom component set this to false and add your custom
//item and modal to strategywrapper component based on generic components
isGenericItem?: boolean
+ createProposalFcn: any
+}
+
+export type MangoStrategy = TreasuryStrategy & {
//async function that pass all props needed to create proposal
// if promise is successfully resolved it will automatically redirect to created proposal
createProposalFcn: HandleCreateProposalWithStrategy
}
+export type SolendStrategy = TreasuryStrategy & {
+ reserves: Array
+ createProposalFcn: CreateSolendStrategyParams
+}
+
export type HandleCreateProposalWithStrategy = (
{ connection, wallet, programId, programVersion, walletPubkey }: RpcContext,
handledMint: string,
diff --git a/components/AdditionalProposalOptions.tsx b/components/AdditionalProposalOptions.tsx
index 00c02ccef6..d57d9298b0 100644
--- a/components/AdditionalProposalOptions.tsx
+++ b/components/AdditionalProposalOptions.tsx
@@ -12,6 +12,7 @@ const AdditionalProposalOptions = ({
setTitle,
setDescription,
defaultTitle,
+ defaultDescription,
voteByCouncil,
setVoteByCouncil,
}: {
@@ -20,6 +21,7 @@ const AdditionalProposalOptions = ({
setTitle: (evt) => void
setDescription: (evt) => void
defaultTitle: string
+ defaultDescription?: string
voteByCouncil: boolean
setVoteByCouncil: (val) => void
}) => {
@@ -52,6 +54,7 @@ const AdditionalProposalOptions = ({
noMaxWidth={true}
label="Proposal Description"
placeholder={
+ defaultDescription ??
'Description of your proposal or use a github gist link (optional)'
}
wrapperClassName="mb-5"
diff --git a/components/TreasuryAccount/AccountOverview.tsx b/components/TreasuryAccount/AccountOverview.tsx
index c4f4d3bfd0..c5ae2a6a27 100644
--- a/components/TreasuryAccount/AccountOverview.tsx
+++ b/components/TreasuryAccount/AccountOverview.tsx
@@ -1,6 +1,6 @@
import Button, { LinkButton } from '@components/Button'
import { getExplorerUrl } from '@components/explorer/tools'
-import { getAccountName } from '@components/instructions/tools'
+import { getAccountName, WSOL_MINT } from '@components/instructions/tools'
import Modal from '@components/Modal'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import useQueryContext from '@hooks/useQueryContext'
@@ -24,7 +24,7 @@ import Tooltip from '@components/Tooltip'
import ConvertToMsol from './ConvertToMsol'
import useStrategiesStore from 'Strategies/store/useStrategiesStore'
import DepositModal from 'Strategies/components/DepositModal'
-import { TreasuryStrategy } from 'Strategies/types/types'
+import { SolendStrategy, TreasuryStrategy } from 'Strategies/types/types'
import BigNumber from 'bignumber.js'
import { MangoAccount } from '@blockworks-foundation/mango-client'
import {
@@ -37,10 +37,23 @@ import LoadingRows from './LoadingRows'
import TradeOnSerum, { TradeOnSerumProps } from './TradeOnSerum'
import { AccountType } from '@utils/uiTypes/assets'
import CreateAta from './CreateAta'
+import {
+ cTokenExchangeRate,
+ getReserveData,
+ SOLEND,
+} from 'Strategies/protocols/solend'
+
+type InvestmentType = TreasuryStrategy & {
+ investedAmount: number
+}
const AccountOverview = () => {
const router = useRouter()
const { ownTokenRecord, ownCouncilTokenRecord } = useRealm()
+ const {
+ governedTokenAccounts,
+ auxiliaryTokenAccounts,
+ } = useGovernanceAssets()
const currentAccount = useTreasuryAccountStore((s) => s.currentAccount)
const nftsPerPubkey = useTreasuryAccountStore((s) => s.governanceNfts)
const nftsCount =
@@ -51,17 +64,16 @@ const AccountOverview = () => {
const { fmtUrlWithCluster } = useQueryContext()
const isNFT = currentAccount?.isNft
const isSol = currentAccount?.isSol
+ const [loading, setLoading] = useState(true)
const isSplToken = currentAccount?.type === AccountType.TOKEN
const isAuxiliaryAccount = currentAccount?.type === AccountType.AuxiliaryToken
- const connected = useWalletStore((s) => s.connected)
const { canUseTransferInstruction } = useGovernanceAssets()
- const connection = useWalletStore((s) => s.connection)
+ const { connection, connected } = useWalletStore((s) => s)
const recentActivity = useTreasuryAccountStore((s) => s.recentActivity)
const isLoadingRecentActivity = useTreasuryAccountStore(
(s) => s.isLoadingRecentActivity
)
const market = useMarketStore((s) => s)
- const [currentMangoDeposits, setCurrentMangoDeposits] = useState(0)
const [mngoAccounts, setMngoAccounts] = useState([])
const [openNftDepositModal, setOpenNftDepositModal] = useState(false)
const [openCommonSendModal, setOpenCommonSendModal] = useState(false)
@@ -70,33 +82,82 @@ const AccountOverview = () => {
const accountPublicKey = currentAccount?.extensions.transferAddress
const strategies = useStrategiesStore((s) => s.strategies)
const [accountInvestments, setAccountInvestments] = useState<
- TreasuryStrategy[]
- >([])
- const [eligibleInvestments, setEligibleInvestments] = useState<
- TreasuryStrategy[]
+ InvestmentType[]
>([])
const [showStrategies, setShowStrategies] = useState(false)
const [
proposedInvestment,
setProposedInvestment,
- ] = useState(null)
+ ] = useState(null)
const [isCopied, setIsCopied] = useState(false)
const [
tradeSerumInfo,
setTradeSerumInfo,
] = useState(null)
+ const strategyMint = currentAccount?.isSol
+ ? WSOL_MINT
+ : currentAccount?.extensions.token?.account.mint.toString()
+ const visibleInvestments = strategies.filter(
+ (strat) => strat.handledMint === strategyMint
+ )
+ const visibleAccounts = accountInvestments.filter(
+ (strat) => strat.handledMint === strategyMint
+ )
useEffect(() => {
- if (strategies.length > 0) {
- const eligibleInvestments = strategies.filter(
- (strat) =>
- strat.handledMint ===
- currentAccount?.extensions.token?.account.mint.toString()
+ const getSlndCTokens = async () => {
+ const accounts = [
+ ...governedTokenAccounts,
+ ...auxiliaryTokenAccounts,
+ ...(currentAccount?.isSol ? [currentAccount] : []),
+ ]
+
+ const solendStrategy = visibleInvestments.filter(
+ (strat) => strat.protocolName === SOLEND
+ )[0]
+
+ const relevantAccs = accounts
+ .map((acc) => {
+ const reserve = (solendStrategy as SolendStrategy)?.reserves.find(
+ (reserve) =>
+ reserve.mintAddress === strategyMint &&
+ reserve.collateralMintAddress ===
+ acc.extensions.mint?.publicKey.toBase58()
+ )
+ if (!reserve || !solendStrategy) return null
+
+ return {
+ acc,
+ reserve,
+ }
+ })
+ .filter(Boolean)
+
+ const reserveStats = await getReserveData(
+ relevantAccs.map((data) => data!.reserve.reserveAddress)
)
- setEligibleInvestments(eligibleInvestments)
+
+ return relevantAccs.map((data) => {
+ const reserve = data!.reserve
+ const account = data!.acc
+
+ const stat = reserveStats.find(
+ (stat) => stat.reserve.lendingMarket === reserve.marketAddress
+ )!
+
+ return {
+ ...solendStrategy,
+ apy: `${reserve.supplyApy.toFixed(2)}%`,
+ protocolName: solendStrategy.protocolName,
+ strategySubtext: `${reserve.marketName} Pool`,
+ investedAmount:
+ ((account.extensions.amount?.toNumber() ?? 0) *
+ cTokenExchangeRate(stat)) /
+ 10 ** reserve.decimals,
+ }
+ }) as Array
}
- }, [currentAccount, strategies])
- useEffect(() => {
+
const handleGetMangoAccounts = async () => {
const currentAccountMint = currentAccount?.extensions.token?.account.mint
const currentPositions = calculateAllDepositsInMangoAccountsForMint(
@@ -104,20 +165,37 @@ const AccountOverview = () => {
currentAccountMint!,
market
)
- setCurrentMangoDeposits(currentPositions)
if (currentPositions > 0) {
- setAccountInvestments(
- eligibleInvestments.filter((x) => x.protocolName === MANGO)
- )
- } else {
- setAccountInvestments([])
+ return strategies
+ .map((invest) => ({
+ ...invest,
+ investedAmount: currentPositions,
+ }))
+ .filter((x) => x.protocolName === MANGO)
}
+
+ return []
}
- if (eligibleInvestments.filter((x) => x.protocolName === MANGO).length) {
- handleGetMangoAccounts()
+
+ const loadData = async () => {
+ const requests = [] as Array>>
+ if (visibleInvestments.filter((x) => x.protocolName === MANGO).length) {
+ requests.push(handleGetMangoAccounts())
+ }
+ if (visibleInvestments.filter((x) => x.protocolName === SOLEND).length) {
+ requests.push(getSlndCTokens())
+ }
+
+ const results = await Promise.all(requests)
+ setLoading(false)
+
+ setAccountInvestments(results.flatMap((x) => x))
}
- }, [eligibleInvestments, currentAccount, mngoAccounts])
+
+ loadData()
+ }, [currentAccount, mngoAccounts])
+
useEffect(() => {
const getMangoAcccounts = async () => {
const accounts = await tryGetMangoAccountsForOwner(
@@ -129,7 +207,7 @@ const AccountOverview = () => {
if (currentAccount) {
getMangoAcccounts()
}
- }, [currentAccount, eligibleInvestments, market])
+ }, [currentAccount, market])
useEffect(() => {
if (isCopied) {
@@ -140,6 +218,14 @@ const AccountOverview = () => {
}
}, [isCopied])
+ useEffect(() => {
+ if (visibleInvestments.length && visibleAccounts.length === 0) {
+ setShowStrategies(true)
+ } else {
+ setShowStrategies(false)
+ }
+ }, [currentAccount, visibleAccounts.length, visibleInvestments.length])
+
const handleCopyAddress = (address: string) => {
navigator.clipboard.writeText(address)
setIsCopied(true)
@@ -149,6 +235,14 @@ const AccountOverview = () => {
return null
}
+ const deposits = visibleAccounts.reduce(
+ (acc, account) => ({
+ ...acc,
+ [account.protocolName]:
+ (acc[account.protocolName] ?? 0) + account.investedAmount,
+ }),
+ {}
+ )
return (
<>
@@ -300,12 +394,17 @@ const AccountOverview = () => {
{showStrategies ? (
- eligibleInvestments.length > 0 ? (
- eligibleInvestments.map((strat, i) => (
+ visibleInvestments.length > 0 ? (
+ visibleInvestments.map((strat, i) => (
setProposedInvestment(strat)}
+ currentDeposits={deposits[strat.protocolName] ?? 0}
+ onClick={() => {
+ setProposedInvestment({
+ ...strat,
+ investedAmount: deposits[strat.protocolName] ?? 0,
+ })
+ }}
strat={strat}
/>
))
@@ -316,18 +415,18 @@ const AccountOverview = () => {
)
- ) : accountInvestments.length > 0 ? (
- accountInvestments.map((strat, i) => (
+ ) : visibleAccounts.length > 0 ? (
+ visibleAccounts.map((strat, i) => (
))
) : (
- No investments for this account
+ {loading ? 'Loading...' : 'No investments for this account'}
)}
@@ -371,13 +470,13 @@ const AccountOverview = () => {
{
setProposedInvestment(null)
}}
- isOpen={proposedInvestment}
+ proposedInvestment={proposedInvestment}
protocolName={proposedInvestment.protocolName}
protocolLogoSrc={proposedInvestment.protocolLogoSrc}
handledTokenName={proposedInvestment.handledTokenSymbol}
@@ -455,27 +554,37 @@ const AccountOverview = () => {
interface StrategyCardProps {
onClick?: () => void
strat: TreasuryStrategy
- currentMangoDeposits: number
+ currentDeposits: number
}
const StrategyCard = ({
onClick,
strat,
- currentMangoDeposits,
+ currentDeposits,
}: StrategyCardProps) => {
const {
handledTokenImgSrc,
strategyName,
protocolName,
+ strategySubtext,
handledTokenSymbol,
apy,
} = strat
const currentPositionFtm = new BigNumber(
- currentMangoDeposits.toFixed(0)
+ currentDeposits.toFixed(2)
).toFormat()
return (
+ {strat.protocolLogoSrc ? (
+
+ ) : null}
{handledTokenImgSrc ? (
) : null}
-
{`${strategyName} ${handledTokenSymbol} on ${protocolName}`}
+
{`${strategyName} ${handledTokenSymbol} on ${protocolName}${
+ strategySubtext ? ` - ${strategySubtext}` : ''
+ }`}
{`${currentPositionFtm} ${handledTokenSymbol}`}
diff --git a/components/instructions/programs/associatedTokenAccount.tsx b/components/instructions/programs/associatedTokenAccount.tsx
index 7f9ec71ba7..d94c01bbb0 100644
--- a/components/instructions/programs/associatedTokenAccount.tsx
+++ b/components/instructions/programs/associatedTokenAccount.tsx
@@ -24,7 +24,7 @@ export const ATA_PROGRAM_INSTRUCTIONS = {
Object.keys(SPL_TOKENS).find(
(name) => SPL_TOKENS[name].mint?.toString() === tokenMint
)!
- ].name ?? 'unknown'
+ ]?.name ?? 'unknown'
return (