From f104c1fdad4ee74649eabe2268d335483c636bce Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 1 Mar 2026 20:14:34 -0300 Subject: [PATCH 1/2] feat(positions): add GET /positions/reference endpoint for adjustPriceWithReference --- positions/positions.controller.ts | 9 +++++++++ positions/positions.service.ts | 27 +++++++++++++++++++++++++++ positions/positions.types.ts | 11 +++++++++++ 3 files changed, 47 insertions(+) diff --git a/positions/positions.controller.ts b/positions/positions.controller.ts index fb7feac..10b2126 100644 --- a/positions/positions.controller.ts +++ b/positions/positions.controller.ts @@ -7,6 +7,7 @@ import { ApiPositionsListing, ApiPositionsMapping, ApiPositionsOwners, + ApiReferencePositions, } from './positions.types'; import { ApiResponse, ApiTags } from '@nestjs/swagger'; @@ -78,4 +79,12 @@ export class PositionsController { geMintingtMapping(): ApiMintingUpdateMapping { return this.positionsService.getMintingUpdatesMapping(); } + + @Get('reference') + @ApiResponse({ + description: 'Returns the active position with the highest price per collateral for cooldown-free price increases', + }) + getReferencePositions(): ApiReferencePositions { + return this.positionsService.getReferencePositions(); + } } diff --git a/positions/positions.service.ts b/positions/positions.service.ts index f07d230..a88543c 100644 --- a/positions/positions.service.ts +++ b/positions/positions.service.ts @@ -12,11 +12,13 @@ import { ApiPositionsListing, ApiPositionsMapping, ApiPositionsOwners, + ApiReferencePositions, MintingUpdateQuery, MintingUpdateQueryObjectArray, OwnersPositionsObjectArray, PositionQuery, PositionsQueryObjectArray, + ReferencePositionsMapping, } from './positions.types'; // Genesis position address from NPM package @@ -117,6 +119,7 @@ export class PositionsService { denied closed original + isChallenged minimumCollateral riskPremiumPPM @@ -237,6 +240,7 @@ export class PositionsService { denied: p.denied, closed: p.closed, original: getAddress(p.original), + isChallenged: p.isChallenged, minimumCollateral: p.minimumCollateral, annualInterestPPM: leadrate + p.riskPremiumPPM, @@ -279,6 +283,29 @@ export class PositionsService { return list; } + getReferencePositions(): ApiReferencePositions { + const now = Math.floor(Date.now() / 1000); + const candidates = Object.values(this.fetchedPositions) as PositionQuery[]; + const map: ReferencePositionsMapping = {}; + + for (const p of candidates) { + if (p.closed || p.denied) continue; + if (p.isChallenged) continue; + if (BigInt(p.principal) === 0n) continue; + if (Number(p.cooldown) >= now) continue; + if (Number(p.expiration) <= now) continue; + + const collateral = p.collateral.toLowerCase() as Address; + const current = map[collateral]; + if (!current || BigInt(p.price) > BigInt(current.price)) { + map[collateral] = p; + } + } + + const collaterals = Object.keys(map) as Address[]; + return { num: collaterals.length, collaterals, map }; + } + getMintingUpdatesList(): ApiMintingUpdateListing { const m = Object.values(this.fetchedMintingUpdates).flat(1) as MintingUpdateQuery[]; return { diff --git a/positions/positions.types.ts b/positions/positions.types.ts index 42cb7a1..fe51b2d 100644 --- a/positions/positions.types.ts +++ b/positions/positions.types.ts @@ -16,6 +16,7 @@ export type PositionQuery = { denied: boolean; closed: boolean; original: Address; + isChallenged: boolean; minimumCollateral: string; annualInterestPPM: number; // @dev: in V2, sum of leadrate and riskPremium @@ -118,6 +119,16 @@ export type ApiMintingUpdateMapping = { map: MintingUpdateQueryObjectArray; }; +export type ReferencePositionsMapping = { + [collateral: Address]: PositionQuery; +}; + +export type ApiReferencePositions = { + num: number; + collaterals: Address[]; + map: ReferencePositionsMapping; +}; + export type ApiPositionDefault = { position: Address; collateral: Address; From a78b95320ba295306010e88816fc08ab55a73100 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 11 Mar 2026 15:41:28 -0300 Subject: [PATCH 2/2] add GET /positions/best-cloneable endpoint --- positions/positions.controller.ts | 12 +++++++++++- positions/positions.service.ts | 25 +++++++++++++++++++++++++ positions/positions.types.ts | 4 ++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/positions/positions.controller.ts b/positions/positions.controller.ts index 10b2126..f92ddc0 100644 --- a/positions/positions.controller.ts +++ b/positions/positions.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { PositionsService } from './positions.service'; import { + ApiBestCloneable, ApiMintingUpdateListing, ApiMintingUpdateMapping, ApiPositionDefault, @@ -9,6 +10,7 @@ import { ApiPositionsOwners, ApiReferencePositions, } from './positions.types'; +import { Address } from 'viem'; import { ApiResponse, ApiTags } from '@nestjs/swagger'; @ApiTags('Positions Controller') @@ -87,4 +89,12 @@ export class PositionsController { getReferencePositions(): ApiReferencePositions { return this.positionsService.getReferencePositions(); } + + @Get('best-cloneable') + @ApiResponse({ + description: 'Returns the best cloneable parent position for the given collateral (highest price, active, not challenged)', + }) + getBestCloneable(@Query('collateral') collateral: string): ApiBestCloneable { + return this.positionsService.getBestCloneableParent((collateral ?? '').toLowerCase() as Address); + } } diff --git a/positions/positions.service.ts b/positions/positions.service.ts index a88543c..bf3ed83 100644 --- a/positions/positions.service.ts +++ b/positions/positions.service.ts @@ -6,6 +6,7 @@ import { Address, erc20Abi, getAddress } from 'viem'; import { PONDER_CLIENT } from '../api.apollo.config'; import { CONFIG, VIEM_CONFIG } from '../api.config'; import { + ApiBestCloneable, ApiMintingUpdateListing, ApiMintingUpdateMapping, ApiPositionDefault, @@ -306,6 +307,30 @@ export class PositionsService { return { num: collaterals.length, collaterals, map }; } + getBestCloneableParent(collateral: Address): ApiBestCloneable { + const now = Math.floor(Date.now() / 1000); + const collateralLower = collateral.toLowerCase() as Address; + const candidates = Object.values(this.fetchedPositions) as PositionQuery[]; + + const cloneable = candidates + .filter( + (p) => + !p.closed && + !p.denied && + p.expiration > now && + p.cooldown < now && + BigInt(p.collateralBalance) >= BigInt(p.minimumCollateral) && + p.collateral.toLowerCase() === collateralLower && + !p.isChallenged, + ) + .sort((a, b) => { + const diff = BigInt(b.price) - BigInt(a.price); + return diff > 0n ? 1 : diff < 0n ? -1 : 0; + }); + + return { position: cloneable[0] ?? null }; + } + getMintingUpdatesList(): ApiMintingUpdateListing { const m = Object.values(this.fetchedMintingUpdates).flat(1) as MintingUpdateQuery[]; return { diff --git a/positions/positions.types.ts b/positions/positions.types.ts index fe51b2d..8560b29 100644 --- a/positions/positions.types.ts +++ b/positions/positions.types.ts @@ -129,6 +129,10 @@ export type ApiReferencePositions = { map: ReferencePositionsMapping; }; +export type ApiBestCloneable = { + position: PositionQuery | null; +}; + export type ApiPositionDefault = { position: Address; collateral: Address;