Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
56 changes: 48 additions & 8 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,6 +48,7 @@ interface Props {
data: VaultData
vaultId: string
displayCurrency: string
assetScale?: number
}

// Vault flags from XLS-65d spec
Expand All @@ -59,7 +61,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 +93,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

const numAmount = Number(amount)
if (Number.isNaN(numAmount)) 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 @@ -300,7 +316,7 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
) {
return '--'
}
const amount = convertedAmount ?? assetsTotal
const amount = convertedAmount ?? '0'
if (amount === undefined) return '--'
if (
['0', '0.00', '0.0000'].includes(
Expand Down Expand Up @@ -334,7 +350,15 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
value={(() => {
if (assetsMaximum === undefined) return t('no_limit')

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

const displayedCurrency: string = getDisplayCurrencyLabel()
Expand All @@ -353,7 +377,15 @@ export const VaultHeader = ({ data, vaultId, displayCurrency }: Props) => {
<TokenTableRow
label={t('available_to_borrow')}
value={(() => {
const parsedAmt = parseAmount(assetsAvailable ?? '0', 2)
const convertedAmount =
convertToDisplayCurrency(assetsAvailable)
if (
convertedAmount === undefined &&
displayCurrency === 'USD'
) {
return '--'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think there's a default empty value variable somewhere, instead of hardcoding repetitively

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not exported constant, each component has a local constant as shown below (for instance, AMM page, Amendments Table etc). I can create a constant here following the same convention, if we need an repo wide export and want to consume it, we can refactor entire codebase in another task.

const DEFAULT_EMPTY_VALUE = '--'

}
const parsedAmt = parseAmount(convertedAmount ?? '0', 2)
if (['0', '0.00', '0.0000'].includes(parsedAmt)) return '--'

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

const displayedCurrency: string = getDisplayCurrencyLabel()
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