-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Asset finance repay redesign (#2282)
* Move transfer debt form so it's showed after the user chooses a source * Add SourceSelect to repay forms * Fix decimals * Add fee charging to finance forms (WIP) * Add remark txs * Update Repay forms and add fees to repay * Fix usabilty of fee select * Fix tx execution * Update charge fee UI * Limit fee charging to borrower (fee must be added with borrower as destination) * Add comment about fees * Split up finance and repay drawers & UI improvements * Move available financing to loan page * Add total amount and rename amount to principal in forms * Deploy PR preview to demo * Purchase/Finance vs Sell/Repay and add totals * Pass fee tx to remark tx to be executed there * Implement transfer debt directly in finance form * Allow multiple fees and check validity of inputs * Disable buttons while form is incomplete * Better form validation for external assets * Finish adding transfer debt to all finance/repay forms * Fix mac price variants from failing on non oracle assets * Fix inconsistencies * Transfer debt source/desitnation only show cash assets * UI changes proposed by Jeroen * Unify finance/repay and transfer debt forms * Fix default behavior of add fee * Add text blurbs * Add charge fee summary * Remove padding on add fees button * Handle input/erros in repay form * Update info texts in all forms * Add error handling and better form control * Fix bug in available external finance form * Updates to inline feedback component * Fix fee tx submission * Validate upper limits in repay forms and fix required vs non required fields * Rename * Fix bugs in internal finance form * Check charged fees against max charageable * Remove indiv error messages in favor of general ones on repay form * Improve error handling in repay form * Fix wording * Fix warning * Remove repay all and allow repayments of just interest * Fix type error * Rename external repay form to sell and use source loan max interest * Rename external finance form to purchase * Prep for cash finance/repayments * Add support for cash assets * Add cash asset suuport to repay * Clean up and fix form submissions * Fix template rendering * Fix decimal point error and TransferDebtAmountMismatched error * Allow fees to be charged by non AO proxy destinations * Cash: rename principal to amount and include withdrawal addresses * Fix fee submission * Revert charging without AO and remove uncharge fees * Attempt to fix proxy call on repay * Fix tx pushed mistake * Wrap proxy calls and move remarks to module * Remove fee percentage from dropdown * Fix showing finance form * Always show repay forms so that money can be transfer even without outstanding debt * Fix bug where initial input is missing in fee category * Charge fee difference instead of uncharging/recharging * Fix available in repay forms * Remove close all transaction * Use USD for all virtual accounting processes * Fix null in extension period * Consitently use two commas in error messages * Price and quantity updates (max, secondary labels), show buttons appropriatly * Add custom padding to drawer * Remove maturity date in loan list * Remove decimals from quantity * Reorder category options in repay * Remove financing date for cash assets * Remove low wallet balance warning * Add interest rate in pricing values * Add tooltip for additional amount input * Better error messages * Improve error messages and remove additional amount from max calcs * Fix repay forms and add errors for balance checking * Asset redesign fixes (#2353) * 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 * Add changes for Jay and add disabled input for principal on external repay * Add tooltips, improve spacing in summary and attempt to charge margin on interest * Correct interest margin calc * Use textencoder instead of Buffer, fix spacing in finance form summary, fix lint warnings * Fix margin buffer and when to use it * convert 5 minute buffer to seconds * Add reserve info box to finance forms * Fix layout of external repay * Add component for ErrorMessage * Add principal calc to external finance form * Add missing gap --------- Co-authored-by: Jeroen Offerijns <[email protected]> Co-authored-by: Jeroen <[email protected]>
- Loading branch information
1 parent
ae9e26d
commit 8c38ff8
Showing
25 changed files
with
1,568 additions
and
836 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,21 @@ | ||
REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com | ||
REACT_APP_DEFAULT_UNLIST_POOLS=false | ||
REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev | ||
REACT_APP_COLLATOR_WSS_URL=wss://fullnode-apps.demo.k-f.dev | ||
REACT_APP_DEFAULT_UNLIST_POOLS=true | ||
REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-demo | ||
REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ | ||
REACT_APP_IS_DEMO=false | ||
REACT_APP_NETWORK=centrifuge | ||
REACT_APP_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev | ||
REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev | ||
REACT_APP_IS_DEMO=true | ||
REACT_APP_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-demo | ||
REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-demo | ||
REACT_APP_POOL_CREATION_TYPE=immediate | ||
REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com | ||
REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development | ||
REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io | ||
REACT_APP_RELAY_WSS_URL=wss://frag-moonbase-relay-rpc-ws.g.moonbase.moonbeam.network | ||
REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-demo-multichain | ||
REACT_APP_SUBSCAN_URL= | ||
REACT_APP_TINLAKE_NETWORK=goerli | ||
REACT_APP_INFURA_KEY=8cd8e043ee8d4001b97a1c37e08fd9dd | ||
REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 | ||
REACT_APP_WHITELISTED_ACCOUNTS= | ||
REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn | ||
REACT_APP_NETWORK=centrifuge | ||
REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json | ||
REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALwmJutBq95s41U9fWnoApCUgvPqPGTh1GSmFnQh5f9fWo93 | ||
REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a | ||
REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz | ||
REACT_APP_TREASURY=kAJkmGxAd6iqX9JjWTdhXgCf2PL1TAphTRYrmEqzBrYhwbXAn | ||
REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn | ||
REACT_APP_TREASURY=kAJkmGxAd6iqX9JjWTdhXgCf2PL1TAphTRYrmEqzBrYhwbXAn |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import { CurrencyBalance, Pool, addressToHex } from '@centrifuge/centrifuge-js' | ||
import { | ||
CombinedSubstrateAccount, | ||
formatBalance, | ||
useCentrifuge, | ||
useCentrifugeApi, | ||
wrapProxyCallsForAccount, | ||
} from '@centrifuge/centrifuge-react' | ||
import { Box, CurrencyInput, IconMinusCircle, IconPlusCircle, Select, Shelf, Stack, Text } from '@centrifuge/fabric' | ||
import { Field, FieldArray, FieldProps, useFormikContext } from 'formik' | ||
import React from 'react' | ||
import { combineLatest, map, of } from 'rxjs' | ||
import { Dec } from '../../utils/Decimal' | ||
import { useBorrower } from '../../utils/usePermissions' | ||
import { usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools' | ||
import { FinanceValues } from './ExternalFinanceForm' | ||
import { RepayValues } from './RepayForm' | ||
|
||
export const ChargeFeesFields = ({ | ||
pool, | ||
borrower, | ||
}: { | ||
pool: Pool | ||
borrower: CombinedSubstrateAccount | undefined | ||
}) => { | ||
const form = useFormikContext<FinanceValues>() | ||
const { data: poolMetadata } = usePoolMetadata(pool) | ||
const poolFees = usePoolFees(pool.id) | ||
// fees can only be charged by the destination address | ||
// fees destination must be set to the AO Proxy address | ||
const chargableFees = React.useMemo( | ||
() => | ||
poolFees?.filter( | ||
(fee) => fee.type !== 'fixed' && borrower && addressToHex(fee.destination) === borrower.actingAddress | ||
), | ||
[poolFees, borrower] | ||
) | ||
|
||
const getOptions = React.useCallback(() => { | ||
const chargableOptions = (chargableFees || []).map((f) => { | ||
const feeName = poolMetadata?.pool?.poolFees?.find((feeMeta) => feeMeta.id === f.id)?.name || 'Unknown Fee' | ||
return { | ||
label: `${feeName}`, | ||
value: f.id.toString(), | ||
} | ||
}) | ||
return chargableFees && chargableFees.length > 1 | ||
? [{ label: 'Select fee', value: '' }, ...chargableOptions] | ||
: chargableOptions | ||
}, [chargableFees, poolMetadata]) | ||
|
||
return ( | ||
<Stack gap={2}> | ||
<FieldArray name="fees"> | ||
{({ remove, push }) => { | ||
return ( | ||
<> | ||
<Stack gap={2}> | ||
<Stack gap={2}> | ||
{form.values.fees.map((fee, index) => { | ||
return ( | ||
<Shelf key={`${fee.id}-${index}`} gap={1} alignItems="flex-start"> | ||
<Box flex={1}> | ||
<Select | ||
options={getOptions()} | ||
label="Fee" | ||
onChange={(e) => { | ||
form.setFieldValue(`fees.${index}.id`, e.target.value) | ||
}} | ||
value={form.values.fees[index].id} | ||
/> | ||
</Box> | ||
<Box flex={1}> | ||
<Field | ||
key={`fees.${index}.amount`} | ||
name={`fees.${index}.amount`} | ||
validate={(value: number) => { | ||
let error | ||
if (!value) { | ||
error = 'Enter an amount or remove the fee' | ||
} | ||
return error | ||
}} | ||
> | ||
{({ field, meta }: FieldProps) => { | ||
return ( | ||
<CurrencyInput | ||
{...field} | ||
label="Amount" | ||
errorMessage={meta.touched ? meta.error : undefined} | ||
currency={pool.currency.symbol} | ||
placeholder="0" | ||
onChange={(value) => form.setFieldValue(`fees.${index}.amount`, value)} | ||
/> | ||
) | ||
}} | ||
</Field> | ||
</Box> | ||
<Box | ||
alignSelf="flex-start" | ||
background="none" | ||
border="none" | ||
as="button" | ||
mt={4} | ||
style={{ cursor: 'pointer' }} | ||
onClick={() => remove(index)} | ||
> | ||
<IconMinusCircle size="20px" /> | ||
</Box> | ||
</Shelf> | ||
) | ||
})} | ||
</Stack> | ||
{chargableFees?.length ? ( | ||
<Shelf | ||
gap={1} | ||
alignItems="center" | ||
as="button" | ||
style={{ cursor: 'pointer', background: 'none', border: 'none' }} | ||
onClick={(e) => { | ||
e.preventDefault() | ||
if (chargableFees.length === 1) { | ||
return push({ id: chargableFees[0].id.toString(), amount: '' }) | ||
} | ||
return push({ id: '', amount: '' }) | ||
}} | ||
> | ||
<IconPlusCircle size="20px" color="textButtonTertiary" /> | ||
<Text variant="label1" color="textButtonTertiary"> | ||
Add fee | ||
</Text> | ||
</Shelf> | ||
) : null} | ||
</Stack> | ||
</> | ||
) | ||
}} | ||
</FieldArray> | ||
</Stack> | ||
) | ||
} | ||
|
||
function ChargePoolFeeSummary({ poolId }: { poolId: string }) { | ||
const form = useFormikContext<FinanceValues | RepayValues>() | ||
const pool = usePool(poolId) | ||
const totalFees = form.values.fees.reduce((acc, fee) => acc.add(Dec(fee.amount || 0)), Dec(0)) | ||
|
||
return form.values.fees.length > 0 ? ( | ||
<Stack gap={1}> | ||
<Shelf justifyContent="space-between"> | ||
<Text variant="label2">Fees</Text> | ||
<Text variant="label2">{formatBalance(Dec(totalFees), pool.currency.symbol, 2)}</Text> | ||
</Shelf> | ||
</Stack> | ||
) : null | ||
} | ||
|
||
export function useChargePoolFees(poolId: string, loanId: string) { | ||
const pool = usePool(poolId) | ||
const borrower = useBorrower(poolId, loanId) | ||
const api = useCentrifugeApi() | ||
const cent = useCentrifuge() | ||
return { | ||
render: () => <ChargeFeesFields pool={pool as Pool} borrower={borrower} />, | ||
renderSummary: () => <ChargePoolFeeSummary poolId={poolId} />, | ||
isValid: ({ values }: { values: Pick<FinanceValues | RepayValues, 'fees'> }) => { | ||
return values.fees.every((fee) => !!fee.id && !!fee.amount) | ||
}, | ||
getBatch: ({ values }: { values: Pick<FinanceValues | RepayValues, 'fees'> }) => { | ||
if (!values.fees.length) return of([]) | ||
const fees = values.fees.flatMap((fee) => { | ||
if (!fee.amount) throw new Error('Charge amount not provided') | ||
if (!borrower) throw new Error('No borrower') | ||
const feeAmount = CurrencyBalance.fromFloat(fee.amount, pool.currency.decimals) | ||
let feeTx = api.tx.poolFees.chargeFee(fee.id, feeAmount.toString()) | ||
return cent.remark | ||
.remark([[{ Loan: [poolId, loanId] }], feeTx], { batch: true }) | ||
.pipe(map((tx) => wrapProxyCallsForAccount(api, tx, borrower, 'Borrow'))) | ||
}) | ||
return combineLatest(fees) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Box, InlineFeedback, Text } from '@centrifuge/fabric' | ||
|
||
type Props = { | ||
children: React.ReactNode | ||
type: 'default' | 'critical' | ||
condition: boolean | ||
} | ||
|
||
const styles: Record<Props['type'], { bg: string; color: string }> = { | ||
default: { | ||
bg: 'statusDefaultBg', | ||
color: 'statusDefault', | ||
}, | ||
critical: { | ||
bg: 'statusCriticalBg', | ||
color: 'statusCritical', | ||
}, | ||
} | ||
|
||
export function ErrorMessage({ children, condition, type }: Props) { | ||
return condition ? ( | ||
<Box bg={styles[type].bg} p={1}> | ||
<InlineFeedback status={type}> | ||
<Text color={styles[type].color}>{children}</Text> | ||
</InlineFeedback> | ||
</Box> | ||
) : null | ||
} |
Oops, something went wrong.