mirror of https://github.com/certusone/oyster.git
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:
parent
819951868f
commit
90d8d63a04
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
}}
|
|
@ -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]);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue