mirror of https://github.com/certusone/oyster.git
Add add votes button and commands
This commit is contained in:
parent
0c95a9ab6c
commit
c5ac500648
|
@ -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<TimelockSet>,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -38,7 +38,6 @@ export default function AddSigners({
|
||||||
failedSigners: string;
|
failedSigners: string;
|
||||||
}) => {
|
}) => {
|
||||||
const signers = values.signers.split(',').map(s => s.trim());
|
const signers = values.signers.split(',').map(s => s.trim());
|
||||||
setSaving(true);
|
|
||||||
if (!adminAccount) {
|
if (!adminAccount) {
|
||||||
notify({
|
notify({
|
||||||
message: LABELS.ADMIN_ACCOUNT_NOT_DEFINED,
|
message: LABELS.ADMIN_ACCOUNT_NOT_DEFINED,
|
||||||
|
@ -46,13 +45,15 @@ export default function AddSigners({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (signers.length == 0 || (signers.length == 1 && !signers[0])) {
|
if (!signers.find(s => s)) {
|
||||||
notify({
|
notify({
|
||||||
message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY,
|
message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
const failedSignersHold: string[] = [];
|
const failedSignersHold: string[] = [];
|
||||||
|
|
||||||
|
@ -79,6 +80,7 @@ export default function AddSigners({
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
setSavePerc(0);
|
setSavePerc(0);
|
||||||
setIsModalVisible(failedSignersHold.length > 0);
|
setIsModalVisible(failedSignersHold.length > 0);
|
||||||
|
if (failedSignersHold.length === 0) form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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<TimelockSet>;
|
||||||
|
}) {
|
||||||
|
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<any>([]);
|
||||||
|
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 ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LABELS.ADD_VOTES}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Modal
|
||||||
|
title={LABELS.ADD_VOTES}
|
||||||
|
visible={isModalVisible}
|
||||||
|
destroyOnClose={true}
|
||||||
|
onOk={form.submit}
|
||||||
|
zIndex={10000}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!saving) setIsModalVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
className={'voters-form'}
|
||||||
|
{...layout}
|
||||||
|
form={form}
|
||||||
|
onFinish={onSubmit}
|
||||||
|
name="control-hooks"
|
||||||
|
>
|
||||||
|
{!saving && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={LABELS.VOTE_MODE}
|
||||||
|
name="voteMode"
|
||||||
|
initialValue={LABELS.SINGLE}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<Radio.Group
|
||||||
|
value={layout}
|
||||||
|
onChange={e =>
|
||||||
|
setBulkModeVisible(e.target.value === LABELS.BULK)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Radio.Button value={LABELS.BULK}>{LABELS.BULK}</Radio.Button>
|
||||||
|
<Radio.Button value={LABELS.SINGLE}>
|
||||||
|
{LABELS.SINGLE}
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{!bulkModeVisible && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="singleVoter"
|
||||||
|
label={LABELS.SINGLE_VOTER}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<Input placeholder={LABELS.SINGLE_KEY} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="singleVoteCount"
|
||||||
|
label={LABELS.VOTE_COUNT}
|
||||||
|
initialValue={0}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{bulkModeVisible && (
|
||||||
|
<Form.Item
|
||||||
|
name="voters"
|
||||||
|
label={LABELS.BULK_VOTERS}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
id="voters"
|
||||||
|
placeholder={LABELS.COMMA_SEPARATED_KEYS_AND_VOTES}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
{saving && <Progress percent={savePerc} status="active" />}
|
||||||
|
|
||||||
|
{!saving && failedVoters.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(failedVoters.join(','));
|
||||||
|
notify({
|
||||||
|
message: LABELS.FAILED_SIGNERS_COPIED_TO_CLIPBOARD,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LABELS.COPY_FAILED_ADDRESSES_TO_CLIPBOARD}
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
voters: failedVoters.join(','),
|
||||||
|
});
|
||||||
|
notify({
|
||||||
|
message: LABELS.FAILED_SIGNERS_COPIED_TO_INPUT,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LABELS.COPY_FAILED_ADDRESSES_TO_INPUT}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
import { ParsedAccount } from '@oyster/common';
|
import { ParsedAccount } from '@oyster/common';
|
||||||
import { Badge, Tag } from 'antd';
|
import { Badge, Tag } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { STATE_COLOR, TimelockSet } from '../../models/timelock';
|
import {
|
||||||
|
STATE_COLOR,
|
||||||
|
TimelockSet,
|
||||||
|
TimelockStateStatus,
|
||||||
|
} from '../../models/timelock';
|
||||||
|
|
||||||
export function StateBadgeRibbon({
|
export function StateBadgeRibbon({
|
||||||
proposal,
|
proposal,
|
||||||
|
@ -26,5 +30,5 @@ export function StateBadge({
|
||||||
}) {
|
}) {
|
||||||
const status = proposal.info.state.status;
|
const status = proposal.info.state.status;
|
||||||
let color = STATE_COLOR[status];
|
let color = STATE_COLOR[status];
|
||||||
return <Tag color={color}>{status}</Tag>;
|
return <Tag color={color}>{TimelockStateStatus[status]}</Tag>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,28 @@ export const LABELS = {
|
||||||
SIGNERS: 'Signers',
|
SIGNERS: 'Signers',
|
||||||
ADD_SIGNERS: 'Add Signers',
|
ADD_SIGNERS: 'Add Signers',
|
||||||
ADMIN_ACCOUNT_NOT_DEFINED: 'Admin account is not defined',
|
ADMIN_ACCOUNT_NOT_DEFINED: 'Admin account is not defined',
|
||||||
|
SIG_ACCOUNT_NOT_DEFINED: 'Signature account is not defined',
|
||||||
ENTER_AT_LEAST_ONE_PUB_KEY: 'Please enter at least one pub key.',
|
ENTER_AT_LEAST_ONE_PUB_KEY: 'Please enter at least one pub key.',
|
||||||
PUB_KEY_FAILED:
|
PUB_KEY_FAILED:
|
||||||
" Pub key failed. Please check your inspector tab for more information. We'll continue onward and add this to a list for you to re-upload in a later save.",
|
" Pub key failed. Please check your inspector tab for more information. We'll continue onward and add this to a list for you to re-upload in a later save.",
|
||||||
ADD: 'Add',
|
ADD: 'Add',
|
||||||
REMOVE: 'Remove',
|
REMOVE: 'Remove',
|
||||||
ADDING_OR_REMOVING: 'Type',
|
ADDING_OR_REMOVING: 'Type',
|
||||||
|
ADDING_VOTES_TO_VOTER: 'Adding votes to voter',
|
||||||
|
PLEASE_WAIT: 'Please wait...',
|
||||||
|
VOTES_ADDED: 'Votes added.',
|
||||||
|
NEW_VOTED_ACCOUNT_ADDED: 'New vote account added.',
|
||||||
|
ADDING_NEW_VOTE_ACCOUNT: 'Adding new vote account...',
|
||||||
|
TRANSACTION: 'Transaction - ',
|
||||||
|
CANT_GIVE_ZERO_VOTES: "Can't give zero votes to a user!",
|
||||||
|
BULK_VOTERS: 'Voters',
|
||||||
|
COMMA_SEPARATED_KEYS_AND_VOTES:
|
||||||
|
'base58 pubkey, vote count, base58 pubkey, vote count, ...',
|
||||||
|
SINGLE_VOTER: 'Single Voter',
|
||||||
|
VOTE_COUNT: 'Vote Amount',
|
||||||
|
SINGLE_KEY: 'base58 pubkey',
|
||||||
|
VOTE_MODE: 'Vote Mode',
|
||||||
|
BULK: 'Bulk',
|
||||||
|
SINGLE: 'Single',
|
||||||
|
ADD_VOTES: 'Add Votes',
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const addCustomSingleSignerTransactionInstruction = (
|
||||||
|
|
||||||
dataLayout.encode(
|
dataLayout.encode(
|
||||||
{
|
{
|
||||||
instruction: TimelockInstruction.addCustomSingleSignerTransaction,
|
instruction: TimelockInstruction.AddCustomSingleSignerTransaction,
|
||||||
slot: new BN(slot),
|
slot: new BN(slot),
|
||||||
instructions: instructionAsBytes,
|
instructions: instructionAsBytes,
|
||||||
position: position,
|
position: position,
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
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 Signatory token]
|
||||||
|
/// Mints voting tokens for a destination account to be used during the voting process.
|
||||||
|
///
|
||||||
|
/// 0. `[writable]` Timelock set account.
|
||||||
|
/// 1. `[writable]` Initialized Voting account.
|
||||||
|
/// 2. `[writable]` Voting mint account.
|
||||||
|
/// 3. `[writable]` Signatory account
|
||||||
|
/// 4. `[writable]` Signatory validation account.
|
||||||
|
/// 5. `[]` Transfer authority
|
||||||
|
/// 6. `[]` Timelock program mint authority
|
||||||
|
/// 7. `[]` Timelock program account pub key.
|
||||||
|
/// 8. `[]` Token program account.
|
||||||
|
export const mintVotingTokensInstruction = (
|
||||||
|
timelockSetAccount: PublicKey,
|
||||||
|
votingAccount: PublicKey,
|
||||||
|
votingMint: PublicKey,
|
||||||
|
signatoryAccount: PublicKey,
|
||||||
|
signatoryValidationAccount: 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.MintVotingTokens,
|
||||||
|
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: signatoryAccount, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: signatoryValidationAccount, 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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -13,8 +13,9 @@ export enum TimelockInstruction {
|
||||||
InitTimelockSet = 1,
|
InitTimelockSet = 1,
|
||||||
AddSigner = 2,
|
AddSigner = 2,
|
||||||
RemoveSigner = 3,
|
RemoveSigner = 3,
|
||||||
addCustomSingleSignerTransaction = 4,
|
AddCustomSingleSignerTransaction = 4,
|
||||||
Sign = 8,
|
Sign = 8,
|
||||||
|
MintVotingTokens = 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelockConfig {
|
export interface TimelockConfig {
|
||||||
|
@ -55,11 +56,11 @@ export enum TimelockStateStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STATE_COLOR: Record<string, string> = {
|
export const STATE_COLOR: Record<string, string> = {
|
||||||
Draft: 'orange',
|
[TimelockStateStatus.Draft]: 'orange',
|
||||||
Voting: 'blue',
|
[TimelockStateStatus.Voting]: 'blue',
|
||||||
Executing: 'green',
|
[TimelockStateStatus.Executing]: 'green',
|
||||||
Completed: 'purple',
|
[TimelockStateStatus.Completed]: 'purple',
|
||||||
Deleted: 'gray',
|
[TimelockStateStatus.Deleted]: 'gray',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TimelockState {
|
export interface TimelockState {
|
||||||
|
@ -154,7 +155,7 @@ export const TimelockSetParser = (
|
||||||
adminValidation: data.adminValidation,
|
adminValidation: data.adminValidation,
|
||||||
votingValidation: data.votingValidation,
|
votingValidation: data.votingValidation,
|
||||||
state: {
|
state: {
|
||||||
status: TimelockStateStatus[data.timelockStateStatus],
|
status: data.timelockStateStatus,
|
||||||
totalVotingTokensMinted: data.totalVotingTokensMinted,
|
totalVotingTokensMinted: data.totalVotingTokensMinted,
|
||||||
totalSigningTokensMinted: data.totalSigningTokensMinted,
|
totalSigningTokensMinted: data.totalSigningTokensMinted,
|
||||||
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
|
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
ConsensusAlgorithm,
|
ConsensusAlgorithm,
|
||||||
INSTRUCTION_LIMIT,
|
INSTRUCTION_LIMIT,
|
||||||
TimelockSet,
|
TimelockSet,
|
||||||
|
TimelockStateStatus,
|
||||||
TimelockTransaction,
|
TimelockTransaction,
|
||||||
} from '../../models/timelock';
|
} from '../../models/timelock';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
@ -18,6 +19,7 @@ import { InstructionCard } from '../../components/Proposal/InstructionCard';
|
||||||
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
|
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';
|
||||||
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;
|
||||||
|
@ -162,12 +164,16 @@ function InnerProposalView({
|
||||||
: 'vertical'
|
: 'vertical'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{adminAccount && adminAccount.info.amount.toNumber() === 1 && (
|
{adminAccount &&
|
||||||
<AddSigners proposal={proposal} />
|
adminAccount.info.amount.toNumber() === 1 &&
|
||||||
)}
|
proposal.info.state.status === TimelockStateStatus.Draft && (
|
||||||
{sigAccount && sigAccount.info.amount.toNumber() === 1 && (
|
<AddSigners proposal={proposal} />
|
||||||
<SignButton proposal={proposal} />
|
)}
|
||||||
)}
|
{sigAccount &&
|
||||||
|
sigAccount.info.amount.toNumber() === 1 &&
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Draft && (
|
||||||
|
<SignButton proposal={proposal} />
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
@ -179,6 +185,20 @@ function InnerProposalView({
|
||||||
}
|
}
|
||||||
suffix={`/ ${proposal.info.state.totalVotingTokensMinted}`}
|
suffix={`/ ${proposal.info.state.totalVotingTokensMinted}`}
|
||||||
/>
|
/>
|
||||||
|
<Space
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
direction={
|
||||||
|
breakpoint.lg || breakpoint.xl || breakpoint.xxl
|
||||||
|
? 'horizontal'
|
||||||
|
: 'vertical'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sigAccount &&
|
||||||
|
sigAccount.info.amount.toNumber() === 1 &&
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Draft && (
|
||||||
|
<AddVotes proposal={proposal} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
|
@ -218,11 +238,15 @@ function InnerProposalView({
|
||||||
|
|
||||||
function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
|
function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
|
||||||
if (proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.Majority) {
|
if (proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.Majority) {
|
||||||
return proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5;
|
return Math.ceil(
|
||||||
|
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
|
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
|
||||||
) {
|
) {
|
||||||
return proposal.info.state.totalVotingTokensMinted.toNumber() * 0.6;
|
return Math.ceil(
|
||||||
|
proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
|
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
|
||||||
) {
|
) {
|
||||||
|
|
Loading…
Reference in New Issue