WIP commit on re-engineering the front end to take into account governance. Not nearly done, and none of it works, but dont want to lose work, so making intermediate commit just in case.

This commit is contained in:
Dummy Tester 123 2021-03-15 17:33:25 -05:00
parent 819951868f
commit 90d8d63a04
24 changed files with 1410 additions and 553 deletions

View File

@ -629,7 +629,7 @@ export const deserializeAccount = (data: Buffer) => {
};
// TODO: expose in spl package
const deserializeMint = (data: Buffer) => {
export const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) {
throw new Error('Not a valid Mint');
}

View File

@ -236,6 +236,79 @@ export const getErrorForTransaction = async (
return errors;
};
export const sendTransactions = async (
connection: Connection,
wallet: any,
instructionSet: TransactionInstruction[][],
signersSet: Account[][],
awaitConfirmation = true,
commitment = 'singleGossip',
successCallback: (txid: string, ind: number) => void = (txid, ind) => {},
failCallback: (txid: string, ind: number) => boolean = (txid, ind) => false,
) => {
const unsignedTxns: Transaction[] = [];
for (let i = 0; i < instructionSet.length; i++) {
const instructions = instructionSet[i];
const signers = signersSet[i];
let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction));
transaction.recentBlockhash = (
await connection.getRecentBlockhash('max')
).blockhash;
transaction.setSigners(
// fee payied by the wallet owner
wallet.publicKey,
...signers.map(s => s.publicKey),
);
if (signers.length > 0) {
transaction.partialSign(...signers);
}
unsignedTxns.push(transaction);
}
const signedTxns = await wallet.signTransactions(unsignedTxns);
const rawTransactions = signedTxns.map((t: Transaction) => t.serialize());
let options = {
skipPreflight: true,
commitment,
};
for (let i = 0; i < rawTransactions.length; i++) {
const rawTransaction = rawTransactions[i];
const txid = await connection.sendRawTransaction(rawTransaction, options);
if (awaitConfirmation) {
const status = (
await connection.confirmTransaction(
txid,
options && (options.commitment as any),
)
).value;
if (status?.err && !failCallback(txid, i)) {
const errors = await getErrorForTransaction(connection, txid);
notify({
message: 'Transaction failed...',
description: (
<>
{errors.map(err => (
<div>{err}</div>
))}
<ExplorerLink address={txid} type="transaction" />
</>
),
type: 'error',
});
throw new Error(
`Raw transaction ${txid} failed (${JSON.stringify(status)})`,
);
} else {
successCallback(txid, i);
}
}
}
};
export const sendTransaction = async (
connection: Connection,
wallet: any,

View File

@ -5,18 +5,13 @@ import {
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { contexts, utils, actions } from '@oyster/common';
import { contexts, utils, actions, ParsedAccount } from '@oyster/common';
import { AccountLayout, MintLayout } from '@solana/spl-token';
import { initTimelockSetInstruction } from '../models/initTimelockSet';
import {
ConsensusAlgorithm,
ExecutionType,
TimelockSetLayout,
TimelockType,
} from '../models/timelock';
import { TimelockConfig, TimelockSetLayout } from '../models/timelock';
const { sendTransaction } = contexts.Connection;
const { sendTransactions } = contexts.Connection;
const { createMint, createTokenAccount } = actions;
const { notify } = utils;
@ -25,9 +20,7 @@ export const createProposal = async (
wallet: any,
name: string,
description: string,
timelockType: TimelockType,
consensusAlgorithm: ConsensusAlgorithm,
executionType: ExecutionType,
timelockConfig: ParsedAccount<TimelockConfig>,
): Promise<Account> => {
const PROGRAM_IDS = utils.programIds();
@ -44,18 +37,25 @@ export const createProposal = async (
const {
sigMint,
voteMint,
yesVoteMint,
noVoteMint,
adminMint,
voteValidationAccount,
sigValidationAccount,
adminValidationAccount,
adminDestinationAccount,
sigDestinationAccount,
yesVoteDumpAccount,
noVoteDumpAccount,
governanceHoldingAccount,
authority,
} = await createValidationAccountsAndMints(
connection,
instructions: associatedInstructions,
signers: associatedSigners,
} = await getAssociatedAccountsAndInstructions(
wallet,
accountRentExempt,
mintRentExempt,
timelockConfig,
);
const timelockRentExempt = await connection.getMinimumBalanceForRentExemption(
@ -80,17 +80,19 @@ export const createProposal = async (
sigMint,
adminMint,
voteMint,
yesVoteMint,
noVoteMint,
sigValidationAccount,
adminValidationAccount,
voteValidationAccount,
adminDestinationAccount,
sigDestinationAccount,
yesVoteDumpAccount,
noVoteDumpAccount,
governanceHoldingAccount,
timelockConfig.info.governanceMint,
timelockConfig.pubkey,
authority,
{
timelockType,
consensusAlgorithm,
executionType,
},
description,
name,
),
@ -103,11 +105,11 @@ export const createProposal = async (
});
try {
let tx = await sendTransaction(
let tx = await sendTransactions(
connection,
wallet,
instructions,
signers,
[...associatedInstructions, instructions],
[...associatedSigners, signers],
true,
);
@ -127,165 +129,192 @@ export const createProposal = async (
interface ValidationReturn {
sigMint: PublicKey;
voteMint: PublicKey;
yesVoteMint: PublicKey;
noVoteMint: PublicKey;
adminMint: PublicKey;
voteValidationAccount: PublicKey;
sigValidationAccount: PublicKey;
adminValidationAccount: PublicKey;
adminDestinationAccount: PublicKey;
sigDestinationAccount: PublicKey;
yesVoteDumpAccount: PublicKey;
noVoteDumpAccount: PublicKey;
governanceHoldingAccount: PublicKey;
authority: PublicKey;
signers: [Account[], Account[], Account[]];
instructions: [
TransactionInstruction[],
TransactionInstruction[],
TransactionInstruction[],
];
}
async function createValidationAccountsAndMints(
connection: Connection,
async function getAssociatedAccountsAndInstructions(
wallet: any,
accountRentExempt: number,
mintRentExempt: number,
timelockConfig: ParsedAccount<TimelockConfig>,
): Promise<ValidationReturn> {
const PROGRAM_IDS = utils.programIds();
notify({
message: `Creating mints...`,
type: 'warn',
description: `Please wait...`,
});
const [authority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
let mintSigners: Account[] = [];
let mintInstructions: TransactionInstruction[] = [];
const adminMint = createMint(
instructions,
mintInstructions,
wallet.publicKey,
mintRentExempt,
0,
authority,
authority,
signers,
mintSigners,
);
const sigMint = createMint(
instructions,
mintInstructions,
wallet.publicKey,
mintRentExempt,
0,
authority,
authority,
signers,
mintSigners,
);
const voteMint = createMint(
instructions,
mintInstructions,
wallet.publicKey,
mintRentExempt,
0,
authority,
authority,
signers,
mintSigners,
);
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
const yesVoteMint = createMint(
mintInstructions,
wallet.publicKey,
mintRentExempt,
0,
authority,
authority,
mintSigners,
);
notify({
message: `Mints created.`,
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
const noVoteMint = createMint(
mintInstructions,
wallet.publicKey,
mintRentExempt,
0,
authority,
authority,
mintSigners,
);
notify({
message: `Creating validation accounts...`,
type: 'warn',
description: `Please wait...`,
});
signers = [];
instructions = [];
let validationSigners: Account[] = [];
let validationInstructions: TransactionInstruction[] = [];
const adminValidationAccount = createTokenAccount(
instructions,
validationInstructions,
wallet.publicKey,
accountRentExempt,
adminMint,
authority,
signers,
validationSigners,
);
const sigValidationAccount = createTokenAccount(
instructions,
validationInstructions,
wallet.publicKey,
accountRentExempt,
sigMint,
authority,
signers,
validationSigners,
);
const voteValidationAccount = createTokenAccount(
instructions,
validationInstructions,
wallet.publicKey,
accountRentExempt,
voteMint,
authority,
signers,
validationSigners,
);
let destinationSigners: Account[] = [];
let destinationInstructions: TransactionInstruction[] = [];
const adminDestinationAccount = createTokenAccount(
instructions,
destinationInstructions,
wallet.publicKey,
accountRentExempt,
adminMint,
wallet.publicKey,
signers,
destinationSigners,
);
const sigDestinationAccount = createTokenAccount(
instructions,
destinationInstructions,
wallet.publicKey,
accountRentExempt,
sigMint,
wallet.publicKey,
signers,
destinationSigners,
);
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
let holdingSigners: Account[] = [];
let holdingInstructions: TransactionInstruction[] = [];
notify({
message: `Admin and signatory accounts created.`,
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
const yesVoteDumpAccount = createTokenAccount(
holdingInstructions,
wallet.publicKey,
accountRentExempt,
yesVoteMint,
wallet.publicKey,
holdingSigners,
);
const noVoteDumpAccount = createTokenAccount(
holdingInstructions,
wallet.publicKey,
accountRentExempt,
noVoteMint,
wallet.publicKey,
holdingSigners,
);
const governanceHoldingAccount = createTokenAccount(
holdingInstructions,
wallet.publicKey,
accountRentExempt,
timelockConfig.info.governanceMint,
wallet.publicKey,
holdingSigners,
);
return {
sigMint,
voteMint,
adminMint,
yesVoteMint,
noVoteMint,
voteValidationAccount,
sigValidationAccount,
adminValidationAccount,
adminDestinationAccount,
sigDestinationAccount,
yesVoteDumpAccount,
noVoteDumpAccount,
governanceHoldingAccount,
authority,
signers: [mintSigners, validationSigners, destinationSigners],
instructions: [
mintInstructions,
validationInstructions,
destinationInstructions,
],
};
}

View File

@ -14,20 +14,21 @@ import {
import { TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { mintVotingTokensInstruction } from '../models/mintVotingTokens';
import { depositVotingTokensInstruction } from '../models/depositVotingTokens';
import { LABELS } from '../constants';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const mintVotingTokens = async (
export const depositVotingTokens = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
signatoryAccount: PublicKey,
newVotingAccountOwner: PublicKey,
existingVoteAccount: PublicKey | undefined,
existingYesVoteAccount: PublicKey | undefined,
existingNoVoteAccount: PublicKey | undefined,
sourceAccount: PublicKey,
votingTokenAmount: number,
) => {
const PROGRAM_IDS = utils.programIds();
@ -45,37 +46,31 @@ export const mintVotingTokens = async (
wallet.publicKey,
accountRentExempt,
proposal.info.votingMint,
newVotingAccountOwner,
wallet.publicKey,
signers,
);
}
notify({
message: LABELS.ADDING_NEW_VOTE_ACCOUNT,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
if (!existingYesVoteAccount) {
createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.yesVotingMint,
wallet.publicKey,
signers,
);
}
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: LABELS.NEW_VOTED_ACCOUNT_ADDED,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
signers = [];
instructions = [];
if (!existingNoVoteAccount) {
createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.noVotingMint,
wallet.publicKey,
signers,
);
}
const [mintAuthority] = await PublicKey.findProgramAddress(
@ -86,20 +81,21 @@ export const mintVotingTokens = async (
const transferAuthority = approve(
instructions,
[],
signatoryAccount,
sourceAccount,
wallet.publicKey,
1,
votingTokenAmount,
);
signers.push(transferAuthority);
instructions.push(
mintVotingTokensInstruction(
proposal.pubkey,
depositVotingTokensInstruction(
existingVoteAccount,
sourceAccount,
proposal.info.governanceHolding,
proposal.info.votingMint,
signatoryAccount,
proposal.info.signatoryValidation,
proposal.pubkey,
proposal.info.config,
transferAuthority.publicKey,
mintAuthority,
votingTokenAmount,

View File

@ -0,0 +1,103 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import {
contexts,
utils,
models,
ParsedAccount,
actions,
} from '@oyster/common';
import { TimelockConfig } from '../models/timelock';
import { AccountLayout, Token } from '@solana/spl-token';
import { LABELS } from '../constants';
const { createTokenAccount } = actions;
const { sendTransactions } = contexts.Connection;
const { notify } = utils;
export interface GovernanceEntryInterface {
owner: PublicKey;
governanceAccount: PublicKey | undefined;
tokenAmount: number;
}
export const mintGovernanceTokens = async (
connection: Connection,
wallet: any,
timelockConfig: ParsedAccount<TimelockConfig>,
entries: GovernanceEntryInterface[],
setSavePerc: (num: number) => void,
onFailedTxn: (index: number) => void,
) => {
const PROGRAM_IDS = utils.programIds();
let allSigners: Account[][] = [];
let allInstructions: TransactionInstruction[][] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
entries.forEach(e => {
const signers: Account[] = [];
const instructions: TransactionInstruction[] = [];
if (!e.governanceAccount)
e.governanceAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
timelockConfig.info.governanceMint,
e.owner,
signers,
);
instructions.push(
Token.createMintToInstruction(
PROGRAM_IDS.token,
timelockConfig.info.governanceMint,
e.governanceAccount,
wallet.publicKey,
[],
e.tokenAmount,
),
);
allSigners.push(signers);
allInstructions.push(instructions);
});
notify({
message: LABELS.ADDING_GOVERNANCE_TOKENS,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
try {
await sendTransactions(
connection,
wallet,
allInstructions,
allSigners,
true,
'singleGossip',
(_txId: string, index: number) => {
setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length)));
},
(_txId: string, index: number) => {
setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length)));
onFailedTxn(index);
return true; // keep going even on failed save
},
);
notify({
message: LABELS.GOVERNANCE_TOKENS_ADDED,
type: 'success',
});
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -0,0 +1,139 @@
import {
Account,
Connection,
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { contexts, utils, actions, models } from '@oyster/common';
import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
import { TimelockConfig, TimelockConfigLayout } from '../models/timelock';
import { initTimelockConfigInstruction } from '../models/initTimelockConfig';
const { sendTransaction } = contexts.Connection;
const { createMint, createTokenAccount } = actions;
const { notify } = utils;
const { approve } = models;
export const registerProgramGovernance = async (
connection: Connection,
wallet: any,
uninitializedTimelockConfig: TimelockConfig,
): Promise<PublicKey> => {
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
MintLayout.span,
);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
if (!uninitializedTimelockConfig.governanceMint) {
// Initialize the mint, an account for the admin, and give them one governance token
// to start their lives with.
uninitializedTimelockConfig.governanceMint = createMint(
instructions,
wallet.publicKey,
mintRentExempt,
0,
wallet.publicKey,
wallet.publicKey,
signers,
);
const adminsGovernanceToken = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
uninitializedTimelockConfig.governanceMint,
wallet.publicKey,
signers,
);
const addAuthority = approve(
instructions,
[],
adminsGovernanceToken,
wallet.publicKey,
1,
);
instructions.push(
Token.createMintToInstruction(
PROGRAM_IDS.token,
uninitializedTimelockConfig.governanceMint,
adminsGovernanceToken,
addAuthority.publicKey,
[],
1,
),
);
signers.push(addAuthority);
}
const timelockRentExempt = await connection.getMinimumBalanceForRentExemption(
TimelockConfigLayout.span,
);
const [timelockConfigKey] = await PublicKey.findProgramAddress(
[
PROGRAM_IDS.timelock.programAccountId.toBuffer(),
uninitializedTimelockConfig.governanceMint.toBuffer(),
uninitializedTimelockConfig.program.toBuffer(),
],
PROGRAM_IDS.timelock.programId,
);
const uninitializedTimelockConfigInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: timelockConfigKey,
lamports: timelockRentExempt,
space: TimelockConfigLayout.span,
programId: PROGRAM_IDS.timelock.programId,
});
instructions.push(uninitializedTimelockConfigInstruction);
instructions.push(
initTimelockConfigInstruction(
timelockConfigKey,
uninitializedTimelockConfig.program,
uninitializedTimelockConfig.governanceMint,
uninitializedTimelockConfig.consensusAlgorithm,
uninitializedTimelockConfig.executionType,
uninitializedTimelockConfig.timelockType,
uninitializedTimelockConfig.votingEntryRule,
uninitializedTimelockConfig.minimumSlotWaitingPeriod,
),
);
notify({
message: 'Initializing governance of program...',
description: 'Please wait...',
type: 'warn',
});
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: 'Program is now governed.',
type: 'success',
description: `Transaction - ${tx}`,
});
return timelockConfigKey;
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -12,9 +12,7 @@ import {
actions,
} from '@oyster/common';
import { TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { mintVotingTokensInstruction } from '../models/mintVotingTokens';
import { TimelockConfig, TimelockSet } from '../models/timelock';
import { LABELS } from '../constants';
import { voteInstruction } from '../models/vote';
const { createTokenAccount } = actions;
@ -26,8 +24,12 @@ export const vote = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
timelockConfig: ParsedAccount<TimelockConfig>,
votingAccount: PublicKey,
votingTokenAmount: number,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
yesVotingTokenAmount: number,
noVotingTokenAmount: number,
) => {
const PROGRAM_IDS = utils.programIds();
@ -44,7 +46,7 @@ export const vote = async (
[],
votingAccount,
wallet.publicKey,
votingTokenAmount,
yesVotingTokenAmount + noVotingTokenAmount,
);
signers.push(transferAuthority);
@ -53,10 +55,17 @@ export const vote = async (
voteInstruction(
proposal.pubkey,
votingAccount,
yesVotingAccount,
noVotingAccount,
proposal.info.votingMint,
proposal.info.yesVotingMint,
proposal.info.noVotingMint,
timelockConfig.info.governanceMint,
timelockConfig.pubkey,
transferAuthority.publicKey,
mintAuthority,
votingTokenAmount,
yesVotingTokenAmount,
noVotingTokenAmount,
),
);

View File

@ -0,0 +1,156 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import {
contexts,
utils,
models,
ParsedAccount,
actions,
} from '@oyster/common';
import { TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { withdrawVotingTokensInstruction } from '../models/withdrawVotingTokens';
import { LABELS } from '../constants';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const withdrawVotingTokens = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
newVotingAccountOwner: PublicKey,
existingVoteAccount: PublicKey | undefined,
existingYesVoteAccount: PublicKey | undefined,
existingNoVoteAccount: PublicKey | undefined,
destinationAccount: PublicKey,
votingTokenAmount: number,
) => {
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
if (!existingVoteAccount) {
existingVoteAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.votingMint,
newVotingAccountOwner,
signers,
);
}
if (!existingYesVoteAccount) {
existingYesVoteAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.yesVotingMint,
newVotingAccountOwner,
signers,
);
}
if (!existingNoVoteAccount) {
existingNoVoteAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.noVotingMint,
newVotingAccountOwner,
signers,
);
}
const [mintAuthority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
// We dont know in this scope how much is in each account so we just ask for all in each.
// Should be alright, this is just permission, not actual moving.
const transferAuthority = approve(
instructions,
[],
existingVoteAccount,
wallet.publicKey,
votingTokenAmount,
);
const yesTransferAuthority = approve(
instructions,
[],
existingYesVoteAccount,
wallet.publicKey,
votingTokenAmount,
);
const noTransferAuthority = approve(
instructions,
[],
existingNoVoteAccount,
wallet.publicKey,
votingTokenAmount,
);
signers.push(transferAuthority);
signers.push(yesTransferAuthority);
signers.push(noTransferAuthority);
instructions.push(
withdrawVotingTokensInstruction(
existingVoteAccount,
existingYesVoteAccount,
existingNoVoteAccount,
destinationAccount,
proposal.info.governanceHolding,
proposal.info.yesVotingDump,
proposal.info.noVotingDump,
proposal.info.votingMint,
proposal.pubkey,
proposal.info.config,
transferAuthority.publicKey,
yesTransferAuthority.publicKey,
noTransferAuthority.publicKey,
mintAuthority,
votingTokenAmount,
),
);
notify({
message: LABELS.WITHDRAWING_VOTING_TOKENS,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: LABELS.TOKENS_WITHDRAWN,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -1,137 +1,165 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd';
import React, { useState } from 'react';
import { TimelockSet } from '../../models/timelock';
import React, { useEffect, useState } from 'react';
import { TimelockConfig, TimelockSet } from '../../models/timelock';
import { utils, contexts, hooks } from '@oyster/common';
import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
import { mintVotingTokens } from '../../actions/mintVotingTokens';
import {
GovernanceEntryInterface,
mintGovernanceTokens,
} from '../../actions/mintGovernanceTokens';
const { notify } = utils;
const { TextArea } = Input;
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { deserializeAccount } = contexts.Accounts;
const { deserializeAccount, useMint } = contexts.Accounts;
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
export default function AddVotes({
proposal,
export default function MintGovernanceTokens({
timelockConfig,
}: {
proposal: ParsedAccount<TimelockSet>;
timelockConfig: ParsedAccount<TimelockConfig>;
}) {
const PROGRAM_IDS = utils.programIds();
const wallet = useWallet();
const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const governanceMint = useMint(timelockConfig.info.governanceMint);
const [saving, setSaving] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [bulkModeVisible, setBulkModeVisible] = useState(false);
const [savePerc, setSavePerc] = useState(0);
const [failedVoters, setFailedVoters] = useState<any>([]);
const [failedGovernances, setFailedGovernances] = useState<any>([]);
const [form] = Form.useForm();
const onSubmit = async (values: {
voters: string;
failedVoters: string;
singleVoter: string;
singleVoteCount: number;
governanceHolders: string;
failedGovernances: string;
singleGovernanceHolder: string;
singleGovernanceCount: number;
}) => {
const { singleVoter, singleVoteCount } = values;
const votersAndCounts = values.voters
? values.voters.split(',').map(s => s.trim())
const { singleGovernanceHolder, singleGovernanceCount } = values;
const governanceHoldersAndCounts = values.governanceHolders
? values.governanceHolders.split(',').map(s => s.trim())
: [];
const voters: any[] = [];
votersAndCounts.forEach((value: string, index: number) => {
if (index % 2 == 0) voters.push([value, 0]);
else voters[voters.length - 1][1] = parseInt(value);
const governanceHolders: GovernanceEntryInterface[] = [];
let failedGovernancesHold: GovernanceEntryInterface[] = [];
const zeroKey = new PublicKey('0');
governanceHoldersAndCounts.forEach((value: string, index: number) => {
if (index % 2 == 0)
governanceHolders.push({
owner: value ? new PublicKey(value) : zeroKey,
tokenAmount: 0,
governanceAccount: undefined,
});
else
governanceHolders[governanceHolders.length - 1].tokenAmount = parseInt(
value,
);
});
console.log('Voters', votersAndCounts);
if (singleVoter) voters.push([singleVoter, singleVoteCount]);
if (!sigAccount) {
notify({
message: LABELS.SIG_ACCOUNT_NOT_DEFINED,
type: 'error',
if (singleGovernanceHolder)
governanceHolders.push({
owner: singleGovernanceHolder
? new PublicKey(singleGovernanceHolder)
: zeroKey,
tokenAmount: singleGovernanceCount,
governanceAccount: undefined,
});
return;
}
if (!voters.find(v => v[0])) {
if (!governanceHolders.find(v => v.owner != zeroKey)) {
notify({
message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY,
type: 'error',
});
return;
}
setSaving(true);
if (voters.find(v => v[1] === 0)) {
if (governanceHolders.find(v => v.tokenAmount === 0)) {
notify({
message: LABELS.CANT_GIVE_ZERO_VOTES,
message: LABELS.CANT_GIVE_ZERO_TOKENS,
type: 'error',
});
setSaving(false);
return;
}
const failedVotersHold: any[] = [];
setSaving(true);
for (let i = 0; i < voters.length; i++) {
const failedGovernanceCatch = (index: number, error: any) => {
if (error) console.error(error);
failedGovernancesHold.push(governanceHolders[index]);
notify({
message:
governanceHolders[index].owner?.toBase58() + LABELS.PUB_KEY_FAILED,
type: 'error',
});
};
const governanceHoldersToRun = [];
for (let i = 0; i < governanceHolders.length; i++) {
try {
const tokenAccounts = await connection.getTokenAccountsByOwner(
new PublicKey(voters[i][0]),
{
programId: PROGRAM_IDS.token,
},
);
const specificToThisMint = tokenAccounts.value.find(
a =>
deserializeAccount(a.account.data).mint.toBase58() ===
proposal.info.votingMint.toBase58(),
);
await mintVotingTokens(
connection,
wallet.wallet,
proposal,
sigAccount.pubkey,
new PublicKey(voters[i][0]),
specificToThisMint?.pubkey,
voters[i][1],
);
setSavePerc(Math.round(100 * ((i + 1) / voters.length)));
if (governanceHolders[i].owner) {
const tokenAccounts = await connection.getTokenAccountsByOwner(
governanceHolders[i].owner || new PublicKey('0'),
{
programId: PROGRAM_IDS.token,
},
);
const specificToThisMint = tokenAccounts.value.find(
a =>
deserializeAccount(a.account.data).mint.toBase58() ===
timelockConfig.info.governanceMint,
);
governanceHolders[i].governanceAccount = specificToThisMint?.pubkey;
governanceHoldersToRun.push(governanceHolders[i]);
}
} catch (e) {
console.error(e);
failedVotersHold.push(voters[i]);
notify({
message: voters[i][0] + LABELS.PUB_KEY_FAILED,
type: 'error',
});
failedGovernanceCatch(i, e);
}
}
setFailedVoters(failedVotersHold);
try {
await mintGovernanceTokens(
connection,
wallet.wallet,
timelockConfig,
governanceHoldersToRun,
setSavePerc,
index => failedGovernanceCatch(index, null),
);
} catch (e) {
console.error(e);
failedGovernancesHold = governanceHolders;
}
setFailedGovernances(failedGovernancesHold);
setSaving(false);
setSavePerc(0);
setIsModalVisible(failedVotersHold.length > 0);
if (failedVotersHold.length === 0) form.resetFields();
setIsModalVisible(failedGovernancesHold.length > 0);
if (failedGovernancesHold.length === 0) form.resetFields();
};
return (
<>
{sigAccount ? (
{governanceMint?.mintAuthority == wallet.wallet?.publicKey ? (
<Button
onClick={() => {
setIsModalVisible(true);
}}
>
{LABELS.ADD_VOTES}
{LABELS.ADD_GOVERNANCE_TOKENS}
</Button>
) : null}
<Modal
title={LABELS.ADD_VOTES}
title={LABELS.ADD_GOVERNANCE_TOKENS}
visible={isModalVisible}
destroyOnClose={true}
onOk={form.submit}
@ -141,7 +169,7 @@ export default function AddVotes({
}}
>
<Form
className={'voters-form'}
className={'governance-form'}
{...layout}
form={form}
onFinish={onSubmit}
@ -150,8 +178,8 @@ export default function AddVotes({
{!saving && (
<>
<Form.Item
label={LABELS.VOTE_MODE}
name="voteMode"
label={LABELS.TOKEN_MODE}
name="tokenMode"
initialValue={LABELS.SINGLE}
rules={[{ required: false }]}
>
@ -170,15 +198,15 @@ export default function AddVotes({
{!bulkModeVisible && (
<>
<Form.Item
name="singleVoter"
label={LABELS.SINGLE_VOTER}
name="singleGovernanceHolder"
label={LABELS.SINGLE_HOLDER}
rules={[{ required: false }]}
>
<Input placeholder={LABELS.SINGLE_KEY} />
</Form.Item>
<Form.Item
name="singleVoteCount"
label={LABELS.VOTE_COUNT}
name="singleGovernanceCount"
label={LABELS.AMOUNT}
initialValue={0}
rules={[{ required: false }]}
>
@ -188,12 +216,11 @@ export default function AddVotes({
)}
{bulkModeVisible && (
<Form.Item
name="voters"
label={LABELS.BULK_VOTERS}
name="governanceHolders"
label={LABELS.BULK_TOKENS}
rules={[{ required: false }]}
>
<TextArea
id="voters"
placeholder={LABELS.COMMA_SEPARATED_KEYS_AND_VOTES}
/>
</Form.Item>
@ -203,7 +230,7 @@ export default function AddVotes({
</Form>
{saving && <Progress percent={savePerc} status="active" />}
{!saving && failedVoters.length > 0 && (
{!saving && failedGovernances.length > 0 && (
<div
style={{
flex: 1,
@ -215,9 +242,9 @@ export default function AddVotes({
>
<Button
onClick={() => {
navigator.clipboard.writeText(failedVoters.join(','));
navigator.clipboard.writeText(failedGovernances.join(','));
notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_CLIPBOARD,
message: LABELS.FAILED_HOLDERS_COPIED_TO_CLIPBOARD,
type: 'success',
});
}}
@ -228,10 +255,10 @@ export default function AddVotes({
<Button
onClick={() => {
form.setFieldsValue({
voters: failedVoters.join(','),
governances: failedGovernances.join(','),
});
notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_INPUT,
message: LABELS.FAILED_HOLDERS_COPIED_TO_CLIPBOARD,
type: 'success',
});
}}

View File

@ -6,8 +6,6 @@ import { contexts, ParsedAccount, hooks, utils } from '@oyster/common';
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
import { SaveOutlined } from '@ant-design/icons';
import { Connection, PublicKey } from '@solana/web3.js';
import { initializeBuffer } from '../../actions/initializeBuffer';
import { loadBufferAccount } from '../../actions/loadBufferAccount';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
@ -35,10 +33,6 @@ export function NewInstructionCard({
const wallet = useWallet();
const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const [tabKey, setTabKey] = useState<UploadType>(UploadType.Base64);
const [inputRef, setInputRef] = useState<Input | null>(null);
const [savePerc, setSavePerc] = useState(0);
const [saving, setSaving] = useState(false);
const onFinish = async (values: {
slot: string;
@ -55,21 +49,6 @@ export function NewInstructionCard({
let instruction = values.instruction;
if (inputRef?.input?.files) {
// Crap, we need to fully upload first...
await handleUploadBpf({
inputRef,
connection,
wallet,
proposal,
sigAccountKey: sigAccount?.pubkey,
setSavePerc,
setSaving,
});
// return for now...
return;
}
if (sigAccount) {
await addCustomSingleSignerTransaction(
connection,
@ -84,115 +63,26 @@ export function NewInstructionCard({
}
};
const content = {
[UploadType.Base64]: (
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item
name="instruction"
label="Instruction"
rules={[{ required: true }]}
>
<Input
maxLength={INSTRUCTION_LIMIT}
placeholder={
"Base64 encoded Solana Message object with single instruction (call message.serialize().toString('base64')) no more than 255 characters"
}
/>
</Form.Item>
</Form>
),
[UploadType.Upgrade]: (
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item
name="destination"
label="Program Address"
rules={[{ required: true }]}
>
<Input
maxLength={INSTRUCTION_LIMIT}
placeholder={'Program Address to Update (Base 58)'}
/>
</Form.Item>
<Form.Item
name="instruction"
label="Instruction"
rules={[{ required: true }]}
>
<Input type="file" ref={ref => setInputRef(ref)} />
</Form.Item>
</Form>
),
};
return !sigAccount ? null : (
<Card
title="New Instruction"
tabList={[
{ key: UploadType.Base64, tab: 'Custom Instruction' },
/*{ key: UploadType.Upgrade, tab: 'Program Upgrade' },*/
]}
activeTabKey={tabKey}
onTabChange={key =>
setTabKey(key === 'Base64' ? UploadType.Base64 : UploadType.Upgrade)
}
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
>
{saving && <Progress percent={savePerc} status="active" />}
{content[tabKey]}
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item
name="instruction"
label="Instruction"
rules={[{ required: true }]}
>
<Input.TextArea
maxLength={INSTRUCTION_LIMIT}
placeholder={`Base64 encoded Solana Message object with single instruction (call message.serialize().toString('base64')) no more than ${INSTRUCTION_LIMIT} characters`}
/>
</Form.Item>
</Form>
</Card>
);
}
async function handleUploadBpf({
inputRef,
connection,
wallet,
proposal,
sigAccountKey,
setSavePerc,
setSaving,
}: {
inputRef: Input;
connection: Connection;
wallet: any;
proposal: ParsedAccount<TimelockSet>;
sigAccountKey: PublicKey | undefined;
setSavePerc: React.Dispatch<React.SetStateAction<number>>;
setSaving: React.Dispatch<React.SetStateAction<boolean>>;
}) {
if (sigAccountKey)
return new Promise(res => {
const reader = new FileReader();
reader.onload = async function () {
const bytes = new Uint8Array(reader.result as ArrayBuffer);
const len = bytes.byteLength;
setSaving(true);
try {
const tempFile = await initializeBuffer(
connection,
wallet.wallet,
len,
);
await loadBufferAccount(
connection,
wallet.wallet,
tempFile,
bytes,
setSavePerc,
);
} catch (e) {
console.error(e);
}
setSaving(false);
setSavePerc(0);
res(true);
};
reader.readAsArrayBuffer((inputRef?.input?.files || [])[0]);
});
}

View File

@ -0,0 +1,104 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider } from 'antd';
import React, { useState } from 'react';
import {
TimelockConfig,
TimelockSet,
TimelockStateStatus,
VotingEntryRule,
} from '../../models/timelock';
import { LABELS } from '../../constants';
import { depositVotingTokens } from '../../actions/depositVotingTokens';
import { contexts, hooks } from '@oyster/common';
import { ExclamationCircleOutlined } from '@ant-design/icons';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { confirm } = Modal;
export function RegisterToVote({
proposal,
timelockConfig,
}: {
proposal: ParsedAccount<TimelockSet>;
timelockConfig: ParsedAccount<TimelockConfig>;
}) {
const wallet = useWallet();
const connection = useConnection();
const voteAccount = useAccountByMint(proposal.info.votingMint);
const yesVoteAccount = useAccountByMint(proposal.info.yesVotingMint);
const noVoteAccount = useAccountByMint(proposal.info.noVotingMint);
const governanceAccount = useAccountByMint(
timelockConfig.info.governanceMint,
);
const alreadyHaveTokens =
voteAccount?.info?.amount?.toNumber() > 0 ||
yesVoteAccount?.info?.amount?.toNumber() > 0 ||
noVoteAccount?.info?.amount?.toNumber() > 0;
const eligibleToView =
(timelockConfig.info.votingEntryRule == VotingEntryRule.DraftOnly &&
proposal.info.state.status == TimelockStateStatus.Draft) ||
timelockConfig.info.votingEntryRule == VotingEntryRule.Anytime;
const [_, setTokenAmount] = useState(1);
return eligibleToView ? (
<Button
type="primary"
disabled={!voteAccount}
onClick={() =>
confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: (
<Row>
<Col span={24}>
<p>
You can convert up to{' '}
{governanceAccount?.info.amount.toNumber() || 0} tokens to
voting tokens to vote on this proposal. You can refund these
at any time.
</p>
{governanceAccount?.info.amount.toNumber() && (
<Slider
min={1}
max={governanceAccount?.info.amount.toNumber() || 0}
onChange={setTokenAmount}
/>
)}
</Col>
</Row>
),
okText: LABELS.CONFIRM,
cancelText: LABELS.CANCEL,
onOk: async () => {
if (governanceAccount) {
// tokenAmount is out of date in this scope, so we use a trick to get it here.
const valueHolder = { value: 0 };
await setTokenAmount(amount => {
valueHolder.value = amount;
return amount;
});
await depositVotingTokens(
connection,
wallet.wallet,
proposal,
voteAccount?.pubkey,
yesVoteAccount?.pubkey,
noVoteAccount?.pubkey,
governanceAccount.pubkey,
valueHolder.value,
);
// reset
setTokenAmount(1);
}
},
})
}
>
{LABELS.REGISTER_TO_VOTE}
</Button>
) : null;
}

View File

@ -1,23 +1,44 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider } from 'antd';
import { Button, Col, Modal, Row, Slider, Switch } from 'antd';
import React, { useState } from 'react';
import { TimelockSet } from '../../models/timelock';
import {
TimelockConfig,
TimelockSet,
TimelockStateStatus,
} from '../../models/timelock';
import { LABELS } from '../../constants';
import { vote } from '../../actions/vote';
import { utils, contexts, hooks } from '@oyster/common';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { contexts, hooks } from '@oyster/common';
import {
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { confirm } = Modal;
export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
export function Vote({
proposal,
timelockConfig,
}: {
proposal: ParsedAccount<TimelockSet>;
timelockConfig: ParsedAccount<TimelockConfig>;
}) {
const wallet = useWallet();
const connection = useConnection();
const voteAccount = useAccountByMint(proposal.info.votingMint);
const [tokenAmount, setTokenAmount] = useState(1);
return (
const yesVoteAccount = useAccountByMint(proposal.info.yesVotingMint);
const noVoteAccount = useAccountByMint(proposal.info.noVotingMint);
const [mode, setMode] = useState(true);
const [_, setTokenAmount] = useState(1);
const eligibleToView =
voteAccount &&
voteAccount.info.amount.toNumber() > 0 &&
proposal.info.state.status === TimelockStateStatus.Voting;
return eligibleToView ? (
<Button
type="primary"
disabled={!voteAccount}
@ -30,34 +51,46 @@ export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
<Col span={24}>
<p>
Burning your {voteAccount?.info.amount.toNumber()} tokens is
an irreversible action and indicates support for this
proposal. Choose how many to burn in favor of this proposal.
an irreversible action. Choose how many to burn in favor OR
against this proposal. Use the switch to indicate preference.
</p>
<Slider
min={1}
max={voteAccount?.info.amount.toNumber()}
onChange={setTokenAmount}
/>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
defaultChecked
onChange={setMode}
/>
</Col>
</Row>
),
okText: 'Confirm',
cancelText: 'Cancel',
okText: LABELS.CONFIRM,
cancelText: LABELS.CANCEL,
onOk: async () => {
if (voteAccount) {
if (voteAccount && yesVoteAccount && noVoteAccount) {
// tokenAmount is out of date in this scope, so we use a trick to get it here.
const valueHolder = { value: 0 };
await setTokenAmount(amount => {
valueHolder.value = amount;
return amount;
});
const yesTokenAmount = mode ? valueHolder.value : 0;
const noTokenAmount = !mode ? valueHolder.value : 0;
await vote(
connection,
wallet.wallet,
proposal,
timelockConfig,
voteAccount.pubkey,
valueHolder.value,
yesVoteAccount.pubkey,
noVoteAccount.pubkey,
yesTokenAmount,
noTokenAmount,
);
// reset
setTokenAmount(1);
@ -68,5 +101,5 @@ export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
>
{LABELS.VOTE}
</Button>
);
) : null;
}

View File

@ -24,6 +24,10 @@ export const LABELS = {
COPY_FAILED_ADDRESSES_TO_CLIPBOARD: 'Copy failed addresses to clipboard',
FAILED_SIGNERS_COPIED_TO_INPUT: 'Failed signers copied to input!',
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!',
FAILED_HOLDERS_COPIED_TO_INPUT:
'Failed governance token holders copied to input!',
FAILED_HOLDERS_COPIED_TO_CLIPBOARD:
'Failed governance token holders copied to clipboard!',
COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
SIGNERS: 'Signers',
ADD_SIGNERS: 'Add Signers',
@ -35,26 +39,34 @@ export const LABELS = {
ADD: 'Add',
REMOVE: 'Remove',
ADDING_OR_REMOVING: 'Type',
ADDING_VOTES_TO_VOTER: 'Adding votes to voter',
ADDING_VOTES_TO_VOTER: 'Converting governance tokens to voting tokens',
VOTES_ADDED: 'Governance tokens converted.',
ADDING_GOVERNANCE_TOKENS: 'Adding governance tokens',
PLEASE_WAIT: 'Please wait...',
VOTES_ADDED: 'Votes added.',
GOVERNANCE_TOKENS_ADDED: 'Governance tokens added.',
NEW_VOTED_ACCOUNT_ADDED: 'New vote account added.',
ADDING_NEW_VOTE_ACCOUNT: 'Adding new vote account...',
TRANSACTION: 'Transaction - ',
CANT_GIVE_ZERO_VOTES: "Can't give zero votes to a user!",
BULK_VOTERS: 'Voters',
CANT_GIVE_ZERO_TOKENS: "Can't give zero tokens to a user!",
BULK_TOKENS: 'Token Holders',
COMMA_SEPARATED_KEYS_AND_VOTES:
'base58 pubkey, vote count, base58 pubkey, vote count, ...',
SINGLE_VOTER: 'Single Voter',
VOTE_COUNT: 'Vote Amount',
SINGLE_HOLDER: 'Token Holder',
AMOUNT: 'Amount',
SINGLE_KEY: 'base58 pubkey',
VOTE_MODE: 'Vote Mode',
TOKEN_MODE: 'Mode',
BULK: 'Bulk',
SINGLE: 'Single',
ADD_VOTES: 'Add Votes',
ADD_GOVERNANCE_TOKENS: 'Add Governance Tokens',
BURNING_VOTES: 'Burning your votes...',
VOTES_BURNED: 'Votes burned',
VOTE: 'Vote',
EXECUTING: 'Executing...',
EXECUTED: 'Executed.',
WITHDRAWING_VOTING_TOKENS: 'Refunding voting tokens as Governance Tokens',
TOKENS_WITHDRAWN: 'Voting tokens refunded as Governance Tokens',
REGISTER_TO_VOTE: 'Register to Vote',
CONFIRM: 'Confirm',
CANCEL: 'Cancel',
ADD_MORE_VOTES: 'Add More Votes',
};

View File

@ -9,9 +9,11 @@ import { useMemo } from 'react';
import { contexts, utils, ParsedAccount } from '@oyster/common';
import {
CustomSingleSignerTimelockTransaction,
CustomSingleSignerTimelockTransactionLayout,
CustomSingleSignerTimelockTransactionParser,
TimelockConfig,
TimelockConfigLayout,
TimelockConfigParser,
TimelockSet,
TimelockSetLayout,
TimelockSetParser,
@ -24,6 +26,7 @@ const { cache } = contexts.Accounts;
export interface ProposalsContextState {
proposals: Record<string, ParsedAccount<TimelockSet>>;
transactions: Record<string, ParsedAccount<TimelockTransaction>>;
configs: Record<string, ParsedAccount<TimelockConfig>>;
}
export const ProposalsContext = React.createContext<ProposalsContextState | null>(
@ -38,15 +41,17 @@ export default function ProposalsProvider({ children = null as any }) {
const [proposals, setProposals] = useState({});
const [transactions, setTransactions] = useState({});
const [configs, setConfigs] = useState({});
useSetupProposalsCache({
connection,
setProposals,
setTransactions,
setConfigs,
});
return (
<ProposalsContext.Provider value={{ proposals, transactions }}>
<ProposalsContext.Provider value={{ proposals, transactions, configs }}>
{children}
</ProposalsContext.Provider>
);
@ -56,10 +61,12 @@ function useSetupProposalsCache({
connection,
setProposals,
setTransactions,
setConfigs,
}: {
connection: Connection;
setProposals: React.Dispatch<React.SetStateAction<{}>>;
setTransactions: React.Dispatch<React.SetStateAction<{}>>;
setConfigs: React.Dispatch<React.SetStateAction<{}>>;
}) {
const PROGRAM_IDS = utils.programIds();
@ -76,30 +83,36 @@ function useSetupProposalsCache({
string,
ParsedAccount<TimelockTransaction>
> = {};
const newConfigs: Record<string, ParsedAccount<TimelockConfig>> = {};
all[0].forEach(a => {
if (a.account.data.length === TimelockSetLayout.span) {
cache.add(a.pubkey, a.account, TimelockSetParser);
const cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>;
newProposals[a.pubkey.toBase58()] = cached;
}
if (
a.account.data.length ===
CustomSingleSignerTimelockTransactionLayout.span
) {
cache.add(
a.pubkey,
a.account,
CustomSingleSignerTimelockTransactionParser,
);
const cached = cache.get(
a.pubkey,
) as ParsedAccount<TimelockTransaction>;
newTransactions[a.pubkey.toBase58()] = cached;
let cached;
switch (a.account.data.length) {
case TimelockSetLayout.span:
cache.add(a.pubkey, a.account, TimelockSetParser);
cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>;
newProposals[a.pubkey.toBase58()] = cached;
break;
case CustomSingleSignerTimelockTransactionLayout.span:
cache.add(
a.pubkey,
a.account,
CustomSingleSignerTimelockTransactionParser,
);
cached = cache.get(a.pubkey) as ParsedAccount<TimelockTransaction>;
newTransactions[a.pubkey.toBase58()] = cached;
break;
case TimelockConfigLayout.span:
cache.add(a.pubkey, a.account, TimelockConfigParser);
cached = cache.get(a.pubkey) as ParsedAccount<TimelockConfig>;
newConfigs[a.pubkey.toBase58()] = cached;
break;
}
});
setProposals(newProposals);
setTransactions(newTransactions);
setConfigs(newConfigs);
});
const subID = connection.onProgramAccountChange(
PROGRAM_IDS.timelock.programId,
@ -111,6 +124,7 @@ function useSetupProposalsCache({
CustomSingleSignerTimelockTransactionParser,
setTransactions,
],
[TimelockConfigLayout.span, TimelockConfigParser, setConfigs],
].forEach(arr => {
const [span, parser, setter] = arr;
if (info.accountInfo.data.length === span) {

View File

@ -1,4 +1,4 @@
import { Message, PublicKey, TransactionInstruction } from '@solana/web3.js';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as Layout from '../utils/layout';
@ -9,8 +9,6 @@ import {
TRANSACTION_SLOTS,
} from './timelock';
import BN from 'bn.js';
import { toUTF8Array } from '@oyster/common/dist/lib/utils';
import { pingInstruction } from './ping';
/// [Requires Signatory token]
/// Adds a Transaction to the Timelock Set. Max of 10 of any Transaction type. More than 10 will throw error.

View File

@ -0,0 +1,74 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as Layout from '../utils/layout';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// [Requires tokens of the Governance mint]
/// Deposits voting tokens to be used during the voting process in a timelock.
/// These tokens are removed from your account and can be returned by withdrawing
/// them from the timelock (but then you will miss the vote.)
///
/// 0. `[writable]` Initialized Voting account to hold your received voting tokens.
/// 1. `[writable]` Source governance token account to deposit tokens from.
/// 2. `[writable]` Governance holding account for timelock that will accept the tokens in escrow.
/// 3. `[writable]` Voting mint account.
/// 4. `[]` Timelock set account.
/// 5. `[]` Timelock config account.
/// 6. `[]` Transfer authority
/// 7. `[]` Timelock program mint authority
/// 8. `[]` Timelock program account pub key.
/// 9. `[]` Token program account.
export const depositVotingTokensInstruction = (
votingAccount: PublicKey,
sourceAccount: PublicKey,
governanceHoldingAccount: PublicKey,
votingMint: PublicKey,
timelockSetAccount: PublicKey,
timelockConfigAccount: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
votingTokenAmount: number,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('votingTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.DepositVotingTokens,
votingTokenAmount: new BN(votingTokenAmount),
},
data,
);
const keys = [
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: sourceAccount, isSigner: false, isWritable: true },
{ pubkey: governanceHoldingAccount, isSigner: false, isWritable: true },
{ pubkey: votingMint, isSigner: false, isWritable: true },
{ pubkey: timelockSetAccount, isSigner: false, isWritable: false },
{ pubkey: timelockConfigAccount, isSigner: false, isWritable: false },
{ pubkey: transferAuthority, isSigner: false, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -0,0 +1,72 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
import * as Layout from '../utils/layout';
/// 0. `[writable]` Uninitialized timelock config account. Needs to be set with pubkey set to PDA with seeds of the
/// program account key, governance mint key, timelock program account key.
/// 1. `[]` Program account to tie this config to.
/// 2. `[]` Governance mint to tie this config to
/// 3. `[]` Timelock program account pub key.
/// 4. `[]` Token program account.
/// 5. `[]` Rent sysvar
export const initTimelockConfigInstruction = (
timelockConfigAccount: PublicKey,
programAccount: PublicKey,
governanceMint: PublicKey,
consensusAlgorithm: number,
executionType: number,
timelockType: number,
votingEntryRule: number,
minimumSlotWaitingPeriod: BN,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
BufferLayout.u8('votingEntryRule'),
Layout.uint64('minimumSlotWaitingPeriod'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.InitTimelockSet,
consensusAlgorithm,
executionType,
timelockType,
votingEntryRule,
minimumSlotWaitingPeriod,
},
data,
);
const keys = [
{ pubkey: timelockConfigAccount, isSigner: true, isWritable: true },
{ pubkey: programAccount, isSigner: false, isWritable: false },
{ pubkey: governanceMint, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -5,42 +5,49 @@ import {
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import {
DESC_SIZE,
NAME_SIZE,
TimelockConfig,
TimelockInstruction,
} from './timelock';
import BN from 'bn.js';
import { DESC_SIZE, NAME_SIZE, TimelockInstruction } from './timelock';
/// Initializes a new empty Timelocked set of Instructions that will be executed at various slots in the future in draft mode.
/// Grants Admin token to caller.
///
/// 0. `[writable]` Uninitialized Timelock set account .
/// 1. `[writable]` Uninitialized Signatory Mint account
/// 2. `[writable]` Uninitialized Admin Mint account
/// 3. `[writable]` Uninitialized Voting Mint account
/// 4. `[writable]` Uninitialized Signatory Validation account
/// 5. `[writable]` Uninitialized Admin Validation account
/// 6. `[writable]` Uninitialized Voting Validation account
/// 7. `[writable]` Uninitialized Destination account for first admin token
/// 8. `[writable]` Uninitialized Destination account for first signatory token
/// 9. `[]` Timelock program mint authority
/// 10. `[]` Timelock Program
/// 11. '[]` Token program id
/// 12. `[]` Rent sysvar
/// 1. `[writable]` Initialized Signatory Mint account
/// 2. `[writable]` Initialized Admin Mint account
/// 3. `[writable]` Initialized Voting Mint account
/// 4. `[writable]` Initialized Yes Voting Mint account
/// 5. `[writable]` Initialized No Voting Mint account
/// 6. `[writable]` Initialized Signatory Validation account
/// 7. `[writable]` Initialized Admin Validation account
/// 8. `[writable]` Initialized Voting Validation account
/// 9. `[writable]` Initialized Destination account for first admin token
/// 10. `[writable]` Initialized Destination account for first signatory token
/// 12. `[writable]` Initialized Yes voting dump account
/// 13. `[writable]` Initialized No voting dump account
/// 14. `[writable]` Initialized Governance holding account
/// 15. `[]` Governance mint
/// 16. `[]` Timelock config account.
/// 17. `[]` Timelock minting authority
/// 18. `[]` Timelock Program
/// 19. '[]` Token program id
/// 20. `[]` Rent sysvar
export const initTimelockSetInstruction = (
timelockSetAccount: PublicKey,
signatoryMintAccount: PublicKey,
adminMintAccount: PublicKey,
votingMintAccount: PublicKey,
yesVotingMintAccount: PublicKey,
noVotingMintAccount: PublicKey,
signatoryValidationAccount: PublicKey,
adminValidationAccount: PublicKey,
votingValidationAccount: PublicKey,
destinationAdminAccount: PublicKey,
destinationSignatoryAccount: PublicKey,
yesVotingDumpAccount: PublicKey,
noVotingDumpAccount: PublicKey,
governanceHoldingAccount: PublicKey,
governanceMintAccount: PublicKey,
timelockConfigAccount: PublicKey,
authority: PublicKey,
timelockConfig: TimelockConfig,
descLink: string,
name: string,
): TransactionInstruction => {
@ -56,9 +63,6 @@ export const initTimelockSetInstruction = (
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
]);
@ -76,9 +80,6 @@ export const initTimelockSetInstruction = (
dataLayout.encode(
{
instruction: TimelockInstruction.InitTimelockSet,
consensusAlgorithm: new BN(timelockConfig.consensusAlgorithm),
executionType: new BN(timelockConfig.executionType),
timelockType: new BN(timelockConfig.timelockType),
descLink: descAsBytes,
name: nameAsBytes,
},
@ -90,11 +91,22 @@ export const initTimelockSetInstruction = (
{ pubkey: signatoryMintAccount, isSigner: false, isWritable: true },
{ pubkey: adminMintAccount, isSigner: false, isWritable: true },
{ pubkey: votingMintAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingMintAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingMintAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryValidationAccount, isSigner: false, isWritable: true },
{ pubkey: adminValidationAccount, isSigner: false, isWritable: true },
{ pubkey: votingValidationAccount, isSigner: false, isWritable: true },
{ pubkey: destinationAdminAccount, isSigner: false, isWritable: true },
{ pubkey: destinationSignatoryAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingDumpAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingDumpAccount, isSigner: false, isWritable: true },
{ pubkey: governanceHoldingAccount, isSigner: false, isWritable: true },
{
pubkey: governanceMintAccount,
isSigner: false,
isWritable: false,
},
{ pubkey: timelockConfigAccount, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,

View File

@ -1,69 +0,0 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as Layout from '../utils/layout';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// [Requires Signatory token]
/// Mints voting tokens for a destination account to be used during the voting process.
///
/// 0. `[writable]` Timelock set account.
/// 1. `[writable]` Initialized Voting account.
/// 2. `[writable]` Voting mint account.
/// 3. `[writable]` Signatory account
/// 4. `[writable]` Signatory validation account.
/// 5. `[]` Transfer authority
/// 6. `[]` Timelock program mint authority
/// 7. `[]` Timelock program account pub key.
/// 8. `[]` Token program account.
export const mintVotingTokensInstruction = (
timelockSetAccount: PublicKey,
votingAccount: PublicKey,
votingMint: PublicKey,
signatoryAccount: PublicKey,
signatoryValidationAccount: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
votingTokenAmount: number,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('votingTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.MintVotingTokens,
votingTokenAmount: new BN(votingTokenAmount),
},
data,
);
const keys = [
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: votingMint, isSigner: false, isWritable: true },
{ pubkey: signatoryAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryValidationAccount, isSigner: false, isWritable: true },
{ pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -6,8 +6,8 @@ import { utils } from '@oyster/common';
export const DESC_SIZE = 200;
export const NAME_SIZE = 32;
export const INSTRUCTION_LIMIT = 500;
export const TRANSACTION_SLOTS = 10;
export const INSTRUCTION_LIMIT = 450;
export const TRANSACTION_SLOTS = 5;
export const TEMP_FILE_TXN_SIZE = 1000;
export enum TimelockInstruction {
@ -17,16 +17,47 @@ export enum TimelockInstruction {
AddCustomSingleSignerTransaction = 4,
Sign = 8,
Vote = 9,
MintVotingTokens = 10,
Ping = 11,
Execute = 12,
UploadTempFile = 13,
DepositVotingTokens = 13,
WithdrawVotingTokens = 14,
}
export interface TimelockConfig {
///version
version: number;
/// Consensus Algorithm
consensusAlgorithm: ConsensusAlgorithm;
/// Execution type
executionType: ExecutionType;
/// Timelock Type
timelockType: TimelockType;
/// Voting entry rule
votingEntryRule: VotingEntryRule;
/// Minimum slot time-distance from creation of proposal for an instruction to be placed
minimumSlotWaitingPeriod: BN;
/// Governance mint (optional)
governanceMint: PublicKey;
/// Program ID that is tied to this config (optional)
program: PublicKey;
}
export const TimelockConfigLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
BufferLayout.u8('votingEntryRule'),
Layout.uint64('minimumSlotWaitingPeriod'),
Layout.publicKey('governanceMint'),
Layout.publicKey('program'),
],
);
export enum VotingEntryRule {
DraftOnly = 0,
Anytime = 1,
}
export enum ConsensusAlgorithm {
@ -92,14 +123,10 @@ export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.str
Layout.publicKey('adminValidation'),
Layout.publicKey('votingValidation'),
BufferLayout.u8('timelockStateStatus'),
Layout.uint64('totalVotingTokensMinted'),
Layout.uint64('totalSigningTokensMinted'),
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
...timelockTxns,
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
],
);
@ -118,6 +145,12 @@ export interface TimelockSet {
/// Mint that creates voting tokens of this instruction
votingMint: PublicKey;
/// Mint that creates evidence of voting YES via token creation
yesVotingMint: PublicKey;
/// Mint that creates evidence of voting NO via token creation
noVotingMint: PublicKey;
/// Used to validate signatory tokens in a round trip transfer
signatoryValidation: PublicKey;
@ -127,13 +160,46 @@ export interface TimelockSet {
/// Used to validate voting tokens in a round trip transfer
votingValidation: PublicKey;
/// Reserve state
state: TimelockState;
/// Governance holding account
governanceHolding: PublicKey;
/// Yes Voting dump account for exchanged vote tokens
yesVotingDump: PublicKey;
/// No Voting dump account for exchanged vote tokens
noVotingDump: PublicKey;
/// configuration values
config: TimelockConfig;
config: PublicKey;
/// Reserve state
state: TimelockState;
}
export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
BufferLayout.u8('executed'),
BufferLayout.u16('instructionEndIndex'),
],
);
export interface TimelockTransaction {
version: number;
slot: BN;
instruction: number[];
executed: number;
instructionEndIndex: number;
}
export interface CustomSingleSignerTimelockTransaction
extends TimelockTransaction {}
export const TimelockSetParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
@ -156,22 +222,22 @@ export const TimelockSetParser = (
signatoryMint: data.signatoryMint,
adminMint: data.adminMint,
votingMint: data.votingMint,
yesVotingMint: data.yesVotingMint,
noVotingMint: data.noVotingMint,
signatoryValidation: data.signatoryValidation,
adminValidation: data.adminValidation,
votingValidation: data.votingValidation,
governanceHolding: data.governanceHolding,
yesVotingDump: data.yesVotingDump,
noVotingDump: data.noVotingDump,
config: data.config,
state: {
status: data.timelockStateStatus,
totalVotingTokensMinted: data.totalVotingTokensMinted,
totalSigningTokensMinted: data.totalSigningTokensMinted,
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''),
timelockTransactions: timelockTxns,
},
config: {
consensusAlgorithm: data.consensusAlgorithm,
executionType: data.executionType,
timelockType: data.timelockType,
},
},
};
@ -203,26 +269,29 @@ export const CustomSingleSignerTimelockTransactionParser = (
return details;
};
export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
BufferLayout.u8('executed'),
BufferLayout.u16('instructionEndIndex'),
],
);
export const TimelockConfigParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = TimelockConfigLayout.decode(buffer);
export interface TimelockTransaction {
version: number;
const details = {
pubkey: pubKey,
account: {
...info,
},
info: {
version: data.version,
consensusAlgorithm: data.consensusAlgorithm,
executionType: data.executionType,
timelockType: data.timelockType,
votingEntryRule: data.votingEntryRule,
minimimSlotWaitingPeriod: data.minimimSlotWaitingPeriod,
governanceMint: data.governanceMint,
program: data.program,
},
};
slot: BN;
instruction: number[];
executed: number;
instructionEndIndex: number;
}
export interface CustomSingleSignerTimelockTransaction
extends TimelockTransaction {}
return details;
};

View File

@ -7,29 +7,43 @@ import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// [Requires Voting tokens]
/// Burns voting tokens, indicating you approve of running this set of transactions. If you tip the consensus,
/// then the transactions begin to be run at their time slots.
/// Burns voting tokens, indicating you approve and/or disapprove of running this set of transactions. If you tip the consensus,
/// then the transactions can begin to be run at their time slots when people click execute.
///
/// 0. `[writable]` Timelock set account.
/// 1. `[writable]` Voting account.
/// 2. `[writable]` Voting mint account.
/// 3. `[]` Transfer authority
/// 4. `[]` Timelock program mint authority
/// 5. `[]` Timelock program account pub key.
/// 6. `[]` Token program account.
/// 1. `[writable]` Your Voting account.
/// 2. `[writable]` Your Yes-Voting account.
/// 3. `[writable]` Your No-Voting account.
/// 4. `[writable]` Voting mint account.
/// 5. `[writable]` Yes Voting mint account.
/// 6. `[writable]` No Voting mint account.
/// 7. `[]` Governance mint account
/// 8. `[]` Timelock config account.
/// 9. `[]` Transfer authority
/// 10. `[]` Timelock program mint authority
/// 11. `[]` Timelock program account pub key.
/// 12. `[]` Token program account.
export const voteInstruction = (
timelockSetAccount: PublicKey,
votingAccount: PublicKey,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
votingMint: PublicKey,
yesVotingMint: PublicKey,
noVotingMint: PublicKey,
governanceMint: PublicKey,
timelockConfig: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
votingTokenAmount: number,
yesVotingTokenAmount: number,
noVotingTokenAmount: number,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('votingTokenAmount'),
Layout.uint64('yesVotingTokenAmount'),
Layout.uint64('noVotingTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
@ -37,7 +51,8 @@ export const voteInstruction = (
dataLayout.encode(
{
instruction: TimelockInstruction.Vote,
votingTokenAmount: new BN(votingTokenAmount),
yesVotingTokenAmount: new BN(yesVotingTokenAmount),
noVotingTokenAmount: new BN(noVotingTokenAmount),
},
data,
);
@ -45,7 +60,13 @@ export const voteInstruction = (
const keys = [
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingAccount, isSigner: false, isWritable: true },
{ pubkey: votingMint, isSigner: false, isWritable: true },
{ pubkey: yesVotingMint, isSigner: false, isWritable: true },
{ pubkey: noVotingMint, isSigner: false, isWritable: true },
{ pubkey: governanceMint, isSigner: false, isWritable: false },
{ pubkey: timelockConfig, isSigner: false, isWritable: false },
{ pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{

View File

@ -0,0 +1,87 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as Layout from '../utils/layout';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// 0. `[writable]` Initialized Voting account from which to remove your voting tokens.
/// 1. `[writable]` Initialized Yes Voting account from which to remove your voting tokens.
/// 2. `[writable]` Initialized No Voting account from which to remove your voting tokens.
/// 3. `[writable]` Governance token account that you wish your actual tokens to be returned to.
/// 4. `[writable]` Governance holding account owned by the timelock that will has the actual tokens in escrow.
/// 6. `[writable]` Initialized Yes Voting dump account owned by timelock set to which to send your voting tokens.
/// 7. `[writable]` Initialized No Voting dump account owned by timelock set to which to send your voting tokens.
/// 8. `[]` Voting mint account.
/// 9. `[]` Timelock set account.
/// 10. `[]` Timelock config account.
/// 11. `[]` Transfer authority
/// 12. `[]` Yes Transfer authority
/// 13. `[]` No Transfer authority
/// 14. `[]` Timelock program mint authority
/// 15. `[]` Timelock program account pub key.
/// 16. `[]` Token program account.
export const withdrawVotingTokensInstruction = (
votingAccount: PublicKey,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
destinationAccount: PublicKey,
governanceHoldingAccount: PublicKey,
yesVotingDump: PublicKey,
noVotingDump: PublicKey,
votingMint: PublicKey,
timelockSetAccount: PublicKey,
timelockConfigAccount: PublicKey,
transferAuthority: PublicKey,
yesTransferAuthority: PublicKey,
noTransferAuthority: PublicKey,
mintAuthority: PublicKey,
votingTokenAmount: number,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('votingTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.WithdrawVotingTokens,
votingTokenAmount: new BN(votingTokenAmount),
},
data,
);
const keys = [
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingAccount, isSigner: false, isWritable: true },
{ pubkey: destinationAccount, isSigner: false, isWritable: true },
{ pubkey: governanceHoldingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingDump, isSigner: false, isWritable: true },
{ pubkey: noVotingDump, isSigner: false, isWritable: true },
{ pubkey: votingMint, isSigner: false, isWritable: false },
{ pubkey: timelockSetAccount, isSigner: false, isWritable: false },
{ pubkey: timelockConfigAccount, isSigner: false, isWritable: false },
{ pubkey: transferAuthority, isSigner: false, isWritable: false },
{ pubkey: yesTransferAuthority, isSigner: false, isWritable: false },
{ pubkey: noTransferAuthority, isSigner: false, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -5,9 +5,11 @@ import { ParsedAccount } from '@oyster/common';
import {
ConsensusAlgorithm,
INSTRUCTION_LIMIT,
TimelockConfig,
TimelockSet,
TimelockStateStatus,
TimelockTransaction,
VotingEntryRule,
} from '../../models/timelock';
import { useParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
@ -19,8 +21,9 @@ import { InstructionCard } from '../../components/Proposal/InstructionCard';
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
import SignButton from '../../components/Proposal/SignButton';
import AddSigners from '../../components/Proposal/AddSigners';
import AddVotes from '../../components/Proposal/AddVotes';
import MintGovernanceTokens from '../../components/Proposal/MintGovernanceTokens';
import { Vote } from '../../components/Proposal/Vote';
import { RegisterToVote } from '../../components/Proposal/RegisterToVote';
export const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
const { useMint } = contexts.Accounts;
const { useAccountByMint } = hooks;
@ -30,13 +33,18 @@ export const ProposalView = () => {
const context = useProposals();
const { id } = useParams<{ id: string }>();
const proposal = context.proposals[id];
const timelockConfig = context.configs[proposal?.info.config.toBase58()];
const sigMint = useMint(proposal?.info.signatoryMint);
const votingMint = useMint(proposal?.info.votingMint);
const governanceMint = useMint(timelockConfig?.info.governanceMint);
return (
<div className="flexColumn">
{proposal && sigMint && votingMint ? (
{proposal && sigMint && votingMint && governanceMint ? (
<InnerProposalView
proposal={proposal}
timelockConfig={timelockConfig}
governanceMint={governanceMint}
votingMint={votingMint}
sigMint={sigMint}
instructions={context.transactions}
@ -53,10 +61,14 @@ function InnerProposalView({
sigMint,
votingMint,
instructions,
timelockConfig,
governanceMint,
}: {
proposal: ParsedAccount<TimelockSet>;
timelockConfig: ParsedAccount<TimelockConfig>;
sigMint: MintInfo;
votingMint: MintInfo;
governanceMint: MintInfo;
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
}) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
@ -196,23 +208,20 @@ function InnerProposalView({
: 'vertical'
}
>
{sigAccount &&
sigAccount.info.amount.toNumber() === 1 &&
proposal.info.state.status === TimelockStateStatus.Draft && (
<AddVotes proposal={proposal} />
)}
{voteAccount &&
voteAccount.info.amount.toNumber() > 0 &&
proposal.info.state.status === TimelockStateStatus.Voting && (
<Vote proposal={proposal} />
)}
<MintGovernanceTokens timelockConfig={timelockConfig} />
<RegisterToVote
timelockConfig={timelockConfig}
proposal={proposal}
/>
<Vote proposal={proposal} timelockConfig={timelockConfig} />
</Space>
</Col>
<Col span={8}>
<Statistic
valueStyle={{ color: 'green' }}
title={LABELS.VOTES_REQUIRED}
value={getVotesRequired(proposal)}
value={getVotesRequired(timelockConfig, governanceMint)}
/>
</Col>
</Row>
@ -246,21 +255,20 @@ function InnerProposalView({
);
}
function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
if (proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.Majority) {
return Math.ceil(
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5,
);
function getVotesRequired(
timelockConfig: ParsedAccount<TimelockConfig>,
governanceMint: MintInfo,
): number {
if (timelockConfig.info.consensusAlgorithm === ConsensusAlgorithm.Majority) {
return Math.ceil(governanceMint.supply.toNumber() * 0.5);
} else if (
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
timelockConfig.info.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
) {
return Math.ceil(
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.66,
);
return Math.ceil(governanceMint.supply.toNumber() * 0.66);
} else if (
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
timelockConfig.info.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
) {
return proposal.info.state.totalVotingTokensMinted.toNumber();
return governanceMint.supply.toNumber();
}
return 0;
}