Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { getProgressBarStepName, shouldUpdateStepImmediately } from './useOrderProgressBarProps'
import { getAddressKey } from '@cowprotocol/cow-sdk'

import { ApiSolverCompetition } from 'common/types/soverCompetition'

import { buildSolverCompetition, getProgressBarStepName, shouldUpdateStepImmediately } from './useOrderProgressBarProps'

import { OrderProgressBarStepName } from '../constants'
import { OrderProgressBarState } from '../types'
Expand Down Expand Up @@ -94,3 +98,64 @@ describe('shouldUpdateStepImmediately', () => {
expect(shouldUpdateStepImmediately(OrderProgressBarStepName.SOLVING, undefined, 0)).toBe(true)
})
})

describe('buildSolverCompetition', () => {
// Backend returns entries ranked ascending, so the last entry is the winner. A `marker` tags
// each raw entry so we can assert which duplicate occurrence survived deduplication.
function entry(solver: string, marker: string): ApiSolverCompetition {
return { solver, marker, executedAmounts: { sell: '1', buy: '1' } } as unknown as ApiSolverCompetition
}

const ADDR_LIVE = '0x1111111111111111111111111111111111111111'
const ADDR_RETIRED = '0x2222222222222222222222222222222222222222'
const ADDR_OTHER = '0x3333333333333333333333333333333333333333'
// A solver's live and retired deployments are distinct on-chain addresses that the CMS maps to
// the same solverId; a different solver maps to its own.
const byAddress = {
[getAddressKey(ADDR_LIVE)]: { solverId: 'baseline' },
[getAddressKey(ADDR_RETIRED)]: { solverId: 'baseline' },
[getAddressKey(ADDR_OTHER)]: { solverId: 'barter' },
} as unknown as Parameters<typeof buildSolverCompetition>[1]

it('keeps the highest-ranked occurrence of a repeated solver as the winner', () => {
const result = buildSolverCompetition(
[entry(ADDR_LIVE, 'first'), entry(ADDR_OTHER, 'barter'), entry(ADDR_RETIRED, 'last')],
byAddress,
)

expect(result.map((s) => s.solverId)).toEqual(['baseline', 'barter'])
// Winner stays at index 0 and is the highest-ranked (last) `baseline` occurrence, not the first.
expect((result[0] as unknown as { marker: string }).marker).toBe('last')
})

it('collapses distinct addresses that resolve to the same solverId', () => {
const result = buildSolverCompetition([entry(ADDR_LIVE, 'live'), entry(ADDR_RETIRED, 'retired')], byAddress)

expect(result.map((s) => s.solverId)).toEqual(['baseline'])
// The higher-ranked (last) address wins the collapsed entry.
expect((result[0] as unknown as { marker: string }).marker).toBe('retired')
})

it('does not deduplicate distinct solvers', () => {
const result = buildSolverCompetition([entry(ADDR_OTHER, 'barter'), entry(ADDR_LIVE, 'baseline')], byAddress)

expect(result.map((s) => s.solverId)).toEqual(['baseline', 'barter'])
})

it('excludes entries without a solver or executedAmounts', () => {
const result = buildSolverCompetition(
[
{ marker: 'no-solver', executedAmounts: {} } as unknown as ApiSolverCompetition,
{ solver: ADDR_LIVE, marker: 'no-amounts' } as unknown as ApiSolverCompetition,
entry(ADDR_OTHER, 'valid'),
],
byAddress,
)

expect(result.map((s) => s.solverId)).toEqual(['barter'])
})

it('returns an empty list when there is no competition data', () => {
expect(buildSolverCompetition(undefined, {})).toEqual([])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -204,26 +204,10 @@ function useOrderBaseProgressBarProps(params: UseOrderProgressBarPropsParams): U

const doNotQuery = getDoNotQueryStatusEndpoint(order, apiSolverCompetition, !!disableProgressBar)

const solverCompetition = useMemo(() => {
const solversMap = apiSolverCompetition?.reduce(
(acc, entry) => {
// If the entry is not a valid or has no executedAmounts, the solution doesn't consider this order, skip it
if (!entry || !entry.solver || !entry.executedAmounts) {
return acc
}
// Merge the solver competition data with the info fetched from CMS under the same key, to avoid duplicates
acc[entry.solver] = mergeSolverData(entry, solversInfoByAddress)
return acc
},
{} as Record<string, SolverCompetition>,
)

return (
Object.values(solversMap || {})
// Reverse it since backend returns the solutions ranked ascending. Winner is the last one.
.reverse()
)
}, [apiSolverCompetition, solversInfoByAddress])
const solverCompetition = useMemo(
() => buildSolverCompetition(apiSolverCompetition, solversInfoByAddress),
[apiSolverCompetition, solversInfoByAddress],
)
const { swapAndBridgeContext } = useSwapAndBridgeContext(
chainId,
isBridgingTrade ? order : undefined,
Expand Down Expand Up @@ -561,6 +545,45 @@ const POOLING_SWR_OPTIONS = {
refreshInterval: ms`1s`,
}

/**
* Builds the displayed solver competition list from the raw orderbook entries.
*
* Backend returns solutions ranked ascending, so the winner is the last entry. We traverse from
* highest to lowest rank and keep the first occurrence of each merged `solverId`. This retains the
* highest-ranked entry per solver identity (deduplicating legacy aliases that normalize to the same
* `solverId`) and keeps the winner at index 0.
*/
export function buildSolverCompetition(
apiSolverCompetition: CompetitionOrderStatus['value'] | undefined,
solversInfoByAddress: Record<string, SolverInfo>,
): SolverCompetition[] {
const seenSolverIds = new Set<string>()

return (apiSolverCompetition || []).reduceRight<SolverCompetition[]>((acc, entry) => {
// If the entry is not valid or has no executedAmounts, the solution doesn't consider this order, skip it
if (!entry || !entry.solver || !entry.executedAmounts) {
return acc
}
// Merge with the info fetched from CMS, then deduplicate on the merged solver identity rather
// than the raw backend address, so a solver's retired deployments (several addresses for the
// same solverId on one chain) collapse into a single entry.
const merged = mergeSolverData(entry, solversInfoByAddress)
const { solverId } = merged
// solverId is always set by mergeSolverData, but the SolverCompetition type keeps it optional;
// entries without one can't be deduplicated by identity, so keep them as-is.
if (solverId === undefined) {
acc.push(merged)
return acc
}
if (seenSolverIds.has(solverId)) {
return acc
}
seenSolverIds.add(solverId)
acc.push(merged)
return acc
}, [])
}

/**
* Merges solverCompetition data returned by the orderbook /status endpoint with
* solver info fetched from CMS.
Expand Down
Loading