Add voting power

This commit is contained in:
Dummy Tester 123 2021-03-01 21:44:24 -06:00
parent c5ac500648
commit 243bb0a321
7 changed files with 249 additions and 10 deletions

View File

@ -0,0 +1,87 @@
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 { mintVotingTokensInstruction } from '../models/mintVotingTokens';
import { LABELS } from '../constants';
import { voteInstruction } from '../models/vote';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const vote = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
votingAccount: PublicKey,
votingTokenAmount: number,
) => {
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const [mintAuthority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
[],
votingAccount,
wallet.publicKey,
votingTokenAmount,
);
signers.push(transferAuthority);
instructions.push(
voteInstruction(
proposal.pubkey,
votingAccount,
proposal.info.votingMint,
transferAuthority.publicKey,
mintAuthority,
votingTokenAmount,
),
);
notify({
message: LABELS.BURNING_VOTES,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: LABELS.VOTES_BURNED,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -17,7 +17,10 @@ export function StateBadgeRibbon({
const status = proposal.info.state.status;
let color = STATE_COLOR[status];
return (
<Badge.Ribbon style={{ backgroundColor: color }} text={status}>
<Badge.Ribbon
style={{ backgroundColor: color }}
text={TimelockStateStatus[status]}
>
{children}
</Badge.Ribbon>
);

View File

@ -0,0 +1,72 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider } from 'antd';
import React, { useState } from 'react';
import { TimelockSet } 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';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { confirm } = Modal;
export function Vote({ proposal }: { proposal: ParsedAccount<TimelockSet> }) {
const wallet = useWallet();
const connection = useConnection();
const voteAccount = useAccountByMint(proposal.info.votingMint);
const [tokenAmount, setTokenAmount] = useState(1);
return (
<Button
type="primary"
disabled={!voteAccount}
onClick={() =>
confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: (
<Row>
<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.
</p>
<Slider
min={1}
max={voteAccount?.info.amount.toNumber()}
onChange={setTokenAmount}
/>
</Col>
</Row>
),
okText: 'Confirm',
cancelText: 'Cancel',
onOk: async () => {
if (voteAccount) {
// 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 vote(
connection,
wallet.wallet,
proposal,
voteAccount.pubkey,
valueHolder.value,
);
// reset
setTokenAmount(1);
}
},
})
}
>
{LABELS.VOTE}
</Button>
);
}

View File

@ -52,4 +52,7 @@ export const LABELS = {
BULK: 'Bulk',
SINGLE: 'Single',
ADD_VOTES: 'Add Votes',
BURNING_VOTES: 'Burning your votes...',
VOTES_BURNED: 'Votes burned',
VOTE: 'Vote',
};

View File

@ -15,6 +15,7 @@ export enum TimelockInstruction {
RemoveSigner = 3,
AddCustomSingleSignerTransaction = 4,
Sign = 8,
Vote = 9,
MintVotingTokens = 10,
}

View File

@ -0,0 +1,64 @@
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 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.
///
/// 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.
export const voteInstruction = (
timelockSetAccount: PublicKey,
votingAccount: PublicKey,
votingMint: 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.Vote,
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: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -20,6 +20,7 @@ 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 { Vote } from '../../components/Proposal/Vote';
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;
@ -60,6 +61,8 @@ function InnerProposalView({
}) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const adminAccount = useAccountByMint(proposal.info.adminMint);
const voteAccount = useAccountByMint(proposal.info.votingMint);
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
.map(k => instructions[k.toBase58()])
.filter(k => k);
@ -198,6 +201,11 @@ function InnerProposalView({
proposal.info.state.status === TimelockStateStatus.Draft && (
<AddVotes proposal={proposal} />
)}
{voteAccount &&
voteAccount.info.amount.toNumber() > 0 &&
proposal.info.state.status === TimelockStateStatus.Voting && (
<Vote proposal={proposal} />
)}
</Space>
</Col>
<Col span={8}>
@ -222,14 +230,15 @@ function InnerProposalView({
/>
</Col>
))}
{instructionsForProposal.length < INSTRUCTION_LIMIT && (
<Col xs={24} sm={24} md={12} lg={8}>
<NewInstructionCard
proposal={proposal}
position={instructionsForProposal.length}
/>
</Col>
)}
{instructionsForProposal.length < INSTRUCTION_LIMIT &&
proposal.info.state.status === TimelockStateStatus.Draft && (
<Col xs={24} sm={24} md={12} lg={8}>
<NewInstructionCard
proposal={proposal}
position={instructionsForProposal.length}
/>
</Col>
)}
</Row>
</Space>
</>
@ -245,7 +254,7 @@ function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
) {
return Math.ceil(
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5,
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.66,
);
} else if (
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus