Skip to content

Commit

Permalink
Asset redesign fixes (#2353)
Browse files Browse the repository at this point in the history
* Fix max quantity and principal

* Update repay boxes

* Update external repay form

* Update finance forms

* Error handling

* Transaction summary

* Add principal amount

* Update principal

* Use ids from dropdown

* Fix asset list report values

* Onchain reserve name

* Update type

* Fix another type

* Change gap
  • Loading branch information
hieronx authored Aug 8, 2024
1 parent 00dc506 commit 763a60c
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 219 deletions.
11 changes: 8 additions & 3 deletions centrifuge-app/src/components/Report/AssetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Text } from '@centrifuge/fabric'
import { useContext, useEffect, useMemo } from 'react'
import { useBasePath } from '../../../src/utils/useBasePath'
import { formatDate } from '../../utils/date'
import { formatBalance } from '../../utils/formatting'
import { formatBalance, formatPercentage } from '../../utils/formatting'
import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
import { useAllPoolAssetSnapshots, usePoolMetadata } from '../../utils/usePools'
import { DataTable, SortableTableHeader } from '../DataTable'
Expand Down Expand Up @@ -82,12 +82,17 @@ function getColumnConfig(isPrivate: boolean, symbol: string) {
formatter: (v: any) => (v ? formatDate(v) : 'Open-end'),
sortKey: 'maturity-date',
},
{ header: 'Valuation method', align: 'left', csvOnly: false, formatter: noop },
{
header: 'Valuation method',
align: 'left',
csvOnly: false,
formatter: (v: any) => (v === 'OutstandingDebt' ? 'At par' : v),
},
{
header: 'Advance rate',
align: 'left',
csvOnly: false,
formatter: (v: any) => (v ? formatBalance(v, symbol, 2) : '-'),
formatter: (v: any) => (v ? formatPercentage(v, true, {}, 2) : '-'),
},
{
header: 'Collateral value',
Expand Down
65 changes: 39 additions & 26 deletions centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
WithdrawAddress,
} from '@centrifuge/centrifuge-js'
import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react'
import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric'
import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
import { BN } from 'bn.js'
import Decimal from 'decimal.js-light'
import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik'
Expand Down Expand Up @@ -159,7 +159,44 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
</Field>
</Shelf>
{source === 'reserve' && withdraw.render()}
<Box bg="statusDefaultBg" p={1}>

{poolFees.render()}

{totalFinance.gt(0) && totalFinance.gt(maxAvailable) && (
<Box bg="statusCriticalBg" p={1}>
<InlineFeedback status="critical">
<Text color="statusCritical">
Principal amount ({formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available
balance ({formatBalance(maxAvailable, displayCurrency, 2)}).
</Text>
</InlineFeedback>
</Box>
)}

<Stack p={2} maxWidth="444px" bg="backgroundTertiary" gap={2} mt={2}>
<Text variant="heading4">Transaction summary</Text>
<Shelf justifyContent="space-between">
<Text variant="label2" color="textPrimary">
Available balance
</Text>
<Text variant="label2">
<Tooltip body={'Balance of the source asset'} style={{ pointerEvents: 'auto' }}>
{formatBalance(maxAvailable, displayCurrency, 2)}
</Tooltip>
</Text>
</Shelf>

<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="label2" color="textPrimary">
Principal amount
</Text>
<Text variant="label2">{formatBalance(totalFinance, displayCurrency, 2)}</Text>
</Shelf>
</Stack>

{poolFees.renderSummary()}

{source === 'reserve' ? (
<InlineFeedback status="default">
<Text color="statusDefault">
Expand All @@ -174,32 +211,8 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
</Text>
</InlineFeedback>
)}
</Box>
{poolFees.render()}
<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="emphasized">Total amount</Text>
<Text variant="emphasized">{formatBalance(totalFinance, displayCurrency, 2)}</Text>
</Shelf>

{poolFees.renderSummary()}

<Shelf justifyContent="space-between">
<Text variant="emphasized">Available</Text>
<Text variant="emphasized">{formatBalance(maxAvailable, displayCurrency, 2)}</Text>
</Shelf>
</Stack>

{totalFinance.gt(0) && totalFinance.gt(maxAvailable) && (
<Box bg="statusCriticalBg" p={1}>
<InlineFeedback status="critical">
<Text color="statusCritical">
Available financing ({formatBalance(maxAvailable, displayCurrency, 2)}) is smaller than the total
principal ({formatBalance(totalFinance, displayCurrency, 2)}).
</Text>
</InlineFeedback>
</Box>
)}
<Stack>
<Button
type="submit"
Expand Down
204 changes: 107 additions & 97 deletions centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActiveLoan, CurrencyBalance, ExternalLoan, findBalance, Price } from '@centrifuge/centrifuge-js'
import { useBalances, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react'
import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric'
import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
import { BN } from 'bn.js'
import Decimal from 'decimal.js-light'
import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik'
Expand Down Expand Up @@ -113,31 +113,31 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
const repayFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(repayForm, repayFormRef)

const { maxAvailable, maxInterest, totalRepay, maxPrincipal } = React.useMemo(() => {
const { maxAvailable, maxInterest, totalRepay, maxQuantity, principalAmount } = React.useMemo(() => {
const outstandingInterest = 'outstandingInterest' in loan ? loan.outstandingInterest.toDecimal() : Dec(0)
const outstandingDebt = 'outstandingDebt' in loan ? loan.outstandingDebt.toDecimal() : Dec(0)
const { quantity, interest, price, amountAdditional } = repayForm.values
const totalRepay = Dec(price || 0)
.mul(quantity || 0)
.add(interest || 0)
.add(amountAdditional || 0)

const principalAmount = Dec(price || 0).mul(quantity || 0)

const maxInterest = outstandingInterest
let maxPrincipal
let maxQuantity = loan.pricing.outstandingQuantity
let maxAvailable
if (destination === 'reserve') {
maxAvailable = balance
maxPrincipal = outstandingDebt.sub(outstandingInterest)
} else {
maxAvailable = UNLIMITED
maxPrincipal = UNLIMITED
}

return {
maxAvailable,
maxInterest,
maxPrincipal,
maxQuantity,
totalRepay,
principalAmount,
}
}, [loan, balance, repayForm.values])

Check warning on line 142 in centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has a missing dependency: 'destination'. Either include it or remove the dependency array

Check warning on line 142 in centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

React Hook React.useMemo has a missing dependency: 'destination'. Either include it or remove the dependency array

Expand All @@ -147,59 +147,57 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d

return (
<FormikProvider value={repayForm}>
<Stack as={Form} gap={2} noValidate ref={repayFormRef}>
<Shelf gap={1}>
<Field
validate={combine(nonNegativeNumberNotRequired(), (val) => {
const principal = Dec(val || 0).mul(repayForm.values.price || 0)
if (principal.gt(maxPrincipal)) {
return `Principal exeeds max (${formatBalance(maxPrincipal, displayCurrency, 2)})`
}
return ''
})}
name="quantity"
>
{({ field, form }: FieldProps) => {
return (
<CurrencyInput
{...field}
label="Quantity"
disabled={isRepayLoading}
onChange={(value) => form.setFieldValue('quantity', value)}
placeholder="0"
onSetMax={() =>
form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber())
}
secondaryLabel={`${loan.pricing.outstandingQuantity.toDecimal().toString()} outstanding`}
/>
)
}}
</Field>
<Field
validate={combine(nonNegativeNumberNotRequired(), (val) => {
const principal = Dec(val || 0).mul(repayForm.values.quantity || 0)
if (principal.gt(maxPrincipal)) {
return `Principal exeeds max (${formatBalance(maxPrincipal, displayCurrency, 2)})`
}
return ''
})}
name="price"
>
{({ field, form }: FieldProps) => {
return (
<CurrencyInput
{...field}
label="Settlement price"
disabled={isRepayLoading}
currency={displayCurrency}
onChange={(value) => form.setFieldValue('price', value)}
decimals={8}
secondaryLabel={'\u200B'} // zero width space
/>
)
}}
</Field>
</Shelf>
<Stack as={Form} gap={3} noValidate ref={repayFormRef}>
<Stack gap={1}>
<Shelf gap={1}>
<Field
validate={combine(nonNegativeNumberNotRequired(), (val) => {
if (Dec(val || 0).gt(maxQuantity.toDecimal())) {
return `Quantity exeeds max (${maxQuantity.toString()})`
}
return ''
})}
name="quantity"
>
{({ field, form }: FieldProps) => {
return (
<CurrencyInput
{...field}
label="Quantity"
disabled={isRepayLoading}
onChange={(value) => form.setFieldValue('quantity', value)}
placeholder="0"
onSetMax={() =>
form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber())
}
/>
)
}}
</Field>
<Field name="price">
{({ field, form }: FieldProps) => {
return (
<CurrencyInput
{...field}
label="Settlement price"
disabled={isRepayLoading}
currency={displayCurrency}
onChange={(value) => form.setFieldValue('price', value)}
decimals={8}
/>
)
}}
</Field>
</Shelf>

<Shelf justifyContent="space-between">
<Text variant="label2"> Principal</Text>
<Text variant="label2" color="textPrimary">
= {formatBalance(principalAmount, displayCurrency, 2)}
</Text>
</Shelf>
</Stack>

{'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && (
<Field
validate={combine(nonNegativeNumberNotRequired(), maxNotRequired(maxInterest.toNumber()))}
Expand Down Expand Up @@ -238,35 +236,9 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
)
}}
</Field>
<Box bg="statusDefaultBg" p={1}>
{destination === 'reserve' ? (
<InlineFeedback status="default">
<Text color="statusDefault">Stablecoins will be transferred to the onchain reserve.</Text>
</InlineFeedback>
) : (
<InlineFeedback status="default">
<Text color="statusDefault">
Virtual accounting process. No onchain stablecoin transfers are expected.
</Text>
</InlineFeedback>
)}
</Box>
{poolFees.render()}
<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="emphasized">Total amount</Text>
<Text variant="emphasized">{formatBalance(totalRepay, displayCurrency, 2)}</Text>
</Shelf>

{poolFees.renderSummary()}
{poolFees.render()}

<Shelf justifyContent="space-between">
<Text variant="emphasized">Available</Text>
<Text variant="emphasized">
{maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)}
</Text>
</Shelf>
</Stack>
{destination === 'reserve' && totalRepay.gt(balance) && (
<Box bg="statusCriticalBg" p={1}>
<InlineFeedback status="critical">
Expand All @@ -278,19 +250,12 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
</InlineFeedback>
</Box>
)}
{Dec(repayForm.values.price || 0)
.mul(repayForm.values.quantity || 0)
.gt(maxPrincipal) && (
{Dec(repayForm.values.quantity || 0).gt(maxQuantity.toDecimal()) && (
<Box bg="statusCriticalBg" p={1}>
<InlineFeedback status="critical">
<Text color="statusCritical">
Principal (
{formatBalance(
Dec(Dec(repayForm.values.price || 0).mul(repayForm.values.quantity || 0)),
displayCurrency,
2
)}
) is greater than the outstanding principal ({formatBalance(maxPrincipal, displayCurrency, 2)}).
Quantity ({repayForm.values.quantity}) is greater than the outstanding quantity (
{maxQuantity.toDecimal().toString()}).
</Text>
</InlineFeedback>
</Box>
Expand All @@ -305,6 +270,51 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
</InlineFeedback>
</Box>
)}

<Stack p={2} maxWidth="444px" bg="backgroundTertiary" gap={2} mt={2}>
<Text variant="heading4">Transaction summary</Text>
<Shelf justifyContent="space-between">
<Text variant="label2" color="textPrimary">
Available balance
</Text>
<Text variant="label2">
<Tooltip
body={
maxAvailable === UNLIMITED
? 'Unlimited because this is a virtual accounting process.'
: 'Balance of the asset originator account on Centrifuge'
}
style={{ pointerEvents: 'auto' }}
>
{maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)}
</Tooltip>
</Text>
</Shelf>

<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="label2" color="textPrimary">
Sale amount
</Text>
<Text variant="label2">{formatBalance(totalRepay, displayCurrency, 2)}</Text>
</Shelf>
</Stack>

{poolFees.renderSummary()}

{destination === 'reserve' ? (
<InlineFeedback status="default">
<Text color="statusDefault">Stablecoins will be transferred to the onchain reserve.</Text>
</InlineFeedback>
) : (
<InlineFeedback status="default">
<Text color="statusDefault">
Virtual accounting process. No onchain stablecoin transfers are expected.
</Text>
</InlineFeedback>
)}
</Stack>

<Stack gap={1}>
<Button
type="submit"
Expand Down
Loading

0 comments on commit 763a60c

Please sign in to comment.