Add ability for appbar to display custom settings, and add add signers button to proposals, soon to be Edit Signers. Next up ,need to deal with why Sign doesnt work for non-Admin signers. It times out, no reason why, but will probably deal with after getting Add Signers to become Edit Signers.

This commit is contained in:
Dummy Tester 123 2021-02-27 18:57:09 -06:00
parent 1c2d1260e0
commit 941170be1d
11 changed files with 381 additions and 9 deletions

View File

@ -12,6 +12,7 @@ export const AppBar = (props: {
left?: JSX.Element; left?: JSX.Element;
right?: JSX.Element; right?: JSX.Element;
useWalletBadge?: boolean; useWalletBadge?: boolean;
additionalSettings?: JSX.Element;
}) => { }) => {
const { connected, wallet } = useWallet(); const { connected, wallet } = useWallet();
@ -19,7 +20,11 @@ export const AppBar = (props: {
<div className="App-Bar-right"> <div className="App-Bar-right">
{props.left} {props.left}
{connected ? ( {connected ? (
props.useWalletBadge ? <CurrentUserWalletBadge /> : <CurrentUserBadge /> props.useWalletBadge ? (
<CurrentUserWalletBadge />
) : (
<CurrentUserBadge />
)
) : ( ) : (
<ConnectButton <ConnectButton
type="text" type="text"
@ -31,7 +36,7 @@ export const AppBar = (props: {
<Popover <Popover
placement="topRight" placement="topRight"
title={LABELS.SETTINGS_TOOLTIP} title={LABELS.SETTINGS_TOOLTIP}
content={<Settings />} content={<Settings additionalSettings={props.additionalSettings} />}
trigger="click" trigger="click"
> >
<Button <Button

View File

@ -3,7 +3,11 @@ import { Button, Select } from 'antd';
import { useWallet } from '../../contexts/wallet'; import { useWallet } from '../../contexts/wallet';
import { ENDPOINTS, useConnectionConfig } from '../../contexts/connection'; import { ENDPOINTS, useConnectionConfig } from '../../contexts/connection';
export const Settings = () => { export const Settings = ({
additionalSettings,
}: {
additionalSettings?: JSX.Element;
}) => {
const { connected, disconnect } = useWallet(); const { connected, disconnect } = useWallet();
const { endpoint, setEndpoint } = useConnectionConfig(); const { endpoint, setEndpoint } = useConnectionConfig();
@ -27,6 +31,7 @@ export const Settings = () => {
Disconnect Disconnect
</Button> </Button>
)} )}
{additionalSettings}
</div> </div>
</> </>
); );

View File

@ -0,0 +1,98 @@
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 { addSignerInstruction } from '../models/addSigner';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const addSigner = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
adminAccount: PublicKey,
newSignatoryAccountOwner: PublicKey,
) => {
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
const newSignerAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt,
proposal.info.signatoryMint,
newSignatoryAccountOwner,
signers,
);
const [mintAuthority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
[],
adminAccount,
wallet.publicKey,
1,
);
signers.push(transferAuthority);
instructions.push(
addSignerInstruction(
newSignerAccount,
proposal.info.signatoryMint,
adminAccount,
proposal.info.adminValidation,
proposal.pubkey,
transferAuthority.publicKey,
mintAuthority,
),
);
notify({
message: 'Adding signer...',
description: 'Please wait...',
type: 'warn',
});
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: 'Signer added.',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -17,7 +17,7 @@ import {
} from '../models/timelock'; } from '../models/timelock';
const { sendTransaction } = contexts.Connection; const { sendTransaction } = contexts.Connection;
const { createMint, createTokenAccount, createUninitializedMint } = actions; const { createMint, createTokenAccount } = actions;
const { notify } = utils; const { notify } = utils;
export const createProposal = async ( export const createProposal = async (

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import './../../App.less'; import './../../App.less';
import { Menu } from 'antd'; import { Divider, Menu } from 'antd';
import { import {
PieChartOutlined, PieChartOutlined,
GithubOutlined, GithubOutlined,

View File

@ -0,0 +1,177 @@
import { ParsedAccount } from '@oyster/common';
import {
Button,
Modal,
Input,
Form,
Tag,
Progress,
Col,
Row,
Space,
} from 'antd';
import React, { useState } from 'react';
import { TimelockSet } from '../../models/timelock';
import { utils, contexts, hooks } from '@oyster/common';
import { addSigner } from '../../actions/addSigner';
import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
const { notify } = utils;
const { TextArea } = Input;
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
export default function AddSigners({
proposal,
}: {
proposal: ParsedAccount<TimelockSet>;
}) {
const wallet = useWallet();
const connection = useConnection();
const adminAccount = useAccountByMint(proposal.info.adminMint);
const [saving, setSaving] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [savePerc, setSavePerc] = useState(0);
const [failedSigners, setFailedSigners] = useState<string[]>([]);
const [form] = Form.useForm();
const onSubmit = async (values: {
signers: string;
failedSigners: string;
}) => {
const signers = values.signers.split(',').map(s => s.trim());
setSaving(true);
if (!adminAccount) {
notify({
message: 'Admin account is not defined',
type: 'error',
});
return;
}
if (signers.length == 0 || (signers.length == 1 && !signers[0])) {
notify({
message: 'Please enter at least one pub key.',
type: 'error',
});
return;
}
const failedSignersHold: string[] = [];
for (let i = 0; i < signers.length; i++) {
try {
await addSigner(
connection,
wallet.wallet,
proposal,
adminAccount.pubkey,
new PublicKey(signers[i]),
);
setSavePerc(Math.round(100 * ((i + 1) / signers.length)));
} catch (e) {
console.error(e);
failedSignersHold.push(signers[i]);
notify({
message: `Pub key ${signers[i]} 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.`,
type: 'error',
});
}
}
setFailedSigners(failedSignersHold);
setSaving(false);
setSavePerc(0);
setIsModalVisible(failedSignersHold.length > 0);
};
return (
<>
{adminAccount ? (
<Button
onClick={() => {
setIsModalVisible(true);
}}
>
{LABELS.ADD_SIGNERS}
</Button>
) : null}
<Modal
title={LABELS.ADD_SIGNERS}
visible={isModalVisible}
destroyOnClose={true}
onOk={form.submit}
zIndex={10000}
onCancel={() => {
if (!saving) setIsModalVisible(false);
}}
>
<Form
className={'signers-form'}
{...layout}
form={form}
onFinish={onSubmit}
name="control-hooks"
>
{!saving && (
<Form.Item
name="signers"
label={LABELS.SIGNERS}
rules={[{ required: true }]}
>
<TextArea
id="signers"
placeholder={LABELS.COMMA_SEPARATED_KEYS}
/>
</Form.Item>
)}
</Form>
{saving && <Progress percent={savePerc} status="active" />}
{!saving && failedSigners.length > 0 && (
<div
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'space-evenly',
alignItems: 'stretch',
display: 'flex',
}}
>
<Button
onClick={() => {
navigator.clipboard.writeText(failedSigners.join(','));
notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_CLIPBOARD,
type: 'success',
});
}}
>
{LABELS.COPY_FAILED_ADDRESSES_TO_CLIPBOARD}
</Button>
<br />
<Button
onClick={() => {
form.setFieldsValue({
signers: failedSigners.join(','),
});
notify({
message: LABELS.FAILED_SIGNERS_COPIED_TO_INPUT,
type: 'success',
});
}}
>
{LABELS.COPY_FAILED_ADDRESSES_TO_INPUT}
</Button>
</div>
)}
</Modal>
</>
);
}

View File

@ -21,7 +21,6 @@ export default function SignButton({
const sigAccount = useAccountByMint(proposal.info.signatoryMint); const sigAccount = useAccountByMint(proposal.info.signatoryMint);
return ( return (
<> <>
<br />
{sigAccount && sigAccount.info.amount.toNumber() === 0 && ( {sigAccount && sigAccount.info.amount.toNumber() === 0 && (
<Button disabled={true} type="primary"> <Button disabled={true} type="primary">
Signed Signed

View File

@ -19,4 +19,12 @@ export const LABELS = {
SIG_GIVEN: 'Sigs Given', SIG_GIVEN: 'Sigs Given',
VOTES_REQUIRED: "Votes Req'd", VOTES_REQUIRED: "Votes Req'd",
VOTES_CAST: 'Votes Cast', VOTES_CAST: 'Votes Cast',
ADMIN_PANEL: 'Admin Panel',
COPY_FAILED_ADDRESSES_TO_INPUT: 'Copy failed addresses to the input',
COPY_FAILED_ADDRESSES_TO_CLIPBOARD: 'Copy failed addresses to clipboard',
FAILED_SIGNERS_COPIED_TO_INPUT: 'Failed signers copied to input!',
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!',
COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
SIGNERS: 'Signers',
ADD_SIGNERS: 'Add Signers',
}; };

View File

@ -0,0 +1,62 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
/// [Requires Admin token]
/// Adds a signatory to the Timelock which means that this timelock can't leave Draft state until yet another signatory burns
/// their signatory token indicating they are satisfied with the instruction queue. They'll receive an signatory token
/// as a result of this call that they can burn later.
///
/// 0. `[writable]` Initialized new signatory account.
/// 1. `[writable]` Initialized Signatory mint account.
/// 2. `[writable]` Admin account.
/// 3. `[writable]` Admin validation account.
/// 4. `[]` Timelock set account.
/// 5. `[]` Transfer authority
/// 6. `[]` Timelock program mint authority
/// 7. `[]` Timelock program account.
/// 8. '[]` Token program id.
export const addSignerInstruction = (
signatoryAccount: PublicKey,
signatoryMintAccount: PublicKey,
adminAccount: PublicKey,
adminValidationAccount: PublicKey,
timelockSetAccount: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.AddSigner,
},
data,
);
const keys = [
{ pubkey: signatoryAccount, isSigner: true, isWritable: true },
{ pubkey: signatoryMintAccount, isSigner: false, isWritable: true },
{ pubkey: adminAccount, isSigner: false, isWritable: true },
{ pubkey: adminValidationAccount, isSigner: false, isWritable: true },
{ pubkey: timelockSetAccount, 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,
});
};

View File

@ -11,6 +11,7 @@ export const TRANSACTION_SLOTS = 10;
export enum TimelockInstruction { export enum TimelockInstruction {
InitTimelockSet = 1, InitTimelockSet = 1,
AddSigner = 2,
addCustomSingleSignerTransaction = 4, addCustomSingleSignerTransaction = 4,
Sign = 8, Sign = 8,
} }

View File

@ -1,4 +1,4 @@
import { Button, Col, Divider, Row, Space, Spin, Statistic } from 'antd'; import { Button, Col, Divider, Grid, Row, Space, Spin, Statistic } from 'antd';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { LABELS } from '../../constants'; import { LABELS } from '../../constants';
import { ParsedAccount } from '@oyster/common'; import { ParsedAccount } from '@oyster/common';
@ -17,9 +17,11 @@ import { MintInfo } from '@solana/spl-token';
import { InstructionCard } from '../../components/Proposal/InstructionCard'; 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';
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;
const { useBreakpoint } = Grid;
export const ProposalView = () => { export const ProposalView = () => {
const context = useProposals(); const context = useProposals();
@ -55,6 +57,7 @@ function InnerProposalView({
instructions: Record<string, ParsedAccount<TimelockTransaction>>; instructions: Record<string, ParsedAccount<TimelockTransaction>>;
}) { }) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint); const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const adminAccount = useAccountByMint(proposal.info.adminMint);
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);
@ -66,6 +69,7 @@ function InnerProposalView({
const [loading, setLoading] = useState(isUrl); const [loading, setLoading] = useState(isUrl);
const [failed, setFailed] = useState(false); const [failed, setFailed] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const breakpoint = useBreakpoint();
useMemo(() => { useMemo(() => {
if (loading) { if (loading) {
@ -119,7 +123,6 @@ function InnerProposalView({
<StateBadge proposal={proposal} /> <StateBadge proposal={proposal} />
</p> </p>
</Col> </Col>
<Col span={2}> </Col>
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
@ -151,7 +154,21 @@ function InnerProposalView({
} }
suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`} suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`}
/> />
{sigAccount && <SignButton proposal={proposal} />} <Space
style={{ marginTop: '10px' }}
direction={
breakpoint.lg || breakpoint.xl || breakpoint.xxl
? 'horizontal'
: 'vertical'
}
>
{adminAccount && adminAccount.info.amount.toNumber() === 1 && (
<AddSigners proposal={proposal} />
)}
{sigAccount && sigAccount.info.amount.toNumber() === 1 && (
<SignButton proposal={proposal} />
)}
</Space>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Statistic <Statistic