mirror of https://github.com/certusone/oyster.git
Add voting power
This commit is contained in:
parent
c5ac500648
commit
243bb0a321
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ export enum TimelockInstruction {
|
|||
RemoveSigner = 3,
|
||||
AddCustomSingleSignerTransaction = 4,
|
||||
Sign = 8,
|
||||
Vote = 9,
|
||||
MintVotingTokens = 10,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue