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 // TODO: expose in spl package
const deserializeMint = (data: Buffer) => { export const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) { if (data.length !== MintLayout.span) {
throw new Error('Not a valid Mint'); throw new Error('Not a valid Mint');
} }

View File

@ -236,6 +236,79 @@ export const getErrorForTransaction = async (
return errors; 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 ( export const sendTransaction = async (
connection: Connection, connection: Connection,
wallet: any, wallet: any,

View File

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

View File

@ -14,20 +14,21 @@ import {
import { TimelockSet } from '../models/timelock'; import { TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token'; import { AccountLayout } from '@solana/spl-token';
import { mintVotingTokensInstruction } from '../models/mintVotingTokens'; import { depositVotingTokensInstruction } from '../models/depositVotingTokens';
import { LABELS } from '../constants'; import { LABELS } from '../constants';
const { createTokenAccount } = actions; const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection; const { sendTransaction } = contexts.Connection;
const { notify } = utils; const { notify } = utils;
const { approve } = models; const { approve } = models;
export const mintVotingTokens = async ( export const depositVotingTokens = async (
connection: Connection, connection: Connection,
wallet: any, wallet: any,
proposal: ParsedAccount<TimelockSet>, proposal: ParsedAccount<TimelockSet>,
signatoryAccount: PublicKey,
newVotingAccountOwner: PublicKey,
existingVoteAccount: PublicKey | undefined, existingVoteAccount: PublicKey | undefined,
existingYesVoteAccount: PublicKey | undefined,
existingNoVoteAccount: PublicKey | undefined,
sourceAccount: PublicKey,
votingTokenAmount: number, votingTokenAmount: number,
) => { ) => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
@ -45,37 +46,31 @@ export const mintVotingTokens = async (
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
proposal.info.votingMint, proposal.info.votingMint,
newVotingAccountOwner, wallet.publicKey,
signers, signers,
); );
notify({
message: LABELS.ADDING_NEW_VOTE_ACCOUNT,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
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 = []; if (!existingYesVoteAccount) {
instructions = []; createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.yesVotingMint,
wallet.publicKey,
signers,
);
}
if (!existingNoVoteAccount) {
createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.noVotingMint,
wallet.publicKey,
signers,
);
} }
const [mintAuthority] = await PublicKey.findProgramAddress( const [mintAuthority] = await PublicKey.findProgramAddress(
@ -86,20 +81,21 @@ export const mintVotingTokens = async (
const transferAuthority = approve( const transferAuthority = approve(
instructions, instructions,
[], [],
signatoryAccount, sourceAccount,
wallet.publicKey, wallet.publicKey,
1, votingTokenAmount,
); );
signers.push(transferAuthority); signers.push(transferAuthority);
instructions.push( instructions.push(
mintVotingTokensInstruction( depositVotingTokensInstruction(
proposal.pubkey,
existingVoteAccount, existingVoteAccount,
sourceAccount,
proposal.info.governanceHolding,
proposal.info.votingMint, proposal.info.votingMint,
signatoryAccount, proposal.pubkey,
proposal.info.signatoryValidation, proposal.info.config,
transferAuthority.publicKey, transferAuthority.publicKey,
mintAuthority, mintAuthority,
votingTokenAmount, 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, actions,
} from '@oyster/common'; } from '@oyster/common';
import { TimelockSet } from '../models/timelock'; import { TimelockConfig, TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { mintVotingTokensInstruction } from '../models/mintVotingTokens';
import { LABELS } from '../constants'; import { LABELS } from '../constants';
import { voteInstruction } from '../models/vote'; import { voteInstruction } from '../models/vote';
const { createTokenAccount } = actions; const { createTokenAccount } = actions;
@ -26,8 +24,12 @@ export const vote = async (
connection: Connection, connection: Connection,
wallet: any, wallet: any,
proposal: ParsedAccount<TimelockSet>, proposal: ParsedAccount<TimelockSet>,
timelockConfig: ParsedAccount<TimelockConfig>,
votingAccount: PublicKey, votingAccount: PublicKey,
votingTokenAmount: number, yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
yesVotingTokenAmount: number,
noVotingTokenAmount: number,
) => { ) => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
@ -44,7 +46,7 @@ export const vote = async (
[], [],
votingAccount, votingAccount,
wallet.publicKey, wallet.publicKey,
votingTokenAmount, yesVotingTokenAmount + noVotingTokenAmount,
); );
signers.push(transferAuthority); signers.push(transferAuthority);
@ -53,10 +55,17 @@ export const vote = async (
voteInstruction( voteInstruction(
proposal.pubkey, proposal.pubkey,
votingAccount, votingAccount,
yesVotingAccount,
noVotingAccount,
proposal.info.votingMint, proposal.info.votingMint,
proposal.info.yesVotingMint,
proposal.info.noVotingMint,
timelockConfig.info.governanceMint,
timelockConfig.pubkey,
transferAuthority.publicKey, transferAuthority.publicKey,
mintAuthority, 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,89 +1,114 @@
import { ParsedAccount } from '@oyster/common'; import { ParsedAccount } from '@oyster/common';
import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd'; import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { TimelockSet } from '../../models/timelock'; import { TimelockConfig, TimelockSet } from '../../models/timelock';
import { utils, contexts, hooks } from '@oyster/common'; import { utils, contexts, hooks } from '@oyster/common';
import { PublicKey } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants'; import { LABELS } from '../../constants';
import { mintVotingTokens } from '../../actions/mintVotingTokens'; import {
GovernanceEntryInterface,
mintGovernanceTokens,
} from '../../actions/mintGovernanceTokens';
const { notify } = utils; const { notify } = utils;
const { TextArea } = Input; const { TextArea } = Input;
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection; const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks; const { deserializeAccount, useMint } = contexts.Accounts;
const { deserializeAccount } = contexts.Accounts;
const layout = { const layout = {
labelCol: { span: 5 }, labelCol: { span: 5 },
wrapperCol: { span: 19 }, wrapperCol: { span: 19 },
}; };
export default function AddVotes({ export default function MintGovernanceTokens({
proposal, timelockConfig,
}: { }: {
proposal: ParsedAccount<TimelockSet>; timelockConfig: ParsedAccount<TimelockConfig>;
}) { }) {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
const wallet = useWallet(); const wallet = useWallet();
const connection = useConnection(); const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint); const governanceMint = useMint(timelockConfig.info.governanceMint);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [bulkModeVisible, setBulkModeVisible] = useState(false); const [bulkModeVisible, setBulkModeVisible] = useState(false);
const [savePerc, setSavePerc] = useState(0); const [savePerc, setSavePerc] = useState(0);
const [failedVoters, setFailedVoters] = useState<any>([]); const [failedGovernances, setFailedGovernances] = useState<any>([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const onSubmit = async (values: { const onSubmit = async (values: {
voters: string; governanceHolders: string;
failedVoters: string; failedGovernances: string;
singleVoter: string; singleGovernanceHolder: string;
singleVoteCount: number; singleGovernanceCount: number;
}) => { }) => {
const { singleVoter, singleVoteCount } = values; const { singleGovernanceHolder, singleGovernanceCount } = values;
const votersAndCounts = values.voters const governanceHoldersAndCounts = values.governanceHolders
? values.voters.split(',').map(s => s.trim()) ? values.governanceHolders.split(',').map(s => s.trim())
: []; : [];
const voters: any[] = []; const governanceHolders: GovernanceEntryInterface[] = [];
votersAndCounts.forEach((value: string, index: number) => { let failedGovernancesHold: GovernanceEntryInterface[] = [];
if (index % 2 == 0) voters.push([value, 0]); const zeroKey = new PublicKey('0');
else voters[voters.length - 1][1] = parseInt(value); 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) { if (singleGovernanceHolder)
notify({ governanceHolders.push({
message: LABELS.SIG_ACCOUNT_NOT_DEFINED, owner: singleGovernanceHolder
type: 'error', ? new PublicKey(singleGovernanceHolder)
: zeroKey,
tokenAmount: singleGovernanceCount,
governanceAccount: undefined,
}); });
return;
} if (!governanceHolders.find(v => v.owner != zeroKey)) {
if (!voters.find(v => v[0])) {
notify({ notify({
message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY,
type: 'error', type: 'error',
}); });
return; return;
} }
setSaving(true);
if (voters.find(v => v[1] === 0)) { if (governanceHolders.find(v => v.tokenAmount === 0)) {
notify({ notify({
message: LABELS.CANT_GIVE_ZERO_VOTES, message: LABELS.CANT_GIVE_ZERO_TOKENS,
type: 'error', type: 'error',
}); });
setSaving(false); setSaving(false);
return; 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 { try {
if (governanceHolders[i].owner) {
const tokenAccounts = await connection.getTokenAccountsByOwner( const tokenAccounts = await connection.getTokenAccountsByOwner(
new PublicKey(voters[i][0]), governanceHolders[i].owner || new PublicKey('0'),
{ {
programId: PROGRAM_IDS.token, programId: PROGRAM_IDS.token,
}, },
@ -91,47 +116,50 @@ export default function AddVotes({
const specificToThisMint = tokenAccounts.value.find( const specificToThisMint = tokenAccounts.value.find(
a => a =>
deserializeAccount(a.account.data).mint.toBase58() === deserializeAccount(a.account.data).mint.toBase58() ===
proposal.info.votingMint.toBase58(), timelockConfig.info.governanceMint,
); );
await mintVotingTokens( governanceHolders[i].governanceAccount = specificToThisMint?.pubkey;
governanceHoldersToRun.push(governanceHolders[i]);
}
} catch (e) {
failedGovernanceCatch(i, e);
}
}
try {
await mintGovernanceTokens(
connection, connection,
wallet.wallet, wallet.wallet,
proposal, timelockConfig,
sigAccount.pubkey, governanceHoldersToRun,
new PublicKey(voters[i][0]), setSavePerc,
specificToThisMint?.pubkey, index => failedGovernanceCatch(index, null),
voters[i][1],
); );
setSavePerc(Math.round(100 * ((i + 1) / voters.length)));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
failedVotersHold.push(voters[i]); failedGovernancesHold = governanceHolders;
notify({
message: voters[i][0] + LABELS.PUB_KEY_FAILED,
type: 'error',
});
} }
}
setFailedVoters(failedVotersHold); setFailedGovernances(failedGovernancesHold);
setSaving(false); setSaving(false);
setSavePerc(0); setSavePerc(0);
setIsModalVisible(failedVotersHold.length > 0); setIsModalVisible(failedGovernancesHold.length > 0);
if (failedVotersHold.length === 0) form.resetFields(); if (failedGovernancesHold.length === 0) form.resetFields();
}; };
return ( return (
<> <>
{sigAccount ? ( {governanceMint?.mintAuthority == wallet.wallet?.publicKey ? (
<Button <Button
onClick={() => { onClick={() => {
setIsModalVisible(true); setIsModalVisible(true);
}} }}
> >
{LABELS.ADD_VOTES} {LABELS.ADD_GOVERNANCE_TOKENS}
</Button> </Button>
) : null} ) : null}
<Modal <Modal
title={LABELS.ADD_VOTES} title={LABELS.ADD_GOVERNANCE_TOKENS}
visible={isModalVisible} visible={isModalVisible}
destroyOnClose={true} destroyOnClose={true}
onOk={form.submit} onOk={form.submit}
@ -141,7 +169,7 @@ export default function AddVotes({
}} }}
> >
<Form <Form
className={'voters-form'} className={'governance-form'}
{...layout} {...layout}
form={form} form={form}
onFinish={onSubmit} onFinish={onSubmit}
@ -150,8 +178,8 @@ export default function AddVotes({
{!saving && ( {!saving && (
<> <>
<Form.Item <Form.Item
label={LABELS.VOTE_MODE} label={LABELS.TOKEN_MODE}
name="voteMode" name="tokenMode"
initialValue={LABELS.SINGLE} initialValue={LABELS.SINGLE}
rules={[{ required: false }]} rules={[{ required: false }]}
> >
@ -170,15 +198,15 @@ export default function AddVotes({
{!bulkModeVisible && ( {!bulkModeVisible && (
<> <>
<Form.Item <Form.Item
name="singleVoter" name="singleGovernanceHolder"
label={LABELS.SINGLE_VOTER} label={LABELS.SINGLE_HOLDER}
rules={[{ required: false }]} rules={[{ required: false }]}
> >
<Input placeholder={LABELS.SINGLE_KEY} /> <Input placeholder={LABELS.SINGLE_KEY} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="singleVoteCount" name="singleGovernanceCount"
label={LABELS.VOTE_COUNT} label={LABELS.AMOUNT}
initialValue={0} initialValue={0}
rules={[{ required: false }]} rules={[{ required: false }]}
> >
@ -188,12 +216,11 @@ export default function AddVotes({
)} )}
{bulkModeVisible && ( {bulkModeVisible && (
<Form.Item <Form.Item
name="voters" name="governanceHolders"
label={LABELS.BULK_VOTERS} label={LABELS.BULK_TOKENS}
rules={[{ required: false }]} rules={[{ required: false }]}
> >
<TextArea <TextArea
id="voters"
placeholder={LABELS.COMMA_SEPARATED_KEYS_AND_VOTES} placeholder={LABELS.COMMA_SEPARATED_KEYS_AND_VOTES}
/> />
</Form.Item> </Form.Item>
@ -203,7 +230,7 @@ export default function AddVotes({
</Form> </Form>
{saving && <Progress percent={savePerc} status="active" />} {saving && <Progress percent={savePerc} status="active" />}
{!saving && failedVoters.length > 0 && ( {!saving && failedGovernances.length > 0 && (
<div <div
style={{ style={{
flex: 1, flex: 1,
@ -215,9 +242,9 @@ export default function AddVotes({
> >
<Button <Button
onClick={() => { onClick={() => {
navigator.clipboard.writeText(failedVoters.join(',')); navigator.clipboard.writeText(failedGovernances.join(','));
notify({ notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_CLIPBOARD, message: LABELS.FAILED_HOLDERS_COPIED_TO_CLIPBOARD,
type: 'success', type: 'success',
}); });
}} }}
@ -228,10 +255,10 @@ export default function AddVotes({
<Button <Button
onClick={() => { onClick={() => {
form.setFieldsValue({ form.setFieldsValue({
voters: failedVoters.join(','), governances: failedGovernances.join(','),
}); });
notify({ notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_INPUT, message: LABELS.FAILED_HOLDERS_COPIED_TO_CLIPBOARD,
type: 'success', type: 'success',
}); });
}} }}

View File

@ -6,8 +6,6 @@ import { contexts, ParsedAccount, hooks, utils } from '@oyster/common';
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction'; import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
import { SaveOutlined } from '@ant-design/icons'; import { SaveOutlined } from '@ant-design/icons';
import { Connection, PublicKey } from '@solana/web3.js'; import { Connection, PublicKey } from '@solana/web3.js';
import { initializeBuffer } from '../../actions/initializeBuffer';
import { loadBufferAccount } from '../../actions/loadBufferAccount';
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection; const { useConnection } = contexts.Connection;
@ -35,10 +33,6 @@ export function NewInstructionCard({
const wallet = useWallet(); const wallet = useWallet();
const connection = useConnection(); const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint); 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: { const onFinish = async (values: {
slot: string; slot: string;
@ -55,21 +49,6 @@ export function NewInstructionCard({
let instruction = values.instruction; 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) { if (sigAccount) {
await addCustomSingleSignerTransaction( await addCustomSingleSignerTransaction(
connection, 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 : ( return !sigAccount ? null : (
<Card <Card
title="New Instruction" 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} />]} actions={[<SaveOutlined key="save" onClick={form.submit} />]}
> >
{saving && <Progress percent={savePerc} status="active" />} <Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
{content[tabKey]} <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> </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 { 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 React, { useState } from 'react';
import { TimelockSet } from '../../models/timelock'; import {
TimelockConfig,
TimelockSet,
TimelockStateStatus,
} from '../../models/timelock';
import { LABELS } from '../../constants'; import { LABELS } from '../../constants';
import { vote } from '../../actions/vote'; import { vote } from '../../actions/vote';
import { utils, contexts, hooks } from '@oyster/common'; import { contexts, hooks } from '@oyster/common';
import { ExclamationCircleOutlined } from '@ant-design/icons'; import {
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection; const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks; const { useAccountByMint } = hooks;
const { confirm } = Modal; 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 wallet = useWallet();
const connection = useConnection(); const connection = useConnection();
const voteAccount = useAccountByMint(proposal.info.votingMint); const voteAccount = useAccountByMint(proposal.info.votingMint);
const [tokenAmount, setTokenAmount] = useState(1); const yesVoteAccount = useAccountByMint(proposal.info.yesVotingMint);
return ( 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 <Button
type="primary" type="primary"
disabled={!voteAccount} disabled={!voteAccount}
@ -30,34 +51,46 @@ export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
<Col span={24}> <Col span={24}>
<p> <p>
Burning your {voteAccount?.info.amount.toNumber()} tokens is Burning your {voteAccount?.info.amount.toNumber()} tokens is
an irreversible action and indicates support for this an irreversible action. Choose how many to burn in favor OR
proposal. Choose how many to burn in favor of this proposal. against this proposal. Use the switch to indicate preference.
</p> </p>
<Slider <Slider
min={1} min={1}
max={voteAccount?.info.amount.toNumber()} max={voteAccount?.info.amount.toNumber()}
onChange={setTokenAmount} onChange={setTokenAmount}
/> />
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
defaultChecked
onChange={setMode}
/>
</Col> </Col>
</Row> </Row>
), ),
okText: 'Confirm', okText: LABELS.CONFIRM,
cancelText: 'Cancel', cancelText: LABELS.CANCEL,
onOk: async () => { 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. // tokenAmount is out of date in this scope, so we use a trick to get it here.
const valueHolder = { value: 0 }; const valueHolder = { value: 0 };
await setTokenAmount(amount => { await setTokenAmount(amount => {
valueHolder.value = amount; valueHolder.value = amount;
return amount; return amount;
}); });
const yesTokenAmount = mode ? valueHolder.value : 0;
const noTokenAmount = !mode ? valueHolder.value : 0;
await vote( await vote(
connection, connection,
wallet.wallet, wallet.wallet,
proposal, proposal,
timelockConfig,
voteAccount.pubkey, voteAccount.pubkey,
valueHolder.value, yesVoteAccount.pubkey,
noVoteAccount.pubkey,
yesTokenAmount,
noTokenAmount,
); );
// reset // reset
setTokenAmount(1); setTokenAmount(1);
@ -68,5 +101,5 @@ export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
> >
{LABELS.VOTE} {LABELS.VOTE}
</Button> </Button>
); ) : null;
} }

View File

@ -24,6 +24,10 @@ export const LABELS = {
COPY_FAILED_ADDRESSES_TO_CLIPBOARD: 'Copy failed addresses to clipboard', COPY_FAILED_ADDRESSES_TO_CLIPBOARD: 'Copy failed addresses to clipboard',
FAILED_SIGNERS_COPIED_TO_INPUT: 'Failed signers copied to input!', FAILED_SIGNERS_COPIED_TO_INPUT: 'Failed signers copied to input!',
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!', 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', COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
SIGNERS: 'Signers', SIGNERS: 'Signers',
ADD_SIGNERS: 'Add Signers', ADD_SIGNERS: 'Add Signers',
@ -35,26 +39,34 @@ export const LABELS = {
ADD: 'Add', ADD: 'Add',
REMOVE: 'Remove', REMOVE: 'Remove',
ADDING_OR_REMOVING: 'Type', 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...', PLEASE_WAIT: 'Please wait...',
VOTES_ADDED: 'Votes added.', GOVERNANCE_TOKENS_ADDED: 'Governance tokens added.',
NEW_VOTED_ACCOUNT_ADDED: 'New vote account added.', NEW_VOTED_ACCOUNT_ADDED: 'New vote account added.',
ADDING_NEW_VOTE_ACCOUNT: 'Adding new vote account...', ADDING_NEW_VOTE_ACCOUNT: 'Adding new vote account...',
TRANSACTION: 'Transaction - ', TRANSACTION: 'Transaction - ',
CANT_GIVE_ZERO_VOTES: "Can't give zero votes to a user!", CANT_GIVE_ZERO_TOKENS: "Can't give zero tokens to a user!",
BULK_VOTERS: 'Voters', BULK_TOKENS: 'Token Holders',
COMMA_SEPARATED_KEYS_AND_VOTES: COMMA_SEPARATED_KEYS_AND_VOTES:
'base58 pubkey, vote count, base58 pubkey, vote count, ...', 'base58 pubkey, vote count, base58 pubkey, vote count, ...',
SINGLE_VOTER: 'Single Voter', SINGLE_HOLDER: 'Token Holder',
VOTE_COUNT: 'Vote Amount', AMOUNT: 'Amount',
SINGLE_KEY: 'base58 pubkey', SINGLE_KEY: 'base58 pubkey',
VOTE_MODE: 'Vote Mode', TOKEN_MODE: 'Mode',
BULK: 'Bulk', BULK: 'Bulk',
SINGLE: 'Single', SINGLE: 'Single',
ADD_VOTES: 'Add Votes', ADD_GOVERNANCE_TOKENS: 'Add Governance Tokens',
BURNING_VOTES: 'Burning your votes...', BURNING_VOTES: 'Burning your votes...',
VOTES_BURNED: 'Votes burned', VOTES_BURNED: 'Votes burned',
VOTE: 'Vote', VOTE: 'Vote',
EXECUTING: 'Executing...', EXECUTING: 'Executing...',
EXECUTED: 'Executed.', 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 { contexts, utils, ParsedAccount } from '@oyster/common';
import { import {
CustomSingleSignerTimelockTransaction,
CustomSingleSignerTimelockTransactionLayout, CustomSingleSignerTimelockTransactionLayout,
CustomSingleSignerTimelockTransactionParser, CustomSingleSignerTimelockTransactionParser,
TimelockConfig,
TimelockConfigLayout,
TimelockConfigParser,
TimelockSet, TimelockSet,
TimelockSetLayout, TimelockSetLayout,
TimelockSetParser, TimelockSetParser,
@ -24,6 +26,7 @@ const { cache } = contexts.Accounts;
export interface ProposalsContextState { export interface ProposalsContextState {
proposals: Record<string, ParsedAccount<TimelockSet>>; proposals: Record<string, ParsedAccount<TimelockSet>>;
transactions: Record<string, ParsedAccount<TimelockTransaction>>; transactions: Record<string, ParsedAccount<TimelockTransaction>>;
configs: Record<string, ParsedAccount<TimelockConfig>>;
} }
export const ProposalsContext = React.createContext<ProposalsContextState | null>( export const ProposalsContext = React.createContext<ProposalsContextState | null>(
@ -38,15 +41,17 @@ export default function ProposalsProvider({ children = null as any }) {
const [proposals, setProposals] = useState({}); const [proposals, setProposals] = useState({});
const [transactions, setTransactions] = useState({}); const [transactions, setTransactions] = useState({});
const [configs, setConfigs] = useState({});
useSetupProposalsCache({ useSetupProposalsCache({
connection, connection,
setProposals, setProposals,
setTransactions, setTransactions,
setConfigs,
}); });
return ( return (
<ProposalsContext.Provider value={{ proposals, transactions }}> <ProposalsContext.Provider value={{ proposals, transactions, configs }}>
{children} {children}
</ProposalsContext.Provider> </ProposalsContext.Provider>
); );
@ -56,10 +61,12 @@ function useSetupProposalsCache({
connection, connection,
setProposals, setProposals,
setTransactions, setTransactions,
setConfigs,
}: { }: {
connection: Connection; connection: Connection;
setProposals: React.Dispatch<React.SetStateAction<{}>>; setProposals: React.Dispatch<React.SetStateAction<{}>>;
setTransactions: React.Dispatch<React.SetStateAction<{}>>; setTransactions: React.Dispatch<React.SetStateAction<{}>>;
setConfigs: React.Dispatch<React.SetStateAction<{}>>;
}) { }) {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
@ -76,30 +83,36 @@ function useSetupProposalsCache({
string, string,
ParsedAccount<TimelockTransaction> ParsedAccount<TimelockTransaction>
> = {}; > = {};
const newConfigs: Record<string, ParsedAccount<TimelockConfig>> = {};
all[0].forEach(a => { all[0].forEach(a => {
if (a.account.data.length === TimelockSetLayout.span) { let cached;
switch (a.account.data.length) {
case TimelockSetLayout.span:
cache.add(a.pubkey, a.account, TimelockSetParser); cache.add(a.pubkey, a.account, TimelockSetParser);
const cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>; cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>;
newProposals[a.pubkey.toBase58()] = cached; newProposals[a.pubkey.toBase58()] = cached;
} break;
if ( case CustomSingleSignerTimelockTransactionLayout.span:
a.account.data.length ===
CustomSingleSignerTimelockTransactionLayout.span
) {
cache.add( cache.add(
a.pubkey, a.pubkey,
a.account, a.account,
CustomSingleSignerTimelockTransactionParser, CustomSingleSignerTimelockTransactionParser,
); );
const cached = cache.get( cached = cache.get(a.pubkey) as ParsedAccount<TimelockTransaction>;
a.pubkey,
) as ParsedAccount<TimelockTransaction>;
newTransactions[a.pubkey.toBase58()] = cached; 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); setProposals(newProposals);
setTransactions(newTransactions); setTransactions(newTransactions);
setConfigs(newConfigs);
}); });
const subID = connection.onProgramAccountChange( const subID = connection.onProgramAccountChange(
PROGRAM_IDS.timelock.programId, PROGRAM_IDS.timelock.programId,
@ -111,6 +124,7 @@ function useSetupProposalsCache({
CustomSingleSignerTimelockTransactionParser, CustomSingleSignerTimelockTransactionParser,
setTransactions, setTransactions,
], ],
[TimelockConfigLayout.span, TimelockConfigParser, setConfigs],
].forEach(arr => { ].forEach(arr => {
const [span, parser, setter] = arr; const [span, parser, setter] = arr;
if (info.accountInfo.data.length === span) { 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 { utils } from '@oyster/common';
import * as Layout from '../utils/layout'; import * as Layout from '../utils/layout';
@ -9,8 +9,6 @@ import {
TRANSACTION_SLOTS, TRANSACTION_SLOTS,
} from './timelock'; } from './timelock';
import BN from 'bn.js'; import BN from 'bn.js';
import { toUTF8Array } from '@oyster/common/dist/lib/utils';
import { pingInstruction } from './ping';
/// [Requires Signatory token] /// [Requires Signatory token]
/// Adds a Transaction to the Timelock Set. Max of 10 of any Transaction type. More than 10 will throw error. /// 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'; } from '@solana/web3.js';
import { utils } from '@oyster/common'; import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout'; import * as BufferLayout from 'buffer-layout';
import { import { DESC_SIZE, NAME_SIZE, TimelockInstruction } from './timelock';
DESC_SIZE,
NAME_SIZE,
TimelockConfig,
TimelockInstruction,
} from './timelock';
import BN from 'bn.js';
/// Initializes a new empty Timelocked set of Instructions that will be executed at various slots in the future in draft mode. /// 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. /// Grants Admin token to caller.
/// ///
/// 0. `[writable]` Uninitialized Timelock set account . /// 0. `[writable]` Uninitialized Timelock set account .
/// 1. `[writable]` Uninitialized Signatory Mint account /// 1. `[writable]` Initialized Signatory Mint account
/// 2. `[writable]` Uninitialized Admin Mint account /// 2. `[writable]` Initialized Admin Mint account
/// 3. `[writable]` Uninitialized Voting Mint account /// 3. `[writable]` Initialized Voting Mint account
/// 4. `[writable]` Uninitialized Signatory Validation account /// 4. `[writable]` Initialized Yes Voting Mint account
/// 5. `[writable]` Uninitialized Admin Validation account /// 5. `[writable]` Initialized No Voting Mint account
/// 6. `[writable]` Uninitialized Voting Validation account /// 6. `[writable]` Initialized Signatory Validation account
/// 7. `[writable]` Uninitialized Destination account for first admin token /// 7. `[writable]` Initialized Admin Validation account
/// 8. `[writable]` Uninitialized Destination account for first signatory token /// 8. `[writable]` Initialized Voting Validation account
/// 9. `[]` Timelock program mint authority /// 9. `[writable]` Initialized Destination account for first admin token
/// 10. `[]` Timelock Program /// 10. `[writable]` Initialized Destination account for first signatory token
/// 11. '[]` Token program id /// 12. `[writable]` Initialized Yes voting dump account
/// 12. `[]` Rent sysvar /// 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 = ( export const initTimelockSetInstruction = (
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
signatoryMintAccount: PublicKey, signatoryMintAccount: PublicKey,
adminMintAccount: PublicKey, adminMintAccount: PublicKey,
votingMintAccount: PublicKey, votingMintAccount: PublicKey,
yesVotingMintAccount: PublicKey,
noVotingMintAccount: PublicKey,
signatoryValidationAccount: PublicKey, signatoryValidationAccount: PublicKey,
adminValidationAccount: PublicKey, adminValidationAccount: PublicKey,
votingValidationAccount: PublicKey, votingValidationAccount: PublicKey,
destinationAdminAccount: PublicKey, destinationAdminAccount: PublicKey,
destinationSignatoryAccount: PublicKey, destinationSignatoryAccount: PublicKey,
yesVotingDumpAccount: PublicKey,
noVotingDumpAccount: PublicKey,
governanceHoldingAccount: PublicKey,
governanceMintAccount: PublicKey,
timelockConfigAccount: PublicKey,
authority: PublicKey, authority: PublicKey,
timelockConfig: TimelockConfig,
descLink: string, descLink: string,
name: string, name: string,
): TransactionInstruction => { ): TransactionInstruction => {
@ -56,9 +63,6 @@ export const initTimelockSetInstruction = (
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'), BufferLayout.u8('instruction'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'), BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'), BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
]); ]);
@ -76,9 +80,6 @@ export const initTimelockSetInstruction = (
dataLayout.encode( dataLayout.encode(
{ {
instruction: TimelockInstruction.InitTimelockSet, instruction: TimelockInstruction.InitTimelockSet,
consensusAlgorithm: new BN(timelockConfig.consensusAlgorithm),
executionType: new BN(timelockConfig.executionType),
timelockType: new BN(timelockConfig.timelockType),
descLink: descAsBytes, descLink: descAsBytes,
name: nameAsBytes, name: nameAsBytes,
}, },
@ -90,11 +91,22 @@ export const initTimelockSetInstruction = (
{ pubkey: signatoryMintAccount, isSigner: false, isWritable: true }, { pubkey: signatoryMintAccount, isSigner: false, isWritable: true },
{ pubkey: adminMintAccount, isSigner: false, isWritable: true }, { pubkey: adminMintAccount, isSigner: false, isWritable: true },
{ pubkey: votingMintAccount, 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: signatoryValidationAccount, isSigner: false, isWritable: true },
{ pubkey: adminValidationAccount, isSigner: false, isWritable: true }, { pubkey: adminValidationAccount, isSigner: false, isWritable: true },
{ pubkey: votingValidationAccount, isSigner: false, isWritable: true }, { pubkey: votingValidationAccount, isSigner: false, isWritable: true },
{ pubkey: destinationAdminAccount, isSigner: false, isWritable: true }, { pubkey: destinationAdminAccount, isSigner: false, isWritable: true },
{ pubkey: destinationSignatoryAccount, 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: authority, isSigner: false, isWritable: false },
{ {
pubkey: PROGRAM_IDS.timelock.programAccountId, 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 DESC_SIZE = 200;
export const NAME_SIZE = 32; export const NAME_SIZE = 32;
export const INSTRUCTION_LIMIT = 500; export const INSTRUCTION_LIMIT = 450;
export const TRANSACTION_SLOTS = 10; export const TRANSACTION_SLOTS = 5;
export const TEMP_FILE_TXN_SIZE = 1000; export const TEMP_FILE_TXN_SIZE = 1000;
export enum TimelockInstruction { export enum TimelockInstruction {
@ -17,16 +17,47 @@ export enum TimelockInstruction {
AddCustomSingleSignerTransaction = 4, AddCustomSingleSignerTransaction = 4,
Sign = 8, Sign = 8,
Vote = 9, Vote = 9,
MintVotingTokens = 10,
Ping = 11, Ping = 11,
Execute = 12, Execute = 12,
UploadTempFile = 13, DepositVotingTokens = 13,
WithdrawVotingTokens = 14,
} }
export interface TimelockConfig { export interface TimelockConfig {
///version
version: number;
/// Consensus Algorithm
consensusAlgorithm: ConsensusAlgorithm; consensusAlgorithm: ConsensusAlgorithm;
/// Execution type
executionType: ExecutionType; executionType: ExecutionType;
/// Timelock Type
timelockType: TimelockType; 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 { export enum ConsensusAlgorithm {
@ -92,14 +123,10 @@ export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.str
Layout.publicKey('adminValidation'), Layout.publicKey('adminValidation'),
Layout.publicKey('votingValidation'), Layout.publicKey('votingValidation'),
BufferLayout.u8('timelockStateStatus'), BufferLayout.u8('timelockStateStatus'),
Layout.uint64('totalVotingTokensMinted'),
Layout.uint64('totalSigningTokensMinted'), Layout.uint64('totalSigningTokensMinted'),
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'), BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'), BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
...timelockTxns, ...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 /// Mint that creates voting tokens of this instruction
votingMint: PublicKey; 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 /// Used to validate signatory tokens in a round trip transfer
signatoryValidation: PublicKey; signatoryValidation: PublicKey;
@ -127,13 +160,46 @@ export interface TimelockSet {
/// Used to validate voting tokens in a round trip transfer /// Used to validate voting tokens in a round trip transfer
votingValidation: PublicKey; votingValidation: PublicKey;
/// Reserve state /// Governance holding account
state: TimelockState; governanceHolding: PublicKey;
/// Yes Voting dump account for exchanged vote tokens
yesVotingDump: PublicKey;
/// No Voting dump account for exchanged vote tokens
noVotingDump: PublicKey;
/// configuration values /// 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 = ( export const TimelockSetParser = (
pubKey: PublicKey, pubKey: PublicKey,
info: AccountInfo<Buffer>, info: AccountInfo<Buffer>,
@ -156,22 +222,22 @@ export const TimelockSetParser = (
signatoryMint: data.signatoryMint, signatoryMint: data.signatoryMint,
adminMint: data.adminMint, adminMint: data.adminMint,
votingMint: data.votingMint, votingMint: data.votingMint,
yesVotingMint: data.yesVotingMint,
noVotingMint: data.noVotingMint,
signatoryValidation: data.signatoryValidation, signatoryValidation: data.signatoryValidation,
adminValidation: data.adminValidation, adminValidation: data.adminValidation,
votingValidation: data.votingValidation, votingValidation: data.votingValidation,
governanceHolding: data.governanceHolding,
yesVotingDump: data.yesVotingDump,
noVotingDump: data.noVotingDump,
config: data.config,
state: { state: {
status: data.timelockStateStatus, status: data.timelockStateStatus,
totalVotingTokensMinted: data.totalVotingTokensMinted,
totalSigningTokensMinted: data.totalSigningTokensMinted, totalSigningTokensMinted: data.totalSigningTokensMinted,
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''), descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''), name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''),
timelockTransactions: timelockTxns, timelockTransactions: timelockTxns,
}, },
config: {
consensusAlgorithm: data.consensusAlgorithm,
executionType: data.executionType,
timelockType: data.timelockType,
},
}, },
}; };
@ -203,26 +269,29 @@ export const CustomSingleSignerTimelockTransactionParser = (
return details; return details;
}; };
export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const TimelockConfigParser = (
[ pubKey: PublicKey,
BufferLayout.u8('version'), info: AccountInfo<Buffer>,
Layout.uint64('slot'), ) => {
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'), const buffer = Buffer.from(info.data);
BufferLayout.u8('executed'), const data = TimelockConfigLayout.decode(buffer);
BufferLayout.u16('instructionEndIndex'),
],
);
export interface TimelockTransaction { const details = {
version: number; 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; return details;
};
instruction: number[];
executed: number;
instructionEndIndex: number;
}
export interface CustomSingleSignerTimelockTransaction
extends TimelockTransaction {}

View File

@ -7,29 +7,43 @@ import { TimelockInstruction } from './timelock';
import BN from 'bn.js'; import BN from 'bn.js';
/// [Requires Voting tokens] /// [Requires Voting tokens]
/// Burns voting tokens, indicating you approve of running this set of transactions. If you tip the consensus, /// Burns voting tokens, indicating you approve and/or disapprove of running this set of transactions. If you tip the consensus,
/// then the transactions begin to be run at their time slots. /// then the transactions can begin to be run at their time slots when people click execute.
/// ///
/// 0. `[writable]` Timelock set account. /// 0. `[writable]` Timelock set account.
/// 1. `[writable]` Voting account. /// 1. `[writable]` Your Voting account.
/// 2. `[writable]` Voting mint account. /// 2. `[writable]` Your Yes-Voting account.
/// 3. `[]` Transfer authority /// 3. `[writable]` Your No-Voting account.
/// 4. `[]` Timelock program mint authority /// 4. `[writable]` Voting mint account.
/// 5. `[]` Timelock program account pub key. /// 5. `[writable]` Yes Voting mint account.
/// 6. `[]` Token program 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 = ( export const voteInstruction = (
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
votingAccount: PublicKey, votingAccount: PublicKey,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
votingMint: PublicKey, votingMint: PublicKey,
yesVotingMint: PublicKey,
noVotingMint: PublicKey,
governanceMint: PublicKey,
timelockConfig: PublicKey,
transferAuthority: PublicKey, transferAuthority: PublicKey,
mintAuthority: PublicKey, mintAuthority: PublicKey,
votingTokenAmount: number, yesVotingTokenAmount: number,
noVotingTokenAmount: number,
): TransactionInstruction => { ): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'), BufferLayout.u8('instruction'),
Layout.uint64('votingTokenAmount'), Layout.uint64('yesVotingTokenAmount'),
Layout.uint64('noVotingTokenAmount'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -37,7 +51,8 @@ export const voteInstruction = (
dataLayout.encode( dataLayout.encode(
{ {
instruction: TimelockInstruction.Vote, instruction: TimelockInstruction.Vote,
votingTokenAmount: new BN(votingTokenAmount), yesVotingTokenAmount: new BN(yesVotingTokenAmount),
noVotingTokenAmount: new BN(noVotingTokenAmount),
}, },
data, data,
); );
@ -45,7 +60,13 @@ export const voteInstruction = (
const keys = [ const keys = [
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true }, { pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: votingAccount, 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: 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: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, 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 { import {
ConsensusAlgorithm, ConsensusAlgorithm,
INSTRUCTION_LIMIT, INSTRUCTION_LIMIT,
TimelockConfig,
TimelockSet, TimelockSet,
TimelockStateStatus, TimelockStateStatus,
TimelockTransaction, TimelockTransaction,
VotingEntryRule,
} from '../../models/timelock'; } from '../../models/timelock';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@ -19,8 +21,9 @@ import { InstructionCard } from '../../components/Proposal/InstructionCard';
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard'; import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
import SignButton from '../../components/Proposal/SignButton'; import SignButton from '../../components/Proposal/SignButton';
import AddSigners from '../../components/Proposal/AddSigners'; 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 { 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()@:%_\+.~#?&//=]*)/; 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 { useMint } = contexts.Accounts;
const { useAccountByMint } = hooks; const { useAccountByMint } = hooks;
@ -30,13 +33,18 @@ export const ProposalView = () => {
const context = useProposals(); const context = useProposals();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const proposal = context.proposals[id]; const proposal = context.proposals[id];
const timelockConfig = context.configs[proposal?.info.config.toBase58()];
const sigMint = useMint(proposal?.info.signatoryMint); const sigMint = useMint(proposal?.info.signatoryMint);
const votingMint = useMint(proposal?.info.votingMint); const votingMint = useMint(proposal?.info.votingMint);
const governanceMint = useMint(timelockConfig?.info.governanceMint);
return ( return (
<div className="flexColumn"> <div className="flexColumn">
{proposal && sigMint && votingMint ? ( {proposal && sigMint && votingMint && governanceMint ? (
<InnerProposalView <InnerProposalView
proposal={proposal} proposal={proposal}
timelockConfig={timelockConfig}
governanceMint={governanceMint}
votingMint={votingMint} votingMint={votingMint}
sigMint={sigMint} sigMint={sigMint}
instructions={context.transactions} instructions={context.transactions}
@ -53,10 +61,14 @@ function InnerProposalView({
sigMint, sigMint,
votingMint, votingMint,
instructions, instructions,
timelockConfig,
governanceMint,
}: { }: {
proposal: ParsedAccount<TimelockSet>; proposal: ParsedAccount<TimelockSet>;
timelockConfig: ParsedAccount<TimelockConfig>;
sigMint: MintInfo; sigMint: MintInfo;
votingMint: MintInfo; votingMint: MintInfo;
governanceMint: MintInfo;
instructions: Record<string, ParsedAccount<TimelockTransaction>>; instructions: Record<string, ParsedAccount<TimelockTransaction>>;
}) { }) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint); const sigAccount = useAccountByMint(proposal.info.signatoryMint);
@ -196,23 +208,20 @@ function InnerProposalView({
: 'vertical' : 'vertical'
} }
> >
{sigAccount && <MintGovernanceTokens timelockConfig={timelockConfig} />
sigAccount.info.amount.toNumber() === 1 && <RegisterToVote
proposal.info.state.status === TimelockStateStatus.Draft && ( timelockConfig={timelockConfig}
<AddVotes proposal={proposal} /> proposal={proposal}
)} />
{voteAccount &&
voteAccount.info.amount.toNumber() > 0 && <Vote proposal={proposal} timelockConfig={timelockConfig} />
proposal.info.state.status === TimelockStateStatus.Voting && (
<Vote proposal={proposal} />
)}
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Statistic <Statistic
valueStyle={{ color: 'green' }} valueStyle={{ color: 'green' }}
title={LABELS.VOTES_REQUIRED} title={LABELS.VOTES_REQUIRED}
value={getVotesRequired(proposal)} value={getVotesRequired(timelockConfig, governanceMint)}
/> />
</Col> </Col>
</Row> </Row>
@ -246,21 +255,20 @@ function InnerProposalView({
); );
} }
function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number { function getVotesRequired(
if (proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.Majority) { timelockConfig: ParsedAccount<TimelockConfig>,
return Math.ceil( governanceMint: MintInfo,
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5, ): number {
); if (timelockConfig.info.consensusAlgorithm === ConsensusAlgorithm.Majority) {
return Math.ceil(governanceMint.supply.toNumber() * 0.5);
} else if ( } else if (
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority timelockConfig.info.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
) { ) {
return Math.ceil( return Math.ceil(governanceMint.supply.toNumber() * 0.66);
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.66,
);
} else if ( } 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; return 0;
} }