mirror of https://github.com/certusone/oyster.git
Added complete working display of instructions and adding instructions
This commit is contained in:
parent
d4bf02c790
commit
1883bfd8ee
|
@ -22,6 +22,9 @@ export const addCustomSingleSignerTransaction = async (
|
||||||
wallet: any,
|
wallet: any,
|
||||||
proposal: ParsedAccount<TimelockSet>,
|
proposal: ParsedAccount<TimelockSet>,
|
||||||
sigAccount: PublicKey,
|
sigAccount: PublicKey,
|
||||||
|
slot: string,
|
||||||
|
instruction: string,
|
||||||
|
position: number,
|
||||||
) => {
|
) => {
|
||||||
const PROGRAM_IDS = utils.programIds();
|
const PROGRAM_IDS = utils.programIds();
|
||||||
|
|
||||||
|
@ -67,9 +70,9 @@ export const addCustomSingleSignerTransaction = async (
|
||||||
proposal.info.signatoryValidation,
|
proposal.info.signatoryValidation,
|
||||||
transferAuthority.publicKey,
|
transferAuthority.publicKey,
|
||||||
authority,
|
authority,
|
||||||
'123',
|
slot,
|
||||||
'12345',
|
instruction,
|
||||||
0,
|
position,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
|
import { ParsedAccount } from '@oyster/common';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import Meta from 'antd/lib/card/Meta';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TimelockTransaction } from '../../models/timelock';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
export function InstructionCard({
|
||||||
|
instruction,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
instruction: ParsedAccount<TimelockTransaction>;
|
||||||
|
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.toNumber()}</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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Spin } from 'antd';
|
||||||
|
import { Form, Input } from 'antd';
|
||||||
|
import { INSTRUCTION_LIMIT, TimelockSet } from '../../models/timelock';
|
||||||
|
import { contexts, ParsedAccount, hooks, utils } from '@oyster/common';
|
||||||
|
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
|
||||||
|
import { SaveOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { useWallet } = contexts.Wallet;
|
||||||
|
const { useConnection } = contexts.Connection;
|
||||||
|
const { useAccountByMint } = hooks;
|
||||||
|
const { notify } = utils;
|
||||||
|
|
||||||
|
const layout = {
|
||||||
|
labelCol: { span: 8 },
|
||||||
|
wrapperCol: { span: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NewInstructionCard({
|
||||||
|
proposal,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
proposal: ParsedAccount<TimelockSet>;
|
||||||
|
position: number;
|
||||||
|
}) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const wallet = useWallet();
|
||||||
|
const connection = useConnection();
|
||||||
|
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
|
||||||
|
const onFinish = async (values: { slot: string; instruction: string }) => {
|
||||||
|
if (!values.slot.match(/^\d*$/)) {
|
||||||
|
notify({
|
||||||
|
message: 'Slot can only be numeric',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sigAccount) {
|
||||||
|
await addCustomSingleSignerTransaction(
|
||||||
|
connection,
|
||||||
|
wallet.wallet,
|
||||||
|
proposal,
|
||||||
|
sigAccount.pubkey,
|
||||||
|
values.slot,
|
||||||
|
values.instruction,
|
||||||
|
position,
|
||||||
|
);
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="New Instruction"
|
||||||
|
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
|
||||||
|
>
|
||||||
|
{!sigAccount ? (
|
||||||
|
<Spin />
|
||||||
|
) : (
|
||||||
|
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
|
||||||
|
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
|
||||||
|
<Input maxLength={64} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="instruction"
|
||||||
|
label="Instruction"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
maxLength={INSTRUCTION_LIMIT}
|
||||||
|
placeholder={'Base58 encoded instruction'}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ import {
|
||||||
TimelockSet,
|
TimelockSet,
|
||||||
TimelockSetLayout,
|
TimelockSetLayout,
|
||||||
TimelockSetParser,
|
TimelockSetParser,
|
||||||
Transaction,
|
TimelockTransaction,
|
||||||
} from '../models/timelock';
|
} from '../models/timelock';
|
||||||
|
|
||||||
const { useConnectionConfig } = contexts.Connection;
|
const { useConnectionConfig } = contexts.Connection;
|
||||||
|
@ -23,7 +23,7 @@ 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>>;
|
transactions: Record<string, ParsedAccount<TimelockTransaction>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProposalsContext = React.createContext<ProposalsContextState | null>(
|
export const ProposalsContext = React.createContext<ProposalsContextState | null>(
|
||||||
|
@ -72,7 +72,10 @@ function useSetupProposalsCache({
|
||||||
};
|
};
|
||||||
Promise.all([query()]).then((all: PublicKeyAndAccount<Buffer>[][]) => {
|
Promise.all([query()]).then((all: PublicKeyAndAccount<Buffer>[][]) => {
|
||||||
const newProposals: Record<string, ParsedAccount<TimelockSet>> = {};
|
const newProposals: Record<string, ParsedAccount<TimelockSet>> = {};
|
||||||
const newTransactions: Record<string, ParsedAccount<Transaction>> = {};
|
const newTransactions: Record<
|
||||||
|
string,
|
||||||
|
ParsedAccount<TimelockTransaction>
|
||||||
|
> = {};
|
||||||
|
|
||||||
all[0].forEach(a => {
|
all[0].forEach(a => {
|
||||||
if (a.account.data.length === TimelockSetLayout.span) {
|
if (a.account.data.length === TimelockSetLayout.span) {
|
||||||
|
@ -89,11 +92,14 @@ function useSetupProposalsCache({
|
||||||
a.account,
|
a.account,
|
||||||
CustomSingleSignerTimelockTransactionParser,
|
CustomSingleSignerTimelockTransactionParser,
|
||||||
);
|
);
|
||||||
const cached = cache.get(a.pubkey) as ParsedAccount<Transaction>;
|
const cached = cache.get(
|
||||||
|
a.pubkey,
|
||||||
|
) as ParsedAccount<TimelockTransaction>;
|
||||||
newTransactions[a.pubkey.toBase58()] = cached;
|
newTransactions[a.pubkey.toBase58()] = cached;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setProposals(newProposals);
|
setProposals(newProposals);
|
||||||
|
setTransactions(newTransactions);
|
||||||
});
|
});
|
||||||
const subID = connection.onProgramAccountChange(
|
const subID = connection.onProgramAccountChange(
|
||||||
PROGRAM_IDS.timelock.programId,
|
PROGRAM_IDS.timelock.programId,
|
||||||
|
@ -112,7 +118,9 @@ function useSetupProposalsCache({
|
||||||
const cached =
|
const cached =
|
||||||
span === TimelockSetLayout.span
|
span === TimelockSetLayout.span
|
||||||
? (cache.get(info.accountId) as ParsedAccount<TimelockSet>)
|
? (cache.get(info.accountId) as ParsedAccount<TimelockSet>)
|
||||||
: (cache.get(info.accountId) as ParsedAccount<Transaction>);
|
: (cache.get(
|
||||||
|
info.accountId,
|
||||||
|
) as ParsedAccount<TimelockTransaction>);
|
||||||
setter((obj: any) => ({
|
setter((obj: any) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[typeof info.accountId === 'string'
|
[typeof info.accountId === 'string'
|
||||||
|
|
|
@ -60,7 +60,6 @@ export const addCustomSingleSignerTransactionInstruction = (
|
||||||
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
|
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
|
||||||
instructionAsBytes.push(0);
|
instructionAsBytes.push(0);
|
||||||
}
|
}
|
||||||
console.log('Num', new BN(slot).toNumber());
|
|
||||||
|
|
||||||
dataLayout.encode(
|
dataLayout.encode(
|
||||||
{
|
{
|
||||||
|
|
|
@ -203,13 +203,14 @@ export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.St
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface Transaction {
|
export interface TimelockTransaction {
|
||||||
version: number;
|
version: number;
|
||||||
|
|
||||||
slot: BN;
|
slot: BN;
|
||||||
|
|
||||||
instruction: string;
|
instruction: string;
|
||||||
}
|
}
|
||||||
export interface CustomSingleSignerTimelockTransaction extends Transaction {
|
export interface CustomSingleSignerTimelockTransaction
|
||||||
|
extends TimelockTransaction {
|
||||||
authorityKey: PublicKey;
|
authorityKey: PublicKey;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,9 @@ function InnerDummyView({
|
||||||
wallet,
|
wallet,
|
||||||
proposal,
|
proposal,
|
||||||
sigAccount.pubkey,
|
sigAccount.pubkey,
|
||||||
|
'123',
|
||||||
|
'12345',
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Card, Col, Divider, Row, Space, Spin, Statistic } from 'antd';
|
import { 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 {
|
import {
|
||||||
ConsensusAlgorithm,
|
ConsensusAlgorithm,
|
||||||
|
INSTRUCTION_LIMIT,
|
||||||
TimelockSet,
|
TimelockSet,
|
||||||
Transaction,
|
TimelockTransaction,
|
||||||
} from '../../models/timelock';
|
} 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';
|
||||||
|
@ -13,9 +14,8 @@ 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 { InstructionCard } from '../../components/Proposal/InstructionCard';
|
||||||
import './style.less';
|
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
|
||||||
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;
|
||||||
|
|
||||||
|
@ -50,8 +50,11 @@ function InnerProposalView({
|
||||||
proposal: ParsedAccount<TimelockSet>;
|
proposal: ParsedAccount<TimelockSet>;
|
||||||
sigMint: MintInfo;
|
sigMint: MintInfo;
|
||||||
votingMint: MintInfo;
|
votingMint: MintInfo;
|
||||||
instructions: Record<string, ParsedAccount<Transaction>>;
|
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
|
||||||
}) {
|
}) {
|
||||||
|
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
|
||||||
|
.map(k => instructions[k.toBase58()])
|
||||||
|
.filter(k => k);
|
||||||
const isUrl = !!proposal.info.state.descLink.match(urlRegex);
|
const isUrl = !!proposal.info.state.descLink.match(urlRegex);
|
||||||
const isGist =
|
const isGist =
|
||||||
!!proposal.info.state.descLink.match(/gist/i) &&
|
!!proposal.info.state.descLink.match(/gist/i) &&
|
||||||
|
@ -170,66 +173,28 @@ function InnerProposalView({
|
||||||
{ 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 => (
|
{instructionsForProposal.map((instruction, position) => (
|
||||||
<Col xs={24} sm={24} md={12} lg={8}>
|
<Col xs={24} sm={24} md={12} lg={8} key={position}>
|
||||||
<InstructionCard
|
<InstructionCard
|
||||||
position={position}
|
position={position + 1}
|
||||||
instruction={{
|
instruction={instruction}
|
||||||
info: {
|
|
||||||
slot: 5,
|
|
||||||
instruction:
|
|
||||||
'Dhr8DF4Wsr9WrwSJixqfe7YFwdgz3GtjTRGF4wRMw3rP91NUmzR8PGJvCw7CpFDnUMij5kSpWpocaAftpCxBJ1oneK89CeWXGEVhyYTxAnhudvUSCXtvvjNhiFC8NQhCHDskA6MdsGJP2Dd1w3kihZeB5N9sXT7NjwZCCZeH5qL5tFhgUja9Nv1ywGvU1WnXDwxsRvPGf5AQNZ5xwwrr7chFV7DoTnwc9D7dJwx3m9mMBdSUcbtP77pUxvZbN1Sw9CTpM2rUUvwSxy2cYJPcgnNKZyq3CPYCa9H2Z9KR2AigcMgRzYswYhBGiz8piMVYL1rQRmRJVnoWALh3K1rS3BPRmvq52G7EJzXdoKiQQU6zzdzFuP6TGvdJrwfppzZrFRQ3n6bb3RaBoqoopnrLwWQA673KEeJCngwnipnTo5K6i72jJBBniqmanmtuFhVne2Gr2QUxDynqwBRvbpBT4uAibmu6PRUL9BhVDJibrb7rJf9RY2TL',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
|
{instructionsForProposal.length < INSTRUCTION_LIMIT && (
|
||||||
|
<Col xs={24} sm={24} md={12} lg={8}>
|
||||||
|
<NewInstructionCard
|
||||||
|
proposal={proposal}
|
||||||
|
position={instructionsForProposal.length}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</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;
|
||||||
|
|
Loading…
Reference in New Issue