Merge pull request #39 from SebastianBor/main

Proposals: Add quick vote
This commit is contained in:
Jordan Prince 2021-04-06 09:43:55 -05:00 committed by GitHub
commit eda4f82640
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 243 additions and 339 deletions

View File

@ -1,3 +1,4 @@
export * from './useUserAccounts';
export * from './useAccountByMint';
export * from './useTokenName';
export * from './useThatState';

View File

@ -0,0 +1,16 @@
import { useState } from 'react';
// Extends useState() hook with async getThatState getter which can be used to get state value in contexts (ex. async callbacks) where up to date state is not available
export function useThatState<T>(initialState: T) {
const [state, setState] = useState<T>(initialState);
const getThatState = () =>
new Promise<T>(resolve => {
// Use NOP setState call to retrieve current state value
setState(s => {
resolve(s);
return s;
});
});
return [state, setState, getThatState] as const;
}

View File

@ -12,17 +12,22 @@ import {
actions,
} from '@oyster/common';
import { TimelockSet } from '../models/timelock';
import { TimelockConfig, TimelockSet, TimelockState } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { depositSourceTokensInstruction } from '../models/depositSourceTokens';
import { LABELS } from '../constants';
import { depositSourceTokensInstruction } from '../models/depositSourceTokens';
import { createEmptyGovernanceVotingRecordInstruction } from '../models/createEmptyGovernanceVotingRecord';
import { voteInstruction } from '../models/vote';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { sendTransactions } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const depositSourceTokens = async (
export const depositSourceTokensAndVote = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
@ -30,12 +35,18 @@ export const depositSourceTokens = async (
existingYesVoteAccount: PublicKey | undefined,
existingNoVoteAccount: PublicKey | undefined,
sourceAccount: PublicKey,
votingTokenAmount: number,
timelockConfig: ParsedAccount<TimelockConfig>,
state: ParsedAccount<TimelockState>,
yesVotingTokenAmount: number,
noVotingTokenAmount: number,
) => {
const votingTokenAmount =
yesVotingTokenAmount > 0 ? yesVotingTokenAmount : noVotingTokenAmount;
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
let depositSigners: Account[] = [];
let depositInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
@ -44,12 +55,12 @@ export const depositSourceTokens = async (
let needToCreateGovAccountToo = !existingVoteAccount;
if (!existingVoteAccount) {
existingVoteAccount = createTokenAccount(
instructions,
depositInstructions,
wallet.publicKey,
accountRentExempt,
proposal.info.votingMint,
wallet.publicKey,
signers,
depositSigners,
);
}
@ -63,7 +74,7 @@ export const depositSourceTokens = async (
);
if (needToCreateGovAccountToo) {
instructions.push(
depositInstructions.push(
createEmptyGovernanceVotingRecordInstruction(
governanceVotingRecord,
proposal.pubkey,
@ -74,24 +85,24 @@ export const depositSourceTokens = async (
}
if (!existingYesVoteAccount) {
createTokenAccount(
instructions,
existingYesVoteAccount = createTokenAccount(
depositInstructions,
wallet.publicKey,
accountRentExempt,
proposal.info.yesVotingMint,
wallet.publicKey,
signers,
depositSigners,
);
}
if (!existingNoVoteAccount) {
createTokenAccount(
instructions,
existingNoVoteAccount = createTokenAccount(
depositInstructions,
wallet.publicKey,
accountRentExempt,
proposal.info.noVotingMint,
wallet.publicKey,
signers,
depositSigners,
);
}
@ -100,17 +111,17 @@ export const depositSourceTokens = async (
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
const depositAuthority = approve(
depositInstructions,
[],
sourceAccount,
wallet.publicKey,
votingTokenAmount,
);
signers.push(transferAuthority);
depositSigners.push(depositAuthority);
instructions.push(
depositInstructions.push(
depositSourceTokensInstruction(
governanceVotingRecord,
existingVoteAccount,
@ -118,31 +129,77 @@ export const depositSourceTokens = async (
proposal.info.sourceHolding,
proposal.info.votingMint,
proposal.pubkey,
transferAuthority.publicKey,
depositAuthority.publicKey,
mintAuthority,
votingTokenAmount,
),
);
let voteSigners: Account[] = [];
let voteInstructions: TransactionInstruction[] = [];
const voteAuthority = approve(
voteInstructions,
[],
existingVoteAccount,
wallet.publicKey,
yesVotingTokenAmount + noVotingTokenAmount,
);
voteSigners.push(voteAuthority);
voteInstructions.push(
voteInstruction(
governanceVotingRecord,
state.pubkey,
existingVoteAccount,
existingYesVoteAccount,
existingNoVoteAccount,
proposal.info.votingMint,
proposal.info.yesVotingMint,
proposal.info.noVotingMint,
proposal.info.sourceMint,
proposal.pubkey,
timelockConfig.pubkey,
voteAuthority.publicKey,
mintAuthority,
yesVotingTokenAmount,
noVotingTokenAmount,
),
);
const [votingMsg, votedMsg, voteTokensMsg] =
yesVotingTokenAmount > 0
? [
LABELS.VOTING_YEAH,
LABELS.VOTED_YEAH,
`${yesVotingTokenAmount} ${LABELS.TOKENS_VOTED_FOR_THE_PROPOSAL}.`,
]
: [
LABELS.VOTING_NAY,
LABELS.VOTED_NAY,
`${noVotingTokenAmount} ${LABELS.TOKENS_VOTED_AGAINST_THE_PROPOSAL}.`,
];
notify({
message: LABELS.ADDING_VOTES_TO_VOTER,
message: votingMsg,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
try {
let tx = await sendTransaction(
await sendTransactions(
connection,
wallet,
instructions,
signers,
[depositInstructions, voteInstructions],
[depositSigners, voteSigners],
true,
);
notify({
message: LABELS.VOTES_ADDED,
message: votedMsg,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
description: voteTokensMsg,
});
} catch (ex) {
console.error(ex);

View File

@ -1,108 +0,0 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import {
contexts,
utils,
models,
ParsedAccount,
actions,
} from '@oyster/common';
import { TimelockConfig, TimelockSet, TimelockState } from '../models/timelock';
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>,
timelockConfig: ParsedAccount<TimelockConfig>,
state: ParsedAccount<TimelockState>,
votingAccount: PublicKey,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
yesVotingTokenAmount: number,
noVotingTokenAmount: 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 [governanceVotingRecord] = await PublicKey.findProgramAddress(
[
PROGRAM_IDS.timelock.programAccountId.toBuffer(),
proposal.pubkey.toBuffer(),
votingAccount.toBuffer(),
],
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
[],
votingAccount,
wallet.publicKey,
yesVotingTokenAmount + noVotingTokenAmount,
);
signers.push(transferAuthority);
instructions.push(
voteInstruction(
governanceVotingRecord,
state.pubkey,
votingAccount,
yesVotingAccount,
noVotingAccount,
proposal.info.votingMint,
proposal.info.yesVotingMint,
proposal.info.noVotingMint,
proposal.info.sourceMint,
proposal.pubkey,
timelockConfig.pubkey,
transferAuthority.publicKey,
mintAuthority,
yesVotingTokenAmount,
noVotingTokenAmount,
),
);
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

@ -12,7 +12,11 @@ import {
actions,
} from '@oyster/common';
import { TimelockSet, TimelockState } from '../models/timelock';
import {
TimelockSet,
TimelockState,
TimelockStateStatus,
} from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { withdrawVotingTokensInstruction } from '../models/withdrawVotingTokens';
import { LABELS } from '../constants';
@ -141,8 +145,13 @@ export const withdrawVotingTokens = async (
),
);
const [msg, completedMsg] =
state.info.status === TimelockStateStatus.Voting
? [LABELS.WITHDRAWING_YOUR_VOTE, LABELS.VOTE_WITHDRAWN]
: [LABELS.REFUNDING_YOUR_TOKENS, LABELS.TOKENS_REFUNDED];
notify({
message: LABELS.WITHDRAWING_VOTING_TOKENS,
message: msg,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
@ -157,7 +166,7 @@ export const withdrawVotingTokens = async (
);
notify({
message: LABELS.TOKENS_WITHDRAWN,
message: completedMsg,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
});

View File

@ -1,106 +0,0 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider } from 'antd';
import React, { useState } from 'react';
import {
TimelockConfig,
TimelockSet,
TimelockState,
TimelockStateStatus,
VotingEntryRule,
} from '../../models/timelock';
import { LABELS } from '../../constants';
import { depositSourceTokens } from '../../actions/depositSourceTokens';
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,
state,
timelockConfig,
}: {
proposal: ParsedAccount<TimelockSet>;
state: ParsedAccount<TimelockState>;
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 userTokenAccount = useAccountByMint(proposal.info.sourceMint);
const alreadyHaveTokens =
(voteAccount && voteAccount.info.amount.toNumber() > 0) ||
(yesVoteAccount && yesVoteAccount.info.amount.toNumber() > 0) ||
(noVoteAccount && noVoteAccount.info.amount.toNumber() > 0);
const eligibleToView =
timelockConfig.info.votingEntryRule == VotingEntryRule.Anytime &&
[TimelockStateStatus.Draft, TimelockStateStatus.Voting].includes(
state.info.status,
);
const [_, setTokenAmount] = useState(1);
return eligibleToView ? (
<Button
type="primary"
onClick={() =>
confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: (
<Row>
<Col span={24}>
<p>
You can convert up to{' '}
{userTokenAccount?.info.amount.toNumber() || 0} tokens to
voting tokens to vote on this proposal. You can refund these
at any time.
</p>
{userTokenAccount?.info.amount.toNumber() && (
<Slider
min={1}
max={userTokenAccount?.info.amount.toNumber() || 0}
onChange={setTokenAmount}
/>
)}
</Col>
</Row>
),
okText: LABELS.CONFIRM,
cancelText: LABELS.CANCEL,
onOk: async () => {
if (userTokenAccount) {
// 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 depositSourceTokens(
connection,
wallet.wallet,
proposal,
voteAccount?.pubkey,
yesVoteAccount?.pubkey,
noVoteAccount?.pubkey,
userTokenAccount.pubkey,
valueHolder.value,
);
// reset
setTokenAmount(1);
}
},
})
}
>
{alreadyHaveTokens ? LABELS.ADD_MORE_VOTES : LABELS.REGISTER_TO_VOTE}
</Button>
) : null;
}

View File

@ -1,6 +1,6 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider, Switch } from 'antd';
import React, { useState } from 'react';
import { Button, Col, Modal, Row } from 'antd';
import React from 'react';
import {
TimelockConfig,
TimelockSet,
@ -8,13 +8,11 @@ import {
TimelockStateStatus,
} from '../../models/timelock';
import { LABELS } from '../../constants';
import { vote } from '../../actions/vote';
import { depositSourceTokensAndVote } from '../../actions/depositSourceTokensAndVote';
import { contexts, hooks } from '@oyster/common';
import {
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import './style.less';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
@ -25,87 +23,84 @@ export function Vote({
proposal,
state,
timelockConfig,
yeahVote,
}: {
proposal: ParsedAccount<TimelockSet>;
state: ParsedAccount<TimelockState>;
timelockConfig: ParsedAccount<TimelockConfig>;
yeahVote: boolean;
}) {
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 [mode, setMode] = useState(true);
const [_, setTokenAmount] = useState(1);
const userTokenAccount = useAccountByMint(proposal.info.sourceMint);
const eligibleToView =
voteAccount &&
voteAccount.info.amount.toNumber() > 0 &&
userTokenAccount &&
userTokenAccount.info.amount.toNumber() > 0 &&
state.info.status === TimelockStateStatus.Voting;
const [btnLabel, title, msg, icon] = yeahVote
? [
LABELS.VOTE_YEAH,
LABELS.VOTE_YEAH_QUESTION,
LABELS.VOTE_YEAH_MSG,
<CheckOutlined />,
]
: [
LABELS.VOTE_NAY,
LABELS.VOTE_NAY_QUESTION,
LABELS.VOTE_NAY_MSG,
<CloseOutlined />,
];
return eligibleToView ? (
<Button
type="primary"
icon={icon}
onClick={() =>
confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
title: title,
icon: icon,
content: (
<Row>
<Col span={24}>
<p>
Burning your {voteAccount?.info.amount.toNumber()} tokens is
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}
/>
<p>{msg}</p>
</Col>
</Row>
),
okText: LABELS.CONFIRM,
cancelText: LABELS.CANCEL,
onOk: async () => {
if (voteAccount && yesVoteAccount && noVoteAccount) {
// tokenAmount and mode is out of date in this scope, so we use a trick to get it here.
const valueHolder = { value: 0, mode: true };
await setTokenAmount(amount => {
valueHolder.value = amount;
return amount;
});
await setMode(mode => {
valueHolder.mode = mode;
return mode;
});
const yesTokenAmount = valueHolder.mode ? valueHolder.value : 0;
const noTokenAmount = !valueHolder.mode ? valueHolder.value : 0;
await vote(
if (userTokenAccount) {
const voteAmount = userTokenAccount.info.amount.toNumber();
const yesTokenAmount = yeahVote ? voteAmount : 0;
const noTokenAmount = !yeahVote ? voteAmount : 0;
await depositSourceTokensAndVote(
connection,
wallet.wallet,
proposal,
voteAccount?.pubkey,
yesVoteAccount?.pubkey,
noVoteAccount?.pubkey,
userTokenAccount.pubkey,
timelockConfig,
state,
voteAccount.pubkey,
yesVoteAccount.pubkey,
noVoteAccount.pubkey,
yesTokenAmount,
noTokenAmount,
);
// reset
setTokenAmount(1);
}
},
})
}
>
{LABELS.VOTE}
{btnLabel}
</Button>
) : null;
}

View File

@ -1,6 +1,6 @@
import { ParsedAccount } from '@oyster/common';
import { Button, Col, Modal, Row, Slider } from 'antd';
import React, { useState } from 'react';
import { Button, Col, Modal, Row } from 'antd';
import React from 'react';
import {
TimelockConfig,
TimelockSet,
@ -17,9 +17,8 @@ const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { confirm } = Modal;
export function WithdrawTokens({
export function WithdrawVote({
proposal,
timelockConfig,
state,
}: {
proposal: ParsedAccount<TimelockSet>;
@ -33,45 +32,52 @@ export function WithdrawTokens({
const noVoteAccount = useAccountByMint(proposal.info.noVotingMint);
const userAccount = useAccountByMint(proposal.info.sourceMint);
const votingTokens =
(voteAccount && voteAccount.info.amount.toNumber()) ||
0 +
((yesVoteAccount && yesVoteAccount.info.amount.toNumber()) || 0) +
((noVoteAccount && noVoteAccount.info.amount.toNumber()) || 0);
let additionalMsg =
state.info.status !== TimelockStateStatus.Voting
? ''
: LABELS.ADDITIONAL_VOTING_MSG;
const [_, setTokenAmount] = useState(1);
return votingTokens > 0 ? (
const eligibleToView =
votingTokens > 0 &&
(state.info.status === TimelockStateStatus.Voting ||
state.info.status === TimelockStateStatus.Completed ||
state.info.status === TimelockStateStatus.Defeated);
const [btnLabel, title, msg, action] =
state.info.status === TimelockStateStatus.Voting
? [
LABELS.WITHDRAW_VOTE,
LABELS.WITHDRAW_YOUR_VOTE_QUESTION,
LABELS.WITHDRAW_YOUR_VOTE_MSG,
LABELS.WITHDRAW,
]
: [
LABELS.REFUND_TOKENS,
LABELS.REFUND_YOUR_TOKENS_QUESTION,
LABELS.REFUND_YOUR_TOKENS_MSG,
LABELS.REFUND,
];
return eligibleToView ? (
<Button
type="primary"
onClick={() =>
confirm({
title: 'Confirm',
title: title,
icon: <ExclamationCircleOutlined />,
content: (
<Row>
<Col span={24}>
<p>You can withdraw up to {votingTokens} voting tokens. </p>
{additionalMsg && <p>{additionalMsg}</p>}
<Slider min={1} max={votingTokens} onChange={setTokenAmount} />
<p>{msg}</p>
</Col>
</Row>
),
okText: LABELS.CONFIRM,
okText: action,
cancelText: LABELS.CANCEL,
onOk: async () => {
if (userAccount) {
// 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 withdrawVotingTokens(
connection,
wallet.wallet,
@ -81,16 +87,14 @@ export function WithdrawTokens({
yesVoteAccount?.pubkey,
noVoteAccount?.pubkey,
userAccount.pubkey,
valueHolder.value,
votingTokens,
);
// reset
setTokenAmount(1);
}
},
})
}
>
{LABELS.REFUND_TOKENS}
{btnLabel}
</Button>
) : null;
}

View File

@ -5,3 +5,13 @@
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* IE */
}
.vote-radio-group {
width: 100%;
user-select: none;
padding: 10px;
.ant-radio-button-wrapper {
width: 50%;
text-align: center;
}
}

View File

@ -39,8 +39,7 @@ export const LABELS = {
ADD: 'Add',
REMOVE: 'Remove',
ADDING_OR_REMOVING: 'Type',
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...',
GOVERNANCE_TOKENS_ADDED: 'Governance tokens added.',
@ -61,18 +60,44 @@ export const LABELS = {
ADD_GOVERNANCE_TOKENS: 'Add Governance Tokens',
ADD_COUNCIL_TOKENS: 'Add Council Tokens',
ACTIONS: 'Actions',
BURNING_VOTES: 'Burning your votes...',
VOTES_BURNED: 'Votes burned',
VOTE: 'Vote',
VOTE_YEAH: 'Yeah',
VOTE_YEAH_QUESTION: 'Vote Yeah?',
VOTE_YEAH_MSG: 'Vote in favour of the proposal.',
VOTING_YEAH: 'Voting for the proposal',
VOTED_YEAH: 'Voted for the proposal',
VOTE_NAY: 'Nay',
VOTE_NAY_QUESTION: 'Vote Nay?',
VOTE_NAY_MSG: 'Vote against the proposal.',
VOTING_NAY: 'Voting against the proposal',
VOTED_NAY: 'Voted against the proposal',
TOKENS_VOTED_FOR_THE_PROPOSAL: 'tokens voted for the proposal',
TOKENS_VOTED_AGAINST_THE_PROPOSAL: 'tokens voted against the proposal',
EXECUTING: 'Executing...',
EXECUTED: 'Executed.',
WITHDRAWING_VOTING_TOKENS: 'Refunding voting tokens as Source Tokens',
TOKENS_WITHDRAWN: 'Voting tokens refunded as Source Tokens',
REGISTER_TO_VOTE: 'Register to Vote',
CONFIRM: 'Confirm',
CANCEL: 'Cancel',
ADD_MORE_VOTES: 'Add More Votes',
WITHDRAW_VOTE: 'Withdraw My Vote',
WITHDRAW_YOUR_VOTE_QUESTION: 'Withdraw your vote?',
WITHDRAW_YOUR_VOTE_MSG:
'Once you withdraw your vote it wont count towards the proposal voting outcome.',
WITHDRAW: 'Withdraw',
WITHDRAWING_YOUR_VOTE: 'Withdrawing your vote',
VOTE_WITHDRAWN: 'Your vote has been withdrawn',
REFUND_TOKENS: 'Refund My Tokens',
REFUND_YOUR_TOKENS_QUESTION: 'Refund your tokens?',
REFUND_YOUR_TOKENS_MSG:
'The proposal has been voted. Refunding your tokens wont change the outcome.',
REFUND: 'Refund',
REFUNDING_YOUR_TOKENS: 'Refunding your tokens',
TOKENS_REFUNDED: 'Your voting tokens have been refunded',
REGISTER_GOVERNANCE: 'Register',
PROGRAM: 'Program ID',
GOVERNANCE: 'Governance Token Holders',

View File

@ -24,8 +24,7 @@ import SignButton from '../../components/Proposal/SignButton';
import AddSigners from '../../components/Proposal/AddSigners';
import MintSourceTokens from '../../components/Proposal/MintSourceTokens';
import { Vote } from '../../components/Proposal/Vote';
import { RegisterToVote } from '../../components/Proposal/RegisterToVote';
import { WithdrawTokens } from '../../components/Proposal/WithdrawTokens';
import { WithdrawVote } from '../../components/Proposal/WithdrawVote';
import './style.less';
import { getGovernanceVotingRecords } from '../../utils/lookups';
import BN from 'bn.js';
@ -296,20 +295,22 @@ function InnerProposalView({
timelockConfig.info.governanceMint.toBase58()
}
/>
<RegisterToVote
timelockConfig={timelockConfig}
proposal={proposal}
state={timelockState}
/>
<WithdrawTokens
<WithdrawVote
timelockConfig={timelockConfig}
proposal={proposal}
state={timelockState}
/>
<Vote
proposal={proposal}
timelockConfig={timelockConfig}
proposal={proposal}
state={timelockState}
yeahVote={true}
/>
<Vote
timelockConfig={timelockConfig}
proposal={proposal}
state={timelockState}
yeahVote={false}
/>
</div>
</Col>