Skip to content
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