Skip to content

Commit

Permalink
reclaim vaults from distribution - mango instruction (#1863)
Browse files Browse the repository at this point in the history
  • Loading branch information
abrzezinski94 authored Oct 7, 2023
1 parent 0f82a9a commit c65948b
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 0 deletions.
9 changes: 9 additions & 0 deletions hooks/useGovernanceAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export default function useGovernanceAssets() {
name: 'Dual Finance',
image: '/img/dual-logo.png',
},
[PackageEnum.Distribution]: {
name: 'Distribution Program',
},
[PackageEnum.Foresight]: {
name: 'Foresight',
isVisible: symbol === 'FORE',
Expand Down Expand Up @@ -746,6 +749,12 @@ export default function useGovernanceAssets() {
isVisible: canUseAuthorityInstruction,
packageId: PackageEnum.VsrPlugin,
},

[Instructions.DistributionCloseVaults]: {
name: 'Close vaults',
isVisible: canUseAuthorityInstruction,
packageId: PackageEnum.Distribution,
},
}

const availablePackages: PackageType[] = Object.entries(packages)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
]
},
"dependencies": {
"@blockworks-foundation/mango-mints-redemption": "0.0.8",
"@blockworks-foundation/mango-v4": "0.19.29",
"@blockworks-foundation/mango-v4-settings": "0.2.13",
"@blockworks-foundation/mangolana": "0.0.1-beta.15",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { useContext, useEffect, useState } from 'react'
import * as yup from 'yup'
import { isFormValid } from '@utils/formValidation'
import { UiInstruction } from '@utils/uiTypes/proposalCreationTypes'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import {
Governance,
SYSTEM_PROGRAM_ID,
serializeInstructionToBase64,
} from '@solana/spl-governance'
import { ProgramAccount } from '@solana/spl-governance'
import { AccountType, AssetAccount } from '@utils/uiTypes/assets'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import { NewProposalContext } from '../../../new'
import InstructionForm, { InstructionInput } from '../FormCreator'
import { InstructionInputType } from '../inputInstructionType'
import {
Distribution,
MangoMintsRedemptionClient,
} from '@blockworks-foundation/mango-mints-redemption'
import { AnchorProvider } from '@coral-xyz/anchor'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
import EmptyWallet from '@utils/Mango/listingTools'
import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'
import { tryGetTokenAccount } from '@utils/tokens'
import Button from '@components/Button'
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
Token,
} from '@solana/spl-token'

interface CloseVaultsForm {
governedAccount: AssetAccount | null
distributionNumber: number
}

type Vault = {
publicKey: PublicKey
amount: bigint
mintIndex: number
mint: PublicKey
}

const CloseVaults = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const wallet = useWalletOnePointOh()
const { assetAccounts } = useGovernanceAssets()
const solAccounts = assetAccounts.filter((x) => x.type === AccountType.SOL)
const connection = useLegacyConnectionContext()
const shouldBeGoverned = !!(index !== 0 && governance)
const [form, setForm] = useState<CloseVaultsForm>({
governedAccount: null,
distributionNumber: 0,
})
const [client, setClient] = useState<MangoMintsRedemptionClient>()
const [distribution, setDistribution] = useState<Distribution>()
const [vaults, setVaults] = useState<{ [pubkey: string]: Vault }>()
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const validateInstruction = async (): Promise<boolean> => {
const { isValid, validationErrors } = await isFormValid(schema, form)
setFormErrors(validationErrors)
return isValid
}
async function getInstruction(): Promise<UiInstruction> {
const isValid = await validateInstruction()
let serializedInstruction = ''
const mintsOfCurrentlyPushedAtaInstructions: string[] = []
const additionalSerializedInstructions: string[] = []
const prerequisiteInstructions: TransactionInstruction[] = []
if (
isValid &&
form.governedAccount?.governance?.account &&
wallet?.publicKey &&
vaults
) {
for (const v of Object.values(vaults)) {
const ataAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
v.mint,
form.governedAccount.extensions.transferAddress!,
true
)

const depositAccountInfo = await connection.current.getAccountInfo(
ataAddress
)
if (
!depositAccountInfo &&
!mintsOfCurrentlyPushedAtaInstructions.find(
(x) => x !== v.mint.toBase58()
)
) {
// generate the instruction for creating the ATA
prerequisiteInstructions.push(
Token.createAssociatedTokenAccountInstruction(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
v.mint,
ataAddress,
form.governedAccount.extensions.transferAddress!,
wallet.publicKey
)
)
mintsOfCurrentlyPushedAtaInstructions.push(v.mint.toBase58())
}

const ix = await client?.program.methods
.vaultClose()
.accounts({
distribution: distribution?.publicKey,
vault: v.publicKey,
mint: v.mint,
destination: ataAddress,
authority: form.governedAccount.extensions.transferAddress,
systemProgram: SYSTEM_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction()
additionalSerializedInstructions.push(serializeInstructionToBase64(ix!))
}
serializedInstruction = ''
}
const obj: UiInstruction = {
additionalSerializedInstructions,
prerequisiteInstructions,
serializedInstruction: serializedInstruction,
isValid,
governance: form.governedAccount?.governance,
customHoldUpTime: form.distributionNumber,
}
return obj
}
const handleSelectDistribution = async (number: number) => {
const distribution = await client?.loadDistribution(number)
setDistribution(distribution)
}
const fetchVaults = async () => {
if (!client || !distribution) return
const v: any = {}
for (let i = 0; i < distribution.metadata!.mints.length; i++) {
const mint = distribution.metadata!.mints[i]
const vaultAddress = distribution.findVaultAddress(
new PublicKey(mint.address)
)
try {
const tokenAccount = await tryGetTokenAccount(
connection.current,
vaultAddress
)

v[vaultAddress.toString()] = {
publicKey: vaultAddress,
amount: tokenAccount?.account.amount,
mint: tokenAccount?.account.mint,
mintIndex: i,
}
} catch {
v[vaultAddress.toString()] = { amount: -1, mintIndex: i }
}
}
setVaults(v)
}
useEffect(() => {
if (distribution) {
fetchVaults()
}
}, [distribution])
useEffect(() => {
const client = new MangoMintsRedemptionClient(
new AnchorProvider(
connection.current,
new EmptyWallet(Keypair.generate()),
{ skipPreflight: true }
)
)
setClient(client)
}, [])
useEffect(() => {
handleSetInstructions(
{ governedAccount: form.governedAccount?.governance, getInstruction },
index
)
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO please fix, it can cause difficult bugs. You might wanna check out https://bobbyhadz.com/blog/react-hooks-exhaustive-deps for info. -@asktree
}, [form])
const schema = yup.object().shape({
governedAccount: yup
.object()
.nullable()
.required('Program governed account is required'),
})
const inputs: InstructionInput[] = [
{
label: 'Governance',
initialValue: form.governedAccount,
name: 'governedAccount',
type: InstructionInputType.GOVERNED_ACCOUNT,
shouldBeGoverned: shouldBeGoverned as any,
governance: governance,
options: solAccounts,
},
{
label: 'Distribution Number',
initialValue: form.distributionNumber,
type: InstructionInputType.INPUT,
additionalComponent: (
<div>
<Button
onClick={() => handleSelectDistribution(form.distributionNumber)}
>
Load
</Button>
</div>
),
inputType: 'number',
name: 'distributionNumber',
},
]

return (
<>
{form && (
<>
<InstructionForm
outerForm={form}
setForm={setForm}
inputs={inputs}
setFormErrors={setFormErrors}
formErrors={formErrors}
></InstructionForm>
{distribution && vaults && (
<div className="border-t border-th-bkg-2 px-6 py-3">
<span className="mr-4 mb-3 flex flex-col whitespace-nowrap text-th-fgd-3">
Vaults to close
</span>
<span className="flex flex-col font-mono text-th-fgd-2">
<div>
{vaults
? Object.entries(vaults).map(([address, vault]) => {
return (
<div key={address} className="flex justify-between">
<p>{address}</p>{' '}
<p>
{
distribution.metadata!.mints[vault.mintIndex]
.properties?.name
}
</p>{' '}
<span>
{vault.amount > -1
? vault.amount.toString()
: 'Deleted'}
</span>
</div>
)
})
: 'Loading...'}
</div>
</span>
</div>
)}
</>
)}
</>
)
}

export default CloseVaults
2 changes: 2 additions & 0 deletions pages/dao/[symbol]/proposal/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import DualVote from './components/instructions/Dual/DualVote'
import DualGso from './components/instructions/Dual/DualGso'
import DualGsoWithdraw from './components/instructions/Dual/DualGsoWithdraw'
import MultiChoiceForm from '../../../../components/MultiChoiceForm'
import CloseVaults from './components/instructions/DistrubtionProgram/CloseVaults'

const TITLE_LENGTH_LIMIT = 130
// the true length limit is either at the tx size level, and maybe also the total account size level (I can't remember)
Expand Down Expand Up @@ -492,6 +493,7 @@ const New = () => {
[Instructions.DualFinanceDelegateWithdraw]: DualVoteDepositWithdraw,
[Instructions.DualFinanceVoteDeposit]: DualVoteDeposit,
[Instructions.DualFinanceVote]: DualVote,
[Instructions.DistributionCloseVaults]: CloseVaults,
[Instructions.MeanCreateAccount]: MeanCreateAccount,
[Instructions.MeanFundAccount]: MeanFundAccount,
[Instructions.MeanWithdrawFromAccount]: MeanWithdrawFromAccount,
Expand Down
2 changes: 2 additions & 0 deletions utils/uiTypes/proposalCreationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum PackageEnum {
Solend,
Switchboard,
VsrPlugin,
Distribution,
}

export interface UiInstruction {
Expand Down Expand Up @@ -340,6 +341,7 @@ export enum Instructions {
DualFinanceDelegateWithdraw,
DualFinanceVoteDeposit,
DualFinanceVote,
DistributionCloseVaults,
DelegateStake,
ForesightAddMarketListToCategory,
ForesightInitCategory,
Expand Down
Loading

1 comment on commit c65948b

@vercel
Copy link

@vercel vercel bot commented on c65948b Oct 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

governance-ui – ./

governance-ui-solana-labs.vercel.app
app.realms.today
governance-ui-git-main-solana-labs.vercel.app

Please sign in to comment.