This commit is contained in:
bartosz-lipinski 2021-03-19 20:55:19 -05:00
commit a5468636ca
23 changed files with 231 additions and 75 deletions

View File

@ -1,14 +1,28 @@
import React from 'react'; import React from 'react';
import { Button } from 'antd'; import { Button, Dropdown, Menu } from 'antd';
import { useCorrectNetwork } from '../../hooks/useCorrectNetwork'; import { useCorrectNetwork } from '../../hooks/useCorrectNetwork';
import { shortenAddress } from '@oyster/common'; import { shortenAddress } from '@oyster/common';
import { useEthereum } from '../../contexts'; import { useEthereum } from '../../contexts';
export const EthereumConnect = () => { export const EthereumConnect = () => {
const { accounts, onConnectEthereum, connected, walletProvider } = useEthereum(); const {
accounts,
onConnectEthereum,
connected,
walletProvider,
select,
} = useEthereum();
const { hasCorrespondingNetworks } = useCorrectNetwork(); const { hasCorrespondingNetworks } = useCorrectNetwork();
const menu = (
<Menu>
<Menu.Item key="3" onClick={select}>
Change Eth Wallet
</Menu.Item>
</Menu>
);
return ( return (
<div style={{ marginRight: 8 }}> <div style={{ marginRight: 8 }}>
{connected ? ( {connected ? (
@ -28,6 +42,13 @@ export const EthereumConnect = () => {
WRONG NETWORK WRONG NETWORK
</Button> </Button>
) )
) : !!walletProvider ? (
<Dropdown.Button
onClick={() => onConnectEthereum && onConnectEthereum()}
overlay={menu}
>
Connect Ethereum
</Dropdown.Button>
) : ( ) : (
<Button onClick={() => onConnectEthereum && onConnectEthereum()}> <Button onClick={() => onConnectEthereum && onConnectEthereum()}>
Connect Ethereum Connect Ethereum

View File

@ -9,10 +9,12 @@ import { AppBar } from '@oyster/common';
import Wormhole from '../Wormhole'; import Wormhole from '../Wormhole';
import { Footer as AppFooter } from './../Footer'; import { Footer as AppFooter } from './../Footer';
import { EthereumConnect } from '../EthereumConnect'; import { EthereumConnect } from '../EthereumConnect';
import { useEthereum } from '../../contexts';
const { Header, Content, Footer } = Layout; const { Header, Content, Footer } = Layout;
export const AppLayout = React.memo((props: any) => { export const AppLayout = React.memo((props: any) => {
const { connected, disconnect } = useEthereum();
const location = useLocation(); const location = useLocation();
const [wormholeReady, setWormholeReady] = useState(false); const [wormholeReady, setWormholeReady] = useState(false);
@ -41,7 +43,21 @@ export const AppLayout = React.memo((props: any) => {
<h2>WORMHOLE</h2> <h2>WORMHOLE</h2>
</Link> </Link>
</div> </div>
<AppBar useWalletBadge={true} left={<EthereumConnect />} /> <AppBar
additionalSettings={
connected ? (
<Button
type="primary"
onClick={() => disconnect()}
style={{ marginTop: '8px' }}
>
Disconnect ETH
</Button>
) : undefined
}
useWalletBadge={true}
left={<EthereumConnect />}
/>
</Header> </Header>
)} )}
<Content style={{ padding: '0 50px', flexDirection: 'column' }}> <Content style={{ padding: '0 50px', flexDirection: 'column' }}>

View File

@ -119,7 +119,7 @@ export const TokenSelectModal = (props: {
visible={isModalVisible} visible={isModalVisible}
onCancel={() => hideModal()} onCancel={() => hideModal()}
footer={null} footer={null}
className={"token-select-modal"} className={'token-select-modal'}
> >
<Input <Input
autoFocus autoFocus

View File

@ -1,8 +1,4 @@
import React, { import React, { createContext, FunctionComponent, useContext } from 'react';
createContext,
FunctionComponent,
useContext,
} from 'react';
import { SolanaBridge } from '../core'; import { SolanaBridge } from '../core';
import { import {
useConnection, useConnection,

View File

@ -14,13 +14,13 @@ import { useWallet as useEthereumWallet } from 'use-wallet';
import WalletConnectProvider from '@walletconnect/web3-provider'; import WalletConnectProvider from '@walletconnect/web3-provider';
// @ts-ignore // @ts-ignore
import Fortmatic from 'fortmatic'; import Fortmatic from 'fortmatic';
import { useWallet, useLocalStorageState} from '@oyster/common'; import { useWallet, useLocalStorageState } from '@oyster/common';
import { WalletAdapter } from '@solana/wallet-base' import { WalletAdapter } from '@solana/wallet-base';
import { TokenList, TokenInfo } from '@uniswap/token-lists'; import { TokenList, TokenInfo } from '@uniswap/token-lists';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { MetamaskWalletAdapter } from '../wallet-adapters/metamask'; import { MetamaskWalletAdapter } from '../wallet-adapters/metamask';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import {WalletConnectWalletAdapter} from "../wallet-adapters/wallet-connect"; import { WalletConnectWalletAdapter } from '../wallet-adapters/wallet-connect';
const ASSETS_URL = const ASSETS_URL =
'https://raw.githubusercontent.com/solana-labs/oyster/main/assets/wallets/'; 'https://raw.githubusercontent.com/solana-labs/oyster/main/assets/wallets/';
@ -48,6 +48,8 @@ export interface EthereumContextState {
connected: boolean; connected: boolean;
chainId: number; chainId: number;
walletProvider: any; walletProvider: any;
select: () => void;
disconnect: () => void;
onConnectEthereum?: () => void; onConnectEthereum?: () => void;
} }
@ -57,6 +59,8 @@ export const EthereumContext = createContext<EthereumContextState>({
accounts: [''], accounts: [''],
chainId: 0, chainId: 0,
connected: false, connected: false,
select() {},
disconnect() {},
walletProvider: null, walletProvider: null,
}); });
@ -142,12 +146,12 @@ export const EthereumProvider: FunctionComponent = ({ children }) => {
}, [setTokens]); }, [setTokens]);
const onConnectEthereum = useCallback(() => { const onConnectEthereum = useCallback(() => {
if (wallet && providerUrl) { if (wallet && providerUrl && !connected) {
wallet.connect(); wallet.connect();
} else { } else if (!connected) {
select(); select();
} }
}, [wallet, providerUrl]); }, [wallet, connected, providerUrl]);
useEffect(() => { useEffect(() => {
if (wallet) { if (wallet) {
@ -162,6 +166,11 @@ export const EthereumProvider: FunctionComponent = ({ children }) => {
}); });
wallet.on('disconnect', error => { wallet.on('disconnect', error => {
setConnected(false); setConnected(false);
setAccounts([]);
// @ts-ignore
setChainId(0);
// @ts-ignore
setProvider(null);
}); });
// @ts-ignore // @ts-ignore
wallet.on('accountsChanged', accounts => { wallet.on('accountsChanged', accounts => {
@ -184,10 +193,10 @@ export const EthereumProvider: FunctionComponent = ({ children }) => {
const close = useCallback(() => setIsModalVisible(false), []); const close = useCallback(() => setIsModalVisible(false), []);
useEffect(() => { useEffect(() => {
if (walletConnected && !connected) { if (walletConnected) {
onConnectEthereum(); onConnectEthereum();
} }
}, [walletConnected, connected, providerUrl]); }, [walletConnected, providerUrl]);
return ( return (
<EthereumContext.Provider <EthereumContext.Provider
@ -199,6 +208,8 @@ export const EthereumProvider: FunctionComponent = ({ children }) => {
connected, connected,
chainId, chainId,
walletProvider, walletProvider,
select,
disconnect: () => wallet?.disconnect(),
onConnectEthereum: () => onConnectEthereum(), onConnectEthereum: () => onConnectEthereum(),
}} }}
> >

View File

@ -81,7 +81,7 @@ export const HomeView = () => {
style: {}, style: {},
}, },
children: ( children: (
<a href={record.explorer} target="_blank" rel="noopener noreferrer" > <a href={record.explorer} target="_blank" rel="noopener noreferrer">
{shortenAddress(text, 6)} {shortenAddress(text, 6)}
</a> </a>
), ),
@ -98,7 +98,11 @@ export const HomeView = () => {
style: {}, style: {},
}, },
children: ( children: (
<a href={record.wrappedExplorer} target="_blank" rel="noopener noreferrer" > <a
href={record.wrappedExplorer}
target="_blank"
rel="noopener noreferrer"
>
{shortenAddress(text, 6)} {shortenAddress(text, 6)}
</a> </a>
), ),

View File

@ -1,7 +1,7 @@
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { PublicKey, Transaction } from '@solana/web3.js'; import { PublicKey, Transaction } from '@solana/web3.js';
import { notify } from '@oyster/common'; import { notify } from '@oyster/common';
import { WalletAdapter } from '@solana/wallet-base' import { WalletAdapter } from '@solana/wallet-base';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
export class MetamaskWalletAdapter export class MetamaskWalletAdapter
@ -95,8 +95,9 @@ export class MetamaskWalletAdapter
} }
disconnect() { disconnect() {
if (this._publicKey) { if (this._provider) {
this._publicKey = null; this._publicKey = null;
this._provider = null;
this.emit('disconnect'); this.emit('disconnect');
} }
} }

View File

@ -1,9 +1,9 @@
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { PublicKey, Transaction } from '@solana/web3.js'; import { PublicKey, Transaction } from '@solana/web3.js';
import { notify } from '@oyster/common'; import { notify } from '@oyster/common';
import { WalletAdapter } from '@solana/wallet-base' import { WalletAdapter } from '@solana/wallet-base';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import WalletConnectProvider from "@walletconnect/web3-provider"; import WalletConnectProvider from '@walletconnect/web3-provider';
export class WalletConnectWalletAdapter export class WalletConnectWalletAdapter
extends EventEmitter extends EventEmitter
@ -13,11 +13,13 @@ export class WalletConnectWalletAdapter
_accounts: Array<any>; _accounts: Array<any>;
_chainID: number; _chainID: number;
_provider: any; _provider: any;
_walletProvider: any;
constructor() { constructor() {
super(); super();
this._publicKey = null; this._publicKey = null;
this._provider = null; this._provider = null;
this._walletProvider = null;
this._accounts = []; this._accounts = [];
this._chainID = 0; this._chainID = 0;
this._onProcess = false; this._onProcess = false;
@ -53,44 +55,52 @@ export class WalletConnectWalletAdapter
// Create WalletConnect Provider // Create WalletConnect Provider
const walletConnectProvider = new WalletConnectProvider({ const walletConnectProvider = new WalletConnectProvider({
infuraId: "535ab8649e9f40cface13cbded7d647e", infuraId: '535ab8649e9f40cface13cbded7d647e',
}); });
walletConnectProvider.enable().then(()=>{ walletConnectProvider
const provider = new ethers.providers.Web3Provider(walletConnectProvider); .enable()
const signer = provider.getSigner(); .then(() => {
signer.getAddress().then(account => { const provider = new ethers.providers.Web3Provider(
this._accounts = [account]; walletConnectProvider,
provider.getNetwork().then(network => { );
this._chainID = network.chainId; const signer = provider.getSigner();
this._provider = provider; signer.getAddress().then(account => {
this.emit('connect'); this._accounts = [account];
provider.getNetwork().then(network => {
this._chainID = network.chainId;
this._provider = provider;
this._walletProvider = walletConnectProvider;
this.emit('connect');
});
}); });
// @ts-ignore
walletConnectProvider.on(
'disconnect',
(code: number, reason: string) => {
this.emit('disconnect', { code, reason });
},
);
// @ts-ignore
walletConnectProvider.on('accountsChanged', (accounts: string[]) => {
this.emit('accountsChanged', accounts);
});
// @ts-ignore
walletConnectProvider.on('chainChanged', (chainId: number) => {
this.emit('chainChanged', chainId);
});
})
.catch(() => {
this.disconnect();
})
.finally(() => {
this._onProcess = false;
}); });
// @ts-ignore
walletConnectProvider.on('disconnect', (code: number, reason: string) => {
this.emit('disconnect', {code, reason});
});
// @ts-ignore
walletConnectProvider.on('accountsChanged', (accounts: string[]) => {
this.emit('accountsChanged', accounts);
});
// @ts-ignore
walletConnectProvider.on('chainChanged', (chainId: number) => {
this.emit('chainChanged', chainId);
});
})
.catch(() => {
this.disconnect();
})
.finally(() => {
this._onProcess = false;
});
} }
disconnect() { disconnect() {
if (this._publicKey) { if (this._provider) {
this._publicKey = null; this._publicKey = null;
this._walletProvider.disconnect();
this.emit('disconnect'); this.emit('disconnect');
} }
} }

View File

@ -74,9 +74,9 @@ export const PROGRAM_IDS = [
name: 'testnet', name: 'testnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'32siiZ12jH2i7BM2wtNuTDw2Hs5nBjVQuBh4P8cTSH3i', '7CxEuz8Qtius9aCyJqGnWZyBNvf6WTTNmA8G26BdMTSF',
), ),
programId: new PublicKey('5KrVJvesyjdMy6Vq5wfuPSMdw7vWuUvtbHG98wBsEkX6'), programId: new PublicKey('8DevpkpN6CsdczP6rQ64CHraApXFrq96oGm4VjSNCs4q'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'), pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
@ -95,9 +95,9 @@ export const PROGRAM_IDS = [
name: 'devnet', name: 'devnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'Gc6ktQPgHDxf9GpN6CdLEnkkqj5NfGYJeLDWFLoN3wNb', '8KkpkoDAQaQqjnkCtNXAyk2A8GLmsmWPjBLK7jmahhxZ',
), ),
programId: new PublicKey('FmxAXMEKaj7BvgH9zdRNMZZYdAk4mBeRdSQwUoM3QYYw'), programId: new PublicKey('7SH5hE7uBecnfMpGjdPyJupgBhFHaXcNMCEgJbmoVV7t'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

View File

@ -89,6 +89,7 @@ export const addCustomSingleSignerTransaction = async (
proposal.pubkey, proposal.pubkey,
sigAccount, sigAccount,
proposal.info.signatoryValidation, proposal.info.signatoryValidation,
proposal.info.config,
transferAuthority.publicKey, transferAuthority.publicKey,
authority, authority,
slot, slot,

View File

@ -70,7 +70,6 @@ export const createProposal = async (
space: TimelockSetLayout.span, space: TimelockSetLayout.span,
programId: PROGRAM_IDS.timelock.programId, programId: PROGRAM_IDS.timelock.programId,
}); });
signers.push(timelockSetKey); signers.push(timelockSetKey);
instructions.push(uninitializedTimelockSetInstruction); instructions.push(uninitializedTimelockSetInstruction);

View File

@ -111,6 +111,7 @@ export const registerProgramGovernance = async (
TimelockType.CustomSingleSignerV1, TimelockType.CustomSingleSignerV1,
uninitializedTimelockConfig.votingEntryRule || VotingEntryRule.Anytime, uninitializedTimelockConfig.votingEntryRule || VotingEntryRule.Anytime,
uninitializedTimelockConfig.minimumSlotWaitingPeriod || new BN(0), uninitializedTimelockConfig.minimumSlotWaitingPeriod || new BN(0),
uninitializedTimelockConfig.timeLimit || new BN(0),
uninitializedTimelockConfig.name || '', uninitializedTimelockConfig.name || '',
), ),
); );

View File

@ -11,6 +11,7 @@ import { Card, Spin } from 'antd';
import Meta from 'antd/lib/card/Meta'; import Meta from 'antd/lib/card/Meta';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { execute } from '../../actions/execute'; import { execute } from '../../actions/execute';
import { LABELS } from '../../constants';
import { import {
TimelockSet, TimelockSet,
TimelockStateStatus, TimelockStateStatus,
@ -47,7 +48,9 @@ export function InstructionCard({
description={ description={
<> <>
<p>Instruction: TODO</p> <p>Instruction: TODO</p>
<p>Slot: {instruction.info.slot.toNumber()}</p> <p>
{LABELS.DELAY}: {instruction.info.slot.toNumber()}
</p>
</> </>
} }
/> />
@ -108,7 +111,8 @@ function PlayStatusButton({
}; };
if (proposal.info.state.status != TimelockStateStatus.Executing) return null; 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) if (playing === Playstate.Unplayed)
return ( return (

View File

@ -1,11 +1,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Progress, Spin } from 'antd'; import { Card, Progress, Spin } from 'antd';
import { Form, Input } 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 { contexts, ParsedAccount, hooks, utils } from '@oyster/common';
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction'; import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
import { SaveOutlined } from '@ant-design/icons'; import { SaveOutlined } from '@ant-design/icons';
import { Connection, PublicKey } from '@solana/web3.js'; import { Connection, PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection; const { useConnection } = contexts.Connection;
@ -25,8 +30,10 @@ enum UploadType {
export function NewInstructionCard({ export function NewInstructionCard({
proposal, proposal,
position, position,
config,
}: { }: {
proposal: ParsedAccount<TimelockSet>; proposal: ParsedAccount<TimelockSet>;
config: ParsedAccount<TimelockConfig>;
position: number; position: number;
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -41,7 +48,19 @@ export function NewInstructionCard({
}) => { }) => {
if (!values.slot.match(/^\d*$/)) { if (!values.slot.match(/^\d*$/)) {
notify({ 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', type: 'error',
}); });
return; return;
@ -69,7 +88,11 @@ export function NewInstructionCard({
actions={[<SaveOutlined key="save" onClick={form.submit} />]} actions={[<SaveOutlined key="save" onClick={form.submit} />]}
> >
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}> <Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}> <Form.Item
name="slot"
label={LABELS.DELAY}
rules={[{ required: true }]}
>
<Input maxLength={64} /> <Input maxLength={64} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@ -92,4 +92,10 @@ export const LABELS = {
LEAVE_BLANK_IF_YOU_WANT_ONE: 'Leave blank if you want one made for you', LEAVE_BLANK_IF_YOU_WANT_ONE: 'Leave blank if you want one made for you',
ADDITIONAL_VOTING_MSG: ADDITIONAL_VOTING_MSG:
' Please note that during voting, you cannot withdraw tokens you have used to vote. You must wait for the vote to complete.', ' Please note that during voting, you cannot withdraw tokens you have used to vote. You must wait for the vote to complete.',
SLOT_MUST_BE_NUMERIC: 'Slot can only be numeric',
SLOT_MUST_BE_GREATER_THAN: 'Slot must be greater than or equal to ',
DELAY: 'Slot Delay',
MIN_SLOT_MUST_BE_NUMERIC: 'Minimum Slot Waiting Period can only be numeric',
TIME_LIMIT_MUST_BE_NUMERIC: 'Time Limit can only be numeric',
TIME_LIMIT: 'Voting Time Limit',
}; };

View File

@ -19,14 +19,17 @@ import BN from 'bn.js';
/// 1. `[writable]` Timelock set account. /// 1. `[writable]` Timelock set account.
/// 2. `[writable]` Signatory account /// 2. `[writable]` Signatory account
/// 3. `[writable]` Signatory validation account. /// 3. `[writable]` Signatory validation account.
/// 4. `[]` Transfer authority /// 4. `[]` Timelock Config account.
/// 5. `[]` Timelock program account. /// 5. `[]` Transfer authority
/// 6. `[]` Token program account. /// 6. `[]` Timelock mint authority
/// 7. `[]` Timelock program account.
/// 8. `[]` Token program account.
export const addCustomSingleSignerTransactionInstruction = ( export const addCustomSingleSignerTransactionInstruction = (
timelockTransactionAccount: PublicKey, timelockTransactionAccount: PublicKey,
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
signatoryAccount: PublicKey, signatoryAccount: PublicKey,
signatoryValidationAccount: PublicKey, signatoryValidationAccount: PublicKey,
timelockConfigAccount: PublicKey,
transferAuthority: PublicKey, transferAuthority: PublicKey,
authority: PublicKey, authority: PublicKey,
slot: string, slot: string,
@ -85,6 +88,7 @@ export const addCustomSingleSignerTransactionInstruction = (
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true }, { pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryAccount, isSigner: false, isWritable: true }, { pubkey: signatoryAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryValidationAccount, isSigner: false, isWritable: true }, { pubkey: signatoryValidationAccount, isSigner: false, isWritable: true },
{ pubkey: timelockConfigAccount, isSigner: false, isWritable: false },
{ pubkey: transferAuthority, isSigner: true, isWritable: false }, { pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false }, { pubkey: authority, isSigner: false, isWritable: false },
{ {

View File

@ -24,6 +24,7 @@ export const initTimelockConfigInstruction = (
timelockType: number, timelockType: number,
votingEntryRule: number, votingEntryRule: number,
minimumSlotWaitingPeriod: BN, minimumSlotWaitingPeriod: BN,
timeLimit: BN,
name: string, name: string,
): TransactionInstruction => { ): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
@ -39,6 +40,7 @@ export const initTimelockConfigInstruction = (
BufferLayout.u8('timelockType'), BufferLayout.u8('timelockType'),
BufferLayout.u8('votingEntryRule'), BufferLayout.u8('votingEntryRule'),
Layout.uint64('minimumSlotWaitingPeriod'), Layout.uint64('minimumSlotWaitingPeriod'),
Layout.uint64('timeLimit'),
BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'), BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'),
]); ]);
@ -57,6 +59,7 @@ export const initTimelockConfigInstruction = (
timelockType, timelockType,
votingEntryRule, votingEntryRule,
minimumSlotWaitingPeriod, minimumSlotWaitingPeriod,
timeLimit,
name: nameAsBytes, name: nameAsBytes,
}, },
data, data,

View File

@ -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 { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout'; import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock'; import { TimelockInstruction } from './timelock';
@ -14,6 +18,7 @@ import { TimelockInstruction } from './timelock';
/// 4. `[]` Timelock mint authority /// 4. `[]` Timelock mint authority
/// 5. `[]` Timelock program account pub key. /// 5. `[]` Timelock program account pub key.
/// 6. `[]` Token program account. /// 6. `[]` Token program account.
/// 7. `[]` Clock sysvar.
export const signInstruction = ( export const signInstruction = (
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
signatoryAccount: PublicKey, signatoryAccount: PublicKey,
@ -46,6 +51,7 @@ export const signInstruction = (
isWritable: false, isWritable: false,
}, },
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false }, { pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
]; ];
return new TransactionInstruction({ return new TransactionInstruction({
keys, keys,

View File

@ -8,7 +8,7 @@ export const DESC_SIZE = 200;
export const NAME_SIZE = 32; export const NAME_SIZE = 32;
export const CONFIG_NAME_LENGTH = 32; export const CONFIG_NAME_LENGTH = 32;
export const INSTRUCTION_LIMIT = 450; export const INSTRUCTION_LIMIT = 450;
export const TRANSACTION_SLOTS = 5; export const TRANSACTION_SLOTS = 4;
export const TEMP_FILE_TXN_SIZE = 1000; export const TEMP_FILE_TXN_SIZE = 1000;
export enum TimelockInstruction { export enum TimelockInstruction {
@ -43,6 +43,8 @@ export interface TimelockConfig {
governanceMint: PublicKey; governanceMint: PublicKey;
/// Program ID that is tied to this config (optional) /// Program ID that is tied to this config (optional)
program: PublicKey; program: PublicKey;
/// Time limit in slots for proposal to be open to voting
timeLimit: BN;
/// Optional name /// Optional name
name: string; name: string;
} }
@ -57,6 +59,7 @@ export const TimelockConfigLayout: typeof BufferLayout.Structure = BufferLayout.
Layout.uint64('minimumSlotWaitingPeriod'), Layout.uint64('minimumSlotWaitingPeriod'),
Layout.publicKey('governanceMint'), Layout.publicKey('governanceMint'),
Layout.publicKey('program'), Layout.publicKey('program'),
Layout.uint64('timeLimit'),
BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'), BufferLayout.seq(BufferLayout.u8(), CONFIG_NAME_LENGTH, 'name'),
], ],
); );
@ -95,6 +98,9 @@ export enum TimelockStateStatus {
/// Deleted /// Deleted
Deleted = 4, Deleted = 4,
/// Defeated
Defeated = 5,
} }
export const STATE_COLOR: Record<string, string> = { export const STATE_COLOR: Record<string, string> = {
@ -103,6 +109,7 @@ export const STATE_COLOR: Record<string, string> = {
[TimelockStateStatus.Executing]: 'green', [TimelockStateStatus.Executing]: 'green',
[TimelockStateStatus.Completed]: 'purple', [TimelockStateStatus.Completed]: 'purple',
[TimelockStateStatus.Deleted]: 'gray', [TimelockStateStatus.Deleted]: 'gray',
[TimelockStateStatus.Defeated]: 'red',
}; };
export interface TimelockState { export interface TimelockState {
@ -111,6 +118,10 @@ export interface TimelockState {
timelockTransactions: PublicKey[]; timelockTransactions: PublicKey[];
name: string; name: string;
descLink: string; descLink: string;
votingEndedAt: BN;
votingBeganAt: BN;
executions: number;
usedTxnSlots: number;
} }
const timelockTxns = []; const timelockTxns = [];
@ -137,6 +148,10 @@ export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.str
Layout.uint64('totalSigningTokensMinted'), Layout.uint64('totalSigningTokensMinted'),
BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'), BufferLayout.seq(BufferLayout.u8(), DESC_SIZE, 'descLink'),
BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'), BufferLayout.seq(BufferLayout.u8(), NAME_SIZE, 'name'),
Layout.uint64('votingEndedAt'),
Layout.uint64('votingBeganAt'),
BufferLayout.u8('executions'),
BufferLayout.u8('usedTxnSlots'),
...timelockTxns, ...timelockTxns,
], ],
); );
@ -248,6 +263,10 @@ export const TimelockSetParser = (
descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''), descLink: utils.fromUTF8Array(data.descLink).replaceAll('\u0000', ''),
name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''), name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''),
timelockTransactions: timelockTxns, timelockTransactions: timelockTxns,
votingEndedAt: data.votingEndedAt,
votingBeganAt: data.votingBeganAt,
executions: data.executions,
usedTxnSlots: data.usedTxnSlots,
}, },
}, },
}; };
@ -298,9 +317,10 @@ export const TimelockConfigParser = (
executionType: data.executionType, executionType: data.executionType,
timelockType: data.timelockType, timelockType: data.timelockType,
votingEntryRule: data.votingEntryRule, votingEntryRule: data.votingEntryRule,
minimimSlotWaitingPeriod: data.minimimSlotWaitingPeriod, minimumSlotWaitingPeriod: data.minimumSlotWaitingPeriod,
governanceMint: data.governanceMint, governanceMint: data.governanceMint,
program: data.program, program: data.program,
timeLimit: data.timeLimit,
name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''), name: utils.fromUTF8Array(data.name).replaceAll('\u0000', ''),
}, },
}; };

View File

@ -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 { utils } from '@oyster/common';
import * as Layout from '../utils/layout'; import * as Layout from '../utils/layout';
@ -23,6 +27,7 @@ import BN from 'bn.js';
/// 10. `[]` Timelock program mint authority /// 10. `[]` Timelock program mint authority
/// 11. `[]` Timelock program account pub key. /// 11. `[]` Timelock program account pub key.
/// 12. `[]` Token program account. /// 12. `[]` Token program account.
/// 13. `[]` Clock sysvar.
export const voteInstruction = ( export const voteInstruction = (
timelockSetAccount: PublicKey, timelockSetAccount: PublicKey,
votingAccount: PublicKey, votingAccount: PublicKey,
@ -75,6 +80,7 @@ export const voteInstruction = (
isWritable: false, isWritable: false,
}, },
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false }, { pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
]; ];
return new TransactionInstruction({ return new TransactionInstruction({

View File

@ -20,6 +20,7 @@ import {
import { PublicKey } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js';
import { Table } from 'antd'; import { Table } from 'antd';
import MintGovernanceTokens from '../../components/Proposal/MintGovernanceTokens'; import MintGovernanceTokens from '../../components/Proposal/MintGovernanceTokens';
import BN from 'bn.js';
const { useUserAccounts } = hooks; const { useUserAccounts } = hooks;
const columns = [ const columns = [
{ {
@ -47,14 +48,21 @@ const columns = [
}, },
{ {
title: LABELS.VOTING_ENTRY_RULES, title: LABELS.VOTING_ENTRY_RULES,
dataIndex: 'votingEntryRules', dataIndex: 'votingEntryRule',
key: 'votingEntryRules', key: 'votingEntryRule',
render: (number: number) => <span>{VotingEntryRule[number]}</span>, render: (number: number) => <span>{VotingEntryRule[number]}</span>,
}, },
{ {
title: LABELS.MINIMUM_SLOT_WAITING_PERIOD, title: LABELS.MINIMUM_SLOT_WAITING_PERIOD,
dataIndex: 'minimumSlotWaitingPeriod', dataIndex: 'minimumSlotWaitingPeriod',
key: 'minimumSlotWaitingPeriod', key: 'minimumSlotWaitingPeriod',
render: (number: BN) => <span>{number.toNumber()}</span>,
},
{
title: LABELS.TIME_LIMIT,
dataIndex: 'timeLimit',
key: 'timeLimit',
render: (number: BN) => <span>{number.toNumber()}</span>,
}, },
{ {
title: LABELS.GOVERNANCE_MINT, title: LABELS.GOVERNANCE_MINT,

View File

@ -79,18 +79,25 @@ export function NewForm({
consensusAlgorithm: ConsensusAlgorithm; consensusAlgorithm: ConsensusAlgorithm;
votingEntryRule: VotingEntryRule; votingEntryRule: VotingEntryRule;
minimumSlotWaitingPeriod: string; minimumSlotWaitingPeriod: string;
timeLimit: string;
governanceMint: string; governanceMint: string;
program: string; program: string;
name: string; name: string;
}) => { }) => {
if (!values.minimumSlotWaitingPeriod.match(/^\d*$/)) { if (!values.minimumSlotWaitingPeriod.match(/^\d*$/)) {
notify({ 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', type: 'error',
}); });
return; return;
} }
const uninitializedConfig = { const uninitializedConfig = {
timelockType: values.timelockType, timelockType: values.timelockType,
executionType: values.executionType, executionType: values.executionType,
@ -102,6 +109,7 @@ export function NewForm({
: undefined, : undefined,
program: new PublicKey(values.program), program: new PublicKey(values.program),
name: values.name, name: values.name,
timeLimit: new BN(values.timeLimit),
}; };
const newConfig = await registerProgramGovernance( const newConfig = await registerProgramGovernance(
@ -143,6 +151,13 @@ export function NewForm({
> >
<Input maxLength={64} /> <Input maxLength={64} />
</Form.Item> </Form.Item>
<Form.Item
name="timeLimit"
label={LABELS.TIME_LIMIT}
rules={[{ required: true }]}
>
<Input maxLength={64} />
</Form.Item>
<Form.Item <Form.Item
name="consensusAlgorithm" name="consensusAlgorithm"
label={LABELS.CONSENSUS_ALGORITHM} label={LABELS.CONSENSUS_ALGORITHM}

View File

@ -257,6 +257,7 @@ function InnerProposalView({
<Col xs={24} sm={24} md={12} lg={8}> <Col xs={24} sm={24} md={12} lg={8}>
<NewInstructionCard <NewInstructionCard
proposal={proposal} proposal={proposal}
config={timelockConfig}
position={instructionsForProposal.length} position={instructionsForProposal.length}
/> />
</Col> </Col>