Skip to content
Merged
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 @@ -75,6 +75,19 @@ export interface BootstrapReferenceRateService {

/** Port for reconciling market offers and invalidating the complete bootstrap strategy. */
export interface BootstrapMakeService {
/**
* Resolves the exact safe offer used for cross-market reservation planning.
* @param parameters - Desired offer and configured inclusive rate bounds.
* @returns The adjusted offer, or `undefined` when owned ladder liquidity covers it completely.
* @throws When current book evidence cannot prove a safe publication.
* @remarks Read-only and live adapters must use the same overlap rules; no mutation occurs.
*/
preview?(parameters: {
marketId: Hex
desiredOffer: BootstrapOffer
minimumRateBps: bigint
maximumRateBps: bigint
}): Promise<BootstrapOffer | undefined>
/**
* Reconciles one market's desired bootstrap offer or invalidates that market group.
* @param parameters - Canonical market, optional desired offer, and stable action reason.
Expand All @@ -87,6 +100,9 @@ export interface BootstrapMakeService {
reconcile(parameters: {
marketId: Hex
desiredOffer?: BootstrapOffer
maximumAssets?: bigint
minimumRateBps?: bigint
maximumRateBps?: bigint
reason:
| 'publish'
| 'replace'
Expand Down Expand Up @@ -191,6 +207,7 @@ type BootstrapRunPlan =
| {
config: BootstrapConfig
decision: PositionBootstrapDecision
plannedOfferAssets?: bigint
verbose?: BootstrapVerbosePlan
}
| { result: BootstrapRunResult }
Expand Down Expand Up @@ -564,9 +581,38 @@ export class PositionBootstrapService {
}
}

let plannedOfferAssets: bigint | undefined
if (decision.kind === 'publish' || decision.kind === 'replace') {
try {
plannedOfferAssets = this.make.preview
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
? ((
await this.make.preview({
marketId: config.marketId,
desiredOffer: decision.offer,
minimumRateBps: config.minimumRateBps,
maximumRateBps: config.maximumRateBps
})
)?.assets ?? 0n)
: decision.offer.assets
} catch (error) {
const halt = await this.haltStrategy(
config.marketId,
'decision',
error,
'bootstrap-decision-failed',
this.transactionObserver(parameters, verbose)
)
return [
...preflightResults(),
await this.withVerboseDetails(halt.result, verbose, undefined, true, halt.makeResult)
]
}
}

plans.push({
config,
decision,
...(plannedOfferAssets === undefined ? {} : { plannedOfferAssets }),
...(verbose
? {
verbose: {
Expand All @@ -588,7 +634,7 @@ export class PositionBootstrapService {
if (decision.kind === 'publish' || decision.kind === 'replace') {
const replacedAssets =
decision.kind === 'replace' ? (position.activeOffer?.assets ?? 0n) : 0n
const exposureDelta = decision.offer.assets - replacedAssets
const exposureDelta = (plannedOfferAssets ?? decision.offer.assets) - replacedAssets
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
reservedAssetsDelta += exposureDelta
reservedAssetsDeltaByMarket.set(config.marketId, marketReservationDelta + exposureDelta)
} else if (decision.kind === 'invalidate' && position.activeOffer) {
Expand Down Expand Up @@ -709,9 +755,15 @@ export class PositionBootstrapService {

let reconciliation: BootstrapMakeResult
try {
const desiredOffer = plan.plannedOfferAssets === 0n ? undefined : decision.offer
reconciliation = await this.make.reconcile({
marketId: config.marketId,
desiredOffer: decision.offer,
desiredOffer,
...(desiredOffer !== undefined &&
plan.plannedOfferAssets !== undefined &&
plan.plannedOfferAssets !== desiredOffer.assets
? { maximumAssets: plan.plannedOfferAssets }
: {}),
reason: decision.kind,
onTransactionSubmitted: this.transactionObserver(parameters, verbose, config.marketId)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ type QuoterBotMakeServices = {
/**
* Wraps both strategy make ports in one failure-tolerant serial mutation queue.
* @param services - Independently serialized bootstrap and ladder mutation ports.
* @returns Equivalent ports whose reconcile, hard-halt, and cleanup calls cannot overlap.
* @remarks Ladder state reads remain concurrent. Serializing writes across strategies prevents
* separate wallet nonce managers from submitting concurrently and ensures shutdown cleanups drain.
* @returns Equivalent ports whose reconcile, preview, hard-halt, and cleanup calls cannot overlap.
* @remarks Ladder state reads remain concurrent. Serializing bootstrap projections with writes keeps
* the prepared publication cache stable; serializing writes across strategies prevents separate
* wallet nonce managers from submitting concurrently and ensures shutdown cleanups drain.
*/
export const serializeQuoterBotWrites = (
services: QuoterBotMakeServices
Expand All @@ -22,6 +23,9 @@ export const serializeQuoterBotWrites = (

return {
bootstrap: {
...(services.bootstrap.preview
? { preview: parameters => enqueue(() => services.bootstrap.preview!(parameters)) }
: {}),
reconcile: parameters => enqueue(() => services.bootstrap.reconcile(parameters)),
hardHalt: parameters => enqueue(() => services.bootstrap.hardHalt(parameters)),
cleanup: parameters => enqueue(() => services.bootstrap.cleanup(parameters))
Expand Down
5 changes: 5 additions & 0 deletions bots/quoter-bot/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ const defaultState = async (config: ConfigService) => {
readOwnedGroupIds: async () => [
...new Set([...(await ownership.read()), ...(await ladderOwnership.readGroupIds())])
],
readBootstrapGroupIds: ownership.read,
readLadderSellGroupIds: async () =>
(await ladderOwnership.read()).flatMap(publication =>
publication.groups.filter(group => group.side === 'lower').map(group => group.groupId)
),
requestTimeoutMs: config.requestTimeoutMs
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type BootstrapBookOffer = {
maker: Address
buy: boolean
tick: bigint
/** Market maturity when the provider included a valid embedded market projection. */
maturity?: bigint
/** Maximum market continuous fee accepted by this offer. */
continuousFeeCap?: bigint
}
Expand Down Expand Up @@ -63,6 +65,11 @@ const parseOffer = (value: unknown, maker: Address): BootstrapBookOffer => {
throw new BootstrapAdapterError('offer-groups-response')
}
const offer = value as Record<string, unknown>
const market =
typeof offer.market === 'object' && offer.market !== null
? (offer.market as Record<string, unknown>)
: undefined
const maturity = market?.maturity
if (
typeof offer.maker !== 'string' ||
typeof offer.buy !== 'boolean' ||
Expand All @@ -84,6 +91,9 @@ const parseOffer = (value: unknown, maker: Address): BootstrapBookOffer => {
maker,
buy: offer.buy,
tick: BigInt(offer.tick),
...(typeof maturity === 'number' && Number.isSafeInteger(maturity)
? { maturity: BigInt(maturity) }
: {}),
continuousFeeCap: unsignedDecimal(offer.continuous_fee_cap)
}
}
Expand Down Expand Up @@ -265,13 +275,23 @@ export const bootstrapReservedLoanAssets = (
*/
export const bootstrapBookOffers = (groups: readonly BootstrapRawGroup[]) => {
const visitedGroups = new Set<Hex>()
const offers = new Map<string, BootstrapBookOffer & { groupId: Hex }>()
const offers = new Map<
string,
BootstrapBookOffer & {
groupId: Hex
remainingAssets: bigint
}
>()
for (const group of groups) {
if (visitedGroups.has(group.id)) continue
visitedGroups.add(group.id)
for (const offer of group.offers) {
const key = `${group.id}:${offer.marketId}:${offer.buy ? 'buy' : 'sell'}:${offer.tick}`
offers.set(key, { ...offer, groupId: group.id })
offers.set(key, {
...offer,
groupId: group.id,
remainingAssets: group.maxAssets - group.consumed
})
}
}
return [...offers.values()]
Expand Down
Loading
Loading