Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finance form: Withdraw address select #2319

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading