mirror of https://github.com/certusone/oyster.git
Working on new layout (mobile friendly) for proposals, and adding instruction submission demo.
This commit is contained in:
parent
dbc8f2eeb9
commit
0f0f268253
|
@ -69,9 +69,9 @@ export const PROGRAM_IDS = [
|
|||
name: 'testnet',
|
||||
timelock: () => ({
|
||||
programAccountId: new PublicKey(
|
||||
'9gBhDCCKV7KELLFRY8sAJZXqDmvUfmNzFzpB2b4FUVVr',
|
||||
'77ZN8QJ234sGjxWR2CkEeuzaNnF888uBJektmWobEMHa',
|
||||
),
|
||||
programId: new PublicKey('9iAeqqppjn7g1Jn8o2cQCqU5aQVV3h4q9bbWdKRbeC2w'),
|
||||
programId: new PublicKey('7ncumqGMduzYcnZarZWumayXpAB7TJH6uvbLYCz8dT68'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
|
||||
|
@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
|
|||
name: 'devnet',
|
||||
timelock: () => ({
|
||||
programAccountId: new PublicKey(
|
||||
'6RGANF5jrRfrZ6PkxJyCjwhmZK4F4B5b5u46HLeVvvjR',
|
||||
'EPsAj4tGpPF3c4w8y7dtuUfemBdPwmr5nji1BH5B18Lq',
|
||||
),
|
||||
programId: new PublicKey('C5o9rMTn131BVppuAUMQKLBvng1Ukw6Mh1SogTuCcYHC'),
|
||||
programId: new PublicKey('5rnMWALKssHK5fDxhpxvgpB9ZM5Yqgegsv2hwMebWMKx'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||
|
|
|
@ -160,22 +160,12 @@ em {
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.flexColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-fill {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-slider {
|
||||
margin: 20px 15px 40px 15px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
Account,
|
||||
Connection,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { contexts, utils, actions, ParsedAccount } from '@oyster/common';
|
||||
|
||||
import {
|
||||
CustomSingleSignerTimelockTransactionLayout,
|
||||
TimelockSet,
|
||||
} from '../models/timelock';
|
||||
import { addCustomSingleSignerTransactionInstruction } from '../models/addCustomSingleSignerTransaction';
|
||||
|
||||
const { sendTransaction } = contexts.Connection;
|
||||
const { notify } = utils;
|
||||
|
||||
export const addCustomSingleSignerTransaction = async (
|
||||
connection: Connection,
|
||||
wallet: any,
|
||||
proposal: ParsedAccount<TimelockSet>,
|
||||
sigAccount: PublicKey,
|
||||
) => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
let signers: Account[] = [];
|
||||
let instructions: TransactionInstruction[] = [];
|
||||
|
||||
const rentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
CustomSingleSignerTimelockTransactionLayout.span,
|
||||
);
|
||||
const txnKey = new Account();
|
||||
|
||||
const uninitializedTxnInstruction = SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: txnKey.publicKey,
|
||||
lamports: rentExempt,
|
||||
space: CustomSingleSignerTimelockTransactionLayout.span,
|
||||
programId: PROGRAM_IDS.timelock.programId,
|
||||
});
|
||||
|
||||
signers.push(txnKey);
|
||||
|
||||
instructions.push(uninitializedTxnInstruction);
|
||||
|
||||
instructions.push(
|
||||
addCustomSingleSignerTransactionInstruction(
|
||||
txnKey.publicKey,
|
||||
proposal.pubkey,
|
||||
sigAccount,
|
||||
proposal.info.signatoryValidation,
|
||||
'0',
|
||||
'12345',
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
notify({
|
||||
message: 'Adding transaction...',
|
||||
description: 'Please wait...',
|
||||
type: 'warn',
|
||||
});
|
||||
|
||||
try {
|
||||
let tx = await sendTransaction(
|
||||
connection,
|
||||
wallet,
|
||||
instructions,
|
||||
signers,
|
||||
true,
|
||||
);
|
||||
|
||||
notify({
|
||||
message: 'Transaction added.',
|
||||
type: 'success',
|
||||
description: `Transaction - ${tx}`,
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { ParsedAccount } from '@oyster/common';
|
||||
import { Badge, Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { STATE_COLOR, TimelockSet } from '../../models/timelock';
|
||||
|
||||
export function StateBadgeRibbon({
|
||||
proposal,
|
||||
children,
|
||||
}: {
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
children: any;
|
||||
}) {
|
||||
const status = proposal.info.state.status;
|
||||
let color = STATE_COLOR[status];
|
||||
return (
|
||||
<Badge.Ribbon style={{ backgroundColor: color }} text={status}>
|
||||
{children}
|
||||
</Badge.Ribbon>
|
||||
);
|
||||
}
|
||||
|
||||
export function StateBadge({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
}) {
|
||||
const status = proposal.info.state.status;
|
||||
let color = STATE_COLOR[status];
|
||||
return <Tag color={color}>{status}</Tag>;
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { contexts, ParsedAccount } from '@oyster/common';
|
||||
import { Card, Spin } from 'antd';
|
||||
import { useProposals } from '../../contexts/proposals';
|
||||
import { TimelockSet } from '../../models/timelock';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { LABELS } from '../../constants';
|
||||
|
||||
const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
||||
|
||||
export function Proposal({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
}) {
|
||||
const isUrl = !!proposal.info.state.descLink.match(urlRegex);
|
||||
const isGist =
|
||||
proposal.info.state.descLink.match(/gist/i) &&
|
||||
proposal.info.state.descLink.match(/github/i);
|
||||
const [content, setContent] = useState(proposal.info.state.descLink);
|
||||
const [loading, setLoading] = useState(isUrl);
|
||||
const [failed, setFailed] = useState(true);
|
||||
if (loading) {
|
||||
let toFetch = proposal.info.state.descLink;
|
||||
const pieces = toFetch.match(urlRegex);
|
||||
if (isGist && pieces) {
|
||||
const justIdWithoutUser = pieces[1].split('/')[2];
|
||||
toFetch = 'https://api.github.com/gists/' + justIdWithoutUser;
|
||||
}
|
||||
fetch(toFetch)
|
||||
.then(async resp => {
|
||||
if (resp.status == 200) {
|
||||
if (isGist) {
|
||||
const jsonContent = await resp.json();
|
||||
const nextUrlFileName = Object.keys(jsonContent['files'])[0];
|
||||
const nextUrl = jsonContent['files'][nextUrlFileName]['raw_url'];
|
||||
fetch(nextUrl).then(async response =>
|
||||
setContent(await response.text()),
|
||||
);
|
||||
} else setContent(await resp.text());
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(response => {
|
||||
setFailed(true);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={LABELS.PROPOSAL + ': ' + proposal.info.state.name}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : isUrl ? (
|
||||
failed ? (
|
||||
<Card.Meta
|
||||
title={LABELS.DESCRIPTION}
|
||||
description={
|
||||
<a href={proposal.info.state.descLink} target="_blank">
|
||||
{LABELS.NO_LOAD}
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ReactMarkdown children={content} />
|
||||
)
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -15,4 +15,7 @@ export const LABELS = {
|
|||
DESCRIPTION: 'Description',
|
||||
PROPOSAL: 'Proposal',
|
||||
NO_LOAD: 'Unable to load markdown. Click to view.',
|
||||
SIG_GIVEN: 'Sigs Given',
|
||||
VOTES_REQUIRED: "Votes Req'd",
|
||||
VOTES_CAST: 'Votes Cast',
|
||||
};
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
TimelockSetParser,
|
||||
} from '../models/timelock';
|
||||
|
||||
const { useWallet } = contexts.Wallet;
|
||||
|
||||
const { useConnectionConfig } = contexts.Connection;
|
||||
const { cache } = contexts.Accounts;
|
||||
|
||||
|
@ -30,10 +32,27 @@ export default function ProposalsProvider({ children = null as any }) {
|
|||
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
|
||||
endpoint,
|
||||
]);
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const [proposals, setProposals] = useState({});
|
||||
|
||||
useSetupProposalsCache({ connection, setProposals });
|
||||
|
||||
return (
|
||||
<ProposalsContext.Provider value={{ proposals }}>
|
||||
{children}
|
||||
</ProposalsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useSetupProposalsCache({
|
||||
connection,
|
||||
setProposals,
|
||||
}: {
|
||||
connection: Connection;
|
||||
setProposals: React.Dispatch<React.SetStateAction<{}>>;
|
||||
}) {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
useEffect(() => {
|
||||
const queryProposals = async () => {
|
||||
const programAccounts = await connection.getProgramAccounts(
|
||||
|
@ -77,14 +96,7 @@ export default function ProposalsProvider({ children = null as any }) {
|
|||
connection.removeProgramAccountChangeListener(subID);
|
||||
};
|
||||
}, [connection, PROGRAM_IDS.timelock.programAccountId]);
|
||||
|
||||
return (
|
||||
<ProposalsContext.Provider value={{ proposals }}>
|
||||
{children}
|
||||
</ProposalsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useProposals = () => {
|
||||
const context = useContext(ProposalsContext);
|
||||
return context as ProposalsContextState;
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import {
|
||||
PublicKey,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { utils } from '@oyster/common';
|
||||
import * as Layout from '../utils/layout';
|
||||
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import {
|
||||
DESC_SIZE,
|
||||
INSTRUCTION_LIMIT,
|
||||
NAME_SIZE,
|
||||
TimelockConfig,
|
||||
TimelockInstruction,
|
||||
TRANSACTION_SLOTS,
|
||||
} from './timelock';
|
||||
import BN from 'bn.js';
|
||||
import { toUTF8Array } from '@oyster/common/dist/lib/utils';
|
||||
|
||||
/// [Requires Signatory token]
|
||||
/// Adds a Transaction to the Timelock Set. Max of 10 of any Transaction type. More than 10 will throw error.
|
||||
/// Creates a PDA using your authority to be used to later execute the instruction.
|
||||
/// This transaction needs to contain authority to execute the program.
|
||||
///
|
||||
/// 0. `[writable]` Uninitialized Timelock Transaction account.
|
||||
/// 1. `[writable]` Timelock set account.
|
||||
/// 2. `[writable]` Signatory account
|
||||
/// 3. `[writable]` Signatory validation account.
|
||||
/// 4. `[]` Timelock program account.
|
||||
/// 5. `[]` Token program account.
|
||||
export const addCustomSingleSignerTransactionInstruction = (
|
||||
timelockTransactionAccount: PublicKey,
|
||||
timelockSetAccount: PublicKey,
|
||||
signatoryAccount: PublicKey,
|
||||
signatoryValidationAccount: PublicKey,
|
||||
slot: string,
|
||||
instruction: string,
|
||||
position: number,
|
||||
): TransactionInstruction => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const instructionAsBytes = toUTF8Array(instruction);
|
||||
if (instructionAsBytes.length > INSTRUCTION_LIMIT) {
|
||||
throw new Error(
|
||||
'Instruction length in bytes is more than ' + INSTRUCTION_LIMIT,
|
||||
);
|
||||
}
|
||||
|
||||
if (position > TRANSACTION_SLOTS) {
|
||||
throw new Error(
|
||||
'Position is more than ' + TRANSACTION_SLOTS + ' which is not allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const dataLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('instruction'),
|
||||
Layout.uint64('slot'),
|
||||
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instructions'),
|
||||
BufferLayout.u8('position'),
|
||||
]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
|
||||
instructionAsBytes.push(0);
|
||||
}
|
||||
console.log('Lenth', instructionAsBytes.length);
|
||||
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: TimelockInstruction.addCustomSingleSignerTransaction,
|
||||
slot: new BN(slot),
|
||||
instructions: instructionAsBytes,
|
||||
position: position,
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{ pubkey: timelockTransactionAccount, isSigner: true, isWritable: true },
|
||||
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: signatoryAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: signatoryValidationAccount, isSigner: false, isWritable: true },
|
||||
{
|
||||
pubkey: PROGRAM_IDS.timelock.programAccountId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
|
||||
];
|
||||
console.log('data', data);
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: PROGRAM_IDS.timelock.programId,
|
||||
data,
|
||||
});
|
||||
};
|
|
@ -105,7 +105,6 @@ export const initTimelockSetInstruction = (
|
|||
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
|
||||
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
||||
];
|
||||
console.log(data);
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: PROGRAM_IDS.timelock.programId,
|
||||
|
|
|
@ -6,11 +6,12 @@ import { utils } from '@oyster/common';
|
|||
|
||||
export const DESC_SIZE = 200;
|
||||
export const NAME_SIZE = 32;
|
||||
export const INSTRUCTION_LIMIT = 255;
|
||||
export const TRANSACTION_SLOTS = 10;
|
||||
|
||||
export enum TimelockInstruction {
|
||||
InitTimelockSet = 1,
|
||||
AddSignatoryMint = 11,
|
||||
AddVotingMint = 12,
|
||||
addCustomSingleSignerTransaction = 4,
|
||||
}
|
||||
|
||||
export interface TimelockConfig {
|
||||
|
@ -50,14 +51,28 @@ export enum TimelockStateStatus {
|
|||
Deleted = 4,
|
||||
}
|
||||
|
||||
export const STATE_COLOR: Record<string, string> = {
|
||||
Draft: 'orange',
|
||||
Voting: 'blue',
|
||||
Executing: 'green',
|
||||
Completed: 'purple',
|
||||
Deleted: 'gray',
|
||||
};
|
||||
|
||||
export interface TimelockState {
|
||||
status: TimelockStateStatus;
|
||||
totalVotingTokensMinted: BN;
|
||||
totalSigningTokensMinted: BN;
|
||||
timelockTransactions: PublicKey[];
|
||||
name: string;
|
||||
descLink: string;
|
||||
}
|
||||
|
||||
const timelockTxns = [];
|
||||
for (let i = 0; i < TRANSACTION_SLOTS; i++) {
|
||||
timelockTxns.push(Layout.publicKey('timelockTxn' + i.toString()));
|
||||
}
|
||||
|
||||
export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.struct(
|
||||
[
|
||||
BufferLayout.u8('version'),
|
||||
|
@ -69,18 +84,10 @@ export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.str
|
|||
Layout.publicKey('votingValidation'),
|
||||
BufferLayout.u8('timelockStateStatus'),
|
||||
Layout.uint64('totalVotingTokensMinted'),
|
||||
Layout.uint64('totalSigningTokensMinted'),
|
||||
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
|
||||
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
|
||||
Layout.publicKey('timelockTxn1'),
|
||||
Layout.publicKey('timelockTxn2'),
|
||||
Layout.publicKey('timelockTxn3'),
|
||||
Layout.publicKey('timelockTxn4'),
|
||||
Layout.publicKey('timelockTxn5'),
|
||||
Layout.publicKey('timelockTxn6'),
|
||||
Layout.publicKey('timelockTxn7'),
|
||||
Layout.publicKey('timelockTxn8'),
|
||||
Layout.publicKey('timelockTxn9'),
|
||||
Layout.publicKey('timelockTxn10'),
|
||||
...timelockTxns,
|
||||
BufferLayout.u8('consensusAlgorithm'),
|
||||
BufferLayout.u8('executionType'),
|
||||
BufferLayout.u8('timelockType'),
|
||||
|
@ -125,6 +132,11 @@ export const TimelockSetParser = (
|
|||
const buffer = Buffer.from(info.data);
|
||||
const data = TimelockSetLayout.decode(buffer);
|
||||
|
||||
const timelockTxns = [];
|
||||
for (let i = 0; i < TRANSACTION_SLOTS; i++) {
|
||||
timelockTxns.push(data['timelockTxn' + i.toString()]);
|
||||
}
|
||||
|
||||
const details = {
|
||||
pubkey: pubKey,
|
||||
account: {
|
||||
|
@ -141,20 +153,10 @@ export const TimelockSetParser = (
|
|||
state: {
|
||||
status: TimelockStateStatus[data.timelockStateStatus],
|
||||
totalVotingTokensMinted: data.totalVotingTokensMinted,
|
||||
totalSigningTokensMinted: data.totalSigningTokensMinted,
|
||||
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
|
||||
name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''),
|
||||
timelockTransactions: [
|
||||
data.timelockTxn1,
|
||||
data.timelockTxn2,
|
||||
data.timelockTxn3,
|
||||
data.timelockTxn4,
|
||||
data.timelockTxn5,
|
||||
data.timelockTxn6,
|
||||
data.timelockTxn7,
|
||||
data.timelockTxn8,
|
||||
data.timelockTxn9,
|
||||
data.timelockTxn10,
|
||||
],
|
||||
timelockTransactions: timelockTxns,
|
||||
},
|
||||
config: {
|
||||
consensusAlgorithm: data.consensusAlgorithm,
|
||||
|
@ -166,3 +168,18 @@ export const TimelockSetParser = (
|
|||
|
||||
return details;
|
||||
};
|
||||
|
||||
export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.Structure = BufferLayout.struct(
|
||||
[
|
||||
Layout.uint64('slot'),
|
||||
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
|
||||
Layout.publicKey('authorityKey'),
|
||||
],
|
||||
);
|
||||
export interface CustomSingleSignerTimelockTransaction {
|
||||
slot: BN;
|
||||
|
||||
instruction: string;
|
||||
|
||||
authorityKey: PublicKey;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { contexts } from '@oyster/common';
|
|||
import { AppLayout } from './components/Layout';
|
||||
import ProposalsProvider from './contexts/proposals';
|
||||
import { DashboardView, HomeView } from './views';
|
||||
import { ProposalView } from './views/proposal';
|
||||
const { WalletProvider } = contexts.Wallet;
|
||||
const { ConnectionProvider } = contexts.Connection;
|
||||
const { AccountsProvider } = contexts.Accounts;
|
||||
|
@ -19,6 +20,8 @@ export function Routes() {
|
|||
<AppLayout>
|
||||
<Switch>
|
||||
<Route exact path="/" component={() => <HomeView />} />
|
||||
<Route path="/proposal/:id" children={<ProposalView />} />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard"
|
||||
|
|
|
@ -1,16 +1,68 @@
|
|||
import { Card, Col, Row } from 'antd';
|
||||
import { Button, Card, Col, Row, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { GUTTER, LABELS } from '../../constants';
|
||||
import { contexts } from '@oyster/common';
|
||||
import { contexts, hooks, ParsedAccount } from '@oyster/common';
|
||||
import './style.less';
|
||||
import { createProposal } from '../../actions/createProposal';
|
||||
import { useProposals } from '../../contexts/proposals';
|
||||
import { TimelockSet } from '../../models/timelock';
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
|
||||
const { useWallet } = contexts.Wallet;
|
||||
const { useConnection } = contexts.Connection;
|
||||
const { useAccountByMint } = hooks;
|
||||
|
||||
export const DashboardView = () => {
|
||||
const { connected } = useWallet();
|
||||
const wallet = useWallet();
|
||||
const connection = useConnection();
|
||||
const context = useProposals();
|
||||
const proposal = Object.keys(context.proposals).length
|
||||
? context.proposals[Object.keys(context.proposals)[0]]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<Row gutter={GUTTER}></Row>
|
||||
<Row gutter={GUTTER} className="home-info-row">
|
||||
<Button onClick={() => createProposal(connection, wallet.wallet)}>
|
||||
Add Proposal
|
||||
</Button>
|
||||
</Row>
|
||||
{proposal && wallet.wallet && connection && (
|
||||
<InnerDummyView
|
||||
proposal={proposal}
|
||||
connection={connection}
|
||||
wallet={wallet.wallet}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function InnerDummyView({
|
||||
proposal,
|
||||
connection,
|
||||
wallet,
|
||||
}: {
|
||||
connection: Connection;
|
||||
wallet: contexts.Wallet.WalletAdapter;
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
}) {
|
||||
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
|
||||
if (!sigAccount) return <Spin />;
|
||||
return (
|
||||
<Row gutter={GUTTER} className="home-info-row">
|
||||
<Button
|
||||
onClick={() =>
|
||||
addCustomSingleSignerTransaction(
|
||||
connection,
|
||||
wallet,
|
||||
proposal,
|
||||
sigAccount.pubkey,
|
||||
)
|
||||
}
|
||||
>
|
||||
Add transaction to {proposal.pubkey.toBase58()}
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,53 +1,64 @@
|
|||
import { Col, Row, Space } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { GUTTER } from '../../constants';
|
||||
import { Button } from 'antd';
|
||||
import { createProposal } from '../../actions/createProposal';
|
||||
import { contexts, ParsedAccount } from '@oyster/common';
|
||||
import { Proposal } from '../../components/Proposal';
|
||||
import { ProposalsContext, useProposals } from '../../contexts/proposals';
|
||||
import { Col, List, Row } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { contexts } from '@oyster/common';
|
||||
import { useProposals } from '../../contexts/proposals';
|
||||
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
||||
import { TimelockSet } from '../../models/timelock';
|
||||
const { useWallet } = contexts.Wallet;
|
||||
const { useConnection } = contexts.Connection;
|
||||
const ROW_SIZE = 3;
|
||||
import { StateBadgeRibbon } from '../../components/Proposal/StateBadge';
|
||||
import { urlRegex } from '../proposal';
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export const HomeView = () => {
|
||||
const wallet = useWallet();
|
||||
const connection = useConnection();
|
||||
const context = useProposals();
|
||||
const [page, setPage] = useState(0);
|
||||
const listData = useMemo(() => {
|
||||
const newListData: any[][] = [[]];
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const newRows: ParsedAccount<TimelockSet>[][] = [[]];
|
||||
Object.keys(context.proposals).forEach(key => {
|
||||
newRows[newRows.length - 1].push(context.proposals[key]);
|
||||
if (newRows[newRows.length - 1].length === ROW_SIZE) newRows.push([]);
|
||||
const proposal = context.proposals[key];
|
||||
newListData[newListData.length - 1].push({
|
||||
href: '#/proposal/' + key,
|
||||
title: proposal.info.state.name,
|
||||
proposal,
|
||||
description: proposal.info.state.descLink.match(urlRegex) ? (
|
||||
<a href={proposal.info.state.descLink} target={'_blank'}>
|
||||
Link to markdown
|
||||
</a>
|
||||
) : (
|
||||
proposal.info.state.descLink
|
||||
),
|
||||
});
|
||||
if (newListData[newListData.length - 1].length == PAGE_SIZE)
|
||||
newListData.push([]);
|
||||
});
|
||||
return newRows;
|
||||
return newListData;
|
||||
}, [context.proposals]);
|
||||
|
||||
return (
|
||||
<div className="flexColumn">
|
||||
<Row gutter={GUTTER} className="home-info-row">
|
||||
<Button onClick={() => createProposal(connection, wallet.wallet)}>
|
||||
Click me
|
||||
</Button>
|
||||
</Row>
|
||||
<Space direction="vertical" size="large">
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={i}
|
||||
gutter={GUTTER}
|
||||
className="home-info-row"
|
||||
justify={'space-around'}
|
||||
>
|
||||
{row.map(proposal => (
|
||||
<Col key={proposal.pubkey.toBase58()} span={6}>
|
||||
<Proposal proposal={proposal} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<Row>
|
||||
<Col flex="auto">
|
||||
<List
|
||||
itemLayout="vertical"
|
||||
size="large"
|
||||
pagination={{
|
||||
onChange: page => {
|
||||
setPage(page);
|
||||
},
|
||||
pageSize: PAGE_SIZE,
|
||||
}}
|
||||
dataSource={listData[page]}
|
||||
renderItem={item => (
|
||||
<StateBadgeRibbon proposal={item.proposal}>
|
||||
<List.Item key={item.title}>
|
||||
<List.Item.Meta
|
||||
avatar={item.badge}
|
||||
title={<a href={item.href}>{item.title}</a>}
|
||||
description={item.description}
|
||||
/>
|
||||
</List.Item>
|
||||
</StateBadgeRibbon>
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import { Col, Divider, Row, Space, Spin, Statistic } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { LABELS } from '../../constants';
|
||||
import { ParsedAccount } from '@oyster/common';
|
||||
import { ConsensusAlgorithm, TimelockSet } from '../../models/timelock';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useProposals } from '../../contexts/proposals';
|
||||
import { StateBadge } from '../../components/Proposal/StateBadge';
|
||||
import { contexts } from '@oyster/common';
|
||||
import { MintInfo } from '@solana/spl-token';
|
||||
export const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
||||
const { useMint } = contexts.Accounts;
|
||||
|
||||
export const ProposalView = () => {
|
||||
const context = useProposals();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const proposal = context.proposals[id];
|
||||
const sigMint = useMint(proposal?.info.signatoryMint);
|
||||
const votingMint = useMint(proposal?.info.votingMint);
|
||||
return (
|
||||
<div className="flexColumn">
|
||||
{proposal && sigMint && votingMint ? (
|
||||
<InnerProposalView
|
||||
proposal={proposal}
|
||||
votingMint={votingMint}
|
||||
sigMint={sigMint}
|
||||
/>
|
||||
) : (
|
||||
<Spin />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function InnerProposalView({
|
||||
proposal,
|
||||
sigMint,
|
||||
votingMint,
|
||||
}: {
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
sigMint: MintInfo;
|
||||
votingMint: MintInfo;
|
||||
}) {
|
||||
const isUrl = !!proposal.info.state.descLink.match(urlRegex);
|
||||
const isGist =
|
||||
!!proposal.info.state.descLink.match(/gist/i) &&
|
||||
!!proposal.info.state.descLink.match(/github/i);
|
||||
const [content, setContent] = useState(proposal.info.state.descLink);
|
||||
const [loading, setLoading] = useState(isUrl);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useMemo(() => {
|
||||
if (loading) {
|
||||
let toFetch = proposal.info.state.descLink;
|
||||
const pieces = toFetch.match(urlRegex);
|
||||
if (isGist && pieces) {
|
||||
const justIdWithoutUser = pieces[1].split('/')[2];
|
||||
toFetch = 'https://api.github.com/gists/' + justIdWithoutUser;
|
||||
}
|
||||
fetch(toFetch)
|
||||
.then(async resp => {
|
||||
if (resp.status == 200) {
|
||||
if (isGist) {
|
||||
const jsonContent = await resp.json();
|
||||
const nextUrlFileName = Object.keys(jsonContent['files'])[0];
|
||||
const nextUrl = jsonContent['files'][nextUrlFileName]['raw_url'];
|
||||
fetch(nextUrl).then(async response =>
|
||||
setContent(await response.text()),
|
||||
);
|
||||
} else setContent(await resp.text());
|
||||
} else {
|
||||
if (resp.status == 403 && isGist)
|
||||
setMsg(
|
||||
'Gist Github API limit exceeded. Click to view on Github directly.',
|
||||
);
|
||||
setFailed(true);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(response => {
|
||||
setFailed(true);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space
|
||||
size="large"
|
||||
split={<Divider type="horizontal" />}
|
||||
direction="vertical"
|
||||
>
|
||||
<Row justify="center" align="middle">
|
||||
<Col span={24}>
|
||||
<p>
|
||||
<span style={{ fontSize: '21px', marginRight: '20px' }}>
|
||||
{LABELS.PROPOSAL}: {proposal.info.state.name}
|
||||
</span>
|
||||
|
||||
<StateBadge proposal={proposal} />
|
||||
</p>
|
||||
</Col>
|
||||
<Col span={2}> </Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : isUrl ? (
|
||||
failed ? (
|
||||
<p>
|
||||
{LABELS.DESCRIPTION}:{' '}
|
||||
<a href={proposal.info.state.descLink} target="_blank">
|
||||
{msg ? msg : LABELS.NO_LOAD}
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<ReactMarkdown children={content} />
|
||||
)
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title={LABELS.SIG_GIVEN}
|
||||
value={
|
||||
proposal.info.state.totalSigningTokensMinted.toNumber() -
|
||||
sigMint.supply.toNumber()
|
||||
}
|
||||
suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title={LABELS.VOTES_CAST}
|
||||
value={
|
||||
proposal.info.state.totalVotingTokensMinted.toNumber() -
|
||||
votingMint.supply.toNumber()
|
||||
}
|
||||
suffix={`/ ${proposal.info.state.totalVotingTokensMinted}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
valueStyle={{ color: 'green' }}
|
||||
title={LABELS.VOTES_REQUIRED}
|
||||
value={getVotesRequired(proposal)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getVotesRequired(proposal: ParsedAccount<TimelockSet>): number {
|
||||
if (proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.Majority) {
|
||||
return proposal.info.state.totalVotingTokensMinted.toNumber() * 0.5;
|
||||
} else if (
|
||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.SuperMajority
|
||||
) {
|
||||
return proposal.info.state.totalVotingTokensMinted.toNumber() * 0.6;
|
||||
} else if (
|
||||
proposal.info.config.consensusAlgorithm === ConsensusAlgorithm.FullConsensus
|
||||
) {
|
||||
return proposal.info.state.totalVotingTokensMinted.toNumber();
|
||||
}
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue