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;
|
const status = proposal.info.state.status;
|
||||||
let color = STATE_COLOR[status];
|
let color = STATE_COLOR[status];
|
||||||
return (
|
return (
|
||||||
<Badge.Ribbon style={{ backgroundColor: color }} text={status}>
|
<Badge.Ribbon
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
text={TimelockStateStatus[status]}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Badge.Ribbon>
|
</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',
|
BULK: 'Bulk',
|
||||||
SINGLE: 'Single',
|
SINGLE: 'Single',
|
||||||
ADD_VOTES: 'Add Votes',
|
ADD_VOTES: 'Add Votes',
|
||||||
|
BURNING_VOTES: 'Burning your votes...',
|
||||||
|
VOTES_BURNED: 'Votes burned',
|
||||||
|
VOTE: 'Vote',
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ export enum TimelockInstruction {
|
||||||
RemoveSigner = 3,
|
RemoveSigner = 3,
|
||||||
AddCustomSingleSignerTransaction = 4,
|
AddCustomSingleSignerTransaction = 4,
|
||||||
Sign = 8,
|
Sign = 8,
|
||||||
|
Vote = 9,
|
||||||
MintVotingTokens = 10,
|
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 SignButton from '../../components/Proposal/SignButton';
|
||||||
import AddSigners from '../../components/Proposal/AddSigners';
|
import AddSigners from '../../components/Proposal/AddSigners';
|
||||||
import AddVotes from '../../components/Proposal/AddVotes';
|
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()@:%_\+.~#?&//=]*)/;
|
export const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
||||||
const { useMint } = contexts.Accounts;
|
const { useMint } = contexts.Accounts;
|
||||||
const { useAccountByMint } = hooks;
|
const { useAccountByMint } = hooks;
|
||||||
|
@ -60,6 +61,8 @@ function InnerProposalView({
|
||||||
}) {
|
}) {
|
||||||
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
|
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
|
||||||
const adminAccount = useAccountByMint(proposal.info.adminMint);
|
const adminAccount = useAccountByMint(proposal.info.adminMint);
|
||||||
|
const voteAccount = useAccountByMint(proposal.info.votingMint);
|
||||||
|
|
||||||
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
|
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
|
||||||
.map(k => instructions[k.toBase58()])
|
.map(k => instructions[k.toBase58()])
|
||||||
.filter(k => k);
|
.filter(k => k);
|
||||||
|
@ -198,6 +201,11 @@ function InnerProposalView({
|
||||||
proposal.info.state.status === TimelockStateStatus.Draft && (
|
proposal.info.state.status === TimelockStateStatus.Draft && (
|
||||||
<AddVotes proposal={proposal} />
|
<AddVotes proposal={proposal} />
|
||||||
)}
|
)}
|
||||||
|
{voteAccount &&
|
||||||
|
voteAccount.info.amount.toNumber() > 0 &&
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Voting && (
|
||||||
|
<Vote proposal={proposal} />
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
@ -222,7 +230,8 @@ function InnerProposalView({
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
{instructionsForProposal.length < INSTRUCTION_LIMIT && (
|
{instructionsForProposal.length < INSTRUCTION_LIMIT &&
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Draft && (
|
||||||
<Col xs={24} sm={24} md={12} lg={8}>
|
<Col xs={24} sm={24} md={12} lg={8}>
|
||||||
<NewInstructionCard
|
<NewInstructionCard
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
|
@ -245,7 +254,7 @@ function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
|
||||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
|
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
|
||||||
) {
|
) {
|
||||||
return Math.ceil(
|
return Math.ceil(
|
||||||
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5,
|
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.66,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
|
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
|
||||||
|
|
Loading…
Reference in New Issue