fix spl token class import for gov

This commit is contained in:
Adrian Brzeziński 2023-04-14 21:13:10 +02:00
commit 3e0f66f063
56 changed files with 1835 additions and 808 deletions

View File

@ -15,6 +15,7 @@ import {
BanknotesIcon,
NewspaperIcon,
PlusCircleIcon,
ArchiveBoxArrowDownIcon,
} from '@heroicons/react/20/solid'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
@ -151,6 +152,15 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
hideIconBg
showTooltip={false}
/>
<MenuItem
active={pathname === '/governance/vote'}
collapsed={false}
icon={<ArchiveBoxArrowDownIcon className="h-5 w-5" />}
title={t('common:vote')}
pagePath="/governance/vote"
hideIconBg
showTooltip={false}
/>
<MenuItem
collapsed={false}
icon={<LightBulbIcon className="h-5 w-5" />}

View File

@ -65,7 +65,7 @@ const TopBar = () => {
<>
<div className="flex w-full items-center justify-between space-x-4">
<span className="mb-0 flex items-center">
{query.token ? (
{query.token || query.market ? (
<div className="mr-2 flex h-16 items-center border-r border-th-bkg-3 pr-4 md:mr-4 md:pr-6">
<IconButton onClick={() => router.back()} hideBg size="small">
<ArrowLeftIcon className="h-6 w-6" />

View File

@ -3,7 +3,7 @@ import ListToken from './ListToken/ListToken'
const GovernancePage = () => {
return (
<div className="p-8 pb-20 md:pb-16 lg:p-10">
<div className="py-8 px-4 pb-20 sm:px-6 md:pb-16 lg:p-10">
<GovernancePageWrapper>
<ListToken />
</GovernancePageWrapper>

View File

@ -10,7 +10,8 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
const connectionContext = GovernanceStore((s) => s.connectionContext)
const initRealm = GovernanceStore((s) => s.initRealm)
const vsrClient = GovernanceStore((s) => s.vsrClient)
const fetchVoterWeight = GovernanceStore((s) => s.fetchVoterWeight)
const fetchVoter = GovernanceStore((s) => s.fetchVoter)
const resetVoter = GovernanceStore((s) => s.resetVoter)
const realm = GovernanceStore((s) => s.realm)
const connection = mangoStore((s) => s.connection)
@ -32,7 +33,9 @@ const GovernancePageWrapper = ({ children }: { children: ReactNode }) => {
connectionContext?.endpoint &&
vsrClient?.program.programId.toBase58()
) {
fetchVoterWeight(publicKey, vsrClient, connectionContext)
fetchVoter(publicKey, vsrClient, connectionContext)
} else {
resetVoter()
}
}, [
publicKey?.toBase58(),

View File

@ -89,7 +89,7 @@ const ListToken = () => {
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
const proposals = GovernanceStore((s) => s.proposals)
const fetchVoterWeight = GovernanceStore((s) => s.fetchVoterWeight)
const fetchVoter = GovernanceStore((s) => s.fetchVoter)
const connectionContext = GovernanceStore((s) => s.connectionContext)
const { t } = useTranslation(['governance'])
@ -364,7 +364,7 @@ const ListToken = () => {
return
}
if (!wallet?.publicKey || !vsrClient || !connectionContext) return
await fetchVoterWeight(wallet.publicKey, vsrClient, connectionContext)
await fetchVoter(wallet.publicKey, vsrClient, connectionContext)
if (voter.voteWeight.cmp(minVoterWeight) === -1) {
notify({

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import GovernancePageWrapper from '../GovernancePageWrapper'
const ListToken = dynamic(() => import('./ListToken'))
const ListTokenPage = () => {
return (
<div className="p-8 pb-20 md:pb-16 lg:p-10">
<GovernancePageWrapper>
<ListToken />
</GovernancePageWrapper>
</div>
)
}
export default ListTokenPage

View File

@ -0,0 +1,267 @@
import {
ProgramAccount,
Proposal,
VoteKind,
VoteRecord,
getGovernanceAccount,
getVoteRecordAddress,
} from '@solana/spl-governance'
import { VoteCountdown } from './VoteCountdown'
import { RawMint } from '@solana/spl-token'
import VoteResults from './VoteResult'
import QuorumProgress from './VoteProgress'
import GovernanceStore from '@store/governanceStore'
import Button from '@components/shared/Button'
import {
ArrowTopRightOnSquareIcon,
HandThumbDownIcon,
HandThumbUpIcon,
} from '@heroicons/react/20/solid'
import { BN } from '@project-serum/anchor'
import { useEffect, useState } from 'react'
import { MANGO_GOVERNANCE_PROGRAM } from 'utils/governance/constants'
import mangoStore from '@store/mangoStore'
import { castVote } from 'utils/governance/instructions/castVote'
import { useWallet } from '@solana/wallet-adapter-react'
import { relinquishVote } from 'utils/governance/instructions/relinquishVote'
import { PublicKey } from '@solana/web3.js'
import { notify } from 'utils/notifications'
import Loading from '@components/shared/Loading'
import { useTranslation } from 'next-i18next'
enum PROCESSED_VOTE_TYPE {
APPROVE,
DENY,
RELINQUISH,
}
const ProposalCard = ({
proposal,
mangoMint,
}: {
proposal: ProgramAccount<Proposal>
mangoMint: RawMint
}) => {
const { t } = useTranslation('governance')
const connection = mangoStore((s) => s.connection)
const client = mangoStore((s) => s.client)
const governances = GovernanceStore((s) => s.governances)
const wallet = useWallet()
const voter = GovernanceStore((s) => s.voter)
const vsrClient = GovernanceStore((s) => s.vsrClient)
const updateProposals = GovernanceStore((s) => s.updateProposals)
const [processedVoteType, setProcessedVoteType] = useState<
PROCESSED_VOTE_TYPE | ''
>('')
const [voteType, setVoteType] = useState<VoteKind | undefined>(undefined)
const [voteRecordAddress, setVoteRecordAddress] = useState<PublicKey | null>(
null
)
const [isVoteCast, setIsVoteCast] = useState(false)
const governance =
governances && governances[proposal.account.governance.toBase58()]
const canVote = voter.voteWeight.cmp(new BN(1)) !== -1
//Approve 0, deny 1
const vote = async (voteType: VoteKind) => {
setProcessedVoteType(
voteType === VoteKind.Approve
? PROCESSED_VOTE_TYPE.APPROVE
: PROCESSED_VOTE_TYPE.DENY
)
try {
await castVote(
connection,
wallet,
proposal,
voter.tokenOwnerRecord!,
voteType,
vsrClient!,
client
)
await updateProposals(proposal.pubkey)
} catch (e) {
notify({
title: 'Error',
description: `${e}`,
type: 'error',
})
}
setProcessedVoteType('')
}
const submitRelinquishVote = async () => {
setProcessedVoteType(PROCESSED_VOTE_TYPE.RELINQUISH)
try {
await relinquishVote(
connection,
wallet,
proposal,
voter.tokenOwnerRecord!,
client,
voteRecordAddress!
)
await updateProposals(proposal.pubkey)
} catch (e) {
notify({
title: 'Error',
description: `${e}`,
type: 'error',
})
}
setProcessedVoteType('')
}
useEffect(() => {
const handleGetVoteRecord = async () => {
setIsVoteCast(false)
try {
await getGovernanceAccount(connection, voteRecordAddress!, VoteRecord)
setIsVoteCast(true)
// eslint-disable-next-line no-empty
} catch (e) {}
}
if (voteRecordAddress?.toBase58()) {
handleGetVoteRecord()
} else {
setIsVoteCast(false)
}
}, [voteRecordAddress, proposal.pubkey.toBase58()])
useEffect(() => {
const handleGetVoteRecordAddress = async () => {
const voteRecordAddress = await getVoteRecordAddress(
MANGO_GOVERNANCE_PROGRAM,
proposal.pubkey,
voter.tokenOwnerRecord!.pubkey!
)
setVoteRecordAddress(voteRecordAddress)
try {
const governanceAccount = await getGovernanceAccount(
connection,
voteRecordAddress,
VoteRecord
)
setIsVoteCast(true)
setVoteType(governanceAccount.account.vote?.voteType)
} catch (e) {
setIsVoteCast(false)
}
}
if (voter.tokenOwnerRecord?.pubkey.toBase58()) {
handleGetVoteRecordAddress()
} else {
setVoteRecordAddress(null)
}
}, [proposal.pubkey.toBase58(), voter.tokenOwnerRecord?.pubkey.toBase58()])
return governance ? (
<div
className="rounded-lg border border-th-bkg-3 p-4 md:p-6"
key={proposal.pubkey.toBase58()}
>
<div className="mb-6 flex flex-col md:flex-row md:items-start md:justify-between">
<div className="pr-6">
<h2 className="mb-2 text-lg md:text-xl">
<a
href={`https://dao.mango.markets/dao/MNGO/proposal/${proposal.pubkey.toBase58()}`}
>
<span className="mr-2">{proposal.account.name}</span>
<ArrowTopRightOnSquareIcon className="mb-1 inline-block h-4 w-4 flex-shrink-0" />
</a>
</h2>
<p className="mb-2 md:mb-0">{proposal.account.descriptionLink}</p>
</div>
<VoteCountdown
proposal={proposal.account}
governance={governance.account}
/>
</div>
<div>
{!isVoteCast ? (
<div className="flex space-x-4">
<Button
className="w-32"
onClick={() => vote(VoteKind.Approve)}
disabled={!canVote || processedVoteType !== ''}
secondary
>
<div className="flex items-center justify-center">
<HandThumbUpIcon className="mr-2 h-4 w-4" />
{processedVoteType === PROCESSED_VOTE_TYPE.APPROVE ? (
<Loading className="w-4"></Loading>
) : (
t('vote-yes')
)}
</div>
</Button>
<Button
className="w-32"
onClick={() => vote(VoteKind.Deny)}
disabled={!canVote || processedVoteType !== ''}
secondary
>
<div className="flex items-center justify-center">
<HandThumbDownIcon className="mr-2 h-4 w-4" />
{processedVoteType === PROCESSED_VOTE_TYPE.DENY ? (
<Loading className="w-4"></Loading>
) : (
t('vote-no')
)}
</div>
</Button>
</div>
) : (
<div className="flex flex-wrap items-center">
<Button
className="mr-4 flex w-40 items-center justify-center"
disabled={processedVoteType !== ''}
secondary
onClick={() => submitRelinquishVote()}
>
{processedVoteType === PROCESSED_VOTE_TYPE.RELINQUISH ? (
<Loading className="w-4"></Loading>
) : (
t('relinquish-vote')
)}
</Button>
{voteType !== undefined ? (
<div className="my-2 flex">
<p className="mr-2">{t('current-vote')}</p>
<span className="font-bold text-th-fgd-2">
{voteType === VoteKind.Approve ? (
<span className="flex items-center">
<HandThumbUpIcon className="mr-1.5 h-3 w-3" />
{t('yes')}
</span>
) : (
<span className="flex items-center">
<HandThumbDownIcon className="mr-1.5 h-3 w-3" />
{t('no')}
</span>
)}
</span>
</div>
) : null}
</div>
)}
</div>
{mangoMint && (
<div className="mt-6 flex w-full flex-col space-y-4 border-t border-th-bkg-3 pt-4 md:flex-row md:space-y-0 md:space-x-6">
<VoteResults communityMint={mangoMint} proposal={proposal.account} />
<QuorumProgress
proposal={proposal}
governance={governance}
communityMint={mangoMint}
/>
</div>
)}
</div>
) : null
}
export default ProposalCard

View File

@ -0,0 +1,117 @@
import { ProgramAccount, Proposal, ProposalState } from '@solana/spl-governance'
import GovernanceStore from '@store/governanceStore'
import mangoStore from '@store/mangoStore'
import { useEffect, useState } from 'react'
import { isInCoolOffTime } from 'utils/governance/proposals'
import { RawMint } from '@solana/spl-token'
import { MANGO_MINT } from 'utils/constants'
import { PublicKey } from '@solana/web3.js'
import dynamic from 'next/dynamic'
import { fmtTokenAmount, tryGetMint } from 'utils/governance/tools'
import { BN } from '@project-serum/anchor'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
import { useTranslation } from 'next-i18next'
import SheenLoader from '@components/shared/SheenLoader'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
const ProposalCard = dynamic(() => import('./ProposalCard'))
const OnBoarding = dynamic(() => import('../OnBoarding'))
const Vote = () => {
const { t } = useTranslation('governance')
const connection = mangoStore((s) => s.connection)
const governances = GovernanceStore((s) => s.governances)
const proposals = GovernanceStore((s) => s.proposals)
const loadingProposals = GovernanceStore((s) => s.loadingProposals)
const voter = GovernanceStore((s) => s.voter)
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
const [mangoMint, setMangoMint] = useState<RawMint | null>(null)
const [votingProposals, setVotingProposals] = useState<
ProgramAccount<Proposal>[]
>([])
useEffect(() => {
if (proposals) {
const activeProposals = Object.values(proposals).filter((x) => {
const governance =
governances && governances[x.account.governance.toBase58()]
const votingEnded =
governance && x.account.getTimeToVoteEnd(governance.account) < 0
const coolOff = isInCoolOffTime(x.account, governance?.account)
return (
!coolOff && !votingEnded && x.account.state === ProposalState.Voting
)
})
setVotingProposals(activeProposals)
} else {
setVotingProposals([])
}
}, [governances, proposals])
useEffect(() => {
const handleGetMangoMint = async () => {
const mangoMint = await tryGetMint(connection, new PublicKey(MANGO_MINT))
setMangoMint(mangoMint!.account)
}
handleGetMangoMint()
}, [])
return (
<div>
<div className="mb-4 flex flex-wrap items-end justify-between">
<h1 className="mr-4">{t('active-proposals')}</h1>
<p className="whitespace-no-wrap mb-0.5 mt-2">
{t('your-votes')}{' '}
<span className="font-mono text-th-fgd-2">
{!loadingVoter
? fmtTokenAmount(voter.voteWeight, MANGO_MINT_DECIMALS)
: 0}
</span>
</p>
</div>
{loadingProposals || loadingRealm ? (
<div className="space-y-3">
<SheenLoader className="flex flex-1">
<div className={`h-56 w-full bg-th-bkg-2`} />
</SheenLoader>
<SheenLoader className="flex flex-1">
<div className={`h-56 w-full bg-th-bkg-2`} />
</SheenLoader>
</div>
) : (
<>
{!loadingVoter ? (
<OnBoarding minVotes={new BN(1000000)}></OnBoarding>
) : null}
<div className="space-y-3">
{votingProposals.length ? (
votingProposals.map(
(x) =>
mangoMint && (
<ProposalCard
key={x.pubkey.toBase58()}
proposal={x}
mangoMint={mangoMint}
></ProposalCard>
)
)
) : (
<div className="flex h-56 items-center justify-center rounded-lg border border-th-bkg-3 p-6">
<div className="flex flex-col items-center">
<NoSymbolIcon className="mb-1 h-6 w-6" />
<p>{t('no-active-proposals')}</p>
</div>
</div>
)}
</div>
</>
)}
</div>
)
}
export default Vote

View File

@ -0,0 +1,115 @@
import React, { useEffect, useState } from 'react'
import { Governance, Proposal } from '@solana/spl-governance'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
interface CountdownState {
days: number
hours: number
minutes: number
seconds: number
}
const ZeroCountdown: CountdownState = {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
}
const isZeroCountdown = (state: CountdownState) =>
state.days === 0 &&
state.hours === 0 &&
state.minutes === 0 &&
state.seconds === 0
export function VoteCountdown({
proposal,
governance,
}: {
proposal: Proposal
governance: Governance
}) {
const { t } = useTranslation(['governance'])
const [countdown, setCountdown] = useState(ZeroCountdown)
useEffect(() => {
if (proposal.isVoteFinalized()) {
setCountdown(ZeroCountdown)
return
}
const getTimeToVoteEnd = () => {
const now = dayjs().unix()
let timeToVoteEnd = proposal.isPreVotingState()
? governance.config.maxVotingTime
: (proposal.votingAt?.toNumber() ?? 0) +
governance.config.maxVotingTime -
now
if (timeToVoteEnd <= 0) {
return ZeroCountdown
}
const days = Math.floor(timeToVoteEnd / 86400)
timeToVoteEnd -= days * 86400
const hours = Math.floor(timeToVoteEnd / 3600) % 24
timeToVoteEnd -= hours * 3600
const minutes = Math.floor(timeToVoteEnd / 60) % 60
timeToVoteEnd -= minutes * 60
const seconds = Math.floor(timeToVoteEnd % 60)
return { days, hours, minutes, seconds }
}
const updateCountdown = () => {
const newState = getTimeToVoteEnd()
setCountdown(newState)
}
const interval = setInterval(() => {
updateCountdown()
}, 1000)
updateCountdown()
return () => clearInterval(interval)
}, [proposal, governance])
return (
<>
{isZeroCountdown(countdown) ? (
<div className="text-fgd-3">{t('voting-ended')}</div>
) : (
<div className="text-fgd-1 flex w-40 items-center">
<div className="text-fgd-3 mr-1">{t('ends')}</div>
{countdown && countdown.days > 0 && (
<>
<div className="bg-bkg-1 rounded px-1 py-0.5">
{countdown.days}d
</div>
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
</>
)}
<div className="bg-bkg-1 rounded px-1 py-0.5">{countdown.hours}h</div>
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
<div className="bg-bkg-1 rounded px-1 py-0.5">
{countdown.minutes}m
</div>
{!countdown.days && (
<>
<span className="text-fgd-3 mx-0.5 font-bold">:</span>
<div className="bg-bkg-1 w-9 rounded px-1 py-0.5">
{countdown.seconds}s
</div>
</>
)}
</div>
)}
</>
)
}

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import GovernancePageWrapper from '../GovernancePageWrapper'
const Vote = dynamic(() => import('./Vote'))
const VotePage = () => {
return (
<div className="py-8 px-4 pb-20 sm:px-6 md:pb-16 lg:p-10">
<GovernancePageWrapper>
<Vote />
</GovernancePageWrapper>
</div>
)
}
export default VotePage

View File

@ -0,0 +1,95 @@
import Tooltip from '@components/shared/Tooltip'
import {
CheckCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/20/solid'
import { Governance, ProgramAccount, Proposal } from '@solana/spl-governance'
import { RawMint } from '@solana/spl-token'
import GovernanceStore from '@store/governanceStore'
import { useTranslation } from 'next-i18next'
import { getMintMaxVoteWeight } from 'utils/governance/proposals'
import { fmtTokenAmount } from 'utils/governance/tools'
type Props = {
governance: ProgramAccount<Governance>
proposal: ProgramAccount<Proposal>
communityMint: RawMint
}
const QuorumProgress = ({ governance, proposal, communityMint }: Props) => {
const { t } = useTranslation(['governance'])
const realm = GovernanceStore((s) => s.realm)
const voteThresholdPct =
governance.account.config.communityVoteThreshold.value || 0
const maxVoteWeight =
realm &&
getMintMaxVoteWeight(
communityMint,
realm.account.config.communityMintMaxVoteWeightSource
)
const minimumYesVotes =
fmtTokenAmount(maxVoteWeight!, communityMint.decimals) *
(voteThresholdPct / 100)
const yesVoteCount = fmtTokenAmount(
proposal.account.getYesVoteCount(),
communityMint.decimals
)
const rawYesVotesRequired = minimumYesVotes - yesVoteCount
const votesRequiredInRange = rawYesVotesRequired < 0 ? 0 : rawYesVotesRequired
const yesVoteProgress = votesRequiredInRange
? 100 - (votesRequiredInRange / minimumYesVotes) * 100
: 100
const yesVotesRequired =
communityMint.decimals == 0
? Math.ceil(votesRequiredInRange)
: votesRequiredInRange
return (
<div className="w-full rounded-md">
<div className="flex items-center">
<div className="w-full">
<div className="flex items-center">
<p className="text-fgd-2 mb-0 mr-1.5">{t('approval-q')}</p>
<Tooltip content={t('quorum-description')}>
<InformationCircleIcon className="text-fgd-2 h-5 w-5 cursor-help" />
</Tooltip>
</div>
{typeof yesVoteProgress !== 'undefined' && yesVoteProgress < 100 ? (
<p className="text-fgd-1 mb-0 font-bold">{`${(
yesVotesRequired ?? 0
).toLocaleString(undefined, {
maximumFractionDigits: 0,
})} ${(yesVoteProgress ?? 0) > 0 ? 'more' : ''} Yes vote${
(yesVotesRequired ?? 0) > 1 ? 's' : ''
} required`}</p>
) : (
<div className="flex items-center">
<CheckCircleIcon className="text-green mr-1.5 h-5 w-5 flex-shrink-0" />
<p className="text-fgd-1 mb-0 font-bold">
{t('required-approval-achieved')}
</p>
</div>
)}
</div>
</div>
<div className="mt-2.5 flex h-2 w-full flex-grow rounded bg-th-bkg-4">
<div
style={{
width: `${yesVoteProgress}%`,
}}
className={`${
(yesVoteProgress ?? 0) >= 100 ? 'bg-th-up' : 'bg-th-fgd-2'
} flex rounded`}
></div>
</div>
</div>
)
}
export default QuorumProgress

View File

@ -0,0 +1,67 @@
import { Proposal } from '@solana/spl-governance'
import VoteResultsBar from './VoteResultBar'
import { fmtTokenAmount } from 'utils/governance/tools'
import { RawMint } from '@solana/spl-token'
import { useTranslation } from 'next-i18next'
type VoteResultsProps = {
proposal: Proposal
communityMint: RawMint
}
const VoteResults = ({ proposal, communityMint }: VoteResultsProps) => {
const { t } = useTranslation(['governance'])
const yesVoteCount = fmtTokenAmount(
proposal.getYesVoteCount(),
communityMint.decimals
)
const noVoteCount = fmtTokenAmount(
proposal.getNoVoteCount(),
communityMint.decimals
)
const totalVoteCount = yesVoteCount + noVoteCount
const getRelativeVoteCount = (voteCount: number) =>
totalVoteCount === 0 ? 0 : (voteCount / totalVoteCount) * 100
const relativeYesVotes = getRelativeVoteCount(yesVoteCount)
const relativeNoVotes = getRelativeVoteCount(noVoteCount)
return (
<div className="flex w-full items-center space-x-4">
{proposal ? (
<div className={`w-full rounded-md`}>
<div className="flex">
<div className="w-1/2">
<p>{t('yes-votes')}</p>
<p className={`hero-text font-bold text-th-fgd-1`}>
{(yesVoteCount ?? 0).toLocaleString()}
<span className="ml-1 text-xs font-normal text-th-fgd-3">
{relativeYesVotes?.toFixed(1)}%
</span>
</p>
</div>
<div className="w-1/2 text-right">
<p>{t('no-votes')}</p>
<p className={`hero-text font-bold text-th-fgd-1`}>
{(noVoteCount ?? 0).toLocaleString()}
<span className="ml-1 text-xs font-normal text-th-fgd-3">
{relativeNoVotes?.toFixed(1)}%
</span>
</p>
</div>
</div>
<VoteResultsBar
approveVotePercentage={relativeYesVotes!}
denyVotePercentage={relativeNoVotes!}
/>
</div>
) : (
<>
<div className="h-12 w-full animate-pulse rounded bg-th-bkg-3" />
</>
)}
</div>
)
}
export default VoteResults

View File

@ -0,0 +1,42 @@
type VoteResultsBarProps = {
approveVotePercentage: number
denyVotePercentage: number
}
const VoteResultsBar = ({
approveVotePercentage = 0,
denyVotePercentage = 0,
}: VoteResultsBarProps) => {
return (
<>
<div className="mt-2.5 flex h-2 w-full flex-grow rounded bg-th-bkg-4">
<div
style={{
width: `${
approveVotePercentage > 2 || approveVotePercentage < 0.01
? approveVotePercentage
: 2
}%`,
}}
className={`flex rounded-l bg-th-up ${
denyVotePercentage < 0.01 && 'rounded'
}`}
></div>
<div
style={{
width: `${
denyVotePercentage > 2 || denyVotePercentage < 0.01
? denyVotePercentage
: 2
}%`,
}}
className={`flex rounded-r bg-th-down ${
approveVotePercentage < 0.01 && 'rounded'
}`}
></div>
</div>
</>
)
}
export default VoteResultsBar

View File

@ -146,6 +146,11 @@ const MoreMenuPanel = ({
path="/governance/listToken"
icon={<PlusCircleIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('common:vote')}
path="/governance/vote"
icon={<PlusCircleIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('learn')}
path="https://docs.mango.markets/"

View File

@ -1,5 +1,3 @@
import { IconButton } from '@components/shared/Button'
import { ChevronLeftIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
@ -16,14 +14,13 @@ const DetailedAreaChart = dynamic(
)
const PerpMarketDetails = ({
marketStats,
perpMarket,
setShowPerpDetails,
}: {
marketStats: PerpStatsItem[]
perpMarket: PerpMarket
setShowPerpDetails: (x: PerpMarket | null) => void
}) => {
const { t } = useTranslation(['common', 'trade'])
const perpStats = mangoStore((s) => s.perpStats.data)
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const [priceDaysToShow, setPriceDaysToShow] = useState('30')
const [oiDaysToShow, setOiDaysToShow] = useState('30')
@ -31,13 +28,10 @@ const PerpMarketDetails = ({
const [instantFundingDaysToShow, setInstantFundingDaysToShow] = useState('30')
const rate = usePerpFundingRate()
const [marketStats, lastStat] = useMemo(() => {
if (!perpStats) return [undefined, undefined]
const stats = perpStats
.filter((stat) => stat.perp_market === perpMarket.name)
.reverse()
return [stats, stats[stats.length - 1]]
}, [perpStats, perpMarket])
const lastStat = useMemo(() => {
if (!marketStats.length) return undefined
return marketStats[marketStats.length - 1]
}, [marketStats])
const fundingRate = useMemo(() => {
if (!lastStat) return 0
@ -75,7 +69,7 @@ const PerpMarketDetails = ({
return (
<div className="grid grid-cols-2">
<div className="col-span-2 flex items-center border-b border-th-bkg-3 px-6 py-3">
{/* <div className="col-span-2 flex items-center border-b border-th-bkg-3 px-6 py-3">
<IconButton
className="mr-4"
onClick={() => setShowPerpDetails(null)}
@ -84,7 +78,7 @@ const PerpMarketDetails = ({
<ChevronLeftIcon className="h-5 w-5" />
</IconButton>
<h2 className="text-lg">{`${perpMarket.name} ${t('stats')}`}</h2>
</div>
</div> */}
{marketStats?.length && lastStat ? (
<>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">

View File

@ -14,13 +14,13 @@ import {
formatFunding,
usePerpFundingRate,
} from '@components/trade/PerpFundingRate'
import { IconButton } from '@components/shared/Button'
import { ChevronRightIcon } from '@heroicons/react/20/solid'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { getDecimalCount } from 'utils/numbers'
import Tooltip from '@components/shared/Tooltip'
import { PerpStatsItem } from 'types'
import useMangoGroup from 'hooks/useMangoGroup'
import { NextRouter, useRouter } from 'next/router'
const SimpleAreaChart = dynamic(
() => import('@components/shared/SimpleAreaChart'),
{ ssr: false }
@ -44,11 +44,12 @@ export const getOneDayPerpStats = (
: []
}
const PerpMarketsTable = ({
setShowPerpDetails,
}: {
setShowPerpDetails: (x: PerpMarket) => void
}) => {
const goToPerpMarketDetails = (market: PerpMarket, router: NextRouter) => {
const query = { ...router.query, ['market']: market.name }
router.push({ pathname: router.pathname, query })
}
const PerpMarketsTable = () => {
const { t } = useTranslation(['common', 'trade'])
const perpMarkets = mangoStore((s) => s.perpMarkets)
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
@ -58,6 +59,7 @@ const PerpMarketsTable = ({
const showTableView = width ? width > breakpoints.md : false
const rate = usePerpFundingRate()
const { group } = useMangoGroup()
const router = useRouter()
return (
<ContentBox hideBorder hidePadding>
@ -82,7 +84,7 @@ const PerpMarketsTable = ({
</TrHead>
</thead>
<tbody>
{perpMarkets.map((market, index) => {
{perpMarkets.map((market) => {
const symbol = market.name.split('-')[0]
const marketStats = getOneDayPerpStats(perpStats, market.name)
@ -93,15 +95,25 @@ const PerpMarketsTable = ({
: 0
let fundingRate
let fundingRateApr
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex
)
fundingRate = marketRate
? `${formatFunding.format(marketRate.funding_rate_hourly)}`
: ''
if (marketRate) {
fundingRate = formatFunding.format(
marketRate.funding_rate_hourly
)
fundingRateApr = formatFunding.format(
marketRate.funding_rate_hourly * 8760
)
} else {
fundingRate = ''
fundingRateApr = ''
}
} else {
fundingRate = ''
fundingRateApr = ''
}
const openInterest = market.baseLotsToUi(market.openInterest)
@ -110,7 +122,7 @@ const PerpMarketsTable = ({
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={market.publicKey.toString()}
onClick={() => setShowPerpDetails(perpMarkets[index])}
onClick={() => goToPerpMarketDetails(market, router)}
>
<Td>
<div className="flex items-center">
@ -137,7 +149,13 @@ const PerpMarketsTable = ({
? COLORS.UP[theme]
: COLORS.DOWN[theme]
}
data={marketStats}
data={marketStats.concat([
{
...marketStats[marketStats.length - 1],
date_hour: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="date_hour"
yKey="price"
@ -171,7 +189,12 @@ const PerpMarketsTable = ({
</Td>
<Td>
<div className="flex flex-col text-right">
<p>{fundingRate}</p>
<p>
{fundingRate}
<span className="mx-1">|</span>
{fundingRateApr}{' '}
<span className="font-body text-th-fgd-3">APR</span>
</p>
</div>
</Td>
<Td>
@ -212,7 +235,6 @@ const PerpMarketsTable = ({
<MobilePerpMarketItem
key={market.publicKey.toString()}
market={market}
setShowPerpDetails={setShowPerpDetails}
/>
)
})}
@ -224,17 +246,12 @@ const PerpMarketsTable = ({
export default PerpMarketsTable
const MobilePerpMarketItem = ({
market,
setShowPerpDetails,
}: {
market: PerpMarket
setShowPerpDetails: (x: PerpMarket) => void
}) => {
const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
const { t } = useTranslation('common')
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const perpStats = mangoStore((s) => s.perpStats.data)
const { theme } = useTheme()
const router = useRouter()
// const rate = usePerpFundingRate()
const symbol = market.name.split('-')[0]
@ -260,12 +277,15 @@ const MobilePerpMarketItem = ({
// }
return (
<div className="border-b border-th-bkg-3 px-6 py-4">
<button
className="w-full border-b border-th-bkg-3 p-4 focus:outline-none"
onClick={() => goToPerpMarketDetails(market, router)}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<MarketLogos market={market} />
<div>
<p className="text-th-fgd-1">{market.name}</p>
<p className="text-left text-th-fgd-1">{market.name}</p>
<div className="flex items-center space-x-3">
<p className="font-mono">
<FormatNumericValue value={market.uiPrice} isUsd />
@ -273,6 +293,8 @@ const MobilePerpMarketItem = ({
<Change change={change} suffix="%" />
</div>
</div>
</div>
<div className="flex items-center space-x-3">
{!loadingPerpStats ? (
marketStats.length ? (
<div className="ml-4 h-10 w-24">
@ -290,11 +312,9 @@ const MobilePerpMarketItem = ({
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
<IconButton onClick={() => setShowPerpDetails(market)} size="medium">
<ChevronRightIcon className="h-5 w-5" />
</IconButton>
</div>
</div>
</div>
</button>
)
}

View File

@ -1,20 +0,0 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { useState } from 'react'
import PerpMarketDetails from './PerpMarketDetails'
import PerpMarketsTable from './PerpMarketsTable'
const PerpStats = () => {
const [showPerpDetails, setShowPerpDetails] = useState<PerpMarket | null>(
null
)
return !showPerpDetails ? (
<PerpMarketsTable setShowPerpDetails={setShowPerpDetails} />
) : (
<PerpMarketDetails
perpMarket={showPerpDetails}
setShowPerpDetails={setShowPerpDetails}
/>
)
}
export default PerpStats

View File

@ -0,0 +1,75 @@
import useMangoGroup from 'hooks/useMangoGroup'
import { useRouter } from 'next/router'
import { useEffect, useMemo } from 'react'
import MarketLogos from '@components/trade/MarketLogos'
import mangoStore from '@store/mangoStore'
import PerpMarketDetails from './PerpMarketDetails'
const PerpStatsPage = () => {
const router = useRouter()
const { market } = router.query
const { group } = useMangoGroup()
const perpStats = mangoStore((s) => s.perpStats.data)
// const [animationSettings] = useLocalStorageState(
// ANIMATION_SETTINGS_KEY,
// INITIAL_ANIMATION_SETTINGS
// )
useEffect(() => {
if (!perpStats || !perpStats.length) {
const actions = mangoStore.getState().actions
actions.fetchPerpStats()
}
}, [perpStats])
const marketDetails = useMemo(() => {
if (!group || !market) return
return group.getPerpMarketByName(market.toString().toUpperCase())
}, [group, market])
const marketStats = useMemo(() => {
if (!marketDetails || !perpStats || !perpStats.length) return []
const marketStats = perpStats
.filter((stat) => stat.market_index === marketDetails.perpMarketIndex)
.reverse()
// const change = marketStats.length
// ? ((marketDetails.uiPrice - marketStats[0].price) /
// marketStats[0].price) *
// 100
// : 0
return marketStats
}, [marketDetails, perpStats])
return marketDetails ? (
<>
<div className="flex flex-col border-b border-th-bkg-3 px-6 py-5 md:flex-row md:items-center md:justify-between">
<div>
<div className="flex items-center">
<MarketLogos market={marketDetails} size="large" />
<h1 className="text-xl">{marketDetails.name}</h1>
</div>
{/* <div className="flex flex-wrap items-end font-display text-5xl text-th-fgd-1">
<div className="mr-3">
{animationSettings['number-scroll'] ? (
<FlipNumbers
height={48}
width={35}
play
delay={0.05}
duration={1}
numbers={formatCurrencyValue(marketDetails.uiPrice)}
/>
) : (
<FormatNumericValue value={marketDetails.uiPrice} isUsd />
)}
</div>
<Change change={change} suffix="%" />
</div> */}
</div>
</div>
<PerpMarketDetails marketStats={marketStats} perpMarket={marketDetails} />
</>
) : null
}
export default PerpStatsPage

View File

@ -1,11 +1,15 @@
import TabButtons from '@components/shared/TabButtons'
import mangoStore from '@store/mangoStore'
import useLocalStorageState from 'hooks/useLocalStorageState'
import useMangoGroup from 'hooks/useMangoGroup'
import { useViewport } from 'hooks/useViewport'
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import { useEffect, useMemo } from 'react'
import { STATS_TAB_KEY } from 'utils/constants'
import { breakpoints } from 'utils/theme'
import MangoStats from './MangoStats'
import PerpStats from './PerpStats'
import PerpMarketsTable from './PerpMarketsTable'
import PerpStatsPage from './PerpStatsPage'
import SpotMarketsTable from './SpotMarketsTable'
import TokenStats from './TokenStats'
@ -13,22 +17,30 @@ import TokenStats from './TokenStats'
const TABS = ['tokens', 'perp-markets', 'spot-markets', 'mango-stats']
const StatsPage = () => {
const [activeTab, setActiveTab] = useState('tokens')
const [activeTab, setActiveTab] = useLocalStorageState(
STATS_TAB_KEY,
'tokens'
)
const actions = mangoStore.getState().actions
const perpStats = mangoStore((s) => s.perpStats.data)
const { group } = useMangoGroup()
const { width } = useViewport()
const fullWidthTabs = width ? width < breakpoints.lg : false
const router = useRouter()
const { market } = router.query
useEffect(() => {
if (group) {
if (group && (!perpStats || !perpStats.length)) {
actions.fetchPerpStats()
}
}, [group])
}, [group, perpStats])
const tabsWithCount: [string, number][] = useMemo(() => {
return TABS.map((t) => [t, 0])
}, [])
return (
return market ? (
<PerpStatsPage />
) : (
<div className="pb-20 md:pb-16">
<div className="border-b border-th-bkg-3">
<TabButtons
@ -51,7 +63,7 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
case 'tokens':
return <TokenStats />
case 'perp-markets':
return <PerpStats />
return <PerpMarketsTable />
case 'spot-markets':
return <SpotMarketsTable />
case 'mango-stats':

View File

@ -174,6 +174,28 @@ const formatTradeHistory = (
})
}
const filterNewFills = (
eventQueueFills: (SerumEvent | PerpFillEvent)[],
apiTradeHistory: (SpotTradeHistory | PerpTradeHistory)[]
): (SerumEvent | PerpFillEvent)[] => {
return eventQueueFills.filter((fill) => {
return !apiTradeHistory.find((trade) => {
if ('order_id' in trade && isSerumFillEvent(fill)) {
return trade.order_id === fill.orderId.toString()
} else if ('seq_num' in trade && isPerpFillEvent(fill)) {
const fillTimestamp = new Date(
fill.timestamp.toNumber() * 1000
).getTime()
const lastApiTradeTimestamp = new Date(
apiTradeHistory[apiTradeHistory.length - 1].block_datetime
).getTime()
if (fillTimestamp < lastApiTradeTimestamp) return true
return trade.seq_num === fill.seqNum.toNumber()
}
})
})
}
const TradeHistory = () => {
const { t } = useTranslation(['common', 'trade'])
const group = mangoStore.getState().group
@ -225,23 +247,21 @@ const TradeHistory = () => {
const combinedTradeHistory = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return []
let newFills: (SerumEvent | PerpFillEvent)[] = []
const combinedTradeHistoryPages = tradeHistoryFromApi?.pages.flat() ?? []
if (eventQueueFillsForOwner?.length) {
newFills = eventQueueFillsForOwner.filter((fill) => {
return !combinedTradeHistoryPages.find((t) => {
if ('order_id' in t && isSerumFillEvent(fill)) {
return t.order_id === fill.orderId.toString()
} else if ('seq_num' in t && isPerpFillEvent(fill)) {
return t.seq_num === fill.seqNum.toNumber()
}
})
})
}
return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [
...newFills,
...combinedTradeHistoryPages,
])
const apiTradeHistory = tradeHistoryFromApi?.pages.flat() ?? []
const newFills: (SerumEvent | PerpFillEvent)[] =
eventQueueFillsForOwner?.length
? filterNewFills(eventQueueFillsForOwner, apiTradeHistory)
: []
const combinedHistory = [...newFills, ...apiTradeHistory]
return formatTradeHistory(
group,
selectedMarket,
mangoAccountAddress,
combinedHistory
)
}, [
eventQueueFillsForOwner,
mangoAccountAddress,

View File

@ -25,6 +25,7 @@
"@project-serum/anchor": "0.25.0",
"@pythnetwork/client": "2.15.0",
"@solana/spl-governance": "0.3.25",
"@solana/spl-token": "0.3.7",
"@solana/wallet-adapter-base": "0.9.20",
"@solana/wallet-adapter-react": "0.15.32",
"@solana/wallet-adapter-wallets": "0.19.11",

View File

@ -1,6 +1,10 @@
import GovernancePage from '@components/governance/GovernancePage'
import type { NextPage } from 'next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import dynamic from 'next/dynamic'
const ListTokenPage = dynamic(
() => import('@components/governance/ListToken/ListTokenPage')
)
export async function getStaticProps({ locale }: { locale: string }) {
return {
@ -17,7 +21,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
}
const Governance: NextPage = () => {
return <GovernancePage />
return <ListTokenPage />
}
export default Governance

25
pages/governance/vote.tsx Normal file
View File

@ -0,0 +1,25 @@
import type { NextPage } from 'next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import dynamic from 'next/dynamic'
const VotePage = dynamic(() => import('@components/governance/Vote/VotePage'))
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'governance',
'search',
'common',
'onboarding',
'profile',
])),
},
}
}
const ListToken: NextPage = () => {
return <VotePage />
}
export default ListToken

View File

@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"list-token": "List Token"
"list-token": "List Token",
"vote": "Vote"
}

View File

@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
"on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
"on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
"vote-yes": "Vote Yes",
"vote-no": "Vote No",
"relinquish-vote": "Relinquish Vote",
"voting-ended": "Voting ended",
"ends": "Ends",
"approval-q": "Approval Quorum",
"required-approval-achieved": "Required approval achieved",
"no-votes": "No Votes",
"yes-votes": "Yes Votes",
"quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.",
"active-proposals": "Active Proposals",
"no-active-proposals": "No active proposals",
"your-votes": "Your Votes:",
"yes": "Yes",
"no": "No",
"current-vote": "Current Vote:"
}

View File

@ -20,7 +20,7 @@
"margin": "Margin",
"margin-desc": "When margin is on you can trade with more size than your token balance. Using margin increases your risk of loss. If you're not an experienced trader, use it with caution.",
"market-selector": "Market Selector",
"market-selector-desc": "Chose the market you want to trade.",
"market-selector-desc": "Choose the market you want to trade.",
"oracle-price": "Oracle Price",
"oracle-price-desc": "The oracle price uses an average of price data from many sources. It's used to avoid price manipulation which could lead to liquidations.",
"orderbook-grouping": "Orderbook Grouping",

View File

@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"list-token": "List Token"
"list-token": "List Token",
"vote": "Vote"
}

View File

@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
"on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
"on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
"vote-yes": "Vote Yes",
"vote-no": "Vote No",
"relinquish-vote": "Relinquish Vote",
"voting-ended": "Voting ended",
"ends": "Ends",
"approval-q": "Approval Quorum",
"required-approval-achieved": "Required approval achieved",
"no-votes": "No Votes",
"yes-votes": "Yes Votes",
"quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.",
"active-proposals": "Active Proposals",
"no-active-proposals": "No active proposals",
"your-votes": "Your Votes:",
"yes": "Yes",
"no": "No",
"current-vote": "Current Vote:"
}

View File

@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"list-token": "List Token"
"list-token": "List Token",
"vote": "Vote"
}

View File

@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
"on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
"on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
"vote-yes": "Vote Yes",
"vote-no": "Vote No",
"relinquish-vote": "Relinquish Vote",
"voting-ended": "Voting ended",
"ends": "Ends",
"approval-q": "Approval Quorum",
"required-approval-achieved": "Required approval achieved",
"no-votes": "No Votes",
"yes-votes": "Yes Votes",
"quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.",
"active-proposals": "Active Proposals",
"no-active-proposals": "No active proposals",
"your-votes": "Your Votes:",
"yes": "Yes",
"no": "No",
"current-vote": "Current Vote:"
}

View File

@ -167,6 +167,7 @@
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"list-token": "List Token"
"list-token": "List Token",
"vote": "Vote"
}

View File

@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
"on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
"on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
"vote-yes": "Vote Yes",
"vote-no": "Vote No",
"relinquish-vote": "Relinquish Vote",
"voting-ended": "Voting ended",
"ends": "Ends",
"approval-q": "Approval Quorum",
"required-approval-achieved": "Required approval achieved",
"no-votes": "No Votes",
"yes-votes": "Yes Votes",
"quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.",
"active-proposals": "Active Proposals",
"no-active-proposals": "No active proposals",
"your-votes": "Your Votes:",
"yes": "Yes",
"no": "No",
"current-vote": "Current Vote:"
}

View File

@ -1,16 +1,16 @@
{
"assets": "Assets",
"assets-liabilities": "Assets & Liabilities",
"liabilities": "Liabilities",
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid",
"tooltip-total-interest": "The value of interest earned (deposits) minus interest paid (borrows)",
"total-funding-earned": "Total Funding Earned",
"week-starting": "Week starting {{week}}"
"assets": "資產",
"assets-liabilities": "資產和債務",
"liabilities": "債務",
"no-pnl-history": "無盈虧歷史",
"pnl-chart": "盈虧圖表",
"pnl-history": "盈虧歷史",
"tooltip-free-collateral": "你可用於交易和借貸的餘額。當你可用的質押品達到0元時你將不能交易、借貸或取款",
"tooltip-leverage": "總資價值除以賬戶餘額",
"tooltip-pnl": "你帳戶的盈虧",
"tooltip-total-collateral": "可用於交易和借貸的質押品(包括未結清的盈虧)",
"tooltip-total-funding": "賺取和支付的合約資金費總和",
"tooltip-total-interest": "你獲取的利息(存款)減你付出的利息(借貸)",
"total-funding-earned": "總資金費",
"week-starting": "從{{week}}來算的一周"
}

View File

@ -1,46 +1,46 @@
{
"activity": "Activity",
"activity-feed": "Activity Feed",
"activity-type": "Activity Type",
"advanced-filters": "Advanced Filters",
"asset-liquidated": "Asset Liquidated",
"asset-returned": "Asset Returned",
"connect-activity": "Connect to view your account activity",
"counterparty": "Counterparty",
"credit": "Credit",
"debit": "Debit",
"deposit": "Deposit",
"deposits": "Deposits",
"execution-price": "Execution Price",
"filter-results": "Filter",
"liquidate_perp_base_position_or_positive_pnl": "Perp Liquidation",
"liquidate_token_with_token": "Spot Liquidation",
"liquidated": "Liquidated",
"liquidation": "Liquidation",
"liquidation-fee": "Liquidation Fee",
"liquidation-type": "Liquidation Type",
"liquidation-side": "Liquidation Side",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"liquidator": "Liquidator",
"net-price": "Net Price",
"net-price-desc": "The trade price inclusive of fees",
"no-activity": "No account activity",
"openbook_trade": "Spot Trade",
"perp-details": "Perp Trade Details",
"perps": "Perps",
"perp_trade": "Perp Trade",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"spot-trade": "Spot Trade",
"swap": "Swap",
"swaps": "Swaps",
"tooltip-fee": "Swap fees paid to other DEXs are not displayed",
"trades": "Trades",
"update": "Update",
"value-from": "Value From",
"value-to": "Value To",
"view-account": "View Account",
"withdraw": "Withdraw",
"withdrawals": "Withdrawals"
"activity": "活動",
"activity-feed": "活動歷史",
"activity-type": "活動類別",
"advanced-filters": "高級篩選",
"asset-liquidated": "清算資產",
"asset-returned": "歸還資產",
"connect-activity": "連接錢包來查看帳戶",
"counterparty": "交易對方",
"credit": "貸方",
"debit": "借方",
"deposit": "存款",
"deposits": "存款",
"execution-price": "成交價格",
"filter-results": "篩選",
"liquidate_perp_base_position_or_positive_pnl": "合約清算",
"liquidate_token_with_token": "現貨清算",
"liquidated": "被清算",
"liquidation": "清算",
"liquidation-details": "清算細節",
"liquidation-fee": "清算費用",
"liquidation-side": "清算方向",
"liquidation-type": "清算類別",
"liquidations": "清算",
"liquidator": "清算者",
"net-price": "淨價",
"net-price-desc": "包含費用的交易價格",
"no-activity": "無帳戶歷史",
"openbook_trade": "現貨交易",
"perp-details": "合約交易細節",
"perp_trade": "合約交易",
"perps": "永續合約",
"reset-filters": "重置篩選",
"select-tokens": "選擇幣種",
"spot-trade": "現貨交易",
"swap": "換幣",
"swaps": "換幣",
"tooltip-fee": "繳給其他DEX的費用這裡不顯示",
"trades": "交易",
"update": "更新",
"value-from": "價值多於",
"value-to": "價值少於",
"view-account": "查看帳戶",
"withdraw": "取款",
"withdrawals": "取款"
}

View File

@ -1,11 +1,11 @@
{
"assets-to-borrow": "Assets to Borrow",
"available-to-borrow": "Available to Borrow",
"borrowed-amount": "Borrowed Amount",
"connect-borrows": "Connect to see your borrows",
"current-borrow-value": "Current Borrow Value",
"liability-weight-desc": "Liability weight adds to the value of the liability in your account health calculation.",
"no-borrows": "No Borrows",
"tooltip-available": "The max amount you can borrow",
"your-borrows": "Your Borrows"
"assets-to-borrow": "可借資產",
"available-to-borrow": "可借餘額",
"borrowed-amount": "借貸數量",
"connect-borrows": "連接而查看你的借貸",
"current-borrow-value": "借貸價值",
"liability-weight-desc": "計算帳戶健康時債務權重會增加債務價值。",
"no-borrows": "無借貸",
"tooltip-available": "你可借的最多資產",
"your-borrows": "你的借貸"
}

View File

@ -1,19 +1,19 @@
{
"are-you-sure": "Are you sure? Closing your account will",
"before-you-continue": "Before you can continue",
"close-account": "Close Account",
"closing-account": "Closing your account...",
"close-all-borrows": "Repay all borrows",
"close-open-orders": "Close all open orders",
"close-perp-positions": "Close and settle all perp positons",
"closing-account-will": "Closing your Mango Account will:",
"delete-your-account": "Delete your Mango Account",
"error-deleting-account": "Error deleting your Mango Account",
"goodbye": "Until next time 👋",
"recover-x-sol": "Recover {{amount}} SOL (rent for your account)",
"settle-balances": "Settle all balances",
"transaction-confirmed": "Transaction Confirmed",
"withdraw-assets-worth": "Withdraw assets worth {{value}}",
"you-must": "Before you can close your account",
"account-closed": "Account Closed 👋"
"are-you-sure": "你確定嗎? 關戶將會",
"before-you-continue": "繼續之前",
"close-account": "關戶",
"closing-account": "正在關閉帳戶...",
"close-all-borrows": "歸還借貸",
"close-open-orders": "取消訂單",
"close-perp-positions": "取消及結清所有合約持倉",
"closing-account-will": "關閉Mango帳戶將會",
"delete-your-account": "刪除您的Mango帳戶",
"error-deleting-account": "刪除Mango帳戶出錯",
"goodbye": "下次見👋",
"recover-x-sol": "恢復{{amount}} SOL (帳戶押金)",
"settle-balances": "結清餘額",
"transaction-confirmed": "交易成功",
"withdraw-assets-worth": "提取價值{{value}}的資產",
"you-must": "關戶之前",
"account-closed": "帳戶關閉了👋"
}

View File

@ -1,172 +1,78 @@
{
"404-heading": "This page was liquidated",
"404-description": "or, never existed...",
"accept-terms": "Accept Terms",
"accept-terms-desc": "By continuing, you accept the Mango",
"account": "Account",
"account-closed": "Account Closed 👋",
"account-balance": "Account Balance",
"account-name": "Account Name",
"account-name-desc": "Organize your accounts by giving them useful names",
"account-settings": "Account Settings",
"account-update-failed": "Failed to update account",
"account-update-success": "Account updated successfully",
"account-value": "Account Value",
"accounts": "Accounts",
"actions": "Actions",
"add-new-account": "Add New Account",
"agree-and-continue": "Agree and Continue",
"all": "All",
"amount": "Amount",
"amount-owed": "Amount Owed",
"asset-liability-weight": "Asset/Liability Weights",
"asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).",
"asset-weight": "Asset Weight",
"asset-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral.",
"available": "Available",
"available-balance": "Available Balance",
"balance": "Balance",
"bal": "Bal",
"balances": "Balances",
"borrow": "Borrow",
"borrow-amount": "Borrow Amount",
"borrow-fee": "Borrow Fee",
"borrow-funds": "Borrow Funds",
"borrow-rate": "Borrow APR",
"buy": "Buy",
"cancel": "Cancel",
"chart-unavailable": "Chart Unavailable",
"clear-all": "Clear All",
"close": "Close",
"close-account": "Close Account",
"close-account-desc": "Are you sure? Closing your account is irreversible",
"closing-account": "Closing your account...",
"collateral-value": "Collateral Value",
"connect": "Connect",
"connect-balances": "Connect to view your balances",
"connect-helper": "Connect to get started",
"copy-address": "Copy Address",
"copy-address-success": "Copied address: {{pk}}",
"country-not-allowed": "Country {{country}} Not Allowed",
"country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.",
"create-account": "Create Account",
"creating-account": "Creating Account...",
"cumulative-interest-value": "Cumulative Interest Earned",
"daily-volume": "24h Volume",
"date": "Date",
"date-from": "Date From",
"date-to": "Date To",
"delegate": "Delegate",
"delegate-account": "Delegate Account",
"delegate-account-info": "Account delegated to {{address}}",
"delegate-desc": "Delegate your Mango account to another wallet address",
"delegate-placeholder": "Enter a wallet address to delegate to",
"delete": "Delete",
"deposit": "Deposit",
"deposit-amount": "Deposit Amount",
"deposit-more-sol": "Your SOL wallet balance is too low. Add more to pay for transactions",
"deposit-rate": "Deposit APR",
"disconnect": "Disconnect",
"documentation": "Documentation",
"edit": "Edit",
"edit-account": "Edit Account Name",
"edit-profile-image": "Edit Profile Image",
"explorer": "Explorer",
"fee": "Fee",
"fees": "Fees",
"free-collateral": "Free Collateral",
"get-started": "Get Started",
"governance": "Governance",
"health": "Health",
"health-impact": "Health Impact",
"health-tooltip": "Projects the health of your account before you make a trade. The first value is your current account health and the second your projected account health.",
"history": "History",
"funding": "Funding",
"insufficient-sol": "Solana requires 0.04454 SOL rent to create a Mango Account. This will be returned if you close your account.",
"interest-earned": "Interest Earned",
"interest-earned-paid": "Interest Earned",
"leaderboard": "Leaderboard",
"learn": "Learn",
"leverage": "Leverage",
"liability-weight": "Liability Weight",
"liquidity": "Liquidity",
"loading": "Loading",
"loan-origination-fee": "Borrow Fee",
"loan-origination-fee-tooltip": "{{fee}} fee for opening a borrow.",
"mango": "Mango",
"mango-stats": "Mango Stats",
"market": "Market",
"max": "Max",
"max-borrow": "Max Borrow",
"more": "More",
"new": "New",
"new-account": "New Account",
"new-account-failed": "Failed to create account",
"new-account-success": "Your new account is ready 😎",
"new-version": "New version available",
"optional": "Optional",
"outstanding-balance": "Outstanding Balance",
"perp": "Perp",
"perp-markets": "Perp Markets",
"pnl": "PnL",
"price": "Price",
"quantity": "Quantity",
"rate": "Rate (APR)",
"rates": "Rates (APR)",
"remove": "Remove",
"remove-delegate": "Remove Delegate",
"repay": "Repay",
"repay-deposit": "Repay & Deposit",
"repay-borrow": "Repay Borrow",
"repayment-amount": "Repayment Amount",
"rolling-change": "24h Change",
"save": "Save",
"select": "Select",
"select-borrow-token": "Select Borrow Token",
"select-deposit-token": "Select Deposit Token",
"select-repay-token": "Select Repay Token",
"select-token": "Select Token",
"select-withdraw-token": "Select Withdraw Token",
"sell": "Sell",
"settings": "Settings",
"show-more": "Show More",
"show-zero-balances": "Show Zero Balances",
"mango-stats": "Mango統計",
"market": "市場",
"max": "最多",
"max-borrow": "最多借貸",
"more": "更多",
"new": "新",
"new-account": "開戶",
"new-account-failed": "開戶出錯",
"new-account-success": "您的新帳戶準備好了😎",
"new-version": "新版本出來了",
"optional": "可選",
"outstanding-balance": "未結餘額",
"perp": "合約",
"perp-markets": "合約市場",
"pnl": "盈虧",
"price": "價格",
"quantity": "數量",
"rate": "利率(APR)",
"rates": "利率(APR)",
"remove": "刪除",
"remove-delegate": "剷除委託",
"repay": "歸還",
"repay-borrow": "還貸",
"repay-deposit": "歸還及存入",
"repayment-amount": "還貸額",
"rolling-change": "24小時變化",
"save": "存",
"select": "選擇",
"select-borrow-token": "選借入幣種",
"select-deposit-token": "選存入幣種",
"select-repay-token": "選環貸幣種",
"select-token": "選幣種",
"select-withdraw-token": "選提出幣種",
"sell": "賣",
"settings": "設置",
"show-more": "顯示更多",
"show-zero-balances": "顯示零餘額",
"solana-tps": "Solana TPS",
"spot": "Spot",
"spot-markets": "Spot Markets",
"stats": "Stats",
"swap": "Swap",
"terms-of-use": "Terms of Use",
"time": "Time",
"today": "Today",
"token": "Token",
"tokens": "Tokens",
"token-collateral-multiplier": "{{token}} Collateral Multiplier",
"tooltip-borrow-rate": "The variable interest rate you'll pay on your borrowed balance",
"tooltip-collateral-value": "The USD amount you can trade or borrow against",
"total": "Total",
"total-borrows": "Total Borrows",
"total-borrow-value": "Total Borrow Value",
"total-collateral": "Total Collateral",
"total-deposits": "Total Deposits",
"total-deposit-value": "Total Deposit Value",
"total-interest-earned": "Total Interest Earned",
"trade": "Trade",
"trade-history": "Trade History",
"transaction": "Transaction",
"unavailable": "Unavailable",
"unowned-helper": "Currently viewing account {{accountPk}}",
"update": "Update",
"update-delegate": "Update Delegate",
"updating-account-name": "Updating Account Name...",
"utilization": "Utilization",
"value": "Value",
"view-transaction": "View Transaction",
"wallet-address": "Wallet Address",
"wallet-balance": "Wallet Balance",
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"list-token": "List Token"
}
"spot": "現貨",
"spot-markets": "現貨市場",
"stats": "統計",
"swap": "換幣",
"terms-of-use": "使用條款",
"time": "時間",
"today": "今天",
"token": "幣種",
"token-collateral-multiplier": "{{token}}質押品乘數",
"tokens": "幣種",
"tooltip-borrow-rate": "您將為借入餘額支付的可變利率",
"tooltip-collateral-value": "您可以交易或借入的美元金額",
"total": "總計",
"total-borrow-value": "總借貸價值",
"total-borrows": "總借貸",
"total-collateral": "總質押品",
"total-deposit-value": "總存款價值",
"total-deposits": "總存款",
"total-interest-earned": "總獲取利息",
"trade": "交易",
"trade-history": "交易紀錄",
"transaction": "交易",
"unavailable": "不可用",
"unowned-helper": "目前查看帳戶 {{accountPk}}",
"update": "更新",
"update-delegate": "改委託帳戶",
"updating-account-name": "正載改帳戶標籤...",
"utilization": "利用率",
"value": "價值",
"view-transaction": "查看交易",
"wallet-address": "錢包地址",
"wallet-balance": "錢包餘額",
"wallet-disconnected": "已斷開錢包連接",
"withdraw": "取款",
"withdraw-amount": "取款額",
"list-token": "列表代币",
"vote": "投票"
}

View File

@ -3,7 +3,7 @@
"cancel": "Cancel",
"connect-wallet": "Connect Wallet",
"on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms",
"on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into",
"on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into",
"on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.",
"tokens-deposited": "Tokens Deposited",
"new-listing": "New Token Listing",
@ -54,5 +54,21 @@
"market-name": "Market Name",
"proposal-title": "Proposal Title",
"proposal-des": "Proposal Description",
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions"
"error-proposal-creation": "Error on proposal creation or no confirmation of transactions",
"vote-yes": "Vote Yes",
"vote-no": "Vote No",
"relinquish-vote": "Relinquish Vote",
"voting-ended": "Voting ended",
"ends": "Ends",
"approval-q": "Approval Quorum",
"required-approval-achieved": "Required approval achieved",
"no-votes": "No Votes",
"yes-votes": "Yes Votes",
"quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.",
"active-proposals": "Active Proposals",
"no-active-proposals": "No active proposals",
"your-votes": "Your Votes:",
"yes": "Yes",
"no": "No",
"current-vote": "Current Vote:"
}

View File

@ -1,58 +1,58 @@
{
"account-dashboard": "Account Dashboard",
"account-dashboard-desc": "Here you'll find the important information related to your account.",
"account-value": "Account Value",
"account-value-desc": "The value of your assets (deposits) minus the value of your liabilities (borrows).",
"free-collateral": "Free Collateral",
"free-collateral-desc": "The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals.",
"health": "Health",
"health-desc": "If your account health reaches 0% your account will be liquidated. You can increase the health of your account by making a deposit.",
"account-summary": "Account Summary",
"account-summary-desc": "Check your key account information from any screen in the app.",
"health-impact": "Health Impact",
"health-impact-desc": "Projects the health of your account before you make a swap. The first value is your current account health and the second, your projected account health.",
"interest-earned": "Interest Earned",
"interest-earned-desc": "The sum of interest earned and interest paid for each token.",
"ioc": "Immediate or Cancel (IoC)",
"ioc-desc": "An order condition that attempts to execute all or part of an order immediately and then cancels any unfilled portion.",
"leverage": "Leverage",
"leverage-desc": "The total size of your positions divided by your total collateral.",
"margin": "Margin",
"margin-desc": "When margin is on you can trade with more size than your token balance. Using margin increases your risk of loss. If you're not an experienced trader, use it with caution.",
"market-selector": "Market Selector",
"market-selector-desc": "Chose the market you want to trade.",
"oracle-price": "Oracle Price",
"oracle-price-desc": "The oracle price uses an average of price data from many sources. It's used to avoid price manipulation which could lead to liquidations.",
"orderbook-grouping": "Orderbook Grouping",
"orderbook-grouping-desc": "Adjust the price intervals to change how orders are grouped. Small intervals will show more small orders in the book",
"pay-token": "Pay Token",
"pay-token-desc": "Select the token you want to swap from (pay/sell). If you have margin switched on and your size is above your token balance a loan will be opened to cover the shortfall. Check the borrow rate before making a margin swap.",
"pnl": "PnL (Profit and Loss)",
"pnl-desc": "The amount your account has made or lost.",
"account-dashboard": "帳戶概要",
"account-dashboard-desc": "在這裡可以看到關於帳戶的重要資料。",
"account-value": "帳戶價值",
"account-value-desc": "你的資產價值(存款)減去你的債務價值(借貸)。",
"free-collateral": "可用的質押品",
"free-collateral-desc": "您可以用於交易或借入的資本金額。當您的可用質押品達到零時,您將無法取款。",
"health": "健康",
"health-desc": "若您的賬戶健康度達到零,您的賬戶將被清算。您可以存款而提高賬戶的健康度。",
"account-summary": "帳戶概要",
"account-summary-desc": "從APP的任何頁面檢查您的主要帳戶信息。",
"health-impact": "健康影響",
"health-impact-desc": "此在您進行換幣之前預測您賬戶的健康狀況。第一個值是您當前的帳戶健康狀況,第二個值是您的預測帳戶健康狀況。",
"interest-earned": "獲取利息",
"interest-earned-desc": "每個幣種賺取及支付的利息總和。",
"ioc": "立刻或取消IoC",
"ioc-desc": "嘗試立刻執行全部或部分訂單然後取消任何未執行部分的訂單條件。",
"leverage": "槓桿",
"leverage-desc": "您持倉的總價值除以您的質押品總價值。",
"margin": "保證金",
"margin-desc": "當保證金開啟時,您能使用比您的帳戶餘額更大的規模進行交易。利用保證金會增加您的損失風險。若您的交易經驗不多,請謹慎使用。",
"market-selector": "市場選擇器",
"market-selector-desc": "選擇您想交易的市場。",
"oracle-price": "預言機價格",
"oracle-price-desc": "預言機價格是來自許多來源的價格數據的平均。它用於避免可能導致清算的市場操縱。",
"orderbook-grouping": "掛單薄分組",
"orderbook-grouping-desc": "調整價格區間以更改訂單的分組方式。小間隔會在掛單簿中顯示更多的小單",
"pay-token": "支付幣種",
"pay-token-desc": "選擇您要換的幣種(支付/出售)。若您開啟了保證金功能並且支付數量多於您的幣種餘額,將借貸以彌補差額。在進行槓桿換幣之前檢查借貸利率。",
"pnl": "盈虧",
"pnl-desc": "您的賬戶賺取或損失的金額。",
"post-only": "Post Only",
"post-only-desc": "An order condition that will only allow your order to enter the orderbook as a maker order. If the condition can't be met the order will be cancelled.",
"profile-menu": "Profile Menu",
"profile-menu-desc": "If you haven't chosen a profile name yet, you'll see your assigned one here. You can edit it and change your profile image from this menu.",
"rates": "Rates",
"rates-desc": "The interest rates (per year) for depositing (green/left) and borrowing (red/right).",
"receive-token": "Receive Token",
"receive-token-desc": "The token you'll receive in your Mango Account after making a swap. You can think of this token as the one you're buying/longing.",
"recent-trades": "Recent Trades",
"recent-trades-desc": "Shows the most recent trades for a market across all accounts.",
"spread": "Spread",
"spread-desc": "The difference between the prices quoted for an immediate sell (ask) and an immediate buy (bid). Or, in other words, the difference between the lowest sell price and the highest buy price.",
"swap": "We've Juiced Swap",
"swap-desc": "The swap you know and love + leverage. Swap lets you trade tokens on their relative strength. Let's say your thesis is BTC will see diminishing returns relative to SOL. You can sell BTC and buy SOL. Now you are long SOL/BTC",
"swap-settings": "Swap Settings",
"swap-settings-desc": "Edit your slippage settings and toggle margin on and off. When margin is off your swaps will be limited by your balance for each token.",
"toggle-orderbook": "Toggle Orderbook",
"toggle-orderbook-desc": "Use these buttons if you only want to see one side of the orderbook. Looking to bid/buy? Toggle off the buy orders to only see the sells and vice versa.",
"total-interest-earned": "Total Interest Earned",
"total-interest-earned-desc": "The value of interest earned (deposits) minus interest paid (borrows).",
"trade": "Trade 100s of Tokens...",
"trade-desc": "A refined interface without listing limits. The tokens you want to trade are now on Mango and no longer only quoted in USDC.",
"unsettled-balance": "Unsettled Balance",
"unsettled-balance-desc": "When a limit order is filled, the funds are placed in your unsettled balances. When you have an unsettled balance you'll see a 'Settle All' button above this table. Use it to move the funds to your account balance.",
"your-accounts": "Your Accounts",
"your-accounts-desc": "Switch between accounts and create new ones. Use multiple accounts to trade isolated margin and protect your capital from liquidation."
"post-only-desc": "一個訂單條件,只允許您作為掛單者進入掛單簿。如果不能滿足條件,訂單將被取消。",
"profile-menu": "個人檔案",
"profile-menu-desc": "若您還沒選擇個人檔案名稱您會在此處看到Mango創造給您的名稱。您可以從此頁面編輯它並更改您的個人頭像。",
"rates": "利率",
"rates-desc": "存款(綠色/左)和借款(紅色/右)的利率(每年)。",
"receive-token": "獲取幣種",
"receive-token-desc": "換幣後您將在Mango賬戶中收到的幣種。您可以將此幣種視為您正在購買/做多的幣種。",
"recent-trades": "最近交易",
"recent-trades-desc": "顯示一個市場所有賬戶的最近交易。",
"spread": "差價",
"spread-desc": "即時賣出和即時買入的差價。換句話說,最低賣出價和最高買入價之間的差額。",
"swap": "新換幣版本",
"swap-desc": "你已熟悉的換幣加槓桿。換幣功能讓您可以根據幣種的相對強度進行幣種交易。舉個例子您認為BTC相對於SOL的價值將降低您可以出售BTC並購買SOL。您就做多SOL/BTC",
"swap-settings": "換幣設定",
"swap-settings-desc": "編輯您的滑點設置並開啟和關閉保證金功能。保證金功能關閉時,換幣將受到每個代幣餘額的限制。",
"toggle-orderbook": "切換掛單薄",
"toggle-orderbook-desc": "若您只想看到掛單簿的一側,請用這些按鈕。想要出價/購買?關閉買單以僅查看賣單,反之亦然。",
"total-interest-earned": "總利息",
"total-interest-earned-desc": "賺取的利息(存款)減去支付的利息(借款)的價值。",
"trade": "買賣數百個幣種...",
"trade-desc": "沒有限制的交易界面。您想要買賣的幣種現在在Mango上不再僅以USDC報價。",
"unsettled-balance": "未結算餘額",
"unsettled-balance-desc": "執行限價單後,資金將存入您的未結算餘額中。當您有未結算餘額時,您會在該表格上方看到“全部結算”按鈕。用它來將資金轉移到您的賬戶餘額。",
"your-accounts": "您的帳戶",
"your-accounts-desc": "在帳戶之間切換並創建新帳戶。使用多個賬戶進行逐倉保證金交易,保護您的資金免遭清算。"
}

View File

@ -1,19 +1,19 @@
{
"bullet-1": "Swap any token pair with margin.",
"bullet-2": "Trade spot & perpetuals up to 10x leverage.",
"bullet-3": "Earn interest on all deposits.",
"bullet-4": "Borrow any listed token.",
"choose-wallet": "Choose Wallet",
"connect-wallet": "Connect Wallet",
"create-account": "Create Your Account",
"create-account-desc": "You need a Mango Account to get started",
"fund-account": "Fund Your Account",
"intro-desc": "A magical new way to interact with DeFi. Groundbreaking safety features designed to keep your funds secure. The easiest way to margin trade any token pair. All powered by flashloans.",
"intro-heading": "Safer. Smarter. Faster.",
"lets-go": "Let's Go",
"profile-desc": "Add an NFT profile image and edit your assigned name",
"save-finish": "Save and Finish",
"skip": "Skip for now",
"skip-finish": "Skip and Finish",
"your-profile": "Your Profile"
"bullet-1": "以保證金來交換任何幣種對",
"bullet-2": "以高達十倍的槓桿率買賣現貨和永續合約。",
"bullet-3": "所有存款可以賺取利息。",
"bullet-4": "借入任何被列出幣種",
"choose-wallet": "選擇錢包",
"connect-wallet": "連接錢包",
"create-account": "開戶",
"create-account-desc": "開始之前您必須開Mango帳戶",
"fund-account": "為您的賬戶注資",
"intro-desc": "一種與DeFi交互的神奇新方式。突破性的安全功能確保您的資金安全。保證金交易任何幣種對的最簡單方法。全部由閃電貸支持。",
"intro-heading": "更安全。更聰明。更迅速。",
"lets-go": "開始嘍",
"profile-desc": "加個NFT個人檔案頭像並編輯您的帳戶標籤",
"save-finish": "保存並完成",
"skip": "暫時跳過",
"skip-finish": "跳過並完成",
"your-profile": "您的個人檔案"
}

View File

@ -10,7 +10,7 @@
"follow": "追蹤",
"following": "追蹤中",
"invalid-characters": "限制於字母、数字和空格",
"length-error": "Names must be less than 20 characters",
"length-error": "標籤必須少於20個字符",
"market-maker": "做市商",
"no-followers": "無追蹤者",
"no-followers-desc": "以隱身模式交易😎",
@ -19,7 +19,7 @@
"no-nfts": "😞 未找到NFT...",
"no-profile-exists": "此帳戶不存在...",
"profile": "帳戶",
"profile-api-error": "Profile update is unavailable. Please try again later",
"profile-api-error": "無法更新帳戶。請稍後再嘗試",
"profile-fetch-fail": "查帳戶細節出錯",
"profile-name": "帳戶標籤",
"profile-pic-failure": "設置頭像失敗",
@ -32,13 +32,13 @@
"save-profile": "保存帳戶",
"set-profile-pic": "設置頭像",
"swing-trader": "擺動交易者",
"total-pnl": "組合總盈虧",
"total-value": "組合總價值",
"total-pnl": "總合盈虧",
"total-value": "總合價值",
"trader": "交易者",
"trader-category": "交易模式",
"unfollow": "取消追蹤",
"uniqueness-api-fail": "Failed to check profile name uniqueness",
"uniqueness-fail": "Profile name is taken. Try another one",
"uniqueness-api-fail": "無法檢查帳戶標籤的唯一性",
"uniqueness-fail": "帳戶標籤已被使用。試試另一個",
"yolo": "YOLO",
"your-profile": "您的帳戶"
}

View File

@ -1,14 +1,14 @@
{
"mango-account": "Account Address",
"mango-account-name": "Account Name",
"no-results": "No Results",
"no-results-desc": "Try another search. Searches are case-sensitive",
"profile-name": "Profile Name",
"results": "Results",
"search": "Search",
"search-accounts": "Search Accounts",
"search-by": "Search By",
"search-for": "Search For",
"search-failed": "Something went wrong. Try again later",
"wallet-pk": "Wallet Address"
"mango-account": "帳戶地址",
"mango-account-name": "帳戶標籤",
"no-results": "無結果",
"no-results-desc": "嘗試其他搜尋。搜尋區分大小寫",
"profile-name": "個人檔案標籤",
"results": "結果",
"search": "搜尋",
"search-accounts": "搜尋帳戶",
"search-by": "搜尋方式",
"search-for": "搜尋",
"search-failed": "出錯了。稍後再試。",
"wallet-pk": "錢包地址"
}

View File

@ -1,54 +1,54 @@
{
"animations": "Animations",
"avocado": "Avocado",
"banana": "Banana",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
"buttons": "Buttons",
"chart-left": "Chart Left",
"chart-middle-ob-left": "Chart Middle, Orderbook Left",
"chart-middle-ob-right": "Chart Middle, Orderbook Right",
"chart-right": "Chart Right",
"animations": "動畫",
"avocado": "酪梨",
"banana": "香蕉",
"blueberry": "藍莓",
"bottom-left": "左下",
"bottom-right": "右下",
"buttons": "按鈕",
"chart-left": "圖表左",
"chart-middle-ob-left": "圖表中,掛單薄左",
"chart-middle-ob-right": "圖表中,掛單薄右",
"chart-right": "圖表右",
"chinese": "简体中文",
"chinese-traditional": "繁體中文",
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"custom": "自定",
"dark": "",
"display": "顯示",
"english": "English",
"high-contrast": "High Contrast",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"notification-position": "Notification Position",
"number-scroll": "Number Scroll",
"olive": "Olive",
"orderbook-flash": "Orderbook Flash",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"high-contrast": "高對比度",
"language": "語言",
"light": "",
"lychee": "荔枝",
"mango": "芒果",
"mango-classic": "芒果經典",
"medium": "",
"notification-position": "通知位置",
"number-scroll": "數字滑動",
"olive": "橄欖",
"orderbook-flash": "掛單薄閃光",
"preferred-explorer": "首選探索器",
"recent-trades": "最近交易",
"rpc": "RPC",
"rpc-provider": "RPC Provider",
"rpc-url": "Enter RPC URL",
"rpc-provider": "RPC提供者",
"rpc-url": "輸入RPC URL",
"russian": "Русский",
"save": "Save",
"slider": "Slider",
"save": "",
"slider": "滑塊",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
"solanafm": "SolanaFM",
"solscan": "Solscan",
"sounds": "Sounds",
"sounds": "聲響",
"spanish": "Español",
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View"
"swap-success": "換幣/交易成功",
"swap-trade-size-selector": "換幣/交易大小選擇器",
"theme": "模式",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易圖表",
"trade-layout": "交易佈局",
"trading-view": "Trading View",
"transaction-fail": "交易失敗",
"transaction-success": "交易成功"
}

View File

@ -1,37 +1,37 @@
{
"confirm-swap": "Confirm Swap",
"connect-swap": "Connect to view your swap history",
"est-liq-price": "Est. Liq Price",
"fees-paid-to": "Fees Paid to {{route}}",
"health-impact": "Health Impact",
"hide-fees": "Hide Fees",
"input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token",
"insufficient-balance": "Insufficient {{symbol}} Balance",
"insufficient-collateral": "Insufficient Collateral",
"margin-swap": "Margin Swap",
"max-slippage": "Max Slippage",
"maximum-cost": "Maximum Cost",
"minimum-received": "Minimum Received",
"no-history": "No swap history",
"output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only",
"paid": "Paid",
"pay": "You Pay",
"preset": "Preset",
"price-impact": "Price Impact",
"rate": "Rate",
"receive": "You Receive",
"received": "Received",
"review-swap": "Review Swap",
"show-fees": "Show Fees",
"slippage": "Slippage",
"swap-history": "Swap History",
"swap-into-1": "Borrow against your collateral and swap with up to 5x leverage.",
"swap-into-2": "Swap your Mango assets via the top DEXs on Solana and get the best possible price.",
"swap-into-3": "Use the slider to set your swap size. Margin can be switched off in swap settings.",
"swap-route": "Swap Route",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed",
"use-margin": "Allow Margin",
"no-swap-found": "No swap found"
"confirm-swap": "確定換幣",
"connect-swap": "連接來查換幣紀錄",
"est-liq-price": "預計清算價格",
"fees-paid-to": "繳給{{route}}的費用",
"health-impact": "健康影響",
"hide-fees": "隱藏費用",
"input-reduce-only-warning": "{{symbol}}處於僅減少模式。您可以將餘額換成另一個幣種",
"insufficient-balance": "{{symbol}}餘額不夠",
"insufficient-collateral": "質押品不夠",
"margin-swap": "槓桿換幣",
"max-slippage": "最多下滑",
"maximum-cost": "最大成本",
"minimum-received": "最小獲取",
"no-history": "無換幣紀錄",
"output-reduce-only-warning": "{{symbol}}處於僅減少模式。換幣限於歸還借貸",
"paid": "付出",
"pay": "你將付出",
"preset": "預設",
"price-impact": "價格影響",
"rate": "匯率",
"receive": "你將獲取",
"received": "獲取",
"review-swap": "檢視換幣",
"show-fees": "顯示費用",
"slippage": "下滑",
"swap-history": "換幣紀錄",
"swap-into-1": "以你的質押品來借貸並進行高達5倍槓桿換幣。",
"swap-into-2": "通過 Solana 上的頂級 DEX 交換您的 Mango 資產,並獲得最優惠的價格。",
"swap-into-3": "使用滑塊設置交換大小。在換幣設置中可以關閉槓桿功能。",
"swap-route": "換幣路線",
"tooltip-borrow-balance": "您將使用您的 {{balance}} {{token}} 餘額並借入 {{borrowAmount}} {{token}} 來執行此交換。當前的 {{token}} 可變借貸利率為 {{rate}}%",
"tooltip-borrow-no-balance": "您將借入 {{borrowAmount}} {{token}} 來執行此交換。當前的 {{token}} 可變借貸利率為 {{rate}}%",
"tooltip-max-slippage": "如果價格下滑超過您的最大滑點,換幣交易將不會被執行",
"use-margin": "允許槓桿",
"no-swap-found": "查不到換幣"
}

View File

@ -1,27 +1,27 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"borrows": "Borrows",
"borrow-rates": "Borrow Rates",
"chart-unavailable": "Chart Unavailable",
"circulating-supply": "Circulating Supply",
"deposits": "Deposits",
"deposit-rates": "Deposit Rates",
"fdv": "Fully Diluted Value",
"fees-tooltip": "Fees collected for loan origination, loan upkeep and Openbook hosting rebates.",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"no-borrowers": "No Borrowers...",
"no-depositors": "No Depositors...",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"top-borrowers": "Top {{symbol}} Borrowers",
"top-depositors": "Top {{symbol}} Depositors",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
"all-time-high": "歷史高價",
"all-time-low": "歷史低價",
"borrow-rates": "借貸利率",
"borrowing": "借入",
"borrows": "借貸",
"chart-unavailable": "無法顯示圖表",
"circulating-supply": "流通供應量",
"deposit-rates": "存款利率",
"deposits": "存款",
"fdv": "全稀釋市值",
"fees-tooltip": "為貸款發放、貸款維護和Openbook收取的費用。",
"go-to-account": "檢視帳戶",
"lending": "借出",
"market-cap": "總市值",
"max-supply": "最大供應量",
"no-borrowers": "無借貸者...",
"no-depositors": "無存款者...",
"token-details": "幣種細節",
"token-not-found": "查不到幣種",
"token-not-found-desc": "'{{token}}'未列入或我們在加載數據時出錯。",
"top-borrowers": "頂級{{symbol}}借貸者",
"top-depositors": "頂級{{symbol}}存款者",
"total-supply": "總供應量",
"total-value": "全市值",
"volume": "24小時交易量"
}

View File

@ -1,79 +1,79 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
"buys": "Buys",
"cancel-order-error": "Failed to cancel order",
"close-confirm": "Market close your {{config_name}} position",
"close-position": "Close Position",
"connect-orders": "Connect to view your open orders",
"connect-positions": "Connect to view your perp positions",
"connect-trade-history": "Connect to view your trade history",
"connect-unsettled": "Connect to view your unsettled funds",
"copy-and-share": "Copy Image to Clipboard",
"current-price": "Current Price",
"entry-price": "Entry Price",
"est-slippage": "Est. Slippage",
"funding-limits": "Funding Limits",
"funding-rate": "1h Avg Funding Rate",
"grouping": "Grouping",
"hide-asks": "Hide Asks",
"hide-bids": "Hide Bids",
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"init-leverage": "Init Leverage",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"maker": "Maker",
"margin": "Margin",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
"no-balances": "No balances",
"no-orders": "No open orders",
"no-positions": "No perp positions",
"no-unsettled": "No unsettled funds",
"notional": "Notional",
"notional-volume": "Notional Volume ($)",
"open-interest": "Open Interest",
"oracle-price": "Oracle Price",
"order-error": "Failed to place order",
"order-type": "Order Type",
"order-value": "Order Value",
"orders": "Orders",
"place-order": "Place {{side}} Order",
"placing-order": "Placing Order",
"positions": "Positions",
"activate-volume-alert": "開啟交易量警報",
"amount": "數量",
"base": "基礎",
"book": "掛單薄",
"buys": "買單",
"cancel-order-error": "取消掛單出錯",
"close-confirm": "市場平倉您的{{config_name}}持倉",
"close-position": "平倉",
"connect-orders": "連接以查看您的訂單",
"connect-positions": "連接以查看您的持倉",
"connect-trade-history": "連接以查看交易紀錄",
"connect-unsettled": "連接以查看未結清餘額",
"copy-and-share": "複製影像",
"current-price": "目前價格",
"entry-price": "入場價",
"est-slippage": "預計下滑",
"funding-limits": "資金費限制",
"funding-rate": "資金費率",
"grouping": "分組",
"hide-asks": "隱藏要價",
"hide-bids": "隱藏出價",
"hourly-funding": "1小時資金費",
"in-orders": "在掛單中",
"init-leverage": "初始槓桿",
"instantaneous-funding": "瞬時資金費率",
"interval-seconds": "間隔(秒)",
"limit-price": "限價價格",
"long": "做多",
"maker": "掛單者",
"margin": "保證金",
"market-details": "{{market}}市場細節",
"max-leverage": "最多槓桿",
"min-order-size": "最小訂單量",
"no-balances": "您沒有餘額",
"no-orders": "您沒有訂單",
"no-positions": "您沒有持倉",
"no-unsettled": "您沒有未結清餘額",
"notional": "合約面值",
"notional-volume": "面值交易量($)",
"open-interest": "持倉量",
"oracle-price": "預言機價格",
"order-error": "下訂單出錯了",
"order-type": "訂單方式",
"order-value": "訂單價值",
"orders": "訂單",
"place-order": "下{{side}}單",
"placing-order": "正在下單",
"positions": "當前持倉",
"post": "Post",
"preview-sound": "Preview Sound",
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
"quote": "Quote",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"short": "Short",
"show-asks": "Show Asks",
"show-bids": "Show Bids",
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"taker": "Taker",
"tick-size": "Tick Size",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-volume-alert": "Volume Alert Settings",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"tweet-position": "Share to Twitter",
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
"preview-sound": "聲音預覽",
"price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。",
"quote": "計價",
"reduce-only": "限減少",
"sells": "賣單",
"settle-funds": "借清資金",
"settle-funds-error": "借清出錯",
"short": "做空",
"show-asks": "顯示要價",
"show-bids": "顯示出價",
"side": "方向",
"size": "數量",
"spread": "差價",
"stable-price": "穩定價格",
"taker": "吃單者",
"tick-size": "波動單位",
"tooltip-enable-margin": "為此交易啟用保證金",
"tooltip-ioc": "IOC交易若不吃單就會被取消。任何無法立刻成交的部分將被取消",
"tooltip-post": "Post交易若不掛單就會被取消。",
"tooltip-slippage": "當前價格與您的交易將執行的價格之間的差值的估計",
"tooltip-stable-price": "穩定價格用於一個安全機制。此機制可以限制用戶在預言機價格快速波動時下風險高的訂單",
"tooltip-volume-alert": "交易量警報設定",
"trade-sounds-tooltip": "為每筆新交易播放警報聲音",
"trades": "交易",
"tweet-position": "分享至Twitter",
"unsettled": "未結清",
"volume-alert": "交易量警報",
"volume-alert-desc": "交易量超過警報設定時播放聲音"
}

View File

@ -10,6 +10,6 @@
"slippage-accept": "若您接受潛在的下滑請使用交易表格進行。",
"slippage-warning": "您的訂單價格({{updatedOrderPrice}})多餘5%{{aboveBelow}}市場價格({{selectedMarketPrice}})表是您也許遭受可觀的下滑。",
"toggle-order-line": "切換訂單線可見性",
"toggle-stable-price": "Toggle stable price line",
"toggle-trade-executions": "Toggle trade execution visibility"
"toggle-stable-price": "切換穩定價格線",
"toggle-trade-executions": "切換成交可見姓"
}

View File

@ -1,5 +1,7 @@
import { AnchorProvider, BN } from '@project-serum/anchor'
import {
getAllProposals,
getProposal,
getTokenOwnerRecord,
getTokenOwnerRecordAddress,
Governance,
@ -17,8 +19,8 @@ import {
} from 'utils/governance/constants'
import { getDeposits } from 'utils/governance/fetch/deposits'
import {
accountsToPubkeyMap,
fetchGovernances,
fetchProposals,
fetchRealm,
} from 'utils/governance/tools'
import { ConnectionContext, EndpointTypes } from 'utils/governance/types'
@ -37,19 +39,21 @@ type IGovernanceStore = {
vsrClient: VsrClient | null
loadingRealm: boolean
loadingVoter: boolean
loadingProposals: boolean
voter: {
voteWeight: BN
wallet: PublicKey
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord> | undefined | null
}
set: (x: (x: IGovernanceStore) => void) => void
initConnection: (connection: Connection) => void
initRealm: (connectionContext: ConnectionContext) => void
fetchVoterWeight: (
fetchVoter: (
wallet: PublicKey,
vsrClient: VsrClient,
connectionContext: ConnectionContext
) => void
resetVoter: () => void
updateProposals: (proposalPk: PublicKey) => void
}
const GovernanceStore = create<IGovernanceStore>((set, get) => ({
@ -60,13 +64,13 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
vsrClient: null,
loadingRealm: false,
loadingVoter: false,
loadingProposals: false,
voter: {
voteWeight: new BN(0),
wallet: PublicKey.default,
tokenOwnerRecord: null,
},
set: (fn) => set(produce(fn)),
fetchVoterWeight: async (
fetchVoter: async (
wallet: PublicKey,
vsrClient: VsrClient,
connectionContext: ConnectionContext
@ -99,11 +103,17 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
})
set((state) => {
state.voter.voteWeight = votingPower
state.voter.wallet = wallet
state.voter.tokenOwnerRecord = tokenOwnerRecord
state.loadingVoter = false
})
},
resetVoter: () => {
const set = get().set
set((state) => {
state.voter.voteWeight = new BN(0)
state.voter.tokenOwnerRecord = null
})
},
initConnection: async (connection) => {
const set = get().set
const connectionContext = {
@ -141,18 +151,40 @@ const GovernanceStore = create<IGovernanceStore>((set, get) => ({
realmId: MANGO_REALM_PK,
}),
])
const proposals = await fetchProposals({
connectionContext: connectionContext,
programId: MANGO_GOVERNANCE_PROGRAM,
governances: Object.keys(governances).map((x) => new PublicKey(x)),
})
set((state) => {
state.loadingProposals = true
})
const proposals = await getAllProposals(
connectionContext.current,
MANGO_GOVERNANCE_PROGRAM,
MANGO_REALM_PK
)
const proposalsObj = accountsToPubkeyMap(proposals.flatMap((p) => p))
set((state) => {
state.loadingProposals = false
state.realm = realm
state.governances = governances
state.proposals = proposals
state.proposals = proposalsObj
state.loadingRealm = false
})
},
updateProposals: async (proposalPk: PublicKey) => {
const state = get()
const set = get().set
set((state) => {
state.loadingProposals = true
})
const proposal = await getProposal(
state.connectionContext!.current!,
proposalPk
)
const newProposals = { ...state.proposals }
newProposals[proposal.pubkey.toBase58()] = proposal
set((state) => {
state.proposals = newProposals
state.loadingProposals = false
})
},
}))
export default GovernanceStore

View File

@ -55,6 +55,8 @@ export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1'
export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1'
export const STATS_TAB_KEY = 'activeStatsTab-0.1'
// Unused
export const PROFILE_CATEGORIES = [
'borrower',

View File

@ -1,116 +0,0 @@
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import {
deserializeBorsh,
getGovernanceSchemaForAccount,
GovernanceAccountType,
ProgramAccount,
Proposal,
} from '@solana/spl-governance'
import { PublicKey } from '@solana/web3.js'
import { ConnectionContext } from '../types'
export const getProposals = async (
pubkeys: PublicKey[],
connection: ConnectionContext,
programId: PublicKey
) => {
const proposalsRaw = await fetch(connection.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([
...pubkeys.map((x) => {
return getProposalsFilter(
programId,
connection,
bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV1])),
x
)
}),
...pubkeys.map((x) => {
return getProposalsFilter(
programId,
connection,
bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV2])),
x
)
}),
]),
})
const accounts: ProgramAccount<Proposal>[] = []
const proposalsData = await proposalsRaw.json()
const rawAccounts = proposalsData
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
proposalsData.flatMap((x: any) => x.result)
: []
for (const rawAccount of rawAccounts) {
try {
const getSchema = getGovernanceSchemaForAccount
const data = Buffer.from(rawAccount.account.data[0], 'base64')
const accountType = data[0]
const account: ProgramAccount<Proposal> = {
pubkey: new PublicKey(rawAccount.pubkey),
account: deserializeBorsh(getSchema(accountType), Proposal, data),
owner: new PublicKey(rawAccount.account.owner),
}
accounts.push(account)
} catch (ex) {
console.info(`Can't deserialize @ ${rawAccount.pubkey}, ${ex}.`)
}
}
const acc: ProgramAccount<Proposal>[][] = []
const reducedAccounts = accounts.reduce((acc, current) => {
const exsitingIdx = acc.findIndex((x) =>
x.find(
(x) =>
x.account.governance.toBase58() ===
current.account.governance.toBase58()
)
)
if (exsitingIdx > -1) {
acc[exsitingIdx].push(current)
} else {
acc.push([current])
}
return acc
}, acc)
return reducedAccounts
}
const getProposalsFilter = (
programId: PublicKey,
connection: ConnectionContext,
memcmpBytes: string,
pk: PublicKey
) => {
return {
jsonrpc: '2.0',
id: 1,
method: 'getProgramAccounts',
params: [
programId.toBase58(),
{
commitment: connection.current.commitment,
encoding: 'base64',
filters: [
{
memcmp: {
offset: 0, // number of bytes
bytes: memcmpBytes, // base58 encoded string
},
},
{
memcmp: {
offset: 1,
bytes: pk.toBase58(),
},
},
],
},
],
}
}

View File

@ -0,0 +1,127 @@
import { Connection, Keypair, TransactionInstruction } from '@solana/web3.js'
import {
ChatMessageBody,
getGovernanceProgramVersion,
GOVERNANCE_CHAT_PROGRAM_ID,
Proposal,
TokenOwnerRecord,
VoteChoice,
VoteKind,
withPostChatMessage,
} from '@solana/spl-governance'
import { ProgramAccount } from '@solana/spl-governance'
import { Vote } from '@solana/spl-governance'
import { withCastVote } from '@solana/spl-governance'
import { VsrClient } from '../voteStakeRegistryClient'
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
import { updateVoterWeightRecord } from './updateVoteWeightRecord'
import { WalletContextState } from '@solana/wallet-adapter-react'
import { MangoClient } from '@blockworks-foundation/mango-v4'
import { notify } from 'utils/notifications'
export async function castVote(
connection: Connection,
wallet: WalletContextState,
proposal: ProgramAccount<Proposal>,
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
voteKind: VoteKind,
vsrClient: VsrClient,
mangoClient: MangoClient,
message?: ChatMessageBody | undefined
) {
const signers: Keypair[] = []
const instructions: TransactionInstruction[] = []
const walletPubkey = wallet.publicKey!
const governanceAuthority = walletPubkey
const payer = walletPubkey
const programVersion = await getGovernanceProgramVersion(
connection,
MANGO_GOVERNANCE_PROGRAM
)
const { updateVoterWeightRecordIx, voterWeightPk } =
await updateVoterWeightRecord(vsrClient, walletPubkey)
instructions.push(updateVoterWeightRecordIx)
// It is not clear that defining these extraneous fields, `deny` and `veto`, is actually necessary.
// See: https://discord.com/channels/910194960941338677/910630743510777926/1044741454175674378
const vote =
voteKind === VoteKind.Approve
? new Vote({
voteType: VoteKind.Approve,
approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })],
deny: undefined,
veto: undefined,
})
: voteKind === VoteKind.Deny
? new Vote({
voteType: VoteKind.Deny,
approveChoices: undefined,
deny: true,
veto: undefined,
})
: voteKind == VoteKind.Veto
? new Vote({
voteType: VoteKind.Veto,
veto: true,
deny: undefined,
approveChoices: undefined,
})
: new Vote({
voteType: VoteKind.Abstain,
veto: undefined,
deny: undefined,
approveChoices: undefined,
})
const tokenMint = proposal.account.governingTokenMint
await withCastVote(
instructions,
MANGO_GOVERNANCE_PROGRAM,
programVersion,
MANGO_REALM_PK,
proposal.account.governance,
proposal.pubkey,
proposal.account.tokenOwnerRecord,
tokenOwnerRecord.pubkey,
governanceAuthority,
tokenMint,
vote,
payer,
voterWeightPk
)
if (message) {
const { updateVoterWeightRecordIx, voterWeightPk } =
await updateVoterWeightRecord(vsrClient, walletPubkey)
instructions.push(updateVoterWeightRecordIx)
await withPostChatMessage(
instructions,
signers,
GOVERNANCE_CHAT_PROGRAM_ID,
MANGO_GOVERNANCE_PROGRAM,
MANGO_REALM_PK,
proposal.account.governance,
proposal.pubkey,
tokenOwnerRecord.pubkey,
governanceAuthority,
payer,
undefined,
message,
voterWeightPk
)
}
const tx = await mangoClient.sendAndConfirmTransaction(instructions)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
}

View File

@ -4,7 +4,6 @@ import {
getSignatoryRecordAddress,
ProgramAccount,
serializeInstructionToBase64,
SYSTEM_PROGRAM_ID,
TokenOwnerRecord,
VoteType,
WalletSigner,
@ -22,12 +21,8 @@ import {
import { chunk } from 'lodash'
import { MANGO_MINT } from 'utils/constants'
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient'
import {
getRegistrarPDA,
getVoterPDA,
getVoterWeightPDA,
} from '../accounts/vsrAccounts'
import { VsrClient } from '../voteStakeRegistryClient'
import { updateVoterWeightRecord } from './updateVoteWeightRecord'
export const createProposal = async (
connection: Connection,
@ -57,27 +52,8 @@ export const createProposal = async (
const options = ['Approve']
const useDenyOption = true
//will run only if plugin is connected with realm
const { registrar } = await getRegistrarPDA(
MANGO_REALM_PK,
new PublicKey(MANGO_MINT),
DEFAULT_VSR_ID
)
const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID)
const { voterWeightPk } = await getVoterWeightPDA(
registrar,
walletPk,
DEFAULT_VSR_ID
)
const updateVoterWeightRecordIx = await client.program.methods
.updateVoterWeightRecord()
.accounts({
registrar,
voter,
voterWeightRecord: voterWeightPk,
systemProgram: SYSTEM_PROGRAM_ID,
})
.instruction()
const { updateVoterWeightRecordIx, voterWeightPk } =
await updateVoterWeightRecord(client, walletPk)
instructions.push(updateVoterWeightRecordIx)
const proposalAddress = await withCreateProposal(

View File

@ -0,0 +1,52 @@
import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'
import {
getGovernanceProgramVersion,
Proposal,
TokenOwnerRecord,
withRelinquishVote,
} from '@solana/spl-governance'
import { ProgramAccount } from '@solana/spl-governance'
import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants'
import { WalletContextState } from '@solana/wallet-adapter-react'
import { MangoClient } from '@blockworks-foundation/mango-v4'
import { notify } from 'utils/notifications'
export async function relinquishVote(
connection: Connection,
wallet: WalletContextState,
proposal: ProgramAccount<Proposal>,
tokenOwnerRecord: ProgramAccount<TokenOwnerRecord>,
mangoClient: MangoClient,
voteRecord: PublicKey
) {
const instructions: TransactionInstruction[] = []
const governanceAuthority = wallet.publicKey!
const beneficiary = wallet.publicKey!
const programVersion = await getGovernanceProgramVersion(
connection,
MANGO_GOVERNANCE_PROGRAM
)
await withRelinquishVote(
instructions,
MANGO_GOVERNANCE_PROGRAM,
programVersion,
MANGO_REALM_PK,
proposal.account.governance,
proposal.pubkey,
tokenOwnerRecord.pubkey,
proposal.account.governingTokenMint,
voteRecord,
governanceAuthority,
beneficiary
)
const tx = await mangoClient.sendAndConfirmTransaction(instructions)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
}

View File

@ -0,0 +1,36 @@
import { PublicKey } from '@solana/web3.js'
import { MANGO_MINT, MANGO_REALM_PK } from '../constants'
import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient'
import {
getRegistrarPDA,
getVoterPDA,
getVoterWeightPDA,
} from '../accounts/vsrAccounts'
import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance'
export const updateVoterWeightRecord = async (
client: VsrClient,
walletPk: PublicKey
) => {
const { registrar } = await getRegistrarPDA(
MANGO_REALM_PK,
new PublicKey(MANGO_MINT),
DEFAULT_VSR_ID
)
const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID)
const { voterWeightPk } = await getVoterWeightPDA(
registrar,
walletPk,
DEFAULT_VSR_ID
)
const updateVoterWeightRecordIx = await client!.program.methods
.updateVoterWeightRecord()
.accounts({
registrar,
voter,
voterWeightRecord: voterWeightPk,
systemProgram: SYSTEM_PROGRAM_ID,
})
.instruction()
return { updateVoterWeightRecordIx, voterWeightPk }
}

View File

@ -0,0 +1,67 @@
import { BN } from '@project-serum/anchor'
import {
Governance,
MintMaxVoteWeightSource,
MintMaxVoteWeightSourceType,
Proposal,
ProposalState,
} from '@solana/spl-governance'
import BigNumber from 'bignumber.js'
import dayjs from 'dayjs'
import { RawMint } from '@solana/spl-token'
export const isInCoolOffTime = (
proposal: Proposal | undefined,
governance: Governance | undefined
) => {
const mainVotingEndedAt = proposal?.signingOffAt
?.addn(governance?.config.maxVotingTime || 0)
.toNumber()
const votingCoolOffTime = governance?.config.votingCoolOffTime || 0
const canFinalizeAt = mainVotingEndedAt
? mainVotingEndedAt + votingCoolOffTime
: mainVotingEndedAt
const endOfProposalAndCoolOffTime = canFinalizeAt
? dayjs(1000 * canFinalizeAt!)
: undefined
const isInCoolOffTime = endOfProposalAndCoolOffTime
? dayjs().isBefore(endOfProposalAndCoolOffTime) &&
mainVotingEndedAt &&
dayjs().isAfter(mainVotingEndedAt * 1000)
: undefined
return !!isInCoolOffTime && proposal!.state !== ProposalState.Defeated
}
/** Returns max VoteWeight for given mint and max source */
export function getMintMaxVoteWeight(
mint: RawMint,
maxVoteWeightSource: MintMaxVoteWeightSource
) {
if (maxVoteWeightSource.type === MintMaxVoteWeightSourceType.SupplyFraction) {
const supplyFraction = maxVoteWeightSource.getSupplyFraction()
const maxVoteWeight = new BigNumber(supplyFraction.toString())
.multipliedBy(mint.supply.toString())
.shiftedBy(-MintMaxVoteWeightSource.SUPPLY_FRACTION_DECIMALS)
return new BN(maxVoteWeight.dp(0, BigNumber.ROUND_DOWN).toString())
} else {
// absolute value
return maxVoteWeightSource.value
}
}
export const calculatePct = (c = new BN(0), total?: BN) => {
if (total?.isZero()) {
return 0
}
return new BN(100)
.mul(c)
.div(total ?? new BN(1))
.toNumber()
}

View File

@ -6,8 +6,6 @@ import {
pubkeyFilter,
} from '@solana/spl-governance'
import { Connection, PublicKey } from '@solana/web3.js'
import { getProposals } from './fetch/getProposals'
import { ConnectionContext } from './types'
import { TokenProgramAccount } from './accounts/vsrAccounts'
import { MintLayout, RawMint } from '@solana/spl-token'
import BN from 'bn.js'
@ -42,25 +40,6 @@ export async function fetchGovernances({
return governancesMap
}
export async function fetchProposals({
connectionContext,
programId,
governances,
}: {
connectionContext: ConnectionContext
programId: PublicKey
governances: PublicKey[]
}) {
const proposalsByGovernance = await getProposals(
governances,
connectionContext,
programId
)
const proposals = accountsToPubkeyMap(proposalsByGovernance.flatMap((p) => p))
return proposals
}
export function accountsToPubkeyMap<T>(accounts: ProgramAccount<T>[]) {
return arrayToRecord(accounts, (a) => a.pubkey.toBase58())
}