Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions src/containers/Vault/VaultHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
shortenAccount,
getCurrencySymbol,
isCurrencyExoticSymbol,
convertScaledPrice,
} from '../../shared/utils'
import './styles.scss'
import { useAnalytics } from '../../shared/analytics'
Expand Down Expand Up @@ -47,8 +48,11 @@ interface Props {
data: VaultData
vaultId: string
displayCurrency: string
assetScale?: number
}

const DEFAULT_EMPTY_VALUE = '--'

// Vault flags from XLS-65d spec
const VAULT_FLAGS = {
lsfVaultPrivate: 0x00010000,
Expand All @@ -59,7 +63,12 @@ const WITHDRAWAL_POLICIES: { [key: number]: string } = {
1: 'first_come_first_served',
}

export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
export const VaultHeader = ({
data,
vaultId,
displayCurrency,
assetScale,
}: Props) => {
const { t } = useTranslation()
const { trackException } = useAnalytics()
const rippledSocket = useContext(SocketContext)
Expand All @@ -86,10 +95,19 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
const convertToDisplayCurrency = (
amount: string | undefined,
): string | undefined => {
if (!amount || displayCurrency !== 'USD') return amount
if (!amount) return amount

let normalized = amount
if (asset?.currency === 'XRP') {
normalized = convertScaledPrice(BigInt(amount), 6)
} else if (asset?.mpt_issuance_id) {
normalized = convertScaledPrice(BigInt(amount), assetScale ?? 0)
}

if (displayCurrency !== 'USD') return normalized

const numAmount = Number(amount)
if (Number.isNaN(numAmount)) return amount
const numAmount = Number(normalized)
if (Number.isNaN(numAmount)) return normalized

return tokenToUsdRate > 0 ? String(numAmount * tokenToUsdRate) : undefined
}
Expand Down Expand Up @@ -298,16 +316,16 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
convertedAmount === undefined &&
displayCurrency === 'USD'
) {
return '--'
return DEFAULT_EMPTY_VALUE
}
const amount = convertedAmount ?? assetsTotal
if (amount === undefined) return '--'
const amount = convertedAmount ?? '0'
if (amount === undefined) return DEFAULT_EMPTY_VALUE
if (
['0', '0.00', '0.0000'].includes(
parseAmount(amount ?? '0', 2),
)
)
return '--'
return DEFAULT_EMPTY_VALUE
// Note: As per the NumberFormat policy, prices in the range of [10_000, 1M] do not display decimal values
// Very large prices (greater than 1M must have two decimal places)
const displayedCurrency: string = getDisplayCurrencyLabel()
Expand All @@ -334,8 +352,17 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
value={(() => {
if (assetsMaximum === undefined) return t('no_limit')

const parsedAmt = parseAmount(assetsMaximum, 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt)) return '--'
const convertedAmount =
convertToDisplayCurrency(assetsMaximum)
if (
convertedAmount === undefined &&
displayCurrency === 'USD'
) {
return DEFAULT_EMPTY_VALUE
}
const parsedAmt = parseAmount(convertedAmount ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt))
return DEFAULT_EMPTY_VALUE

const displayedCurrency: string = getDisplayCurrencyLabel()
if (
Expand All @@ -353,8 +380,17 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
<TokenTableRow
label={t('available_to_borrow')}
value={(() => {
const parsedAmt = parseAmount(assetsAvailable ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt)) return '--'
const convertedAmount =
convertToDisplayCurrency(assetsAvailable)
if (
convertedAmount === undefined &&
displayCurrency === 'USD'
) {
return DEFAULT_EMPTY_VALUE
}
const parsedAmt = parseAmount(convertedAmount ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt))
return DEFAULT_EMPTY_VALUE

const displayedCurrency: string = getDisplayCurrencyLabel()
if (
Expand All @@ -368,8 +404,17 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
<TokenTableRow
label={t('unrealized_loss')}
value={(() => {
const parsedAmt = parseAmount(lossUnrealized ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt)) return '--'
const convertedAmount =
convertToDisplayCurrency(lossUnrealized)
if (
convertedAmount === undefined &&
displayCurrency === 'USD'
) {
return DEFAULT_EMPTY_VALUE
}
const parsedAmt = parseAmount(convertedAmount ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt))
return DEFAULT_EMPTY_VALUE

const displayedCurrency: string = getDisplayCurrencyLabel()
if (
Expand Down
91 changes: 79 additions & 12 deletions src/containers/Vault/VaultHeader/test/VaultHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,9 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsTotal: '12500000', // 12.5 million
AssetsAvailable: '5000000', // 5 million
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsTotal: '12500000000000', // 12.5M XRP
AssetsAvailable: '5000000000000', // 5M XRP
}

render(
Expand All @@ -458,7 +459,6 @@ describe('VaultHeader Component', () => {
)

// Numbers >= 1,000,000 should display with M suffix
// Verify exact formatted values: 12,500,000 -> "12.5M XRP", 5,000,000 -> "5M XRP"
expect(screen.getByText('\uE900 12.50M')).toBeInTheDocument()
expect(screen.getByText('\uE900 5.00M')).toBeInTheDocument()
})
Expand All @@ -467,8 +467,9 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsAvailable: '250000', // 250 thousand
LossUnrealized: '75000', // 75 thousand
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsAvailable: '250000000000', // 250K XRP
LossUnrealized: '75000000000', // 75K XRP
}

render(
Expand All @@ -482,7 +483,6 @@ describe('VaultHeader Component', () => {
)

// Numbers >= 1,000 but < 1,000,000 should display with K suffix
// Verify exact formatted values: 250,000 -> "250K XRP", 75,000 -> "75K XRP"
expect(screen.getByText('\uE900 250.00K')).toBeInTheDocument()
expect(screen.getByText('\uE900 75.00K')).toBeInTheDocument()
})
Expand All @@ -491,7 +491,8 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsAvailable: '500', // Less than 1000
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsAvailable: '500000000', // 500 XRP
}

render(
Expand All @@ -505,7 +506,6 @@ describe('VaultHeader Component', () => {
)

// Numbers < 1,000 should display as-is without K/M suffix
// Verify exact formatted value: 500 -> "500 XRP"
expect(screen.getByText('\uE900 500.00')).toBeInTheDocument()
})

Expand Down Expand Up @@ -1050,6 +1050,43 @@ describe('VaultHeader Component', () => {
expect(screen.getByText('5,000.00 VTKN')).toBeInTheDocument()
})
})

it('scales raw MPT amounts by 10^AssetScale before display', async () => {
const mptId = '00001234ABCD5678EF90ABCDEF1234567890ABCDEF'
const mptMetadata = {
ticker: 'VTKN',
name: 'Vault Token',
}
const mptMetadataHex = Buffer.from(JSON.stringify(mptMetadata))
.toString('hex')
.toUpperCase()

mockedGetMPTIssuance.mockResolvedValue({
node: { MPTokenMetadata: mptMetadataHex, AssetScale: 6 },
})

const vaultData = {
Owner: 'rTestOwner',
Asset: { mpt_issuance_id: mptId },
// Raw value 5,000,000 with AssetScale 6 → 5 VTKN (not 5,000,000 VTKN)
AssetsAvailable: '5000000',
}

render(
<TestWrapper>
<VaultHeader
data={vaultData}
vaultId="ABC123"
displayCurrency="XRP"
assetScale={6}
/>
</TestWrapper>,
)

await waitFor(() => {
expect(screen.getByText('5.00 VTKN')).toBeInTheDocument()
})
})
})

/**
Expand Down Expand Up @@ -1119,7 +1156,8 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsMaximum: '10000000', // 10 million
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsMaximum: '10000000000000', // 10M XRP
}

render(
Expand Down Expand Up @@ -1170,7 +1208,8 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsTotal: '5000000',
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsTotal: '5000000000000', // 5M XRP
}

render(
Expand Down Expand Up @@ -1303,7 +1342,8 @@ describe('VaultHeader Component', () => {
const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
AssetsTotal: '1000000', // 1 million XRP
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsTotal: '1000000000000', // 1M XRP
}

render(
Expand All @@ -1317,7 +1357,6 @@ describe('VaultHeader Component', () => {
)

// 1,000,000 XRP * 2.5 = 2,500,000 USD = "2.50M USD"
// formatAmount joins [prefix, formattedNum, currency] with spaces
expect(screen.getByText('$2.50M USD')).toBeInTheDocument()
})

Expand Down Expand Up @@ -1368,5 +1407,33 @@ describe('VaultHeader Component', () => {
const tvlRow = screen.getByText('Total Value Locked (TVL)').closest('tr')
expect(tvlRow).toHaveTextContent('Total Value Locked (TVL)$2.00M USD')
})

it('converts Available to Borrow to USD using exchange rate', () => {
mockXRPToUSDRate.mockReturnValue(2.5)
mockTokenToUSDRate.mockImplementation((token: any) => {
if (token?.currency === 'XRP') return 2.5
return 0
})

const vaultData = {
Owner: 'rTestOwner',
Asset: { currency: 'XRP' },
// XRP amounts on the Vault ledger entry are in drops (1 XRP = 1,000,000 drops)
AssetsAvailable: '2000000000000', // 2M XRP * 2.5 USD = $5M
}

render(
<TestWrapper>
<VaultHeader
data={vaultData}
vaultId="ABC123"
displayCurrency="USD"
/>
</TestWrapper>,
)

const availableRow = screen.getByText('Available to Borrow').closest('tr')
expect(availableRow).toHaveTextContent('Available to Borrow$5.00M USD')
})
})
})
19 changes: 16 additions & 3 deletions src/containers/Vault/VaultLoans/BrokerDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
shortenMPTID,
getCurrencySymbol,
isCurrencyExoticSymbol,
convertScaledPrice,
} from '../../shared/utils'

// TODO: Use types from xrpl.js instead of hand-writing it.
Expand Down Expand Up @@ -36,6 +37,7 @@ interface Props {
asset?: AssetInfo
loans?: any[]
mptTicker?: string
assetScale?: number
}

export const BrokerDetails = ({
Expand All @@ -44,6 +46,7 @@ export const BrokerDetails = ({
asset,
loans,
mptTicker,
assetScale,
}: Props) => {
const { t } = useTranslation()
const { rate: tokenToUsdRate } = useTokenToUSDRate(asset)
Expand All @@ -52,9 +55,18 @@ export const BrokerDetails = ({
const convertToDisplayCurrency = (
amount: string | undefined,
): string | undefined => {
if (!amount || displayCurrency !== 'USD') return amount
const numAmount = Number(amount)
if (Number.isNaN(numAmount)) return amount
if (!amount) return amount

let normalized = amount
if (asset?.currency === 'XRP') {
normalized = convertScaledPrice(BigInt(amount), 6)
} else if (asset?.mpt_issuance_id) {
normalized = convertScaledPrice(BigInt(amount), assetScale ?? 0)
}
if (displayCurrency !== 'USD') return normalized

const numAmount = Number(normalized)
if (Number.isNaN(numAmount)) return normalized
return tokenToUsdRate > 0 ? String(numAmount * tokenToUsdRate) : undefined
}

Expand Down Expand Up @@ -162,6 +174,7 @@ export const BrokerDetails = ({
}
displayCurrency={displayCurrency}
asset={asset}
assetScale={assetScale}
isCurrencySpecialSymbol={
asset?.currency !== undefined &&
isCurrencyExoticSymbol(asset?.currency)
Expand Down
3 changes: 3 additions & 0 deletions src/containers/Vault/VaultLoans/BrokerLoansTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Props {
currency: string
displayCurrency: string
asset?: AssetInfo
assetScale?: number
isCurrencySpecialSymbol?: boolean
}

Expand All @@ -28,6 +29,7 @@ export const BrokerLoansTable = ({
currency,
displayCurrency,
asset,
assetScale,
isCurrencySpecialSymbol = false,
}: Props) => {
const { t } = useTranslation()
Expand Down Expand Up @@ -126,6 +128,7 @@ export const BrokerLoansTable = ({
currency={currency}
displayCurrency={effectiveDisplayCurrency}
asset={asset}
assetScale={assetScale}
isCurrencySpecialSymbol={isCurrencySpecialSymbol}
/>
))}
Expand Down
Loading
Loading