Skip to content

Commit

Permalink
Finance form: Withdraw address select (#2319)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser committed Aug 2, 2024
1 parent 801d94e commit 8cefe43
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 64 deletions.
6 changes: 4 additions & 2 deletions centrifuge-app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,13 @@ export const ethConfig = {

export const config = import.meta.env.REACT_APP_NETWORK === 'altair' ? ALTAIR : CENTRIFUGE

const assetHubChainId = import.meta.env.REACT_APP_IS_DEMO ? 1001 : 1000

export const parachainNames: Record<number, string> = {
1000: 'Asset Hub',
[assetHubChainId]: 'Asset Hub',
}
export const parachainIcons: Record<number, string> = {
1000: assetHubLogo,
[assetHubChainId]: assetHubLogo,
}

const infuraKey = import.meta.env.REACT_APP_INFURA_KEY
Expand Down
159 changes: 109 additions & 50 deletions centrifuge-app/src/pages/Loan/FinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import {
Button,
Card,
CurrencyInput,
Flex,
Grid,
GridRow,
InlineFeedback,
Select,
SelectInner,
Shelf,
Stack,
Text,
Expand All @@ -51,7 +53,7 @@ import { isExternalLoan } from './utils'

const TOKENMUX_PALLET_ACCOUNTID = '0x6d6f646c6366672f746d75780000000000000000000000000000000000000000'

type Key = `${'parachain' | 'evm'}:${number}`
type Key = `${'parachain' | 'evm'}:${number}` | 'centrifuge'
type FinanceValues = {
amount: number | '' | Decimal
withdraw: undefined | WithdrawAddress
Expand Down Expand Up @@ -226,18 +228,19 @@ function WithdrawSelect({ withdrawAddresses }: { withdrawAddresses: WithdrawAddr
}

function Mux({
withdrawAddressesByDomain,
withdrawAmounts,
selectedAddressIndexByCurrency,
setSelectedAddressIndex,
}: {
amount: Decimal
total: Decimal
withdrawAddressesByDomain: Record<string, WithdrawAddress[]>
withdrawAmounts: WithdrawBucket[]
selectedAddressIndexByCurrency: Record<string, number>
setSelectedAddressIndex: (currency: string, index: number) => void
}) {
const utils = useCentrifugeUtils()
const getName = useGetNetworkName()
const getIcon = useGetNetworkIcon()

return (
<Stack gap={1}>
<Text variant="body2">Transactions per network</Text>
Expand All @@ -252,32 +255,51 @@ function Mux({
No suitable withdraw addresses
</Text>
)}
{withdrawAmounts.map(({ currency, amount, locationKey }) => {
const address = withdrawAddressesByDomain[locationKey][0]
{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">{truncateAddress(utils.formatAddress(address.address))}</Text>
<Text variant="body3">
<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' }}
<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
/>
{typeof address.location === 'string'
? getName(address.location as any)
: 'parachain' in address.location
? parachainNames[address.location.parachain]
: getName(address.location.evm)}
</Shelf>
</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>
)
Expand All @@ -294,6 +316,7 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
const muxBalances = useBalances(TOKENMUX_PALLET_ACCOUNTID)
const cent: Centrifuge = useCentrifuge()
const api = useCentrifugeApi()
const [selectedAddressIndexByCurrency, setSelectedAddressIndexByCurrency] = React.useState<Record<string, number>>({})

const ao = access.assetOriginators.find((a) => a.address === borrower.actingAddress)
const withdrawAddresses = ao?.transferAllowlist ?? []
Expand Down Expand Up @@ -330,25 +353,26 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
}

const sortedBalances = sortBalances(muxBalances?.currencies || [], pool.currency)
const withdrawAmounts = muxBalances?.currencies
? divideBetweenCurrencies(amount, sortedBalances, withdrawAddresses)
: []
const totalAvailable = withdrawAmounts.reduce((acc, cur) => acc.add(cur.amount), Dec(0))
const withdrawAddressesByDomain: Record<string, WithdrawAddress[]> = {}
withdrawAddresses.forEach((addr) => {
const key = locationToKey(addr.location)
if (withdrawAddressesByDomain[key]) {
withdrawAddressesByDomain[key].push(addr)
} else {
withdrawAddressesByDomain[key] = [addr]
}
const ignoredCurrencies = Object.entries(selectedAddressIndexByCurrency).flatMap(([key, index]) => {
return index === -1 ? [key] : []
})
const { buckets: withdrawAmounts } = muxBalances?.currencies
? divideBetweenCurrencies(amount, sortedBalances, withdrawAddresses, ignoredCurrencies)
: { buckets: [] }

const totalAvailable = withdrawAmounts.reduce((acc, cur) => acc.add(cur.amount), Dec(0))

return {
render: () => (
<Mux
withdrawAddressesByDomain={withdrawAddressesByDomain}
withdrawAmounts={withdrawAmounts}
selectedAddressIndexByCurrency={selectedAddressIndexByCurrency}
setSelectedAddressIndex={(currencyKey, index) => {
setSelectedAddressIndexByCurrency((prev) => ({
...prev,
[currencyToString(currencyKey)]: index,
}))
}}
total={totalAvailable}
amount={amount}
/>
Expand All @@ -357,8 +381,8 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
getBatch: () => {
return combineLatest(
withdrawAmounts.flatMap((bucket) => {
// TODO: Select specific withdraw address for a domain if there's multiple
const withdraw = withdrawAddressesByDomain[bucket.locationKey][0]
const index = selectedAddressIndexByCurrency[bucket.currencyKey] ?? 0
const withdraw = bucket.addresses[index]
if (bucket.amount.isZero()) return []
return [
of(
Expand Down Expand Up @@ -394,6 +418,8 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
const order: Record<string, number> = {
'evm:8453': 5,
'evm:84531': 5,
'parachain:1000': 4,
'parachain:1001': 4,
'parachain:2000': 4,
'evm:42220': 3,
'evm:44787': 3,
Expand All @@ -418,37 +444,70 @@ function sortBalances(balances: AccountCurrencyBalance[], localPoolCurrency: Cur
}

function locationToKey(location: WithdrawAddress['location']) {
return Object.entries(location)[0].join(':') as Key
return typeof location === 'string' ? location : (Object.entries(location)[0].join(':') as Key)
}

type WithdrawBucket = { currency: CurrencyMetadata; amount: Decimal; locationKey: Key }
function currencyToString(currencyKey: CurrencyMetadata['key']) {
return JSON.stringify(currencyKey).replace(/"/g, '')
}

type WithdrawBucket = {
currency: CurrencyMetadata
amount: Decimal
locationKey: Key
currencyKey: string
addresses: WithdrawAddress[]
}
function divideBetweenCurrencies(
amount: Decimal,
balances: AccountCurrencyBalance[],
withdrawAddresses: WithdrawAddress[],
ignoredCurrencies: string[],
result: WithdrawBucket[] = []
) {
const [next, ...rest] = balances

if (!next) return result
if (!next) {
return {
buckets: result,
remainder: amount,
}
}

const hasAddress = !!withdrawAddresses.find(
(addr) => locationToKey(addr.location) === locationToKey(getCurrencyLocation(next.currency))
const addresses = withdrawAddresses.filter((addr) =>
[locationToKey(getCurrencyLocation(next.currency)), 'centrifuge'].includes(locationToKey(addr.location))
)
const key = locationToKey(getCurrencyLocation(next.currency))

let combinedResult = [...result]
let remainder = amount
if (hasAddress) {
if (addresses.length) {
const balanceDec = next.balance.toDecimal()
if (remainder.lte(balanceDec)) {
combinedResult.push({ amount: remainder, currency: next.currency, locationKey: key })
let obj = {
currency: next.currency,
locationKey: key,
addresses,
currencyKey: currencyToString(next.currency.key),
}
if (ignoredCurrencies.includes(obj.currencyKey)) {
combinedResult.push({
amount: Dec(0),
...obj,
})
} else if (remainder.lte(balanceDec)) {
combinedResult.push({
amount: remainder,
...obj,
})
remainder = Dec(0)
} else {
remainder = remainder.sub(balanceDec)
combinedResult.push({ amount: balanceDec, currency: next.currency, locationKey: key })
combinedResult.push({
amount: balanceDec,
...obj,
})
}
}

return divideBetweenCurrencies(remainder, rest, withdrawAddresses, combinedResult)
return divideBetweenCurrencies(remainder, rest, withdrawAddresses, ignoredCurrencies, combinedResult)
}
33 changes: 21 additions & 12 deletions fabric/src/components/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
label?: string | React.ReactElement
placeholder?: string
errorMessage?: string
small?: boolean
}

const StyledSelect = styled.select`
Expand All @@ -40,20 +41,28 @@ const StyledSelect = styled.select`
}
`

const Chevron = styled(IconChevronDown)`
position: absolute;
top: 0;
right: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
pointer-events: none;
`

export function SelectInner({ options, placeholder, disabled, ...rest }: Omit<SelectProps, 'label' | 'errorMessage'>) {
export function SelectInner({
options,
placeholder,
disabled,
small,
...rest
}: Omit<SelectProps, 'label' | 'errorMessage'>) {
return (
<Flex position="relative" width="100%">
<Chevron color={disabled ? 'textSecondary' : 'textPrimary'} />
<IconChevronDown
color={disabled ? 'textSecondary' : 'textPrimary'}
size={small ? 'iconSmall' : 'iconMedium'}
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
marginTop: 'auto',
marginBottom: 'auto',
pointerEvents: 'none',
}}
/>
<StyledSelect disabled={disabled} {...rest}>
{placeholder && (
<option value="" disabled>
Expand Down

0 comments on commit 8cefe43

Please sign in to comment.