From c5ac50064872a3d50005e02d3549a918344ecfb0 Mon Sep 17 00:00:00 2001 From: Dummy Tester 123 Date: Mon, 1 Mar 2021 13:19:55 -0600 Subject: [PATCH] Add add votes button and commands --- .../proposals/src/actions/mintVotingTokens.ts | 133 ++++++++++ .../src/components/Proposal/AddSigners.tsx | 6 +- .../src/components/Proposal/AddVotes.tsx | 246 ++++++++++++++++++ .../src/components/Proposal/StateBadge.tsx | 8 +- packages/proposals/src/constants/labels.ts | 18 ++ .../addCustomSingleSignerTransaction.ts | 2 +- .../proposals/src/models/mintVotingTokens.ts | 69 +++++ packages/proposals/src/models/timelock.ts | 15 +- .../proposals/src/views/proposal/index.tsx | 40 ++- 9 files changed, 517 insertions(+), 20 deletions(-) create mode 100644 packages/proposals/src/actions/mintVotingTokens.ts create mode 100644 packages/proposals/src/components/Proposal/AddVotes.tsx create mode 100644 packages/proposals/src/models/mintVotingTokens.ts diff --git a/packages/proposals/src/actions/mintVotingTokens.ts b/packages/proposals/src/actions/mintVotingTokens.ts new file mode 100644 index 0000000..222246b --- /dev/null +++ b/packages/proposals/src/actions/mintVotingTokens.ts @@ -0,0 +1,133 @@ +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'; +const { createTokenAccount } = actions; +const { sendTransaction } = contexts.Connection; +const { notify } = utils; +const { approve } = models; + +export const mintVotingTokens = async ( + connection: Connection, + wallet: any, + proposal: ParsedAccount, + signatoryAccount: PublicKey, + newVotingAccountOwner: PublicKey, + existingVoteAccount: PublicKey | undefined, + votingTokenAmount: number, +) => { + const PROGRAM_IDS = utils.programIds(); + + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + if (!existingVoteAccount) { + existingVoteAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + proposal.info.votingMint, + newVotingAccountOwner, + signers, + ); + + notify({ + message: LABELS.ADDING_NEW_VOTE_ACCOUNT, + description: LABELS.PLEASE_WAIT, + type: 'warn', + }); + + try { + let tx = await sendTransaction( + connection, + wallet, + instructions, + signers, + true, + ); + + notify({ + message: LABELS.NEW_VOTED_ACCOUNT_ADDED, + type: 'success', + description: LABELS.TRANSACTION + ` ${tx}`, + }); + } catch (ex) { + console.error(ex); + throw new Error(); + } + + signers = []; + instructions = []; + } + + const [mintAuthority] = await PublicKey.findProgramAddress( + [PROGRAM_IDS.timelock.programAccountId.toBuffer()], + PROGRAM_IDS.timelock.programId, + ); + + const transferAuthority = approve( + instructions, + [], + signatoryAccount, + wallet.publicKey, + 1, + ); + + signers.push(transferAuthority); + + instructions.push( + mintVotingTokensInstruction( + proposal.pubkey, + existingVoteAccount, + proposal.info.votingMint, + signatoryAccount, + proposal.info.signatoryValidation, + transferAuthority.publicKey, + mintAuthority, + votingTokenAmount, + ), + ); + + notify({ + message: LABELS.ADDING_VOTES_TO_VOTER, + description: LABELS.PLEASE_WAIT, + type: 'warn', + }); + + try { + let tx = await sendTransaction( + connection, + wallet, + instructions, + signers, + true, + ); + + notify({ + message: LABELS.VOTES_ADDED, + type: 'success', + description: LABELS.TRANSACTION + ` ${tx}`, + }); + } catch (ex) { + console.error(ex); + throw new Error(); + } +}; diff --git a/packages/proposals/src/components/Proposal/AddSigners.tsx b/packages/proposals/src/components/Proposal/AddSigners.tsx index 6ade436..0a699e9 100644 --- a/packages/proposals/src/components/Proposal/AddSigners.tsx +++ b/packages/proposals/src/components/Proposal/AddSigners.tsx @@ -38,7 +38,6 @@ export default function AddSigners({ failedSigners: string; }) => { const signers = values.signers.split(',').map(s => s.trim()); - setSaving(true); if (!adminAccount) { notify({ message: LABELS.ADMIN_ACCOUNT_NOT_DEFINED, @@ -46,13 +45,15 @@ export default function AddSigners({ }); return; } - if (signers.length == 0 || (signers.length == 1 && !signers[0])) { + if (!signers.find(s => s)) { notify({ message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, type: 'error', }); + return; } + setSaving(true); const failedSignersHold: string[] = []; @@ -79,6 +80,7 @@ export default function AddSigners({ setSaving(false); setSavePerc(0); setIsModalVisible(failedSignersHold.length > 0); + if (failedSignersHold.length === 0) form.resetFields(); }; return ( diff --git a/packages/proposals/src/components/Proposal/AddVotes.tsx b/packages/proposals/src/components/Proposal/AddVotes.tsx new file mode 100644 index 0000000..1ca4fa7 --- /dev/null +++ b/packages/proposals/src/components/Proposal/AddVotes.tsx @@ -0,0 +1,246 @@ +import { ParsedAccount } from '@oyster/common'; +import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd'; +import React, { useState } from 'react'; +import { TimelockSet } from '../../models/timelock'; +import { utils, contexts, hooks } from '@oyster/common'; +import { PublicKey } from '@solana/web3.js'; +import { LABELS } from '../../constants'; +import { mintVotingTokens } from '../../actions/mintVotingTokens'; + +const { notify } = utils; +const { TextArea } = Input; +const { useWallet } = contexts.Wallet; +const { useConnection } = contexts.Connection; +const { useAccountByMint } = hooks; +const { deserializeAccount } = contexts.Accounts; + +const layout = { + labelCol: { span: 5 }, + wrapperCol: { span: 19 }, +}; + +export default function AddVotes({ + proposal, +}: { + proposal: ParsedAccount; +}) { + const PROGRAM_IDS = utils.programIds(); + const wallet = useWallet(); + const connection = useConnection(); + const sigAccount = useAccountByMint(proposal.info.signatoryMint); + const [saving, setSaving] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [bulkModeVisible, setBulkModeVisible] = useState(false); + const [savePerc, setSavePerc] = useState(0); + const [failedVoters, setFailedVoters] = useState([]); + const [form] = Form.useForm(); + + const onSubmit = async (values: { + voters: string; + failedVoters: string; + singleVoter: string; + singleVoteCount: number; + }) => { + const { singleVoter, singleVoteCount } = values; + const votersAndCounts = values.voters + ? values.voters.split(',').map(s => s.trim()) + : []; + const voters: any[] = []; + votersAndCounts.forEach((value: string, index: number) => { + if (index % 2 == 0) voters.push([value, 0]); + else voters[voters.length - 1][1] = parseInt(value); + }); + console.log('Voters', votersAndCounts); + if (singleVoter) voters.push([singleVoter, singleVoteCount]); + + if (!sigAccount) { + notify({ + message: LABELS.SIG_ACCOUNT_NOT_DEFINED, + type: 'error', + }); + return; + } + if (!voters.find(v => v[0])) { + notify({ + message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, + type: 'error', + }); + return; + } + setSaving(true); + + if (voters.find(v => v[1] === 0)) { + notify({ + message: LABELS.CANT_GIVE_ZERO_VOTES, + type: 'error', + }); + setSaving(false); + return; + } + + const failedVotersHold: any[] = []; + + for (let i = 0; i < voters.length; i++) { + try { + const tokenAccounts = await connection.getTokenAccountsByOwner( + new PublicKey(voters[i][0]), + { + programId: PROGRAM_IDS.token, + }, + ); + const specificToThisMint = tokenAccounts.value.find( + a => + deserializeAccount(a.account.data).mint.toBase58() === + proposal.info.votingMint.toBase58(), + ); + await mintVotingTokens( + connection, + wallet.wallet, + proposal, + sigAccount.pubkey, + new PublicKey(voters[i][0]), + specificToThisMint?.pubkey, + voters[i][1], + ); + setSavePerc(Math.round(100 * ((i + 1) / voters.length))); + } catch (e) { + console.error(e); + failedVotersHold.push(voters[i]); + notify({ + message: voters[i][0] + LABELS.PUB_KEY_FAILED, + type: 'error', + }); + } + } + setFailedVoters(failedVotersHold); + setSaving(false); + setSavePerc(0); + setIsModalVisible(failedVotersHold.length > 0); + if (failedVotersHold.length === 0) form.resetFields(); + }; + + return ( + <> + {sigAccount ? ( + + ) : null} + { + if (!saving) setIsModalVisible(false); + }} + > +
+ {!saving && ( + <> + + + setBulkModeVisible(e.target.value === LABELS.BULK) + } + > + {LABELS.BULK} + + {LABELS.SINGLE} + + + + {!bulkModeVisible && ( + <> + + + + + + + + )} + {bulkModeVisible && ( + +