Working on new layout (mobile friendly) for proposals, and adding instruction submission demo.

This commit is contained in:
Dummy Tester 123 2021-02-21 19:09:26 -06:00
parent dbc8f2eeb9
commit 0f0f268253
14 changed files with 565 additions and 167 deletions

View File

@ -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'),

View File

@ -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;
}

View File

@ -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();
}
};

View File

@ -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>;
}

View File

@ -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>
);
}

View File

@ -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',
};

View File

@ -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;

View File

@ -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,
});
};

View File

@ -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,

View File

@ -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;
}

View File

@ -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"

View File

@ -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>
);
}

View File

@ -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
),
});
return newRows;
if (newListData[newListData.length - 1].length == PAGE_SIZE)
newListData.push([]);
});
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} />
<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>
))}
</Space>
</div>
);
};

View File

@ -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;
}