From c65948b6b5a08fc4bb75c6ec6791863807fe0443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezi=C5=84ski?= Date: Sat, 7 Oct 2023 23:19:13 +0200 Subject: [PATCH] reclaim vaults from distribution - mango instruction (#1863) --- hooks/useGovernanceAssets.ts | 9 + package.json | 1 + .../DistrubtionProgram/CloseVaults.tsx | 276 ++++++++++++++++++ pages/dao/[symbol]/proposal/new.tsx | 2 + utils/uiTypes/proposalCreationTypes.ts | 2 + yarn.lock | 40 +++ 6 files changed, 330 insertions(+) create mode 100644 pages/dao/[symbol]/proposal/components/instructions/DistrubtionProgram/CloseVaults.tsx diff --git a/hooks/useGovernanceAssets.ts b/hooks/useGovernanceAssets.ts index d840222d7f..b2126797a2 100644 --- a/hooks/useGovernanceAssets.ts +++ b/hooks/useGovernanceAssets.ts @@ -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', @@ -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) diff --git a/package.json b/package.json index a1d970630e..9331c8329d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/dao/[symbol]/proposal/components/instructions/DistrubtionProgram/CloseVaults.tsx b/pages/dao/[symbol]/proposal/components/instructions/DistrubtionProgram/CloseVaults.tsx new file mode 100644 index 0000000000..c7358c5f58 --- /dev/null +++ b/pages/dao/[symbol]/proposal/components/instructions/DistrubtionProgram/CloseVaults.tsx @@ -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 | 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({ + governedAccount: null, + distributionNumber: 0, + }) + const [client, setClient] = useState() + const [distribution, setDistribution] = useState() + const [vaults, setVaults] = useState<{ [pubkey: string]: Vault }>() + const [formErrors, setFormErrors] = useState({}) + const { handleSetInstructions } = useContext(NewProposalContext) + const validateInstruction = async (): Promise => { + const { isValid, validationErrors } = await isFormValid(schema, form) + setFormErrors(validationErrors) + return isValid + } + async function getInstruction(): Promise { + 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: ( +
+ +
+ ), + inputType: 'number', + name: 'distributionNumber', + }, + ] + + return ( + <> + {form && ( + <> + + {distribution && vaults && ( +
+ + Vaults to close + + +
+ {vaults + ? Object.entries(vaults).map(([address, vault]) => { + return ( +
+

{address}

{' '} +

+ { + distribution.metadata!.mints[vault.mintIndex] + .properties?.name + } +

{' '} + + {vault.amount > -1 + ? vault.amount.toString() + : 'Deleted'} + +
+ ) + }) + : 'Loading...'} +
+
+
+ )} + + )} + + ) +} + +export default CloseVaults diff --git a/pages/dao/[symbol]/proposal/new.tsx b/pages/dao/[symbol]/proposal/new.tsx index dbc8bb36e0..51a1c03dc2 100644 --- a/pages/dao/[symbol]/proposal/new.tsx +++ b/pages/dao/[symbol]/proposal/new.tsx @@ -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) @@ -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, diff --git a/utils/uiTypes/proposalCreationTypes.ts b/utils/uiTypes/proposalCreationTypes.ts index ee5a258612..79c4824d03 100644 --- a/utils/uiTypes/proposalCreationTypes.ts +++ b/utils/uiTypes/proposalCreationTypes.ts @@ -26,6 +26,7 @@ export enum PackageEnum { Solend, Switchboard, VsrPlugin, + Distribution, } export interface UiInstruction { @@ -340,6 +341,7 @@ export enum Instructions { DualFinanceDelegateWithdraw, DualFinanceVoteDeposit, DualFinanceVote, + DistributionCloseVaults, DelegateStake, ForesightAddMarketListToCategory, ForesightInitCategory, diff --git a/yarn.lock b/yarn.lock index b41bd79626..7b7beed530 100644 --- a/yarn.lock +++ b/yarn.lock @@ -452,6 +452,17 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@blockworks-foundation/mango-mints-redemption@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-mints-redemption/-/mango-mints-redemption-0.0.8.tgz#6b02f3e81c9be14fa95ba8da5f156167ac1b971b" + integrity sha512-P0F+e6I/TcCGunqZlTaKvH5YbTbqBxx6EyMV4Av2h6VFlwFmJBF2RoUwF27JCpD3F1VzY8nMKGILrLhiTqyWPQ== + dependencies: + "@coral-xyz/anchor" "^0.28.0" + "@solana/spl-token" "^0.3.8" + axios "^1.4.0" + keccak256 "^1.0.6" + merkletreejs "^0.3.10" + "@blockworks-foundation/mango-v4-settings@0.2.13": version "0.2.13" resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4-settings/-/mango-v4-settings-0.2.13.tgz#c75c1ea2e8e4c7888e45e979d75d18a2342142bf" @@ -6663,6 +6674,15 @@ axios@^1.1.3: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.4.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" + integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" @@ -11090,6 +11110,15 @@ jsqr@^1.2.0: array-includes "^3.1.5" object.assign "^4.1.3" +keccak256@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" + integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== + dependencies: + bn.js "^5.2.0" + buffer "^6.0.3" + keccak "^3.0.2" + keccak@^3.0.0, keccak@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" @@ -11620,6 +11649,17 @@ merkletreejs@^0.2.32: treeify "^1.1.0" web3-utils "^1.3.4" +merkletreejs@^0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.3.10.tgz#b9abdfc5e3aadaf9eb8b0a35c4b87aea33f5d4b7" + integrity sha512-lin42tKfRdkW+6iE5pjtQ9BnH+1Hk3sJ5Fn9hUUSjcXRcJbSISHgPCfYvMNEXiNqZPhz/TyRPEV30qgnujsQ7A== + dependencies: + bignumber.js "^9.0.1" + buffer-reverse "^1.0.1" + crypto-js "^3.1.9-1" + treeify "^1.1.0" + web3-utils "^1.3.4" + micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"