Overview
This guide walks you through integrating a new protocol instruction into the Governance UI by:
- Adding your protocol/package name to the existing
PackageEnum
. - Extending the
packages
configuration object so the UI can display the correct name and icon. - Creating a new instruction enum in
ProposalCreationType.ts
. - Updating
useGovernanceAssets.ts
to reference your custom instruction. - Adding a new folder and instruction component under
pages/dao/[symbol]/proposal/components/instructions
. - Providing a boilerplate code example (
AltSet
) that you can adapt to your own logic. - Decoding instructions for display in the proposal UI.
1. Extend the PackageEnum
In ProposalCreationType.ts
, add your protocol name to the PackageEnum
:
export enum PackageEnum {
Common,
Distribution,
Dual,
GatewayPlugin,
Identity,
MangoMarketV4,
MeanFinance,
NftPlugin,
PsyFinance,
Pyth,
Serum,
Solend,
Symmetry,
Squads,
Switchboard,
VsrPlugin,
// Add your protocol here
MyAwesomeProtocol,
}
2. Update the packages in useGovernanceAssets.ts
In useGovernanceAssets.ts
, locate the packages
object and add your protocol configuration. This ensures the protocol name and icon show up in the UI:
[PackageEnum.Solend]: {
name: 'Solend',
image: '/img/solend.png',
},
// Add your protocol
[PackageEnum.MyAwesomeProtocol]: {
name: 'My Awesome Protocol',
image: '/img/my-awesome-protocol.png', // path to your protocol icon
},
3. Add a new instruction name in ProposalCreationType.ts
Inside ProposalCreationType.ts
, under the Instructions
enum, add your custom instruction name. For example:
export enum Instructions {
Base64,
Burn,
// Add your custom instruction
MyAwesomeProtocolDoSomething,
}
This step tells the UI about a new type of instruction that can be selected when creating proposals.
4. Add your new instruction to useGovernanceAssets.ts
Under the instructions
object (or wherever instructions are listed), add your custom instruction so it can appear in the UI. For example:
[Instructions.MangoV4StubOracleSet]: {
name: 'Set Stub Oracle Value',
packageId: PackageEnum.MangoMarketV4,
isVisible: canUseAnyInstruction,
},
// Add your new instruction reference
[Instructions.MyAwesomeProtocolDoSomething]: {
name: 'Do Something Great',
packageId: PackageEnum.MyAwesomeProtocol,
isVisible: canUseAnyInstruction, // or any logic you need
},
5. Add a new folder and instruction component
Inside pages/dao/[symbol]/proposal/components/instructions
, create a folder named after your protocol, e.g. MyAwesomeProtocol
, and create a file with a descriptive name for your instruction, such as DoSomething.tsx
.
Here is a sample instruction file based on the Mango AltSet
example. This boilerplate demonstrates how to:
- Accept form data
- Validate with
yup
- Build the serialized instruction
- Return a
UiInstruction
object
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { useContext, useEffect, useState } from "react";
import { PublicKey } from "@solana/web3.js";
import * as yup from "yup";
import { isFormValid, validatePubkey } from "@utils/formValidation";
import { UiInstruction } from "@utils/uiTypes/proposalCreationTypes";
import { NewProposalContext } from "../../../new";
import useGovernanceAssets from "@hooks/useGovernanceAssets";
import { Governance } from "@solana/spl-governance";
import { ProgramAccount } from "@solana/spl-governance";
import { serializeInstructionToBase64 } from "@solana/spl-governance";
import { AccountType, AssetAccount } from "@utils/uiTypes/assets";
import InstructionForm, { InstructionInput } from "../FormCreator";
import { InstructionInputType } from "../inputInstructionType";
import useWalletOnePointOh from "@hooks/useWalletOnePointOh";
interface DoSomethingForm {
governedAccount: AssetAccount | null;
targetAddress: string;
someValue: number;
holdupTime: number;
}
const DoSomething = ({
index,
governance,
}: {
index: number;
governance: ProgramAccount<Governance> | null;
}) => {
const wallet = useWalletOnePointOh();
const { assetAccounts } = useGovernanceAssets();
// Filter accounts if needed
const filteredAccounts = assetAccounts.filter(
(x) => x.type === AccountType.SOL,
);
const shouldBeGoverned = !!(index !== 0 && governance);
const [form, setForm] = useState<DoSomethingForm>({
governedAccount: null,
targetAddress: "",
someValue: 0,
holdupTime: 0,
});
const [formErrors, setFormErrors] = useState({});
const { handleSetInstructions } = useContext(NewProposalContext);
// Yup schema for validation
const schema = yup.object().shape({
governedAccount: yup
.object()
.nullable()
.required("Governed account is required"),
targetAddress: yup
.string()
.required()
.test("is-valid-address", "Please enter a valid PublicKey", (value) =>
value ? validatePubkey(value) : false,
),
someValue: yup.number().required().min(1),
});
// 1) Validate the form
// 2) Build and serialize the instruction
const getInstruction = async (): Promise<UiInstruction> => {
const { isValid, validationErrors } = await isFormValid(schema, form);
setFormErrors(validationErrors);
let serializedInstruction = "";
if (
isValid &&
form.governedAccount?.governance?.account &&
wallet?.publicKey
) {
// This is where you'd call your custom program
// Replace with your own program/client logic
const ix = /* your custom instruction builder */ await Promise.resolve({
keys: [],
programId: new PublicKey("MyAwesomeProtocol111111111111111111111111"),
data: Buffer.from([]),
});
serializedInstruction = serializeInstructionToBase64(ix);
}
const obj: UiInstruction = {
serializedInstruction: serializedInstruction,
isValid,
governance: form.governedAccount?.governance,
customHoldUpTime: form.holdupTime,
};
return obj;
};
useEffect(() => {
// The part that integrates with the new proposal creation
handleSetInstructions(
{ governedAccount: form.governedAccount?.governance, getInstruction },
index,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form]);
// Prepare fields for the form
const inputs: InstructionInput[] = [
{
label: "Governance",
initialValue: form.governedAccount,
name: "governedAccount",
type: InstructionInputType.GOVERNED_ACCOUNT,
shouldBeGoverned: shouldBeGoverned as any,
governance: governance,
options: filteredAccounts,
},
{
label: "Instruction hold up time (days)",
initialValue: form.holdupTime,
type: InstructionInputType.INPUT,
inputType: "number",
name: "holdupTime",
},
{
label: "Target Address",
initialValue: form.targetAddress,
type: InstructionInputType.INPUT,
name: "targetAddress",
},
{
label: "Some Value",
initialValue: form.someValue,
type: InstructionInputType.INPUT,
inputType: "number",
name: "someValue",
},
];
return (
<InstructionForm
outerForm={form}
setForm={setForm}
inputs={inputs}
setFormErrors={setFormErrors}
formErrors={formErrors}
/>
);
};
export default DoSomething;
Make sure you update the import
paths to match your own folder structure if they differ.
6. If you have multiple instructions in one proposal
To add more instructions into a single proposal, you can return multiple instructions or “prerequisite instructions” from getInstruction
. For example:
const obj: UiInstruction = {
prerequisiteInstructions: prerequisiteInstructions,
prerequisiteInstructionsSigners: prerequsieInstructionsSigners,
serializedInstruction: serializedInstruction,
additionalSerializedInstructions: additionalSerializedInstructions,
isValid,
governance: form.governedAccount?.governance,
chunkBy: 1,
};
chunkBy
determines how many instructions go into a single transaction to avoid exceeding the transaction size limit.
7. Show decoded values in the proposal view
To display decoded values in the proposal view (e.g., “Show Instruction Data”), add a decoder for your program ID under components/instructions/programs
. Example approach:
import { BN } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import BufferLayout from "buffer-layout";
const MY_AWESOME_PROTOCOL_INSTRUCTIONS = {
1: {
name: "MyProtocol: Do Something Great",
accounts: [
{ name: "Account 1" },
{ name: "Account 2" },
// etc.
],
getDataUI: async (connection: Connection, data: Uint8Array) => {
// decode instruction data
const layout = BufferLayout.struct([BufferLayout.u32("someValue")]);
const decoded = layout.decode(Buffer.from(data), 1);
return (
<>
<div>someValue: {decoded.someValue}</div>
</>
);
},
},
};
export const MY_AWESOME_PROTOCOL_DESCRIPTORS = {
[new PublicKey("MyAwesomeProtocol111111111111111111111111").toBase58()]:
MY_AWESOME_PROTOCOL_INSTRUCTIONS,
};
Then in components/instructions/tools.tsx
, spread your exported descriptors:
import { MY_AWESOME_PROTOCOL_DESCRIPTORS } from "./programs/MyAwesomeProtocol";
export const INSTRUCTION_DESCRIPTORS = {
...SPL_TOKEN_INSTRUCTIONS,
...BPF_UPGRADEABLE_LOADER_INSTRUCTIONS,
...RAYDIUM_INSTRUCTIONS,
...MARINADE_INSTRUCTIONS,
...LIDO_INSTRUCTIONS,
...SWITCHBOARD_INSTRUCTIONS,
...SOLEND_PROGRAM_INSTRUCTIONS,
...ATA_PROGRAM_INSTRUCTIONS,
...SYSTEM_INSTRUCTIONS,
...VOTE_STAKE_REGISTRY_INSTRUCTIONS,
...NFT_VOTER_INSTRUCTIONS,
...NAME_SERVICE_INSTRUCTIONS,
...TOKEN_AUCTION_INSTRUCTIONS,
...VALIDATORDAO_INSTRUCTIONS,
...POSEIDON_INSTRUCTIONS,
...MANGO_V4_INSTRUCTIONS,
...DUAL_INSTRUCTIONS,
...STAKE_INSTRUCTIONS,
...STAKE_SANCTUM_INSTRUCTIONS,
...JUPITER_REF,
...SYMMETRY_V2_INSTRUCTIONS,
// Add your protocol here
...MY_AWESOME_PROTOCOL_DESCRIPTORS,
};
With this in place, the UI can decode and display your custom instruction data on the proposal page.
Conclusion
By following these steps, you integrate your new protocol and instructions into the Governance UI. Each instruction can include custom form inputs, validations, and UiInstruction
creation. The UI will display your new package, instructions, and (optionally) decoded data on the proposal page. Feel free to customize this setup as needed for your specific program logic!