Skip to content

Commit

Permalink
feat: payouts in funding cycle history (#4356)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyd-eth authored Jun 11, 2024
1 parent 9bafdf1 commit af95cb4
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { render, screen } from '@testing-library/react'
import { V2V3CurrencyOption } from 'models/v2v3/currencyOption'
import { useHistoricalConfigurationPanel } from '../hooks/useConfigurationPanel/useHistoricalConfigurationPanel'
import { HistoricalConfigurationPanel } from './HistoricalConfigurationPanel'

Expand Down Expand Up @@ -42,6 +43,10 @@ describe('CurrentUpcomingConfigurationPanel', () => {
<HistoricalConfigurationPanel
fundingCycle={{ id: '1' } as any}
metadata={{ id: '1' } as any}
withdrawnAmountAndCurrency={{
amount: 1,
currency: 1 as V2V3CurrencyOption,
}}
/>,
)
})
Expand All @@ -51,6 +56,10 @@ describe('CurrentUpcomingConfigurationPanel', () => {
<HistoricalConfigurationPanel
fundingCycle={{ id: '1' } as any}
metadata={{ id: '1' } as any}
withdrawnAmountAndCurrency={{
amount: 100,
currency: 2 as V2V3CurrencyOption,
}}
/>,
)
expect(screen.getByTestId('configuration-panel')).toHaveTextContent(
Expand All @@ -69,11 +78,19 @@ describe('CurrentUpcomingConfigurationPanel', () => {
<HistoricalConfigurationPanel
fundingCycle={{ id: '1' } as any}
metadata={{ id: '1' } as any}
withdrawnAmountAndCurrency={{
amount: 1,
currency: 1 as V2V3CurrencyOption,
}}
/>,
)
expect(useHistoricalConfigurationPanel).toHaveBeenCalledWith({
fundingCycle: { id: '1' },
metadata: { id: '1' },
withdrawnAmountAndCurrency: {
amount: 1,
currency: 1 as V2V3CurrencyOption,
},
})
})
})
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { V2V3CurrencyOption } from 'models/v2v3/currencyOption'
import {
V2V3FundingCycle,
V2V3FundingCycleMetadata,
} from 'models/v2v3/fundingCycle'
import { useHistoricalConfigurationPanel } from '../hooks/useConfigurationPanel/useHistoricalConfigurationPanel'
import { ConfigurationPanel } from './ConfigurationPanel'
import { HistoricalPayoutsData } from './HistoricalPayoutsData'

type HistoricalConfigurationPanelProps = {
export type HistoricalConfigurationPanelProps = {
fundingCycle: V2V3FundingCycle
metadata: V2V3FundingCycleMetadata
withdrawnAmountAndCurrency: {
amount: number
currency: V2V3CurrencyOption
}
}

export const HistoricalConfigurationPanel: React.FC<
HistoricalConfigurationPanelProps
> = p => {
const props = useHistoricalConfigurationPanel(p)
return <ConfigurationPanel {...props} />

return (
<div className="flex flex-col gap-8">
<ConfigurationPanel {...props} />
<HistoricalPayoutsData {...p} />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useProjectMetadataContext } from 'contexts/shared/ProjectMetadataContext'
import { BigNumber } from 'ethers'
import { FundingCyclesQuery } from 'generated/graphql'
import useProjectDistributionLimit from 'hooks/v2v3/contractReader/useProjectDistributionLimit'
import { V2V3CurrencyOption } from 'models/v2v3/currencyOption'
import moment from 'moment'
import React, { Fragment, useMemo } from 'react'
import { twMerge } from 'tailwind-merge'
import { isBigNumberish } from 'utils/bigNumbers'
import { formatCurrencyAmount } from 'utils/format/formatCurrencyAmount'
import { fromWad } from 'utils/format/formatNumber'
import { V2V3_CURRENCY_ETH } from 'utils/v2v3/currency'
import {
sgFCToV2V3FundingCycle,
sgFCToV2V3FundingCycleMetadata,
} from 'utils/v2v3/fundingCycle'
import { useProjectContext } from '../../../hooks/useProjectContext'
import { HistoricalConfigurationPanel } from './HistoricalConfigurationPanel'

type QueriedFundingCycle = FundingCyclesQuery['fundingCycles'][number]

const HistoricalCycle: React.FC<QueriedFundingCycle> = cycle => {
const { projectId } = useProjectMetadataContext()
const { primaryETHTerminal } = useProjectContext()

const { data: distributionLimit } = useProjectDistributionLimit({
projectId,
configuration: cycle.configuration.toString(),
terminal: primaryETHTerminal,
})
const currency =
useMemo(() => {
if (typeof distributionLimit === 'undefined') return

if (
!(Array.isArray(distributionLimit) && distributionLimit.length === 2)
) {
console.error(
'Unexpected result from distributionLimitOf',
distributionLimit,
)
throw new Error('Unexpected result from distributionLimitOf')
}

const [, currency] = distributionLimit

if (!isBigNumberish(currency)) {
console.error(
'Unexpected result from distributionLimitOf',
distributionLimit,
)
throw new Error('Unexpected result from distributionLimitOf')
}
const _currencyOption = BigNumber.from(currency).toNumber()
if (_currencyOption !== 0) return _currencyOption as V2V3CurrencyOption
}, [distributionLimit]) ?? V2V3_CURRENCY_ETH

const withdrawnAmountAndCurrency = {
amount: parseFloat(fromWad(cycle.withdrawnAmount)),
currency: currency,
}

return (
<Disclosure key={cycle.number} as={Fragment}>
{({ open }) => (
<div className="p-4 pr-2">
<Disclosure.Button
data-testid={`disclosure-button-${cycle.number}`}
as="div"
className="grid cursor-pointer grid-cols-config-table gap-3 whitespace-nowrap text-sm font-medium"
>
<div>#{cycle.number}</div>
<div>
<span>
{formatCurrencyAmount(withdrawnAmountAndCurrency) ?? '0'}
</span>
</div>
<div className="text-grey-500 dark:text-slate-200">
{`${moment(
(cycle.startTimestamp + cycle.duration) * 1000,
).fromNow(true)} ago`}
</div>
<div className="text-gray-500 flex items-center justify-end whitespace-nowrap text-sm">
<ChevronDownIcon
className={twMerge(open && 'rotate-180', 'h-5 w-5')}
/>
</div>
</Disclosure.Button>
<Transition
show={open}
as={Fragment}
enter="transition-all ease-in-out duration-300"
enterFrom="max-h-0 overflow-hidden opacity-0"
enterTo="max-h-[1000px] overflow-hidden opacity-100"
leave="transition-all ease-in-out duration-300"
leaveFrom="max-h-[1000px] overflow-hidden opacity-100"
leaveTo="max-h-0 overflow-hidden opacity-0"
>
<Disclosure.Panel className="p-4">
<HistoricalConfigurationPanel
fundingCycle={sgFCToV2V3FundingCycle(cycle)}
metadata={sgFCToV2V3FundingCycleMetadata(cycle)}
withdrawnAmountAndCurrency={withdrawnAmountAndCurrency}
/>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
)
}

export default HistoricalCycle
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import EthereumAddress from 'components/EthereumAddress'
import { ETH_PAYOUT_SPLIT_GROUP } from 'constants/splits'
import { ProjectMetadataContext } from 'contexts/shared/ProjectMetadataContext'
import useProjectSplits from 'hooks/v2v3/contractReader/useProjectSplits'
import round from 'lodash/round'
import React, { useContext } from 'react'
import { formatCurrencyAmount } from 'utils/format/formatCurrencyAmount'
import { V2V3_CURRENCY_ETH } from 'utils/v2v3/currency'
import { derivePayoutAmount } from 'utils/v2v3/distributions'
import { ConfigurationPanelTableData } from './ConfigurationPanel'
import { ConfigurationTable } from './ConfigurationTable'
import { HistoricalConfigurationPanelProps } from './HistoricalConfigurationPanel'

export const HistoricalPayoutsData: React.FC<
HistoricalConfigurationPanelProps
> = p => {
const { projectId } = useContext(ProjectMetadataContext)

const config = p.fundingCycle?.configuration?.toString()
const { data: payoutSplits } = useProjectSplits({
projectId,
splitGroup: ETH_PAYOUT_SPLIT_GROUP,
domain: config,
})

const currency = p.withdrawnAmountAndCurrency.currency
const withdrawnAmount = p.withdrawnAmountAndCurrency.amount

const payoutSplitsData =
payoutSplits?.reduce<ConfigurationPanelTableData>((acc, split) => {
const key = split.beneficiary ?? ''
const payoutAmount = round(
derivePayoutAmount({
payoutSplit: split,
distributionLimit: withdrawnAmount,
}),
currency === V2V3_CURRENCY_ETH ? 4 : 2,
)

const formattedPayout =
formatCurrencyAmount({
amount: payoutAmount,
currency,
}) ?? '0'

acc[key] = {
name: <EthereumAddress address={split.beneficiary} />,
new: <span>{formattedPayout}</span>,
}
return acc
}, {}) ?? {}

return (
<>
{payoutSplits && payoutSplits.length ? (
<ConfigurationTable title="Payouts" data={payoutSplitsData} />
) : null}
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { Trans, t } from '@lingui/macro'
import { Button } from 'antd'
import { useProjectContext } from 'components/v2v3/V2V3Project/ProjectDashboard/hooks/useProjectContext'
import { useProjectMetadataContext } from 'contexts/shared/ProjectMetadataContext'
import { BigNumber } from 'ethers'
import useProjectDistributionLimit from 'hooks/v2v3/contractReader/useProjectDistributionLimit'
import { V2V3CurrencyOption } from 'models/v2v3/currencyOption'
import moment from 'moment'
import { Fragment, useMemo, useState } from 'react'
import { useState } from 'react'
import { twMerge } from 'tailwind-merge'
import { isBigNumberish } from 'utils/bigNumbers'
import { formatCurrencyAmount } from 'utils/format/formatCurrencyAmount'
import { fromWad } from 'utils/format/formatNumber'
import {
sgFCToV2V3FundingCycle,
sgFCToV2V3FundingCycleMetadata,
} from 'utils/v2v3/fundingCycle'
import { usePastFundingCycles } from '../hooks/usePastFundingCycles'
import { HistoricalConfigurationPanel } from './HistoricalConfigurationPanel'
import HistoricalCycle from './HistoricalCycle'

export const HistorySubPanel = () => {
const { projectId } = useProjectMetadataContext()
Expand Down Expand Up @@ -60,50 +47,8 @@ export const HistorySubPanel = () => {
<div className="text-error-400">{error.message}</div>
) : (
<>
{data?.fundingCycles.map(cycle => (
<Disclosure key={cycle.number} as={Fragment}>
{({ open }) => (
<div className="p-4 pr-2">
<Disclosure.Button
data-testid={`disclosure-button-${cycle.number}`}
as="div"
className="grid cursor-pointer grid-cols-config-table gap-3 whitespace-nowrap text-sm font-medium"
>
<div>#{cycle.number}</div>
<div>
<FormattedWithdrawnAmount {...cycle} />
</div>
<div className="text-grey-500 dark:text-slate-200">
{`${moment(
(cycle.startTimestamp + cycle.duration) * 1000,
).fromNow(true)} ago`}
</div>
<div className="text-gray-500 flex items-center justify-end whitespace-nowrap text-sm">
<ChevronDownIcon
className={twMerge(open && 'rotate-180', 'h-5 w-5')}
/>
</div>
</Disclosure.Button>
<Transition
show={open}
as={Fragment}
enter="transition-all ease-in-out duration-300"
enterFrom="max-h-0 overflow-hidden opacity-0"
enterTo="max-h-[1000px] overflow-hidden opacity-100"
leave="transition-all ease-in-out duration-300"
leaveFrom="max-h-[1000px] overflow-hidden opacity-100"
leaveTo="max-h-0 overflow-hidden opacity-0"
>
<Disclosure.Panel className="p-4">
<HistoricalConfigurationPanel
fundingCycle={sgFCToV2V3FundingCycle(cycle)}
metadata={sgFCToV2V3FundingCycleMetadata(cycle)}
/>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
{data?.fundingCycles.map((cycle, index) => (
<HistoricalCycle {...cycle} key={index} />
))}
{isLoading ? (
<LoadingState />
Expand Down Expand Up @@ -159,56 +104,3 @@ const SkeletonRow = () => (
</div>
</div>
)

/**
* Component to get the currency for a specific funding cycle, and render its formatted withdrawnAmount in that currency
*/
function FormattedWithdrawnAmount({
configuration,
withdrawnAmount,
}: {
configuration: BigNumber
withdrawnAmount: BigNumber
}) {
const { projectId } = useProjectMetadataContext()
const { primaryETHTerminal } = useProjectContext()

const { data: distributionLimit } = useProjectDistributionLimit({
projectId,
configuration: configuration.toString(),
terminal: primaryETHTerminal,
})

const currencyOption = useMemo(() => {
if (typeof distributionLimit === 'undefined') return

if (!(Array.isArray(distributionLimit) && distributionLimit.length === 2)) {
console.error(
'Unexpected result from distributionLimitOf',
distributionLimit,
)
throw new Error('Unexpected result from distributionLimitOf')
}

const [, currency] = distributionLimit

if (!isBigNumberish(currency)) {
console.error(
'Unexpected result from distributionLimitOf',
distributionLimit,
)
throw new Error('Unexpected result from distributionLimitOf')
}
const _currencyOption = BigNumber.from(currency).toNumber()
if (_currencyOption !== 0) return _currencyOption as V2V3CurrencyOption
}, [distributionLimit])

return (
<span>
{formatCurrencyAmount({
amount: fromWad(withdrawnAmount),
currency: currencyOption,
}) ?? '0Ξ'}
</span>
)
}
Loading

0 comments on commit af95cb4

Please sign in to comment.