mirror of https://github.com/certusone/oyster.git
Added sign button and command
This commit is contained in:
parent
1883bfd8ee
commit
1c2d1260e0
|
@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
|
||||||
name: 'devnet',
|
name: 'devnet',
|
||||||
timelock: () => ({
|
timelock: () => ({
|
||||||
programAccountId: new PublicKey(
|
programAccountId: new PublicKey(
|
||||||
'H4yVRsmzbbE9fMUREPQWsu7auiYgE398iF5rq5nHLdq4',
|
'BNRKDb6vrbfYE4hyALrVLa9V38U2YE9cHMc1RpazG2EG',
|
||||||
),
|
),
|
||||||
programId: new PublicKey('2NKCKpE1kNhY3Qr3VGikPQtiENTjSEFsJN6NyAUpoFaD'),
|
programId: new PublicKey('CcaR57vwHPJ2BJdErjQ42dchFFuvhnxG1jeTxWZwAjjs'),
|
||||||
}),
|
}),
|
||||||
wormhole: () => ({
|
wormhole: () => ({
|
||||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -48,30 +48,26 @@ export function NewInstructionCard({
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return !sigAccount ? null : (
|
||||||
<Card
|
<Card
|
||||||
title="New Instruction"
|
title="New Instruction"
|
||||||
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
|
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
|
||||||
>
|
>
|
||||||
{!sigAccount ? (
|
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
|
||||||
<Spin />
|
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
|
||||||
) : (
|
<Input maxLength={64} />
|
||||||
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
|
</Form.Item>
|
||||||
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
|
<Form.Item
|
||||||
<Input maxLength={64} />
|
name="instruction"
|
||||||
</Form.Item>
|
label="Instruction"
|
||||||
<Form.Item
|
rules={[{ required: true }]}
|
||||||
name="instruction"
|
>
|
||||||
label="Instruction"
|
<Input
|
||||||
rules={[{ required: true }]}
|
maxLength={INSTRUCTION_LIMIT}
|
||||||
>
|
placeholder={'Base58 encoded instruction'}
|
||||||
<Input
|
/>
|
||||||
maxLength={INSTRUCTION_LIMIT}
|
</Form.Item>
|
||||||
placeholder={'Base58 encoded instruction'}
|
</Form>
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,6 +12,7 @@ export const TRANSACTION_SLOTS = 10;
|
||||||
export enum TimelockInstruction {
|
export enum TimelockInstruction {
|
||||||
InitTimelockSet = 1,
|
InitTimelockSet = 1,
|
||||||
addCustomSingleSignerTransaction = 4,
|
addCustomSingleSignerTransaction = 4,
|
||||||
|
Sign = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelockConfig {
|
export interface TimelockConfig {
|
||||||
|
|
|
@ -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 React, { useMemo, useState } from 'react';
|
||||||
import { LABELS } from '../../constants';
|
import { LABELS } from '../../constants';
|
||||||
import { ParsedAccount } from '@oyster/common';
|
import { ParsedAccount } from '@oyster/common';
|
||||||
|
@ -12,12 +12,14 @@ 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, hooks } from '@oyster/common';
|
||||||
import { MintInfo } from '@solana/spl-token';
|
import { MintInfo } from '@solana/spl-token';
|
||||||
import { InstructionCard } from '../../components/Proposal/InstructionCard';
|
import { InstructionCard } from '../../components/Proposal/InstructionCard';
|
||||||
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
|
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()@:%_\+.~#?&//=]*)/;
|
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;
|
||||||
|
const { useAccountByMint } = hooks;
|
||||||
|
|
||||||
export const ProposalView = () => {
|
export const ProposalView = () => {
|
||||||
const context = useProposals();
|
const context = useProposals();
|
||||||
|
@ -52,6 +54,7 @@ function InnerProposalView({
|
||||||
votingMint: MintInfo;
|
votingMint: MintInfo;
|
||||||
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
|
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
|
||||||
}) {
|
}) {
|
||||||
|
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
|
||||||
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
|
const instructionsForProposal: ParsedAccount<TimelockTransaction>[] = proposal.info.state.timelockTransactions
|
||||||
.map(k => instructions[k.toBase58()])
|
.map(k => instructions[k.toBase58()])
|
||||||
.filter(k => k);
|
.filter(k => k);
|
||||||
|
@ -148,6 +151,7 @@ function InnerProposalView({
|
||||||
}
|
}
|
||||||
suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`}
|
suffix={`/ ${proposal.info.state.totalSigningTokensMinted.toNumber()}`}
|
||||||
/>
|
/>
|
||||||
|
{sigAccount && <SignButton proposal={proposal} />}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function NewFormMenuItem() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
setRedirect('');
|
setTimeout(() => setRedirect(''), 100);
|
||||||
return <Redirect push to={'/proposal/' + redirect} />;
|
return <Redirect push to={'/proposal/' + redirect} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue