mirror of https://github.com/certusone/oyster.git
Merge branch 'main' into lending/xcl
This commit is contained in:
commit
10a6a5d5cb
|
@ -63,7 +63,7 @@ export const PROGRAM_IDS = [
|
|||
{
|
||||
name: 'mainnet-beta',
|
||||
governance: () => ({
|
||||
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
|
||||
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||
|
@ -85,7 +85,7 @@ export const PROGRAM_IDS = [
|
|||
{
|
||||
name: 'testnet',
|
||||
governance: () => ({
|
||||
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
|
||||
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
|
||||
|
@ -104,7 +104,7 @@ export const PROGRAM_IDS = [
|
|||
{
|
||||
name: 'devnet',
|
||||
governance: () => ({
|
||||
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
|
||||
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||
|
@ -123,7 +123,7 @@ export const PROGRAM_IDS = [
|
|||
{
|
||||
name: 'localnet',
|
||||
governance: () => ({
|
||||
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
|
||||
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||
|
|
|
@ -15,9 +15,6 @@ import {
|
|||
} from '@oyster/common';
|
||||
|
||||
import { AccountLayout, MintLayout, Token, u64 } from '@solana/spl-token';
|
||||
import { setAuthority } from '@project-serum/serum/lib/token-instructions';
|
||||
import { GOVERNANCE_PROGRAM_SEED } from '../../models/accounts';
|
||||
import { serializeInstructionToBase64 } from '../../models/serialisation';
|
||||
|
||||
const { notify } = utils;
|
||||
export interface SourceEntryInterface {
|
||||
|
@ -29,27 +26,25 @@ export const generateGovernanceArtifacts = async (
|
|||
connection: Connection,
|
||||
wallet: any,
|
||||
) => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
let communityMintSigners: Account[] = [];
|
||||
let communityMintInstruction: TransactionInstruction[] = [];
|
||||
|
||||
// Setup community mint
|
||||
const [communityMintAddress, otherOwner] = await withMint(
|
||||
const { mintAddress: communityMintAddress } = await withMint(
|
||||
communityMintInstruction,
|
||||
communityMintSigners,
|
||||
connection,
|
||||
wallet,
|
||||
9,
|
||||
new u64('4205522598596271000'),
|
||||
new u64('6007889426566101064'),
|
||||
0,
|
||||
new u64('7000'),
|
||||
new u64('10000'),
|
||||
);
|
||||
|
||||
let councilMinSigners: Account[] = [];
|
||||
let councilMintInstructions: TransactionInstruction[] = [];
|
||||
|
||||
// Setup council mint
|
||||
const [councilMintAddress] = await withMint(
|
||||
const { mintAddress: councilMintAddress } = await withMint(
|
||||
councilMintInstructions,
|
||||
councilMinSigners,
|
||||
connection,
|
||||
|
@ -63,52 +58,17 @@ export const generateGovernanceArtifacts = async (
|
|||
let governanceSigners: Account[] = [];
|
||||
let governanceInstructions: TransactionInstruction[] = [];
|
||||
|
||||
// Token governance artifacts
|
||||
const tokenGovernance = await withTokenGovernance(
|
||||
governanceInstructions,
|
||||
governanceSigners,
|
||||
connection,
|
||||
wallet,
|
||||
0,
|
||||
new u64(200),
|
||||
);
|
||||
|
||||
let realmName = `Realm-${communityMintAddress.toBase58().substring(0, 5)}`;
|
||||
let governedAccount = communityMintAddress;
|
||||
|
||||
const [realmAddress] = await PublicKey.findProgramAddress(
|
||||
[Buffer.from(GOVERNANCE_PROGRAM_SEED), Buffer.from(realmName)],
|
||||
PROGRAM_IDS.governance.programId,
|
||||
);
|
||||
|
||||
const [governanceAddress] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from('account-governance'),
|
||||
realmAddress.toBuffer(),
|
||||
governedAccount.toBuffer(),
|
||||
],
|
||||
PROGRAM_IDS.governance.programId,
|
||||
);
|
||||
|
||||
// Use setAuthority from Serum because I couldn't get Token.createSetAuthorityInstruction to work
|
||||
// It looks like version mismatch because the function in the SDK takes authorityType params
|
||||
// This step will be uneccesery once we have CreateMintAuthority instruction
|
||||
let ix = setAuthority({
|
||||
target: communityMintAddress,
|
||||
currentAuthority: wallet.publicKey,
|
||||
newAuthority: governanceAddress,
|
||||
authorityType: 'MintTokens',
|
||||
});
|
||||
|
||||
governanceInstructions.push(ix);
|
||||
|
||||
const mintToInstruction: TransactionInstruction = Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
communityMintAddress,
|
||||
otherOwner,
|
||||
governanceAddress,
|
||||
[],
|
||||
1,
|
||||
);
|
||||
|
||||
const instructionBase64 = serializeInstructionToBase64(mintToInstruction);
|
||||
|
||||
// const upgrade = await createUpgradeInstruction(
|
||||
// new PublicKey('Hita5Lun87S4MADAF4vGoWEgFm5DyuVqxoWzzqYxS3AD'),
|
||||
// new PublicKey('EUn3VY7uiAVvi3X72Pfe8DcXbeLHMu5mVbavdQDKTViK'),
|
||||
// new PublicKey('FqSReK9R8QxvFZgdrAwGT3gsYp1ZGfiFjS8xrzyyadn3'),
|
||||
// );
|
||||
// const instructionBase64 = serializeInstructionToBase64(upgrade);
|
||||
|
||||
notify({
|
||||
message: 'Creating Governance artifacts...',
|
||||
|
@ -139,7 +99,7 @@ export const generateGovernanceArtifacts = async (
|
|||
realmName,
|
||||
communityMintAddress,
|
||||
councilMintAddress,
|
||||
instructionBase64,
|
||||
tokenGovernance,
|
||||
};
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
@ -147,6 +107,69 @@ export const generateGovernanceArtifacts = async (
|
|||
}
|
||||
};
|
||||
|
||||
const withTokenGovernance = async (
|
||||
instructions: TransactionInstruction[],
|
||||
signers: Account[],
|
||||
connection: Connection,
|
||||
wallet: any,
|
||||
decimals: number,
|
||||
amount: u64,
|
||||
) => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
MintLayout.span,
|
||||
);
|
||||
|
||||
const tokenAccountRentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
AccountLayout.span,
|
||||
);
|
||||
|
||||
const mintAddress = createMint(
|
||||
instructions,
|
||||
wallet.publicKey,
|
||||
mintRentExempt,
|
||||
decimals,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
signers,
|
||||
);
|
||||
|
||||
const tokenAccountAddress = createTokenAccount(
|
||||
instructions,
|
||||
wallet.publicKey,
|
||||
tokenAccountRentExempt,
|
||||
mintAddress,
|
||||
wallet.publicKey,
|
||||
signers,
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
mintAddress,
|
||||
tokenAccountAddress,
|
||||
wallet.publicKey,
|
||||
[],
|
||||
new u64(amount),
|
||||
),
|
||||
);
|
||||
|
||||
const beneficiaryTokenAccountAddress = createTokenAccount(
|
||||
instructions,
|
||||
wallet.publicKey,
|
||||
tokenAccountRentExempt,
|
||||
mintAddress,
|
||||
wallet.publicKey,
|
||||
signers,
|
||||
);
|
||||
|
||||
return {
|
||||
tokenAccountAddress: tokenAccountAddress.toBase58(),
|
||||
beneficiaryTokenAccountAddress: beneficiaryTokenAccountAddress.toBase58(),
|
||||
};
|
||||
};
|
||||
|
||||
const withMint = async (
|
||||
instructions: TransactionInstruction[],
|
||||
signers: Account[],
|
||||
|
@ -189,18 +212,16 @@ const withMint = async (
|
|||
signers,
|
||||
);
|
||||
|
||||
if (amount) {
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
mintAddress,
|
||||
tokenAccountAddress,
|
||||
wallet.publicKey,
|
||||
[],
|
||||
new u64(amount),
|
||||
),
|
||||
);
|
||||
}
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
mintAddress,
|
||||
tokenAccountAddress,
|
||||
wallet.publicKey,
|
||||
[],
|
||||
new u64(amount),
|
||||
),
|
||||
);
|
||||
|
||||
const otherOwner = new Account();
|
||||
instructions.push(
|
||||
|
@ -240,5 +261,5 @@ const withMint = async (
|
|||
),
|
||||
);
|
||||
|
||||
return [mintAddress, otherOwnerTokenAccount];
|
||||
return { mintAddress, otherOwnerTokenAccount };
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import { GovernanceConfig } from '../models/accounts';
|
|||
import { withCreateProgramGovernance } from '../models/withCreateProgramGovernance';
|
||||
import { sendTransactionWithNotifications } from '../tools/transactions';
|
||||
import { withCreateMintGovernance } from '../models/withCreateMintGovernance';
|
||||
import { withCreateTokenGovernance } from '../models/withCreateTokenGovernance';
|
||||
|
||||
export const registerGovernance = async (
|
||||
connection: Connection,
|
||||
|
@ -57,6 +58,19 @@ export const registerGovernance = async (
|
|||
).governanceAddress;
|
||||
break;
|
||||
}
|
||||
case GovernanceType.Token: {
|
||||
governanceAddress = (
|
||||
await withCreateTokenGovernance(
|
||||
instructions,
|
||||
realm,
|
||||
config,
|
||||
transferAuthority!,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
)
|
||||
).governanceAddress;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Governance type ${governanceType} is not supported yet.`,
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { ParsedAccount, TokenIcon } from '@oyster/common';
|
||||
import { Avatar, Badge } from 'antd';
|
||||
import React from 'react';
|
||||
import { Governance, ProposalState } from '../../models/accounts';
|
||||
|
||||
import { useProposalsByGovernance } from '../../hooks/apiHooks';
|
||||
|
||||
import './style.less';
|
||||
|
||||
export function GovernanceBadge({
|
||||
governance,
|
||||
size = 40,
|
||||
showVotingCount = true,
|
||||
}: {
|
||||
governance: ParsedAccount<Governance>;
|
||||
size?: number;
|
||||
showVotingCount?: boolean;
|
||||
}) {
|
||||
const proposals = useProposalsByGovernance(governance?.pubkey);
|
||||
const color = governance.info.isProgramGovernance() ? 'green' : 'gray';
|
||||
const useAvatar =
|
||||
governance.info.isProgramGovernance() ||
|
||||
governance.info.isAccountGovernance();
|
||||
|
||||
return (
|
||||
<Badge
|
||||
count={
|
||||
showVotingCount
|
||||
? proposals.filter(p => p.info.state === ProposalState.Voting).length
|
||||
: 0
|
||||
}
|
||||
>
|
||||
<div style={{ width: size * 1.3, height: size }}>
|
||||
{governance.info.isMintGovernance() && (
|
||||
<TokenIcon
|
||||
mintAddress={governance.info.config.governedAccount}
|
||||
size={size}
|
||||
/>
|
||||
)}
|
||||
{governance.info.isTokenGovernance() && (
|
||||
<div
|
||||
style={{ position: 'relative' }}
|
||||
className="token-icon-container"
|
||||
>
|
||||
<TokenIcon
|
||||
style={{ position: 'absolute', left: size * 0.5 }}
|
||||
mintAddress={governance.info.config.governedAccount}
|
||||
size={size * 0.6}
|
||||
/>
|
||||
<TokenIcon
|
||||
mintAddress={governance.info.config.governedAccount}
|
||||
size={size * 0.6}
|
||||
/>
|
||||
<TokenIcon
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: size * 0.25,
|
||||
top: size * 0.3,
|
||||
}}
|
||||
mintAddress={governance.info.config.governedAccount}
|
||||
size={size * 0.6}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{useAvatar && (
|
||||
<Avatar
|
||||
size={size}
|
||||
gap={2}
|
||||
style={{ background: color, marginRight: 5 }}
|
||||
>
|
||||
{governance.info.config.governedAccount.toBase58().slice(0, 5)}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
@import '~antd/dist/antd.dark.less';
|
||||
|
||||
// add transparent shadow to token icons
|
||||
.token-icon-container > div > div {
|
||||
box-shadow: 0px 0px 0px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { contexts, ParsedAccount } from '@oyster/common';
|
||||
import { TokenOwnerRecord } from '../../models/accounts';
|
||||
import { formatTokenAmount } from '../../tools/text';
|
||||
|
||||
const { useMint } = contexts.Accounts;
|
||||
|
||||
export function RealmDepositBadge({
|
||||
councilTokenOwnerRecord,
|
||||
communityTokenOwnerRecord,
|
||||
}: {
|
||||
councilTokenOwnerRecord: ParsedAccount<TokenOwnerRecord> | undefined;
|
||||
communityTokenOwnerRecord: ParsedAccount<TokenOwnerRecord> | undefined;
|
||||
}) {
|
||||
const communityMint = useMint(
|
||||
communityTokenOwnerRecord?.info.governingTokenMint,
|
||||
);
|
||||
|
||||
const councilMint = useMint(councilTokenOwnerRecord?.info.governingTokenMint);
|
||||
|
||||
if (!councilTokenOwnerRecord && !communityTokenOwnerRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>deposited </span>
|
||||
{communityTokenOwnerRecord && (
|
||||
<span>
|
||||
{`tokens: ${formatTokenAmount(
|
||||
communityMint,
|
||||
communityTokenOwnerRecord.info.governingTokenDepositAmount,
|
||||
)}`}
|
||||
</span>
|
||||
)}
|
||||
{communityTokenOwnerRecord && councilTokenOwnerRecord && ' | '}
|
||||
{councilTokenOwnerRecord && (
|
||||
<span>
|
||||
{`council tokens: ${formatTokenAmount(
|
||||
councilMint,
|
||||
councilTokenOwnerRecord.info.governingTokenDepositAmount,
|
||||
)}`}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -110,6 +110,7 @@ export const LABELS = {
|
|||
GOVERNANCE_OVER: 'governance over',
|
||||
PROGRAM: 'Program',
|
||||
MINT: 'Mint',
|
||||
TOKEN_ACCOUNT: 'Token Account',
|
||||
|
||||
REGISTER: 'Register',
|
||||
REGISTERING: 'Registering',
|
||||
|
@ -117,13 +118,15 @@ export const LABELS = {
|
|||
PROGRAM_ID_LABEL: 'program id',
|
||||
MINT_ADDRESS_LABEL: 'mint address',
|
||||
ACCOUNT_ADDRESS: 'account address',
|
||||
TOKEN_ACCOUNT_ADDRESS: 'token account address',
|
||||
|
||||
MIN_TOKENS_TO_CREATE_PROPOSAL: 'min tokens to create proposal',
|
||||
MIN_INSTRUCTION_HOLD_UP_TIME: 'min instruction hold up time (slots)',
|
||||
MAX_VOTING_TIME: 'max voting time (slots)',
|
||||
MIN_INSTRUCTION_HOLD_UP_TIME_DAYS: 'min instruction hold up time (days)',
|
||||
MAX_VOTING_TIME_DAYS: 'max voting time (days)',
|
||||
|
||||
TRANSFER_UPGRADE_AUTHORITY: 'transfer upgrade authority',
|
||||
TRANSFER_MINT_AUTHORITY: 'transfer mint authority',
|
||||
UPGRADE_AUTHORITY: 'upgrade authority',
|
||||
MINT_AUTHORITY: 'mint authority',
|
||||
TOKEN_OWNER: 'token owner',
|
||||
|
||||
PROGRAM_ID: 'Program ID',
|
||||
INSTRUCTION: 'Instruction',
|
||||
|
@ -145,7 +148,7 @@ export const LABELS = {
|
|||
MINIMUM_SLOT_WAITING_PERIOD: 'Minimum slots between proposal and vote',
|
||||
SELECT_CONFIG: 'Select Governed Program',
|
||||
CONFIG: 'Governed Program',
|
||||
GIST_PLACEHOLDER: 'Github Gist link',
|
||||
GIST_PLACEHOLDER: 'Github Gist link (optional)',
|
||||
NAME: 'Name',
|
||||
|
||||
PUBLIC_KEY: 'Public Key',
|
||||
|
@ -155,7 +158,7 @@ export const LABELS = {
|
|||
' Please note that during voting, if you withdraw your tokens, your vote will not count towards the voting total. You must wait for the vote to complete in order for your withdrawal to not affect the voting.',
|
||||
SLOT_MUST_BE_NUMERIC: 'Slot can only be numeric',
|
||||
SLOT_MUST_BE_GREATER_THAN: 'Slot must be greater than or equal to ',
|
||||
HOLD_UP_TIME: 'hold up time',
|
||||
HOLD_UP_TIME_DAYS: 'hold up time (days)',
|
||||
|
||||
MIN_SLOT_MUST_BE_NUMERIC: 'Minimum Slot Waiting Period can only be numeric',
|
||||
TIME_LIMIT_MUST_BE_NUMERIC: 'Time Limit can only be numeric',
|
||||
|
|
|
@ -86,6 +86,15 @@ export function useWalletTokenOwnerRecord(
|
|||
);
|
||||
}
|
||||
|
||||
/// Returns all TokenOwnerRecords for the current wallet
|
||||
export function useWalletTokenOwnerRecords() {
|
||||
const { wallet } = useWallet();
|
||||
|
||||
return useGovernanceAccountsByFilter<TokenOwnerRecord>(TokenOwnerRecord, [
|
||||
pubkeyFilter(1 + 32 + 32, wallet?.publicKey),
|
||||
]);
|
||||
}
|
||||
|
||||
export function useProposalAuthority(proposalOwner: PublicKey | undefined) {
|
||||
const { wallet, connected } = useWallet();
|
||||
const tokenOwnerRecord = useTokenOwnerRecord(proposalOwner);
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { ParsedAccount } from '@oyster/common';
|
||||
import { Governance, Proposal } from '../models/accounts';
|
||||
import { useIsBeyondSlot } from './useIsBeyondSlot';
|
||||
import { useIsBeyondTimestamp } from './useIsBeyondTimestamp';
|
||||
|
||||
export const useHasVotingTimeExpired = (
|
||||
governance: ParsedAccount<Governance>,
|
||||
proposal: ParsedAccount<Proposal>,
|
||||
) => {
|
||||
return useIsBeyondSlot(
|
||||
return useIsBeyondTimestamp(
|
||||
proposal.info.votingAt
|
||||
? proposal.info.votingAt.toNumber() +
|
||||
governance.info.config.maxVotingTime.toNumber()
|
||||
? proposal.info.votingAt.toNumber() + governance.info.config.maxVotingTime
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import { useConnection } from '@oyster/common';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useIsBeyondSlot = (slot: number | undefined) => {
|
||||
const connection = useConnection();
|
||||
const [isBeyondSlot, setIsBeyondSlot] = useState<boolean | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = (async () => {
|
||||
const currentSlot = await connection.getSlot();
|
||||
if (currentSlot > slot) {
|
||||
setIsBeyondSlot(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBeyondSlot(false);
|
||||
|
||||
const id = setInterval(() => {
|
||||
connection.getSlot().then(currentSlot => {
|
||||
if (currentSlot > slot) {
|
||||
setIsBeyondSlot(true);
|
||||
clearInterval(id!);
|
||||
}
|
||||
});
|
||||
}, 5000); // TODO: How to estimate the slot distance to avoid uneccesery checks?
|
||||
|
||||
return id;
|
||||
})();
|
||||
|
||||
return () => {
|
||||
sub.then(id => id && clearInterval(id));
|
||||
};
|
||||
}, [connection, slot]);
|
||||
|
||||
return isBeyondSlot;
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import { useConnection } from '@oyster/common';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useIsBeyondTimestamp = (timestamp: number | undefined) => {
|
||||
const connection = useConnection();
|
||||
const [isBeyondTimestamp, setIsBeyondTimestamp] = useState<
|
||||
boolean | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!timestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = (async () => {
|
||||
const now = moment().unix();
|
||||
|
||||
if (now > timestamp) {
|
||||
setIsBeyondTimestamp(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBeyondTimestamp(false);
|
||||
|
||||
const id = setInterval(() => {
|
||||
const now = moment().unix();
|
||||
if (now > timestamp) {
|
||||
setIsBeyondTimestamp(true);
|
||||
clearInterval(id!);
|
||||
}
|
||||
}, 5000); // TODO: Use actual timestamp to calculate the interval
|
||||
|
||||
return id;
|
||||
})();
|
||||
|
||||
return () => {
|
||||
sub.then(id => id && clearInterval(id));
|
||||
};
|
||||
}, [connection, timestamp]);
|
||||
|
||||
return isBeyondTimestamp;
|
||||
};
|
|
@ -1,6 +1,8 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { utils } from '@oyster/common';
|
||||
import { utils, constants } from '@oyster/common';
|
||||
|
||||
const { ZERO } = constants;
|
||||
|
||||
/// Seed prefix for Governance Program PDAs
|
||||
export const GOVERNANCE_PROGRAM_SEED = 'governance';
|
||||
|
@ -16,6 +18,7 @@ export enum GovernanceAccountType {
|
|||
VoteRecord = 7,
|
||||
ProposalInstruction = 8,
|
||||
MintGovernance = 9,
|
||||
TokenGovernance = 10,
|
||||
}
|
||||
|
||||
export interface GovernanceAccount {
|
||||
|
@ -50,27 +53,52 @@ export function getAccountTypes(accountClass: GovernanceAccountClass) {
|
|||
GovernanceAccountType.AccountGovernance,
|
||||
GovernanceAccountType.ProgramGovernance,
|
||||
GovernanceAccountType.MintGovernance,
|
||||
GovernanceAccountType.TokenGovernance,
|
||||
];
|
||||
default:
|
||||
throw Error(`${accountClass} account is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
export enum VoteThresholdPercentageType {
|
||||
YesVote,
|
||||
Quorum,
|
||||
}
|
||||
|
||||
export enum VoteWeightSource {
|
||||
Deposit,
|
||||
Snapshot,
|
||||
}
|
||||
|
||||
export enum InstructionExecutionStatus {
|
||||
Success,
|
||||
Error,
|
||||
}
|
||||
|
||||
export enum InstructionExecutionFlags {
|
||||
Ordered,
|
||||
UseTransaction,
|
||||
}
|
||||
|
||||
export class Realm {
|
||||
accountType = GovernanceAccountType.Realm;
|
||||
|
||||
communityMint: PublicKey;
|
||||
|
||||
reserved: BN;
|
||||
|
||||
councilMint: PublicKey | undefined;
|
||||
|
||||
name: string;
|
||||
|
||||
constructor(args: {
|
||||
communityMint: PublicKey;
|
||||
reserved: BN;
|
||||
councilMint: PublicKey | undefined;
|
||||
name: string;
|
||||
}) {
|
||||
this.communityMint = args.communityMint;
|
||||
this.reserved = args.reserved;
|
||||
this.councilMint = args.councilMint;
|
||||
this.name = args.name;
|
||||
}
|
||||
|
@ -79,48 +107,69 @@ export class Realm {
|
|||
export class GovernanceConfig {
|
||||
realm: PublicKey;
|
||||
governedAccount: PublicKey;
|
||||
yesVoteThresholdPercentage: number;
|
||||
minTokensToCreateProposal: number;
|
||||
minInstructionHoldUpTime: BN;
|
||||
maxVotingTime: BN;
|
||||
voteThresholdPercentageType: VoteThresholdPercentageType;
|
||||
voteThresholdPercentage: number;
|
||||
minTokensToCreateProposal: BN;
|
||||
minInstructionHoldUpTime: number;
|
||||
maxVotingTime: number;
|
||||
voteWeightSource: VoteWeightSource;
|
||||
proposalCoolOffTime: number;
|
||||
|
||||
constructor(args: {
|
||||
realm: PublicKey;
|
||||
governedAccount: PublicKey;
|
||||
yesVoteThresholdPercentage: number;
|
||||
minTokensToCreateProposal: number;
|
||||
minInstructionHoldUpTime: BN;
|
||||
maxVotingTime: BN;
|
||||
voteThresholdPercentageType?: VoteThresholdPercentageType;
|
||||
voteThresholdPercentage: number;
|
||||
minTokensToCreateProposal: BN;
|
||||
minInstructionHoldUpTime: number;
|
||||
maxVotingTime: number;
|
||||
voteWeightSource?: VoteWeightSource;
|
||||
proposalCoolOffTime?: number;
|
||||
}) {
|
||||
this.realm = args.realm;
|
||||
this.governedAccount = args.governedAccount;
|
||||
this.yesVoteThresholdPercentage = args.yesVoteThresholdPercentage;
|
||||
this.voteThresholdPercentageType =
|
||||
args.voteThresholdPercentageType ?? VoteThresholdPercentageType.YesVote;
|
||||
this.voteThresholdPercentage = args.voteThresholdPercentage;
|
||||
this.minTokensToCreateProposal = args.minTokensToCreateProposal;
|
||||
this.minInstructionHoldUpTime = args.minInstructionHoldUpTime;
|
||||
this.maxVotingTime = args.maxVotingTime;
|
||||
this.voteWeightSource = args.voteWeightSource ?? VoteWeightSource.Deposit;
|
||||
this.proposalCoolOffTime = args.proposalCoolOffTime ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class Governance {
|
||||
accountType: GovernanceAccountType;
|
||||
config: GovernanceConfig;
|
||||
reserved: BN;
|
||||
proposalCount: number;
|
||||
|
||||
isProgramGovernance() {
|
||||
return this.accountType === GovernanceAccountType.ProgramGovernance;
|
||||
}
|
||||
|
||||
isAccountGovernance() {
|
||||
return this.accountType === GovernanceAccountType.AccountGovernance;
|
||||
}
|
||||
|
||||
isMintGovernance() {
|
||||
return this.accountType === GovernanceAccountType.MintGovernance;
|
||||
}
|
||||
|
||||
isTokenGovernance() {
|
||||
return this.accountType === GovernanceAccountType.TokenGovernance;
|
||||
}
|
||||
|
||||
constructor(args: {
|
||||
accountType: number;
|
||||
config: GovernanceConfig;
|
||||
reserved?: BN;
|
||||
proposalCount: number;
|
||||
}) {
|
||||
this.accountType = args.accountType;
|
||||
this.config = args.config;
|
||||
this.reserved = args.reserved ?? ZERO;
|
||||
this.proposalCount = args.proposalCount;
|
||||
}
|
||||
}
|
||||
|
@ -136,12 +185,14 @@ export class TokenOwnerRecord {
|
|||
|
||||
governingTokenDepositAmount: BN;
|
||||
|
||||
governanceDelegate?: PublicKey;
|
||||
|
||||
unrelinquishedVotesCount: number;
|
||||
|
||||
totalVotesCount: number;
|
||||
|
||||
reserved: BN;
|
||||
|
||||
governanceDelegate?: PublicKey;
|
||||
|
||||
constructor(args: {
|
||||
realm: PublicKey;
|
||||
governingTokenMint: PublicKey;
|
||||
|
@ -149,6 +200,7 @@ export class TokenOwnerRecord {
|
|||
governingTokenDepositAmount: BN;
|
||||
unrelinquishedVotesCount: number;
|
||||
totalVotesCount: number;
|
||||
reserved: BN;
|
||||
}) {
|
||||
this.realm = args.realm;
|
||||
this.governingTokenMint = args.governingTokenMint;
|
||||
|
@ -156,6 +208,7 @@ export class TokenOwnerRecord {
|
|||
this.governingTokenDepositAmount = args.governingTokenDepositAmount;
|
||||
this.unrelinquishedVotesCount = args.unrelinquishedVotesCount;
|
||||
this.totalVotesCount = args.totalVotesCount;
|
||||
this.reserved = args.reserved;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,31 +265,35 @@ export class Proposal {
|
|||
|
||||
signatoriesSignedOffCount: number;
|
||||
|
||||
descriptionLink: string;
|
||||
|
||||
name: string;
|
||||
|
||||
yesVotesCount: BN;
|
||||
|
||||
noVotesCount: BN;
|
||||
|
||||
instructionsExecutedCount: number;
|
||||
|
||||
instructionsCount: number;
|
||||
|
||||
instructionsNextIndex: number;
|
||||
|
||||
draftAt: BN;
|
||||
|
||||
signingOffAt: BN | null;
|
||||
|
||||
votingAt: BN | null;
|
||||
|
||||
votingAtSlot: BN | null;
|
||||
|
||||
votingCompletedAt: BN | null;
|
||||
|
||||
executingAt: BN | null;
|
||||
|
||||
closedAt: BN | null;
|
||||
|
||||
instructionsExecutedCount: number;
|
||||
executionFlags: InstructionExecutionFlags | null;
|
||||
|
||||
instructionsCount: number;
|
||||
name: string;
|
||||
|
||||
instructionsNextIndex: number;
|
||||
descriptionLink: string;
|
||||
|
||||
constructor(args: {
|
||||
governance: PublicKey;
|
||||
|
@ -252,12 +309,14 @@ export class Proposal {
|
|||
draftAt: BN;
|
||||
signingOffAt: BN | null;
|
||||
votingAt: BN | null;
|
||||
votingAtSlot: BN | null;
|
||||
votingCompletedAt: BN | null;
|
||||
executingAt: BN | null;
|
||||
closedAt: BN | null;
|
||||
instructionsExecutedCount: number;
|
||||
instructionsCount: number;
|
||||
instructionsNextIndex: number;
|
||||
executionFlags: InstructionExecutionFlags;
|
||||
}) {
|
||||
this.governance = args.governance;
|
||||
this.governingTokenMint = args.governingTokenMint;
|
||||
|
@ -272,12 +331,14 @@ export class Proposal {
|
|||
this.draftAt = args.draftAt;
|
||||
this.signingOffAt = args.signingOffAt;
|
||||
this.votingAt = args.votingAt;
|
||||
this.votingAtSlot = args.votingAtSlot;
|
||||
this.votingCompletedAt = args.votingCompletedAt;
|
||||
this.executingAt = args.executingAt;
|
||||
this.closedAt = args.closedAt;
|
||||
this.instructionsExecutedCount = args.instructionsExecutedCount;
|
||||
this.instructionsCount = args.instructionsCount;
|
||||
this.instructionsNextIndex = args.instructionsNextIndex;
|
||||
this.executionFlags = args.executionFlags;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -381,19 +442,25 @@ export class InstructionData {
|
|||
export class ProposalInstruction {
|
||||
accountType = GovernanceAccountType.ProposalInstruction;
|
||||
proposal: PublicKey;
|
||||
holdUpTime: BN;
|
||||
instructionIndex: number;
|
||||
holdUpTime: number;
|
||||
instruction: InstructionData;
|
||||
executedAt: BN | null;
|
||||
executionStatus: InstructionExecutionStatus | null;
|
||||
|
||||
constructor(args: {
|
||||
proposal: PublicKey;
|
||||
holdUpTime: BN;
|
||||
instructionIndex: number;
|
||||
holdUpTime: number;
|
||||
instruction: InstructionData;
|
||||
executedAt: BN | null;
|
||||
executionStatus: InstructionExecutionStatus | null;
|
||||
}) {
|
||||
this.proposal = args.proposal;
|
||||
this.instructionIndex = args.instructionIndex;
|
||||
this.holdUpTime = args.holdUpTime;
|
||||
this.instruction = args.instruction;
|
||||
this.executedAt = args.executedAt;
|
||||
this.executionStatus = args.executionStatus;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ export enum GovernanceType {
|
|||
Account,
|
||||
Program,
|
||||
Mint,
|
||||
Token,
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@ export const GovernanceError: Record<number, string> = [
|
|||
"Provided upgrade authority doesn't match current program upgrade authority", // InvalidUpgradeAuthority
|
||||
'Current program upgrade authority must sign transaction', // UpgradeAuthorityMustSign
|
||||
'Given program is not upgradable', //ProgramNotUpgradable
|
||||
'Invalid token owner', //InvalidTokenOwner
|
||||
'Current token owner must sign transaction', // TokenOwnerMustSign
|
||||
'Given VoteThresholdPercentageType is not supported', //VoteThresholdPercentageTypeNotSupported
|
||||
'Given VoteWeightSource is not supported', //VoteWeightSourceNotSupported
|
||||
'Proposal cool off time is not supported', // ProposalCoolOffTimeNotSupported
|
||||
];
|
||||
|
||||
export function getTransactionErrorMsg(error: SendTransactionError) {
|
||||
|
|
|
@ -24,6 +24,7 @@ export enum GovernanceInstruction {
|
|||
ExecuteInstruction = 16,
|
||||
|
||||
CreateMintGovernance = 17,
|
||||
CreateTokenGovernance = 18,
|
||||
}
|
||||
|
||||
export class CreateRealmArgs {
|
||||
|
@ -85,6 +86,18 @@ export class CreateMintGovernanceArgs {
|
|||
}
|
||||
}
|
||||
|
||||
export class CreateTokenGovernanceArgs {
|
||||
instruction: GovernanceInstruction =
|
||||
GovernanceInstruction.CreateTokenGovernance;
|
||||
config: GovernanceConfig;
|
||||
transferTokenOwner: boolean;
|
||||
|
||||
constructor(args: { config: GovernanceConfig; transferTokenOwner: boolean }) {
|
||||
this.config = args.config;
|
||||
this.transferTokenOwner = !!args.transferTokenOwner;
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateProposalArgs {
|
||||
instruction: GovernanceInstruction = GovernanceInstruction.CreateProposal;
|
||||
name: string;
|
||||
|
|
|
@ -10,6 +10,7 @@ export async function createUpgradeInstruction(
|
|||
programId: PublicKey,
|
||||
bufferAddress: PublicKey,
|
||||
governance: PublicKey,
|
||||
spillAddress: PublicKey,
|
||||
) {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
|
@ -35,7 +36,7 @@ export async function createUpgradeInstruction(
|
|||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: governance,
|
||||
pubkey: spillAddress,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
CreateProgramGovernanceArgs,
|
||||
CreateProposalArgs,
|
||||
CreateRealmArgs,
|
||||
CreateTokenGovernanceArgs,
|
||||
DepositGoverningTokensArgs,
|
||||
ExecuteInstructionArgs,
|
||||
FinalizeVoteArgs,
|
||||
|
@ -39,12 +40,6 @@ import {
|
|||
} from './accounts';
|
||||
import { serialize } from 'borsh';
|
||||
|
||||
// TODO: Review the limits. Most likely they are leftovers from the legacy version
|
||||
export const MAX_PROPOSAL_DESCRIPTION_LENGTH = 200;
|
||||
export const MAX_PROPOSAL_NAME_LENGTH = 32;
|
||||
export const MAX_REALM_NAME_LENGTH = 32;
|
||||
export const MAX_INSTRUCTION_BASE64_LENGTH = 450;
|
||||
|
||||
// Temp. workaround to support u16.
|
||||
(BinaryReader.prototype as any).readU16 = function () {
|
||||
const reader = (this as unknown) as BinaryReader;
|
||||
|
@ -138,6 +133,17 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateTokenGovernanceArgs,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['instruction', 'u8'],
|
||||
['config', GovernanceConfig],
|
||||
['transferTokenOwner', 'u8'],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateProposalArgs,
|
||||
{
|
||||
|
@ -205,7 +211,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
fields: [
|
||||
['instruction', 'u8'],
|
||||
['index', 'u16'],
|
||||
['holdUpTime', 'u64'],
|
||||
['holdUpTime', 'u32'],
|
||||
['instructionData', InstructionData],
|
||||
],
|
||||
},
|
||||
|
@ -253,6 +259,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
fields: [
|
||||
['accountType', 'u8'],
|
||||
['communityMint', 'pubkey'],
|
||||
['reserved', 'u64'],
|
||||
['councilMint', { kind: 'option', type: 'pubkey' }],
|
||||
['name', 'string'],
|
||||
],
|
||||
|
@ -265,6 +272,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
fields: [
|
||||
['accountType', 'u8'],
|
||||
['config', GovernanceConfig],
|
||||
['reserved', 'u64'],
|
||||
['proposalCount', 'u32'],
|
||||
],
|
||||
},
|
||||
|
@ -276,10 +284,13 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
fields: [
|
||||
['realm', 'pubkey'],
|
||||
['governedAccount', 'pubkey'],
|
||||
['yesVoteThresholdPercentage', 'u8'],
|
||||
['minTokensToCreateProposal', 'u16'],
|
||||
['minInstructionHoldUpTime', 'u64'],
|
||||
['maxVotingTime', 'u64'],
|
||||
['voteThresholdPercentageType', 'u8'],
|
||||
['voteThresholdPercentage', 'u8'],
|
||||
['minTokensToCreateProposal', 'u64'],
|
||||
['minInstructionHoldUpTime', 'u32'],
|
||||
['maxVotingTime', 'u32'],
|
||||
['voteWeightSource', 'u8'],
|
||||
['proposalCoolOffTime', 'u32'],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -293,9 +304,10 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
['governingTokenMint', 'pubkey'],
|
||||
['governingTokenOwner', 'pubkey'],
|
||||
['governingTokenDepositAmount', 'u64'],
|
||||
['governanceDelegate', { kind: 'option', type: 'pubkey' }],
|
||||
['unrelinquishedVotesCount', 'u32'],
|
||||
['totalVotesCount', 'u32'],
|
||||
['reserved', 'u64'],
|
||||
['governanceDelegate', { kind: 'option', type: 'pubkey' }],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -311,19 +323,21 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
['tokenOwnerRecord', 'pubkey'],
|
||||
['signatoriesCount', 'u8'],
|
||||
['signatoriesSignedOffCount', 'u8'],
|
||||
['descriptionLink', 'string'],
|
||||
['name', 'string'],
|
||||
['yesVotesCount', 'u64'],
|
||||
['noVotesCount', 'u64'],
|
||||
['draftAt', 'u64'],
|
||||
['signingOffAt', { kind: 'option', type: 'u64' }],
|
||||
['votingAt', { kind: 'option', type: 'u64' }],
|
||||
['votingCompletedAt', { kind: 'option', type: 'u64' }],
|
||||
['executingAt', { kind: 'option', type: 'u64' }],
|
||||
['closedAt', { kind: 'option', type: 'u64' }],
|
||||
['instructionsExecutedCount', 'u16'],
|
||||
['instructionsCount', 'u16'],
|
||||
['instructionsNextIndex', 'u16'],
|
||||
['draftAt', 'u64'],
|
||||
['signingOffAt', { kind: 'option', type: 'u64' }],
|
||||
['votingAt', { kind: 'option', type: 'u64' }],
|
||||
['votingAtSlot', { kind: 'option', type: 'u64' }],
|
||||
['votingCompletedAt', { kind: 'option', type: 'u64' }],
|
||||
['executingAt', { kind: 'option', type: 'u64' }],
|
||||
['closedAt', { kind: 'option', type: 'u64' }],
|
||||
['executionFlags', { kind: 'option', type: 'u8' }],
|
||||
['name', 'string'],
|
||||
['descriptionLink', 'string'],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -369,9 +383,11 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
|
|||
fields: [
|
||||
['accountType', 'u8'],
|
||||
['proposal', 'pubkey'],
|
||||
['holdUpTime', 'u64'],
|
||||
['instructionIndex', 'u16'],
|
||||
['holdUpTime', 'u32'],
|
||||
['instruction', InstructionData],
|
||||
['executedAt', { kind: 'option', type: 'u64' }],
|
||||
['executionStatus', { kind: 'option', type: 'u8' }],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { utils } from '@oyster/common';
|
||||
import {
|
||||
PublicKey,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { GOVERNANCE_SCHEMA } from './serialisation';
|
||||
import { serialize } from 'borsh';
|
||||
import { GovernanceConfig } from './accounts';
|
||||
import { CreateTokenGovernanceArgs } from './instructions';
|
||||
|
||||
export const withCreateTokenGovernance = async (
|
||||
instructions: TransactionInstruction[],
|
||||
realm: PublicKey,
|
||||
config: GovernanceConfig,
|
||||
transferTokenOwner: boolean,
|
||||
tokenOwner: PublicKey,
|
||||
payer: PublicKey,
|
||||
): Promise<{ governanceAddress: PublicKey }> => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const args = new CreateTokenGovernanceArgs({
|
||||
config,
|
||||
transferTokenOwner,
|
||||
});
|
||||
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args));
|
||||
|
||||
const [tokenGovernanceAddress] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from('token-governance'),
|
||||
realm.toBuffer(),
|
||||
config.governedAccount.toBuffer(),
|
||||
],
|
||||
PROGRAM_IDS.governance.programId,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: realm,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: tokenGovernanceAddress,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: config.governedAccount,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: tokenOwner,
|
||||
isWritable: false,
|
||||
isSigner: true,
|
||||
},
|
||||
{
|
||||
pubkey: payer,
|
||||
isWritable: false,
|
||||
isSigner: true,
|
||||
},
|
||||
{
|
||||
pubkey: PROGRAM_IDS.token,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: PROGRAM_IDS.system,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: SYSVAR_RENT_PUBKEY,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
];
|
||||
|
||||
instructions.push(
|
||||
new TransactionInstruction({
|
||||
keys,
|
||||
programId: PROGRAM_IDS.governance.programId,
|
||||
data,
|
||||
}),
|
||||
);
|
||||
|
||||
return { governanceAddress: tokenGovernanceAddress };
|
||||
};
|
|
@ -14,7 +14,3 @@ export const formDefaults = {
|
|||
...formVerticalLayout,
|
||||
validateMessages: formValidateMessages,
|
||||
};
|
||||
|
||||
export const formSlotInputStyle = {
|
||||
width: 250,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { MintInfo } from '@solana/spl-token';
|
||||
import BN from 'bn.js';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
export function formatTokenAmount(mint: MintInfo | undefined, amount: BN) {
|
||||
return mint
|
||||
? new BigNumber(amount.toString()).shiftedBy(-mint.decimals).toFormat()
|
||||
: amount.toString();
|
||||
}
|
|
@ -20,7 +20,11 @@ const GovernanceArtifacts = () => {
|
|||
const [realmName, setRealmName] = useState('');
|
||||
const [communityMint, setCommunityMint] = useState('');
|
||||
const [councilMint, setCouncilMint] = useState('');
|
||||
const [instruction, setInstruction] = useState('');
|
||||
const [tokenGovernance, setTokenGovernance] = useState({
|
||||
tokenAccountAddress: '',
|
||||
beneficiaryTokenAccountAddress: '',
|
||||
});
|
||||
|
||||
const [generated, setGenerated] = useState(false);
|
||||
|
||||
const onGenerateArtifacts = async () => {
|
||||
|
@ -30,13 +34,14 @@ const GovernanceArtifacts = () => {
|
|||
communityMintAddress,
|
||||
councilMintAddress,
|
||||
realmName,
|
||||
instructionBase64,
|
||||
tokenGovernance,
|
||||
} = await generateGovernanceArtifacts(connection, wallet);
|
||||
|
||||
setCommunityMint(communityMintAddress.toBase58());
|
||||
setCouncilMint(councilMintAddress.toBase58());
|
||||
setRealmName(realmName);
|
||||
setInstruction(instructionBase64);
|
||||
setTokenGovernance(tokenGovernance);
|
||||
|
||||
setGenerated(true);
|
||||
};
|
||||
|
||||
|
@ -64,8 +69,16 @@ const GovernanceArtifacts = () => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h3>instruction: </h3>
|
||||
<div className="test-data">{instruction}</div>
|
||||
<h3>token governance - token account: </h3>
|
||||
<div className="test-data">
|
||||
{tokenGovernance.tokenAccountAddress}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>token governance - beneficiary token account: </h3>
|
||||
<div className="test-data">
|
||||
{tokenGovernance.beneficiaryTokenAccountAddress}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Avatar, Badge, Col, List, Row } from 'antd';
|
||||
import { Badge, Col, List, Row } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useRealm } from '../../contexts/GovernanceContext';
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { AddNewProposal } from './NewProposal';
|
|||
import { useKeyParam } from '../../hooks/useKeyParam';
|
||||
import { Proposal, ProposalState } from '../../models/accounts';
|
||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||
import { GovernanceBadge } from '../../components/GovernanceBadge/governanceBadge';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
|
@ -34,8 +35,6 @@ export const GovernanceView = () => {
|
|||
|
||||
const mint = communityTokenMint?.toBase58() || '';
|
||||
|
||||
const color = governance?.info.isProgramGovernance() ? 'green' : 'gray';
|
||||
|
||||
const proposalItems = useMemo(() => {
|
||||
const getCompareKey = (p: Proposal) =>
|
||||
`${p.state === ProposalState.Voting ? 0 : 1}${p.name}`;
|
||||
|
@ -71,16 +70,14 @@ export const GovernanceView = () => {
|
|||
>
|
||||
<Col flex="auto" xxl={15} xs={24} className="proposals-container">
|
||||
<div className="proposals-header">
|
||||
{governance?.info.isMintGovernance() ? (
|
||||
<TokenIcon
|
||||
mintAddress={governance?.info.config.governedAccount}
|
||||
{governance && (
|
||||
<GovernanceBadge
|
||||
size={60}
|
||||
/>
|
||||
) : (
|
||||
<Avatar style={{ background: color, marginRight: 5 }} size={60}>
|
||||
{governance?.info.config.governedAccount.toBase58().slice(0, 5)}
|
||||
</Avatar>
|
||||
governance={governance}
|
||||
showVotingCount={false}
|
||||
></GovernanceBadge>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h1>{realm?.info.name}</h1>
|
||||
<h2>
|
||||
|
|
|
@ -2,10 +2,7 @@ import React, { useState } from 'react';
|
|||
import { ButtonProps, Radio } from 'antd';
|
||||
import { Form, Input } from 'antd';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
MAX_PROPOSAL_DESCRIPTION_LENGTH,
|
||||
MAX_PROPOSAL_NAME_LENGTH,
|
||||
} from '../../models/serialisation';
|
||||
|
||||
import { LABELS } from '../../constants';
|
||||
import { contexts, ParsedAccount } from '@oyster/common';
|
||||
import { createProposal } from '../../actions/createProposal';
|
||||
|
@ -81,7 +78,7 @@ export function AddNewProposal({
|
|||
governance.info.config.realm,
|
||||
governance.pubkey,
|
||||
values.name,
|
||||
values.descriptionLink,
|
||||
values.descriptionLink ?? '',
|
||||
governingTokenMint,
|
||||
proposalIndex,
|
||||
);
|
||||
|
@ -136,17 +133,14 @@ export function AddNewProposal({
|
|||
label={LABELS.NAME_LABEL}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input maxLength={MAX_PROPOSAL_NAME_LENGTH} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="descriptionLink"
|
||||
label={LABELS.DESCRIPTION_LABEL}
|
||||
rules={[{ required: true }]}
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input
|
||||
maxLength={MAX_PROPOSAL_DESCRIPTION_LENGTH}
|
||||
placeholder={LABELS.GIST_PLACEHOLDER}
|
||||
/>
|
||||
<Input placeholder={LABELS.GIST_PLACEHOLDER} />
|
||||
</Form.Item>
|
||||
</ModalFormAction>
|
||||
);
|
||||
|
|
|
@ -10,26 +10,51 @@ import { RegisterRealm } from './registerRealm';
|
|||
import { LABELS } from '../../constants';
|
||||
|
||||
import { RealmBadge } from '../../components/RealmBadge/realmBadge';
|
||||
import { useWalletTokenOwnerRecords } from '../../hooks/apiHooks';
|
||||
import { RealmDepositBadge } from '../../components/RealmDepositBadge/realmDepositBadge';
|
||||
|
||||
export const HomeView = () => {
|
||||
const history = useHistory();
|
||||
const realms = useRealms();
|
||||
const tokenOwnerRecords = useWalletTokenOwnerRecords();
|
||||
|
||||
const realmItems = useMemo(() => {
|
||||
return realms
|
||||
.sort((r1, r2) => r1.info.name.localeCompare(r2.info.name))
|
||||
.map(r => ({
|
||||
href: '/realm/' + r.pubkey.toBase58(),
|
||||
title: r.info.name,
|
||||
badge: (
|
||||
<RealmBadge
|
||||
communityMint={r.info.communityMint}
|
||||
councilMint={r.info.councilMint}
|
||||
></RealmBadge>
|
||||
),
|
||||
key: r.pubkey.toBase58(),
|
||||
}));
|
||||
}, [realms]);
|
||||
.map(r => {
|
||||
const communityTokenOwnerRecord = tokenOwnerRecords.find(
|
||||
tor =>
|
||||
tor.info.governingTokenMint.toBase58() ===
|
||||
r.info.communityMint.toBase58(),
|
||||
);
|
||||
|
||||
const councilTokenOwnerRecord =
|
||||
r.info.councilMint &&
|
||||
tokenOwnerRecords.find(
|
||||
tor =>
|
||||
tor.info.governingTokenMint.toBase58() ===
|
||||
r.info.councilMint!.toBase58(),
|
||||
);
|
||||
|
||||
return {
|
||||
href: '/realm/' + r.pubkey.toBase58(),
|
||||
title: r.info.name,
|
||||
badge: (
|
||||
<RealmBadge
|
||||
communityMint={r.info.communityMint}
|
||||
councilMint={r.info.councilMint}
|
||||
></RealmBadge>
|
||||
),
|
||||
key: r.pubkey.toBase58(),
|
||||
description: (
|
||||
<RealmDepositBadge
|
||||
communityTokenOwnerRecord={communityTokenOwnerRecord}
|
||||
councilTokenOwnerRecord={councilTokenOwnerRecord}
|
||||
></RealmDepositBadge>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [realms, tokenOwnerRecords]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -56,6 +81,7 @@ export const HomeView = () => {
|
|||
<List.Item.Meta
|
||||
avatar={item.badge}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
></List.Item.Meta>
|
||||
</List.Item>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import { ButtonProps, Switch } from 'antd';
|
||||
import { Form, Input } from 'antd';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { MAX_REALM_NAME_LENGTH } from '../../models/serialisation';
|
||||
|
||||
import { LABELS } from '../../constants';
|
||||
import { contexts } from '@oyster/common';
|
||||
import { Redirect } from 'react-router';
|
||||
|
@ -65,7 +65,7 @@ export function RegisterRealm({ buttonProps }: { buttonProps: ButtonProps }) {
|
|||
label={LABELS.NAME_LABEL}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input maxLength={MAX_REALM_NAME_LENGTH} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<MintFormItem
|
||||
|
|
|
@ -399,33 +399,35 @@ function InnerProposalView({
|
|||
<Row>
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
defaultActiveKey="description"
|
||||
size="large"
|
||||
style={{ marginBottom: 32 }}
|
||||
>
|
||||
<TabPane tab="Description" key="1">
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : isUrl ? (
|
||||
failed ? (
|
||||
<p>
|
||||
{LABELS.DESCRIPTION}:{' '}
|
||||
<a
|
||||
href={proposal.info.descriptionLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{msg ? msg : LABELS.NO_LOAD}
|
||||
</a>
|
||||
</p>
|
||||
{proposal.info.descriptionLink && (
|
||||
<TabPane tab="Description" key="description">
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : isUrl ? (
|
||||
failed ? (
|
||||
<p>
|
||||
{LABELS.DESCRIPTION}:{' '}
|
||||
<a
|
||||
href={proposal.info.descriptionLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{msg ? msg : LABELS.NO_LOAD}
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<ReactMarkdown children={content} />
|
||||
)
|
||||
) : (
|
||||
<ReactMarkdown children={content} />
|
||||
)
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab={LABELS.INSTRUCTIONS} key="2">
|
||||
content
|
||||
)}
|
||||
</TabPane>
|
||||
)}
|
||||
<TabPane tab={LABELS.INSTRUCTIONS} key="instructions">
|
||||
<Row
|
||||
gutter={[
|
||||
{ xs: 8, sm: 16, md: 24, lg: 32 },
|
||||
|
@ -464,10 +466,10 @@ function getMinRequiredYesVoteScore(
|
|||
governingTokenMint: MintInfo,
|
||||
): string {
|
||||
const minVotes =
|
||||
governance.info.config.yesVoteThresholdPercentage === 100
|
||||
governance.info.config.voteThresholdPercentage === 100
|
||||
? governingTokenMint.supply
|
||||
: governingTokenMint.supply
|
||||
.mul(new BN(governance.info.config.yesVoteThresholdPercentage))
|
||||
.mul(new BN(governance.info.config.voteThresholdPercentage))
|
||||
.div(new BN(100));
|
||||
|
||||
return new BigNumber(minVotes.toString())
|
||||
|
|
|
@ -76,7 +76,7 @@ export function InstructionCard({
|
|||
<>
|
||||
<p>{`${LABELS.INSTRUCTION}: ${instructionDetails.dataBase64}`}</p>
|
||||
<p>
|
||||
{LABELS.HOLD_UP_TIME}: {instruction.info.holdUpTime.toNumber()}
|
||||
{LABELS.HOLD_UP_TIME_DAYS}: {instruction.info.holdUpTime / 86400}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { ExplorerLink, ParsedAccount, utils } from '@oyster/common';
|
||||
import { ExplorerLink, ParsedAccount, utils, contexts } from '@oyster/common';
|
||||
import { Token } from '@solana/spl-token';
|
||||
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
import {
|
||||
|
@ -14,27 +14,28 @@ import {
|
|||
} from 'antd';
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { AccountFormItem } from '../../../components/AccountFormItem/accountFormItem';
|
||||
import { Governance } from '../../../models/accounts';
|
||||
import { createUpgradeInstruction } from '../../../models/sdkInstructions';
|
||||
import {
|
||||
MAX_INSTRUCTION_BASE64_LENGTH,
|
||||
serializeInstructionToBase64,
|
||||
} from '../../../models/serialisation';
|
||||
import { formVerticalLayout } from '../../../tools/forms';
|
||||
import { serializeInstructionToBase64 } from '../../../models/serialisation';
|
||||
import { formDefaults } from '../../../tools/forms';
|
||||
const { useWallet } = contexts.Wallet;
|
||||
|
||||
const InstructionInput = ({
|
||||
export default function InstructionInput({
|
||||
governance,
|
||||
onChange,
|
||||
}: {
|
||||
governance: ParsedAccount<Governance>;
|
||||
onChange?: (v: any) => void;
|
||||
}) => {
|
||||
}) {
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [instruction, setInstruction] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const creatorsEnabled =
|
||||
governance.info.isMintGovernance() || governance.info.isProgramGovernance();
|
||||
governance.info.isMintGovernance() ||
|
||||
governance.info.isProgramGovernance() ||
|
||||
governance.info.isTokenGovernance();
|
||||
|
||||
const updateInstruction = (instruction: string) => {
|
||||
setInstruction(instruction);
|
||||
|
@ -53,7 +54,6 @@ const InstructionInput = ({
|
|||
<Input.TextArea
|
||||
value={instruction}
|
||||
onChange={e => updateInstruction(e.target.value)}
|
||||
maxLength={MAX_INSTRUCTION_BASE64_LENGTH}
|
||||
placeholder={`base64 encoded serialized Solana Instruction`}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -75,7 +75,11 @@ const InstructionInput = ({
|
|||
okText="Create"
|
||||
onCancel={() => setIsFormVisible(false)}
|
||||
title={`Create ${
|
||||
governance.info.isProgramGovernance() ? 'Upgrade Program' : 'Mint To'
|
||||
governance.info.isProgramGovernance()
|
||||
? 'Upgrade Program'
|
||||
: governance.info.isMintGovernance()
|
||||
? 'Mint To'
|
||||
: 'Transfer'
|
||||
} Instruction`}
|
||||
>
|
||||
{governance.info.isProgramGovernance() && (
|
||||
|
@ -92,10 +96,17 @@ const InstructionInput = ({
|
|||
governance={governance}
|
||||
></MintToForm>
|
||||
)}
|
||||
{governance.info.isTokenGovernance() && (
|
||||
<TransferForm
|
||||
form={form}
|
||||
onCreateInstruction={onCreateInstruction}
|
||||
governance={governance}
|
||||
></TransferForm>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const UpgradeProgramForm = ({
|
||||
form,
|
||||
|
@ -106,17 +117,24 @@ const UpgradeProgramForm = ({
|
|||
governance: ParsedAccount<Governance>;
|
||||
onCreateInstruction: (instruction: TransactionInstruction) => void;
|
||||
}) => {
|
||||
const { wallet } = useWallet();
|
||||
|
||||
if (!wallet?.publicKey) {
|
||||
return <div>Wallet not connected</div>;
|
||||
}
|
||||
|
||||
const onCreate = async ({ bufferAddress }: { bufferAddress: string }) => {
|
||||
const upgradeIx = await createUpgradeInstruction(
|
||||
governance.info.config.governedAccount,
|
||||
new PublicKey(bufferAddress),
|
||||
governance.pubkey,
|
||||
wallet.publicKey!,
|
||||
);
|
||||
onCreateInstruction(upgradeIx);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...formVerticalLayout} form={form} onFinish={onCreate}>
|
||||
<Form {...formDefaults} form={form} onFinish={onCreate}>
|
||||
<Form.Item label="program id">
|
||||
<ExplorerLink
|
||||
address={governance.info.config.governedAccount}
|
||||
|
@ -126,6 +144,9 @@ const UpgradeProgramForm = ({
|
|||
<Form.Item label="upgrade authority (governance account)">
|
||||
<ExplorerLink address={governance.pubkey} type="address" />
|
||||
</Form.Item>
|
||||
<Form.Item label="spill account (wallet)">
|
||||
<ExplorerLink address={wallet.publicKey} type="address" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bufferAddress"
|
||||
label="buffer address"
|
||||
|
@ -169,7 +190,7 @@ const MintToForm = ({
|
|||
|
||||
return (
|
||||
<Form
|
||||
{...formVerticalLayout}
|
||||
{...formDefaults}
|
||||
form={form}
|
||||
onFinish={onCreate}
|
||||
initialValues={{ amount: 1 }}
|
||||
|
@ -197,4 +218,59 @@ const MintToForm = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default InstructionInput;
|
||||
const TransferForm = ({
|
||||
form,
|
||||
governance,
|
||||
onCreateInstruction,
|
||||
}: {
|
||||
form: FormInstance;
|
||||
governance: ParsedAccount<Governance>;
|
||||
onCreateInstruction: (instruction: TransactionInstruction) => void;
|
||||
}) => {
|
||||
const onCreate = async ({
|
||||
destination,
|
||||
amount,
|
||||
}: {
|
||||
destination: string;
|
||||
amount: number;
|
||||
}) => {
|
||||
const { token: tokenProgramId } = utils.programIds();
|
||||
|
||||
const mintToIx = Token.createTransferInstruction(
|
||||
tokenProgramId,
|
||||
governance.info.config.governedAccount,
|
||||
new PublicKey(destination),
|
||||
governance.pubkey,
|
||||
[],
|
||||
amount,
|
||||
);
|
||||
|
||||
onCreateInstruction(mintToIx);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
{...formDefaults}
|
||||
form={form}
|
||||
onFinish={onCreate}
|
||||
initialValues={{ amount: 1 }}
|
||||
>
|
||||
<Form.Item label="source account">
|
||||
<ExplorerLink
|
||||
address={governance.info.config.governedAccount}
|
||||
type="address"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="account owner (governance account)">
|
||||
<ExplorerLink address={governance.pubkey} type="address" />
|
||||
</Form.Item>
|
||||
<AccountFormItem
|
||||
name="destination"
|
||||
label="destination account"
|
||||
></AccountFormItem>
|
||||
<Form.Item name="amount" label="amount" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useProposalAuthority } from '../../../hooks/apiHooks';
|
|||
import { insertInstruction } from '../../../actions/insertInstruction';
|
||||
import '../style.less';
|
||||
|
||||
import { formVerticalLayout } from '../../../tools/forms';
|
||||
import { formDefaults } from '../../../tools/forms';
|
||||
import InstructionInput from './InstructionInput';
|
||||
|
||||
const { useWallet } = contexts.Wallet;
|
||||
|
@ -47,7 +47,7 @@ export function NewInstructionCard({
|
|||
proposal,
|
||||
proposalAuthority!.pubkey,
|
||||
index,
|
||||
values.holdUpTime,
|
||||
values.holdUpTime * 86400,
|
||||
values.instruction,
|
||||
);
|
||||
|
||||
|
@ -57,35 +57,33 @@ export function NewInstructionCard({
|
|||
}
|
||||
};
|
||||
|
||||
const minHoldUpTime = governance.info.config.minInstructionHoldUpTime / 86400;
|
||||
|
||||
return !proposalAuthority ? null : (
|
||||
<Card
|
||||
title="New Instruction"
|
||||
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
|
||||
>
|
||||
<Form
|
||||
{...formVerticalLayout}
|
||||
{...formDefaults}
|
||||
form={form}
|
||||
name="control-hooks"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
holdUpTime:
|
||||
governance.info.config.minInstructionHoldUpTime.toNumber(),
|
||||
holdUpTime: minHoldUpTime,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="holdUpTime"
|
||||
label={LABELS.HOLD_UP_TIME}
|
||||
label={LABELS.HOLD_UP_TIME_DAYS}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber
|
||||
maxLength={64}
|
||||
min={governance.info.config.minInstructionHoldUpTime.toNumber()}
|
||||
/>
|
||||
<InputNumber min={minHoldUpTime} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="instruction"
|
||||
label="Instruction"
|
||||
label="instruction"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InstructionInput governance={governance}></InstructionInput>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { Col, List, Row } from 'antd';
|
||||
import { Col, List, Row, Typography } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRealm } from '../../contexts/GovernanceContext';
|
||||
|
||||
import { useGovernancesByRealm } from '../../hooks/apiHooks';
|
||||
import {
|
||||
useGovernancesByRealm,
|
||||
useWalletTokenOwnerRecord,
|
||||
} from '../../hooks/apiHooks';
|
||||
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
||||
|
||||
import { Background } from '../../components/Background';
|
||||
|
@ -14,7 +17,11 @@ import { DepositGoverningTokens } from './DepositGoverningTokens';
|
|||
import { WithdrawGoverningTokens } from './WithdrawGoverningTokens';
|
||||
|
||||
import { RealmBadge } from '../../components/RealmBadge/realmBadge';
|
||||
import { GovernanceBadge } from './governanceBadge';
|
||||
import { GovernanceBadge } from '../../components/GovernanceBadge/governanceBadge';
|
||||
import AccountDescription from './accountDescription';
|
||||
import { RealmDepositBadge } from '../../components/RealmDepositBadge/realmDepositBadge';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const RealmView = () => {
|
||||
const history = useHistory();
|
||||
|
@ -23,6 +30,16 @@ export const RealmView = () => {
|
|||
const realm = useRealm(realmKey);
|
||||
const governances = useGovernancesByRealm(realmKey);
|
||||
|
||||
const communityTokenOwnerRecord = useWalletTokenOwnerRecord(
|
||||
realm?.pubkey,
|
||||
realm?.info.communityMint,
|
||||
);
|
||||
|
||||
const councilTokenOwnerRecord = useWalletTokenOwnerRecord(
|
||||
realm?.pubkey,
|
||||
realm?.info.councilMint,
|
||||
);
|
||||
|
||||
const governanceItems = useMemo(() => {
|
||||
return governances
|
||||
.sort((g1, g2) =>
|
||||
|
@ -35,6 +52,7 @@ export const RealmView = () => {
|
|||
href: '/governance/' + g.pubkey,
|
||||
title: g.info.config.governedAccount.toBase58(),
|
||||
badge: <GovernanceBadge governance={g}></GovernanceBadge>,
|
||||
description: <AccountDescription governance={g}></AccountDescription>,
|
||||
}));
|
||||
}, [governances]);
|
||||
|
||||
|
@ -52,8 +70,14 @@ export const RealmView = () => {
|
|||
councilMint={realm?.info.councilMint}
|
||||
></RealmBadge>
|
||||
|
||||
<Col>
|
||||
<Col style={{ textAlign: 'left', marginLeft: 8 }}>
|
||||
<h1>{realm?.info.name}</h1>
|
||||
<Text type="secondary">
|
||||
<RealmDepositBadge
|
||||
communityTokenOwnerRecord={communityTokenOwnerRecord}
|
||||
councilTokenOwnerRecord={councilTokenOwnerRecord}
|
||||
></RealmDepositBadge>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
@ -102,6 +126,7 @@ export const RealmView = () => {
|
|||
<List.Item.Meta
|
||||
title={item.title}
|
||||
avatar={item.badge}
|
||||
description={item.description}
|
||||
></List.Item.Meta>
|
||||
</List.Item>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Governance } from '../../models/accounts';
|
||||
import {
|
||||
deserializeMint,
|
||||
ParsedAccount,
|
||||
useAccount,
|
||||
useConnection,
|
||||
} from '@oyster/common';
|
||||
|
||||
import { MintInfo } from '@solana/spl-token';
|
||||
|
||||
export default function AccountDescription({
|
||||
governance,
|
||||
}: {
|
||||
governance: ParsedAccount<Governance>;
|
||||
}) {
|
||||
const connection = useConnection();
|
||||
const [mintAccount, setMintAccount] = useState<MintInfo | null>();
|
||||
|
||||
const tokenAccount = useAccount(governance.info.config.governedAccount);
|
||||
|
||||
useEffect(() => {
|
||||
if (!governance.info.isMintGovernance()) {
|
||||
return;
|
||||
}
|
||||
connection
|
||||
.getAccountInfo(governance.info.config.governedAccount)
|
||||
.then(info => info && deserializeMint(info.data))
|
||||
.then(setMintAccount);
|
||||
}, [connection, governance]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{governance.info.isTokenGovernance() &&
|
||||
tokenAccount &&
|
||||
`Token Balance: ${tokenAccount.info.amount}`}
|
||||
{mintAccount && `Mint Supply: ${mintAccount.supply}`}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import { ParsedAccount, TokenIcon } from '@oyster/common';
|
||||
import { Avatar, Badge } from 'antd';
|
||||
import React from 'react';
|
||||
import { Governance, ProposalState } from '../../models/accounts';
|
||||
|
||||
import { useProposalsByGovernance } from '../../hooks/apiHooks';
|
||||
|
||||
export function GovernanceBadge({
|
||||
governance,
|
||||
}: {
|
||||
governance: ParsedAccount<Governance>;
|
||||
}) {
|
||||
const proposals = useProposalsByGovernance(governance?.pubkey);
|
||||
const color = governance.info.isProgramGovernance() ? 'green' : 'gray';
|
||||
|
||||
return (
|
||||
<Badge
|
||||
count={
|
||||
proposals.filter(p => p.info.state === ProposalState.Voting).length
|
||||
}
|
||||
>
|
||||
<div style={{ width: 55, height: 45 }}>
|
||||
{governance.info.isMintGovernance() ? (
|
||||
<TokenIcon
|
||||
mintAddress={governance.info.config.governedAccount}
|
||||
size={40}
|
||||
/>
|
||||
) : (
|
||||
<Avatar size="large" gap={2} style={{ background: color }}>
|
||||
{governance.info.config.governedAccount.toBase58().slice(0, 5)}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
}
|
|
@ -10,12 +10,12 @@ import { Redirect } from 'react-router';
|
|||
import { GovernanceType } from '../../models/enums';
|
||||
import { registerGovernance } from '../../actions/registerGovernance';
|
||||
import { GovernanceConfig } from '../../models/accounts';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { useKeyParam } from '../../hooks/useKeyParam';
|
||||
import { ModalFormAction } from '../../components/ModalFormAction/modalFormAction';
|
||||
import { formSlotInputStyle } from '../../tools/forms';
|
||||
|
||||
import { AccountFormItem } from '../../components/AccountFormItem/accountFormItem';
|
||||
import BN from 'bn.js';
|
||||
|
||||
const { useWallet } = contexts.Wallet;
|
||||
const { useConnection } = contexts.Connection;
|
||||
|
@ -43,10 +43,10 @@ export function RegisterGovernance({
|
|||
const config = new GovernanceConfig({
|
||||
realm: realmKey,
|
||||
governedAccount: new PublicKey(values.governedAccountAddress),
|
||||
yesVoteThresholdPercentage: values.yesVoteThresholdPercentage,
|
||||
minTokensToCreateProposal: values.minTokensToCreateProposal,
|
||||
minInstructionHoldUpTime: new BN(values.minInstructionHoldUpTime),
|
||||
maxVotingTime: new BN(values.maxVotingTime),
|
||||
voteThresholdPercentage: values.yesVoteThresholdPercentage,
|
||||
minTokensToCreateProposal: new BN(values.minTokensToCreateProposal),
|
||||
minInstructionHoldUpTime: values.minInstructionHoldUpTime * 86400,
|
||||
maxVotingTime: values.maxVotingTime * 86400,
|
||||
});
|
||||
return await registerGovernance(
|
||||
connection,
|
||||
|
@ -89,6 +89,9 @@ export function RegisterGovernance({
|
|||
{LABELS.PROGRAM}
|
||||
</Radio.Button>
|
||||
<Radio.Button value={GovernanceType.Mint}>{LABELS.MINT}</Radio.Button>
|
||||
<Radio.Button value={GovernanceType.Token}>
|
||||
{LABELS.TOKEN_ACCOUNT}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
|
@ -99,19 +102,24 @@ export function RegisterGovernance({
|
|||
? LABELS.PROGRAM_ID_LABEL
|
||||
: governanceType === GovernanceType.Mint
|
||||
? LABELS.MINT_ADDRESS_LABEL
|
||||
: governanceType === GovernanceType.Token
|
||||
? LABELS.TOKEN_ACCOUNT_ADDRESS
|
||||
: LABELS.ACCOUNT_ADDRESS
|
||||
}
|
||||
></AccountFormItem>
|
||||
|
||||
{(governanceType === GovernanceType.Program ||
|
||||
governanceType === GovernanceType.Mint) && (
|
||||
governanceType === GovernanceType.Mint ||
|
||||
governanceType === GovernanceType.Token) && (
|
||||
<Form.Item
|
||||
name="transferAuthority"
|
||||
label={
|
||||
label={`transfer ${
|
||||
governanceType === GovernanceType.Program
|
||||
? LABELS.TRANSFER_UPGRADE_AUTHORITY
|
||||
: LABELS.TRANSFER_MINT_AUTHORITY
|
||||
}
|
||||
? LABELS.UPGRADE_AUTHORITY
|
||||
: governanceType === GovernanceType.Mint
|
||||
? LABELS.MINT_AUTHORITY
|
||||
: LABELS.TOKEN_OWNER
|
||||
} to governance`}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox></Checkbox>
|
||||
|
@ -129,20 +137,20 @@ export function RegisterGovernance({
|
|||
|
||||
<Form.Item
|
||||
name="minInstructionHoldUpTime"
|
||||
label={LABELS.MIN_INSTRUCTION_HOLD_UP_TIME}
|
||||
label={LABELS.MIN_INSTRUCTION_HOLD_UP_TIME_DAYS}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={1}
|
||||
>
|
||||
<InputNumber min={1} style={formSlotInputStyle} />
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxVotingTime"
|
||||
label={LABELS.MAX_VOTING_TIME}
|
||||
label={LABELS.MAX_VOTING_TIME_DAYS}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={1000000}
|
||||
initialValue={3}
|
||||
>
|
||||
<InputNumber min={1} style={formSlotInputStyle} />
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="yesVoteThresholdPercentage"
|
||||
|
|
Loading…
Reference in New Issue