Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion apps/explorer/src/api/operator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export type Order = Pick<
executedFeeAmount: BigNumber
executedFee: BigNumber | null
totalFee: BigNumber
// Derived client-side from trades' executedProtocolFees, not returned by the API.
// protocolFeeTokenAddress is the surplus-side token (may differ from executedFeeToken).
networkCosts?: BigNumber
protocolFees?: BigNumber
protocolFeeTokenAddress?: string
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
cancelled: boolean
status: OrderStatus
partiallyFilled: boolean
Expand All @@ -127,7 +132,7 @@ export type RawTrade = TradeMetaData
/**
* Enriched Trade type
*/
export type Trade = Pick<RawTrade, 'blockNumber' | 'logIndex' | 'owner' | 'txHash'> & {
export type Trade = Pick<RawTrade, 'blockNumber' | 'logIndex' | 'owner' | 'txHash' | 'executedProtocolFees'> & {
orderId: string
kind?: OrderKind
buyAmount: BigNumber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { ReactNode, useState } from 'react'

import { AppDataWrapper } from 'components/common/AppDataWrapper'
import { RowWithCopyButton } from 'components/common/RowWithCopyButton'
import { ShowMoreButton } from 'components/common/ShowMoreButton'
import { useAppData } from 'hooks/useAppData'

import * as styledEl from './AppDataRowContent.styles'

import { AppDataContent } from '../AppData/AppDataContent'

interface AppDataRowContentProps {
Expand Down Expand Up @@ -44,9 +43,9 @@ export function AppDataRowContent({ appData, showExpanded = false, fullAppData }
)}
&nbsp;
{hasAppData && (
<styledEl.ShowMoreButton onClick={() => setShowDecodedAppData((state) => !state)}>
<ShowMoreButton onClick={() => setShowDecodedAppData((state) => !state)}>
{showDecodedAppData ? '[-] Show less' : '[+] Show more'}
</styledEl.ShowMoreButton>
</ShowMoreButton>
)}
</>
<div className={`hidden-content ${appDataError && 'error'}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,34 @@ TinyFee18DecimalsToken.args = {
...defaultProps,
order: { ...order, executedFeeAmount: new BigNumber('1'), sellToken: WETH },
}

export const WithBreakdownSameToken = Template.bind({})
WithBreakdownSameToken.args = {
...defaultProps,
order: {
...order,
executedFeeAmount: order.feeAmount,
fullyFilled: true,
totalFee: new BigNumber('200000'),
networkCosts: new BigNumber('150000'),
protocolFees: new BigNumber('50000'),
protocolFeeTokenAddress: order.sellTokenAddress,
executedFeeToken: order.sellTokenAddress,
},
}

// Surplus-side protocol fee: charged in the buy token while network costs stay in the sell token
export const WithBreakdownSurplusToken = Template.bind({})
WithBreakdownSurplusToken.args = {
...defaultProps,
order: {
...order,
executedFeeAmount: order.feeAmount,
fullyFilled: true,
totalFee: new BigNumber('200000'),
networkCosts: new BigNumber('200000'),
protocolFees: new BigNumber('1166200'),
protocolFeeTokenAddress: order.buyTokenAddress,
executedFeeToken: order.sellTokenAddress,
},
}
100 changes: 56 additions & 44 deletions apps/explorer/src/components/orders/GasFeeDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// TODO: Enable once API is ready
// import { NumbersBreakdown } from 'components/orders/NumbersBreakdown'

import React, { useMemo } from 'react'

import { TokenErc20 } from '@gnosis.pm/dex-js'
import BigNumber from 'bignumber.js'
import { NumbersBreakdown } from 'components/orders/NumbersBreakdown'
import { ZERO_BIG_NUMBER } from 'const'
import styled from 'styled-components/macro'

Expand All @@ -17,44 +17,19 @@ const Wrapper = styled.div`

export type Props = { order: Order }

// TODO: Enable once API is ready
// const fetchFeeBreakdown = async (initialFee: string): Promise<any> => {
// // TODO: Simulating API call to fetch fee breakdown data
// return new Promise((resolve) => {
// resolve({
// networkCosts: 'TODO: Get network costs here',
// fee: 'TODO: Get fee here',
// total: initialFee,
// })
// })
// }

// TODO: Enable once API is ready
// const renderFeeBreakdown = (data: any): React.ReactNode => {
// return (
// <table>
// <tbody>
// <tr>
// <td>Network Costs:</td>
// <td>{data.networkCosts}</td>
// </tr>
// <tr>
// <td>Fee:</td>
// <td>{data.fee}</td>
// </tr>
// <tr>
// <td>Total Costs & Fees:</td>
// <td>{data.total}</td>
// </tr>
// </tbody>
// </table>
// )
// }

export function GasFeeDisplay(props: Props): React.ReactNode | null {
const { order } = props
const {
order: { feeAmount, sellToken, sellTokenAddress, fullyFilled, totalFee },
} = props
feeAmount,
sellToken,
sellTokenAddress,
fullyFilled,
totalFee,
networkCosts,
protocolFees,
protocolFeeTokenAddress,
executedFeeToken,
} = order

const { executedFeeFormatted, totalFeeFormatted, quoteSymbol } = useMemo(() => {
if (!sellToken) {
Expand Down Expand Up @@ -89,14 +64,51 @@ export function GasFeeDisplay(props: Props): React.ReactNode | null {
[noFee, executedFeeFormatted, quoteSymbol, fullyFilled, feeAmount, totalFeeFormatted],
)

const sameDenomination =
!!protocolFeeTokenAddress && executedFeeToken?.toLowerCase() === protocolFeeTokenAddress.toLowerCase()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return (
<Wrapper>
{FeeElement}
{/*TODO: Enable once API is ready*/}
{/*<NumbersBreakdown*/}
{/* fetchData={() => fetchFeeBreakdown(`${formattedExecutedFee} ${quoteSymbol}`)}*/}
{/* renderContent={renderFeeBreakdown}*/}
{/*/>*/}
{networkCosts && protocolFees && (
<NumbersBreakdown>
<table>
<tbody>
<tr>
<td>Network Costs:</td>
<td>{formatFee(order, networkCosts, executedFeeToken)}</td>
</tr>
<tr>
<td>Fee:</td>
<td>{formatFee(order, protocolFees, protocolFeeTokenAddress)}</td>
</tr>
{sameDenomination && (
<tr>
<td>Total Costs &amp; Fees:</td>
<td>
{executedFeeFormatted} {quoteSymbol}
</td>
</tr>
)}
</tbody>
</table>
</NumbersBreakdown>
)}
</Wrapper>
)
}

function formatFee(order: Order, amount: BigNumber, address: string | undefined): string {
const token = resolveToken(order, address)
if (!token) return address ? `${amount.toString(10)} ${address}` : amount.toString(10)
const { formattedAmount, symbol } = formatTokenAmount(amount, token)
return `${formattedAmount} ${symbol}`
}

function resolveToken(order: Order, address: string | undefined): TokenErc20 | undefined {
const target = address?.toLowerCase()
if (!target) return order.sellToken || undefined
if (order.sellToken?.address.toLowerCase() === target) return order.sellToken
if (order.buyToken?.address.toLowerCase() === target) return order.buyToken
return undefined
}
56 changes: 56 additions & 0 deletions apps/explorer/src/components/orders/NumbersBreakdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { PropsWithChildren, ReactNode } from 'react'

import { Media } from '@cowprotocol/ui'

import { ShowMoreButton } from 'components/common/ShowMoreButton'
import useSafeState from 'hooks/useSafeState'
import styled from 'styled-components/macro'

const DetailsWrapper = styled.div`
display: flex;
margin: 1.8rem 0 1rem;
border-radius: 0.6rem;
line-height: 1.6;
width: max-content;
align-items: flex-start;
word-break: break-all;
overflow: auto;
border: 1px solid rgb(151 151 184 / 10%);
background: rgb(151 151 184 / 10%);

${Media.upToSmall()} {
width: 100%;
}

table {
width: 100%;
border-collapse: collapse;
}

td {
padding: 0.1rem 0.5rem;
}

tr:not(:last-child) td {
border-bottom: 1px solid rgb(151 151 184 / 15%);
}
`

type BreakdownProps = {
showExpanded?: boolean
} & PropsWithChildren

export const NumbersBreakdown = ({ children, showExpanded = false }: BreakdownProps): ReactNode => {
const [showDetails, setShowDetails] = useSafeState<boolean>(showExpanded)

const handleToggle = (): void => {
setShowDetails(!showDetails)
}

return (
<>
<ShowMoreButton onClick={handleToggle}>{showDetails ? '[-] Show less' : '[+] Show more'}</ShowMoreButton>
{showDetails && <DetailsWrapper>{children}</DetailsWrapper>}
</>
)
}
27 changes: 17 additions & 10 deletions apps/explorer/src/components/orders/OrderDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { knownBridgeProviders } from 'sdk/cowSdk'
import { useNetworkId } from 'state/network'
import { SWRResponse } from 'swr'
import { Errors } from 'types'
import { formatPercentage } from 'utils'
import { formatPercentage, getFees } from 'utils'

import { useCrossChainOrder } from 'modules/bridge'

Expand Down Expand Up @@ -80,7 +80,7 @@ const tabItems = (
solvedBy?: OrderSolverInfo,
isSolvedByLoading?: boolean,
): TabItemInterface[] => {
const order = getOrderWithTxHash(_order, trades, hasMultipleTrades)
const order = enrichOrderFromTrades(_order, trades, hasMultipleTrades)
const areTokensLoaded = Boolean(order?.buyToken && order?.sellToken)
const isLoadingForTheFirstTime = isOrderLoading && !areTokensLoaded
const filledPercentage = order?.filledPercentage && formatPercentage(order.filledPercentage)
Expand Down Expand Up @@ -149,15 +149,22 @@ const tabItems = (
}

/**
* Get the order with txHash set if it has a single trade
*
* That is the case for any filled fill or kill or a partial fill that has a single trade
* Returns the order enriched with fields derived from its trades:
* - networkCosts/protocolFees/protocolFeeTokenAddress aggregated across trades
* - txHash and executionDate when the order has a single trade (fill or kill,
* or a partial fill with a single trade so far)
*/
function getOrderWithTxHash(order: Order | null, trades: Trade[], hasMultipleTrades: boolean): Order | null {
if (order && trades.length === 1 && !hasMultipleTrades) {
return { ...order, txHash: trades[0].txHash || undefined, executionDate: trades[0].executionTime || undefined }
function enrichOrderFromTrades(order: Order | null, trades: Trade[], hasMultipleTrades: boolean): Order | null {
if (!order) return order

const enriched = { ...order, ...getFees(order, trades) }

if (trades.length === 1 && !hasMultipleTrades) {
enriched.txHash = trades[0].txHash || undefined
enriched.executionDate = trades[0].executionTime || undefined
}
return order

return enriched
}

function hasMultipleTradesForOrder(trades: Trade[], tableState: TableState): boolean {
Expand Down Expand Up @@ -192,7 +199,7 @@ export const OrderDetails: React.FC<Props> = (props) => {
const crossChainOrderResponse = useCrossChainOrder(order?.uid)
const hasMultipleTrades = hasMultipleTradesForOrder(trades, tableState)
const isMultiFill = order?.partiallyFillable && !order.txHash && hasMultipleTrades
const orderWithTxHash = getOrderWithTxHash(order, trades, hasMultipleTrades)
const orderWithTxHash = enrichOrderFromTrades(order, trades, hasMultipleTrades)
const { solver: solvedBy, isLoading: isSolvedByLoading } = useOrderSolver(
showSolverDetails && !isMultiFill ? orderWithTxHash : null,
)
Expand Down
Loading
Loading