mango-v4-ui/components/modals/DashboardSuggestedValuesMod...

829 lines
28 KiB
TypeScript

import { ReactNode, useCallback, useEffect, useState } from 'react'
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import mangoStore from '@store/mangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
import GovernanceStore from '@store/governanceStore'
import {
formatSuggestedValues,
getApiTokenName,
getFormattedBankValues,
} from 'utils/governance/listingTools'
import {
Bank,
Group,
OracleProvider,
toUiDecimals,
} from '@blockworks-foundation/mango-v4'
import { AccountMeta, Transaction } from '@solana/web3.js'
import { BN } from '@project-serum/anchor'
import {
MANGO_DAO_WALLET,
MANGO_DAO_WALLET_GOVERNANCE,
} from 'utils/governance/constants'
import { createProposal } from 'utils/governance/instructions/createProposal'
import { notify } from 'utils/notifications'
import Button from '@components/shared/Button'
import {
compareObjectsAndGetDifferentKeys,
tryGetPubKey,
} from 'utils/governance/tools'
import { Disclosure } from '@headlessui/react'
import {
LISTING_PRESET,
LISTING_PRESETS,
LISTING_PRESETS_KEY,
MidPriceImpact,
getPresetWithAdjustedDepositLimit,
getPresetWithAdjustedNetBorrows,
} from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools'
import Select from '@components/forms/Select'
import Loading from '@components/shared/Loading'
import Label from '@components/forms/Label'
import Input from '@components/forms/Input'
import Switch from '@components/forms/Switch'
import { PublicKey } from '@metaplex-foundation/js'
//import useBanks from 'hooks/useBanks'
const DashboardSuggestedValues = ({
isOpen,
onClose,
bank,
group,
suggestedTierKey,
currentTier,
midPriceImp,
}: ModalProps & {
bank: Bank
group: Group
suggestedTierKey: LISTING_PRESETS_KEY
currentTier: LISTING_PRESET | undefined
midPriceImp: MidPriceImpact[]
}) => {
const client = mangoStore((s) => s.client)
//do not deconstruct wallet is used for anchor to sign
const wallet = useWallet()
//const { banks } = useBanks()
const connection = mangoStore((s) => s.connection)
const fee = mangoStore((s) => s.priorityFee)
const voter = GovernanceStore((s) => s.voter)
const vsrClient = GovernanceStore((s) => s.vsrClient)
const proposals = GovernanceStore((s) => s.proposals)
const [oracle, setOracle] = useState('')
const [forcePythOracle, setForcePythOracle] = useState(false)
const PRESETS = LISTING_PRESETS
const [proposedTier, setProposedTier] =
useState<LISTING_PRESETS_KEY>(suggestedTierKey)
const [proposing, setProposing] = useState(false)
useEffect(() => {
setForcePythOracle(bank?.oracleProvider === OracleProvider.Pyth)
}, [bank.oracleProvider])
// const proposeMultipleEdit = async () => {
// const toPropose = banks.filter(
// (x) =>
// x.oracleProvider === OracleProvider.Switchboard && x.reduceOnly === 0,
// )
// const walletSigner = wallet as never
// const proposalInstructions = []
// for (const bank of toPropose) {
// const ix = await client!.program.methods
// .tokenEdit(
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// 0,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// false,
// false,
// 2,
// null,
// true,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// false,
// false,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// )
// .accounts({
// group: group!.publicKey,
// fallbackOracle:
// bank.fallbackOracle && tryGetPubKey(bank.fallbackOracle.toBase58())
// ? bank.fallbackOracle
// : PublicKey.default,
// oracle: bank.oracle,
// admin: MANGO_DAO_WALLET,
// mintInfo: group!.mintInfosMapByTokenIndex.get(bank.tokenIndex)!
// .publicKey,
// })
// .remainingAccounts([
// {
// pubkey: bank.publicKey,
// isWritable: true,
// isSigner: false,
// } as AccountMeta,
// ])
// .instruction()
// proposalInstructions.push(ix)
// }
// const index = proposals ? Object.values(proposals).length : 0
// const proposalAddress = await createProposal(
// connection,
// client,
// walletSigner,
// MANGO_DAO_WALLET_GOVERNANCE,
// voter.tokenOwnerRecord!,
// `Edit switchboard oracle tokens`,
// 'Adjust settings to current liquidity',
// index,
// proposalInstructions,
// vsrClient!,
// fee,
// )
// window.open(
// `https://dao.mango.markets/dao/MNGO/proposal/${proposalAddress.toBase58()}`,
// '_blank',
// )
// }
const proposeNewSuggestedValues = useCallback(
async (
bank: Bank,
invalidFieldsKeys: string[],
tokenTier: LISTING_PRESETS_KEY,
) => {
const proposalTx = []
const mintInfo = group!.mintInfosMapByTokenIndex.get(bank.tokenIndex)!
const preset = getPresetWithAdjustedDepositLimit(
getPresetWithAdjustedNetBorrows(
PRESETS[tokenTier],
bank.uiDeposits(),
bank.uiPrice,
toUiDecimals(PRESETS[tokenTier].netBorrowLimitPerWindowQuote, 6),
),
bank.uiPrice,
bank.mintDecimals,
)
const fieldsToChange = invalidFieldsKeys.reduce(
(obj, key) => ({ ...obj, [key]: preset[key as keyof typeof preset] }),
{},
) as Partial<typeof preset>
const oracleConfFilter =
fieldsToChange.oracleConfFilter === undefined
? null
: fieldsToChange.oracleConfFilter
const maxStalenessSlots =
(fieldsToChange.maxStalenessSlots as number | string) === '' ||
fieldsToChange.maxStalenessSlots === -1
? null
: fieldsToChange.maxStalenessSlots
const isThereNeedOfSendingOracleConfig =
bank.oracleConfig.confFilter.toNumber() !== oracleConfFilter ||
bank.oracleConfig.maxStalenessSlots.toNumber() !== maxStalenessSlots
const rateConfigs = {
adjustmentFactor: getNullOrVal(fieldsToChange.adjustmentFactor),
util0: getNullOrVal(fieldsToChange.util0),
rate0: getNullOrVal(fieldsToChange.rate0),
util1: getNullOrVal(fieldsToChange.util1),
rate1: getNullOrVal(fieldsToChange.rate1),
maxRate: getNullOrVal(fieldsToChange.maxRate),
}
const isThereNeedOfSendingRateConfigs = Object.values(rateConfigs).filter(
(x) => x !== null,
).length
const ix = await client!.program.methods
.tokenEdit(
oracle ? tryGetPubKey(oracle) : null,
isThereNeedOfSendingOracleConfig
? {
confFilter:
fieldsToChange.oracleConfFilter === undefined
? bank.oracleConfig.confFilter.toNumber()
: fieldsToChange.oracleConfFilter,
maxStalenessSlots:
fieldsToChange.maxStalenessSlots === undefined
? bank.oracleConfig.maxStalenessSlots.toNumber() === -1
? null
: bank.oracleConfig.maxStalenessSlots.toNumber()
: fieldsToChange.maxStalenessSlots === -1
? null
: fieldsToChange.maxStalenessSlots,
}
: null,
fieldsToChange.groupInsuranceFund === undefined
? null
: fieldsToChange.groupInsuranceFund,
isThereNeedOfSendingRateConfigs
? {
adjustmentFactor:
fieldsToChange.adjustmentFactor ||
bank.adjustmentFactor.toNumber(),
util0: fieldsToChange.util0 || bank.util0.toNumber(),
rate0: fieldsToChange.rate0 || bank.rate0.toNumber(),
util1: fieldsToChange.util1 || bank.util1.toNumber(),
rate1: fieldsToChange.rate1 || bank.rate1.toNumber(),
maxRate: fieldsToChange.maxRate || bank.maxRate.toNumber(),
}
: null,
getNullOrVal(fieldsToChange.loanFeeRate),
getNullOrVal(fieldsToChange.loanOriginationFeeRate),
getNullOrVal(fieldsToChange.maintAssetWeight),
getNullOrVal(fieldsToChange.initAssetWeight),
getNullOrVal(fieldsToChange.maintLiabWeight),
getNullOrVal(fieldsToChange.initLiabWeight),
getNullOrVal(fieldsToChange.liquidationFee),
null,
null,
null,
getNullOrVal(fieldsToChange.minVaultToDepositsRatio),
getNullOrVal(fieldsToChange.netBorrowLimitPerWindowQuote)
? new BN(fieldsToChange.netBorrowLimitPerWindowQuote!)
: null,
getNullOrVal(fieldsToChange.netBorrowLimitWindowSizeTs)
? new BN(fieldsToChange.netBorrowLimitWindowSizeTs!)
: null,
getNullOrVal(fieldsToChange.borrowWeightScaleStartQuote),
getNullOrVal(fieldsToChange.depositWeightScaleStartQuote),
false,
false,
getNullOrVal(fieldsToChange.reduceOnly),
null,
null,
getNullOrVal(fieldsToChange.tokenConditionalSwapTakerFeeRate),
getNullOrVal(fieldsToChange.tokenConditionalSwapMakerFeeRate),
getNullOrVal(fieldsToChange.flashLoanSwapFeeRate),
//do not edit of interest curve scaling
null,
getNullOrVal(fieldsToChange.interestTargetUtilization),
null,
null,
null,
null,
false,
false,
getNullOrVal(fieldsToChange.depositLimit) !== null
? new BN(fieldsToChange.depositLimit!.toString())
: null,
getNullOrVal(fieldsToChange.zeroUtilRate),
getNullOrVal(fieldsToChange.platformLiquidationFee),
fieldsToChange.disableAssetLiquidation === undefined
? null
: fieldsToChange.disableAssetLiquidation,
getNullOrVal(fieldsToChange.collateralFeePerDay),
null,
preset.preset_name,
)
.accounts({
group: group!.publicKey,
fallbackOracle:
bank.fallbackOracle && tryGetPubKey(bank.fallbackOracle.toBase58())
? bank.fallbackOracle
: PublicKey.default,
oracle: bank.oracle,
admin: MANGO_DAO_WALLET,
mintInfo: mintInfo.publicKey,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.instruction()
proposalTx.push(ix)
const walletSigner = wallet as never
try {
setProposing(true)
const simTransaction = new Transaction({ feePayer: wallet.publicKey })
simTransaction.add(...proposalTx)
const simulation = await connection.simulateTransaction(simTransaction)
if (!simulation.value.err) {
const index = proposals ? Object.values(proposals).length : 0
const proposalAddress = await createProposal(
connection,
client,
walletSigner,
MANGO_DAO_WALLET_GOVERNANCE,
voter.tokenOwnerRecord!,
`Edit token ${bank.name}`,
'Adjust settings to current liquidity',
index,
proposalTx,
vsrClient!,
fee,
)
window.open(
`https://dao.mango.markets/dao/MNGO/proposal/${proposalAddress.toBase58()}`,
'_blank',
)
} else {
throw simulation.value.logs
}
} catch (e) {
notify({
title: 'Error during proposal creation',
description: `${e}`,
type: 'error',
})
}
setProposing(false)
},
[
PRESETS,
client,
connection,
fee,
group,
oracle,
proposals,
voter.tokenOwnerRecord,
vsrClient,
wallet,
],
)
const mintInfo = group.mintInfosMapByMint.get(bank.mint.toString())
const formattedBankValues = getFormattedBankValues(group, bank)
const suggestedValues = getPresetWithAdjustedDepositLimit(
getPresetWithAdjustedNetBorrows(
PRESETS[proposedTier as LISTING_PRESETS_KEY] as LISTING_PRESET,
bank.uiDeposits(),
bank.uiPrice,
toUiDecimals(
PRESETS[proposedTier as LISTING_PRESETS_KEY]
.netBorrowLimitPerWindowQuote,
6,
),
),
bank.uiPrice,
bank.mintDecimals,
)
const suggestedFormattedPreset = formatSuggestedValues(suggestedValues)
type SuggestedFormattedPreset = typeof suggestedFormattedPreset
const invalidKeys: (keyof SuggestedFormattedPreset)[] = Object.keys(
suggestedValues,
).length
? compareObjectsAndGetDifferentKeys<SuggestedFormattedPreset>(
formattedBankValues,
suggestedFormattedPreset,
).filter(
(x: string) =>
suggestedFormattedPreset[x as keyof SuggestedFormattedPreset] !==
undefined &&
suggestedFormattedPreset[x as keyof SuggestedFormattedPreset] !==
null,
)
: []
const suggestedFields: Partial<SuggestedFormattedPreset> = invalidKeys.reduce(
(obj, key) => {
return {
...obj,
[key]: suggestedFormattedPreset[key],
}
},
{},
)
return (
<Modal panelClassNames="!max-w-[800px]" isOpen={isOpen} onClose={onClose}>
<h3 className="mb-6">
<span>
{bank.name} - Suggested tier: {PRESETS[suggestedTierKey].preset_name}{' '}
Current tier: ~ {currentTier?.preset_name}
</span>
<div className="py-4">
<p className="mb-2">
I want to use pyth oracle (will show presets available for pyth)
</p>
<Switch
checked={forcePythOracle}
onChange={(checked) => setForcePythOracle(checked)}
/>
</div>
<Select
value={PRESETS[proposedTier].preset_name}
onChange={(tier: LISTING_PRESETS_KEY) => setProposedTier(tier)}
className="w-full"
>
{Object.keys(PRESETS).map((name) => (
<Select.Option key={name} value={name}>
<div className="flex w-full items-center justify-between">
{PRESETS[name as LISTING_PRESETS_KEY].preset_name}{' '}
{`{${PRESETS[name as LISTING_PRESETS_KEY].preset_key}}`}
{name === suggestedTierKey ? ' - suggested' : ''}
</div>
</Select.Option>
))}
</Select>
</h3>
<div className="flex w-full flex-col">
<div className="p-4">
<div className="mb-2">
<Label text="Oracle pk (Leave empty if no change)" />
<Input
type="text"
value={oracle}
onChange={(e) => {
setOracle(e.target.value)
}}
/>
{oracle !== '' && !tryGetPubKey(oracle) && (
<div className="text-th-error">Invalid publickey</div>
)}
</div>
</div>
<Disclosure.Panel>
<KeyValuePair
label="Loan Fee Rate"
value={`${formattedBankValues.loanFeeRate} bps`}
proposedValue={
suggestedFields.loanFeeRate &&
`${suggestedFields.loanFeeRate} bps`
}
/>
<KeyValuePair
label="Loan origination fee rate"
value={`${formattedBankValues.loanOriginationFeeRate} bps`}
proposedValue={
suggestedFields.loanOriginationFeeRate &&
`${suggestedFields.loanOriginationFeeRate} bps`
}
/>
<KeyValuePair
label="Maint Asset/Liab Weight"
value={`${formattedBankValues.maintAssetWeight} /
${formattedBankValues.maintLiabWeight}`}
proposedValue={
(suggestedFields.maintAssetWeight ||
suggestedFields.maintLiabWeight) &&
`${
suggestedFields.maintAssetWeight ||
formattedBankValues.maintAssetWeight
} /
${
suggestedFields.maintLiabWeight ||
formattedBankValues.maintLiabWeight
}`
}
/>
<KeyValuePair
label="Init Asset/Liab Weight"
value={`${formattedBankValues.initAssetWeight} /
${formattedBankValues.initLiabWeight}`}
proposedValue={
(suggestedFields.initAssetWeight ||
suggestedFields.initLiabWeight) &&
`${
suggestedFields.initAssetWeight ||
formattedBankValues.initAssetWeight
} /
${
suggestedFields.initLiabWeight ||
formattedBankValues.initLiabWeight
}`
}
/>
<KeyValuePair
label="Deposit weight scale start quote"
value={`$${formattedBankValues.depositWeightScaleStartQuote}`}
proposedValue={
suggestedFields.depositWeightScaleStartQuote &&
`$${suggestedFields.depositWeightScaleStartQuote}`
}
/>
<KeyValuePair
label="Borrow weight scale start quote"
value={`$${formattedBankValues.borrowWeightScaleStartQuote}`}
proposedValue={
suggestedFields.borrowWeightScaleStartQuote &&
`$${suggestedFields.borrowWeightScaleStartQuote}`
}
/>
<KeyValuePair
label="Rate params"
value={
<span className="text-right">
{`${formattedBankValues.rate0}% @ ${formattedBankValues.util0}% util, `}
{`${formattedBankValues.rate1}% @ ${formattedBankValues.util1}% util, `}
{`${formattedBankValues.maxRate}% @ 100% util`}
</span>
}
proposedValue={
(suggestedFields.rate0 ||
suggestedFields.rate1 ||
suggestedFields.util0 ||
suggestedFields.util1 ||
suggestedFields.maxRate) && (
<span className="text-right">
{`${suggestedFields.rate0 || formattedBankValues.rate0}% @ ${
suggestedFields.util0 || formattedBankValues.util0
}% util, `}
{`${suggestedFields.rate1 || formattedBankValues.rate1}% @ ${
suggestedFields.util1 || formattedBankValues.util1
}% util, `}
{`${
suggestedFields.maxRate || formattedBankValues.maxRate
}% @ 100% util`}
</span>
)
}
/>
<KeyValuePair
label="Adjustment factor"
value={`${formattedBankValues.adjustmentFactor}%`}
proposedValue={
suggestedFields.adjustmentFactor &&
`${suggestedFields.adjustmentFactor}%`
}
/>
<KeyValuePair
label="Oracle: Conf Filter"
value={`${formattedBankValues.oracleConfFilter}%`}
proposedValue={
suggestedFields.oracleConfFilter &&
`${suggestedFields.oracleConfFilter}%`
}
/>
<KeyValuePair
label="Oracle: Max Staleness"
value={`${bank.oracleConfig.maxStalenessSlots} slots`}
proposedValue={
suggestedFields.maxStalenessSlots &&
`${suggestedFields.maxStalenessSlots} slots`
}
/>
<KeyValuePair
label="Group Insurance Fund"
value={`${mintInfo!.groupInsuranceFund}`}
/>
<KeyValuePair
label="Min vault to deposits ratio"
value={`${formattedBankValues.minVaultToDepositsRatio}%`}
proposedValue={
suggestedFields.minVaultToDepositsRatio &&
`${suggestedFields.minVaultToDepositsRatio}%`
}
/>
<KeyValuePair
label="Net borrows in window / Net borrow limit per window quote"
value={`$${formattedBankValues.netBorrowsInWindow} / $${formattedBankValues.netBorrowLimitPerWindowQuote}`}
proposedValue={
(suggestedFields.minVaultToDepositsRatio ||
suggestedFields.netBorrowLimitPerWindowQuote) &&
`$${
suggestedFields.minVaultToDepositsRatio ||
formattedBankValues.minVaultToDepositsRatio
} / $${
suggestedFields.netBorrowLimitPerWindowQuote ||
formattedBankValues.netBorrowLimitPerWindowQuote
}`
}
/>
<KeyValuePair
label="Liquidation fee"
value={`${formattedBankValues.liquidationFee}%`}
proposedValue={
suggestedFields.liquidationFee &&
`${suggestedFields.liquidationFee}%`
}
/>
<KeyValuePair
label="Group Insurance Fund"
value={`${formattedBankValues.groupInsuranceFund}`}
proposedValue={
suggestedFields.groupInsuranceFund !== undefined &&
`${suggestedFields.groupInsuranceFund}`
}
/>
<KeyValuePair
label="Net Borrow Limit Window Size Ts"
value={`${formattedBankValues.netBorrowLimitWindowSizeTs}`}
proposedValue={
suggestedFields.netBorrowLimitWindowSizeTs !== undefined &&
`${suggestedFields.netBorrowLimitWindowSizeTs}`
}
/>
<KeyValuePair
label="Stable Price Delay Interval Seconds"
value={`${formattedBankValues.stablePriceDelayIntervalSeconds}`}
proposedValue={
suggestedFields.stablePriceDelayIntervalSeconds !== undefined &&
`${suggestedFields.stablePriceDelayIntervalSeconds}`
}
/>
<KeyValuePair
label="Stable Price Growth Limit"
value={`${formattedBankValues.stablePriceGrowthLimit}`}
proposedValue={
suggestedFields.stablePriceGrowthLimit !== undefined &&
`${suggestedFields.stablePriceGrowthLimit}`
}
/>
<KeyValuePair
label="Stable Price Delay Growth Limit"
value={`${formattedBankValues.stablePriceDelayGrowthLimit}`}
proposedValue={
suggestedFields.stablePriceDelayGrowthLimit !== undefined &&
`${suggestedFields.stablePriceDelayGrowthLimit}`
}
/>
<KeyValuePair
label="Token Conditional Swap Taker Fee Rate"
value={`${formattedBankValues.tokenConditionalSwapTakerFeeRate}`}
proposedValue={
suggestedFields.tokenConditionalSwapTakerFeeRate !== undefined &&
`${suggestedFields.tokenConditionalSwapTakerFeeRate}`
}
/>
<KeyValuePair
label="Token Conditional Swap Maker Fee Rate"
value={`${formattedBankValues.tokenConditionalSwapMakerFeeRate}`}
proposedValue={
suggestedFields.tokenConditionalSwapMakerFeeRate !== undefined &&
`${suggestedFields.tokenConditionalSwapMakerFeeRate}`
}
/>
<KeyValuePair
label="Interest Target Utilization"
value={`${formattedBankValues.interestTargetUtilization}`}
proposedValue={
suggestedFields.interestTargetUtilization !== undefined &&
`${suggestedFields.interestTargetUtilization}`
}
/>
<KeyValuePair
label="Deposit Limit"
value={`${formattedBankValues.depositLimit}`}
proposedValue={
suggestedFields.depositLimit !== undefined &&
`${toUiDecimals(
new BN(suggestedFields.depositLimit.toString()),
bank.mintDecimals,
)}`
}
/>
<KeyValuePair
label="Flash Loan Swap Fee Rate"
value={`${formattedBankValues.flashLoanSwapFeeRate}`}
proposedValue={
suggestedFields.flashLoanSwapFeeRate !== undefined &&
`${suggestedFields.flashLoanSwapFeeRate}`
}
/>
<KeyValuePair
label="Reduce Only"
value={`${formattedBankValues.reduceOnly}`}
proposedValue={
suggestedFields.reduceOnly !== undefined &&
`${suggestedFields.reduceOnly}`
}
/>
<KeyValuePair
label="Zero Util Rate"
value={`${formattedBankValues.zeroUtilRate} bps`}
proposedValue={
suggestedFields.zeroUtilRate &&
`${suggestedFields.zeroUtilRate} bps`
}
/>
<KeyValuePair
label="Liquidation Fee"
value={`${formattedBankValues.platformLiquidationFee} bps`}
proposedValue={
suggestedFields.platformLiquidationFee &&
`${suggestedFields.platformLiquidationFee} bps`
}
/>
<div>
<h3 className="mb-4 pl-6">Price impacts</h3>
{midPriceImp
.filter((x) => x.symbol === getApiTokenName(bank.name))
.map((x) => (
<div className="flex pl-6" key={x.target_amount}>
<p className="mr-4 w-[150px] space-x-4">
<span>Amount:</span>
<span>${x.target_amount}</span>
</p>
<p className="space-x-4">
<span>Price impact:</span>{' '}
<span>{x.avg_price_impact_percent.toFixed(3)}%</span>
</p>
</div>
))}
</div>
</Disclosure.Panel>
{invalidKeys.length && (
<div className="flex items-center p-4">
<p className="mr-auto ">
Green values are params that needs to change suggested by current
liquidity
</p>
<Button
onClick={() =>
proposeNewSuggestedValues(
bank,
invalidKeys,
proposedTier as LISTING_PRESETS_KEY,
)
}
disabled={!wallet.connected || proposing}
>
{proposing ? <Loading></Loading> : 'Propose new suggested values'}
</Button>
</div>
)}
</div>
</Modal>
)
}
export default DashboardSuggestedValues
const getNullOrVal = (val: number | undefined) => {
if (val !== undefined) {
return val
}
return null
}
const KeyValuePair = ({
label,
value,
proposedValue,
}: {
label: string
value: number | ReactNode | string
proposedValue?: number | ReactNode | string
}) => {
return (
<div className="flex items-center justify-between border-t border-th-bkg-2 px-6 py-3">
<span className="mr-4 flex flex-col whitespace-nowrap text-th-fgd-3">
{label}
</span>
<span className="flex flex-col font-mono text-th-fgd-2">
<div>
{proposedValue && <span>Current: </span>}
<span className={`${proposedValue ? 'text-th-warning' : ''}`}>
{value}
</span>
</div>
<div>
{proposedValue && <span>Suggested: </span>}
<span>
{proposedValue && (
<span className="text-th-success">{proposedValue}</span>
)}
</span>
</div>
</span>
</div>
)
}