Skip to content

Commit

Permalink
Finance Loan: Token muxing (#2038)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored Mar 26, 2024
1 parent cbdf011 commit dddc9d4
Show file tree
Hide file tree
Showing 18 changed files with 520 additions and 210 deletions.
56 changes: 38 additions & 18 deletions centrifuge-app/src/components/Swaps/Orders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { ButtonGroup } from '../ButtonGroup'
import { Column, DataTable } from '../DataTable'
import { PageSection } from '../PageSection'

const TOKENMUX_PALLET_ACCOUNTID = '0x6d6f646c6366672f746d75780000000000000000000000000000000000000000'

export type OrdersProps = {
buyOrSell?: CurrencyKey
}
Expand All @@ -70,7 +72,6 @@ export type SwapOrder = {

export function Orders({ buyOrSell }: OrdersProps) {
const cent = useCentrifuge()
const api = useCentrifugeApi()
const currencies = useCurrencies()
const utils = useCentrifugeUtils()
const [selectedOrder, setSelectedOrder] = React.useState<SwapOrder>()
Expand Down Expand Up @@ -137,7 +138,8 @@ export function Orders({ buyOrSell }: OrdersProps) {
)
})
)
return api.query.orderBook.orders.entries().pipe(
return cent.getApi().pipe(
switchMap((api) => api.query.orderBook.orders.entries()),
map((rawOrders) => {
return rawOrders.map(([, value]) => {
const order = value.toPrimitive() as {
Expand Down Expand Up @@ -207,7 +209,19 @@ export function Orders({ buyOrSell }: OrdersProps) {
export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onClose: () => void; order: SwapOrder }) {
const [account] = useSuitableAccounts({})
const utils = useCentrifugeUtils()
const balances = useBalances(account?.actingAddress)

const isMaybeMuxDeposit = typeof order.buyCurrency.key !== 'string' && 'LocalAsset' in order.buyCurrency.key
const isMaybeMuxBurn = typeof order.sellCurrency.key !== 'string' && 'LocalAsset' in order.sellCurrency.key
const isMaybeMuxSwap = isMaybeMuxBurn || isMaybeMuxDeposit
const localCurrency = isMaybeMuxDeposit ? order.buyCurrency : order.sellCurrency
const otherCurrency = isMaybeMuxDeposit ? order.sellCurrency : order.buyCurrency
const isMuxSwap =
isMaybeMuxSwap &&
String((localCurrency.key as any).LocalAsset) === String(otherCurrency.additional?.localRepresentation)
const isMuxBurn = isMuxSwap && isMaybeMuxBurn
const isMuxDeposit = isMuxSwap && isMaybeMuxDeposit

const balances = useBalances(isMuxBurn ? TOKENMUX_PALLET_ACCOUNTID : account?.actingAddress)
const api = useCentrifugeApi()
const consts = useCentrifugeConsts()
const getNetworkName = useGetNetworkName()
Expand All @@ -232,9 +246,13 @@ export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onC
'Fulfill order',
(cent) => (args: [transferTo: string | null, amount: CurrencyBalance | null], options) => {
const [transferTo, amount] = args
let swapTx = api.tx.orderBook.fillOrder(order.id, order.sellAmount.toString())
let fn = api.tx.orderBook.fillOrder
if (isMuxSwap) {
fn = api.tx.tokenMux.matchSwap
}
let swapTx = fn(order.id, order.sellAmount.toString())
if (amount) {
swapTx = api.tx.orderBook.fillOrder(order.id, amount.toString())
swapTx = fn(order.id, amount.toString())
}

if (transferTo) {
Expand Down Expand Up @@ -298,7 +316,7 @@ export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onC

if (!account) return null

const balanceLow = balanceDec.lt(orderBuyDec)
const balanceLow = !isMuxDeposit && balanceDec.lt(orderBuyDec)
const { isTransferEnabled, isPartialEnabled } = form.values
const disabled = isPartialEnabled ? false : balanceLow

Expand All @@ -311,22 +329,24 @@ export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onC
<Shelf alignItems="center" alignSelf="center" gap={4} flexWrap="nowrap" mb={2}>
<Text variant="heading3" style={{ position: 'relative' }}>
<Text fontSize={24}>{formatBalance(order.buyAmount)}</Text> {order.buyCurrency.symbol}
<Box position="absolute" top="100%" left={0}>
<Text
variant="label2"
color={balanceLow ? 'statusCritical' : undefined}
style={{ whiteSpace: 'nowrap' }}
>
{formatBalance(balanceDec, order.buyCurrency.symbol)} available
</Text>
</Box>
{!isMuxDeposit && (
<Box position="absolute" top="100%" left={0}>
<Text
variant="label2"
color={balanceLow ? 'statusCritical' : undefined}
style={{ whiteSpace: 'nowrap' }}
>
{formatBalance(balanceDec, order.buyCurrency.symbol)} available
</Text>
</Box>
)}
</Text>
<IconArrowRight />
<Text variant="heading3">
<Text fontSize={24}>{formatBalance(order.sellAmount)}</Text> {order.sellCurrency.symbol}
</Text>
</Shelf>
{balanceLow && (
{balanceLow && !isMuxSwap && (
<TextInput
label={
orderBuyCurrencyLocation
Expand All @@ -349,7 +369,7 @@ export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onC
)}
</Stack>

{orderBuyDec.gt(minFulfillDec) && (
{orderBuyDec.gt(minFulfillDec) && !isMuxDeposit && (
<Card p={2}>
<Stack gap={2}>
<Field type="checkbox" name="isPartialEnabled" as={Checkbox} label="Fulfill order partially" />
Expand Down Expand Up @@ -387,7 +407,7 @@ export function SwapAndSendDialog({ open, onClose, order }: { open: boolean; onC
</Card>
)}

{orderSellCurrencyLocation && (
{orderSellCurrencyLocation && !isMuxSwap && (
<Card p={2}>
<Stack gap={2}>
<Field
Expand Down
4 changes: 4 additions & 0 deletions centrifuge-app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TransactionOptions } from '@centrifuge/centrifuge-js'
import { EvmChains } from '@centrifuge/centrifuge-react'
import { altairDark, centrifugeLight } from '@centrifuge/fabric'
import arbitrumLogo from '@centrifuge/fabric/assets/logos/arbitrum.svg'
import assetHubLogo from '@centrifuge/fabric/assets/logos/assethub.svg'
import baseLogo from '@centrifuge/fabric/assets/logos/base.svg'
import celoLogo from '@centrifuge/fabric/assets/logos/celo.svg'
import ethereumLogo from '@centrifuge/fabric/assets/logos/ethereum.svg'
Expand Down Expand Up @@ -159,6 +160,9 @@ export const config = import.meta.env.REACT_APP_NETWORK === 'altair' ? ALTAIR :
export const parachainNames: Record<number, string> = {
1000: 'Asset Hub',
}
export const parachainIcons: Record<number, string> = {
1000: assetHubLogo,
}

const infuraKey = import.meta.env.REACT_APP_INFURA_KEY

Expand Down
2 changes: 1 addition & 1 deletion centrifuge-app/src/pages/IssuerCreatePool/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ function CreatePoolForm() {
].filter(Boolean)
)
setMultisigData({ callData: proxiedPoolCreate.method.toHex(), hash: proxiedPoolCreate.method.hash.toHex() })
return cent.wrapSignAndSend(api, submittable, { ...options, multisig: undefined, proxy: [] })
return cent.wrapSignAndSend(api, submittable, { ...options, multisig: undefined, proxies: undefined })
})
)
},
Expand Down
22 changes: 13 additions & 9 deletions centrifuge-app/src/pages/IssuerPool/Access/AssetOriginators.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
addressToHex,
computeTrancheId,
getCurrencyLocation,
isSameAddress,
PoolMetadata,
TransactionOptions,
Expand Down Expand Up @@ -38,6 +39,7 @@ import { FieldWithErrorMessage } from '../../../components/FieldWithErrorMessage
import { Identity } from '../../../components/Identity'
import { PageSection } from '../../../components/PageSection'
import { parachainNames } from '../../../config'
import { looksLike } from '../../../utils/helpers'
import { useIdentity } from '../../../utils/useIdentity'
import { useDomainRouters } from '../../../utils/useLiquidityPools'
import { getKeyForReceiver, usePoolAccess, useSuitableAccounts, WithdrawKey } from '../../../utils/usePermissions'
Expand Down Expand Up @@ -131,16 +133,22 @@ function AOForm({
const chainIds = routers?.map((r) => r.chainId) || []
const getName = useGetNetworkName()

const isLocalAsset = typeof pool.currency.key !== 'string' && 'LocalAsset' in pool.currency.key
const destinations = [
'centrifuge',
...chainIds
.map((cid) => ({ evm: cid }))
.filter(
() => pool.currency.additional?.transferability && 'liquidityPools' in pool.currency.additional.transferability
(location) =>
(pool.currency.additional?.transferability &&
'liquidityPools' in pool.currency.additional.transferability &&
looksLike(location, getCurrencyLocation(pool.currency))) ||
isLocalAsset
),
...Object.keys(parachainNames)
...(Object.keys(parachainNames)
.map((pid) => ({ parachain: Number(pid) }))
.filter(() => pool.currency.additional?.transferability && 'xcm' in pool.currency.additional.transferability),
.filter(() => pool.currency.additional?.transferability && 'xcm' in pool.currency.additional.transferability) ||
isLocalAsset),
]

const { showPodAccountCreation } = useDebugFlags()
Expand Down Expand Up @@ -267,12 +275,8 @@ function AOForm({
[keys.documentKey, 'P2PDocumentSigning', 'ECDSA'],
]),
collectionId && [api.tx.uniques.create(collectionId, ao.address)],
addedWithdrawAddresses.map((w) =>
api.tx.transferAllowList.addTransferAllowance(pool.currency.key, w)
),
removedWithdrawAddresses.map((w) =>
api.tx.transferAllowList.removeTransferAllowance(pool.currency.key, w)
),
addedWithdrawAddresses.map((w) => api.tx.transferAllowList.addTransferAllowance('All', w)),
removedWithdrawAddresses.map((w) => api.tx.transferAllowList.removeTransferAllowance('All', w)),
]
.filter(Boolean)
.flat(2)
Expand Down
55 changes: 31 additions & 24 deletions centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { CurrencyBalance, ExternalLoan, Pool, Price, WithdrawAddress } from '@centrifuge/centrifuge-js'
import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react'
import { Box, Button, Card, CurrencyInput, Shelf, Stack, Text } from '@centrifuge/fabric'
import Decimal from 'decimal.js-light'
import { Field, FieldProps, Form, FormikProvider, useFormik, useFormikContext } from 'formik'
import * as React from 'react'
import { combineLatest, switchMap } from 'rxjs'
import { Dec, min } from '../../utils/Decimal'
import { formatBalance } from '../../utils/formatting'
import { useFocusInvalidInput } from '../../utils/useFocusInvalidInput'
import { useAvailableFinancing } from '../../utils/useLoans'
import { useBorrower } from '../../utils/usePermissions'
import { usePool } from '../../utils/usePools'
import { combine, maxPriceVariance, nonNegativeNumber, required, settlementPrice } from '../../utils/validation'
import { WithdrawSelect } from './FinanceForm'
import { useWithdraw } from './FinanceForm'

type FinanceValues = {
price: number | '' | Decimal
Expand All @@ -22,11 +23,26 @@ type FinanceValues = {
export function ExternalFinanceForm({ loan }: { loan: ExternalLoan }) {
const pool = usePool(loan.poolId) as Pool
const account = useBorrower(loan.poolId, loan.id)
const api = useCentrifugeApi()
if (!account) throw new Error('No borrower')
const { current: availableFinancing } = useAvailableFinancing(loan.poolId, loan.id)
const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction(
'Finance asset',
(cent) => cent.pools.financeExternalLoan,
(cent) => (args: [poolId: string, loanId: string, quantity: Price, price: CurrencyBalance], options) => {
const [poolId, loanId, quantity, price] = args
return combineLatest([
cent.pools.financeExternalLoan([poolId, loanId, quantity, price], { batch: true }),
withdraw.getBatch(financeForm),
]).pipe(
switchMap(([loanTx, batch]) => {
let tx = wrapProxyCallsForAccount(api, loanTx, account, 'Borrow')
if (batch.length) {
tx = api.tx.utility.batchAll([tx, ...batch])
}
return cent.wrapSignAndSend(api, tx, { ...options, proxies: undefined })
})
)
},
{
onSuccess: () => {
financeForm.resetForm()
Expand All @@ -44,18 +60,9 @@ export function ExternalFinanceForm({ loan }: { loan: ExternalLoan }) {
const price = CurrencyBalance.fromFloat(values.price, pool.currency.decimals)
const quantity = Price.fromFloat(Dec(values.faceValue).div(loan.pricing.notional.toDecimal()))

doFinanceTransaction(
[
loan.poolId,
loan.id,
quantity,
price,
values.withdraw ? { ...values.withdraw, currency: pool.currency.key } : undefined,
],
{
account,
}
)
doFinanceTransaction([loan.poolId, loan.id, quantity, price], {
account,
})
actions.setSubmitting(false)
},
validateOnMount: true,
Expand All @@ -64,6 +71,12 @@ export function ExternalFinanceForm({ loan }: { loan: ExternalLoan }) {
const financeFormRef = React.useRef<HTMLFormElement>(null)
useFocusInvalidInput(financeForm, financeFormRef)

const amountDec = Dec(financeForm.values.price || 0)
.mul(Dec(financeForm.values.faceValue || 0))
.div(loan.pricing.notional.toDecimal())

const withdraw = useWithdraw(loan.poolId, account, amountDec)

if (loan.status === 'Closed' || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'oracle')) {
return null
}
Expand All @@ -80,25 +93,19 @@ export function ExternalFinanceForm({ loan }: { loan: ExternalLoan }) {
<FormikProvider value={financeForm}>
<Stack as={Form} gap={2} noValidate ref={financeFormRef}>
<ExternalFinanceFields loan={loan} pool={pool} />
<WithdrawSelect loan={loan} borrower={account} />
{withdraw.render()}
<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="emphasized">Total amount</Text>
<Text variant="emphasized">
{financeForm.values.price && !Number.isNaN(financeForm.values.price as number)
? formatBalance(
Dec(financeForm.values.price || 0)
.mul(Dec(financeForm.values.faceValue || 0))
.div(loan.pricing.notional.toDecimal()),
pool?.currency.symbol,
2
)
? formatBalance(amountDec, pool?.currency.symbol, 2)
: `0.00 ${pool.currency.symbol}`}
</Text>
</Shelf>
</Stack>
<Stack px={1}>
<Button type="submit" loading={isFinanceLoading}>
<Button type="submit" loading={isFinanceLoading} disabled={!withdraw.isValid}>
Finance asset
</Button>
</Stack>
Expand Down
Loading

0 comments on commit dddc9d4

Please sign in to comment.