Added sign button and command

This commit is contained in:
Dummy Tester 123 2021-02-26 16:19:27 -06:00
parent 1883bfd8ee
commit 1c2d1260e0
8 changed files with 221 additions and 25 deletions

View File

@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
name: 'devnet',
timelock: () => ({
programAccountId: new PublicKey(
'H4yVRsmzbbE9fMUREPQWsu7auiYgE398iF5rq5nHLdq4',
'BNRKDb6vrbfYE4hyALrVLa9V38U2YE9cHMc1RpazG2EG',
),
programId: new PublicKey('2NKCKpE1kNhY3Qr3VGikPQtiENTjSEFsJN6NyAUpoFaD'),
programId: new PublicKey('CcaR57vwHPJ2BJdErjQ42dchFFuvhnxG1jeTxWZwAjjs'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

View File

@ -0,0 +1,75 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import { contexts, utils, models, ParsedAccount } from '@oyster/common';
import { TimelockSet } from '../models/timelock';
import { signInstruction } from '../models/sign';
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
const { approve } = models;
export const sign = async (
connection: Connection,
wallet: any,
proposal: ParsedAccount<TimelockSet>,
sigAccount: PublicKey,
) => {
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const [mintAuthority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
[],
sigAccount,
wallet.publicKey,
1,
);
signers.push(transferAuthority);
instructions.push(
signInstruction(
proposal.pubkey,
sigAccount,
proposal.info.signatoryMint,
transferAuthority.publicKey,
mintAuthority,
),
);
notify({
message: 'Signing proposal...',
description: 'Please wait...',
type: 'warn',
});
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: 'Proposal signed.',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -48,30 +48,26 @@ export function NewInstructionCard({
form.resetFields();
}
};
return (
return !sigAccount ? null : (
<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>
)}
<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>
);
}

View File

@ -0,0 +1,65 @@
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { ParsedAccount, hooks, contexts, utils } from '@oyster/common';
import { Button, Modal } from 'antd';
import React from 'react';
import { sign } from '../../actions/sign';
import { TimelockSet } from '../../models/timelock';
const { confirm } = Modal;
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
const { useAccountByMint } = hooks;
const { notify } = utils;
export default function SignButton({
proposal,
}: {
proposal: ParsedAccount<TimelockSet>;
}) {
const wallet = useWallet();
const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
return (
<>
<br />
{sigAccount && sigAccount.info.amount.toNumber() === 0 && (
<Button disabled={true} type="primary">
Signed
</Button>
)}
{sigAccount && sigAccount.info.amount.toNumber() > 0 && (
<Button
type="primary"
onClick={() => {
confirm({
title: 'Do you want to sign this proposal?',
icon: <ExclamationCircleOutlined />,
content: 'This is a non-reversible action.',
onOk() {
if (!sigAccount) {
notify({
message: 'Signature account is not defined',
type: 'error',
});
return;
}
return sign(
connection,
wallet.wallet,
proposal,
sigAccount.pubkey,
);
},
onCancel() {
// no-op
},
});
}}
>
Sign
</Button>
)}
</>
);
}

View File

@ -0,0 +1,55 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
/// [Requires Signatory token]
/// Burns signatory token, indicating you approve of moving this Timelock set from Draft state to Voting state.
/// The last Signatory token to be burned moves the state to Voting.
///
/// 0. `[writable]` Timelock set account pub key.
/// 1. `[writable]` Signatory account
/// 2. `[writable]` Signatory mint account.
/// 3. `[]` Transfer authority
/// 4. `[]` Timelock mint authority
/// 5. `[]` Timelock program account pub key.
/// 6. `[]` Token program account.
export const signInstruction = (
timelockSetAccount: PublicKey,
signatoryAccount: PublicKey,
signatoryMintAccount: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.Sign,
},
data,
);
const keys = [
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryMintAccount, isSigner: false, isWritable: true },
{ pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -12,6 +12,7 @@ export const TRANSACTION_SLOTS = 10;
export enum TimelockInstruction {
InitTimelockSet = 1,
addCustomSingleSignerTransaction = 4,
Sign = 8,
}
export interface TimelockConfig {

View File

@ -1,4 +1,4 @@
import { Col, Divider, Row, Space, Spin, Statistic } from 'antd';
import { Button, Col, Divider, Row, Space, Spin, Statistic } from 'antd';
import React, { useMemo, useState } from 'react';
import { LABELS } from '../../constants';
import { ParsedAccount } from '@oyster/common';
@ -12,12 +12,14 @@ 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 { contexts, hooks } from '@oyster/common';
import { MintInfo } from '@solana/spl-token';
import { InstructionCard } from '../../components/Proposal/InstructionCard';
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
import SignButton from '../../components/Proposal/SignButton';
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 { useAccountByMint } = hooks;
export const ProposalView = () => {
const context = useProposals();
@ -52,6 +54,7 @@ function InnerProposalView({
votingMint: MintInfo;
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
}) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
.map(k => instructions[k.toBase58()])
.filter(k => k);
@ -148,6 +151,7 @@ function InnerProposalView({
}
suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`}
/>
{sigAccount && <SignButton proposal={proposal} />}
</Col>
<Col span={8}>
<Statistic

View File

@ -36,7 +36,7 @@ export function NewFormMenuItem() {
};
if (redirect) {
setRedirect('');
setTimeout(() => setRedirect(''), 100);
return <Redirect push to={'/proposal/' + redirect} />;
}