Skip to content

Commit

Permalink
Merge branch 'main' into fix-invest-chart
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored Sep 2, 2024
2 parents 79ae89d + 47af6ec commit 3912629
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 124 deletions.
52 changes: 23 additions & 29 deletions centrifuge-app/src/components/Report/CashflowStatement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,37 +202,31 @@ export function CashflowStatement({ pool }: { pool: Pool }) {

const netCashflowRecords: Row[] = React.useMemo(() => {
return [
...(poolFeeStates
?.map((poolFeeStateByPeriod) => {
return Object.values(poolFeeStateByPeriod)
?.map((feeState) => {
// some fee data may be incomplete since fees may have been added sometime after pool creation
// this fill the nonexistant fee data with zero values
let missingStates: {
timestamp: string
sumPaidAmountByPeriod: CurrencyBalance
}[] = []
if (feeState.length !== poolStates?.length) {
const missingTimestamps = poolStates
?.map((state) => state.timestamp)
.filter((timestamp) => !feeState.find((state) => state.timestamp === timestamp))
missingStates =
missingTimestamps?.map((timestamp) => {
return {
timestamp,
sumPaidAmountByPeriod: CurrencyBalance.fromFloat(0, pool.currency.decimals),
}
}) || []
}
...(Object.entries(poolFeeStates || {})?.flatMap(([, feeState]) => {
// some fee data may be incomplete since fees may have been added sometime after pool creation
// this fill the nonexistant fee data with zero values
let missingStates: {
timestamp: string
sumPaidAmountByPeriod: CurrencyBalance
}[] = []
if (feeState.length !== poolStates?.length) {
const missingTimestamps = poolStates
?.map((state) => state.timestamp)
.filter((timestamp) => !feeState.find((state) => state.timestamp.slice(0, 10) === timestamp.slice(0, 10)))
missingStates =
missingTimestamps?.map((timestamp) => {
return {
name: feeState[0].poolFee.name,
value: [...missingStates, ...feeState].map((state) => state.sumPaidAmountByPeriod.toDecimal().neg()),
formatter: (v: any) => `${formatBalance(v, pool.currency.displayName, 2)}`,
timestamp,
sumPaidAmountByPeriod: CurrencyBalance.fromFloat(0, pool.currency.decimals),
}
})
.flat()
})
.flat() || []),
}) || []
}
return {
name: feeState[0].poolFee.name,
value: [...missingStates, ...feeState].map((state) => state.sumPaidAmountByPeriod.toDecimal().neg()),
formatter: (v: any) => `${formatBalance(v, pool.currency.displayName, 2)}`,
}
}) || []),
{
name: 'Net cash flow after fees',
value:
Expand Down
6 changes: 5 additions & 1 deletion centrifuge-app/src/pages/Loan/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Box, InlineFeedback, Text } from '@centrifuge/fabric'

type Props = {
children: React.ReactNode
type: 'default' | 'critical'
type: 'default' | 'critical' | 'warning'
condition: boolean
}

Expand All @@ -15,6 +15,10 @@ const styles: Record<Props['type'], { bg: string; color: string }> = {
bg: 'statusCriticalBg',
color: 'statusCritical',
},
warning: {
bg: 'statusWarningBg',
color: 'statusWarning',
},
}

export function ErrorMessage({ children, condition, type }: Props) {
Expand Down
11 changes: 9 additions & 2 deletions centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,18 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
validateOnMount: true,
})

React.useEffect(() => {
financeForm.validateForm()
}, [source])

Check warning on line 107 in centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx

View workflow job for this annotation

GitHub Actions / build-app

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

Check warning on line 107 in centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

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

const financeFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(financeForm, financeFormRef)

const totalFinance = Dec(financeForm.values.price || 0).mul(Dec(financeForm.values.quantity || 0))
const maxAvailable =
source === 'reserve' ? pool.reserve.available.toDecimal() : sourceLoan.outstandingDebt.toDecimal()

const withdraw = useWithdraw(loan.poolId, account!, totalFinance)
const withdraw = useWithdraw(loan.poolId, account!, totalFinance, source)

if (loan.status === 'Closed' || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'oracle')) {
return null
Expand Down Expand Up @@ -244,7 +248,10 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
type="submit"
loading={isFinanceLoading}
disabled={
!withdraw.isValid || !poolFees.isValid(financeForm) || !financeForm.isValid || maxAvailable.eq(0)
!withdraw.isValid(financeForm) ||
!poolFees.isValid(financeForm) ||
!financeForm.isValid ||
maxAvailable.eq(0)
}
>
Purchase
Expand Down
4 changes: 4 additions & 0 deletions centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
validateOnMount: true,
})

React.useEffect(() => {
repayForm.validateForm()
}, [destination])

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

View workflow job for this annotation

GitHub Actions / build-app

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

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

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

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

const repayFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(repayForm, repayFormRef)

Expand Down
171 changes: 95 additions & 76 deletions centrifuge-app/src/pages/Loan/FinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string
validateOnMount: true,
})

React.useEffect(() => {
financeForm.validateForm()
}, [source])

Check warning on line 166 in centrifuge-app/src/pages/Loan/FinanceForm.tsx

View workflow job for this annotation

GitHub Actions / build-app

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

Check warning on line 166 in centrifuge-app/src/pages/Loan/FinanceForm.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

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

const financeFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(financeForm, financeFormRef)

const withdraw = useWithdraw(loan.poolId, account!, Dec(financeForm.values.principal || 0))
const withdraw = useWithdraw(loan.poolId, account!, Dec(financeForm.values.principal || 0), source)

if (loan.status === 'Closed') {
return null
Expand Down Expand Up @@ -318,7 +322,7 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string
loading={isFinanceLoading}
disabled={
!financeForm.values.principal ||
!withdraw.isValid ||
!withdraw.isValid(financeForm) ||
!poolFees.isValid(financeForm) ||
!financeForm.isValid ||
maxAvailable.eq(0)
Expand All @@ -334,7 +338,7 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string
)
}

function WithdrawSelect({ withdrawAddresses }: { withdrawAddresses: WithdrawAddress[] }) {
function WithdrawSelect({ withdrawAddresses, poolId }: { withdrawAddresses: WithdrawAddress[]; poolId: string }) {
const form = useFormikContext<Pick<FinanceValues, 'withdraw'>>()
const utils = useCentrifugeUtils()
const getName = useGetNetworkName()
Expand All @@ -357,7 +361,15 @@ function WithdrawSelect({ withdrawAddresses }: { withdrawAddresses: WithdrawAddr
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [withdrawAddresses.length])

if (!withdrawAddresses.length) return null
if (!withdrawAddresses.length)
return (
<ErrorMessage type="warning" condition={!withdrawAddresses.length}>
<Stack gap={1}>
To purchase/finance this asset, the pool must set trusted withdrawal addresses to which funds will be sent.
<AnchorTextLink href={`#/issuer/${poolId}/access`}>Add trusted addresses</AnchorTextLink>
</Stack>
</ErrorMessage>
)

return (
<Select
Expand All @@ -377,9 +389,11 @@ function Mux({
withdrawAmounts,
selectedAddressIndexByCurrency,
setSelectedAddressIndex,
poolId,
}: {
amount: Decimal
total: Decimal
poolId: string
withdrawAmounts: WithdrawBucket[]
selectedAddressIndexByCurrency: Record<string, number>
setSelectedAddressIndex: (currency: string, index: number) => void
Expand All @@ -389,73 +403,79 @@ function Mux({
const getIcon = useGetNetworkIcon()
return (
<Stack gap={1}>
<Text variant="body2">Transactions per network</Text>
<Grid columns={3} rowGap={1}>
<GridRow borderBottomColor="borderPrimary" borderBottomWidth="1px" borderBottomStyle="solid" pb="4px">
<Text variant="label2">Amount</Text>
<Text variant="label2">Address</Text>
<Text variant="label2">Network</Text>
</GridRow>
{!withdrawAmounts.length && (
<Text variant="body3" color="statusCritical">
No suitable withdraw addresses
</Text>
)}
{withdrawAmounts.map(({ currency, amount, addresses, currencyKey }) => {
const index = selectedAddressIndexByCurrency[currencyKey] ?? 0
const address = addresses.at(index >>> 0) // undefined when index is -1
return (
<GridRow>
<Text variant="body3">{formatBalance(amount, currency.symbol)}</Text>
<Text variant="body3">
<Flex pr={1}>
<SelectInner
options={[
{ label: 'Ignore', value: '-1' },
...addresses.map((addr, index) => ({
label: truncateAddress(utils.formatAddress(addr.address)),
value: index.toString(),
})),
]}
value={index.toString()}
onChange={(event) => setSelectedAddressIndex(currencyKey, parseInt(event.target.value))}
small
/>
</Flex>
</Text>
<Text variant="body3">
{address && (
<Shelf gap="4px">
<Box
as="img"
src={
typeof address.location !== 'string' && 'parachain' in address.location
? parachainIcons[address.location.parachain]
: getIcon(typeof address.location === 'string' ? address.location : address.location.evm)
}
alt=""
width="iconSmall"
height="iconSmall"
style={{ objectFit: 'contain' }}
bleedY="4px"
/>
{typeof address.location === 'string'
? getName(address.location as any)
: 'parachain' in address.location
? parachainNames[address.location.parachain]
: getName(address.location.evm)}
</Shelf>
)}
</Text>
{!withdrawAmounts.length ? (
<ErrorMessage type="warning" condition={!withdrawAmounts.length}>
<Stack gap={1}>
To purchase/finance this asset, the pool must set trusted withdrawal addresses to which funds will be sent.
<AnchorTextLink href={`#/issuer/${poolId}/access`}>Add trusted addresses</AnchorTextLink>
</Stack>
</ErrorMessage>
) : (
<>
<Text variant="body2">Transactions per network</Text>
<Grid columns={3} rowGap={1}>
<GridRow borderBottomColor="borderPrimary" borderBottomWidth="1px" borderBottomStyle="solid" pb="4px">
<Text variant="label2">Amount</Text>
<Text variant="label2">Address</Text>
<Text variant="label2">Network</Text>
</GridRow>
)
})}
</Grid>
{withdrawAmounts.map(({ currency, amount, addresses, currencyKey }) => {
const index = selectedAddressIndexByCurrency[currencyKey] ?? 0
const address = addresses.at(index >>> 0) // undefined when index is -1
return (
<GridRow>
<Text variant="body3">{formatBalance(amount, currency.symbol)}</Text>
<Text variant="body3">
<Flex pr={1}>
<SelectInner
options={[
{ label: 'Ignore', value: '-1' },
...addresses.map((addr, index) => ({
label: truncateAddress(utils.formatAddress(addr.address)),
value: index.toString(),
})),
]}
value={index.toString()}
onChange={(event) => setSelectedAddressIndex(currencyKey, parseInt(event.target.value))}
small
/>
</Flex>
</Text>
<Text variant="body3">
{address && (
<Shelf gap="4px">
<Box
as="img"
src={
typeof address.location !== 'string' && 'parachain' in address.location
? parachainIcons[address.location.parachain]
: getIcon(typeof address.location === 'string' ? address.location : address.location.evm)
}
alt=""
width="iconSmall"
height="iconSmall"
style={{ objectFit: 'contain' }}
bleedY="4px"
/>
{typeof address.location === 'string'
? getName(address.location as any)
: 'parachain' in address.location
? parachainNames[address.location.parachain]
: getName(address.location.evm)}
</Shelf>
)}
</Text>
</GridRow>
)
})}
</Grid>
</>
)}
</Stack>
)
}

export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount, amount: Decimal) {
export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount, amount: Decimal, source: string) {
const pool = usePool(poolId)
const isLocalAsset = typeof pool.currency.key !== 'string' && 'LocalAsset' in pool.currency.key
const access = usePoolAccess(poolId)
Expand All @@ -467,16 +487,12 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
const ao = access.assetOriginators.find((a) => a.address === borrower.actingAddress)
const withdrawAddresses = ao?.transferAllowlist ?? []

if (!isLocalAsset || !withdrawAddresses.length) {
if (!withdrawAddresses.length)
return {
render: () => null,
isValid: true,
getBatch: () => of([]),
}
if (!isLocalAsset) {
return {
render: () => <WithdrawSelect withdrawAddresses={withdrawAddresses} />,
isValid: true,
render: () => <WithdrawSelect withdrawAddresses={withdrawAddresses} poolId={poolId} />,
isValid: ({ values }: { values: Pick<FinanceValues, 'withdraw'> }) => {
return source === 'reserve' ? !!values.withdraw : true
},
getBatch: ({ values }: { values: Pick<FinanceValues, 'withdraw'> }) => {
if (!values.withdraw) return of([])
return cent.pools
Expand Down Expand Up @@ -511,6 +527,7 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
return {
render: () => (
<Mux
poolId={poolId}
withdrawAmounts={withdrawAmounts}
selectedAddressIndexByCurrency={selectedAddressIndexByCurrency}
setSelectedAddressIndex={(currencyKey, index) => {
Expand All @@ -523,7 +540,9 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
amount={amount}
/>
),
isValid: amount.lte(totalAvailable),
isValid: ({ values }: { values: Pick<FinanceValues, 'withdraw'> }) => {
return source === 'reserve' ? amount.lte(totalAvailable) && !!values.withdraw : true
},
getBatch: () => {
return combineLatest(
withdrawAmounts.flatMap((bucket) => {
Expand Down
4 changes: 4 additions & 0 deletions centrifuge-app/src/pages/Loan/RepayForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLo
validateOnMount: true,
})

React.useEffect(() => {
repayForm.validateForm()
}, [destination])

Check warning on line 184 in centrifuge-app/src/pages/Loan/RepayForm.tsx

View workflow job for this annotation

GitHub Actions / build-app

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

Check warning on line 184 in centrifuge-app/src/pages/Loan/RepayForm.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

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

const repayFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(repayForm, repayFormRef)

Expand Down
Loading

0 comments on commit 3912629

Please sign in to comment.