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
1 change: 1 addition & 0 deletions docs/content/developers/intent-gateway/simplex.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ logging = "info" # trace | debug | info | warn |
# Required for HyperFX / solver selection
substratePrivateKey = "0x.." # Hex seed or mnemonic for Hyperbridge extrinsics
hyperbridgeWsUrl = "wss://nexus.ibp.network" # Hyperbridge WebSocket endpoint
indexerUrl = "https://nexus.indexer.polytope.technology" # Hyperbridge Indexer Endpoint

[simplex.queue]
maxRechecks = 10
Expand Down
2 changes: 2 additions & 0 deletions sdk/packages/simplex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
"decimal.js": "^10.5.0",
"dotenv": "^16.4.7",
"ethers": "^5.7.2",
"graphql": "^16.13.1",
"graphql-request": "^7.4.0",
"jsbi": "^4.3.2",
"p-queue": "^8.1.0",
"pino": "^9.9.5",
Expand Down
22 changes: 22 additions & 0 deletions sdk/packages/simplex/src/bin/simplex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { RebalancingService } from "@/services/RebalancingService"
import { getLogger, configureLogger, type LogLevel } from "@/services/Logger"
import { CacheService } from "@/services/CacheService"
import { BidStorageService } from "@/services/BidStorageService"
import { LimitOrderStorageService } from "@/services/LimitOrderStorageService"
import { MissedOrderRecoveryService } from "@/services/MissedOrderRecoveryService"
import { initializeSignerFromToml, type SignerConfig } from "@/services/wallet"
import { MetricsService } from "@/services/MetricsService"
import type { BinanceCexConfig } from "@/services/rebalancers/index"
Expand Down Expand Up @@ -179,6 +181,8 @@ interface FillerTomlConfig {
solverAccountContractAddress?: string
/** Target gas units for EntryPoint deposits per chain. Defaults to 3,000,000. */
targetGasUnits?: number
/** Hyperbridge indexer GraphQL endpoint for missed order recovery on startup. */
indexerUrl?: string
}
strategies: StrategyConfig[]
chains: UserProvidedChainConfig[]
Expand Down Expand Up @@ -305,6 +309,10 @@ program
"Bid storage initialized for fund recovery tracking",
)

// Initialize limit order storage for deferred order tracking
const limitOrderStorage = new LimitOrderStorageService(configService.getDataDir())
logger.info("Limit order storage initialized for deferred order tracking")

// Initialize strategies with shared services
logger.info("Initializing strategies...")
const strategies = config.strategies.map((strategyConfig) => {
Expand Down Expand Up @@ -414,6 +422,18 @@ program
logger.info("Rebalancing service initialized")
}

// Initialize missed order recovery service if indexer URL is configured
let missedOrderRecovery: MissedOrderRecoveryService | undefined
if (config.simplex.indexerUrl) {
missedOrderRecovery = new MissedOrderRecoveryService(
config.simplex.indexerUrl,
limitOrderStorage,
strategies,
chainClientManager,
)
logger.info({ indexerUrl: config.simplex.indexerUrl }, "Missed order recovery service initialized")
}

// Initialize and start the intent filler
logger.info("Starting intent filler...")
const intentFiller = new IntentFiller(
Expand All @@ -426,6 +446,8 @@ program
runtimeSigner,
rebalancingService,
bidStorageService,
limitOrderStorage,
missedOrderRecovery,
)

// Initialize (sets up EIP-7702 delegation if solver selection is configured)
Expand Down
144 changes: 144 additions & 0 deletions sdk/packages/simplex/src/core/filler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
ChainClientManager,
ContractInteractionService,
DelegationService,
LimitOrderStorageService,
MissedOrderRecoveryService,
RebalancingService,
} from "@/services"
import { FillerConfigService } from "@/services/FillerConfigService"
Expand All @@ -32,10 +34,13 @@ export class IntentFiller {
private delegationService?: DelegationService
private rebalancingService?: RebalancingService
private bidStorage?: BidStorageService
private limitOrderStorage?: LimitOrderStorageService
private missedOrderRecovery?: MissedOrderRecoveryService
private retractionQueue: pQueue
private pendingRetractions = new Set<string>()
private rebalancingInterval?: NodeJS.Timeout
private retractionSweepInterval?: NodeJS.Timeout
private limitOrderSweepInterval?: NodeJS.Timeout
private hyperbridge: Promise<IntentsCoprocessor> | undefined = undefined
private config: FillerConfig
private configService: FillerConfigService
Expand All @@ -53,6 +58,8 @@ export class IntentFiller {
signer: SigningAccount,
rebalancingService?: RebalancingService,
bidStorage?: BidStorageService,
limitOrderStorage?: LimitOrderStorageService,
missedOrderRecovery?: MissedOrderRecoveryService,
) {
this.configService = configService
this.signer = signer
Expand All @@ -61,6 +68,8 @@ export class IntentFiller {
this.contractService = contractService
this.rebalancingService = rebalancingService
this.bidStorage = bidStorage
this.limitOrderStorage = limitOrderStorage
this.missedOrderRecovery = missedOrderRecovery
this.monitor = new EventMonitor(chainConfigs, configService, this.chainClientManager, this.fillerAddress)
this.strategies = strategies
this.config = config
Expand Down Expand Up @@ -135,6 +144,14 @@ export class IntentFiller {
}
}
}

// Recover orders placed while the filler was offline (non-blocking)
if (this.missedOrderRecovery) {
this.missedOrderRecovery.recover().then(
(recovered) => this.logger.info({ recovered }, "Missed order recovery complete"),
(err) => this.logger.error({ err }, "Missed order recovery failed"),
)
}
}

/**
Expand All @@ -159,6 +176,10 @@ export class IntentFiller {
if (this.bidStorage && this.hyperbridge) {
this.startRetractionSweep()
}

if (this.limitOrderStorage) {
this.startLimitOrderSweep()
}
}

/**
Expand Down Expand Up @@ -242,9 +263,117 @@ export class IntentFiller {
}
}

private startLimitOrderSweep(): void {
const SWEEP_INTERVAL_MS = 60_000 // 1 minute

this.limitOrderSweepInterval = setInterval(() => {
this.sweepLimitOrders().catch((error) => {
this.logger.error({ error }, "Error in limit order sweep")
})
}, SWEEP_INTERVAL_MS)

this.logger.info("Periodic limit order sweep started (every 60 seconds)")
}

private async sweepLimitOrders(): Promise<void> {
if (!this.limitOrderStorage) return

const pendingOrders = this.limitOrderStorage.getPendingLimitOrders()
if (pendingOrders.length === 0) return

this.logger.debug({ count: pendingOrders.length }, "Checking pending limit orders")

// Batch getBlockNumber calls per destination chain
const blockNumberCache = new Map<string, bigint>()

for (const stored of pendingOrders) {
try {
// Check deadline using cached block numbers per chain
let currentBlock = blockNumberCache.get(stored.destinationChain)
if (currentBlock === undefined) {
const destClient = this.chainClientManager.getPublicClient(stored.destinationChain)
currentBlock = await destClient.getBlockNumber()
blockNumberCache.set(stored.destinationChain, currentBlock)
}

const deadlineBlock = BigInt(stored.deadlineBlock)
if (currentBlock >= deadlineBlock) {
this.limitOrderStorage.deleteLimitOrder(stored.orderId)
this.logger.info(
{ orderId: stored.orderId, currentBlock: currentBlock.toString(), deadline: stored.deadlineBlock },
"Limit order expired (deadline passed)",
)
continue
}

// Find the matching strategy
const strategy = this.strategies.find((s) => s.name === stored.strategyName)
if (!strategy) {
this.limitOrderStorage.deleteLimitOrder(stored.orderId)
this.logger.warn(
{ orderId: stored.orderId, strategyName: stored.strategyName },
"Limit order strategy no longer available, removing",
)
continue
}

const order = this.limitOrderStorage.deserializeOrder(stored.orderJson)

// Check if the order has already been filled on-chain
try {
const gateway = await this.contractService.getIntentGateway(order.source, order.destination)
const isFilled = await gateway.isOrderFilled(order)
if (isFilled) {
this.limitOrderStorage.deleteLimitOrder(stored.orderId)
this.logger.info({ orderId: stored.orderId }, "Limit order already filled on-chain, removing")
continue
}
} catch (err) {
this.logger.warn({ orderId: stored.orderId, err }, "Failed to check if order is filled, proceeding with re-evaluation")
}

// Re-evaluate profitability
const profitability = await strategy.calculateProfitability(order)
if (profitability <= 0) {
continue // Still not profitable, keep for next sweep
}

this.logger.info(
{ orderId: stored.orderId, profitability },
"Limit order now profitable, submitting for execution",
)

this.limitOrderStorage.deleteLimitOrder(stored.orderId)

// Determine solver selection state and input USD value
const solverSelectionActive = this.contractService.getCache().getSolverSelection(order.destination)
if (solverSelectionActive && !this.hyperbridge) {
this.logger.warn({ orderId: stored.orderId }, "Solver selection active but no Hyperbridge, skipping")
continue
}

const baseInputUsd = await this.contractService.getInputUsdValue(order)
let inputUsdValue = baseInputUsd
if (typeof strategy.getOrderUsdValue === "function") {
const stratValue = await strategy.getOrderUsdValue(order)
if (stratValue) {
inputUsdValue = Decimal.max(baseInputUsd, stratValue.inputUsd)
}
}

this.executeOrder(order, strategy, solverSelectionActive ?? false, inputUsdValue, profitability)
} catch (error) {
this.logger.error({ orderId: stored.orderId, err: error }, "Error processing limit order")
}
}
}

public async stop(): Promise<void> {
this.monitor.stopListening()

// Record shutdown time for missed order recovery on next startup
this.limitOrderStorage?.setLastShutdownTime(new Date().toISOString())

// Stop rebalancing interval
if (this.rebalancingInterval) {
clearInterval(this.rebalancingInterval)
Expand All @@ -258,6 +387,12 @@ export class IntentFiller {
this.logger.info("Periodic retraction sweep stopped")
}

if (this.limitOrderSweepInterval) {
clearInterval(this.limitOrderSweepInterval)
this.limitOrderSweepInterval = undefined
this.logger.info("Periodic limit order sweep stopped")
}

// Wait for all queues to complete
const promises: Promise<void>[] = []
this.chainQueues.forEach((queue) => {
Expand Down Expand Up @@ -415,6 +550,15 @@ export class IntentFiller {
// Execute immediately
if (evaluationResult) {
this.executeOrder(order, evaluationResult.strategy, solverSelectionActive, inputUsdValue, evaluationResult.profitability)
} else if (this.limitOrderStorage) {
// Store as limit order if any FXFiller strategy could fill but wasn't profitable
for (const [strategy, canFill] of canFillCache) {
if (canFill && strategy.name === "FXFiller") {
this.limitOrderStorage.storeLimitOrder(order, strategy.name)
this.logger.info({ orderId: order.id }, "Order stored as limit order for future re-evaluation")
break
}
}
}
} catch (error) {
this.logger.error({ orderId: order.id, err: error }, "Error processing order")
Expand Down
Loading
Loading