Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export interface LadderReferenceRateService {

/** Consumer-owned blocking make boundary for ladder reconciliation and safety invalidation. */
export interface LadderMakeService {
/**
* Invalidates durable strategy groups whose markets are no longer configured.
* @returns Completion after every removed-market group is canceled or already consumed.
* @throws When ownership cannot be read or complete cancellation cannot be confirmed.
* @remarks Implementations must be idempotent; composition may invoke this before readiness and
* again when a cycle starts. Read-only implementations may omit the operation.
*/
cleanupRemovedMarkets?(): Promise<void>
/**
* Reads the currently active strategy-owned quote set from live book truth.
* @param marketId - Canonical market identifier whose active roots must be reconstructed.
Expand Down Expand Up @@ -195,6 +203,7 @@ export class LadderMarketMakerService {
verbose?: boolean
onTransactionSubmitted?: (event: LadderTransactionSubmittedEvent) => void | Promise<void>
}): Promise<LadderMonitorReport> {
await this.make.cleanupRemovedMarkets?.()
if (this.configs.length === 0) {
throw new LadderConfigurationError(
'ladder',
Expand Down Expand Up @@ -309,6 +318,7 @@ export class LadderMarketMakerService {
* details. All publication and invalidation side effects pass exclusively through `make`.
*/
async runOnce(parameters: LadderRunParameters = {}) {
await this.make.cleanupRemovedMarkets?.()
if (this.configs.length === 0) {
throw new LadderConfigurationError('ladder', 'requires at least one configured market')
}
Expand All @@ -334,26 +344,6 @@ export class LadderMarketMakerService {

const results: LadderRunResult[] = []
for (const config of this.configs) {
let market: LadderMarketState
try {
market = await this.positions.readMarket(config.marketId)
} catch (error) {
const { result, invalidation } = await this.failedMarketRead(config, error, parameters)
const submittedTransactions =
invalidation === undefined || invalidation === 'logged'
? undefined
: invalidation.submittedTransactions
results.push(
await this.completeResult(config, result, parameters, {
config,
currentState: { status: 'failed', errorName: operatorErrorName(error) },
...(submittedTransactions ? { submittedTransactions } : {})
})
)
if (result.status === 'halted') return results
continue
}

let active: LadderQuoteSet | undefined
try {
active = await this.make.readActive(config.marketId)
Expand All @@ -374,6 +364,26 @@ export class LadderMarketMakerService {
return results
}

let market: LadderMarketState
try {
market = await this.positions.readMarket(config.marketId)
} catch (error) {
const { result, invalidation } = await this.failedMarketRead(config, error, parameters)
const submittedTransactions =
invalidation === undefined || invalidation === 'logged'
? undefined
: invalidation.submittedTransactions
results.push(
await this.completeResult(config, result, parameters, {
config,
currentState: { status: 'failed', errorName: operatorErrorName(error) },
...(submittedTransactions ? { submittedTransactions } : {})
})
)
if (result.status === 'halted') return results
continue
}

const currentState: LadderVerboseState = {
status: 'observed',
market,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export const serializeMarketMakingWrites = (
readActive: marketId => services.ladder.readActive(marketId),
reconcile: parameters => enqueue(() => services.ladder.reconcile(parameters)),
hardHalt: parameters => enqueue(() => services.ladder.hardHalt(parameters)),
cleanup: parameters => enqueue(() => services.ladder.cleanup(parameters))
cleanup: parameters => enqueue(() => services.ladder.cleanup(parameters)),
cleanupRemovedMarkets: services.ladder.cleanupRemovedMarkets
? () => enqueue(() => services.ladder.cleanupRemovedMarkets!())
: undefined
}
}
}
31 changes: 23 additions & 8 deletions bots/market-making/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,20 @@ const defaultState = async (config: ConfigService) => {
* @param environment - Environment map used for lazy validated configuration.
* @param dependencies - Optional state and workflow-port factories used by isolated tests.
* @returns An application exposing a single asynchronous CLI `run` boundary.
* @remarks Composition is side-effect free: configuration and providers are built lazily per
* command, writer commands gate on readiness first, and `--readonly` swaps every mutation port for
* terminal output. `start` shares one cross-strategy mutation queue so separate writer adapters
* cannot race signer nonces.
* @remarks Composition is side-effect free. Configuration and provider construction occur lazily
* for `setup-check`, `bootstrap`, `ladder`, `start`, or `invalidate`. Setup is read-only and preserves
* concurrent independent reads through `Promise.all`. `--readonly` selects address-only identity
* before any private-key validation and replaces every workflow mutation port with terminal output.
* Writer commands first clean up durable ladder groups for removed markets, then assert readiness
* before running their application service. Setup monitoring emits read-only readiness reports
* at a one-minute cadence and halts nonzero on the first failed report. Bootstrap monitoring uses
* the same cadence and invalidates strategy-owned groups after its shutdown signal. Ladder
* monitoring uses the shortest configured ladder cadence and invalidates active owned ladder groups
* after shutdown. `start` gates readiness once, launches all three monitors concurrently, and uses
* one cross-strategy mutation queue so separate writer adapters cannot race signer nonces. Explicit
* invalidation uses a narrower cancellation preflight so unknown maker
* groups can be removed without weakening normal readiness. One-shot writers run only for their
* respective explicit commands.
*/
export const createApplication = (
environment: Environment = Bun.env,
Expand Down Expand Up @@ -182,6 +192,9 @@ export const createApplication = (
},
async options => {
const config = await loadConfig(options)
const ladderAdapters = await (dependencies.createLadderAdapters?.(config) ??
createProductionLadderAdapters(config))
if (!config.readOnly) await ladderAdapters.make.cleanupRemovedMarkets?.()
const state = dependencies.createState?.(config) ?? (await defaultState(config))
await new SetupCheckService(state, config.setup, config.readOnly).assertReady()
const injectedAdapters = dependencies.createBootstrapAdapters?.(config)
Expand All @@ -201,10 +214,11 @@ export const createApplication = (
},
async options => {
const config = await loadConfig(options)
const state = dependencies.createState?.(config) ?? (await defaultState(config))
await new SetupCheckService(state, config.setup, config.readOnly).assertReady()
const adapters = await (dependencies.createLadderAdapters?.(config) ??
createProductionLadderAdapters(config))
if (!config.readOnly) await adapters.make.cleanupRemovedMarkets?.()
const state = dependencies.createState?.(config) ?? (await defaultState(config))
await new SetupCheckService(state, config.setup, config.readOnly).assertReady()
const writeReadOnlyEvent = parseEventWriter(options.writeEvent)
const make = config.readOnly
? new ReadOnlyLadderMakeService(
Expand All @@ -223,6 +237,9 @@ export const createApplication = (
},
async options => {
const config = await loadConfig(options)
const ladderAdapters = await (dependencies.createLadderAdapters?.(config) ??
createProductionLadderAdapters(config))
if (!config.readOnly) await ladderAdapters.make.cleanupRemovedMarkets?.()
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
if (config.bootstrap.length === 0) {
throw new BootstrapConfigurationError(
'bootstrap',
Expand All @@ -248,8 +265,6 @@ export const createApplication = (
? new ReadOnlyBootstrapMakeService(readOnlyWriter(options.writeEvent))
: bootstrapAdapters.make

const ladderAdapters = await (dependencies.createLadderAdapters?.(config) ??
createProductionLadderAdapters(config))
const ladderMake = config.readOnly
? new ReadOnlyLadderMakeService(
ladderAdapters.make,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { BootstrapOffer } from '../../domain/bootstrap/position-bootstrap'
import type { BootstrapActiveGroup, BootstrapInventoryReader } from './bootstrap-position.service'

import { pendingLadderQuoteSets } from '../ladder/ladder-active-publication.utils'
import { readLadderBookOffers } from '../ladder/ladder-book.utils'
import { pendingLadderBuyReservations } from '../ladder/ladder-cash-reservation.utils'
import { createLadderGroupOwnership } from '../ladder/ladder-group-ownership.utils'
import { buildLadderTree } from '../ladder/ladder-offer.utils'
Expand Down Expand Up @@ -365,7 +366,15 @@ export const createProductionBootstrapAdapters = (
)
)
const completeBookOffers = async () => {
const [groups, ladderPublications] = await Promise.all([readGroups(), ladderOwnership.read()])
const [groups, ladderPublications, wholeBook] = await Promise.all([
readGroups(),
ladderOwnership.read(),
readLadderBookOffers({
baseUrl: config.morphoApiBaseUrl,
marketIds: config.setup.marketIds,
timeoutMs: config.requestTimeoutMs
})
])
const pendingLadderOffers = (
await Promise.all(
pendingLadderQuoteSets(ladderPublications, groups).map(async quote => {
Expand All @@ -386,7 +395,7 @@ export const createProductionBootstrapAdapters = (
return {
groups,
ladderPublications,
book: [...bootstrapBookOffers(groups), ...pendingLadderOffers]
book: [...wholeBook, ...bootstrapBookOffers(groups), ...pendingLadderOffers]
}
}
const prepareMempoolPublication = (
Expand Down
83 changes: 83 additions & 0 deletions bots/market-making/src/infrastructure/ladder/ladder-book.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Hex } from 'viem'

import { bytesToHex, hexToBytes, isHex, size } from 'viem'

import type { JsonRequest } from '../setup-state/http-json.utils'

import { requestJson } from '../setup-state/http-json.utils'
import { LadderAdapterError } from './ladder-adapter.error'

const MAX_ITEMS = 1_000

const bytes32 = (value: unknown) => {
if (typeof value !== 'string' || !isHex(value, { strict: true }) || size(value) !== 32) {
throw new LadderAdapterError('book-response')
}
return bytesToHex(hexToBytes(value))
}

/**
* Reads both takeable sides of every configured market through the Router whole-book boundary.
* @param parameters - Router origin, configured market IDs, deadline, and optional request boundary.
* @returns Every active market offer required for negative-spread validation.
* @throws `LadderAdapterError` when a provider response is malformed or exceeds the endpoint bound.
*/
export const readLadderBookOffers = async (parameters: {
baseUrl: string
marketIds: readonly Hex[]
timeoutMs: number
request?: JsonRequest
}) => {
const request = parameters.request ?? requestJson
const deadline = performance.now() + parameters.timeoutMs
const readSide = async (marketId: Hex, side: 'asks' | 'bids') => {
const remainingMs = Math.floor(deadline - performance.now())
if (remainingMs <= 0) throw new LadderAdapterError('book-timeout')
const rawResponse = await request(
`${parameters.baseUrl}/v0/midnight/books/${marketId}/${side}/takeable-offers`,
'morpho-api',
Math.min(parameters.timeoutMs, remainingMs)
)
if (typeof rawResponse !== 'object' || rawResponse === null || Array.isArray(rawResponse)) {
throw new LadderAdapterError('book-response')
}
const response = rawResponse as { data?: unknown }
if (!Array.isArray(response.data) || response.data.length > MAX_ITEMS) {
throw new LadderAdapterError('book-response')
}
return { marketId, side, values: response.data }
}
const pages = await Promise.all(
parameters.marketIds.flatMap(marketId =>
(['asks', 'bids'] as const).map(side => readSide(marketId, side))
)
)
return pages.flatMap(({ marketId, side, values }) =>
values.map(value => {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new LadderAdapterError('book-response')
}
const row = value as Record<string, unknown>
const offer = row.offer
if (typeof offer !== 'object' || offer === null || Array.isArray(offer)) {
throw new LadderAdapterError('book-response')
}
const raw = offer as Record<string, unknown>
if (typeof raw.tick !== 'number' || !Number.isSafeInteger(raw.tick)) {
throw new LadderAdapterError('book-response')
}
const expectedBuy = side === 'bids'
if (typeof raw.buy !== 'boolean' || raw.buy !== expectedBuy) {
throw new LadderAdapterError('book-response')
}
const returnedMarketId = bytes32(row.market_id)
if (returnedMarketId !== marketId) throw new LadderAdapterError('book-response')
return {
groupId: bytes32(raw.group),
marketId: returnedMarketId,
buy: expectedBuy,
tick: BigInt(raw.tick)
}
})
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Hex } from 'viem'

/** Inputs required to derive replacement-aware ladder inventory capacities. */
type LadderCapacityParameters = {
marketId: Hex
balance: bigint
currentCredit: bigint
otherMarketCredit: bigint
creditSaleCapacityAssets: bigint
targetMarketExposureAssets: bigint
maximumTotalExposureAssets: bigint
reservations: readonly { id: Hex; marketIds: readonly Hex[]; assets: bigint }[]
}

/**
* Derives market and aggregate ladder capacity without double-reserving replaced groups.
* @param parameters - Wallet balance, accrued credit, active reservations, and exposure limits.
* @returns Fresh side, market, and total capacities.
*/
export const calculateLadderCapacities = (parameters: LadderCapacityParameters) => {
const reserved = parameters.reservations.reduce((sum, item) => sum + item.assets, 0n)
const marketReserved = parameters.reservations
.filter(item => item.marketIds.includes(parameters.marketId))
.reduce((sum, item) => sum + item.assets, 0n)
const unreservedCash = parameters.balance > reserved ? parameters.balance - reserved : 0n
const currentExposure = parameters.currentCredit + parameters.otherMarketCredit + reserved
const totalLendRoom =
parameters.maximumTotalExposureAssets > currentExposure
? parameters.maximumTotalExposureAssets - currentExposure
: 0n
const currentMarketExposure = parameters.currentCredit + marketReserved
const marketLendRoom =
parameters.targetMarketExposureAssets > currentMarketExposure
? parameters.targetMarketExposureAssets - currentMarketExposure
: 0n
const lendRoom = totalLendRoom < marketLendRoom ? totalLendRoom : marketLendRoom
const cash = unreservedCash < lendRoom ? unreservedCash : lendRoom
const credit =
parameters.currentCredit < parameters.creditSaleCapacityAssets
? parameters.currentCredit
: parameters.creditSaleCapacityAssets
const market = cash + credit
const total = totalLendRoom + credit
return {
lowerRateCapacityAssets: credit,
higherRateCapacityAssets: cash,
targetMarketCapacityAssets: market,
maximumTotalCapacityAssets: total
}
}
Loading
Loading