diff --git a/packages/common/src/utils/ids.ts b/packages/common/src/utils/ids.ts index 41582f9..d149880 100644 --- a/packages/common/src/utils/ids.ts +++ b/packages/common/src/utils/ids.ts @@ -74,9 +74,9 @@ export const PROGRAM_IDS = [ name: 'testnet', timelock: () => ({ programAccountId: new PublicKey( - '32siiZ12jH2i7BM2wtNuTDw2Hs5nBjVQuBh4P8cTSH3i', + '7CxEuz8Qtius9aCyJqGnWZyBNvf6WTTNmA8G26BdMTSF', ), - programId: new PublicKey('5KrVJvesyjdMy6Vq5wfuPSMdw7vWuUvtbHG98wBsEkX6'), + programId: new PublicKey('8DevpkpN6CsdczP6rQ64CHraApXFrq96oGm4VjSNCs4q'), }), wormhole: () => ({ pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'), @@ -95,9 +95,9 @@ export const PROGRAM_IDS = [ name: 'devnet', timelock: () => ({ programAccountId: new PublicKey( - 'Gc6ktQPgHDxf9GpN6CdLEnkkqj5NfGYJeLDWFLoN3wNb', + '8KkpkoDAQaQqjnkCtNXAyk2A8GLmsmWPjBLK7jmahhxZ', ), - programId: new PublicKey('FmxAXMEKaj7BvgH9zdRNMZZYdAk4mBeRdSQwUoM3QYYw'), + programId: new PublicKey('7SH5hE7uBecnfMpGjdPyJupgBhFHaXcNMCEgJbmoVV7t'), }), wormhole: () => ({ pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), diff --git a/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts b/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts index 82c71c2..1c0bf1e 100644 --- a/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts +++ b/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts @@ -89,6 +89,7 @@ export const addCustomSingleSignerTransaction = async ( proposal.pubkey, sigAccount, proposal.info.signatoryValidation, + proposal.info.config, transferAuthority.publicKey, authority, slot, diff --git a/packages/proposals/src/actions/createProposal.ts b/packages/proposals/src/actions/createProposal.ts index 68ddaad..2618cc2 100644 --- a/packages/proposals/src/actions/createProposal.ts +++ b/packages/proposals/src/actions/createProposal.ts @@ -70,7 +70,6 @@ export const createProposal = async ( space: TimelockSetLayout.span, programId: PROGRAM_IDS.timelock.programId, }); - signers.push(timelockSetKey); instructions.push(uninitializedTimelockSetInstruction); diff --git a/packages/proposals/src/actions/registerProgramGovernance.ts b/packages/proposals/src/actions/registerProgramGovernance.ts index f5f0d31..71c9118 100644 --- a/packages/proposals/src/actions/registerProgramGovernance.ts +++ b/packages/proposals/src/actions/registerProgramGovernance.ts @@ -111,6 +111,7 @@ export const registerProgramGovernance = async ( TimelockType.CustomSingleSignerV1, uninitializedTimelockConfig.votingEntryRule || VotingEntryRule.Anytime, uninitializedTimelockConfig.minimumSlotWaitingPeriod || new BN(0), + uninitializedTimelockConfig.timeLimit || new BN(0), uninitializedTimelockConfig.name || '', ), ); diff --git a/packages/proposals/src/components/Proposal/InstructionCard.tsx b/packages/proposals/src/components/Proposal/InstructionCard.tsx index 75d83a7..9a41f58 100644 --- a/packages/proposals/src/components/Proposal/InstructionCard.tsx +++ b/packages/proposals/src/components/Proposal/InstructionCard.tsx @@ -11,6 +11,7 @@ import { Card, Spin } from 'antd'; import Meta from 'antd/lib/card/Meta'; import React, { useState } from 'react'; import { execute } from '../../actions/execute'; +import { LABELS } from '../../constants'; import { TimelockSet, TimelockStateStatus, @@ -47,7 +48,9 @@ export function InstructionCard({ description={ <>

Instruction: TODO

-

Slot: {instruction.info.slot.toNumber()}

+

+ {LABELS.DELAY}: {instruction.info.slot.toNumber()} +

} /> @@ -108,7 +111,8 @@ function PlayStatusButton({ }; if (proposal.info.state.status != TimelockStateStatus.Executing) return null; - if (currSlot < instruction.info.slot.toNumber()) return null; + const elapsedTime = currSlot - proposal.info.state.votingEndedAt.toNumber(); + if (elapsedTime < instruction.info.slot.toNumber()) return null; if (playing === Playstate.Unplayed) return ( diff --git a/packages/proposals/src/components/Proposal/NewInstructionCard.tsx b/packages/proposals/src/components/Proposal/NewInstructionCard.tsx index e54403f..39312f8 100644 --- a/packages/proposals/src/components/Proposal/NewInstructionCard.tsx +++ b/packages/proposals/src/components/Proposal/NewInstructionCard.tsx @@ -1,11 +1,16 @@ import React, { useState } from 'react'; import { Card, Progress, Spin } from 'antd'; import { Form, Input } from 'antd'; -import { INSTRUCTION_LIMIT, TimelockSet } from '../../models/timelock'; +import { + INSTRUCTION_LIMIT, + TimelockConfig, + TimelockSet, +} from '../../models/timelock'; import { contexts, ParsedAccount, hooks, utils } from '@oyster/common'; import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction'; import { SaveOutlined } from '@ant-design/icons'; import { Connection, PublicKey } from '@solana/web3.js'; +import { LABELS } from '../../constants'; const { useWallet } = contexts.Wallet; const { useConnection } = contexts.Connection; @@ -25,8 +30,10 @@ enum UploadType { export function NewInstructionCard({ proposal, position, + config, }: { proposal: ParsedAccount; + config: ParsedAccount; position: number; }) { const [form] = Form.useForm(); @@ -41,7 +48,19 @@ export function NewInstructionCard({ }) => { if (!values.slot.match(/^\d*$/)) { notify({ - message: 'Slot can only be numeric', + message: LABELS.SLOT_MUST_BE_NUMERIC, + type: 'error', + }); + return; + } + + if ( + parseInt(values.slot) < config.info.minimumSlotWaitingPeriod.toNumber() + ) { + notify({ + message: + LABELS.SLOT_MUST_BE_GREATER_THAN + + config.info.minimumSlotWaitingPeriod.toString(), type: 'error', }); return; @@ -69,7 +88,11 @@ export function NewInstructionCard({ actions={[]} >
- + { const PROGRAM_IDS = utils.programIds(); @@ -39,6 +40,7 @@ export const initTimelockConfigInstruction = ( BufferLayout.u8('timelockType'), BufferLayout.u8('votingEntryRule'), Layout.uint64('minimumSlotWaitingPeriod'), + Layout.uint64('timeLimit'), BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'), ]); @@ -57,6 +59,7 @@ export const initTimelockConfigInstruction = ( timelockType, votingEntryRule, minimumSlotWaitingPeriod, + timeLimit, name: nameAsBytes, }, data, diff --git a/packages/proposals/src/models/sign.ts b/packages/proposals/src/models/sign.ts index f02cf4c..5d7e6f6 100644 --- a/packages/proposals/src/models/sign.ts +++ b/packages/proposals/src/models/sign.ts @@ -1,4 +1,8 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + TransactionInstruction, +} from '@solana/web3.js'; import { utils } from '@oyster/common'; import * as BufferLayout from 'buffer-layout'; import { TimelockInstruction } from './timelock'; @@ -14,6 +18,7 @@ import { TimelockInstruction } from './timelock'; /// 4. `[]` Timelock mint authority /// 5. `[]` Timelock program account pub key. /// 6. `[]` Token program account. +/// 7. `[]` Clock sysvar. export const signInstruction = ( timelockSetAccount: PublicKey, signatoryAccount: PublicKey, @@ -46,6 +51,7 @@ export const signInstruction = ( isWritable: false, }, { pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, ]; return new TransactionInstruction({ keys, diff --git a/packages/proposals/src/models/timelock.ts b/packages/proposals/src/models/timelock.ts index 50a3212..83d122a 100644 --- a/packages/proposals/src/models/timelock.ts +++ b/packages/proposals/src/models/timelock.ts @@ -8,7 +8,7 @@ export const DESC_SIZE = 200; export const NAME_SIZE = 32; export const CONFIG_NAME_LENGTH = 32; export const INSTRUCTION_LIMIT = 450; -export const TRANSACTION_SLOTS = 5; +export const TRANSACTION_SLOTS = 4; export const TEMP_FILE_TXN_SIZE = 1000; export enum TimelockInstruction { @@ -43,6 +43,8 @@ export interface TimelockConfig { governanceMint: PublicKey; /// Program ID that is tied to this config (optional) program: PublicKey; + /// Time limit in slots for proposal to be open to voting + timeLimit: BN; /// Optional name name: string; } @@ -57,6 +59,7 @@ export const TimelockConfigLayout: typeof BufferLayout.Structure = BufferLayout. Layout.uint64('minimumSlotWaitingPeriod'), Layout.publicKey('governanceMint'), Layout.publicKey('program'), + Layout.uint64('timeLimit'), BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'), ], ); @@ -95,6 +98,9 @@ export enum TimelockStateStatus { /// Deleted Deleted = 4, + + /// Defeated + Defeated = 5, } export const STATE_COLOR: Record = { @@ -103,6 +109,7 @@ export const STATE_COLOR: Record = { [TimelockStateStatus.Executing]: 'green', [TimelockStateStatus.Completed]: 'purple', [TimelockStateStatus.Deleted]: 'gray', + [TimelockStateStatus.Defeated]: 'red', }; export interface TimelockState { @@ -111,6 +118,10 @@ export interface TimelockState { timelockTransactions: PublicKey[]; name: string; descLink: string; + votingEndedAt: BN; + votingBeganAt: BN; + executions: number; + usedTxnSlots: number; } const timelockTxns = []; @@ -137,6 +148,10 @@ export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.str Layout.uint64('totalSigningTokensMinted'), BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'), BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'), + Layout.uint64('votingEndedAt'), + Layout.uint64('votingBeganAt'), + BufferLayout.u8('executions'), + BufferLayout.u8('usedTxnSlots'), ...timelockTxns, ], ); @@ -248,6 +263,10 @@ export const TimelockSetParser = ( descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''), name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''), timelockTransactions: timelockTxns, + votingEndedAt: data.votingEndedAt, + votingBeganAt: data.votingBeganAt, + executions: data.executions, + usedTxnSlots: data.usedTxnSlots, }, }, }; @@ -298,9 +317,10 @@ export const TimelockConfigParser = ( executionType: data.executionType, timelockType: data.timelockType, votingEntryRule: data.votingEntryRule, - minimimSlotWaitingPeriod: data.minimimSlotWaitingPeriod, + minimumSlotWaitingPeriod: data.minimumSlotWaitingPeriod, governanceMint: data.governanceMint, program: data.program, + timeLimit: data.timeLimit, name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''), }, }; diff --git a/packages/proposals/src/models/vote.ts b/packages/proposals/src/models/vote.ts index 5f8912a..286325b 100644 --- a/packages/proposals/src/models/vote.ts +++ b/packages/proposals/src/models/vote.ts @@ -1,4 +1,8 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + TransactionInstruction, +} from '@solana/web3.js'; import { utils } from '@oyster/common'; import * as Layout from '../utils/layout'; @@ -23,6 +27,7 @@ import BN from 'bn.js'; /// 10. `[]` Timelock program mint authority /// 11. `[]` Timelock program account pub key. /// 12. `[]` Token program account. +/// 13. `[]` Clock sysvar. export const voteInstruction = ( timelockSetAccount: PublicKey, votingAccount: PublicKey, @@ -75,6 +80,7 @@ export const voteInstruction = ( isWritable: false, }, { pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, ]; return new TransactionInstruction({ diff --git a/packages/proposals/src/views/governance/index.tsx b/packages/proposals/src/views/governance/index.tsx index 4b39f79..ff1c638 100644 --- a/packages/proposals/src/views/governance/index.tsx +++ b/packages/proposals/src/views/governance/index.tsx @@ -20,6 +20,7 @@ import { import { PublicKey } from '@solana/web3.js'; import { Table } from 'antd'; import MintGovernanceTokens from '../../components/Proposal/MintGovernanceTokens'; +import BN from 'bn.js'; const { useUserAccounts } = hooks; const columns = [ { @@ -47,14 +48,21 @@ const columns = [ }, { title: LABELS.VOTING_ENTRY_RULES, - dataIndex: 'votingEntryRules', - key: 'votingEntryRules', + dataIndex: 'votingEntryRule', + key: 'votingEntryRule', render: (number: number) => {VotingEntryRule[number]}, }, { title: LABELS.MINIMUM_SLOT_WAITING_PERIOD, dataIndex: 'minimumSlotWaitingPeriod', key: 'minimumSlotWaitingPeriod', + render: (number: BN) => {number.toNumber()}, + }, + { + title: LABELS.TIME_LIMIT, + dataIndex: 'timeLimit', + key: 'timeLimit', + render: (number: BN) => {number.toNumber()}, }, { title: LABELS.GOVERNANCE_MINT, diff --git a/packages/proposals/src/views/governance/register.tsx b/packages/proposals/src/views/governance/register.tsx index 512fa8a..0b64a1a 100644 --- a/packages/proposals/src/views/governance/register.tsx +++ b/packages/proposals/src/views/governance/register.tsx @@ -79,18 +79,25 @@ export function NewForm({ consensusAlgorithm: ConsensusAlgorithm; votingEntryRule: VotingEntryRule; minimumSlotWaitingPeriod: string; + timeLimit: string; governanceMint: string; program: string; name: string; }) => { if (!values.minimumSlotWaitingPeriod.match(/^\d*$/)) { notify({ - message: 'Minimum Slot Waiting Period can only be numeric', + message: LABELS.MIN_SLOT_MUST_BE_NUMERIC, + type: 'error', + }); + return; + } + if (!values.timeLimit.match(/^\d*$/)) { + notify({ + message: LABELS.TIME_LIMIT_MUST_BE_NUMERIC, type: 'error', }); return; } - const uninitializedConfig = { timelockType: values.timelockType, executionType: values.executionType, @@ -102,6 +109,7 @@ export function NewForm({ : undefined, program: new PublicKey(values.program), name: values.name, + timeLimit: new BN(values.timeLimit), }; const newConfig = await registerProgramGovernance( @@ -143,6 +151,13 @@ export function NewForm({ > + + +