Massive wip commit - added form for creating proposals, and working on displaying transactions

This commit is contained in:
Dummy Tester 123 2021-02-24 19:56:18 -06:00
parent 5cfd98264c
commit d4bf02c790
12 changed files with 400 additions and 67 deletions

View File

@ -69,9 +69,9 @@ export const PROGRAM_IDS = [
name: 'testnet', name: 'testnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'52goe5j8e25JWzcH6WnAjZ8bWfp4KwxPZsaxQ4fK1Xwx', '32siiZ12jH2i7BM2wtNuTDw2Hs5nBjVQuBh4P8cTSH3i',
), ),
programId: new PublicKey('5n4ciqDUt5MwRMFsViBXfQLGfqfg9mSZqgaKLu8zyrgw'), programId: new PublicKey('5KrVJvesyjdMy6Vq5wfuPSMdw7vWuUvtbHG98wBsEkX6'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'), pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
name: 'devnet', name: 'devnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'H59qsZj8wwA9h45UTbVwuYePcXXhKeeAtab3ujdQoqRc', 'H4yVRsmzbbE9fMUREPQWsu7auiYgE398iF5rq5nHLdq4',
), ),
programId: new PublicKey('DCPZ3VHspU2LGSHs2gaqUXYQykZ9C4iFX68KqATjBcLG'), programId: new PublicKey('2NKCKpE1kNhY3Qr3VGikPQtiENTjSEFsJN6NyAUpoFaD'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
@ -111,9 +111,9 @@ export const PROGRAM_IDS = [
name: 'localnet', name: 'localnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'9gBhDCCKV7KELLFRY8sAJZXqDmvUfmNzFzpB2b4FUVVr', 'GLmfd9F4bEY43Sg35RhvMN3N4uf5MvnTbX1bkKxVJ958',
), ),
programId: new PublicKey('9iAeqqppjn7g1Jn8o2cQCqU5aQVV3h4q9bbWdKRbeC2w'), programId: new PublicKey('Dj4W4ZEs5ATmAjx7pRt2gSTCgPHcjYig6KXZ4vBhPXrr'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

View File

@ -5,7 +5,7 @@ import {
SystemProgram, SystemProgram,
TransactionInstruction, TransactionInstruction,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { contexts, utils, actions, ParsedAccount } from '@oyster/common'; import { contexts, utils, models, ParsedAccount } from '@oyster/common';
import { import {
CustomSingleSignerTimelockTransactionLayout, CustomSingleSignerTimelockTransactionLayout,
@ -15,6 +15,7 @@ import { addCustomSingleSignerTransactionInstruction } from '../models/addCustom
const { sendTransaction } = contexts.Connection; const { sendTransaction } = contexts.Connection;
const { notify } = utils; const { notify } = utils;
const { approve } = models;
export const addCustomSingleSignerTransaction = async ( export const addCustomSingleSignerTransaction = async (
connection: Connection, connection: Connection,
@ -49,12 +50,22 @@ export const addCustomSingleSignerTransaction = async (
instructions.push(uninitializedTxnInstruction); instructions.push(uninitializedTxnInstruction);
const transferAuthority = approve(
instructions,
[],
sigAccount,
wallet.publicKey,
1,
);
signers.push(transferAuthority);
instructions.push( instructions.push(
addCustomSingleSignerTransactionInstruction( addCustomSingleSignerTransactionInstruction(
txnKey.publicKey, txnKey.publicKey,
proposal.pubkey, proposal.pubkey,
sigAccount, sigAccount,
proposal.info.signatoryValidation, proposal.info.signatoryValidation,
transferAuthority.publicKey,
authority, authority,
'123', '123',
'12345', '12345',

View File

@ -20,7 +20,15 @@ const { sendTransaction } = contexts.Connection;
const { createMint, createTokenAccount, createUninitializedMint } = actions; const { createMint, createTokenAccount, createUninitializedMint } = actions;
const { notify } = utils; const { notify } = utils;
export const createProposal = async (connection: Connection, wallet: any) => { export const createProposal = async (
connection: Connection,
wallet: any,
name: string,
description: string,
timelockType: TimelockType,
consensusAlgorithm: ConsensusAlgorithm,
executionType: ExecutionType,
): Promise<Account> => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
let signers: Account[] = []; let signers: Account[] = [];
@ -79,12 +87,12 @@ export const createProposal = async (connection: Connection, wallet: any) => {
sigDestinationAccount, sigDestinationAccount,
authority, authority,
{ {
timelockType: TimelockType.CustomSingleSignerV1, timelockType,
consensusAlgorithm: ConsensusAlgorithm.Majority, consensusAlgorithm,
executionType: ExecutionType.AllOrNothing, executionType,
}, },
'https://gist.github.com/dummytester123/bd3567f80e13a27b02a2e0fb891ecab1', description,
'Name', name,
), ),
); );
@ -108,6 +116,8 @@ export const createProposal = async (connection: Connection, wallet: any) => {
type: 'success', type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
return timelockSetKey;
} catch (ex) { } catch (ex) {
console.error(ex); console.error(ex);
throw new Error(); throw new Error();
@ -209,7 +219,7 @@ async function createValidationAccountsAndMints(
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
adminMint, adminMint,
PROGRAM_IDS.timelock.programAccountId, authority,
signers, signers,
); );
@ -218,7 +228,7 @@ async function createValidationAccountsAndMints(
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
sigMint, sigMint,
PROGRAM_IDS.timelock.programAccountId, authority,
signers, signers,
); );
@ -227,7 +237,7 @@ async function createValidationAccountsAndMints(
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
voteMint, voteMint,
PROGRAM_IDS.timelock.programAccountId, authority,
signers, signers,
); );

View File

@ -6,6 +6,8 @@ import {
GithubOutlined, GithubOutlined,
HomeOutlined, HomeOutlined,
ForkOutlined, ForkOutlined,
BulbOutlined,
UploadOutlined,
// LineChartOutlined // LineChartOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
@ -15,6 +17,7 @@ import { Link, useLocation } from 'react-router-dom';
import { LABELS } from '../../constants'; import { LABELS } from '../../constants';
import config from './../../../package.json'; import config from './../../../package.json';
import { contexts, components } from '@oyster/common'; import { contexts, components } from '@oyster/common';
import { NewFormMenuItem } from '../../views/proposal/new';
const { AppBar } = components; const { AppBar } = components;
const { useConnectionConfig } = contexts.Connection; const { useConnectionConfig } = contexts.Connection;
@ -24,6 +27,7 @@ export const AppLayout = React.memo((props: any) => {
const location = useLocation(); const location = useLocation();
const paths: { [key: string]: string } = { const paths: { [key: string]: string } = {
'/': '1',
'/dashboard': '2', '/dashboard': '2',
}; };
@ -62,7 +66,7 @@ export const AppLayout = React.memo((props: any) => {
defaultSelectedKeys={[defaultKey]} defaultSelectedKeys={[defaultKey]}
mode="inline" mode="inline"
> >
<Menu.Item key="1" icon={<HomeOutlined />}> <Menu.Item key="1" icon={<BulbOutlined />}>
<Link <Link
to={{ to={{
pathname: '/', pathname: '/',
@ -71,7 +75,10 @@ export const AppLayout = React.memo((props: any) => {
{LABELS.MENU_HOME} {LABELS.MENU_HOME}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="2" icon={<PieChartOutlined />}> <Menu.Item key="2" icon={<UploadOutlined />}>
<NewFormMenuItem />
</Menu.Item>
<Menu.Item key="3" icon={<PieChartOutlined />}>
<Link <Link
to={{ to={{
pathname: '/dashboard', pathname: '/dashboard',

View File

@ -4,7 +4,8 @@ export const LABELS = {
'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.', 'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.',
FOOTER: FOOTER:
'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.', 'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.',
MENU_HOME: 'Home', MENU_HOME: 'Proposals',
NEW_PROPOSAL: 'New Proposal',
MENU_DASHBOARD: 'Dashboard', MENU_DASHBOARD: 'Dashboard',
APP_TITLE: 'Oyster Proposals', APP_TITLE: 'Oyster Proposals',
CONNECT_BUTTON: 'Connect', CONNECT_BUTTON: 'Connect',

View File

@ -9,18 +9,21 @@ import { useMemo } from 'react';
import { contexts, utils, ParsedAccount } from '@oyster/common'; import { contexts, utils, ParsedAccount } from '@oyster/common';
import { import {
CustomSingleSignerTimelockTransaction,
CustomSingleSignerTimelockTransactionLayout,
CustomSingleSignerTimelockTransactionParser,
TimelockSet, TimelockSet,
TimelockSetLayout, TimelockSetLayout,
TimelockSetParser, TimelockSetParser,
Transaction,
} from '../models/timelock'; } from '../models/timelock';
const { useWallet } = contexts.Wallet;
const { useConnectionConfig } = contexts.Connection; const { useConnectionConfig } = contexts.Connection;
const { cache } = contexts.Accounts; const { cache } = contexts.Accounts;
export interface ProposalsContextState { export interface ProposalsContextState {
proposals: Record<string, ParsedAccount<TimelockSet>>; proposals: Record<string, ParsedAccount<TimelockSet>>;
transactions: Record<string, ParsedAccount<Transaction>>;
} }
export const ProposalsContext = React.createContext<ProposalsContextState | null>( export const ProposalsContext = React.createContext<ProposalsContextState | null>(
@ -34,11 +37,16 @@ export default function ProposalsProvider({ children = null as any }) {
]); ]);
const [proposals, setProposals] = useState({}); const [proposals, setProposals] = useState({});
const [transactions, setTransactions] = useState({});
useSetupProposalsCache({ connection, setProposals }); useSetupProposalsCache({
connection,
setProposals,
setTransactions,
});
return ( return (
<ProposalsContext.Provider value={{ proposals }}> <ProposalsContext.Provider value={{ proposals, transactions }}>
{children} {children}
</ProposalsContext.Provider> </ProposalsContext.Provider>
); );
@ -47,48 +55,72 @@ export default function ProposalsProvider({ children = null as any }) {
function useSetupProposalsCache({ function useSetupProposalsCache({
connection, connection,
setProposals, setProposals,
setTransactions,
}: { }: {
connection: Connection; connection: Connection;
setProposals: React.Dispatch<React.SetStateAction<{}>>; setProposals: React.Dispatch<React.SetStateAction<{}>>;
setTransactions: React.Dispatch<React.SetStateAction<{}>>;
}) { }) {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
useEffect(() => { useEffect(() => {
const queryProposals = async () => { const query = async () => {
const programAccounts = await connection.getProgramAccounts( const programAccounts = await connection.getProgramAccounts(
PROGRAM_IDS.timelock.programId, PROGRAM_IDS.timelock.programId,
); );
return programAccounts; return programAccounts;
}; };
Promise.all([queryProposals()]).then( Promise.all([query()]).then((all: PublicKeyAndAccount<Buffer>[][]) => {
(all: PublicKeyAndAccount<Buffer>[][]) => {
const newProposals: Record<string, ParsedAccount<TimelockSet>> = {}; const newProposals: Record<string, ParsedAccount<TimelockSet>> = {};
const newTransactions: Record<string, ParsedAccount<Transaction>> = {};
all[0].forEach(a => { all[0].forEach(a => {
if (a.account.data.length === TimelockSetLayout.span) { if (a.account.data.length === TimelockSetLayout.span) {
cache.add(a.pubkey, a.account, TimelockSetParser); cache.add(a.pubkey, a.account, TimelockSetParser);
const cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>; const cached = cache.get(a.pubkey) as ParsedAccount<TimelockSet>;
console.log('Got', a.pubkey.toBase58());
newProposals[a.pubkey.toBase58()] = cached; newProposals[a.pubkey.toBase58()] = cached;
} }
if (
a.account.data.length ===
CustomSingleSignerTimelockTransactionLayout.span
) {
cache.add(
a.pubkey,
a.account,
CustomSingleSignerTimelockTransactionParser,
);
const cached = cache.get(a.pubkey) as ParsedAccount<Transaction>;
newTransactions[a.pubkey.toBase58()] = cached;
}
}); });
setProposals(newProposals); setProposals(newProposals);
}, });
);
const subID = connection.onProgramAccountChange( const subID = connection.onProgramAccountChange(
PROGRAM_IDS.timelock.programId, PROGRAM_IDS.timelock.programId,
async (info: KeyedAccountInfo) => { async (info: KeyedAccountInfo) => {
if (info.accountInfo.data.length === TimelockSetLayout.span) { [
cache.add(info.accountId, info.accountInfo, TimelockSetParser); [TimelockSetLayout.span, TimelockSetParser, setProposals],
const cached = cache.get( [
info.accountId, CustomSingleSignerTimelockTransactionLayout.span,
) as ParsedAccount<TimelockSet>; CustomSingleSignerTimelockTransactionParser,
setProposals(proposals => ({ setTransactions,
...proposals, ],
].forEach(arr => {
const [span, parser, setter] = arr;
if (info.accountInfo.data.length === span) {
cache.add(info.accountId, info.accountInfo, parser);
const cached =
span === TimelockSetLayout.span
? (cache.get(info.accountId) as ParsedAccount<TimelockSet>)
: (cache.get(info.accountId) as ParsedAccount<Transaction>);
setter((obj: any) => ({
...obj,
[typeof info.accountId === 'string' [typeof info.accountId === 'string'
? info.accountId ? info.accountId
: info.accountId.toBase58()]: cached, : info.accountId.toBase58()]: cached,
})); }));
} }
});
}, },
'singleGossip', 'singleGossip',
); );

View File

@ -1,17 +1,10 @@
import { import { PublicKey, TransactionInstruction } from '@solana/web3.js';
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common'; import { utils } from '@oyster/common';
import * as Layout from '../utils/layout'; import * as Layout from '../utils/layout';
import * as BufferLayout from 'buffer-layout'; import * as BufferLayout from 'buffer-layout';
import { import {
DESC_SIZE,
INSTRUCTION_LIMIT, INSTRUCTION_LIMIT,
NAME_SIZE,
TimelockConfig,
TimelockInstruction, TimelockInstruction,
TRANSACTION_SLOTS, TRANSACTION_SLOTS,
} from './timelock'; } from './timelock';
@ -27,13 +20,15 @@ import { toUTF8Array } from '@oyster/common/dist/lib/utils';
/// 1. `[writable]` Timelock set account. /// 1. `[writable]` Timelock set account.
/// 2. `[writable]` Signatory account /// 2. `[writable]` Signatory account
/// 3. `[writable]` Signatory validation account. /// 3. `[writable]` Signatory validation account.
/// 4. `[]` Timelock program account. /// 4. `[]` Transfer authority
/// 5. `[]` Token program account. /// 5. `[]` Timelock program account.
/// 6. `[]` Token program account.
export const addCustomSingleSignerTransactionInstruction = ( export const addCustomSingleSignerTransactionInstruction = (
timelockTransactionAccount: PublicKey, timelockTransactionAccount: PublicKey,
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
signatoryAccount: PublicKey, signatoryAccount: PublicKey,
signatoryValidationAccount: PublicKey, signatoryValidationAccount: PublicKey,
transferAuthority: PublicKey,
authority: PublicKey, authority: PublicKey,
slot: string, slot: string,
instruction: string, instruction: string,
@ -82,6 +77,7 @@ export const addCustomSingleSignerTransactionInstruction = (
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true }, { pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryAccount, isSigner: false, isWritable: true }, { pubkey: signatoryAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryValidationAccount, isSigner: false, isWritable: true }, { pubkey: signatoryValidationAccount, isSigner: false, isWritable: true },
{ pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false }, { pubkey: authority, isSigner: false, isWritable: false },
{ {
pubkey: PROGRAM_IDS.timelock.programAccountId, pubkey: PROGRAM_IDS.timelock.programAccountId,
@ -90,7 +86,7 @@ export const addCustomSingleSignerTransactionInstruction = (
}, },
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false }, { pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
]; ];
console.log('data', data);
return new TransactionInstruction({ return new TransactionInstruction({
keys, keys,
programId: PROGRAM_IDS.timelock.programId, programId: PROGRAM_IDS.timelock.programId,

View File

@ -169,6 +169,31 @@ export const TimelockSetParser = (
return details; return details;
}; };
export const CustomSingleSignerTimelockTransactionParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = CustomSingleSignerTimelockTransactionLayout.decode(buffer);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: {
version: data.version,
slot: data.slot,
instruction: utils
.fromUTF8Array(data.instruction)
.replaceAll('\u0000', ''),
authorityKey: data.authorityKey,
},
};
return details;
};
export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[ [
BufferLayout.u8('version'), BufferLayout.u8('version'),
@ -177,12 +202,14 @@ export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.St
Layout.publicKey('authorityKey'), Layout.publicKey('authorityKey'),
], ],
); );
export interface CustomSingleSignerTimelockTransaction {
export interface Transaction {
version: number; version: number;
slot: BN; slot: BN;
instruction: string; instruction: string;
}
export interface CustomSingleSignerTimelockTransaction extends Transaction {
authorityKey: PublicKey; authorityKey: PublicKey;
} }

View File

@ -5,7 +5,12 @@ import { contexts, hooks, ParsedAccount } from '@oyster/common';
import './style.less'; import './style.less';
import { createProposal } from '../../actions/createProposal'; import { createProposal } from '../../actions/createProposal';
import { useProposals } from '../../contexts/proposals'; import { useProposals } from '../../contexts/proposals';
import { TimelockSet } from '../../models/timelock'; import {
ConsensusAlgorithm,
ExecutionType,
TimelockSet,
TimelockType,
} from '../../models/timelock';
import { Connection } from '@solana/web3.js'; import { Connection } from '@solana/web3.js';
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction'; import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
@ -23,7 +28,19 @@ export const DashboardView = () => {
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
<Row gutter={GUTTER} className="home-info-row"> <Row gutter={GUTTER} className="home-info-row">
<Button onClick={() => createProposal(connection, wallet.wallet)}> <Button
onClick={() =>
createProposal(
connection,
wallet.wallet,
'Billy',
'https://gist.github.com/dummytester123/bd3567f80e13a27b02a2e0fb891ecab1',
TimelockType.CustomSingleSignerV1,
ConsensusAlgorithm.Majority,
ExecutionType.AnyAboveVoteFinishSlot,
)
}
>
Add Proposal Add Proposal
</Button> </Button>
</Row> </Row>

View File

@ -1,14 +1,21 @@
import { Col, Divider, Row, Space, Spin, Statistic } from 'antd'; import { Card, Col, Divider, 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';
import { ConsensusAlgorithm, TimelockSet } from '../../models/timelock'; import {
ConsensusAlgorithm,
TimelockSet,
Transaction,
} from '../../models/timelock';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { useProposals } from '../../contexts/proposals'; import { useProposals } from '../../contexts/proposals';
import { StateBadge } from '../../components/Proposal/StateBadge'; import { StateBadge } from '../../components/Proposal/StateBadge';
import { contexts } from '@oyster/common'; import { contexts } from '@oyster/common';
import { MintInfo } from '@solana/spl-token'; import { MintInfo } from '@solana/spl-token';
import Meta from 'antd/lib/card/Meta';
import './style.less';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
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;
@ -25,6 +32,7 @@ export const ProposalView = () => {
proposal={proposal} proposal={proposal}
votingMint={votingMint} votingMint={votingMint}
sigMint={sigMint} sigMint={sigMint}
instructions={context.transactions}
/> />
) : ( ) : (
<Spin /> <Spin />
@ -37,10 +45,12 @@ function InnerProposalView({
proposal, proposal,
sigMint, sigMint,
votingMint, votingMint,
instructions,
}: { }: {
proposal: ParsedAccount<TimelockSet>; proposal: ParsedAccount<TimelockSet>;
sigMint: MintInfo; sigMint: MintInfo;
votingMint: MintInfo; votingMint: MintInfo;
instructions: Record<string, ParsedAccount<Transaction>>;
}) { }) {
const isUrl = !!proposal.info.state.descLink.match(urlRegex); const isUrl = !!proposal.info.state.descLink.match(urlRegex);
const isGist = const isGist =
@ -154,11 +164,72 @@ function InnerProposalView({
/> />
</Col> </Col>
</Row> </Row>
<Row
gutter={[
{ xs: 8, sm: 16, md: 24, lg: 32 },
{ xs: 8, sm: 16, md: 24, lg: 32 },
]}
>
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(position => (
<Col xs={24} sm={24} md={12} lg={8}>
<InstructionCard
position={position}
instruction={{
info: {
slot: 5,
instruction:
'Dhr8DF4Wsr9WrwSJixqfe7YFwdgz3GtjTRGF4wRMw3rP91NUmzR8PGJvCw7CpFDnUMij5kSpWpocaAftpCxBJ1oneK89CeWXGEVhyYTxAnhudvUSCXtvvjNhiFC8NQhCHDskA6MdsGJP2Dd1w3kihZeB5N9sXT7NjwZCCZeH5qL5tFhgUja9Nv1ywGvU1WnXDwxsRvPGf5AQNZ5xwwrr7chFV7DoTnwc9D7dJwx3m9mMBdSUcbtP77pUxvZbN1Sw9CTpM2rUUvwSxy2cYJPcgnNKZyq3CPYCa9H2Z9KR2AigcMgRzYswYhBGiz8piMVYL1rQRmRJVnoWALh3K1rS3BPRmvq52G7EJzXdoKiQQU6zzdzFuP6TGvdJrwfppzZrFRQ3n6bb3RaBoqoopnrLwWQA673KEeJCngwnipnTo5K6i72jJBBniqmanmtuFhVne2Gr2QUxDynqwBRvbpBT4uAibmu6PRUL9BhVDJibrb7rJf9RY2TL',
},
}}
/>
</Col>
))}
</Row>
</Space> </Space>
</> </>
); );
} }
export function InstructionCard({
instruction,
position,
}: {
instruction: any; //ParsedAccount<Transaction>;
position: number;
}) {
const [tabKey, setTabKey] = useState('info');
const contentList: Record<string, JSX.Element> = {
info: (
<Meta
title={'Program: TODO'}
description={
<>
<p>Instruction: TODO</p>
<p>Slot: {instruction.info.slot}</p>
</>
}
/>
),
data: <p className="wordwrap">{instruction.info.instruction}</p>,
};
return (
<Card
tabList={[
{ key: 'info', tab: 'Info' },
{ key: 'data', tab: 'Data' },
]}
title={'Instruction #' + position}
activeTabKey={tabKey}
onTabChange={setTabKey}
actions={[<EditOutlined key="edit" />, <DeleteOutlined key="delete" />]}
>
{contentList[tabKey]}
</Card>
);
}
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 proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5;

View File

@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { Modal } from 'antd';
import { Form, Input, Select } from 'antd';
import { Account } from '@solana/web3.js';
import {
ConsensusAlgorithm,
DESC_SIZE,
ExecutionType,
NAME_SIZE,
TimelockType,
} from '../../models/timelock';
import { Link } from 'react-router-dom';
import { LABELS } from '../../constants';
import { contexts } from '@oyster/common';
import { createProposal } from '../../actions/createProposal';
import { Redirect } from 'react-router';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { Option } = Select;
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
export function NewFormMenuItem() {
const [isModalVisible, setIsModalVisible] = useState(false);
const [redirect, setRedirect] = useState('');
const handleOk = (a: Account) => {
setIsModalVisible(false);
setRedirect(a.publicKey.toBase58());
};
const handleCancel = () => {
setIsModalVisible(false);
};
if (redirect) {
setRedirect('');
return <Redirect push to={'/proposal/' + redirect} />;
}
return (
<>
<Link
to={{
pathname: '/',
}}
onClick={() => setIsModalVisible(true)}
>
{LABELS.NEW_PROPOSAL}
</Link>
<NewForm
handleOk={handleOk}
handleCancel={handleCancel}
isModalVisible={isModalVisible}
/>
</>
);
}
export function NewForm({
handleOk,
handleCancel,
isModalVisible,
}: {
handleOk: (a: Account) => void;
handleCancel: () => void;
isModalVisible: boolean;
}) {
const [form] = Form.useForm();
const wallet = useWallet();
const connection = useConnection();
const onFinish = async (values: {
name: string;
description: string;
timelockType: TimelockType;
executionType: ExecutionType;
consensusAlgorithm: ConsensusAlgorithm;
}) => {
const newSet = await createProposal(
connection,
wallet.wallet,
values.name,
values.description,
values.timelockType,
values.consensusAlgorithm,
values.executionType,
);
handleOk(newSet);
};
return (
<Modal
title="New Proposal"
visible={isModalVisible}
onOk={form.submit}
onCancel={handleCancel}
>
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input maxLength={NAME_SIZE} />
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{ required: true }]}
>
<Input maxLength={DESC_SIZE} placeholder="Github Gist link" />
</Form.Item>
<Form.Item
name="consensusAlgorithm"
label="Consensus Algorithm"
rules={[{ required: true }]}
initialValue={ConsensusAlgorithm.Majority}
>
<Select placeholder="Select the Consensus Algorithm">
<Option value={ConsensusAlgorithm.Majority}>Majority</Option>
<Option value={ConsensusAlgorithm.FullConsensus}>
Full Consensus
</Option>
<Option value={ConsensusAlgorithm.SuperMajority}>
Super Majority
</Option>
</Select>
</Form.Item>
<Form.Item
name="executionType"
label="Execution Type"
rules={[{ required: true }]}
initialValue={ExecutionType.AnyAboveVoteFinishSlot}
>
<Select placeholder="Select the type of execution">
<Option value={ExecutionType.AnyAboveVoteFinishSlot}>
Any Above Vote Finish Slot
</Option>
<Option value={ExecutionType.AllOrNothing}>All or Nothing</Option>
</Select>
</Form.Item>
<Form.Item
name="timelockType"
label="Proposal Type"
rules={[{ required: true }]}
initialValue={TimelockType.CustomSingleSignerV1}
>
<Select placeholder="Select the type of Proposal">
<Option value={TimelockType.CustomSingleSignerV1}>
Single Signer
</Option>
</Select>
</Form.Item>
</Form>
</Modal>
);
}

View File

@ -0,0 +1,7 @@
.wordwrap {
white-space: pre-wrap; /* CSS3 */
white-space: -moz-pre-wrap; /* Firefox */
white-space: -pre-wrap; /* Opera <7 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* IE */
}