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
2 changes: 1 addition & 1 deletion sdk/packages/simplex/scripts/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FROM node:22

# Install dependencies required for builds
RUN apt-get update && apt-get install -y python3 make g++ git curl protobuf-compiler --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y python3 make g++ git curl protobuf-compiler libprotobuf-dev --no-install-recommends && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -g 1001 simplex && \
Expand Down
1 change: 1 addition & 0 deletions sdk/packages/simplex/scripts/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-admin-api"
networks:
- simplex-network
ports:
Expand Down
14 changes: 14 additions & 0 deletions sdk/packages/simplex/scripts/generate-proto.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"

echo "Generating TypeScript from proto files..."
# Locate well-known proto includes (wrappers.proto, descriptor.proto, etc.)
WELL_KNOWN_PROTOS=""
for candidate in "$(dirname "$(command -v protoc)" 2>/dev/null)/../include" /usr/include /usr/local/include; do
if [ -f "$candidate/google/protobuf/wrappers.proto" ]; then
WELL_KNOWN_PROTOS="$candidate"
break
fi
done
if [ -z "$WELL_KNOWN_PROTOS" ]; then
echo "Error: Could not find Google well-known proto includes (google/protobuf/wrappers.proto)"
exit 1
fi

protoc \
--plugin="protoc-gen-ts_proto=$PLUGIN" \
--ts_proto_out="$OUT_DIR" \
Expand All @@ -78,6 +91,7 @@ protoc \
--ts_proto_opt=useExactTypes=false \
--ts_proto_opt=forceLong=string \
-I="$PROTO_DIR" \
-I="$WELL_KNOWN_PROTOS" \
"$PROTO_DIR"/mpcvault/platform/v1/api.proto \
"$PROTO_DIR"/mpcvault/platform/v1/error.proto

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 },
"targets": [
{
"expr": "sum by (chain_id) (increase(simplex_order_volume_usd_total[$__range]))",
"expr": "sum by (chain_id) (simplex_order_volume_usd_total)",
"legendFormat": "chain {{chain_id}}",
"instant": false,
"range": true,
Expand All @@ -214,7 +214,7 @@
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 },
"targets": [
{
"expr": "sum by (chain_id) (increase(simplex_order_profit_usd_total[$__range]))",
"expr": "sum by (chain_id) (simplex_order_profit_usd_total)",
"legendFormat": "chain {{chain_id}}",
"instant": false,
"range": true,
Expand Down
2 changes: 1 addition & 1 deletion sdk/packages/simplex/scripts/monitoring/prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ global:
scrape_configs:
- job_name: "simplex"
static_configs:
- targets: ["simplex:9090"]
- targets: ["hyperbridge-simplex:9090"]
16 changes: 14 additions & 2 deletions sdk/packages/simplex/src/bin/simplex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,12 @@ interface FillerTomlConfig {
pendingQueue: PendingQueueConfig
logging?: LoggingConfig
watchOnly?: boolean | Record<string, boolean>
substratePrivateKey?: string
hyperbridgeWsUrl?: string
substratePrivateKey: string
hyperbridgeWsUrl: string
entryPointAddress?: string
solverAccountContractAddress?: string
/** Target gas units for EntryPoint deposits per chain. Defaults to 3,000,000. */
targetGasUnits?: number
}
strategies: StrategyConfig[]
chains: (UserProvidedChainConfig & { bundlerUrl?: string })[]
Expand Down Expand Up @@ -317,6 +319,7 @@ program
entryPointAddress: config.simplex.entryPointAddress,
dataDir: options.dataDir,
rebalancing: config.rebalancing,
targetGasUnits: config.simplex.targetGasUnits,
}

const configService = new FillerConfigService(fillerChainConfigs, fillerConfigForService)
Expand Down Expand Up @@ -489,6 +492,7 @@ program
exoticTokenAddresses,
hyperbridgeWsUrl: config.simplex.hyperbridgeWsUrl,
substratePrivateKey: config.simplex.substratePrivateKey,
dataDir: options.dataDir,
})
metrics.start(metricsPort, metricsHost)
}
Expand Down Expand Up @@ -557,6 +561,14 @@ function validateConfig(config: FillerTomlConfig): void {
throw new Error("Signer configuration is required via [simplex.signer]")
}

if (!config.simplex?.substratePrivateKey) {
throw new Error("simplex.substratePrivateKey is required")
}

if (!config.simplex?.hyperbridgeWsUrl) {
throw new Error("simplex.hyperbridgeWsUrl is required")
}

if ((!config.strategies || config.strategies.length === 0) && !allChainsWatchOnly) {
throw new Error("At least one strategy must be configured (unless all chains are in watchOnly mode)")
}
Expand Down
34 changes: 23 additions & 11 deletions sdk/packages/simplex/src/core/filler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ export class IntentFiller {
}

/**
* Initializes the filler, including setting up EIP-7702 delegation if solver selection is active on any chain.
* This should be called before start().
* Initializes the filler, including setting up EIP-7702 delegation and
* depositing the target amount to the EntryPoint on chains where solver
* selection is active. This should be called before start().
*/
public async initialize(): Promise<void> {
// Check which chains have solver selection active
Expand All @@ -123,6 +124,16 @@ export class IntentFiller {
if (!result.success) {
this.logger.warn({ results: result.results }, "Some chains failed EIP-7702 delegation setup")
}

// Ensure EntryPoint deposit covers target gas units on each chain
const targetGasUnits = this.configService.getTargetGasUnits()
for (const chain of chainsWithSolverSelection) {
try {
await this.contractService.topUpEntryPointDeposit(chain, targetGasUnits)
} catch (err) {
this.logger.error({ chain, err }, "Failed to deposit to EntryPoint at startup")
}
}
}
}

Expand Down Expand Up @@ -257,9 +268,6 @@ export class IntentFiller {

await Promise.all(promises)

// Withdraw any remaining EntryPoint deposits back to the solver EOA
await this.contractService.withdrawAllEntryPointDeposits()

// Disconnect shared Hyperbridge connection
if (this.hyperbridge) {
const service = await this.hyperbridge.catch(() => null)
Expand Down Expand Up @@ -500,18 +508,13 @@ 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")
})
}

const execStartMs = Date.now()
const hyperbridgeService = solverSelectionActive ? await this.hyperbridge : undefined
const result = await bestStrategy.executeOrder(order, hyperbridgeService)
const execDurationSec = (Date.now() - execStartMs) / 1000
this.monitor.emit("orderTiming", { orderId: order.id, phase: "execution", durationSec: execDurationSec })
this.logger.info({ orderId: order.id, result }, "Order execution completed")

if (result.success) {
this.monitor.emit("orderFilled", { orderId: order.id, hash: result.txHash, volumeUsd: inputUsdValue.toNumber(), profitUsd, chainId: getChainId(order.source) })
}
Expand Down Expand Up @@ -548,6 +551,15 @@ export class IntentFiller {
}

private handleOrderFilledOnChain(commitment: HexString, filler: string, chainId: number): void {
// Top up EntryPoint deposit if we were the filler
if (filler.toLowerCase() === this.fillerAddress.toLowerCase()) {
const chain = `EVM-${chainId}`
const targetGasUnits = this.configService.getTargetGasUnits()
this.contractService.topUpEntryPointDeposit(chain, targetGasUnits, 1_000_000n).catch((err) => {
this.logger.error({ commitment, chain, err }, "Post-fill EntryPoint deposit top-up failed")
})
}

if (!this.bidStorage || !this.hyperbridge) {
return
}
Expand Down
102 changes: 62 additions & 40 deletions sdk/packages/simplex/src/services/ContractInteractionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,75 +282,97 @@ export class ContractInteractionService {
}

/**
* Ensures the solver's EntryPoint deposit has enough native token to cover
* the estimated gas cost for a given order.
* Tops up the solver's EntryPoint deposit so it covers at least
* `targetGasUnits` at the current gas price. Skips if the wallet
* balance cannot afford at least 1M gas units (not enough to send txs).
*
* Uses cached gas estimates (from estimateGasFillPost) and, if the current
* deposit is insufficient, tops up by depositing 10% of the solver's EOA
* native balance on the destination chain.
* @param chain - The chain identifier
* @param targetGasUnits - Gas units the deposit should cover (default 3M)
* @param thresholdGasUnits - Only top up if deposit is below this many gas units (defaults to targetGasUnits)
*/
async ensureEntryPointDeposit(order: Order): Promise<void> {
if (!order.id) {
this.logger.warn({ destination: order.destination }, "Order has no ID, skipping EntryPoint deposit check")
async topUpEntryPointDeposit(chain: string, targetGasUnits: bigint = 3_000_000n, thresholdGasUnits?: bigint): Promise<void> {
const effectiveThreshold = thresholdGasUnits ?? targetGasUnits
const entryPointAddress = this.configService.getEntryPointAddress(chain)
if (!entryPointAddress) {
return
}

const gasEstimate = this.cacheService.getGasEstimate(order.id)
if (!gasEstimate) {
this.logger.warn(
{ orderId: order.id, destination: order.destination },
"No cached gas estimate found, skipping EntryPoint deposit check",
)
const publicClient = this.clientManager.getPublicClient(chain)
const [currentDeposit, solverBalance, gasPrice] = await Promise.all([
this.getSolverEntryPointBalance(chain),
publicClient.getBalance({ address: this.solverAccountAddress }),
publicClient.getGasPrice(),
])

if (gasPrice === 0n) {
this.logger.warn({ chain }, "Gas price is zero, skipping EntryPoint top-up")
return
}

const requiredNative = 3n * gasEstimate.totalGasCostWei
// Skip if wallet can't afford at least 1M gas units
const walletGasUnits = solverBalance / gasPrice
const minWalletGasUnits = 1_000_000n

const currentDeposit = await this.getSolverEntryPointBalance(order.destination)
if (walletGasUnits < minWalletGasUnits) {
this.logger.warn(
{
chain,
walletBalance: formatEther(solverBalance),
walletGasUnits: walletGasUnits.toString(),
gasPrice: gasPrice.toString(),
},
"Wallet balance too low to afford minimum gas, skipping EntryPoint top-up",
)
return
}

this.logger.debug(
{
orderId: order.id,
destination: order.destination,
currentDeposit: formatEther(currentDeposit),
requiredNative: formatEther(requiredNative),
},
"EntryPoint deposit gas coverage check",
)
const targetDeposit = targetGasUnits * gasPrice
const thresholdDeposit = effectiveThreshold * gasPrice
const depositGasUnits = currentDeposit / gasPrice

if (currentDeposit >= requiredNative) {
if (currentDeposit >= thresholdDeposit) {
this.logger.info(
{
chain,
currentDeposit: formatEther(currentDeposit),
depositGasUnits: depositGasUnits.toString(),
targetGasUnits: targetGasUnits.toString(),
walletBalance: formatEther(solverBalance),
},
"EntryPoint deposit covers target gas units, no top-up needed",
)
return
}

const publicClient = this.clientManager.getPublicClient(order.destination)
const solverBalance = await publicClient.getBalance({ address: this.solverAccountAddress })
const depositAmount = solverBalance / 10n
const deficit = targetDeposit - currentDeposit

if (depositAmount === 0n) {
if (solverBalance < deficit) {
this.logger.warn(
{
orderId: order.id,
destination: order.destination,
chain,
deficit: formatEther(deficit),
solverBalance: formatEther(solverBalance),
depositGasUnits: depositGasUnits.toString(),
targetGasUnits: targetGasUnits.toString(),
},
"Solver EOA balance too low to top up EntryPoint deposit",
"Solver EOA balance insufficient to reach target deposit, depositing available balance",
)
await this.depositToEntryPoint(chain, solverBalance)
return
}

this.logger.info(
{
orderId: order.id,
destination: order.destination,
requiredNative: formatEther(requiredNative),
chain,
currentDeposit: formatEther(currentDeposit),
solverBalance: formatEther(solverBalance),
depositAmount: formatEther(depositAmount),
depositGasUnits: depositGasUnits.toString(),
targetGasUnits: targetGasUnits.toString(),
topUpAmount: formatEther(deficit),
},
"Top up EntryPoint deposit by 10% of solver EOA balance",
"Topping up EntryPoint deposit to cover target gas units",
)

await this.depositToEntryPoint(order.destination, depositAmount)
await this.depositToEntryPoint(chain, deficit)
}

/**
Expand Down
13 changes: 13 additions & 0 deletions sdk/packages/simplex/src/services/FillerConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export interface FillerConfig {
*/
gasFeeBump?: GasFeeBumpConfig
rebalancing?: RebalancingConfig
/**
* Target gas units the EntryPoint deposit should cover per chain.
* Defaults to 3,000,000 if not set.
*/
targetGasUnits?: number
}

/**
Expand Down Expand Up @@ -303,4 +308,12 @@ export class FillerConfigService {
getTriggerPercentage(): number | undefined {
return this.fillerConfig?.rebalancing?.triggerPercentage
}

/**
* Get target gas units for EntryPoint deposits.
* Defaults to 3,000,000 if not configured.
*/
getTargetGasUnits(): bigint {
return BigInt(this.fillerConfig?.targetGasUnits ?? 3_000_000)
}
}
Loading
Loading