Skip to content

Commit

Permalink
Merge branch 'main' into 2561_connect_wallet_feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
kattylucy authored Dec 19, 2024
2 parents c89eb6a + 254503b commit e089690
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 107 deletions.
2 changes: 1 addition & 1 deletion centrifuge-app/src/components/Charts/PriceYieldChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function PriceYieldChart({

if (!poolId) throw new Error('Pool not found')

const { trancheStates: tranches } = useDailyPoolStates(poolId, undefined, undefined, false) || {}
const { trancheStates: tranches } = useDailyPoolStates(poolId, undefined, undefined) || {}
const trancheStates = tranches?.[trancheId]
const pool = usePool(poolId)

Expand Down
14 changes: 5 additions & 9 deletions centrifuge-app/src/components/LoanList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath'
import { AssetSnapshot, CurrencyBalance, Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js'
import { CurrencyBalance, Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js'
import {
AnchorButton,
Box,
Expand Down Expand Up @@ -27,10 +27,9 @@ import { formatBalance, formatPercentage } from '../utils/formatting'
import { useFilters } from '../utils/useFilters'
import { useMetadata } from '../utils/useMetadata'
import { useCentNFT } from '../utils/useNFTs'
import { usePool, usePoolMetadata } from '../utils/usePools'
import { useAllPoolAssetSnapshots, usePool, usePoolMetadata } from '../utils/usePools'
import { Column, DataTable, SortableTableHeader } from './DataTable'
import { prefetchRoute } from './Root'
import { Spinner } from './Spinner'

type Row = (Loan | TinlakeLoan) & {
idSortKey: number
Expand All @@ -46,15 +45,14 @@ type Row = (Loan | TinlakeLoan) & {

type Props = {
loans: Loan[] | TinlakeLoan[]
snapshots: AssetSnapshot[]
isLoading: boolean
}

export function LoanList({ loans, snapshots, isLoading }: Props) {
export function LoanList({ loans }: Props) {
const { pid: poolId } = useParams<{ pid: string }>()
if (!poolId) throw new Error('Pool not found')

const navigate = useNavigate()
const { data: snapshots } = useAllPoolAssetSnapshots(poolId, new Date().toISOString().slice(0, 10))
const pool = usePool(poolId)
const isTinlakePool = poolId?.startsWith('0x')
const basePath = useBasePath()
Expand Down Expand Up @@ -267,8 +265,6 @@ export function LoanList({ loans, snapshots, isLoading }: Props) {
const filteredData = isLoading ? [] : showRepaid ? rows : rows.filter((row) => !row.marketValue?.isZero())
const pagination = usePagination({ data: filteredData, pageSize: 20 })

if (isLoading) return <Spinner />

return (
<>
<Box pt={1} pb={2} paddingX={1} display="flex" justifyContent="space-between" alignItems="center">
Expand All @@ -293,7 +289,7 @@ export function LoanList({ loans, snapshots, isLoading }: Props) {
View asset transactions
</Button>
<AnchorButton
href={csvUrl ?? undefined}
href={csvUrl ?? ''}
download={`pool-assets-${poolId}.csv`}
variant="inverted"
icon={IconDownload}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,13 @@ export function AddAddressInput({

const exists = !!truncated && existingAddresses.some((addr) => isSameAddress(addr, address))

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const newAddress = e.target.value
setAddress(newAddress)
}

function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
const address = e.target.value
if (truncated) {
onAdd(addressToHex(address))
setAddress('')
}
}

return (
<Grid columns={2} equalColumns gap={4} alignItems="center">
<AddressInput
clearIcon
placeholder="Search to add address..."
value={address}
onChange={handleChange}
onBlur={handleBlur}
onChange={(e) => setAddress(e.target.value)}
/>
{address &&
(truncated ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function PoolDomain({ poolId, domain, refetch }: { poolId: string; domain: Domai
</React.Fragment>
))}
{domain.trancheTokens[t.id] && (
<a href={explorer.address(domain.trancheTokens[t.id])} target="_blank" rel="noopener noreferrer">
<a href={explorer.address(domain.trancheTokens[t.id]!)} target="_blank" rel="noopener noreferrer">
<Button variant="secondary" small style={{ width: '100%' }}>
<Shelf gap={1}>
<span>View {t.currency.symbol} token</span>
Expand Down
6 changes: 3 additions & 3 deletions centrifuge-app/src/pages/Loan/FinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Field, FieldProps, Form, FormikProvider, useField, useFormik, useFormik
import * as React from 'react'
import { combineLatest, map, of, switchMap } from 'rxjs'
import { useTheme } from 'styled-components'
import { AnchorTextLink } from '../../components/TextLink'
import { AnchorTextLink, RouterTextLink } from '../../components/TextLink'
import { parachainIcons, parachainNames } from '../../config'
import { Dec, min } from '../../utils/Decimal'
import { formatBalance } from '../../utils/formatting'
Expand Down Expand Up @@ -409,7 +409,7 @@ function WithdrawSelect({ withdrawAddresses, poolId }: { withdrawAddresses: With
<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>
<RouterTextLink to={`/issuer/${poolId}/access`}>Add trusted addresses</RouterTextLink>
</Stack>
</ErrorMessage>
)
Expand Down Expand Up @@ -452,7 +452,7 @@ function Mux({
<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>
<RouterTextLink to={`/issuer/${poolId}/access`}>Add trusted addresses</RouterTextLink>
</Stack>
</ErrorMessage>
) : (
Expand Down
176 changes: 111 additions & 65 deletions centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ActiveLoan,
addressToHex,
CreatedLoan,
CurrencyBalance,
CurrencyKey,
Expand Down Expand Up @@ -29,9 +30,11 @@ import {
} from '@centrifuge/fabric'
import { stringToHex } from '@polkadot/util'
import { BN } from 'bn.js'
import { keccak256, SigningKey, toUtf8Bytes } from 'ethers'
import { Field, FieldProps, FormikProvider, useFormik } from 'formik'
import * as React from 'react'
import { Observable, catchError, combineLatest, from, map, of, switchMap } from 'rxjs'
import { useQueryClient } from 'react-query'
import { combineLatest, defer, firstValueFrom, switchMap } from 'rxjs'
import daiLogo from '../../assets/images/dai-logo.svg'
import usdcLogo from '../../assets/images/usdc-logo.svg'
import { ButtonGroup } from '../../components/ButtonGroup'
Expand All @@ -43,6 +46,7 @@ import { Dec } from '../../utils/Decimal'
import { formatBalance } from '../../utils/formatting'
import { useLiquidity } from '../../utils/useLiquidity'
import { useActiveDomains } from '../../utils/useLiquidityPools'
import { metadataQueryFn } from '../../utils/useMetadata'
import { useSuitableAccounts } from '../../utils/usePermissions'
import { usePool, usePoolAccountOrders, usePoolFees } from '../../utils/usePools'
import { usePoolsForWhichAccountIsFeeder } from '../../utils/usePoolsForWhichAccountIsFeeder'
Expand All @@ -51,17 +55,22 @@ import { isCashLoan, isExternalLoan } from '../Loan/utils'

type Attestation = {
portfolio: {
timestamp: number
decimals: number
assets: {
asset: string
assetId?: string
name: string
quantity: string
price: string
}[]
netAssetValue: string
netFeeValue: string
tokenSupply: string[]
tokenPrice: string[]
signature?: string
tokenAddresses: Record<string, string[]>
}
signature?: {
hash: string
publicKey: string
}
}

Expand All @@ -82,13 +91,14 @@ type Row = FormValues['feed'][0] | ActiveLoan | CreatedLoan
const MAX_COLLECT = 100 // maximum number of transactions to collect in one batch

export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const queryClient = useQueryClient()
const { data: domains } = useActiveDomains(poolId)
const allowedPools = usePoolsForWhichAccountIsFeeder()
const isFeeder = !!allowedPools?.find((p) => p.id === poolId)
const [isEditing, setIsEditing] = React.useState(false)
const [isConfirming, setIsConfirming] = React.useState(false)
const orders = usePoolAccountOrders(poolId)
const [liquidityAdminAccount] = useSuitableAccounts({ poolId, poolRole: ['LiquidityAdmin'] })
let [liquidityAdminAccount] = useSuitableAccounts({ poolId, poolRole: ['LiquidityAdmin'] })
const pool = usePool(poolId)
const [allLoans] = useCentrifugeQuery(['loans', poolId], (cent) => cent.pools.getLoans([poolId]), {
enabled: !!poolId && !!pool,
Expand All @@ -97,15 +107,17 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const { substrate } = useWallet()
const poolFees = usePoolFees(poolId)

const externalLoans = React.useMemo(
const activeLoans = React.useMemo(
() =>
(allLoans?.filter(
// Keep external loans, except ones that are fully repaid
(l) => isExternalLoan(l) && l.status !== 'Closed' && (!('presentValue' in l) || !l.presentValue.isZero())
) as ExternalLoan[]) ?? [],
allLoans?.filter(
// Filter out loans that are closed or fully repaid
(l) => l.status !== 'Closed' && (!('presentValue' in l) || !l.presentValue.isZero())
) ?? [],
[allLoans]
)

const externalLoans = React.useMemo(() => activeLoans.filter(isExternalLoan), [activeLoans])

const cashLoans =
(allLoans?.filter((l) => isCashLoan(l) && l.status !== 'Closed') as (CreatedLoan | ActiveLoan)[]) ?? []
const api = useCentrifugeApi()
Expand All @@ -127,58 +139,96 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const { execute, isLoading } = useCentrifugeTransaction(
'Update NAV',
(cent) => (args: [values: FormValues], options) => {
const attestation: Attestation = {
portfolio: {
decimals: pool.currency.decimals,
assets: externalLoans.map((l) => ({
asset: l.id,
quantity: l.pricing.outstandingQuantity.toString(),
price: (l as ActiveLoan).currentPrice?.toString() ?? '0',
})),
netAssetValue: pool.nav.total.toString(),
netFeeValue: pool.nav.fees.toString(),
tokenSupply: pool.tranches.map((t) => t.totalIssuance.toString()),
tokenPrice: pool.tranches.map((t) => t.tokenPrice?.toString() ?? '0'),
},
}

let $signMessage: Observable<string | null> = of(null)
if (provider) {
$signMessage = from(provider.getSigner()).pipe(
switchMap((signer) => from(signer.signMessage(JSON.stringify(attestation)))),
catchError((error) => {
console.error('EVM signing failed:', error)
return of(null)
})
const $attestationHash = defer(async () => {
const nftsByNftId = new Map(
(
await firstValueFrom(
cent.nfts.getNfts([allLoans![0].asset.collectionId, activeLoans.map((l) => l.asset.nftId)])
)
).map((nft) => [nft.id, nft])
)
} else if (substrate?.selectedAccount?.address && substrate?.selectedWallet?.signer?.signRaw) {
$signMessage = from(
substrate.selectedWallet.signer.signRaw({
address: substrate.selectedAccount.address,
data: stringToHex(JSON.stringify(attestation)),
type: 'bytes',
})
).pipe(
map(({ signature }) => signature),
catchError((error) => {
console.error('Substrate signing failed:', error)
return of(null)
const nftMetas = await Promise.all(
activeLoans.map((l) => {
const nft = nftsByNftId.get(l.asset.nftId)
if (!nft?.metadataUri) return null
return queryClient.fetchQuery(['metadata', nft.metadataUri], () => metadataQueryFn(nft.metadataUri!, cent))
})
)
}

const $attestationHash = $signMessage.pipe(
switchMap((signature) => {
if (signature) {
attestation.portfolio.signature = signature
return cent.metadata.pinJson(attestation)
} else {
console.warn('No signature available')
return of(null)
const attestation: Attestation = {
portfolio: {
timestamp: Math.floor(Date.now() / 1000),
decimals: pool.currency.decimals,
assets: [
{
assetId: '0',
name: 'Onchain reserve',
quantity: pool.nav.reserve.toString(),
price: CurrencyBalance.fromFloat(1, pool.currency.decimals).toString(),
},
{
name: 'Accrued fees',
quantity: pool.nav.fees.toString(),
price: CurrencyBalance.fromFloat(-1, pool.currency.decimals).toString(),
},
...activeLoans.map((l, i) =>
isExternalLoan(l)
? {
assetId: l.id,
name: nftMetas[i]?.name ?? '',
quantity: CurrencyBalance.fromFloat(
l.pricing.outstandingQuantity.toDecimal(),
pool.currency.decimals
).toString(),
price: (l as ActiveLoan).currentPrice?.toString() ?? '0',
}
: {
assetId: l.id,
name: nftMetas[i]?.name ?? '',
quantity: (l as ActiveLoan).presentValue.toString(),
price: CurrencyBalance.fromFloat(1, pool.currency.decimals).toString(),
}
),
],
netAssetValue: pool.nav.total.toString(),
tokenSupply: pool.tranches.map((t) => t.totalIssuance.toString()),
tokenPrice: pool.tranches.map((t) => t.tokenPrice?.toString() ?? '0'),
tokenAddresses: Object.fromEntries(
domains
?.map((d) => [d.chainId, Object.values(d.trancheTokens) as string[]] as const)
.filter(([, tokens]) => !tokens.every((t) => t === null)) || []
),
},
}

let signature: { hash: string; publicKey: string } | null = null
try {
const message = JSON.stringify(attestation.portfolio)
if (provider) {
const signer = await provider.getSigner()
const sig = await signer.signMessage(message)
const hash = keccak256(toUtf8Bytes(`\x19Ethereum Signed Message:\n${message.length}${message}`))
const recoveredPubKey = SigningKey.recoverPublicKey(hash, sig)
signature = { hash: sig, publicKey: recoveredPubKey }
} else if (substrate.selectedAccount?.address && substrate?.selectedWallet?.signer?.signRaw) {
const { address } = substrate.selectedAccount
const { signature: sig } = await substrate.selectedWallet.signer.signRaw({
address: address,
data: stringToHex(message),
type: 'bytes',
})
signature = { hash: sig, publicKey: addressToHex(address) }
}
}),
map((result) => (result ? result.ipfsHash : null))
)
} catch {}
if (!signature) return null

attestation.signature = signature
try {
const result = await firstValueFrom(cent.metadata.pinJson(attestation))
return result.ipfsHash
} catch {
return null
}
})

const deployedDomains = domains?.filter((domain) => domain.hasDeployedLp)
const updateTokenPrices = deployedDomains
Expand All @@ -194,12 +244,8 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
)
: []

return combineLatest([
$attestationHash,
cent.pools.closeEpoch([poolId, false], { batch: true }),
...updateTokenPrices,
]).pipe(
switchMap(([attestationHash, closeTx, ...updateTokenPricesTxs]) => {
return combineLatest([$attestationHash, ...updateTokenPrices]).pipe(
switchMap(([attestationHash, ...updateTokenPricesTxs]) => {
if (!attestationHash) {
throw new Error('Attestation signing failed')
}
Expand All @@ -218,7 +264,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {

if (liquidityAdminAccount && orders?.length) {
batch.push(
...closeTx.method.args[0],
api.tx.poolSystem.closeEpoch(poolId),
...orders
.slice(0, ordersFullyExecutable ? MAX_COLLECT : 0)
.map((order) =>
Expand Down
Loading

0 comments on commit e089690

Please sign in to comment.