mirror of https://github.com/certusone/oyster.git
Merge pull request #39 from SebastianBor/main
Proposals: Add quick vote
This commit is contained in:
commit
eda4f82640
|
@ -1,3 +1,4 @@
|
|||
export * from './useUserAccounts';
|
||||
export * from './useAccountByMint';
|
||||
export * from './useTokenName';
|
||||
export * from './useThatState';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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}`,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 won’t 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 won’t 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',
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue