Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
51 changes: 37 additions & 14 deletions sdk/packages/simplex/src/bin/simplex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ interface FxStrategyConfig {
maxOrderUsd: string
/** Map of chain identifier (e.g. "EVM-97") to exotic token contract address */
exoticTokenAddresses: Record<string, HexString>
/** Optional per-chain confirmation policies for cross-chain orders */
confirmationPolicies?: Record<string, ChainConfirmationPolicy>
}

type StrategyConfig = BasicStrategyConfig | FxStrategyConfig
Expand Down Expand Up @@ -387,20 +389,24 @@ program
confirmationPolicy,
)
}
case "hyperfx": {
const bidPricePolicy = new FillerPricePolicy({ points: strategyConfig.bidPriceCurve })
const askPricePolicy = new FillerPricePolicy({ points: strategyConfig.askPriceCurve })
return new FXFiller(
privateKey,
configService,
chainClientManager,
contractService,
bidPricePolicy,
askPricePolicy,
strategyConfig.maxOrderUsd,
strategyConfig.exoticTokenAddresses,
)
}
case "hyperfx": {
const bidPricePolicy = new FillerPricePolicy({ points: strategyConfig.bidPriceCurve })
const askPricePolicy = new FillerPricePolicy({ points: strategyConfig.askPriceCurve })
const fxConfirmationPolicy = strategyConfig.confirmationPolicies
? new ConfirmationPolicy(strategyConfig.confirmationPolicies)
: undefined
return new FXFiller(
privateKey,
configService,
chainClientManager,
contractService,
bidPricePolicy,
askPricePolicy,
strategyConfig.maxOrderUsd,
strategyConfig.exoticTokenAddresses,
fxConfirmationPolicy,
)
}
default:
throw new Error(`Unknown strategy type: ${(strategyConfig as StrategyConfig).type}`)
}
Expand Down Expand Up @@ -609,6 +615,23 @@ function validateConfig(config: FillerTomlConfig): void {
if (!strategy.exoticTokenAddresses || Object.keys(strategy.exoticTokenAddresses).length === 0) {
throw new Error("FX strategy must have at least one entry in 'exoticTokenAddresses'")
}

if (strategy.confirmationPolicies) {
for (const [chainId, policy] of Object.entries(strategy.confirmationPolicies)) {
if (!policy.points || !Array.isArray(policy.points) || policy.points.length < 2) {
throw new Error(
`FX confirmation policy for chain ${chainId} must have a 'points' array with at least 2 points`,
)
}
for (const point of policy.points) {
if (point.amount === undefined || point.value === undefined) {
throw new Error(
`Each point in FX confirmation policy for chain ${chainId} must have 'amount' and 'value'`,
)
}
}
}
}
}
}
}
Expand Down
52 changes: 23 additions & 29 deletions sdk/packages/simplex/src/core/filler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,26 +283,22 @@ export class IntentFiller {
// Base layer: stable-only USD value from ContractInteractionService
const baseInputUsd = await this.contractService.getInputUsdValue(order)

// Strategy layer: first strategy that can price the order wins
let inputUsdValue = baseInputUsd
const canFillCache = new Map<FillerStrategy, boolean>()
for (const strategy of this.strategies) {
// Skip strategies that cannot price inputs
if (typeof strategy.getOrderUsdValue !== "function") continue

let canFill = false
try {
canFill = await strategy.canFill(order)
canFillCache.set(strategy, await strategy.canFill(order))
} catch (err) {
this.logger.error(
{ orderId: order.id, strategy: strategy.name, err },
"Error checking canFill during inputUsdValue computation",
)
this.logger.error({ orderId: order.id, strategy: strategy.name, err }, "Error checking canFill")
canFillCache.set(strategy, false)
}
canFillCache.set(strategy, canFill)
if (!canFill) continue
}

let inputUsdValue = baseInputUsd
for (const [strategy, canFill] of canFillCache) {
if (!canFill || typeof strategy.getOrderUsdValue !== "function") continue
try {
const stratValue = await strategy.getOrderUsdValue(order)

if (stratValue != null) {
inputUsdValue = Decimal.max(baseInputUsd, stratValue.inputUsd)
break
Expand All @@ -315,17 +311,19 @@ export class IntentFiller {
}
}

// Derive required confirmations from whichever matched strategy has a policy
const isCrossChain = order.source !== order.destination
let requiredConfirmations = 0
for (const [strategy, canFill] of canFillCache) {
if (!canFill || !strategy.confirmationPolicy) continue
requiredConfirmations = Math.max(
requiredConfirmations,
strategy.confirmationPolicy.getConfirmationBlocks(
getChainId(order.source)!,
inputUsdValue.toNumber(),
),
)
if (isCrossChain) {
for (const [strategy, canFill] of canFillCache) {
if (!canFill || !strategy.confirmationPolicy) continue
requiredConfirmations = Math.max(
requiredConfirmations,
strategy.confirmationPolicy.getConfirmationBlocks(
getChainId(order.source)!,
inputUsdValue.toNumber(),
),
)
}
}

// Run confirmation waiting and evaluation in parallel.
Expand Down Expand Up @@ -423,8 +421,7 @@ export class IntentFiller {

const eligibleStrategies = await Promise.all(
this.strategies.map(async (strategy) => {
const canFill = canFillCache.has(strategy) ? canFillCache.get(strategy)! : await strategy.canFill(order)
if (!canFill) return null
if (!canFillCache.get(strategy)) return null

const profitability = await strategy.calculateProfitability(order)
return { strategy, profitability }
Expand Down Expand Up @@ -471,10 +468,7 @@ export class IntentFiller {
try {
if (solverSelectionActive) {
this.contractService.ensureEntryPointDeposit(order).catch((err) => {
this.logger.error(
{ orderId: order.id, err },
"Background EntryPoint deposit top-up failed",
)
this.logger.error({ orderId: order.id, err }, "Background EntryPoint deposit top-up failed")
})
}

Expand Down
47 changes: 47 additions & 0 deletions sdk/packages/simplex/src/services/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,22 @@ interface FillerOutputsCache {
timestamp: number
}

export interface CachedPairClassification {
inputIsStable: boolean
stableToken: string
exoticToken: string
}

interface PairClassificationsCache {
pairs: CachedPairClassification[]
timestamp: number
}

interface CacheData {
gasEstimates: Record<string, GasEstimateCache>
swapOperations: Record<string, SwapOperationsCache>
fillerOutputs: Record<string, FillerOutputsCache>
pairClassifications: Record<string, PairClassificationsCache>
feeTokens: Record<string, { address: HexString; decimals: number }>
perByteFees: Record<string, Record<string, bigint>>
tokenDecimals: Record<string, Record<HexString, number>>
Expand All @@ -57,6 +69,7 @@ export class CacheService {
gasEstimates: {},
swapOperations: {},
fillerOutputs: {},
pairClassifications: {},
feeTokens: {},
perByteFees: {},
tokenDecimals: {},
Expand Down Expand Up @@ -95,6 +108,15 @@ export class CacheService {
staleFillerOutputIds.forEach((orderId) => {
delete this.cacheData.fillerOutputs[orderId]
})

// Clean up pair classifications
const stalePairIds = Object.entries(this.cacheData.pairClassifications)
.filter(([_, data]) => !this.isCacheValid(data.timestamp))
.map(([orderId]) => orderId)

stalePairIds.forEach((orderId) => {
delete this.cacheData.pairClassifications[orderId]
})
}

getGasEstimate(orderId: string): {
Expand Down Expand Up @@ -231,6 +253,31 @@ export class CacheService {
}
}

getPairClassifications(orderId: string): CachedPairClassification[] | null {
try {
const cache = this.cacheData.pairClassifications[orderId]
if (cache && this.isCacheValid(cache.timestamp)) {
return cache.pairs
}
return null
} catch (error) {
this.logger.error({ err: error }, "Error getting pair classifications")
return null
}
}

setPairClassifications(orderId: string, pairs: CachedPairClassification[]): void {
try {
this.cacheData.pairClassifications[orderId] = {
pairs,
timestamp: Date.now(),
}
} catch (error) {
this.logger.error({ err: error }, "Error setting pair classifications")
throw error
}
}

getFeeTokenWithDecimals(chain: string): { address: HexString; decimals: number } | null {
try {
const cache = this.cacheData.feeTokens[chain]
Expand Down
Loading
Loading