From 1c2d1260e07289c8316c56eb5c06ecc3321b1053 Mon Sep 17 00:00:00 2001 From: Dummy Tester 123 Date: Fri, 26 Feb 2021 16:19:27 -0600 Subject: [PATCH] Added sign button and command --- packages/common/src/utils/ids.ts | 4 +- packages/proposals/src/actions/sign.ts | 75 +++++++++++++++++++ .../Proposal/NewInstructionCard.tsx | 36 ++++----- .../src/components/Proposal/SignButton.tsx | 65 ++++++++++++++++ packages/proposals/src/models/sign.ts | 55 ++++++++++++++ packages/proposals/src/models/timelock.ts | 1 + .../proposals/src/views/proposal/index.tsx | 8 +- packages/proposals/src/views/proposal/new.tsx | 2 +- 8 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 packages/proposals/src/actions/sign.ts create mode 100644 packages/proposals/src/components/Proposal/SignButton.tsx create mode 100644 packages/proposals/src/models/sign.ts diff --git a/packages/common/src/utils/ids.ts b/packages/common/src/utils/ids.ts index 6222797..144ea08 100644 --- a/packages/common/src/utils/ids.ts +++ b/packages/common/src/utils/ids.ts @@ -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'), diff --git a/packages/proposals/src/actions/sign.ts b/packages/proposals/src/actions/sign.ts new file mode 100644 index 0000000..0cb75c9 --- /dev/null +++ b/packages/proposals/src/actions/sign.ts @@ -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, + 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(); + } +}; diff --git a/packages/proposals/src/components/Proposal/NewInstructionCard.tsx b/packages/proposals/src/components/Proposal/NewInstructionCard.tsx index d5fcbb9..7c96d5d 100644 --- a/packages/proposals/src/components/Proposal/NewInstructionCard.tsx +++ b/packages/proposals/src/components/Proposal/NewInstructionCard.tsx @@ -48,30 +48,26 @@ export function NewInstructionCard({ form.resetFields(); } }; - return ( + return !sigAccount ? null : ( ]} > - {!sigAccount ? ( - - ) : ( -
- - - - - - -
- )} +
+ + + + + + +
); } diff --git a/packages/proposals/src/components/Proposal/SignButton.tsx b/packages/proposals/src/components/Proposal/SignButton.tsx new file mode 100644 index 0000000..3477441 --- /dev/null +++ b/packages/proposals/src/components/Proposal/SignButton.tsx @@ -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; +}) { + const wallet = useWallet(); + const connection = useConnection(); + const sigAccount = useAccountByMint(proposal.info.signatoryMint); + return ( + <> +
+ {sigAccount && sigAccount.info.amount.toNumber() === 0 && ( + + )} + {sigAccount && sigAccount.info.amount.toNumber() > 0 && ( + + )} + + ); +} diff --git a/packages/proposals/src/models/sign.ts b/packages/proposals/src/models/sign.ts new file mode 100644 index 0000000..f02cf4c --- /dev/null +++ b/packages/proposals/src/models/sign.ts @@ -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, + }); +}; diff --git a/packages/proposals/src/models/timelock.ts b/packages/proposals/src/models/timelock.ts index 439c92d..2252fc7 100644 --- a/packages/proposals/src/models/timelock.ts +++ b/packages/proposals/src/models/timelock.ts @@ -12,6 +12,7 @@ export const TRANSACTION_SLOTS = 10; export enum TimelockInstruction { InitTimelockSet = 1, addCustomSingleSignerTransaction = 4, + Sign = 8, } export interface TimelockConfig { diff --git a/packages/proposals/src/views/proposal/index.tsx b/packages/proposals/src/views/proposal/index.tsx index a7c4703..f3a406e 100644 --- a/packages/proposals/src/views/proposal/index.tsx +++ b/packages/proposals/src/views/proposal/index.tsx @@ -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>; }) { + const sigAccount = useAccountByMint(proposal.info.signatoryMint); const instructionsForProposal: ParsedAccount[] = 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 && } setRedirect(''), 100); return ; }