DAO Delegation Support (#1692)

Co-authored-by: Adrian Brzeziński <a.brzezinski94@gmail.com>
This commit is contained in:
DonDuala 2023-06-28 14:35:51 -04:00 committed by GitHub
parent 766de633a3
commit e2d34f924c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 677 additions and 2 deletions

View File

@ -66,7 +66,8 @@ export default async function depositTokens({
wallet!.publicKey,
transferAuthority.publicKey,
wallet!.publicKey,
amountBN
amountBN,
true
)
const transaction = new Transaction()

View File

@ -80,6 +80,8 @@ export const GOVERNANCE_INSTRUCTIONS = {
connection,
realm.owner
)
//accounts[2] is token account not mint account
const mintInfoQuery = await fetchMintInfoByPubkey(
connection,
accounts[2].pubkey
@ -90,7 +92,11 @@ export const GOVERNANCE_INSTRUCTIONS = {
DepositGoverningTokensArgs,
Buffer.from(data)
) as DepositGoverningTokensArgs
console.log(
args.amount,
mintInfoQuery?.result,
accounts[2].pubkey.toBase58()
)
return (
<>
<p>

View File

@ -400,6 +400,21 @@ export default function useGovernanceAssets() {
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},
[Instructions.DualFinanceDelegate]: {
name: 'Delegate',
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},
[Instructions.DualFinanceDelegateWithdraw]: {
name: 'Withdraw Delegate',
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},
[Instructions.DualFinanceVoteDeposit]: {
name: 'Vote Deposit',
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},
/*

View File

@ -0,0 +1,119 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useContext, useEffect, useState } from 'react'
import { ProgramAccount, Governance } from '@solana/spl-governance'
import {
UiInstruction,
DualFinanceDelegateForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { NewProposalContext } from '../../../new'
import GovernedAccountSelect from '../../GovernedAccountSelect'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import Input from '@components/inputs/Input'
import { getDelegateInstruction } from '@utils/instructions/Dual/delegate'
import { getDualFinanceDelegateSchema } from '@utils/validations'
import Tooltip from '@components/Tooltip'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
const DualDelegate = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const [form, setForm] = useState<DualFinanceDelegateForm>({
delegateAccount: undefined,
realm: undefined,
delegateToken: undefined,
})
const connection = useLegacyConnectionContext()
const wallet = useWalletOnePointOh()
const shouldBeGoverned = !!(index !== 0 && governance)
const { assetAccounts } = useGovernanceAssets()
const [governedAccount, setGovernedAccount] = useState<
ProgramAccount<Governance> | undefined
>(undefined)
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const handleSetForm = ({ propertyName, value }) => {
setFormErrors({})
setForm({ ...form, [propertyName]: value })
}
const schema = getDualFinanceDelegateSchema()
useEffect(() => {
function getInstruction(): Promise<UiInstruction> {
return getDelegateInstruction({
connection,
form,
schema,
setFormErrors,
wallet,
})
}
handleSetInstructions(
{ governedAccount: governedAccount, getInstruction },
index
)
}, [
form,
governedAccount,
handleSetInstructions,
index,
connection,
schema,
wallet,
])
useEffect(() => {
setGovernedAccount(form.delegateToken?.governance)
}, [form.delegateToken?.governance])
// TODO: Include this in the config instruction which can optionally be done
// if the project doesnt need to change where the tokens get returned to.
return (
<>
<Tooltip content="Account to Delegate Votes">
<Input
label="Delegate Account"
value={form.delegateAccount}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'delegateAccount',
})
}
error={formErrors['delegateAccount']}
/>
</Tooltip>
<Input
label="Realm"
value={form.realm}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'realm',
})
}
error={formErrors['realm']}
/>
<Tooltip content="Token to be delegated.">
<GovernedAccountSelect
label="Delegate Token"
governedAccounts={assetAccounts}
onChange={(value) => {
handleSetForm({ value, propertyName: 'delegateToken' })
}}
value={form.delegateToken}
error={formErrors['delegateToken']}
shouldBeGoverned={shouldBeGoverned}
governance={governance}
type="token"
></GovernedAccountSelect>
</Tooltip>
</>
)
}
export default DualDelegate

View File

@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useContext, useEffect, useState } from 'react'
import { ProgramAccount, Governance } from '@solana/spl-governance'
import {
UiInstruction,
DualFinanceDelegateWithdrawForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { NewProposalContext } from '../../../new'
import GovernedAccountSelect from '../../GovernedAccountSelect'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import Input from '@components/inputs/Input'
import { getDelegateWithdrawInstruction } from '@utils/instructions/Dual/delegate'
import { getDualFinanceDelegateWithdrawSchema } from '@utils/validations'
import Tooltip from '@components/Tooltip'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
const DualDelegateWithdraw = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const [form, setForm] = useState<DualFinanceDelegateWithdrawForm>({
realm: undefined,
delegateToken: undefined,
})
const connection = useLegacyConnectionContext()
const wallet = useWalletOnePointOh()
const shouldBeGoverned = !!(index !== 0 && governance)
const { assetAccounts } = useGovernanceAssets()
const [governedAccount, setGovernedAccount] = useState<
ProgramAccount<Governance> | undefined
>(undefined)
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const handleSetForm = ({ propertyName, value }) => {
setFormErrors({})
setForm({ ...form, [propertyName]: value })
}
const schema = getDualFinanceDelegateWithdrawSchema()
useEffect(() => {
function getInstruction(): Promise<UiInstruction> {
return getDelegateWithdrawInstruction({
connection,
form,
schema,
setFormErrors,
wallet,
})
}
handleSetInstructions(
{ governedAccount: governedAccount, getInstruction },
index
)
}, [
form,
governedAccount,
handleSetInstructions,
index,
connection,
schema,
wallet,
])
useEffect(() => {
setGovernedAccount(form.delegateToken?.governance)
}, [form.delegateToken?.governance])
// TODO: Include this in the config instruction which can optionally be done
// if the project doesnt need to change where the tokens get returned to.
return (
<>
<Input
label="Realm"
value={form.realm}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'realm',
})
}
error={formErrors['realm']}
/>
<Tooltip content="Token to be delegated.">
<GovernedAccountSelect
label="Delegate Token"
governedAccounts={assetAccounts}
onChange={(value) => {
handleSetForm({ value, propertyName: 'delegateToken' })
}}
value={form.delegateToken}
error={formErrors['delegateToken']}
shouldBeGoverned={shouldBeGoverned}
governance={governance}
type="token"
></GovernedAccountSelect>
</Tooltip>
</>
)
}
export default DualDelegateWithdraw

View File

@ -0,0 +1,114 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useContext, useEffect, useState } from 'react'
import { ProgramAccount, Governance } from '@solana/spl-governance'
import {
UiInstruction,
DualFinanceVoteDepositForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { NewProposalContext } from '../../../new'
import GovernedAccountSelect from '../../GovernedAccountSelect'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import Input from '@components/inputs/Input'
import { getVoteDepositInstruction } from '@utils/instructions/Dual/delegate'
import { getDualFinanceVoteDepositSchema } from '@utils/validations'
import Tooltip from '@components/Tooltip'
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
const DualVoteDeposit = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const [form, setForm] = useState<DualFinanceVoteDepositForm>({
numTokens: 0,
realm: undefined,
delegateToken: undefined,
})
const connection = useLegacyConnectionContext()
const wallet = useWalletOnePointOh()
const shouldBeGoverned = !!(index !== 0 && governance)
const { assetAccounts } = useGovernanceAssets()
const [governedAccount, setGovernedAccount] = useState<
ProgramAccount<Governance> | undefined
>(undefined)
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const handleSetForm = ({ propertyName, value }) => {
setFormErrors({})
setForm({ ...form, [propertyName]: value })
}
const schema = getDualFinanceVoteDepositSchema()
useEffect(() => {
function getInstruction(): Promise<UiInstruction> {
return getVoteDepositInstruction({
connection,
form,
schema,
setFormErrors,
wallet,
})
}
handleSetInstructions(
{ governedAccount: governedAccount, getInstruction },
index
)
}, [form, governedAccount, handleSetInstructions, index, connection, schema, wallet])
useEffect(() => {
handleSetForm({ value: undefined, propertyName: 'mintPk' })
}, [form.delegateToken])
useEffect(() => {
setGovernedAccount(form.delegateToken?.governance)
}, [form.delegateToken])
// TODO: Include this in the config instruction which can optionally be done
// if the project doesnt need to change where the tokens get returned to.
return (
<>
<Tooltip content="How many option tokens are exercised staking options.">
<Input
label="Quantity"
value={form.numTokens}
type="number"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'numTokens',
})
}
error={formErrors['numTokens']}
/>
</Tooltip>
<Input
label="Realm"
value={form.realm}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'realm',
})
}
error={formErrors['realm']}
/>
<Tooltip content="Token to be delegated.">
<GovernedAccountSelect
label="Delegate Token"
governedAccounts={assetAccounts}
onChange={(value) => {
handleSetForm({ value, propertyName: 'delegateToken' })
}}
value={form.delegateToken}
error={formErrors['delegateToken']}
shouldBeGoverned={shouldBeGoverned}
governance={governance}
type="token"
></GovernedAccountSelect>
</Tooltip>
</>
)
}
export default DualVoteDeposit

View File

@ -115,6 +115,9 @@ import RemoveServiceFromDID from './components/instructions/Identity/RemoveServi
import DualAirdrop from './components/instructions/Dual/DualAirdrop'
import DualWithdraw from './components/instructions/Dual/DualWithdraw'
import DualExercise from './components/instructions/Dual/DualExercise'
import DualDelegate from './components/instructions/Dual/DualDelegate'
import DualDelegateWithdraw from './components/instructions/Dual/DualDelegateWithdraw'
import DualVoteDeposit from './components/instructions/Dual/DualVoteDeposit'
import PsyFinanceMintAmericanOptions from './components/instructions/PsyFinance/MintAmericanOptions'
import IxGateSet from './components/instructions/Mango/MangoV4/IxGateSet'
import StubOracleCreate from './components/instructions/Mango/MangoV4/StubOracleCreate'
@ -422,6 +425,9 @@ const New = () => {
[Instructions.DualFinanceLiquidityStakingOption]: LiquidityStakingOption,
[Instructions.DualFinanceWithdraw]: DualWithdraw,
[Instructions.DualFinanceExercise]: DualExercise,
[Instructions.DualFinanceDelegate]: DualDelegate,
[Instructions.DualFinanceDelegateWithdraw]: DualDelegateWithdraw,
[Instructions.DualFinanceVoteDeposit]: DualVoteDeposit,
[Instructions.MeanCreateAccount]: MeanCreateAccount,
[Instructions.MeanFundAccount]: MeanFundAccount,
[Instructions.MeanWithdrawFromAccount]: MeanWithdrawFromAccount,

View File

@ -0,0 +1,251 @@
import {
TokenOwnerRecord,
getGovernanceProgramVersion,
getTokenOwnerRecordAddress,
serializeInstructionToBase64,
withCreateTokenOwnerRecord,
withDepositGoverningTokens,
withSetGovernanceDelegate,
ProgramAccount,
getTokenOwnerRecordForRealm,
withWithdrawGoverningTokens,
} from '@solana/spl-governance'
import { ConnectionContext } from '@utils/connection'
import { validateInstruction } from '@utils/instructionTools'
import {
DualFinanceVoteDepositForm,
DualFinanceDelegateForm,
UiInstruction,
DualFinanceDelegateWithdrawForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { WalletAdapter } from '@solana/wallet-adapter-base'
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { getMintNaturalAmountFromDecimalAsBN } from '@tools/sdk/units'
interface DelegateArgs {
connection: ConnectionContext
form: DualFinanceDelegateForm
setFormErrors: any
schema: any
wallet: WalletAdapter | undefined
}
interface DelegateWithdrawArgs {
connection: ConnectionContext
form: DualFinanceDelegateWithdrawForm
setFormErrors: any
schema: any
wallet: WalletAdapter | undefined
}
interface VoteDepositArgs {
connection: ConnectionContext
form: DualFinanceVoteDepositForm
setFormErrors: any
schema: any
wallet: WalletAdapter | undefined
}
export async function getDelegateInstruction({
connection,
wallet,
form,
schema,
setFormErrors,
}: DelegateArgs): Promise<UiInstruction> {
const isValid = await validateInstruction({ schema, form, setFormErrors })
const serializedInstruction = ''
const additionalSerializedInstructions: string[] = []
const prerequisiteInstructions: TransactionInstruction[] = []
const instructions: TransactionInstruction[] = []
if (
isValid &&
form.delegateAccount &&
wallet?.publicKey &&
form.realm &&
form.delegateToken &&
form.delegateToken.extensions.mint?.publicKey
) {
const programVersion = await getGovernanceProgramVersion(
connection.current,
form.delegateToken?.governance.owner // governance program public key
)
await withSetGovernanceDelegate(
instructions,
form.delegateToken.governance.owner, // publicKey of program/programId
programVersion, // program version of realm
new PublicKey(form.realm), // realm public key
form.delegateToken.extensions.mint.publicKey, // mint of governance token
form.delegateToken.governance.nativeTreasuryAddress, // governingTokenOwner (walletId) publicKey of tokenOwnerRecord of this wallet
form.delegateToken.governance.nativeTreasuryAddress, // governanceAuthority: publicKey of connected wallet
new PublicKey(form.delegateAccount) // public key of wallet who to delegated vote to
)
for (const ix of instructions) {
additionalSerializedInstructions.push(serializeInstructionToBase64(ix))
}
}
return {
serializedInstruction,
isValid: true,
prerequisiteInstructions: prerequisiteInstructions,
governance: form.delegateToken?.governance,
additionalSerializedInstructions,
chunkBy: 1,
}
}
export async function getVoteDepositInstruction({
connection,
wallet,
form,
schema,
setFormErrors,
}: VoteDepositArgs): Promise<UiInstruction> {
const isValid = await validateInstruction({ schema, form, setFormErrors })
const serializedInstruction = ''
const additionalSerializedInstructions: string[] = []
const prerequisiteInstructions: TransactionInstruction[] = []
const instructions: TransactionInstruction[] = []
if (
isValid &&
form.numTokens &&
wallet?.publicKey &&
form.realm &&
form.delegateToken &&
form.delegateToken.extensions.mint?.publicKey
) {
const programVersion = await getGovernanceProgramVersion(
connection.current,
form.delegateToken.governance.owner // governance program public key
)
const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress(
form.delegateToken.governance.owner,
new PublicKey(form.realm),
form.delegateToken.extensions.mint.publicKey,
form.delegateToken.governance.nativeTreasuryAddress
)
let existingTokenOwnerRecord: null | ProgramAccount<TokenOwnerRecord> = null
try {
existingTokenOwnerRecord = await getTokenOwnerRecordForRealm(
connection.current,
form.delegateToken.governance.owner,
new PublicKey(form.realm),
form.delegateToken.extensions.mint.publicKey,
form.delegateToken.governance.nativeTreasuryAddress
)
} catch (e) {
console.log(e)
}
console.log(existingTokenOwnerRecord)
if (!existingTokenOwnerRecord) {
console.log(
'Creating new vote record',
tokenOwnerRecordAddress.toBase58(),
connection.current.rpcEndpoint
)
await withCreateTokenOwnerRecord(
instructions,
form.delegateToken.governance.owner,
programVersion,
new PublicKey(form.realm),
form.delegateToken.governance.nativeTreasuryAddress,
form.delegateToken.extensions.mint.publicKey,
form.delegateToken.governance.nativeTreasuryAddress
)
}
await withDepositGoverningTokens(
instructions,
form.delegateToken.governance.owner, // publicKey of program/programId
programVersion, // program version of realm
new PublicKey(form.realm), // realm public key
form.delegateToken.pubkey,
form.delegateToken.extensions.mint.publicKey, // mint of governance token
form.delegateToken.governance.nativeTreasuryAddress, // governingTokenOwner (walletId) publicKey of tokenOwnerRecord of this wallet
form.delegateToken.governance.nativeTreasuryAddress, // governanceAuthority: publicKey of connected wallet
form.delegateToken.governance.nativeTreasuryAddress,
getMintNaturalAmountFromDecimalAsBN(
form.numTokens,
form.delegateToken.extensions.mint.account.decimals
)
)
for (const ix of instructions) {
additionalSerializedInstructions.push(serializeInstructionToBase64(ix))
}
}
return {
serializedInstruction,
isValid: true,
prerequisiteInstructions: prerequisiteInstructions,
governance: form.delegateToken?.governance,
additionalSerializedInstructions,
chunkBy: 1,
}
}
// TODO: Remove need for delegateToken
// TODO: use a prequiste if community mint ata was burnt
// TODO: Relinquish vote or finalize if delegate has active vote
export async function getDelegateWithdrawInstruction({
connection,
wallet,
form,
schema,
setFormErrors,
}: DelegateWithdrawArgs): Promise<UiInstruction> {
const isValid = await validateInstruction({ schema, form, setFormErrors })
const serializedInstruction = ''
const additionalSerializedInstructions: string[] = []
const prerequisiteInstructions: TransactionInstruction[] = []
const instructions: TransactionInstruction[] = []
if (
isValid &&
wallet?.publicKey &&
form.realm &&
form.delegateToken &&
form.delegateToken.extensions.mint?.publicKey
) {
const programVersion = await getGovernanceProgramVersion(
connection.current,
form.delegateToken?.governance.owner // governance program public key
)
await withSetGovernanceDelegate(
instructions,
form.delegateToken.governance.owner, // publicKey of program/programId
programVersion, // program version of realm
new PublicKey(form.realm), // realm public key
form.delegateToken.extensions.mint.publicKey, // mint of governance token
form.delegateToken.governance.nativeTreasuryAddress, // governingTokenOwner (walletId) publicKey of tokenOwnerRecord of this wallet
form.delegateToken.governance.nativeTreasuryAddress, // governanceAuthority: publicKey of connected wallet
// @ts-ignore
null // remove delegate
)
await withWithdrawGoverningTokens(
instructions,
form.delegateToken.governance.owner, // publicKey of program/programId
programVersion, // program version of realm
new PublicKey(form.realm), // realm public key
form.delegateToken.pubkey,
form.delegateToken.extensions.mint.publicKey,
form.delegateToken.governance.nativeTreasuryAddress,
)
for (const ix of instructions) {
additionalSerializedInstructions.push(serializeInstructionToBase64(ix))
}
}
return {
serializedInstruction,
isValid: true,
prerequisiteInstructions: prerequisiteInstructions,
governance: form.delegateToken?.governance,
additionalSerializedInstructions,
chunkBy: 1,
}
}

View File

@ -416,6 +416,9 @@ export enum Instructions {
DualFinanceInitStrike,
DualFinanceStakingOption,
DualFinanceWithdraw,
DualFinanceDelegate,
DualFinanceDelegateWithdraw,
DualFinanceVoteDeposit,
EverlendDeposit,
EverlendWithdraw,
ForesightAddMarketListToCategory,
@ -593,3 +596,20 @@ export interface DualFinanceWithdrawForm {
baseTreasury: AssetAccount | undefined
mintPk: string | undefined
}
export interface DualFinanceDelegateForm {
delegateAccount: string | undefined
realm: string | undefined
delegateToken: AssetAccount | undefined
}
export interface DualFinanceDelegateWithdrawForm {
realm: string | undefined
delegateToken: AssetAccount | undefined
}
export interface DualFinanceVoteDepositForm {
numTokens: number
realm: string | undefined
delegateToken: AssetAccount | undefined
}

View File

@ -684,6 +684,45 @@ export const getDualFinanceWithdrawSchema = () => {
})
}
export const getDualFinanceDelegateSchema = () => {
return yup.object().shape({
delegateAccount: yup
.string()
.test('is-valid-address1', 'Please enter a valid PublicKey', (value) =>
value ? validatePubkey(value) : true
),
realm: yup
.string()
.test('is-valid-address1', 'Please enter a valid PublicKey', (value) =>
value ? validatePubkey(value) : true
),
token: yup.object().typeError('Delegate Token is required'),
})
}
export const getDualFinanceDelegateWithdrawSchema = () => {
return yup.object().shape({
realm: yup
.string()
.test('is-valid-address1', 'Please enter a valid PublicKey', (value) =>
value ? validatePubkey(value) : true
),
token: yup.object().typeError('Delegate Token is required'),
})
}
export const getDualFinanceVoteDepositSchema = () => {
return yup.object().shape({
numTokens: yup.number().typeError('Num tokens is required'),
realm: yup
.string()
.test('is-valid-address1', 'Please enter a valid PublicKey', (value) =>
value ? validatePubkey(value) : true
),
token: yup.object().typeError('Delegate Token is required'),
})
}
export const getGoblinGoldDepositSchema = ({ form }) => {
const governedTokenAccount = form.governedTokenAccount as AssetAccount
return yup.object().shape({