@hyperbridge/simplex 0.16.13 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sqlite.js CHANGED
@@ -667,6 +667,7 @@ var LIMIT_ORDER_COLUMNS = `
667
667
  status,
668
668
  commitment,
669
669
  order_nonce as orderNonce,
670
+ entry_point as entryPoint,
670
671
  book_expires_at as bookExpiresAt,
671
672
  book_price as bookPrice,
672
673
  last_error as lastError,
@@ -703,6 +704,7 @@ var SqliteLimitOrderStore = class {
703
704
  status TEXT NOT NULL,
704
705
  commitment TEXT,
705
706
  order_nonce TEXT NOT NULL DEFAULT '0',
707
+ entry_point TEXT,
706
708
  book_expires_at TEXT,
707
709
  book_price TEXT,
708
710
  last_error TEXT,
@@ -727,6 +729,10 @@ var SqliteLimitOrderStore = class {
727
729
 
728
730
  CREATE INDEX IF NOT EXISTS idx_limit_order_fills_order ON limit_order_fills(limit_order_id);
729
731
  `);
732
+ if (!columnNames(this.db, "limit_orders").has("entry_point")) {
733
+ this.db.exec("ALTER TABLE limit_orders ADD COLUMN entry_point TEXT");
734
+ this.logger.info({ column: "entry_point" }, "Migrated limit order storage schema");
735
+ }
730
736
  if (!columnNames(this.db, "limit_order_fills").has("amount_in")) {
731
737
  this.db.exec("ALTER TABLE limit_order_fills ADD COLUMN amount_in TEXT");
732
738
  this.logger.info({ column: "amount_in" }, "Migrated limit order fill schema");
@@ -791,13 +797,14 @@ var SqliteLimitOrderStore = class {
791
797
  const result = this.db.prepare(`
792
798
  UPDATE limit_orders
793
799
  SET commitment = ?, book_expires_at = ?, book_price = ?, order_nonce = ?,
794
- status = ?, last_error = ?, updated_at = datetime('now')
800
+ entry_point = ?, status = ?, last_error = ?, updated_at = datetime('now')
795
801
  WHERE id = ?${statusGuard(only)}
796
802
  `).run(
797
803
  posting.commitment,
798
804
  posting.bookExpiresAt,
799
805
  posting.bookPrice,
800
806
  posting.orderNonce,
807
+ posting.entryPoint,
801
808
  posting.status,
802
809
  posting.lastError,
803
810
  id,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/data/sqlite/index.ts","../src/services/Logger.ts","../src/data/sqlite/schema.ts","../src/data/sqlite/activity.ts","../src/data/memory.ts","../src/data/sqlite/bids.ts","../src/data/sqlite/inventory.ts","../src/data/sqlite/limit-orders.ts","../src/data/sqlite/state.ts"],"sourcesContent":["import { existsSync, mkdirSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type {\n\tActivityStore,\n\tBidStore,\n\tInventoryStore,\n\tLimitOrderStore,\n\tSimplexDataStore,\n\tStateStore,\n} from \"@/data/types\"\nimport { SqliteActivityStore } from \"./activity\"\nimport { SqliteBidStore } from \"./bids\"\nimport { SqliteInventoryStore } from \"./inventory\"\nimport { SqliteLimitOrderStore } from \"./limit-orders\"\nimport { SqliteStateStore } from \"./state\"\n\nexport { SqliteActivityStore } from \"./activity\"\nexport { SqliteBidStore } from \"./bids\"\nexport { SqliteInventoryStore } from \"./inventory\"\nexport { SqliteLimitOrderStore } from \"./limit-orders\"\nexport { SqliteStateStore } from \"./state\"\n\n/**\n * How long a write waits for another connection's lock before giving up.\n * Matches the default better-sqlite3 applied for us; `node:sqlite` defaults to 0,\n * which turns any momentary contention into an instant SQLITE_BUSY throw — and a\n * lost bid write is a deposit the retraction sweep can no longer find.\n *\n * Set through the PRAGMA rather than `DatabaseSync`'s `timeout` option on purpose.\n * That option only exists in Node >=22.18 and >=24 (`@types/node` says 22.16, which\n * is wrong — the version table on nodejs.org is authoritative), so it is silently\n * ignored on 22.16, 22.17 and the whole 23 line, all of which `engines.node`\n * admits. The PRAGMA is plain SQLite and works wherever `node:sqlite` does.\n */\nconst BUSY_TIMEOUT_MS = 5_000\n\n/**\n * File-backed {@link SimplexDataStore} — the durable default, and what the\n * `simplex` CLI uses.\n *\n * Keeps bids and activity in separate database files (`bids.db`,\n * `activity.db`) so an existing data directory written by an earlier version is\n * picked up unchanged. Operator state and limit orders ride in `bids.db` beside\n * the bids; a `runtime-state.json` from before that is imported once and deleted.\n * Inventory snapshots ride in `activity.db`, which is the history.\n *\n * Built on `node:sqlite`, so there is nothing to install and nothing to\n * compile — the engine ships inside the Node runtime. That is why the package\n * has no native dependency and why `engines.node` names a version new enough to\n * have `node:sqlite` unflagged.\n */\nexport class SqliteDataStore implements SimplexDataStore {\n\treadonly bids: BidStore\n\treadonly activity: ActivityStore\n\treadonly state: StateStore\n\treadonly limitOrders: LimitOrderStore\n\treadonly inventory: InventoryStore\n\n\tprivate databases: DatabaseSync[]\n\tprivate logger: Logger\n\n\tconstructor(dataDir: string, loggers: LoggerContext = defaultLoggerContext()) {\n\t\tthis.logger = loggers.get(\"data-store\")\n\t\tif (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true })\n\n\t\tconst bidsDb = new DatabaseSync(join(dataDir, \"bids.db\"))\n\t\tconst activityDb = new DatabaseSync(join(dataDir, \"activity.db\"))\n\t\t// `node:sqlite` has no .pragma(); exec() discards the rows these return.\n\t\tfor (const db of [bidsDb, activityDb]) db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)\n\t\tactivityDb.exec(\"PRAGMA journal_mode = WAL\")\n\n\t\tthis.databases = [bidsDb, activityDb]\n\t\tthis.bids = new SqliteBidStore(bidsDb, loggers)\n\t\tthis.activity = new SqliteActivityStore(activityDb, loggers)\n\t\tthis.state = new SqliteStateStore(bidsDb, dataDir, loggers)\n\t\tthis.limitOrders = new SqliteLimitOrderStore(bidsDb, loggers)\n\t\tthis.inventory = new SqliteInventoryStore(activityDb)\n\n\t\tthis.logger.info({ dataDir }, \"SQLite data store opened\")\n\t}\n\n\t/**\n\t * Closes both databases. Called by `Simplex.stop()` when Simplex opened the\n\t * store itself, and by whoever passed it in otherwise — `bootFiller` only\n\t * closes a store it owns, since a caller's may be shared with another solver.\n\t *\n\t * Deliberately not wired to a `process.on(\"exit\")` hook: the old services did\n\t * that in their constructors, which leaked a listener per instance and made\n\t * two fillers in one process race each other's teardown. Both databases are\n\t * crash-safe without an explicit close.\n\t *\n\t * `isOpen` guards the second call: unlike better-sqlite3's no-op close,\n\t * `DatabaseSync.close()` throws on an already-closed handle, and `stop()`\n\t * being called twice is not an error worth logging. Compared against `false`\n\t * rather than truthiness so a runtime without the property (it landed in\n\t * 22.15, below our engines floor but `engines` is only advisory) still\n\t * attempts the close instead of silently skipping every database.\n\t */\n\tasync close(): Promise<void> {\n\t\tfor (const db of this.databases) {\n\t\t\tif (db.isOpen === false) continue\n\t\t\ttry {\n\t\t\t\tdb.close()\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err }, \"Failed to close database cleanly\")\n\t\t\t}\n\t\t}\n\t}\n}\n","import pino, { stdSerializers } from \"pino\"\n\nexport const LOG_LEVELS = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"] as const\n\nexport type LogLevel = (typeof LOG_LEVELS)[number]\n\n/**\n * Anything that accepts a line of text: `process.stdout`, a `fs` write stream, a\n * socket, a `pino-pretty` transform, or an object literal that forwards into\n * your own logger. Node's `Writable` satisfies it as-is.\n */\nexport interface LogSink {\n\twrite(line: string): void\n}\n\ntype LogFn = {\n\t(obj: unknown, msg?: string, ...args: unknown[]): void\n\t(msg: string, ...args: unknown[]): void\n}\n\nexport interface Logger {\n\ttrace: LogFn\n\tdebug: LogFn\n\tinfo: LogFn\n\twarn: LogFn\n\terror: LogFn\n\tfatal: LogFn\n}\n\ntype LoggerOptions = {\n\tmodule?: string\n}\n\n/**\n * viem hangs the whole contract ABI off its errors and repeats the same message\n * once per wrapper in the cause chain. Serialised verbatim, one failed\n * `readContract` prints several hundred lines of ABI JSON. None of it says\n * anything the short message and the root cause don't.\n */\nconst NOISY_ERROR_FIELDS = [\n\t\"abi\",\n\t\"args\",\n\t\"formattedArgs\",\n\t\"metaMessages\",\n\t\"docsPath\",\n\t\"version\",\n\t\"contractAddress\",\n\t\"functionName\",\n\t\"sender\",\n] as const\n\n/** Deep frames are bundler offsets into dist/bin/simplex.js — useless for diagnosis. */\nconst MAX_STACK_FRAMES = 6\n\n/** `at fn (/very/long/.pnpm/viem@x/node_modules/viem/a/b.ts:1:2)` -> `at fn (b.ts:1:2)`. */\nfunction compactFrame(frame: string): string {\n\treturn frame.trim().replace(/\\(?([^\\s(]*[/\\\\])?([^/\\\\\\s()]+:\\d+:\\d+)\\)?$/, \"($2)\")\n}\n\n/** Innermost `cause` message: the line that actually says what went wrong. */\nfunction rootCause(error: unknown): string | undefined {\n\tlet current: unknown = error\n\tlet deepest: string | undefined\n\tfor (let depth = 0; depth < 8; depth++) {\n\t\tconst cause = (current as { cause?: unknown })?.cause\n\t\tif (!cause) break\n\t\tif (cause instanceof Error) {\n\t\t\tdeepest = `${cause.name}: ${cause.message}`.split(\"\\n\")[0]\n\t\t} else if (typeof cause === \"string\") {\n\t\t\tdeepest = cause.split(\"\\n\")[0]\n\t\t}\n\t\tcurrent = cause\n\t}\n\treturn deepest\n}\n\n/** The endpoint viem was talking to, recovered before metaMessages is dropped. */\nfunction endpointOf(error: unknown): string | undefined {\n\tconst meta = (error as { metaMessages?: unknown })?.metaMessages\n\tif (!Array.isArray(meta)) return undefined\n\tconst line = meta.find((m) => typeof m === \"string\" && m.startsWith(\"URL: \"))\n\treturn typeof line === \"string\" ? line.slice(5) : undefined\n}\n\n/**\n * Keeps what identifies the failure — type, short message, details, the failing\n * endpoint and the root cause — and discards the ABI dump, the repeated message\n * bodies and the deep bundler stack frames.\n */\nfunction compactError(error: unknown): unknown {\n\tif (!(error instanceof Error)) return stdSerializers.err(error as Error)\n\n\tconst serialized = stdSerializers.err(error) as Record<string, unknown>\n\tconst endpoint = endpointOf(error)\n\tconst cause = rootCause(error)\n\n\tfor (const field of NOISY_ERROR_FIELDS) delete serialized[field]\n\n\t// `message` restates shortMessage plus every wrapper's context; the short\n\t// form carries the same information in one line. This applies to viem's\n\t// errors, which is where the duplication comes from — an error we built\n\t// ourselves has no wrapper padding to strip, and truncating it would throw\n\t// away the per-provider failure list that is the whole point of the message.\n\tconst shortMessage = serialized.shortMessage\n\tif (error.name === \"QuorumError\") {\n\t\t// Keep it whole.\n\t} else if (typeof shortMessage === \"string\" && shortMessage.length > 0) {\n\t\tserialized.message = shortMessage\n\t\tdelete serialized.shortMessage\n\t} else if (typeof serialized.message === \"string\") {\n\t\tserialized.message = serialized.message.split(\"\\n\")[0]\n\t}\n\n\tif (typeof serialized.stack === \"string\") {\n\t\tconst frames = serialized.stack\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().startsWith(\"at \"))\n\t\t\t.slice(0, MAX_STACK_FRAMES)\n\t\t\t.map(compactFrame)\n\t\tif (frames.length > 0) serialized.stack = frames.join(\" <- \")\n\t\telse delete serialized.stack\n\t}\n\n\t// `name` is always the same string as `type`.\n\tdelete serialized.name\n\n\tif (endpoint) serialized.endpoint = endpoint\n\tif (cause && cause !== serialized.message) serialized.cause = cause\n\n\treturn serialized\n}\n\n/**\n * An isolated logging destination: its own sinks, its own level, its own pino\n * instance.\n *\n * Each `Simplex` owns one, so two fillers in a process log to their own sinks at\n * their own verbosity and never cross-contaminate. Services receive the context\n * their filler was started with and resolve module loggers from it.\n */\nexport class LoggerContext {\n\tprivate level: LogLevel\n\tprivate readonly sinks = new Set<LogSink>()\n\tprivate base: pino.Logger | undefined\n\tprivate readonly children = new Map<string, pino.Logger>()\n\n\tconstructor(options: { level?: LogLevel; sink?: LogSink } = {}) {\n\t\t// Checked here rather than at the first log: pino throws on an unknown level,\n\t\t// and with a silent default that throw would surface much later, from\n\t\t// whatever happened to log first after a sink attached.\n\t\tif (options.level && !LOG_LEVELS.includes(options.level)) {\n\t\t\tthrow new Error(`Unknown log level '${options.level}'. Use one of: ${LOG_LEVELS.join(\", \")}`)\n\t\t}\n\t\tthis.level = options.level ?? \"info\"\n\t\tif (options.sink) this.sinks.add(options.sink)\n\t}\n\n\t/**\n\t * Drops the memoized loggers so the next call rebuilds at the current level\n\t * and sink set. Module loggers are wrappers rather than pino instances\n\t * precisely so this reaches loggers that already exist — replacing the base\n\t * logger would leave every service that had captured a child logging at the\n\t * old level forever.\n\t */\n\tprivate invalidate(): void {\n\t\tthis.base = undefined\n\t\tthis.children.clear()\n\t}\n\n\tprivate baseLogger(): pino.Logger {\n\t\tif (this.base) return this.base\n\t\tconst sinks = this.sinks\n\t\tthis.base = pino(\n\t\t\t{\n\t\t\t\t// With no sink registered nothing can consume a record, so don't pay to\n\t\t\t\t// serialize one. This is what makes an unconfigured filler free as well\n\t\t\t\t// as silent.\n\t\t\t\tlevel: sinks.size === 0 ? \"silent\" : this.level,\n\t\t\t\tserializers: {\n\t\t\t\t\terror: compactError,\n\t\t\t\t\terr: compactError,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\twrite(line: string) {\n\t\t\t\t\tfor (const sink of sinks) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tsink.write(line)\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// A broken sink is the host's problem, not a reason to fail a fill.\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\treturn this.base\n\t}\n\n\tprivate childFor(module?: string): pino.Logger {\n\t\tif (!module) return this.baseLogger()\n\t\tlet child = this.children.get(module)\n\t\tif (!child) {\n\t\t\tchild = this.baseLogger().child({ moduleTag: `[${module}]` })\n\t\t\tthis.children.set(module, child)\n\t\t}\n\t\treturn child\n\t}\n\n\t/** Registers a destination and returns a function that removes it. */\n\taddSink(sink: LogSink): () => void {\n\t\tthis.sinks.add(sink)\n\t\tthis.invalidate()\n\t\treturn () => {\n\t\t\tthis.sinks.delete(sink)\n\t\t\tthis.invalidate()\n\t\t}\n\t}\n\n\t/** Sets verbosity, including for loggers already handed out. */\n\tsetLevel(level: LogLevel): void {\n\t\tif (!LOG_LEVELS.includes(level)) {\n\t\t\tthrow new Error(`Unknown log level '${level}'. Use one of: ${LOG_LEVELS.join(\", \")}`)\n\t\t}\n\t\tthis.level = level\n\t\tthis.invalidate()\n\t}\n\n\t/** A logger tagged with `module`. Safe to hold; it resolves the current state per call. */\n\tget(module?: string): Logger {\n\t\treturn new ModuleLogger(this, module)\n\t}\n\n\t/** @internal — used by {@link ModuleLogger} to resolve per call. */\n\tresolve(module?: string): pino.Logger {\n\t\treturn this.childFor(module)\n\t}\n}\n\n/** Resolves through its context per call, so level and sink changes apply immediately. */\nclass ModuleLogger implements Logger {\n\tconstructor(\n\t\tprivate readonly context: LoggerContext,\n\t\tprivate readonly module?: string,\n\t) {}\n\n\tprivate forward(method: keyof Logger): LogFn {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: pino's LogFn overloads don't survive a generic forward\n\t\treturn (...args: any[]) => (this.context.resolve(this.module)[method] as (...a: any[]) => void)(...args)\n\t}\n\n\ttrace = this.forward(\"trace\")\n\tdebug = this.forward(\"debug\")\n\tinfo = this.forward(\"info\")\n\twarn = this.forward(\"warn\")\n\terror = this.forward(\"error\")\n\tfatal = this.forward(\"fatal\")\n}\n\n/**\n * The fallback context, used by code that is not scoped to a single filler: the\n * CLI, the setup wizard, and anything constructed before a `Simplex` exists.\n *\n * An embedded filler never logs here — `Simplex` creates its own context — so\n * leaving this unconfigured is what keeps the library silent in a host process.\n */\nconst processContext = new LoggerContext()\n\n/**\n * Registers a destination on the process-wide context and returns a function\n * that removes it.\n *\n * This is for the application layer. To capture a specific filler's output, pass\n * `SimplexOptions.logger` instead — a filler's records go to its own context,\n * not here.\n */\nexport function addLogSink(sink: LogSink): () => void {\n\treturn processContext.addSink(sink)\n}\n\n/** Sets the level of the process-wide context. Does not affect running fillers. */\nexport function configureLogger(level: LogLevel): void {\n\tprocessContext.setLevel(level)\n}\n\n/** A logger on the process-wide context. Instance-scoped code resolves from its own {@link LoggerContext}. */\nexport function getLogger(moduleOrOptions?: string | LoggerOptions): Logger {\n\tconst options: LoggerOptions =\n\t\ttypeof moduleOrOptions === \"string\" ? { module: moduleOrOptions } : moduleOrOptions || {}\n\treturn processContext.get(options.module)\n}\n\n/** The fallback context, for constructors that were given none. */\nexport function defaultLoggerContext(): LoggerContext {\n\treturn processContext\n}\n\n/**\n * Resolves a module logger from a context that may be absent.\n *\n * Services read their context off the `FillerConfigService` or\n * `ChainClientManager` they were constructed with, and `bootFiller` always\n * supplies real ones. Hand-built test doubles generally do not, so this falls\n * back to the process context rather than forcing every mock to carry a logger.\n */\nexport function moduleLogger(context: LoggerContext | undefined, module: string): Logger {\n\treturn (context ?? processContext).get(module)\n}\n","import type { DatabaseSync } from \"node:sqlite\"\n\n/** The one column of a `PRAGMA table_info` row this package reads. */\ninterface TableInfoRow {\n\tname: string\n}\n\n/**\n * The columns `table` currently has.\n *\n * Both stores add columns in place to databases written before those columns\n * existed, and this is how they find out what is already there. `node:sqlite`\n * types every column as `SQLOutputValue`, so the row shape is asserted here\n * once instead of at each call site.\n *\n * `table` is interpolated rather than bound because PRAGMA takes no bound\n * parameters; every caller passes a literal.\n */\nexport function columnNames(db: DatabaseSync, table: string): Set<string> {\n\tconst rows = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as TableInfoRow[]\n\treturn new Set(rows.map((row) => row.name))\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type { ActivityEvent, ActivityInsert, ActivityStore, OrderHistoryPage, OrderSummary, WalletTx } from \"@/data/types\"\nimport { columnNames } from \"./schema\"\n\nconst MAX_ROWS = 10_000\nconst PRUNE_EVERY = 500\n\n// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\nfunction toActivityEvent(row: any): ActivityEvent {\n\treturn {\n\t\tid: row.id,\n\t\tts: row.ts,\n\t\ttype: row.type,\n\t\torderId: row.order_id,\n\t\tchainId: row.chain_id,\n\t\tstrategy: row.strategy,\n\t\tsuccess: row.success === null ? null : row.success === 1,\n\t\treason: row.reason,\n\t\tvolumeUsd: row.volume_usd,\n\t\tprofitUsd: row.profit_usd,\n\t\ttxHash: row.tx_hash,\n\t\torder: parseOrder(row.order_json),\n\t}\n}\n\nfunction parseOrder(json: string | null): OrderSummary | null {\n\tif (!json) return null\n\ttry {\n\t\treturn JSON.parse(json) as OrderSummary\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction capLimit(limit: number): number {\n\treturn Math.min(Math.max(limit, 1), 500)\n}\n\n/**\n * SQLite-backed {@link ActivityStore}.\n *\n * Unlike the bid store this is observability rather than correctness, so it\n * prunes: every {@link PRUNE_EVERY} inserts the oldest rows beyond\n * {@link MAX_ROWS} are dropped from both tables. A prune failure is logged and\n * swallowed — losing the trim must never fail the write that triggered it.\n */\nexport class SqliteActivityStore implements ActivityStore {\n\tprivate logger: Logger\n\tprivate insertsSincePrune = 0\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"activity\")\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS events (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tts INTEGER NOT NULL,\n\t\t\t\ttype TEXT NOT NULL,\n\t\t\t\torder_id TEXT,\n\t\t\t\tchain_id INTEGER,\n\t\t\t\tstrategy TEXT,\n\t\t\t\tsuccess INTEGER,\n\t\t\t\treason TEXT,\n\t\t\t\tvolume_usd REAL,\n\t\t\t\tprofit_usd REAL,\n\t\t\t\ttx_hash TEXT\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_events_id ON events(id);\n\t\t\tCREATE TABLE IF NOT EXISTS wallet_txs (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tts INTEGER NOT NULL,\n\t\t\t\tkind TEXT NOT NULL,\n\t\t\t\tchain_id INTEGER,\n\t\t\t\ttoken TEXT,\n\t\t\t\tamount TEXT,\n\t\t\t\tto_address TEXT,\n\t\t\t\ttx_hash TEXT NOT NULL,\n\t\t\t\tsponsored INTEGER\n\t\t\t);\n\t\t`)\n\t\t// Added after the first release: rows written before it have no order summary.\n\t\tconst columns = columnNames(this.db, \"events\")\n\t\tif (!columns.has(\"order_json\")) {\n\t\t\tthis.db.exec(\"ALTER TABLE events ADD COLUMN order_json TEXT\")\n\t\t\tthis.logger.info({ column: \"order_json\" }, \"Migrated activity schema\")\n\t\t}\n\t\t// Added with the ledger's amount columns: the side that came back (vault shares, redeemed assets).\n\t\tconst walletColumns = columnNames(this.db, \"wallet_txs\")\n\t\tfor (const column of [\"token_in\", \"amount_in\"]) {\n\t\t\tif (walletColumns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE wallet_txs ADD COLUMN ${column} TEXT`)\n\t\t\tthis.logger.info({ column }, \"Migrated wallet ledger schema\")\n\t\t}\n\t}\n\n\tasync record(event: ActivityInsert): Promise<ActivityEvent> {\n\t\tconst ts = Date.now()\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO events (ts, type, order_id, chain_id, strategy, success, reason, volume_usd, profit_usd, tx_hash, order_json)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tts,\n\t\t\t\tevent.type,\n\t\t\t\tevent.orderId ?? null,\n\t\t\t\tevent.chainId ?? null,\n\t\t\t\tevent.strategy ?? null,\n\t\t\t\tevent.success === undefined || event.success === null ? null : event.success ? 1 : 0,\n\t\t\t\tevent.reason ?? null,\n\t\t\t\tevent.volumeUsd ?? null,\n\t\t\t\tevent.profitUsd ?? null,\n\t\t\t\tevent.txHash ?? null,\n\t\t\t\tevent.order ? JSON.stringify(event.order) : null,\n\t\t\t)\n\n\t\tif (++this.insertsSincePrune >= PRUNE_EVERY) {\n\t\t\tthis.insertsSincePrune = 0\n\t\t\ttry {\n\t\t\t\tthis.db.prepare(\"DELETE FROM events WHERE id <= (SELECT MAX(id) FROM events) - ?\").run(MAX_ROWS)\n\t\t\t\tthis.db.prepare(\"DELETE FROM wallet_txs WHERE id <= (SELECT MAX(id) FROM wallet_txs) - ?\").run(MAX_ROWS)\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err }, \"Activity log prune failed\")\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: Number(result.lastInsertRowid),\n\t\t\tts,\n\t\t\ttype: event.type,\n\t\t\torderId: event.orderId ?? null,\n\t\t\tchainId: event.chainId ?? null,\n\t\t\tstrategy: event.strategy ?? null,\n\t\t\tsuccess: event.success ?? null,\n\t\t\treason: event.reason ?? null,\n\t\t\tvolumeUsd: event.volumeUsd ?? null,\n\t\t\tprofitUsd: event.profitUsd ?? null,\n\t\t\ttxHash: event.txHash ?? null,\n\t\t\torder: event.order ?? null,\n\t\t}\n\t}\n\n\tasync recent(limit = 100, beforeId?: number): Promise<ActivityEvent[]> {\n\t\tconst capped = capLimit(limit)\n\t\tconst rows = beforeId\n\t\t\t? this.db.prepare(\"SELECT * FROM events WHERE id < ? ORDER BY id DESC LIMIT ?\").all(beforeId, capped)\n\t\t\t: this.db.prepare(\"SELECT * FROM events ORDER BY id DESC LIMIT ?\").all(capped)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync fills(limit = 100): Promise<ActivityEvent[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT * FROM events WHERE type = 'filled' AND tx_hash IS NOT NULL AND chain_id IS NOT NULL ORDER BY id DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync orderIdsMissingSummary(limit = 500): Promise<string[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT order_id FROM events WHERE order_id IS NOT NULL AND order_json IS NULL GROUP BY order_id ORDER BY MAX(id) DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => row.order_id as string)\n\t}\n\n\tasync attachOrder(orderId: string, order: OrderSummary): Promise<ActivityEvent[]> {\n\t\tconst json = JSON.stringify(order)\n\t\t// `node:sqlite` has no transaction wrapper, so the boundary is written out.\n\t\t// The SELECT and the UPDATE have to agree on which rows lacked a summary:\n\t\t// without the transaction a concurrent writer could fill one in between\n\t\t// them, and the ids returned here would name rows this call did not change.\n\t\tlet changed: number[]\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tconst missing = this.db\n\t\t\t\t.prepare(\"SELECT id FROM events WHERE order_id = ? AND order_json IS NULL\")\n\t\t\t\t.all(orderId) as unknown as Array<{ id: number }>\n\t\t\tchanged = missing.map((row) => row.id)\n\t\t\tif (changed.length > 0) {\n\t\t\t\tthis.db.prepare(\"UPDATE events SET order_json = ? WHERE order_id = ? AND order_json IS NULL\").run(json, orderId)\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t} catch (err) {\n\t\t\t// A COMMIT that fails has already rolled back, and rolling back twice\n\t\t\t// throws over the original error — `isTransaction` says which case this is.\n\t\t\t// Compared against `false`, not truthiness: the Node 23 line never got the\n\t\t\t// property, and treating `undefined` as \"no transaction\" would skip the\n\t\t\t// ROLLBACK and leave the connection wedged mid-transaction for good. The\n\t\t\t// inner catch is only for that case; `err` is what propagates either way.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t\tif (changed.length === 0) return []\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM events WHERE id IN (${changed.map(() => \"?\").join(\",\")}) ORDER BY id`)\n\t\t\t.all(...changed)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync unsettledOrders(limit = 500): Promise<string[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT order_id FROM events\n\t\t\t\t WHERE order_id IS NOT NULL\n\t\t\t\t GROUP BY order_id\n\t\t\t\t HAVING SUM(type = 'bid' OR (type = 'filled' AND volume_usd IS NOT NULL)) > 0\n\t\t\t\t AND SUM(type = 'lost' OR (type = 'filled' AND volume_usd IS NULL)) = 0\n\t\t\t\t ORDER BY MAX(id) DESC LIMIT ?`,\n\t\t\t)\n\t\t\t.all(capLimit(limit)) as unknown as Array<{ order_id: string }>\n\t\treturn rows.map((row) => row.order_id)\n\t}\n\n\tasync retypeLegacyBid(orderId: string): Promise<ActivityEvent[]> {\n\t\tconst ids = (\n\t\t\tthis.db\n\t\t\t\t.prepare(\"SELECT id FROM events WHERE order_id = ? AND type = 'filled' AND volume_usd IS NOT NULL\")\n\t\t\t\t.all(orderId) as unknown as Array<{ id: number }>\n\t\t).map((row) => row.id)\n\t\tif (ids.length === 0) return []\n\t\tthis.db\n\t\t\t.prepare(\"UPDATE events SET type = 'bid' WHERE order_id = ? AND type = 'filled' AND volume_usd IS NOT NULL\")\n\t\t\t.run(orderId)\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM events WHERE id IN (${ids.map(() => \"?\").join(\",\")}) ORDER BY id`)\n\t\t\t.all(...ids)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync knowsOrder(orderId: string): Promise<boolean> {\n\t\treturn this.db.prepare(\"SELECT 1 FROM events WHERE order_id = ? LIMIT 1\").get(orderId) !== undefined\n\t}\n\n\tasync orderHistory(page: number, pageSize: number): Promise<OrderHistoryPage> {\n\t\tconst size = capLimit(pageSize)\n\t\tconst current = Math.max(1, Math.floor(page))\n\t\tconst total = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(DISTINCT order_id) AS n FROM events WHERE order_id IS NOT NULL\").get() as unknown as {\n\t\t\t\tn: number\n\t\t\t}\n\t\t).n\n\t\tconst heads = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT order_id FROM events WHERE order_id IS NOT NULL GROUP BY order_id ORDER BY MAX(id) DESC LIMIT ? OFFSET ?\",\n\t\t\t)\n\t\t\t.all(size, (current - 1) * size) as unknown as Array<{ order_id: string }>\n\t\tconst ids = heads.map((head) => head.order_id)\n\t\tconst byOrder = new Map<string, ActivityEvent[]>(ids.map((id) => [id, []]))\n\t\tif (ids.length > 0) {\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(`SELECT * FROM events WHERE order_id IN (${ids.map(() => \"?\").join(\",\")}) ORDER BY id DESC`)\n\t\t\t\t.all(...ids)\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\t\tfor (const row of (rows as any[]).map(toActivityEvent)) byOrder.get(row.orderId as string)?.push(row)\n\t\t}\n\t\treturn {\n\t\t\tpage: current,\n\t\t\tpageSize: size,\n\t\t\ttotal,\n\t\t\torders: ids.map((orderId) => ({ orderId, events: byOrder.get(orderId) ?? [] })),\n\t\t}\n\t}\n\n\tasync recordWalletTx(tx: Omit<WalletTx, \"id\" | \"ts\">): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO wallet_txs (ts, kind, chain_id, token, amount, to_address, tx_hash, sponsored, token_in, amount_in)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tDate.now(),\n\t\t\t\ttx.kind,\n\t\t\t\ttx.chainId,\n\t\t\t\ttx.token,\n\t\t\t\ttx.amount,\n\t\t\t\ttx.to,\n\t\t\t\ttx.txHash,\n\t\t\t\ttx.sponsored === null ? null : tx.sponsored ? 1 : 0,\n\t\t\t\ttx.tokenIn ?? null,\n\t\t\t\ttx.amountIn ?? null,\n\t\t\t)\n\t}\n\n\tasync walletTxsWithoutAmounts(limit = 200): Promise<WalletTx[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT * FROM wallet_txs WHERE kind IN ('sweep', 'redeem') AND token IS NULL ORDER BY id DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toWalletTx)\n\t}\n\n\tasync updateWalletTx(\n\t\tid: number,\n\t\tpatch: Pick<WalletTx, \"token\" | \"amount\" | \"to\" | \"tokenIn\" | \"amountIn\">,\n\t): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(\"UPDATE wallet_txs SET token = ?, amount = ?, to_address = ?, token_in = ?, amount_in = ? WHERE id = ?\")\n\t\t\t.run(patch.token, patch.amount, patch.to, patch.tokenIn ?? null, patch.amountIn ?? null, id)\n\t}\n\n\tasync walletTxs(limit = 100): Promise<WalletTx[]> {\n\t\tconst rows = this.db.prepare(\"SELECT * FROM wallet_txs ORDER BY id DESC LIMIT ?\").all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toWalletTx)\n\t}\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\nfunction toWalletTx(row: any): WalletTx {\n\treturn {\n\t\tid: row.id,\n\t\tts: row.ts,\n\t\tkind: row.kind,\n\t\tchainId: row.chain_id,\n\t\ttoken: row.token,\n\t\tamount: row.amount,\n\t\tto: row.to_address,\n\t\ttxHash: row.tx_hash,\n\t\tsponsored: row.sponsored === null ? null : row.sponsored === 1,\n\t\ttokenIn: row.token_in ?? null,\n\t\tamountIn: row.amount_in ?? null,\n\t}\n}\n","import type {\n\tActivityEvent,\n\tActivityInsert,\n\tActivityStore,\n\tOrderHistoryPage,\n\tOrderSummary,\n\tBidInsert,\n\tBidStats,\n\tBidStore,\n\tInventorySnapshot,\n\tInventoryStore,\n\tLimitOrder,\n\tLimitOrderFill,\n\tLimitOrderFillInsert,\n\tLimitOrderFillRecord,\n\tLimitOrderFilter,\n\tLimitOrderHold,\n\tLimitOrderInsert,\n\tLimitOrderPosting,\n\tLimitOrderStatus,\n\tLimitOrderStore,\n\tRuntimeState,\n\tSimplexDataStore,\n\tStateStore,\n\tStoredBid,\n\tWalletTx,\n} from \"./types\"\n\n/**\n * Rows kept per collection before pruning. Bid rows are pruned settled-first —\n * a row whose deposit is still reclaimable (successful or pending, unretracted)\n * survives any cap, since the retraction sweep finds work by querying this store.\n */\nconst MAX_ROWS = 10_000\n\n/** Newest-first reads are capped here however large a `limit` the caller passes. */\nconst MAX_LIMIT = 500\n\n/**\n * \"YYYY-MM-DD HH:MM:SS\" in UTC — the format SQLite's `datetime('now')` produces.\n * Kept identical across adapters because `StoredBid.createdAt` is compared\n * lexicographically to find expired bids, and `toISOString()`'s \"T\" separator\n * (ASCII 84) sorts after a space (ASCII 32), which would make every bid look\n * expired.\n */\nexport function sqliteDatetime(date: Date): string {\n\treturn date.toISOString().replace(\"T\", \" \").replace(/\\.\\d{3}Z$/, \"\")\n}\n\n/** A bid whose deposit may be locked on Hyperbridge: confirmed, or still pooled. */\nfunction reclaimable(row: StoredBid): boolean {\n\treturn row.success || row.pending\n}\n\nfunction capLimit(limit: number): number {\n\treturn Math.min(Math.max(limit, 1), MAX_LIMIT)\n}\n\nclass MemoryBidStore implements BidStore {\n\tprivate rows: StoredBid[] = []\n\tprivate nextId = 1\n\n\tasync store(bid: BidInsert): Promise<void> {\n\t\tthis.rows.push({\n\t\t\tid: this.nextId++,\n\t\t\tcommitment: bid.commitment,\n\t\t\tbid: bid.bid ?? null,\n\t\t\textrinsicHash: bid.extrinsicHash ?? null,\n\t\t\tblockHash: bid.blockHash ?? null,\n\t\t\tsuccess: bid.success,\n\t\t\tpending: bid.pending === true,\n\t\t\terror: bid.error ?? null,\n\t\t\tcreatedAt: sqliteDatetime(new Date()),\n\t\t\tretracted: false,\n\t\t\tretractedAt: null,\n\t\t\tretractExtrinsicHash: null,\n\t\t\tdead: false,\n\t\t\treservations: bid.reservations ?? [],\n\t\t})\n\t\tif (this.rows.length > MAX_ROWS) {\n\t\t\t// Only ever drop rows with nothing left to reclaim. A successful or pending\n\t\t\t// bid holds a Hyperbridge deposit that the sweep finds by querying this\n\t\t\t// store, so evicting one by age strands it — and this store is the\n\t\t\t// library's default, which is exactly where that is least survivable.\n\t\t\tconst settled = this.rows.filter((row) => row.retracted || (!row.success && !row.pending))\n\t\t\tconst excess = this.rows.length - MAX_ROWS\n\t\t\tconst drop = new Set(settled.slice(0, excess))\n\t\t\tif (drop.size > 0) this.rows = this.rows.filter((row) => !drop.has(row))\n\t\t}\n\t}\n\n\tasync byCommitment(commitment: string): Promise<StoredBid | null> {\n\t\t// Newest wins: a commitment can be re-bid after a retraction.\n\t\tfor (let i = this.rows.length - 1; i >= 0; i--) {\n\t\t\tif (this.rows[i].commitment === commitment) return { ...this.rows[i] }\n\t\t}\n\t\treturn null\n\t}\n\n\tasync unretractedReclaimable(): Promise<StoredBid[]> {\n\t\treturn this.rows.filter((row) => reclaimable(row) && !row.retracted).map((row) => ({ ...row }))\n\t}\n\n\tasync expiredUnretracted(maxAgeMs: number): Promise<StoredBid[]> {\n\t\tconst cutoff = sqliteDatetime(new Date(Date.now() - maxAgeMs))\n\t\treturn this.rows\n\t\t\t.filter((row) => reclaimable(row) && !row.retracted && (row.dead || row.createdAt < cutoff))\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync markRetracted(commitment: string, retractExtrinsicHash: string | null): Promise<boolean> {\n\t\tlet changed = false\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.retracted) continue\n\t\t\trow.retracted = true\n\t\t\trow.retractedAt = sqliteDatetime(new Date())\n\t\t\trow.retractExtrinsicHash = retractExtrinsicHash\n\t\t\tchanged = true\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync claimReservation(commitment: string, bid?: string): Promise<LimitOrderHold[]> {\n\t\t// Bids on one incoming order share a commitment and differ by identifier, so a\n\t\t// claim names the bid. Without one, every outstanding hold on the commitment\n\t\t// comes back, which is what a filled or dead order needs.\n\t\tconst claimed: LimitOrderHold[] = []\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.reservations.length === 0) continue\n\t\t\tif (bid !== undefined && row.bid !== bid) continue\n\t\t\tclaimed.push(...row.reservations)\n\t\t\trow.reservations = []\n\t\t}\n\t\treturn claimed\n\t}\n\n\tasync byLimitOrder(limitOrderId: string, limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.filter((row) => row.reservations.some((hold) => hold.limitOrderId === limitOrderId))\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync markDead(commitment: string): Promise<boolean> {\n\t\tlet changed = false\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.retracted || row.dead) continue\n\t\t\trow.dead = true\n\t\t\tchanged = true\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync recent(limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync byCommitments(commitments: string[]): Promise<StoredBid[]> {\n\t\tconst wanted = new Set(commitments)\n\t\treturn this.rows\n\t\t\t.filter((row) => wanted.has(row.commitment))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync failed(limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.filter((row) => !row.success)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync byDateRange(from: Date, to: Date): Promise<StoredBid[]> {\n\t\tconst start = sqliteDatetime(from)\n\t\tconst end = sqliteDatetime(to)\n\t\treturn this.rows\n\t\t\t.filter((row) => row.createdAt >= start && row.createdAt <= end)\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync stats(): Promise<BidStats> {\n\t\treturn {\n\t\t\ttotal: this.rows.length,\n\t\t\tsuccessful: this.rows.filter((row) => row.success).length,\n\t\t\tfailed: this.rows.filter((row) => !row.success).length,\n\t\t\tretracted: this.rows.filter((row) => row.retracted).length,\n\t\t\tpendingRetraction: this.rows.filter((row) => reclaimable(row) && !row.retracted).length,\n\t\t}\n\t}\n}\n\nclass MemoryActivityStore implements ActivityStore {\n\tprivate events: ActivityEvent[] = []\n\tprivate walletRows: WalletTx[] = []\n\tprivate nextEventId = 1\n\tprivate nextWalletId = 1\n\n\tasync record(event: ActivityInsert): Promise<ActivityEvent> {\n\t\tconst row: ActivityEvent = {\n\t\t\tid: this.nextEventId++,\n\t\t\tts: Date.now(),\n\t\t\ttype: event.type,\n\t\t\torderId: event.orderId ?? null,\n\t\t\tchainId: event.chainId ?? null,\n\t\t\tstrategy: event.strategy ?? null,\n\t\t\tsuccess: event.success ?? null,\n\t\t\treason: event.reason ?? null,\n\t\t\tvolumeUsd: event.volumeUsd ?? null,\n\t\t\tprofitUsd: event.profitUsd ?? null,\n\t\t\ttxHash: event.txHash ?? null,\n\t\t\torder: event.order ?? null,\n\t\t}\n\t\tthis.events.push(row)\n\t\tif (this.events.length > MAX_ROWS) this.events.splice(0, this.events.length - MAX_ROWS)\n\t\treturn { ...row }\n\t}\n\n\tasync recent(limit = 100, beforeId?: number): Promise<ActivityEvent[]> {\n\t\tconst pool = beforeId === undefined ? this.events : this.events.filter((row) => row.id < beforeId)\n\t\treturn pool\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync fills(limit = 100): Promise<ActivityEvent[]> {\n\t\treturn this.events\n\t\t\t.filter((row) => row.type === \"filled\" && row.txHash !== null && row.chainId !== null)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync orderIdsMissingSummary(limit = 500): Promise<string[]> {\n\t\tconst ids: string[] = []\n\t\tfor (let i = this.events.length - 1; i >= 0 && ids.length < capLimit(limit); i--) {\n\t\t\tconst row = this.events[i]\n\t\t\tif (row.orderId && row.order === null && !ids.includes(row.orderId)) ids.push(row.orderId)\n\t\t}\n\t\treturn ids\n\t}\n\n\tasync attachOrder(orderId: string, order: OrderSummary): Promise<ActivityEvent[]> {\n\t\tconst changed: ActivityEvent[] = []\n\t\tfor (const row of this.events) {\n\t\t\tif (row.orderId === orderId && row.order === null) {\n\t\t\t\trow.order = order\n\t\t\t\tchanged.push({ ...row })\n\t\t\t}\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync knowsOrder(orderId: string): Promise<boolean> {\n\t\treturn this.events.some((row) => row.orderId === orderId)\n\t}\n\n\tasync unsettledOrders(limit = 500): Promise<string[]> {\n\t\tconst isBid = (row: ActivityEvent) => row.type === \"bid\" || (row.type === \"filled\" && row.volumeUsd !== null)\n\t\tconst isSettled = (row: ActivityEvent) => row.type === \"lost\" || (row.type === \"filled\" && row.volumeUsd === null)\n\t\tconst ids: string[] = []\n\t\tfor (let i = this.events.length - 1; i >= 0 && ids.length < capLimit(limit); i--) {\n\t\t\tconst orderId = this.events[i].orderId\n\t\t\tif (!orderId || ids.includes(orderId)) continue\n\t\t\tconst rows = this.events.filter((row) => row.orderId === orderId)\n\t\t\tif (rows.some(isBid) && !rows.some(isSettled)) ids.push(orderId)\n\t\t}\n\t\treturn ids\n\t}\n\n\tasync retypeLegacyBid(orderId: string): Promise<ActivityEvent[]> {\n\t\tconst changed: ActivityEvent[] = []\n\t\tfor (const row of this.events) {\n\t\t\tif (row.orderId === orderId && row.type === \"filled\" && row.volumeUsd !== null) {\n\t\t\t\trow.type = \"bid\"\n\t\t\t\tchanged.push({ ...row })\n\t\t\t}\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync orderHistory(page: number, pageSize: number): Promise<OrderHistoryPage> {\n\t\tconst size = capLimit(pageSize)\n\t\tconst current = Math.max(1, Math.floor(page))\n\t\t// Distinct order ids by newest activity, then the page's slice.\n\t\tconst ordered: string[] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (let i = this.events.length - 1; i >= 0; i--) {\n\t\t\tconst orderId = this.events[i].orderId\n\t\t\tif (orderId && !seen.has(orderId)) {\n\t\t\t\tseen.add(orderId)\n\t\t\t\tordered.push(orderId)\n\t\t\t}\n\t\t}\n\t\tconst ids = ordered.slice((current - 1) * size, current * size)\n\t\treturn {\n\t\t\tpage: current,\n\t\t\tpageSize: size,\n\t\t\ttotal: ordered.length,\n\t\t\torders: ids.map((orderId) => ({\n\t\t\t\torderId,\n\t\t\t\tevents: this.events\n\t\t\t\t\t.filter((row) => row.orderId === orderId)\n\t\t\t\t\t.reverse()\n\t\t\t\t\t.map((row) => ({ ...row })),\n\t\t\t})),\n\t\t}\n\t}\n\n\tasync recordWalletTx(tx: Omit<WalletTx, \"id\" | \"ts\">): Promise<void> {\n\t\t// Mirror SQLite: the optional inbound fields read back as null, never undefined.\n\t\tthis.walletRows.push({ ...tx, tokenIn: tx.tokenIn ?? null, amountIn: tx.amountIn ?? null, id: this.nextWalletId++, ts: Date.now() })\n\t\tif (this.walletRows.length > MAX_ROWS) this.walletRows.splice(0, this.walletRows.length - MAX_ROWS)\n\t}\n\n\tasync walletTxsWithoutAmounts(limit = 200): Promise<WalletTx[]> {\n\t\treturn this.walletRows\n\t\t\t.filter((row) => (row.kind === \"sweep\" || row.kind === \"redeem\") && row.token === null)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync updateWalletTx(\n\t\tid: number,\n\t\tpatch: Pick<WalletTx, \"token\" | \"amount\" | \"to\" | \"tokenIn\" | \"amountIn\">,\n\t): Promise<void> {\n\t\tconst row = this.walletRows.find((candidate) => candidate.id === id)\n\t\tif (row) Object.assign(row, patch)\n\t}\n\n\tasync walletTxs(limit = 100): Promise<WalletTx[]> {\n\t\treturn this.walletRows\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n}\n\nclass MemoryLimitOrderStore implements LimitOrderStore {\n\tprivate orders = new Map<string, LimitOrder>()\n\tprivate fillRows: LimitOrderFill[] = []\n\tprivate nextFillId = 1\n\n\tasync recordFill(fill: LimitOrderFillInsert): Promise<void> {\n\t\tthis.fillRows.push({\n\t\t\tid: this.nextFillId++,\n\t\t\tlimitOrderId: fill.limitOrderId,\n\t\t\tcommitment: fill.commitment,\n\t\t\tbid: fill.bid ?? null,\n\t\t\tamount: fill.amount,\n\t\t\tamountIn: fill.amountIn ?? null,\n\t\t\ttransactionHash: fill.transactionHash ?? null,\n\t\t\tfilledAt: sqliteDatetime(new Date()),\n\t\t})\n\t}\n\n\tasync fillHistory(): Promise<LimitOrderFillRecord[]> {\n\t\treturn this.fillRows.flatMap((fill) => {\n\t\t\tconst order = this.orders.get(fill.limitOrderId)\n\t\t\tif (!order) return []\n\t\t\tconst { id, limitOrderId, amount, amountIn, filledAt } = fill\n\t\t\tconst { book, base, quote, side, price } = order\n\t\t\treturn [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }]\n\t\t})\n\t}\n\n\tasync fills(limitOrderId: string, limit = 100): Promise<LimitOrderFill[]> {\n\t\treturn this.fillRows\n\t\t\t.filter((fill) => fill.limitOrderId === limitOrderId)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((fill) => ({ ...fill }))\n\t}\n\n\tasync create(order: LimitOrderInsert): Promise<LimitOrder> {\n\t\tconst now = sqliteDatetime(new Date())\n\t\tconst stored: LimitOrder = {\n\t\t\t...order,\n\t\t\tacceptedSources: [...order.acceptedSources],\n\t\t\tremaining: order.size,\n\t\t\treserved: \"0\",\n\t\t\texpiresAt: order.expiresAt ?? null,\n\t\t\tstatus: \"open\",\n\t\t\tcommitment: null,\n\t\t\torderNonce: order.orderNonce ?? \"0\",\n\t\t\tbookExpiresAt: null,\n\t\t\tbookPrice: null,\n\t\t\tlastError: null,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t}\n\t\tthis.orders.set(stored.id, stored)\n\t\treturn { ...stored }\n\t}\n\n\tasync get(id: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\treturn order ? { ...order } : null\n\t}\n\n\tasync list(filter: LimitOrderFilter = {}): Promise<LimitOrder[]> {\n\t\treturn [...this.orders.values()]\n\t\t\t.filter(\n\t\t\t\t(order) =>\n\t\t\t\t\t(filter.status === undefined || order.status === filter.status) &&\n\t\t\t\t\t(filter.fillChain === undefined || order.fillChain === filter.fillChain) &&\n\t\t\t\t\t(filter.book === undefined || order.book === filter.book),\n\t\t\t)\n\t\t\t.sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n\t\t\t.map((order) => ({ ...order }))\n\t}\n\n\tasync open(): Promise<LimitOrder[]> {\n\t\treturn this.list({ status: \"open\" })\n\t}\n\n\tasync setPosting(\n\t\tid: string,\n\t\tposting: LimitOrderPosting,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\treturn this.patch(id, posting, only)\n\t}\n\n\tasync setStatus(\n\t\tid: string,\n\t\tstatus: LimitOrderStatus,\n\t\tlastError: string | null = null,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\treturn this.patch(id, { status, lastError }, only)\n\t}\n\n\tasync reserve(id: string, amount: string): Promise<boolean> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order || order.status !== \"open\") return false\n\t\tif (BigInt(amount) > BigInt(order.remaining)) return false\n\t\tconst reserved = BigInt(order.reserved) + BigInt(amount)\n\t\tthis.patch(id, { reserved: reserved.toString() })\n\t\treturn true\n\t}\n\n\tasync drawDown(id: string, amount: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return null\n\t\tconst remaining = BigInt(order.remaining) - BigInt(amount)\n\t\treturn this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() })\n\t}\n\n\tasync clampRemaining(id: string, room: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order || order.status !== \"open\") return null\n\t\tconst sum = BigInt(room) + BigInt(order.reserved)\n\t\tconst target = sum > 0n ? sum : 0n\n\t\tif (target >= BigInt(order.remaining)) return null\n\t\treturn this.patch(id, { remaining: target.toString() })\n\t}\n\n\tasync release(id: string, amount: string): Promise<void> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return\n\t\tconst reserved = BigInt(order.reserved) - BigInt(amount)\n\t\tthis.patch(id, { reserved: (reserved > 0n ? reserved : 0n).toString() })\n\t}\n\n\t/**\n\t * Snapshot and restore, since this store is what `Simplex.start` uses when a\n\t * consumer configures no persistence. A settlement that half-landed here would\n\t * leave an order advertising output it has already paid, which is the same\n\t * money either backend is protecting.\n\t */\n\tasync transaction<T>(settle: () => Promise<T>): Promise<T> {\n\t\tconst snapshot = new Map([...this.orders].map(([id, order]) => [id, { ...order }]))\n\t\tconst fillCount = this.fillRows.length\n\t\ttry {\n\t\t\treturn await settle()\n\t\t} catch (err) {\n\t\t\tthis.orders = snapshot\n\t\t\tthis.fillRows = this.fillRows.slice(0, fillCount)\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tprivate patch(id: string, fields: Partial<LimitOrder>, only?: readonly LimitOrderStatus[]): LimitOrder | null {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return null\n\t\tif (only && only.length > 0 && !only.includes(order.status)) return null\n\t\tconst next = { ...order, ...fields, updatedAt: sqliteDatetime(new Date()) }\n\t\tthis.orders.set(id, next)\n\t\treturn { ...next }\n\t}\n}\n\nclass MemoryStateStore implements StateStore {\n\tprivate state: RuntimeState = {}\n\n\tasync get(): Promise<RuntimeState> {\n\t\treturn { ...this.state }\n\t}\n\n\tasync set(state: RuntimeState): Promise<void> {\n\t\tthis.state = { ...state }\n\t}\n}\n\n/**\n * Process-local data store — the default when `Simplex.start` is given no\n * `data` option.\n *\n * Nothing survives the process. That is fine for tests, short-lived embedded\n * fillers and watch-only observers, but a filler that submits bids should use a\n * durable store: a lost bid record is a Hyperbridge deposit nobody retracts.\n * Settled rows are evicted past a cap; live bids (deposits still reclaimable) are never dropped.\n */\nclass MemoryInventoryStore implements InventoryStore {\n\tprivate rows: InventorySnapshot[] = []\n\n\tasync record(balances: Record<string, number>): Promise<void> {\n\t\tthis.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(new Date()), balances: { ...balances } })\n\t}\n\n\tasync latest(): Promise<InventorySnapshot | null> {\n\t\tconst last = this.rows[this.rows.length - 1]\n\t\treturn last ? { ...last, balances: { ...last.balances } } : null\n\t}\n\n\tasync since(from: Date): Promise<InventorySnapshot[]> {\n\t\tconst cutoff = sqliteDatetime(from)\n\t\treturn this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }))\n\t}\n}\n\nexport class MemoryDataStore implements SimplexDataStore {\n\treadonly bids: BidStore = new MemoryBidStore()\n\treadonly activity: ActivityStore = new MemoryActivityStore()\n\treadonly state: StateStore = new MemoryStateStore()\n\treadonly limitOrders: LimitOrderStore = new MemoryLimitOrderStore()\n\treadonly inventory: InventoryStore = new MemoryInventoryStore()\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport { sqliteDatetime } from \"@/data/memory\"\nimport type { BidInsert, BidStats, BidStore, LimitOrderHold, StoredBid } from \"@/data/types\"\nimport { columnNames } from \"./schema\"\n\n/** Column list shared by every SELECT that returns a StoredBid. */\nconst BID_COLUMNS = `\n\tid,\n\tcommitment,\n\tbid,\n\textrinsic_hash as extrinsicHash,\n\tblock_hash as blockHash,\n\tsuccess,\n\tpending,\n\terror,\n\tcreated_at as createdAt,\n\tretracted,\n\tretracted_at as retractedAt,\n\tretract_extrinsic_hash as retractExtrinsicHash,\n\tdead,\n\tbid,\n\treservations\n`\n\n/**\n * SQLite-backed {@link BidStore}.\n *\n * `node:sqlite` is synchronous, so every method here resolves immediately —\n * the promises exist to satisfy the interface, not because work is deferred.\n * That also means `store` is durable the moment it resolves, which is what the\n * retraction sweep relies on.\n */\n/** The holds on a bid row, tolerating a row written before they were a list. */\nfunction parseHolds(raw: string | null): LimitOrderHold[] {\n\tif (!raw) return []\n\ttry {\n\t\tconst parsed = JSON.parse(raw)\n\t\treturn Array.isArray(parsed) ? parsed : []\n\t} catch {\n\t\treturn []\n\t}\n}\n\nexport class SqliteBidStore implements BidStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"bid-storage\")\n\t\tthis.initializeSchema()\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS bids (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tcommitment TEXT NOT NULL,\n\t\t\t\tbid TEXT,\n\t\t\t\textrinsic_hash TEXT,\n\t\t\t\tblock_hash TEXT,\n\t\t\t\tsuccess INTEGER NOT NULL,\n\t\t\t\tpending INTEGER NOT NULL DEFAULT 0,\n\t\t\t\terror TEXT,\n\t\t\t\tcreated_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tretracted INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tretracted_at TEXT,\n\t\t\t\tretract_extrinsic_hash TEXT,\n\t\t\t\tdead INTEGER NOT NULL DEFAULT 0,\n\t\t\t\treservations TEXT\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_commitment ON bids(commitment);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_success ON bids(success);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_retracted ON bids(retracted);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_created_at ON bids(created_at);\n\t\t`)\n\n\t\t// Databases created before a column existed need it added in place.\n\t\tconst columns = columnNames(this.db, \"bids\")\n\t\tfor (const column of [\"dead\", \"pending\"] as const) {\n\t\t\tif (columns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE bids ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`)\n\t\t\tthis.logger.info({ column }, \"Migrated bid storage schema\")\n\t\t}\n\t\t// A row written before bids carried an identifier has none, and nothing to retract through\n\t\t// the current calls.\n\t\tfor (const column of [\"bid\", \"reservations\"] as const) {\n\t\t\tif (columns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE bids ADD COLUMN ${column} TEXT`)\n\t\t\tthis.logger.info({ column }, \"Migrated bid storage schema\")\n\t\t}\n\n\t\t// After the migration above, not with the other indexes: a database created\n\t\t// before `bid` existed has no such column until the ALTER runs.\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_bids_bid ON bids(commitment, bid)\")\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\tprivate toStoredBid(row: any): StoredBid {\n\t\treturn {\n\t\t\t...row,\n\t\t\tsuccess: Boolean(row.success),\n\t\t\tpending: Boolean(row.pending),\n\t\t\tretracted: Boolean(row.retracted),\n\t\t\tdead: Boolean(row.dead),\n\t\t\treservations: parseHolds(row.reservations),\n\t\t}\n\t}\n\n\tasync store(bid: BidInsert): Promise<void> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO bids (commitment, bid, extrinsic_hash, block_hash, success, pending, error, reservations)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tbid.commitment,\n\t\t\t\tbid.bid ?? null,\n\t\t\t\tbid.extrinsicHash || null,\n\t\t\t\tbid.blockHash || null,\n\t\t\t\tbid.success ? 1 : 0,\n\t\t\t\tbid.pending ? 1 : 0,\n\t\t\t\tbid.error || null,\n\t\t\t\tbid.reservations?.length ? JSON.stringify(bid.reservations) : null,\n\t\t\t)\n\n\t\tthis.logger.debug({ id: result.lastInsertRowid, commitment: bid.commitment, success: bid.success }, \"Bid stored\")\n\t}\n\n\tasync byCommitment(commitment: string): Promise<StoredBid | null> {\n\t\tconst row = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE commitment = ? ORDER BY created_at DESC, id DESC LIMIT 1`)\n\t\t\t.get(commitment)\n\t\treturn row ? this.toStoredBid(row) : null\n\t}\n\n\tasync unretractedReclaimable(): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE (success = 1 OR pending = 1) AND retracted = 0 ORDER BY created_at ASC`)\n\t\t\t.all()\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync expiredUnretracted(maxAgeMs: number): Promise<StoredBid[]> {\n\t\t// Cutoff must match SQLite's datetime('now') format — see sqliteDatetime.\n\t\tconst cutoff = sqliteDatetime(new Date(Date.now() - maxAgeMs))\n\t\tconst rows = this.db\n\t\t\t.prepare(`\n\t\t\t\tSELECT ${BID_COLUMNS}\n\t\t\t\tFROM bids\n\t\t\t\tWHERE (success = 1 OR pending = 1) AND retracted = 0 AND (dead = 1 OR created_at < ?)\n\t\t\t\tORDER BY created_at ASC\n\t\t\t`)\n\t\t\t.all(cutoff)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync byDateRange(from: Date, to: Date): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC`)\n\t\t\t.all(sqliteDatetime(from), sqliteDatetime(to))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync markRetracted(commitment: string, retractExtrinsicHash: string | null): Promise<boolean> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE bids\n\t\t\t\tSET retracted = 1, retracted_at = datetime('now'), retract_extrinsic_hash = ?\n\t\t\t\tWHERE commitment = ? AND retracted = 0\n\t\t\t`)\n\t\t\t.run(retractExtrinsicHash, commitment)\n\n\t\tif (result.changes > 0) {\n\t\t\tthis.logger.info({ commitment, retractExtrinsicHash }, \"Bid marked as retracted\")\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tasync claimReservation(commitment: string, bid?: string): Promise<LimitOrderHold[]> {\n\t\t// Several bids can share a commitment: simplex bids each limit order that can\n\t\t// serve one incoming order, and they differ by their bid identifier. Claiming\n\t\t// by row id is what keeps one bid's settlement from taking another's holds,\n\t\t// and what stops two rows carrying identical reservation JSON being cleared\n\t\t// together by a guard that cannot tell them apart.\n\t\tconst rows = (\n\t\t\tbid === undefined\n\t\t\t\t? this.db\n\t\t\t\t\t\t.prepare(\"SELECT id, reservations FROM bids WHERE commitment = ? AND reservations IS NOT NULL ORDER BY id\")\n\t\t\t\t\t\t.all(commitment)\n\t\t\t\t: this.db\n\t\t\t\t\t\t.prepare(\n\t\t\t\t\t\t\t\"SELECT id, reservations FROM bids WHERE commitment = ? AND bid = ? AND reservations IS NOT NULL ORDER BY id\",\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.all(commitment, bid)\n\t\t) as { id: number; reservations: string }[]\n\n\t\tconst claimed: LimitOrderHold[] = []\n\t\tfor (const row of rows) {\n\t\t\t// Guarded on the row, so two settlers racing the same bid cannot both come\n\t\t\t// away holding it.\n\t\t\tconst result = this.db.prepare(\"UPDATE bids SET reservations = NULL WHERE id = ? AND reservations = ?\").run(row.id, row.reservations)\n\t\t\tif (result.changes === 1) claimed.push(...parseHolds(row.reservations))\n\t\t}\n\t\treturn claimed\n\t}\n\n\tasync byLimitOrder(limitOrderId: string, limit = 100): Promise<StoredBid[]> {\n\t\t// Matched in SQL on the id inside the JSON, then filtered exactly here: the\n\t\t// LIKE narrows the scan, and the parse is what decides.\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE reservations LIKE ? ORDER BY id DESC LIMIT ?`)\n\t\t\t.all(`%${limitOrderId}%`, Math.min(Math.max(limit, 1), 500))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[])\n\t\t\t.map((row) => this.toStoredBid(row))\n\t\t\t.filter((bid) => bid.reservations.some((hold) => hold.limitOrderId === limitOrderId))\n\t}\n\n\tasync markDead(commitment: string): Promise<boolean> {\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE bids SET dead = 1 WHERE commitment = ? AND retracted = 0 AND dead = 0\")\n\t\t\t.run(commitment)\n\n\t\tif (result.changes > 0) {\n\t\t\tthis.logger.debug({ commitment }, \"Bid marked as dead (order filled on-chain)\")\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tasync stats(): Promise<BidStats> {\n\t\t// COUNT is always a number; every SUM is null when the table is empty,\n\t\t// which is what the `|| 0`s below are for.\n\t\tconst stats = this.db\n\t\t\t.prepare(`\n\t\t\t\tSELECT\n\t\t\t\t\tCOUNT(*) as total,\n\t\t\t\t\tSUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,\n\t\t\t\t\tSUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed,\n\t\t\t\t\tSUM(CASE WHEN retracted = 1 THEN 1 ELSE 0 END) as retracted,\n\t\t\t\t\tSUM(CASE WHEN (success = 1 OR pending = 1) AND retracted = 0 THEN 1 ELSE 0 END) as pendingRetraction\n\t\t\t\tFROM bids\n\t\t\t`)\n\t\t\t.get() as unknown as Record<keyof BidStats, number | null>\n\n\t\treturn {\n\t\t\ttotal: stats.total || 0,\n\t\t\tsuccessful: stats.successful || 0,\n\t\t\tfailed: stats.failed || 0,\n\t\t\tretracted: stats.retracted || 0,\n\t\t\tpendingRetraction: stats.pendingRetraction || 0,\n\t\t}\n\t}\n\n\tasync failed(limit = 100): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE success = 0 ORDER BY created_at DESC LIMIT ?`)\n\t\t\t.all(limit)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync byCommitments(commitments: string[]): Promise<StoredBid[]> {\n\t\tif (commitments.length === 0) return []\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT ${BID_COLUMNS} FROM bids WHERE commitment IN (${commitments.map(() => \"?\").join(\",\")}) ORDER BY id DESC`,\n\t\t\t)\n\t\t\t.all(...commitments)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync recent(limit = 100): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids ORDER BY id DESC LIMIT ?`)\n\t\t\t.all(Math.min(Math.max(limit, 1), 500))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport type { InventorySnapshot, InventoryStore } from \"@/data/types\"\n\n/** A row as SQLite returns it, with the balances still as the JSON they are stored as. */\ninterface InventoryRow {\n\tid: number\n\ttakenAt: string\n\tbalances: string\n}\n\n/**\n * SQLite-backed {@link InventoryStore}, in `activity.db` beside the rest of the history.\n *\n * One row per snapshot, with the balances as a JSON object rather than a row per token: a\n * snapshot is only ever read whole, and the set of tokens a solver holds changes over time.\n * A database written before snapshots existed simply gains the table, with nothing in it.\n */\nexport class SqliteInventoryStore implements InventoryStore {\n\tconstructor(private db: DatabaseSync) {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS inventory_snapshots (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\ttaken_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tbalances TEXT NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_inventory_snapshots_taken_at ON inventory_snapshots(taken_at);\n\t\t`)\n\t}\n\n\tprivate toSnapshot(row: InventoryRow): InventorySnapshot {\n\t\treturn { id: row.id, takenAt: row.takenAt, balances: JSON.parse(row.balances) }\n\t}\n\n\tasync record(balances: Record<string, number>): Promise<void> {\n\t\tthis.db.prepare(\"INSERT INTO inventory_snapshots (balances) VALUES (?)\").run(JSON.stringify(balances))\n\t}\n\n\tasync latest(): Promise<InventorySnapshot | null> {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT id, taken_at as takenAt, balances FROM inventory_snapshots ORDER BY id DESC LIMIT 1\")\n\t\t\t.get() as unknown as InventoryRow | undefined\n\t\treturn row ? this.toSnapshot(row) : null\n\t}\n\n\tasync since(from: Date): Promise<InventorySnapshot[]> {\n\t\t// The same \"YYYY-MM-DD HH:MM:SS\" the column holds, so the comparison is between like strings.\n\t\tconst cutoff = from\n\t\t\t.toISOString()\n\t\t\t.replace(\"T\", \" \")\n\t\t\t.replace(/\\.\\d{3}Z$/, \"\")\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, taken_at as takenAt, balances FROM inventory_snapshots WHERE taken_at >= ? ORDER BY taken_at, id\",\n\t\t\t)\n\t\t\t.all(cutoff) as unknown as InventoryRow[]\n\t\treturn rows.map((row) => this.toSnapshot(row))\n\t}\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport { columnNames } from \"./schema\"\nimport type {\n\tLimitOrder,\n\tLimitOrderFill,\n\tLimitOrderFillInsert,\n\tLimitOrderFillRecord,\n\tLimitOrderFilter,\n\tLimitOrderInsert,\n\tLimitOrderPosting,\n\tLimitOrderStatus,\n\tLimitOrderStore,\n} from \"@/data/types\"\n\n/** Column list shared by every SELECT that returns a LimitOrder. */\nconst LIMIT_ORDER_COLUMNS = `\n\tid,\n\tbook,\n\tbase,\n\tquote,\n\tside,\n\tfill_chain as fillChain,\n\tprice,\n\tsize,\n\tremaining,\n\treserved,\n\taccepted_sources as acceptedSources,\n\tttl_secs as ttlSecs,\n\texpires_at as expiresAt,\n\tstatus,\n\tcommitment,\n\torder_nonce as orderNonce,\n\tbook_expires_at as bookExpiresAt,\n\tbook_price as bookPrice,\n\tlast_error as lastError,\n\tcreated_at as createdAt,\n\tupdated_at as updatedAt\n`\n\n/**\n * A guarded write that did not apply because another writer got there first.\n *\n * Only reachable with a second process on one `bids.db`: everything here is\n * synchronous and nothing awaits between the read and the write. It is an error\n * rather than a silent no-op because both writes it guards move real money.\n */\nexport class LimitOrderWriteError extends Error {}\n\n/**\n * SQLite-backed {@link LimitOrderStore}, sharing `bids.db` with the bid store.\n *\n * Amounts are stored as the decimal strings they arrive as. SQLite's own\n * integers top out at 64 bits, which a 1e18 amount overruns as soon as the size\n * passes about 18 tokens, so they are never arithmetic in SQL.\n */\n/** `AND status IN (...)` for a guarded write, or nothing when the caller did not ask. */\nfunction statusGuard(only?: readonly LimitOrderStatus[]): string {\n\treturn only && only.length > 0 ? ` AND status IN (${only.map(() => \"?\").join(\", \")})` : \"\"\n}\n\nexport class SqliteLimitOrderStore implements LimitOrderStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"limit-order-store\")\n\t\tthis.initializeSchema()\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS limit_orders (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tbook TEXT NOT NULL,\n\t\t\t\tbase TEXT NOT NULL,\n\t\t\t\tquote TEXT NOT NULL,\n\t\t\t\tside TEXT NOT NULL,\n\t\t\t\tfill_chain TEXT NOT NULL,\n\t\t\t\tprice TEXT NOT NULL,\n\t\t\t\tsize TEXT NOT NULL,\n\t\t\t\tremaining TEXT NOT NULL,\n\t\t\t\treserved TEXT NOT NULL DEFAULT '0',\n\t\t\t\taccepted_sources TEXT NOT NULL,\n\t\t\t\tttl_secs INTEGER NOT NULL,\n\t\t\t\texpires_at TEXT,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tcommitment TEXT,\n\t\t\t\torder_nonce TEXT NOT NULL DEFAULT '0',\n\t\t\t\tbook_expires_at TEXT,\n\t\t\t\tbook_price TEXT,\n\t\t\t\tlast_error TEXT,\n\t\t\t\tcreated_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tupdated_at TEXT NOT NULL DEFAULT (datetime('now'))\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_status ON limit_orders(status);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_fill_chain ON limit_orders(fill_chain);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_commitment ON limit_orders(commitment);\n\n\t\t\tCREATE TABLE IF NOT EXISTS limit_order_fills (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlimit_order_id TEXT NOT NULL,\n\t\t\t\tcommitment TEXT NOT NULL,\n\t\t\t\tbid TEXT,\n\t\t\t\tamount TEXT NOT NULL,\n\t\t\t\tamount_in TEXT,\n\t\t\t\ttransaction_hash TEXT,\n\t\t\t\tfilled_at TEXT NOT NULL DEFAULT (datetime('now'))\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_order_fills_order ON limit_order_fills(limit_order_id);\n\t\t`)\n\n\t\t// A database created before fills kept what they took in needs the column added in place.\n\t\t// Its existing rows stay null, and are priced at their order's rate when read. The column\n\t\t// is nullable with no default, so a solver still on the earlier version can go on writing\n\t\t// fills to the same file.\n\t\tif (!columnNames(this.db, \"limit_order_fills\").has(\"amount_in\")) {\n\t\t\tthis.db.exec(\"ALTER TABLE limit_order_fills ADD COLUMN amount_in TEXT\")\n\t\t\tthis.logger.info({ column: \"amount_in\" }, \"Migrated limit order fill schema\")\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\tprivate toLimitOrder(row: any): LimitOrder {\n\t\treturn { ...row, acceptedSources: JSON.parse(row.acceptedSources) }\n\t}\n\n\tprivate read(id: string): LimitOrder | null {\n\t\tconst row = this.db.prepare(`SELECT ${LIMIT_ORDER_COLUMNS} FROM limit_orders WHERE id = ?`).get(id)\n\t\treturn row ? this.toLimitOrder(row) : null\n\t}\n\n\tasync create(order: LimitOrderInsert): Promise<LimitOrder> {\n\t\tthis.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO limit_orders (\n\t\t\t\t\tid, book, base, quote, side, fill_chain, price, size, remaining,\n\t\t\t\t\taccepted_sources, ttl_secs, expires_at, order_nonce, status\n\t\t\t\t)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'open')\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\torder.id,\n\t\t\t\torder.book,\n\t\t\t\torder.base,\n\t\t\t\torder.quote,\n\t\t\t\torder.side,\n\t\t\t\torder.fillChain,\n\t\t\t\torder.price,\n\t\t\t\torder.size,\n\t\t\t\torder.size,\n\t\t\t\tJSON.stringify(order.acceptedSources),\n\t\t\t\torder.ttlSecs,\n\t\t\t\torder.expiresAt ?? null,\n\t\t\t\torder.orderNonce ?? \"0\",\n\t\t\t)\n\t\tthis.logger.info({ id: order.id, book: order.book, side: order.side }, \"Limit order created\")\n\t\treturn this.read(order.id)!\n\t}\n\n\tasync get(id: string): Promise<LimitOrder | null> {\n\t\treturn this.read(id)\n\t}\n\n\tasync list(filter: LimitOrderFilter = {}): Promise<LimitOrder[]> {\n\t\tconst clauses: string[] = []\n\t\tconst args: string[] = []\n\t\tfor (const [column, value] of [\n\t\t\t[\"status\", filter.status],\n\t\t\t[\"fill_chain\", filter.fillChain],\n\t\t\t[\"book\", filter.book],\n\t\t] as const) {\n\t\t\tif (value === undefined) continue\n\t\t\tclauses.push(`${column} = ?`)\n\t\t\targs.push(value)\n\t\t}\n\t\tconst where = clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${LIMIT_ORDER_COLUMNS} FROM limit_orders ${where} ORDER BY created_at DESC`)\n\t\t\t.all(...args)\n\t\treturn rows.map((row) => this.toLimitOrder(row))\n\t}\n\n\tasync open(): Promise<LimitOrder[]> {\n\t\treturn this.list({ status: \"open\" })\n\t}\n\n\tasync setPosting(\n\t\tid: string,\n\t\tposting: LimitOrderPosting,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders\n\t\t\t\tSET commitment = ?, book_expires_at = ?, book_price = ?, order_nonce = ?,\n\t\t\t\t status = ?, last_error = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ?${statusGuard(only)}\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tposting.commitment,\n\t\t\t\tposting.bookExpiresAt,\n\t\t\t\tposting.bookPrice,\n\t\t\t\tposting.orderNonce,\n\t\t\t\tposting.status,\n\t\t\t\tposting.lastError,\n\t\t\t\tid,\n\t\t\t\t...(only ?? []),\n\t\t\t)\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync setStatus(\n\t\tid: string,\n\t\tstatus: LimitOrderStatus,\n\t\tlastError: string | null = null,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\tconst result = this.db\n\t\t\t.prepare(\n\t\t\t\t`UPDATE limit_orders SET status = ?, last_error = ?, updated_at = datetime('now') WHERE id = ?${statusGuard(only)}`,\n\t\t\t)\n\t\t\t.run(status, lastError, id, ...(only ?? []))\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync reserve(id: string, amount: string): Promise<boolean> {\n\t\tconst order = this.read(id)\n\t\tif (!order || order.status !== \"open\") return false\n\t\tif (BigInt(amount) > BigInt(order.remaining)) return false\n\t\tconst reserved = BigInt(order.reserved) + BigInt(amount)\n\n\t\t// Guarded on the `reserved` this decision was read against, so a hold that\n\t\t// moved it in between loses here instead of being overwritten. The\n\t\t// arithmetic cannot happen in SQL: a 1e18 amount overruns a 64-bit integer.\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders SET reserved = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ? AND reserved = ? AND status = 'open'\n\t\t\t`)\n\t\t\t.run(reserved.toString(), id, order.reserved)\n\t\treturn result.changes === 1\n\t}\n\n\tasync transaction<T>(settle: () => Promise<T>): Promise<T> {\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tconst result = await settle()\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t\treturn result\n\t\t} catch (err) {\n\t\t\t// Same shape as the state store's, for the same reason: a COMMIT that\n\t\t\t// failed has already rolled back, so an unconditional ROLLBACK would throw\n\t\t\t// over the real cause. Compared against `false` rather than truthiness,\n\t\t\t// because the Node 23 line never got the property and treating `undefined`\n\t\t\t// as \"no transaction\" would wedge the connection mid-transaction.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tasync recordFill(fill: LimitOrderFillInsert): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, amount_in, transaction_hash) VALUES (?, ?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tfill.limitOrderId,\n\t\t\t\tfill.commitment,\n\t\t\t\tfill.bid ?? null,\n\t\t\t\tfill.amount,\n\t\t\t\tfill.amountIn ?? null,\n\t\t\t\tfill.transactionHash ?? null,\n\t\t\t)\n\t}\n\n\tasync fillHistory(): Promise<LimitOrderFillRecord[]> {\n\t\t// Joined rather than left-joined: a fill is only written against an order that exists, and\n\t\t// one without its order's side and rate could not be priced anyway.\n\t\treturn this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT f.id, f.limit_order_id as limitOrderId, o.book, o.base, o.quote, o.side, o.price,\n\t\t\t\t\tf.amount, f.amount_in as amountIn, f.filled_at as filledAt\n\t\t\t\tFROM limit_order_fills f JOIN limit_orders o ON o.id = f.limit_order_id\n\t\t\t\tORDER BY f.filled_at, f.id`,\n\t\t\t)\n\t\t\t.all() as unknown as LimitOrderFillRecord[]\n\t}\n\n\tasync fills(limitOrderId: string, limit = 100): Promise<LimitOrderFill[]> {\n\t\treturn this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, amount_in as amountIn, transaction_hash as transactionHash,\n\t\t\t\t\tfilled_at as filledAt\n\t\t\t\tFROM limit_order_fills WHERE limit_order_id = ? ORDER BY id DESC LIMIT ?`,\n\t\t\t)\n\t\t\t.all(limitOrderId, Math.min(Math.max(limit, 1), 500)) as unknown as LimitOrderFill[]\n\t}\n\n\tasync drawDown(id: string, amount: string): Promise<LimitOrder | null> {\n\t\tconst order = this.read(id)\n\t\tif (!order) return null\n\t\t// Floored: a fill that somehow delivered more than the order had left has\n\t\t// nothing further to give, and a negative remaining would read as capacity.\n\t\tconst remaining = BigInt(order.remaining) - BigInt(amount)\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE limit_orders SET remaining = ?, updated_at = datetime('now') WHERE id = ? AND remaining = ?\")\n\t\t\t.run((remaining > 0n ? remaining : 0n).toString(), id, order.remaining)\n\t\t// A guard that fails is another writer moving `remaining` between the read\n\t\t// and the write, which only a second process on one database can do. Losing\n\t\t// it quietly would leave the order advertising output it has already paid,\n\t\t// so it is reported rather than swallowed.\n\t\tif (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid draw-down`)\n\t\treturn this.read(id)\n\t}\n\n\tasync clampRemaining(id: string, room: string): Promise<LimitOrder | null> {\n\t\tconst order = this.read(id)\n\t\tif (!order || order.status !== \"open\") return null\n\t\tconst sum = BigInt(room) + BigInt(order.reserved)\n\t\tconst target = sum > 0n ? sum : 0n\n\t\tif (target >= BigInt(order.remaining)) return null\n\n\t\t// Guarded on everything the decision was read against. A guard that fails\n\t\t// leaves the row as the other writer left it, which is no worse than not\n\t\t// having clamped, so it answers null and the next pass decides afresh.\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders SET remaining = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ? AND remaining = ? AND reserved = ? AND status = 'open'\n\t\t\t`)\n\t\t\t.run(target.toString(), id, order.remaining, order.reserved)\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync release(id: string, amount: string): Promise<void> {\n\t\tconst order = this.read(id)\n\t\tif (!order) return\n\t\t// Floored at zero: a double release would otherwise leave a negative\n\t\t// reservation, which hands out capacity the order does not have.\n\t\tconst reserved = BigInt(order.reserved) - BigInt(amount)\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE limit_orders SET reserved = ?, updated_at = datetime('now') WHERE id = ? AND reserved = ?\")\n\t\t\t.run((reserved > 0n ? reserved : 0n).toString(), id, order.reserved)\n\t\tif (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid release`)\n\t}\n}\n","import { readFileSync, unlinkSync } from \"node:fs\"\nimport { resolve } from \"node:path\"\nimport type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type { RuntimeState, StateStore } from \"@/data/types\"\n\n/**\n * Where operator state lived before it moved into the database, newest first:\n * beside the databases, then the pre-data-directory location. Imported once and\n * deleted — a paused filler that silently resumed on upgrade would start\n * committing capital the operator had stopped.\n */\nconst RETIRED_STATE_FILE = \"runtime-state.json\"\nconst RETIRED_STATE_DIR = \".filler-data\"\n\n/**\n * SQLite-backed {@link StateStore}, sharing `bids.db` with the bid store.\n *\n * `node:sqlite` is synchronous, so every method resolves immediately — the\n * promises satisfy the interface, they do not defer work.\n *\n * One row per {@link RuntimeState} key, JSON-encoded, so a write touches only\n * the keys it names. That is what the JSON file this replaces could not do: it\n * was rewritten whole and non-atomically, so a crash mid-write truncated it and\n * lost the operator's pause, the very thing it existed to carry across a\n * restart, while two overlapping read-modify-writes could drop one another's key.\n */\nexport class SqliteStateStore implements StateStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tdataDir: string,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"state-storage\")\n\t\tthis.initializeSchema()\n\t\tthis.importRetiredStateFile(dataDir)\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS runtime_state (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tvalue TEXT NOT NULL\n\t\t\t);\n\t\t`)\n\t}\n\n\t/**\n\t * Moves state out of the JSON file a previous version wrote, then removes\n\t * the copies it managed to read. Runs only against an empty table, so it can\n\t * never overwrite state this database already holds.\n\t */\n\tprivate importRetiredStateFile(dataDir: string): void {\n\t\tconst { rows } = this.db.prepare(\"SELECT COUNT(*) as rows FROM runtime_state\").get() as unknown as {\n\t\t\trows: number\n\t\t}\n\t\tif (rows > 0) return\n\n\t\t// Resolved and de-duplicated: `--data-dir .filler-data` makes both entries\n\t\t// the same file under different strings, which would import it twice and\n\t\t// then unlink it twice — the second failing, and logging that a stale file\n\t\t// survived when it did not.\n\t\tconst paths = [\n\t\t\t...new Set([\n\t\t\t\tresolve(dataDir, RETIRED_STATE_FILE),\n\t\t\t\tresolve(process.cwd(), RETIRED_STATE_DIR, RETIRED_STATE_FILE),\n\t\t\t]),\n\t\t]\n\t\tconst recovered: { path: string; state: RuntimeState }[] = []\n\n\t\tfor (const path of paths) {\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown\n\t\t\t\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\t\t\t\tthrow new Error(`expected a JSON object, got ${Array.isArray(parsed) ? \"an array\" : typeof parsed}`)\n\t\t\t\t}\n\t\t\t\trecovered.push({ path, state: parsed as RuntimeState })\n\t\t\t} catch (err) {\n\t\t\t\t// An absent file is the normal case and says nothing. Anything else —\n\t\t\t\t// no read permission, an I/O error, malformed JSON — may still hold the\n\t\t\t\t// operator's pause, so the file stays put for them to recover. Deleting\n\t\t\t\t// it would destroy the state this import exists to rescue.\n\t\t\t\tif ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n\t\t\t\t\tthis.logger.warn({ err, path }, \"Could not read the retired state file; leaving it in place\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst [first] = recovered\n\t\tif (first) {\n\t\t\tthis.write(first.state, true)\n\t\t\tthis.logger.info(\n\t\t\t\t{ path: first.path, keys: Object.keys(first.state) },\n\t\t\t\t\"Imported operator state from its retired JSON file\",\n\t\t\t)\n\t\t}\n\n\t\t// Delete only what was read back. A copy left behind is read again by the\n\t\t// next empty database, resurrecting a pause the operator has since lifted,\n\t\t// so every readable copy goes — not just the one that was imported.\n\t\tfor (const { path } of recovered) {\n\t\t\ttry {\n\t\t\t\tunlinkSync(path)\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err, path }, \"Could not delete the retired state file\")\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate read(): RuntimeState {\n\t\tconst rows = this.db.prepare(\"SELECT key, value FROM runtime_state\").all() as unknown as {\n\t\t\tkey: string\n\t\t\tvalue: string\n\t\t}[]\n\t\tconst state: Record<string, unknown> = {}\n\t\tfor (const row of rows) state[row.key] = JSON.parse(row.value)\n\t\treturn state as RuntimeState\n\t}\n\n\t/**\n\t * Writes the keys `state` names. With `replace`, keys it does not name are\n\t * dropped — the whole-record semantics {@link StateStore.set} promises.\n\t */\n\tprivate write(state: Partial<RuntimeState>, replace: boolean): void {\n\t\tconst upsert = this.db.prepare(`\n\t\t\tINSERT INTO runtime_state (key, value) VALUES (?, ?)\n\t\t\tON CONFLICT(key) DO UPDATE SET value = excluded.value\n\t\t`)\n\t\tconst remove = this.db.prepare(\"DELETE FROM runtime_state WHERE key = ?\")\n\n\t\t// `node:sqlite` has no transaction helper of its own, so the statements are\n\t\t// bracketed by hand. Without this a `set` could be observed with the old\n\t\t// rows deleted and the new ones not yet written.\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tif (replace) this.db.exec(\"DELETE FROM runtime_state\")\n\t\t\tfor (const [key, value] of Object.entries(state)) {\n\t\t\t\tif (value === undefined) remove.run(key)\n\t\t\t\telse upsert.run(key, JSON.stringify(value))\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t} catch (err) {\n\t\t\t// Same shape as `SqliteActivityStore.attachOrder`, for the same reason: a\n\t\t\t// COMMIT that fails has already rolled back, so an unconditional ROLLBACK\n\t\t\t// throws `cannot rollback - no transaction is active` over the real cause\n\t\t\t// — and the real cause is the one worth reading, here more than there,\n\t\t\t// since `persist` only logs it and the migration's write fails a boot.\n\t\t\t// Compared against `false`, not truthiness: the Node 23 line never got\n\t\t\t// the property, and treating `undefined` as \"no transaction\" would skip\n\t\t\t// the ROLLBACK and wedge the connection mid-transaction for good.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tasync get(): Promise<RuntimeState> {\n\t\treturn this.read()\n\t}\n\n\tasync set(state: RuntimeState): Promise<void> {\n\t\tthis.persist(() => this.write(state, true))\n\t}\n\n\t/**\n\t * Merges `patch` in one transaction, leaving every other key untouched. The\n\t * generic {@link StateStore} fallback reads and writes back the whole record,\n\t * which loses a concurrent writer's key; this cannot.\n\t *\n\t * The read-back is inside the guard with the write. Every production caller\n\t * reaches this store through here — `Simplex.pause/resume` and the CLI's\n\t * `setPaused` — so a throw is a pause that reports failure\n\t * while the filler is actually paused, which is the one thing {@link persist}\n\t * exists to prevent. With the database unreadable the requested patch is the\n\t * most that can honestly be said about the state; the failure is in the log,\n\t * and no caller reads this value.\n\t */\n\tasync patch(patch: Partial<RuntimeState>): Promise<RuntimeState> {\n\t\treturn (\n\t\t\tthis.persist(() => {\n\t\t\t\tthis.write(patch, false)\n\t\t\t\treturn this.read()\n\t\t\t}) ?? patch\n\t\t)\n\t}\n\n\t/**\n\t * Runs a write, logging rather than throwing if it fails, and returning\n\t * `undefined` in that case. A pause that cannot be persisted must still pause\n\t * the filler; only its survival across a restart is lost.\n\t */\n\tprivate persist<T>(work: () => T): T | undefined {\n\t\ttry {\n\t\t\treturn work()\n\t\t} catch (err) {\n\t\t\tthis.logger.warn({ err }, \"Could not persist operator state\")\n\t\t\treturn undefined\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/data/sqlite/index.ts","../src/services/Logger.ts","../src/data/sqlite/schema.ts","../src/data/sqlite/activity.ts","../src/data/memory.ts","../src/data/sqlite/bids.ts","../src/data/sqlite/inventory.ts","../src/data/sqlite/limit-orders.ts","../src/data/sqlite/state.ts"],"sourcesContent":["import { existsSync, mkdirSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type {\n\tActivityStore,\n\tBidStore,\n\tInventoryStore,\n\tLimitOrderStore,\n\tSimplexDataStore,\n\tStateStore,\n} from \"@/data/types\"\nimport { SqliteActivityStore } from \"./activity\"\nimport { SqliteBidStore } from \"./bids\"\nimport { SqliteInventoryStore } from \"./inventory\"\nimport { SqliteLimitOrderStore } from \"./limit-orders\"\nimport { SqliteStateStore } from \"./state\"\n\nexport { SqliteActivityStore } from \"./activity\"\nexport { SqliteBidStore } from \"./bids\"\nexport { SqliteInventoryStore } from \"./inventory\"\nexport { SqliteLimitOrderStore } from \"./limit-orders\"\nexport { SqliteStateStore } from \"./state\"\n\n/**\n * How long a write waits for another connection's lock before giving up.\n * Matches the default better-sqlite3 applied for us; `node:sqlite` defaults to 0,\n * which turns any momentary contention into an instant SQLITE_BUSY throw — and a\n * lost bid write is a deposit the retraction sweep can no longer find.\n *\n * Set through the PRAGMA rather than `DatabaseSync`'s `timeout` option on purpose.\n * That option only exists in Node >=22.18 and >=24 (`@types/node` says 22.16, which\n * is wrong — the version table on nodejs.org is authoritative), so it is silently\n * ignored on 22.16, 22.17 and the whole 23 line, all of which `engines.node`\n * admits. The PRAGMA is plain SQLite and works wherever `node:sqlite` does.\n */\nconst BUSY_TIMEOUT_MS = 5_000\n\n/**\n * File-backed {@link SimplexDataStore} — the durable default, and what the\n * `simplex` CLI uses.\n *\n * Keeps bids and activity in separate database files (`bids.db`,\n * `activity.db`) so an existing data directory written by an earlier version is\n * picked up unchanged. Operator state and limit orders ride in `bids.db` beside\n * the bids; a `runtime-state.json` from before that is imported once and deleted.\n * Inventory snapshots ride in `activity.db`, which is the history.\n *\n * Built on `node:sqlite`, so there is nothing to install and nothing to\n * compile — the engine ships inside the Node runtime. That is why the package\n * has no native dependency and why `engines.node` names a version new enough to\n * have `node:sqlite` unflagged.\n */\nexport class SqliteDataStore implements SimplexDataStore {\n\treadonly bids: BidStore\n\treadonly activity: ActivityStore\n\treadonly state: StateStore\n\treadonly limitOrders: LimitOrderStore\n\treadonly inventory: InventoryStore\n\n\tprivate databases: DatabaseSync[]\n\tprivate logger: Logger\n\n\tconstructor(dataDir: string, loggers: LoggerContext = defaultLoggerContext()) {\n\t\tthis.logger = loggers.get(\"data-store\")\n\t\tif (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true })\n\n\t\tconst bidsDb = new DatabaseSync(join(dataDir, \"bids.db\"))\n\t\tconst activityDb = new DatabaseSync(join(dataDir, \"activity.db\"))\n\t\t// `node:sqlite` has no .pragma(); exec() discards the rows these return.\n\t\tfor (const db of [bidsDb, activityDb]) db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)\n\t\tactivityDb.exec(\"PRAGMA journal_mode = WAL\")\n\n\t\tthis.databases = [bidsDb, activityDb]\n\t\tthis.bids = new SqliteBidStore(bidsDb, loggers)\n\t\tthis.activity = new SqliteActivityStore(activityDb, loggers)\n\t\tthis.state = new SqliteStateStore(bidsDb, dataDir, loggers)\n\t\tthis.limitOrders = new SqliteLimitOrderStore(bidsDb, loggers)\n\t\tthis.inventory = new SqliteInventoryStore(activityDb)\n\n\t\tthis.logger.info({ dataDir }, \"SQLite data store opened\")\n\t}\n\n\t/**\n\t * Closes both databases. Called by `Simplex.stop()` when Simplex opened the\n\t * store itself, and by whoever passed it in otherwise — `bootFiller` only\n\t * closes a store it owns, since a caller's may be shared with another solver.\n\t *\n\t * Deliberately not wired to a `process.on(\"exit\")` hook: the old services did\n\t * that in their constructors, which leaked a listener per instance and made\n\t * two fillers in one process race each other's teardown. Both databases are\n\t * crash-safe without an explicit close.\n\t *\n\t * `isOpen` guards the second call: unlike better-sqlite3's no-op close,\n\t * `DatabaseSync.close()` throws on an already-closed handle, and `stop()`\n\t * being called twice is not an error worth logging. Compared against `false`\n\t * rather than truthiness so a runtime without the property (it landed in\n\t * 22.15, below our engines floor but `engines` is only advisory) still\n\t * attempts the close instead of silently skipping every database.\n\t */\n\tasync close(): Promise<void> {\n\t\tfor (const db of this.databases) {\n\t\t\tif (db.isOpen === false) continue\n\t\t\ttry {\n\t\t\t\tdb.close()\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err }, \"Failed to close database cleanly\")\n\t\t\t}\n\t\t}\n\t}\n}\n","import pino, { stdSerializers } from \"pino\"\n\nexport const LOG_LEVELS = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"] as const\n\nexport type LogLevel = (typeof LOG_LEVELS)[number]\n\n/**\n * Anything that accepts a line of text: `process.stdout`, a `fs` write stream, a\n * socket, a `pino-pretty` transform, or an object literal that forwards into\n * your own logger. Node's `Writable` satisfies it as-is.\n */\nexport interface LogSink {\n\twrite(line: string): void\n}\n\ntype LogFn = {\n\t(obj: unknown, msg?: string, ...args: unknown[]): void\n\t(msg: string, ...args: unknown[]): void\n}\n\nexport interface Logger {\n\ttrace: LogFn\n\tdebug: LogFn\n\tinfo: LogFn\n\twarn: LogFn\n\terror: LogFn\n\tfatal: LogFn\n}\n\ntype LoggerOptions = {\n\tmodule?: string\n}\n\n/**\n * viem hangs the whole contract ABI off its errors and repeats the same message\n * once per wrapper in the cause chain. Serialised verbatim, one failed\n * `readContract` prints several hundred lines of ABI JSON. None of it says\n * anything the short message and the root cause don't.\n */\nconst NOISY_ERROR_FIELDS = [\n\t\"abi\",\n\t\"args\",\n\t\"formattedArgs\",\n\t\"metaMessages\",\n\t\"docsPath\",\n\t\"version\",\n\t\"contractAddress\",\n\t\"functionName\",\n\t\"sender\",\n] as const\n\n/** Deep frames are bundler offsets into dist/bin/simplex.js — useless for diagnosis. */\nconst MAX_STACK_FRAMES = 6\n\n/** `at fn (/very/long/.pnpm/viem@x/node_modules/viem/a/b.ts:1:2)` -> `at fn (b.ts:1:2)`. */\nfunction compactFrame(frame: string): string {\n\treturn frame.trim().replace(/\\(?([^\\s(]*[/\\\\])?([^/\\\\\\s()]+:\\d+:\\d+)\\)?$/, \"($2)\")\n}\n\n/** Innermost `cause` message: the line that actually says what went wrong. */\nfunction rootCause(error: unknown): string | undefined {\n\tlet current: unknown = error\n\tlet deepest: string | undefined\n\tfor (let depth = 0; depth < 8; depth++) {\n\t\tconst cause = (current as { cause?: unknown })?.cause\n\t\tif (!cause) break\n\t\tif (cause instanceof Error) {\n\t\t\tdeepest = `${cause.name}: ${cause.message}`.split(\"\\n\")[0]\n\t\t} else if (typeof cause === \"string\") {\n\t\t\tdeepest = cause.split(\"\\n\")[0]\n\t\t}\n\t\tcurrent = cause\n\t}\n\treturn deepest\n}\n\n/** The endpoint viem was talking to, recovered before metaMessages is dropped. */\nfunction endpointOf(error: unknown): string | undefined {\n\tconst meta = (error as { metaMessages?: unknown })?.metaMessages\n\tif (!Array.isArray(meta)) return undefined\n\tconst line = meta.find((m) => typeof m === \"string\" && m.startsWith(\"URL: \"))\n\treturn typeof line === \"string\" ? line.slice(5) : undefined\n}\n\n/**\n * Keeps what identifies the failure — type, short message, details, the failing\n * endpoint and the root cause — and discards the ABI dump, the repeated message\n * bodies and the deep bundler stack frames.\n */\nfunction compactError(error: unknown): unknown {\n\tif (!(error instanceof Error)) return stdSerializers.err(error as Error)\n\n\tconst serialized = stdSerializers.err(error) as Record<string, unknown>\n\tconst endpoint = endpointOf(error)\n\tconst cause = rootCause(error)\n\n\tfor (const field of NOISY_ERROR_FIELDS) delete serialized[field]\n\n\t// `message` restates shortMessage plus every wrapper's context; the short\n\t// form carries the same information in one line. This applies to viem's\n\t// errors, which is where the duplication comes from — an error we built\n\t// ourselves has no wrapper padding to strip, and truncating it would throw\n\t// away the per-provider failure list that is the whole point of the message.\n\tconst shortMessage = serialized.shortMessage\n\tif (error.name === \"QuorumError\") {\n\t\t// Keep it whole.\n\t} else if (typeof shortMessage === \"string\" && shortMessage.length > 0) {\n\t\tserialized.message = shortMessage\n\t\tdelete serialized.shortMessage\n\t} else if (typeof serialized.message === \"string\") {\n\t\tserialized.message = serialized.message.split(\"\\n\")[0]\n\t}\n\n\tif (typeof serialized.stack === \"string\") {\n\t\tconst frames = serialized.stack\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().startsWith(\"at \"))\n\t\t\t.slice(0, MAX_STACK_FRAMES)\n\t\t\t.map(compactFrame)\n\t\tif (frames.length > 0) serialized.stack = frames.join(\" <- \")\n\t\telse delete serialized.stack\n\t}\n\n\t// `name` is always the same string as `type`.\n\tdelete serialized.name\n\n\tif (endpoint) serialized.endpoint = endpoint\n\tif (cause && cause !== serialized.message) serialized.cause = cause\n\n\treturn serialized\n}\n\n/**\n * An isolated logging destination: its own sinks, its own level, its own pino\n * instance.\n *\n * Each `Simplex` owns one, so two fillers in a process log to their own sinks at\n * their own verbosity and never cross-contaminate. Services receive the context\n * their filler was started with and resolve module loggers from it.\n */\nexport class LoggerContext {\n\tprivate level: LogLevel\n\tprivate readonly sinks = new Set<LogSink>()\n\tprivate base: pino.Logger | undefined\n\tprivate readonly children = new Map<string, pino.Logger>()\n\n\tconstructor(options: { level?: LogLevel; sink?: LogSink } = {}) {\n\t\t// Checked here rather than at the first log: pino throws on an unknown level,\n\t\t// and with a silent default that throw would surface much later, from\n\t\t// whatever happened to log first after a sink attached.\n\t\tif (options.level && !LOG_LEVELS.includes(options.level)) {\n\t\t\tthrow new Error(`Unknown log level '${options.level}'. Use one of: ${LOG_LEVELS.join(\", \")}`)\n\t\t}\n\t\tthis.level = options.level ?? \"info\"\n\t\tif (options.sink) this.sinks.add(options.sink)\n\t}\n\n\t/**\n\t * Drops the memoized loggers so the next call rebuilds at the current level\n\t * and sink set. Module loggers are wrappers rather than pino instances\n\t * precisely so this reaches loggers that already exist — replacing the base\n\t * logger would leave every service that had captured a child logging at the\n\t * old level forever.\n\t */\n\tprivate invalidate(): void {\n\t\tthis.base = undefined\n\t\tthis.children.clear()\n\t}\n\n\tprivate baseLogger(): pino.Logger {\n\t\tif (this.base) return this.base\n\t\tconst sinks = this.sinks\n\t\tthis.base = pino(\n\t\t\t{\n\t\t\t\t// With no sink registered nothing can consume a record, so don't pay to\n\t\t\t\t// serialize one. This is what makes an unconfigured filler free as well\n\t\t\t\t// as silent.\n\t\t\t\tlevel: sinks.size === 0 ? \"silent\" : this.level,\n\t\t\t\tserializers: {\n\t\t\t\t\terror: compactError,\n\t\t\t\t\terr: compactError,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\twrite(line: string) {\n\t\t\t\t\tfor (const sink of sinks) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tsink.write(line)\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// A broken sink is the host's problem, not a reason to fail a fill.\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\treturn this.base\n\t}\n\n\tprivate childFor(module?: string): pino.Logger {\n\t\tif (!module) return this.baseLogger()\n\t\tlet child = this.children.get(module)\n\t\tif (!child) {\n\t\t\tchild = this.baseLogger().child({ moduleTag: `[${module}]` })\n\t\t\tthis.children.set(module, child)\n\t\t}\n\t\treturn child\n\t}\n\n\t/** Registers a destination and returns a function that removes it. */\n\taddSink(sink: LogSink): () => void {\n\t\tthis.sinks.add(sink)\n\t\tthis.invalidate()\n\t\treturn () => {\n\t\t\tthis.sinks.delete(sink)\n\t\t\tthis.invalidate()\n\t\t}\n\t}\n\n\t/** Sets verbosity, including for loggers already handed out. */\n\tsetLevel(level: LogLevel): void {\n\t\tif (!LOG_LEVELS.includes(level)) {\n\t\t\tthrow new Error(`Unknown log level '${level}'. Use one of: ${LOG_LEVELS.join(\", \")}`)\n\t\t}\n\t\tthis.level = level\n\t\tthis.invalidate()\n\t}\n\n\t/** A logger tagged with `module`. Safe to hold; it resolves the current state per call. */\n\tget(module?: string): Logger {\n\t\treturn new ModuleLogger(this, module)\n\t}\n\n\t/** @internal — used by {@link ModuleLogger} to resolve per call. */\n\tresolve(module?: string): pino.Logger {\n\t\treturn this.childFor(module)\n\t}\n}\n\n/** Resolves through its context per call, so level and sink changes apply immediately. */\nclass ModuleLogger implements Logger {\n\tconstructor(\n\t\tprivate readonly context: LoggerContext,\n\t\tprivate readonly module?: string,\n\t) {}\n\n\tprivate forward(method: keyof Logger): LogFn {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: pino's LogFn overloads don't survive a generic forward\n\t\treturn (...args: any[]) => (this.context.resolve(this.module)[method] as (...a: any[]) => void)(...args)\n\t}\n\n\ttrace = this.forward(\"trace\")\n\tdebug = this.forward(\"debug\")\n\tinfo = this.forward(\"info\")\n\twarn = this.forward(\"warn\")\n\terror = this.forward(\"error\")\n\tfatal = this.forward(\"fatal\")\n}\n\n/**\n * The fallback context, used by code that is not scoped to a single filler: the\n * CLI, the setup wizard, and anything constructed before a `Simplex` exists.\n *\n * An embedded filler never logs here — `Simplex` creates its own context — so\n * leaving this unconfigured is what keeps the library silent in a host process.\n */\nconst processContext = new LoggerContext()\n\n/**\n * Registers a destination on the process-wide context and returns a function\n * that removes it.\n *\n * This is for the application layer. To capture a specific filler's output, pass\n * `SimplexOptions.logger` instead — a filler's records go to its own context,\n * not here.\n */\nexport function addLogSink(sink: LogSink): () => void {\n\treturn processContext.addSink(sink)\n}\n\n/** Sets the level of the process-wide context. Does not affect running fillers. */\nexport function configureLogger(level: LogLevel): void {\n\tprocessContext.setLevel(level)\n}\n\n/** A logger on the process-wide context. Instance-scoped code resolves from its own {@link LoggerContext}. */\nexport function getLogger(moduleOrOptions?: string | LoggerOptions): Logger {\n\tconst options: LoggerOptions =\n\t\ttypeof moduleOrOptions === \"string\" ? { module: moduleOrOptions } : moduleOrOptions || {}\n\treturn processContext.get(options.module)\n}\n\n/** The fallback context, for constructors that were given none. */\nexport function defaultLoggerContext(): LoggerContext {\n\treturn processContext\n}\n\n/**\n * Resolves a module logger from a context that may be absent.\n *\n * Services read their context off the `FillerConfigService` or\n * `ChainClientManager` they were constructed with, and `bootFiller` always\n * supplies real ones. Hand-built test doubles generally do not, so this falls\n * back to the process context rather than forcing every mock to carry a logger.\n */\nexport function moduleLogger(context: LoggerContext | undefined, module: string): Logger {\n\treturn (context ?? processContext).get(module)\n}\n","import type { DatabaseSync } from \"node:sqlite\"\n\n/** The one column of a `PRAGMA table_info` row this package reads. */\ninterface TableInfoRow {\n\tname: string\n}\n\n/**\n * The columns `table` currently has.\n *\n * Both stores add columns in place to databases written before those columns\n * existed, and this is how they find out what is already there. `node:sqlite`\n * types every column as `SQLOutputValue`, so the row shape is asserted here\n * once instead of at each call site.\n *\n * `table` is interpolated rather than bound because PRAGMA takes no bound\n * parameters; every caller passes a literal.\n */\nexport function columnNames(db: DatabaseSync, table: string): Set<string> {\n\tconst rows = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as TableInfoRow[]\n\treturn new Set(rows.map((row) => row.name))\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type { ActivityEvent, ActivityInsert, ActivityStore, OrderHistoryPage, OrderSummary, WalletTx } from \"@/data/types\"\nimport { columnNames } from \"./schema\"\n\nconst MAX_ROWS = 10_000\nconst PRUNE_EVERY = 500\n\n// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\nfunction toActivityEvent(row: any): ActivityEvent {\n\treturn {\n\t\tid: row.id,\n\t\tts: row.ts,\n\t\ttype: row.type,\n\t\torderId: row.order_id,\n\t\tchainId: row.chain_id,\n\t\tstrategy: row.strategy,\n\t\tsuccess: row.success === null ? null : row.success === 1,\n\t\treason: row.reason,\n\t\tvolumeUsd: row.volume_usd,\n\t\tprofitUsd: row.profit_usd,\n\t\ttxHash: row.tx_hash,\n\t\torder: parseOrder(row.order_json),\n\t}\n}\n\nfunction parseOrder(json: string | null): OrderSummary | null {\n\tif (!json) return null\n\ttry {\n\t\treturn JSON.parse(json) as OrderSummary\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction capLimit(limit: number): number {\n\treturn Math.min(Math.max(limit, 1), 500)\n}\n\n/**\n * SQLite-backed {@link ActivityStore}.\n *\n * Unlike the bid store this is observability rather than correctness, so it\n * prunes: every {@link PRUNE_EVERY} inserts the oldest rows beyond\n * {@link MAX_ROWS} are dropped from both tables. A prune failure is logged and\n * swallowed — losing the trim must never fail the write that triggered it.\n */\nexport class SqliteActivityStore implements ActivityStore {\n\tprivate logger: Logger\n\tprivate insertsSincePrune = 0\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"activity\")\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS events (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tts INTEGER NOT NULL,\n\t\t\t\ttype TEXT NOT NULL,\n\t\t\t\torder_id TEXT,\n\t\t\t\tchain_id INTEGER,\n\t\t\t\tstrategy TEXT,\n\t\t\t\tsuccess INTEGER,\n\t\t\t\treason TEXT,\n\t\t\t\tvolume_usd REAL,\n\t\t\t\tprofit_usd REAL,\n\t\t\t\ttx_hash TEXT\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_events_id ON events(id);\n\t\t\tCREATE TABLE IF NOT EXISTS wallet_txs (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tts INTEGER NOT NULL,\n\t\t\t\tkind TEXT NOT NULL,\n\t\t\t\tchain_id INTEGER,\n\t\t\t\ttoken TEXT,\n\t\t\t\tamount TEXT,\n\t\t\t\tto_address TEXT,\n\t\t\t\ttx_hash TEXT NOT NULL,\n\t\t\t\tsponsored INTEGER\n\t\t\t);\n\t\t`)\n\t\t// Added after the first release: rows written before it have no order summary.\n\t\tconst columns = columnNames(this.db, \"events\")\n\t\tif (!columns.has(\"order_json\")) {\n\t\t\tthis.db.exec(\"ALTER TABLE events ADD COLUMN order_json TEXT\")\n\t\t\tthis.logger.info({ column: \"order_json\" }, \"Migrated activity schema\")\n\t\t}\n\t\t// Added with the ledger's amount columns: the side that came back (vault shares, redeemed assets).\n\t\tconst walletColumns = columnNames(this.db, \"wallet_txs\")\n\t\tfor (const column of [\"token_in\", \"amount_in\"]) {\n\t\t\tif (walletColumns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE wallet_txs ADD COLUMN ${column} TEXT`)\n\t\t\tthis.logger.info({ column }, \"Migrated wallet ledger schema\")\n\t\t}\n\t}\n\n\tasync record(event: ActivityInsert): Promise<ActivityEvent> {\n\t\tconst ts = Date.now()\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO events (ts, type, order_id, chain_id, strategy, success, reason, volume_usd, profit_usd, tx_hash, order_json)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tts,\n\t\t\t\tevent.type,\n\t\t\t\tevent.orderId ?? null,\n\t\t\t\tevent.chainId ?? null,\n\t\t\t\tevent.strategy ?? null,\n\t\t\t\tevent.success === undefined || event.success === null ? null : event.success ? 1 : 0,\n\t\t\t\tevent.reason ?? null,\n\t\t\t\tevent.volumeUsd ?? null,\n\t\t\t\tevent.profitUsd ?? null,\n\t\t\t\tevent.txHash ?? null,\n\t\t\t\tevent.order ? JSON.stringify(event.order) : null,\n\t\t\t)\n\n\t\tif (++this.insertsSincePrune >= PRUNE_EVERY) {\n\t\t\tthis.insertsSincePrune = 0\n\t\t\ttry {\n\t\t\t\tthis.db.prepare(\"DELETE FROM events WHERE id <= (SELECT MAX(id) FROM events) - ?\").run(MAX_ROWS)\n\t\t\t\tthis.db.prepare(\"DELETE FROM wallet_txs WHERE id <= (SELECT MAX(id) FROM wallet_txs) - ?\").run(MAX_ROWS)\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err }, \"Activity log prune failed\")\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: Number(result.lastInsertRowid),\n\t\t\tts,\n\t\t\ttype: event.type,\n\t\t\torderId: event.orderId ?? null,\n\t\t\tchainId: event.chainId ?? null,\n\t\t\tstrategy: event.strategy ?? null,\n\t\t\tsuccess: event.success ?? null,\n\t\t\treason: event.reason ?? null,\n\t\t\tvolumeUsd: event.volumeUsd ?? null,\n\t\t\tprofitUsd: event.profitUsd ?? null,\n\t\t\ttxHash: event.txHash ?? null,\n\t\t\torder: event.order ?? null,\n\t\t}\n\t}\n\n\tasync recent(limit = 100, beforeId?: number): Promise<ActivityEvent[]> {\n\t\tconst capped = capLimit(limit)\n\t\tconst rows = beforeId\n\t\t\t? this.db.prepare(\"SELECT * FROM events WHERE id < ? ORDER BY id DESC LIMIT ?\").all(beforeId, capped)\n\t\t\t: this.db.prepare(\"SELECT * FROM events ORDER BY id DESC LIMIT ?\").all(capped)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync fills(limit = 100): Promise<ActivityEvent[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT * FROM events WHERE type = 'filled' AND tx_hash IS NOT NULL AND chain_id IS NOT NULL ORDER BY id DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync orderIdsMissingSummary(limit = 500): Promise<string[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT order_id FROM events WHERE order_id IS NOT NULL AND order_json IS NULL GROUP BY order_id ORDER BY MAX(id) DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => row.order_id as string)\n\t}\n\n\tasync attachOrder(orderId: string, order: OrderSummary): Promise<ActivityEvent[]> {\n\t\tconst json = JSON.stringify(order)\n\t\t// `node:sqlite` has no transaction wrapper, so the boundary is written out.\n\t\t// The SELECT and the UPDATE have to agree on which rows lacked a summary:\n\t\t// without the transaction a concurrent writer could fill one in between\n\t\t// them, and the ids returned here would name rows this call did not change.\n\t\tlet changed: number[]\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tconst missing = this.db\n\t\t\t\t.prepare(\"SELECT id FROM events WHERE order_id = ? AND order_json IS NULL\")\n\t\t\t\t.all(orderId) as unknown as Array<{ id: number }>\n\t\t\tchanged = missing.map((row) => row.id)\n\t\t\tif (changed.length > 0) {\n\t\t\t\tthis.db.prepare(\"UPDATE events SET order_json = ? WHERE order_id = ? AND order_json IS NULL\").run(json, orderId)\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t} catch (err) {\n\t\t\t// A COMMIT that fails has already rolled back, and rolling back twice\n\t\t\t// throws over the original error — `isTransaction` says which case this is.\n\t\t\t// Compared against `false`, not truthiness: the Node 23 line never got the\n\t\t\t// property, and treating `undefined` as \"no transaction\" would skip the\n\t\t\t// ROLLBACK and leave the connection wedged mid-transaction for good. The\n\t\t\t// inner catch is only for that case; `err` is what propagates either way.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t\tif (changed.length === 0) return []\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM events WHERE id IN (${changed.map(() => \"?\").join(\",\")}) ORDER BY id`)\n\t\t\t.all(...changed)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync unsettledOrders(limit = 500): Promise<string[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT order_id FROM events\n\t\t\t\t WHERE order_id IS NOT NULL\n\t\t\t\t GROUP BY order_id\n\t\t\t\t HAVING SUM(type = 'bid' OR (type = 'filled' AND volume_usd IS NOT NULL)) > 0\n\t\t\t\t AND SUM(type = 'lost' OR (type = 'filled' AND volume_usd IS NULL)) = 0\n\t\t\t\t ORDER BY MAX(id) DESC LIMIT ?`,\n\t\t\t)\n\t\t\t.all(capLimit(limit)) as unknown as Array<{ order_id: string }>\n\t\treturn rows.map((row) => row.order_id)\n\t}\n\n\tasync retypeLegacyBid(orderId: string): Promise<ActivityEvent[]> {\n\t\tconst ids = (\n\t\t\tthis.db\n\t\t\t\t.prepare(\"SELECT id FROM events WHERE order_id = ? AND type = 'filled' AND volume_usd IS NOT NULL\")\n\t\t\t\t.all(orderId) as unknown as Array<{ id: number }>\n\t\t).map((row) => row.id)\n\t\tif (ids.length === 0) return []\n\t\tthis.db\n\t\t\t.prepare(\"UPDATE events SET type = 'bid' WHERE order_id = ? AND type = 'filled' AND volume_usd IS NOT NULL\")\n\t\t\t.run(orderId)\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM events WHERE id IN (${ids.map(() => \"?\").join(\",\")}) ORDER BY id`)\n\t\t\t.all(...ids)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toActivityEvent)\n\t}\n\n\tasync knowsOrder(orderId: string): Promise<boolean> {\n\t\treturn this.db.prepare(\"SELECT 1 FROM events WHERE order_id = ? LIMIT 1\").get(orderId) !== undefined\n\t}\n\n\tasync orderHistory(page: number, pageSize: number): Promise<OrderHistoryPage> {\n\t\tconst size = capLimit(pageSize)\n\t\tconst current = Math.max(1, Math.floor(page))\n\t\tconst total = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(DISTINCT order_id) AS n FROM events WHERE order_id IS NOT NULL\").get() as unknown as {\n\t\t\t\tn: number\n\t\t\t}\n\t\t).n\n\t\tconst heads = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT order_id FROM events WHERE order_id IS NOT NULL GROUP BY order_id ORDER BY MAX(id) DESC LIMIT ? OFFSET ?\",\n\t\t\t)\n\t\t\t.all(size, (current - 1) * size) as unknown as Array<{ order_id: string }>\n\t\tconst ids = heads.map((head) => head.order_id)\n\t\tconst byOrder = new Map<string, ActivityEvent[]>(ids.map((id) => [id, []]))\n\t\tif (ids.length > 0) {\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(`SELECT * FROM events WHERE order_id IN (${ids.map(() => \"?\").join(\",\")}) ORDER BY id DESC`)\n\t\t\t\t.all(...ids)\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\t\tfor (const row of (rows as any[]).map(toActivityEvent)) byOrder.get(row.orderId as string)?.push(row)\n\t\t}\n\t\treturn {\n\t\t\tpage: current,\n\t\t\tpageSize: size,\n\t\t\ttotal,\n\t\t\torders: ids.map((orderId) => ({ orderId, events: byOrder.get(orderId) ?? [] })),\n\t\t}\n\t}\n\n\tasync recordWalletTx(tx: Omit<WalletTx, \"id\" | \"ts\">): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO wallet_txs (ts, kind, chain_id, token, amount, to_address, tx_hash, sponsored, token_in, amount_in)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tDate.now(),\n\t\t\t\ttx.kind,\n\t\t\t\ttx.chainId,\n\t\t\t\ttx.token,\n\t\t\t\ttx.amount,\n\t\t\t\ttx.to,\n\t\t\t\ttx.txHash,\n\t\t\t\ttx.sponsored === null ? null : tx.sponsored ? 1 : 0,\n\t\t\t\ttx.tokenIn ?? null,\n\t\t\t\ttx.amountIn ?? null,\n\t\t\t)\n\t}\n\n\tasync walletTxsWithoutAmounts(limit = 200): Promise<WalletTx[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT * FROM wallet_txs WHERE kind IN ('sweep', 'redeem') AND token IS NULL ORDER BY id DESC LIMIT ?\",\n\t\t\t)\n\t\t\t.all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toWalletTx)\n\t}\n\n\tasync updateWalletTx(\n\t\tid: number,\n\t\tpatch: Pick<WalletTx, \"token\" | \"amount\" | \"to\" | \"tokenIn\" | \"amountIn\">,\n\t): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(\"UPDATE wallet_txs SET token = ?, amount = ?, to_address = ?, token_in = ?, amount_in = ? WHERE id = ?\")\n\t\t\t.run(patch.token, patch.amount, patch.to, patch.tokenIn ?? null, patch.amountIn ?? null, id)\n\t}\n\n\tasync walletTxs(limit = 100): Promise<WalletTx[]> {\n\t\tconst rows = this.db.prepare(\"SELECT * FROM wallet_txs ORDER BY id DESC LIMIT ?\").all(capLimit(limit))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map(toWalletTx)\n\t}\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\nfunction toWalletTx(row: any): WalletTx {\n\treturn {\n\t\tid: row.id,\n\t\tts: row.ts,\n\t\tkind: row.kind,\n\t\tchainId: row.chain_id,\n\t\ttoken: row.token,\n\t\tamount: row.amount,\n\t\tto: row.to_address,\n\t\ttxHash: row.tx_hash,\n\t\tsponsored: row.sponsored === null ? null : row.sponsored === 1,\n\t\ttokenIn: row.token_in ?? null,\n\t\tamountIn: row.amount_in ?? null,\n\t}\n}\n","import type {\n\tActivityEvent,\n\tActivityInsert,\n\tActivityStore,\n\tOrderHistoryPage,\n\tOrderSummary,\n\tBidInsert,\n\tBidStats,\n\tBidStore,\n\tInventorySnapshot,\n\tInventoryStore,\n\tLimitOrder,\n\tLimitOrderFill,\n\tLimitOrderFillInsert,\n\tLimitOrderFillRecord,\n\tLimitOrderFilter,\n\tLimitOrderHold,\n\tLimitOrderInsert,\n\tLimitOrderPosting,\n\tLimitOrderStatus,\n\tLimitOrderStore,\n\tRuntimeState,\n\tSimplexDataStore,\n\tStateStore,\n\tStoredBid,\n\tWalletTx,\n} from \"./types\"\n\n/**\n * Rows kept per collection before pruning. Bid rows are pruned settled-first —\n * a row whose deposit is still reclaimable (successful or pending, unretracted)\n * survives any cap, since the retraction sweep finds work by querying this store.\n */\nconst MAX_ROWS = 10_000\n\n/** Newest-first reads are capped here however large a `limit` the caller passes. */\nconst MAX_LIMIT = 500\n\n/**\n * \"YYYY-MM-DD HH:MM:SS\" in UTC — the format SQLite's `datetime('now')` produces.\n * Kept identical across adapters because `StoredBid.createdAt` is compared\n * lexicographically to find expired bids, and `toISOString()`'s \"T\" separator\n * (ASCII 84) sorts after a space (ASCII 32), which would make every bid look\n * expired.\n */\nexport function sqliteDatetime(date: Date): string {\n\treturn date.toISOString().replace(\"T\", \" \").replace(/\\.\\d{3}Z$/, \"\")\n}\n\n/** A bid whose deposit may be locked on Hyperbridge: confirmed, or still pooled. */\nfunction reclaimable(row: StoredBid): boolean {\n\treturn row.success || row.pending\n}\n\nfunction capLimit(limit: number): number {\n\treturn Math.min(Math.max(limit, 1), MAX_LIMIT)\n}\n\nclass MemoryBidStore implements BidStore {\n\tprivate rows: StoredBid[] = []\n\tprivate nextId = 1\n\n\tasync store(bid: BidInsert): Promise<void> {\n\t\tthis.rows.push({\n\t\t\tid: this.nextId++,\n\t\t\tcommitment: bid.commitment,\n\t\t\tbid: bid.bid ?? null,\n\t\t\textrinsicHash: bid.extrinsicHash ?? null,\n\t\t\tblockHash: bid.blockHash ?? null,\n\t\t\tsuccess: bid.success,\n\t\t\tpending: bid.pending === true,\n\t\t\terror: bid.error ?? null,\n\t\t\tcreatedAt: sqliteDatetime(new Date()),\n\t\t\tretracted: false,\n\t\t\tretractedAt: null,\n\t\t\tretractExtrinsicHash: null,\n\t\t\tdead: false,\n\t\t\treservations: bid.reservations ?? [],\n\t\t})\n\t\tif (this.rows.length > MAX_ROWS) {\n\t\t\t// Only ever drop rows with nothing left to reclaim. A successful or pending\n\t\t\t// bid holds a Hyperbridge deposit that the sweep finds by querying this\n\t\t\t// store, so evicting one by age strands it — and this store is the\n\t\t\t// library's default, which is exactly where that is least survivable.\n\t\t\tconst settled = this.rows.filter((row) => row.retracted || (!row.success && !row.pending))\n\t\t\tconst excess = this.rows.length - MAX_ROWS\n\t\t\tconst drop = new Set(settled.slice(0, excess))\n\t\t\tif (drop.size > 0) this.rows = this.rows.filter((row) => !drop.has(row))\n\t\t}\n\t}\n\n\tasync byCommitment(commitment: string): Promise<StoredBid | null> {\n\t\t// Newest wins: a commitment can be re-bid after a retraction.\n\t\tfor (let i = this.rows.length - 1; i >= 0; i--) {\n\t\t\tif (this.rows[i].commitment === commitment) return { ...this.rows[i] }\n\t\t}\n\t\treturn null\n\t}\n\n\tasync unretractedReclaimable(): Promise<StoredBid[]> {\n\t\treturn this.rows.filter((row) => reclaimable(row) && !row.retracted).map((row) => ({ ...row }))\n\t}\n\n\tasync expiredUnretracted(maxAgeMs: number): Promise<StoredBid[]> {\n\t\tconst cutoff = sqliteDatetime(new Date(Date.now() - maxAgeMs))\n\t\treturn this.rows\n\t\t\t.filter((row) => reclaimable(row) && !row.retracted && (row.dead || row.createdAt < cutoff))\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync markRetracted(commitment: string, retractExtrinsicHash: string | null): Promise<boolean> {\n\t\tlet changed = false\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.retracted) continue\n\t\t\trow.retracted = true\n\t\t\trow.retractedAt = sqliteDatetime(new Date())\n\t\t\trow.retractExtrinsicHash = retractExtrinsicHash\n\t\t\tchanged = true\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync claimReservation(commitment: string, bid?: string): Promise<LimitOrderHold[]> {\n\t\t// Bids on one incoming order share a commitment and differ by identifier, so a\n\t\t// claim names the bid. Without one, every outstanding hold on the commitment\n\t\t// comes back, which is what a filled or dead order needs.\n\t\tconst claimed: LimitOrderHold[] = []\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.reservations.length === 0) continue\n\t\t\tif (bid !== undefined && row.bid !== bid) continue\n\t\t\tclaimed.push(...row.reservations)\n\t\t\trow.reservations = []\n\t\t}\n\t\treturn claimed\n\t}\n\n\tasync byLimitOrder(limitOrderId: string, limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.filter((row) => row.reservations.some((hold) => hold.limitOrderId === limitOrderId))\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync markDead(commitment: string): Promise<boolean> {\n\t\tlet changed = false\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.commitment !== commitment || row.retracted || row.dead) continue\n\t\t\trow.dead = true\n\t\t\tchanged = true\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync recent(limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync byCommitments(commitments: string[]): Promise<StoredBid[]> {\n\t\tconst wanted = new Set(commitments)\n\t\treturn this.rows\n\t\t\t.filter((row) => wanted.has(row.commitment))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync failed(limit = 100): Promise<StoredBid[]> {\n\t\treturn this.rows\n\t\t\t.filter((row) => !row.success)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync byDateRange(from: Date, to: Date): Promise<StoredBid[]> {\n\t\tconst start = sqliteDatetime(from)\n\t\tconst end = sqliteDatetime(to)\n\t\treturn this.rows\n\t\t\t.filter((row) => row.createdAt >= start && row.createdAt <= end)\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync stats(): Promise<BidStats> {\n\t\treturn {\n\t\t\ttotal: this.rows.length,\n\t\t\tsuccessful: this.rows.filter((row) => row.success).length,\n\t\t\tfailed: this.rows.filter((row) => !row.success).length,\n\t\t\tretracted: this.rows.filter((row) => row.retracted).length,\n\t\t\tpendingRetraction: this.rows.filter((row) => reclaimable(row) && !row.retracted).length,\n\t\t}\n\t}\n}\n\nclass MemoryActivityStore implements ActivityStore {\n\tprivate events: ActivityEvent[] = []\n\tprivate walletRows: WalletTx[] = []\n\tprivate nextEventId = 1\n\tprivate nextWalletId = 1\n\n\tasync record(event: ActivityInsert): Promise<ActivityEvent> {\n\t\tconst row: ActivityEvent = {\n\t\t\tid: this.nextEventId++,\n\t\t\tts: Date.now(),\n\t\t\ttype: event.type,\n\t\t\torderId: event.orderId ?? null,\n\t\t\tchainId: event.chainId ?? null,\n\t\t\tstrategy: event.strategy ?? null,\n\t\t\tsuccess: event.success ?? null,\n\t\t\treason: event.reason ?? null,\n\t\t\tvolumeUsd: event.volumeUsd ?? null,\n\t\t\tprofitUsd: event.profitUsd ?? null,\n\t\t\ttxHash: event.txHash ?? null,\n\t\t\torder: event.order ?? null,\n\t\t}\n\t\tthis.events.push(row)\n\t\tif (this.events.length > MAX_ROWS) this.events.splice(0, this.events.length - MAX_ROWS)\n\t\treturn { ...row }\n\t}\n\n\tasync recent(limit = 100, beforeId?: number): Promise<ActivityEvent[]> {\n\t\tconst pool = beforeId === undefined ? this.events : this.events.filter((row) => row.id < beforeId)\n\t\treturn pool\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync fills(limit = 100): Promise<ActivityEvent[]> {\n\t\treturn this.events\n\t\t\t.filter((row) => row.type === \"filled\" && row.txHash !== null && row.chainId !== null)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync orderIdsMissingSummary(limit = 500): Promise<string[]> {\n\t\tconst ids: string[] = []\n\t\tfor (let i = this.events.length - 1; i >= 0 && ids.length < capLimit(limit); i--) {\n\t\t\tconst row = this.events[i]\n\t\t\tif (row.orderId && row.order === null && !ids.includes(row.orderId)) ids.push(row.orderId)\n\t\t}\n\t\treturn ids\n\t}\n\n\tasync attachOrder(orderId: string, order: OrderSummary): Promise<ActivityEvent[]> {\n\t\tconst changed: ActivityEvent[] = []\n\t\tfor (const row of this.events) {\n\t\t\tif (row.orderId === orderId && row.order === null) {\n\t\t\t\trow.order = order\n\t\t\t\tchanged.push({ ...row })\n\t\t\t}\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync knowsOrder(orderId: string): Promise<boolean> {\n\t\treturn this.events.some((row) => row.orderId === orderId)\n\t}\n\n\tasync unsettledOrders(limit = 500): Promise<string[]> {\n\t\tconst isBid = (row: ActivityEvent) => row.type === \"bid\" || (row.type === \"filled\" && row.volumeUsd !== null)\n\t\tconst isSettled = (row: ActivityEvent) => row.type === \"lost\" || (row.type === \"filled\" && row.volumeUsd === null)\n\t\tconst ids: string[] = []\n\t\tfor (let i = this.events.length - 1; i >= 0 && ids.length < capLimit(limit); i--) {\n\t\t\tconst orderId = this.events[i].orderId\n\t\t\tif (!orderId || ids.includes(orderId)) continue\n\t\t\tconst rows = this.events.filter((row) => row.orderId === orderId)\n\t\t\tif (rows.some(isBid) && !rows.some(isSettled)) ids.push(orderId)\n\t\t}\n\t\treturn ids\n\t}\n\n\tasync retypeLegacyBid(orderId: string): Promise<ActivityEvent[]> {\n\t\tconst changed: ActivityEvent[] = []\n\t\tfor (const row of this.events) {\n\t\t\tif (row.orderId === orderId && row.type === \"filled\" && row.volumeUsd !== null) {\n\t\t\t\trow.type = \"bid\"\n\t\t\t\tchanged.push({ ...row })\n\t\t\t}\n\t\t}\n\t\treturn changed\n\t}\n\n\tasync orderHistory(page: number, pageSize: number): Promise<OrderHistoryPage> {\n\t\tconst size = capLimit(pageSize)\n\t\tconst current = Math.max(1, Math.floor(page))\n\t\t// Distinct order ids by newest activity, then the page's slice.\n\t\tconst ordered: string[] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (let i = this.events.length - 1; i >= 0; i--) {\n\t\t\tconst orderId = this.events[i].orderId\n\t\t\tif (orderId && !seen.has(orderId)) {\n\t\t\t\tseen.add(orderId)\n\t\t\t\tordered.push(orderId)\n\t\t\t}\n\t\t}\n\t\tconst ids = ordered.slice((current - 1) * size, current * size)\n\t\treturn {\n\t\t\tpage: current,\n\t\t\tpageSize: size,\n\t\t\ttotal: ordered.length,\n\t\t\torders: ids.map((orderId) => ({\n\t\t\t\torderId,\n\t\t\t\tevents: this.events\n\t\t\t\t\t.filter((row) => row.orderId === orderId)\n\t\t\t\t\t.reverse()\n\t\t\t\t\t.map((row) => ({ ...row })),\n\t\t\t})),\n\t\t}\n\t}\n\n\tasync recordWalletTx(tx: Omit<WalletTx, \"id\" | \"ts\">): Promise<void> {\n\t\t// Mirror SQLite: the optional inbound fields read back as null, never undefined.\n\t\tthis.walletRows.push({ ...tx, tokenIn: tx.tokenIn ?? null, amountIn: tx.amountIn ?? null, id: this.nextWalletId++, ts: Date.now() })\n\t\tif (this.walletRows.length > MAX_ROWS) this.walletRows.splice(0, this.walletRows.length - MAX_ROWS)\n\t}\n\n\tasync walletTxsWithoutAmounts(limit = 200): Promise<WalletTx[]> {\n\t\treturn this.walletRows\n\t\t\t.filter((row) => (row.kind === \"sweep\" || row.kind === \"redeem\") && row.token === null)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n\n\tasync updateWalletTx(\n\t\tid: number,\n\t\tpatch: Pick<WalletTx, \"token\" | \"amount\" | \"to\" | \"tokenIn\" | \"amountIn\">,\n\t): Promise<void> {\n\t\tconst row = this.walletRows.find((candidate) => candidate.id === id)\n\t\tif (row) Object.assign(row, patch)\n\t}\n\n\tasync walletTxs(limit = 100): Promise<WalletTx[]> {\n\t\treturn this.walletRows\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((row) => ({ ...row }))\n\t}\n}\n\nclass MemoryLimitOrderStore implements LimitOrderStore {\n\tprivate orders = new Map<string, LimitOrder>()\n\tprivate fillRows: LimitOrderFill[] = []\n\tprivate nextFillId = 1\n\n\tasync recordFill(fill: LimitOrderFillInsert): Promise<void> {\n\t\tthis.fillRows.push({\n\t\t\tid: this.nextFillId++,\n\t\t\tlimitOrderId: fill.limitOrderId,\n\t\t\tcommitment: fill.commitment,\n\t\t\tbid: fill.bid ?? null,\n\t\t\tamount: fill.amount,\n\t\t\tamountIn: fill.amountIn ?? null,\n\t\t\ttransactionHash: fill.transactionHash ?? null,\n\t\t\tfilledAt: sqliteDatetime(new Date()),\n\t\t})\n\t}\n\n\tasync fillHistory(): Promise<LimitOrderFillRecord[]> {\n\t\treturn this.fillRows.flatMap((fill) => {\n\t\t\tconst order = this.orders.get(fill.limitOrderId)\n\t\t\tif (!order) return []\n\t\t\tconst { id, limitOrderId, amount, amountIn, filledAt } = fill\n\t\t\tconst { book, base, quote, side, price } = order\n\t\t\treturn [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }]\n\t\t})\n\t}\n\n\tasync fills(limitOrderId: string, limit = 100): Promise<LimitOrderFill[]> {\n\t\treturn this.fillRows\n\t\t\t.filter((fill) => fill.limitOrderId === limitOrderId)\n\t\t\t.slice(-capLimit(limit))\n\t\t\t.reverse()\n\t\t\t.map((fill) => ({ ...fill }))\n\t}\n\n\tasync create(order: LimitOrderInsert): Promise<LimitOrder> {\n\t\tconst now = sqliteDatetime(new Date())\n\t\tconst stored: LimitOrder = {\n\t\t\t...order,\n\t\t\tacceptedSources: [...order.acceptedSources],\n\t\t\tremaining: order.size,\n\t\t\treserved: \"0\",\n\t\t\texpiresAt: order.expiresAt ?? null,\n\t\t\tstatus: \"open\",\n\t\t\tcommitment: null,\n\t\t\torderNonce: order.orderNonce ?? \"0\",\n\t\t\tentryPoint: null,\n\t\t\tbookExpiresAt: null,\n\t\t\tbookPrice: null,\n\t\t\tlastError: null,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t}\n\t\tthis.orders.set(stored.id, stored)\n\t\treturn { ...stored }\n\t}\n\n\tasync get(id: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\treturn order ? { ...order } : null\n\t}\n\n\tasync list(filter: LimitOrderFilter = {}): Promise<LimitOrder[]> {\n\t\treturn [...this.orders.values()]\n\t\t\t.filter(\n\t\t\t\t(order) =>\n\t\t\t\t\t(filter.status === undefined || order.status === filter.status) &&\n\t\t\t\t\t(filter.fillChain === undefined || order.fillChain === filter.fillChain) &&\n\t\t\t\t\t(filter.book === undefined || order.book === filter.book),\n\t\t\t)\n\t\t\t.sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n\t\t\t.map((order) => ({ ...order }))\n\t}\n\n\tasync open(): Promise<LimitOrder[]> {\n\t\treturn this.list({ status: \"open\" })\n\t}\n\n\tasync setPosting(\n\t\tid: string,\n\t\tposting: LimitOrderPosting,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\treturn this.patch(id, posting, only)\n\t}\n\n\tasync setStatus(\n\t\tid: string,\n\t\tstatus: LimitOrderStatus,\n\t\tlastError: string | null = null,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\treturn this.patch(id, { status, lastError }, only)\n\t}\n\n\tasync reserve(id: string, amount: string): Promise<boolean> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order || order.status !== \"open\") return false\n\t\tif (BigInt(amount) > BigInt(order.remaining)) return false\n\t\tconst reserved = BigInt(order.reserved) + BigInt(amount)\n\t\tthis.patch(id, { reserved: reserved.toString() })\n\t\treturn true\n\t}\n\n\tasync drawDown(id: string, amount: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return null\n\t\tconst remaining = BigInt(order.remaining) - BigInt(amount)\n\t\treturn this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() })\n\t}\n\n\tasync clampRemaining(id: string, room: string): Promise<LimitOrder | null> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order || order.status !== \"open\") return null\n\t\tconst sum = BigInt(room) + BigInt(order.reserved)\n\t\tconst target = sum > 0n ? sum : 0n\n\t\tif (target >= BigInt(order.remaining)) return null\n\t\treturn this.patch(id, { remaining: target.toString() })\n\t}\n\n\tasync release(id: string, amount: string): Promise<void> {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return\n\t\tconst reserved = BigInt(order.reserved) - BigInt(amount)\n\t\tthis.patch(id, { reserved: (reserved > 0n ? reserved : 0n).toString() })\n\t}\n\n\t/**\n\t * Snapshot and restore, since this store is what `Simplex.start` uses when a\n\t * consumer configures no persistence. A settlement that half-landed here would\n\t * leave an order advertising output it has already paid, which is the same\n\t * money either backend is protecting.\n\t */\n\tasync transaction<T>(settle: () => Promise<T>): Promise<T> {\n\t\tconst snapshot = new Map([...this.orders].map(([id, order]) => [id, { ...order }]))\n\t\tconst fillCount = this.fillRows.length\n\t\ttry {\n\t\t\treturn await settle()\n\t\t} catch (err) {\n\t\t\tthis.orders = snapshot\n\t\t\tthis.fillRows = this.fillRows.slice(0, fillCount)\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tprivate patch(id: string, fields: Partial<LimitOrder>, only?: readonly LimitOrderStatus[]): LimitOrder | null {\n\t\tconst order = this.orders.get(id)\n\t\tif (!order) return null\n\t\tif (only && only.length > 0 && !only.includes(order.status)) return null\n\t\tconst next = { ...order, ...fields, updatedAt: sqliteDatetime(new Date()) }\n\t\tthis.orders.set(id, next)\n\t\treturn { ...next }\n\t}\n}\n\nclass MemoryStateStore implements StateStore {\n\tprivate state: RuntimeState = {}\n\n\tasync get(): Promise<RuntimeState> {\n\t\treturn { ...this.state }\n\t}\n\n\tasync set(state: RuntimeState): Promise<void> {\n\t\tthis.state = { ...state }\n\t}\n}\n\n/**\n * Process-local data store — the default when `Simplex.start` is given no\n * `data` option.\n *\n * Nothing survives the process. That is fine for tests, short-lived embedded\n * fillers and watch-only observers, but a filler that submits bids should use a\n * durable store: a lost bid record is a Hyperbridge deposit nobody retracts.\n * Settled rows are evicted past a cap; live bids (deposits still reclaimable) are never dropped.\n */\nclass MemoryInventoryStore implements InventoryStore {\n\tprivate rows: InventorySnapshot[] = []\n\n\tasync record(balances: Record<string, number>): Promise<void> {\n\t\tthis.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(new Date()), balances: { ...balances } })\n\t}\n\n\tasync latest(): Promise<InventorySnapshot | null> {\n\t\tconst last = this.rows[this.rows.length - 1]\n\t\treturn last ? { ...last, balances: { ...last.balances } } : null\n\t}\n\n\tasync since(from: Date): Promise<InventorySnapshot[]> {\n\t\tconst cutoff = sqliteDatetime(from)\n\t\treturn this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }))\n\t}\n}\n\nexport class MemoryDataStore implements SimplexDataStore {\n\treadonly bids: BidStore = new MemoryBidStore()\n\treadonly activity: ActivityStore = new MemoryActivityStore()\n\treadonly state: StateStore = new MemoryStateStore()\n\treadonly limitOrders: LimitOrderStore = new MemoryLimitOrderStore()\n\treadonly inventory: InventoryStore = new MemoryInventoryStore()\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport { sqliteDatetime } from \"@/data/memory\"\nimport type { BidInsert, BidStats, BidStore, LimitOrderHold, StoredBid } from \"@/data/types\"\nimport { columnNames } from \"./schema\"\n\n/** Column list shared by every SELECT that returns a StoredBid. */\nconst BID_COLUMNS = `\n\tid,\n\tcommitment,\n\tbid,\n\textrinsic_hash as extrinsicHash,\n\tblock_hash as blockHash,\n\tsuccess,\n\tpending,\n\terror,\n\tcreated_at as createdAt,\n\tretracted,\n\tretracted_at as retractedAt,\n\tretract_extrinsic_hash as retractExtrinsicHash,\n\tdead,\n\tbid,\n\treservations\n`\n\n/**\n * SQLite-backed {@link BidStore}.\n *\n * `node:sqlite` is synchronous, so every method here resolves immediately —\n * the promises exist to satisfy the interface, not because work is deferred.\n * That also means `store` is durable the moment it resolves, which is what the\n * retraction sweep relies on.\n */\n/** The holds on a bid row, tolerating a row written before they were a list. */\nfunction parseHolds(raw: string | null): LimitOrderHold[] {\n\tif (!raw) return []\n\ttry {\n\t\tconst parsed = JSON.parse(raw)\n\t\treturn Array.isArray(parsed) ? parsed : []\n\t} catch {\n\t\treturn []\n\t}\n}\n\nexport class SqliteBidStore implements BidStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"bid-storage\")\n\t\tthis.initializeSchema()\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS bids (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tcommitment TEXT NOT NULL,\n\t\t\t\tbid TEXT,\n\t\t\t\textrinsic_hash TEXT,\n\t\t\t\tblock_hash TEXT,\n\t\t\t\tsuccess INTEGER NOT NULL,\n\t\t\t\tpending INTEGER NOT NULL DEFAULT 0,\n\t\t\t\terror TEXT,\n\t\t\t\tcreated_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tretracted INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tretracted_at TEXT,\n\t\t\t\tretract_extrinsic_hash TEXT,\n\t\t\t\tdead INTEGER NOT NULL DEFAULT 0,\n\t\t\t\treservations TEXT\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_commitment ON bids(commitment);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_success ON bids(success);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_retracted ON bids(retracted);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_bids_created_at ON bids(created_at);\n\t\t`)\n\n\t\t// Databases created before a column existed need it added in place.\n\t\tconst columns = columnNames(this.db, \"bids\")\n\t\tfor (const column of [\"dead\", \"pending\"] as const) {\n\t\t\tif (columns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE bids ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`)\n\t\t\tthis.logger.info({ column }, \"Migrated bid storage schema\")\n\t\t}\n\t\t// A row written before bids carried an identifier has none, and nothing to retract through\n\t\t// the current calls.\n\t\tfor (const column of [\"bid\", \"reservations\"] as const) {\n\t\t\tif (columns.has(column)) continue\n\t\t\tthis.db.exec(`ALTER TABLE bids ADD COLUMN ${column} TEXT`)\n\t\t\tthis.logger.info({ column }, \"Migrated bid storage schema\")\n\t\t}\n\n\t\t// After the migration above, not with the other indexes: a database created\n\t\t// before `bid` existed has no such column until the ALTER runs.\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_bids_bid ON bids(commitment, bid)\")\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\tprivate toStoredBid(row: any): StoredBid {\n\t\treturn {\n\t\t\t...row,\n\t\t\tsuccess: Boolean(row.success),\n\t\t\tpending: Boolean(row.pending),\n\t\t\tretracted: Boolean(row.retracted),\n\t\t\tdead: Boolean(row.dead),\n\t\t\treservations: parseHolds(row.reservations),\n\t\t}\n\t}\n\n\tasync store(bid: BidInsert): Promise<void> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO bids (commitment, bid, extrinsic_hash, block_hash, success, pending, error, reservations)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tbid.commitment,\n\t\t\t\tbid.bid ?? null,\n\t\t\t\tbid.extrinsicHash || null,\n\t\t\t\tbid.blockHash || null,\n\t\t\t\tbid.success ? 1 : 0,\n\t\t\t\tbid.pending ? 1 : 0,\n\t\t\t\tbid.error || null,\n\t\t\t\tbid.reservations?.length ? JSON.stringify(bid.reservations) : null,\n\t\t\t)\n\n\t\tthis.logger.debug({ id: result.lastInsertRowid, commitment: bid.commitment, success: bid.success }, \"Bid stored\")\n\t}\n\n\tasync byCommitment(commitment: string): Promise<StoredBid | null> {\n\t\tconst row = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE commitment = ? ORDER BY created_at DESC, id DESC LIMIT 1`)\n\t\t\t.get(commitment)\n\t\treturn row ? this.toStoredBid(row) : null\n\t}\n\n\tasync unretractedReclaimable(): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE (success = 1 OR pending = 1) AND retracted = 0 ORDER BY created_at ASC`)\n\t\t\t.all()\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync expiredUnretracted(maxAgeMs: number): Promise<StoredBid[]> {\n\t\t// Cutoff must match SQLite's datetime('now') format — see sqliteDatetime.\n\t\tconst cutoff = sqliteDatetime(new Date(Date.now() - maxAgeMs))\n\t\tconst rows = this.db\n\t\t\t.prepare(`\n\t\t\t\tSELECT ${BID_COLUMNS}\n\t\t\t\tFROM bids\n\t\t\t\tWHERE (success = 1 OR pending = 1) AND retracted = 0 AND (dead = 1 OR created_at < ?)\n\t\t\t\tORDER BY created_at ASC\n\t\t\t`)\n\t\t\t.all(cutoff)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync byDateRange(from: Date, to: Date): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC`)\n\t\t\t.all(sqliteDatetime(from), sqliteDatetime(to))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync markRetracted(commitment: string, retractExtrinsicHash: string | null): Promise<boolean> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE bids\n\t\t\t\tSET retracted = 1, retracted_at = datetime('now'), retract_extrinsic_hash = ?\n\t\t\t\tWHERE commitment = ? AND retracted = 0\n\t\t\t`)\n\t\t\t.run(retractExtrinsicHash, commitment)\n\n\t\tif (result.changes > 0) {\n\t\t\tthis.logger.info({ commitment, retractExtrinsicHash }, \"Bid marked as retracted\")\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tasync claimReservation(commitment: string, bid?: string): Promise<LimitOrderHold[]> {\n\t\t// Several bids can share a commitment: simplex bids each limit order that can\n\t\t// serve one incoming order, and they differ by their bid identifier. Claiming\n\t\t// by row id is what keeps one bid's settlement from taking another's holds,\n\t\t// and what stops two rows carrying identical reservation JSON being cleared\n\t\t// together by a guard that cannot tell them apart.\n\t\tconst rows = (\n\t\t\tbid === undefined\n\t\t\t\t? this.db\n\t\t\t\t\t\t.prepare(\"SELECT id, reservations FROM bids WHERE commitment = ? AND reservations IS NOT NULL ORDER BY id\")\n\t\t\t\t\t\t.all(commitment)\n\t\t\t\t: this.db\n\t\t\t\t\t\t.prepare(\n\t\t\t\t\t\t\t\"SELECT id, reservations FROM bids WHERE commitment = ? AND bid = ? AND reservations IS NOT NULL ORDER BY id\",\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.all(commitment, bid)\n\t\t) as { id: number; reservations: string }[]\n\n\t\tconst claimed: LimitOrderHold[] = []\n\t\tfor (const row of rows) {\n\t\t\t// Guarded on the row, so two settlers racing the same bid cannot both come\n\t\t\t// away holding it.\n\t\t\tconst result = this.db.prepare(\"UPDATE bids SET reservations = NULL WHERE id = ? AND reservations = ?\").run(row.id, row.reservations)\n\t\t\tif (result.changes === 1) claimed.push(...parseHolds(row.reservations))\n\t\t}\n\t\treturn claimed\n\t}\n\n\tasync byLimitOrder(limitOrderId: string, limit = 100): Promise<StoredBid[]> {\n\t\t// Matched in SQL on the id inside the JSON, then filtered exactly here: the\n\t\t// LIKE narrows the scan, and the parse is what decides.\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE reservations LIKE ? ORDER BY id DESC LIMIT ?`)\n\t\t\t.all(`%${limitOrderId}%`, Math.min(Math.max(limit, 1), 500))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[])\n\t\t\t.map((row) => this.toStoredBid(row))\n\t\t\t.filter((bid) => bid.reservations.some((hold) => hold.limitOrderId === limitOrderId))\n\t}\n\n\tasync markDead(commitment: string): Promise<boolean> {\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE bids SET dead = 1 WHERE commitment = ? AND retracted = 0 AND dead = 0\")\n\t\t\t.run(commitment)\n\n\t\tif (result.changes > 0) {\n\t\t\tthis.logger.debug({ commitment }, \"Bid marked as dead (order filled on-chain)\")\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tasync stats(): Promise<BidStats> {\n\t\t// COUNT is always a number; every SUM is null when the table is empty,\n\t\t// which is what the `|| 0`s below are for.\n\t\tconst stats = this.db\n\t\t\t.prepare(`\n\t\t\t\tSELECT\n\t\t\t\t\tCOUNT(*) as total,\n\t\t\t\t\tSUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,\n\t\t\t\t\tSUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed,\n\t\t\t\t\tSUM(CASE WHEN retracted = 1 THEN 1 ELSE 0 END) as retracted,\n\t\t\t\t\tSUM(CASE WHEN (success = 1 OR pending = 1) AND retracted = 0 THEN 1 ELSE 0 END) as pendingRetraction\n\t\t\t\tFROM bids\n\t\t\t`)\n\t\t\t.get() as unknown as Record<keyof BidStats, number | null>\n\n\t\treturn {\n\t\t\ttotal: stats.total || 0,\n\t\t\tsuccessful: stats.successful || 0,\n\t\t\tfailed: stats.failed || 0,\n\t\t\tretracted: stats.retracted || 0,\n\t\t\tpendingRetraction: stats.pendingRetraction || 0,\n\t\t}\n\t}\n\n\tasync failed(limit = 100): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE success = 0 ORDER BY created_at DESC LIMIT ?`)\n\t\t\t.all(limit)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync byCommitments(commitments: string[]): Promise<StoredBid[]> {\n\t\tif (commitments.length === 0) return []\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT ${BID_COLUMNS} FROM bids WHERE commitment IN (${commitments.map(() => \"?\").join(\",\")}) ORDER BY id DESC`,\n\t\t\t)\n\t\t\t.all(...commitments)\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n\n\tasync recent(limit = 100): Promise<StoredBid[]> {\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${BID_COLUMNS} FROM bids ORDER BY id DESC LIMIT ?`)\n\t\t\t.all(Math.min(Math.max(limit, 1), 500))\n\t\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\t\treturn (rows as any[]).map((row) => this.toStoredBid(row))\n\t}\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport type { InventorySnapshot, InventoryStore } from \"@/data/types\"\n\n/** A row as SQLite returns it, with the balances still as the JSON they are stored as. */\ninterface InventoryRow {\n\tid: number\n\ttakenAt: string\n\tbalances: string\n}\n\n/**\n * SQLite-backed {@link InventoryStore}, in `activity.db` beside the rest of the history.\n *\n * One row per snapshot, with the balances as a JSON object rather than a row per token: a\n * snapshot is only ever read whole, and the set of tokens a solver holds changes over time.\n * A database written before snapshots existed simply gains the table, with nothing in it.\n */\nexport class SqliteInventoryStore implements InventoryStore {\n\tconstructor(private db: DatabaseSync) {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS inventory_snapshots (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\ttaken_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tbalances TEXT NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_inventory_snapshots_taken_at ON inventory_snapshots(taken_at);\n\t\t`)\n\t}\n\n\tprivate toSnapshot(row: InventoryRow): InventorySnapshot {\n\t\treturn { id: row.id, takenAt: row.takenAt, balances: JSON.parse(row.balances) }\n\t}\n\n\tasync record(balances: Record<string, number>): Promise<void> {\n\t\tthis.db.prepare(\"INSERT INTO inventory_snapshots (balances) VALUES (?)\").run(JSON.stringify(balances))\n\t}\n\n\tasync latest(): Promise<InventorySnapshot | null> {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT id, taken_at as takenAt, balances FROM inventory_snapshots ORDER BY id DESC LIMIT 1\")\n\t\t\t.get() as unknown as InventoryRow | undefined\n\t\treturn row ? this.toSnapshot(row) : null\n\t}\n\n\tasync since(from: Date): Promise<InventorySnapshot[]> {\n\t\t// The same \"YYYY-MM-DD HH:MM:SS\" the column holds, so the comparison is between like strings.\n\t\tconst cutoff = from\n\t\t\t.toISOString()\n\t\t\t.replace(\"T\", \" \")\n\t\t\t.replace(/\\.\\d{3}Z$/, \"\")\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, taken_at as takenAt, balances FROM inventory_snapshots WHERE taken_at >= ? ORDER BY taken_at, id\",\n\t\t\t)\n\t\t\t.all(cutoff) as unknown as InventoryRow[]\n\t\treturn rows.map((row) => this.toSnapshot(row))\n\t}\n}\n","import type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport { columnNames } from \"./schema\"\nimport type {\n\tLimitOrder,\n\tLimitOrderFill,\n\tLimitOrderFillInsert,\n\tLimitOrderFillRecord,\n\tLimitOrderFilter,\n\tLimitOrderInsert,\n\tLimitOrderPosting,\n\tLimitOrderStatus,\n\tLimitOrderStore,\n} from \"@/data/types\"\n\n/** Column list shared by every SELECT that returns a LimitOrder. */\nconst LIMIT_ORDER_COLUMNS = `\n\tid,\n\tbook,\n\tbase,\n\tquote,\n\tside,\n\tfill_chain as fillChain,\n\tprice,\n\tsize,\n\tremaining,\n\treserved,\n\taccepted_sources as acceptedSources,\n\tttl_secs as ttlSecs,\n\texpires_at as expiresAt,\n\tstatus,\n\tcommitment,\n\torder_nonce as orderNonce,\n\tentry_point as entryPoint,\n\tbook_expires_at as bookExpiresAt,\n\tbook_price as bookPrice,\n\tlast_error as lastError,\n\tcreated_at as createdAt,\n\tupdated_at as updatedAt\n`\n\n/**\n * A guarded write that did not apply because another writer got there first.\n *\n * Only reachable with a second process on one `bids.db`: everything here is\n * synchronous and nothing awaits between the read and the write. It is an error\n * rather than a silent no-op because both writes it guards move real money.\n */\nexport class LimitOrderWriteError extends Error {}\n\n/**\n * SQLite-backed {@link LimitOrderStore}, sharing `bids.db` with the bid store.\n *\n * Amounts are stored as the decimal strings they arrive as. SQLite's own\n * integers top out at 64 bits, which a 1e18 amount overruns as soon as the size\n * passes about 18 tokens, so they are never arithmetic in SQL.\n */\n/** `AND status IN (...)` for a guarded write, or nothing when the caller did not ask. */\nfunction statusGuard(only?: readonly LimitOrderStatus[]): string {\n\treturn only && only.length > 0 ? ` AND status IN (${only.map(() => \"?\").join(\", \")})` : \"\"\n}\n\nexport class SqliteLimitOrderStore implements LimitOrderStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"limit-order-store\")\n\t\tthis.initializeSchema()\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS limit_orders (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tbook TEXT NOT NULL,\n\t\t\t\tbase TEXT NOT NULL,\n\t\t\t\tquote TEXT NOT NULL,\n\t\t\t\tside TEXT NOT NULL,\n\t\t\t\tfill_chain TEXT NOT NULL,\n\t\t\t\tprice TEXT NOT NULL,\n\t\t\t\tsize TEXT NOT NULL,\n\t\t\t\tremaining TEXT NOT NULL,\n\t\t\t\treserved TEXT NOT NULL DEFAULT '0',\n\t\t\t\taccepted_sources TEXT NOT NULL,\n\t\t\t\tttl_secs INTEGER NOT NULL,\n\t\t\t\texpires_at TEXT,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tcommitment TEXT,\n\t\t\t\torder_nonce TEXT NOT NULL DEFAULT '0',\n\t\t\t\tentry_point TEXT,\n\t\t\t\tbook_expires_at TEXT,\n\t\t\t\tbook_price TEXT,\n\t\t\t\tlast_error TEXT,\n\t\t\t\tcreated_at TEXT NOT NULL DEFAULT (datetime('now')),\n\t\t\t\tupdated_at TEXT NOT NULL DEFAULT (datetime('now'))\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_status ON limit_orders(status);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_fill_chain ON limit_orders(fill_chain);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_orders_commitment ON limit_orders(commitment);\n\n\t\t\tCREATE TABLE IF NOT EXISTS limit_order_fills (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlimit_order_id TEXT NOT NULL,\n\t\t\t\tcommitment TEXT NOT NULL,\n\t\t\t\tbid TEXT,\n\t\t\t\tamount TEXT NOT NULL,\n\t\t\t\tamount_in TEXT,\n\t\t\t\ttransaction_hash TEXT,\n\t\t\t\tfilled_at TEXT NOT NULL DEFAULT (datetime('now'))\n\t\t\t);\n\n\t\t\tCREATE INDEX IF NOT EXISTS idx_limit_order_fills_order ON limit_order_fills(limit_order_id);\n\t\t`)\n\n\t\t// A database written before postings recorded their EntryPoint gets the\n\t\t// column in place. Its rows read null, which is v0.8.\n\t\tif (!columnNames(this.db, \"limit_orders\").has(\"entry_point\")) {\n\t\t\tthis.db.exec(\"ALTER TABLE limit_orders ADD COLUMN entry_point TEXT\")\n\t\t\tthis.logger.info({ column: \"entry_point\" }, \"Migrated limit order storage schema\")\n\t\t}\n\n\t\t// A database created before fills kept what they took in needs the column added in place.\n\t\t// Its existing rows stay null, and are priced at their order's rate when read. The column\n\t\t// is nullable with no default, so a solver still on the earlier version can go on writing\n\t\t// fills to the same file.\n\t\tif (!columnNames(this.db, \"limit_order_fills\").has(\"amount_in\")) {\n\t\t\tthis.db.exec(\"ALTER TABLE limit_order_fills ADD COLUMN amount_in TEXT\")\n\t\t\tthis.logger.info({ column: \"amount_in\" }, \"Migrated limit order fill schema\")\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: raw sqlite row\n\tprivate toLimitOrder(row: any): LimitOrder {\n\t\treturn { ...row, acceptedSources: JSON.parse(row.acceptedSources) }\n\t}\n\n\tprivate read(id: string): LimitOrder | null {\n\t\tconst row = this.db.prepare(`SELECT ${LIMIT_ORDER_COLUMNS} FROM limit_orders WHERE id = ?`).get(id)\n\t\treturn row ? this.toLimitOrder(row) : null\n\t}\n\n\tasync create(order: LimitOrderInsert): Promise<LimitOrder> {\n\t\tthis.db\n\t\t\t.prepare(`\n\t\t\t\tINSERT INTO limit_orders (\n\t\t\t\t\tid, book, base, quote, side, fill_chain, price, size, remaining,\n\t\t\t\t\taccepted_sources, ttl_secs, expires_at, order_nonce, status\n\t\t\t\t)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'open')\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\torder.id,\n\t\t\t\torder.book,\n\t\t\t\torder.base,\n\t\t\t\torder.quote,\n\t\t\t\torder.side,\n\t\t\t\torder.fillChain,\n\t\t\t\torder.price,\n\t\t\t\torder.size,\n\t\t\t\torder.size,\n\t\t\t\tJSON.stringify(order.acceptedSources),\n\t\t\t\torder.ttlSecs,\n\t\t\t\torder.expiresAt ?? null,\n\t\t\t\torder.orderNonce ?? \"0\",\n\t\t\t)\n\t\tthis.logger.info({ id: order.id, book: order.book, side: order.side }, \"Limit order created\")\n\t\treturn this.read(order.id)!\n\t}\n\n\tasync get(id: string): Promise<LimitOrder | null> {\n\t\treturn this.read(id)\n\t}\n\n\tasync list(filter: LimitOrderFilter = {}): Promise<LimitOrder[]> {\n\t\tconst clauses: string[] = []\n\t\tconst args: string[] = []\n\t\tfor (const [column, value] of [\n\t\t\t[\"status\", filter.status],\n\t\t\t[\"fill_chain\", filter.fillChain],\n\t\t\t[\"book\", filter.book],\n\t\t] as const) {\n\t\t\tif (value === undefined) continue\n\t\t\tclauses.push(`${column} = ?`)\n\t\t\targs.push(value)\n\t\t}\n\t\tconst where = clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\"\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT ${LIMIT_ORDER_COLUMNS} FROM limit_orders ${where} ORDER BY created_at DESC`)\n\t\t\t.all(...args)\n\t\treturn rows.map((row) => this.toLimitOrder(row))\n\t}\n\n\tasync open(): Promise<LimitOrder[]> {\n\t\treturn this.list({ status: \"open\" })\n\t}\n\n\tasync setPosting(\n\t\tid: string,\n\t\tposting: LimitOrderPosting,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders\n\t\t\t\tSET commitment = ?, book_expires_at = ?, book_price = ?, order_nonce = ?,\n\t\t\t\t entry_point = ?, status = ?, last_error = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ?${statusGuard(only)}\n\t\t\t`)\n\t\t\t.run(\n\t\t\t\tposting.commitment,\n\t\t\t\tposting.bookExpiresAt,\n\t\t\t\tposting.bookPrice,\n\t\t\t\tposting.orderNonce,\n\t\t\t\tposting.entryPoint,\n\t\t\t\tposting.status,\n\t\t\t\tposting.lastError,\n\t\t\t\tid,\n\t\t\t\t...(only ?? []),\n\t\t\t)\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync setStatus(\n\t\tid: string,\n\t\tstatus: LimitOrderStatus,\n\t\tlastError: string | null = null,\n\t\tonly?: readonly LimitOrderStatus[],\n\t): Promise<LimitOrder | null> {\n\t\tconst result = this.db\n\t\t\t.prepare(\n\t\t\t\t`UPDATE limit_orders SET status = ?, last_error = ?, updated_at = datetime('now') WHERE id = ?${statusGuard(only)}`,\n\t\t\t)\n\t\t\t.run(status, lastError, id, ...(only ?? []))\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync reserve(id: string, amount: string): Promise<boolean> {\n\t\tconst order = this.read(id)\n\t\tif (!order || order.status !== \"open\") return false\n\t\tif (BigInt(amount) > BigInt(order.remaining)) return false\n\t\tconst reserved = BigInt(order.reserved) + BigInt(amount)\n\n\t\t// Guarded on the `reserved` this decision was read against, so a hold that\n\t\t// moved it in between loses here instead of being overwritten. The\n\t\t// arithmetic cannot happen in SQL: a 1e18 amount overruns a 64-bit integer.\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders SET reserved = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ? AND reserved = ? AND status = 'open'\n\t\t\t`)\n\t\t\t.run(reserved.toString(), id, order.reserved)\n\t\treturn result.changes === 1\n\t}\n\n\tasync transaction<T>(settle: () => Promise<T>): Promise<T> {\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tconst result = await settle()\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t\treturn result\n\t\t} catch (err) {\n\t\t\t// Same shape as the state store's, for the same reason: a COMMIT that\n\t\t\t// failed has already rolled back, so an unconditional ROLLBACK would throw\n\t\t\t// over the real cause. Compared against `false` rather than truthiness,\n\t\t\t// because the Node 23 line never got the property and treating `undefined`\n\t\t\t// as \"no transaction\" would wedge the connection mid-transaction.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tasync recordFill(fill: LimitOrderFillInsert): Promise<void> {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, amount_in, transaction_hash) VALUES (?, ?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tfill.limitOrderId,\n\t\t\t\tfill.commitment,\n\t\t\t\tfill.bid ?? null,\n\t\t\t\tfill.amount,\n\t\t\t\tfill.amountIn ?? null,\n\t\t\t\tfill.transactionHash ?? null,\n\t\t\t)\n\t}\n\n\tasync fillHistory(): Promise<LimitOrderFillRecord[]> {\n\t\t// Joined rather than left-joined: a fill is only written against an order that exists, and\n\t\t// one without its order's side and rate could not be priced anyway.\n\t\treturn this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT f.id, f.limit_order_id as limitOrderId, o.book, o.base, o.quote, o.side, o.price,\n\t\t\t\t\tf.amount, f.amount_in as amountIn, f.filled_at as filledAt\n\t\t\t\tFROM limit_order_fills f JOIN limit_orders o ON o.id = f.limit_order_id\n\t\t\t\tORDER BY f.filled_at, f.id`,\n\t\t\t)\n\t\t\t.all() as unknown as LimitOrderFillRecord[]\n\t}\n\n\tasync fills(limitOrderId: string, limit = 100): Promise<LimitOrderFill[]> {\n\t\treturn this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, amount_in as amountIn, transaction_hash as transactionHash,\n\t\t\t\t\tfilled_at as filledAt\n\t\t\t\tFROM limit_order_fills WHERE limit_order_id = ? ORDER BY id DESC LIMIT ?`,\n\t\t\t)\n\t\t\t.all(limitOrderId, Math.min(Math.max(limit, 1), 500)) as unknown as LimitOrderFill[]\n\t}\n\n\tasync drawDown(id: string, amount: string): Promise<LimitOrder | null> {\n\t\tconst order = this.read(id)\n\t\tif (!order) return null\n\t\t// Floored: a fill that somehow delivered more than the order had left has\n\t\t// nothing further to give, and a negative remaining would read as capacity.\n\t\tconst remaining = BigInt(order.remaining) - BigInt(amount)\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE limit_orders SET remaining = ?, updated_at = datetime('now') WHERE id = ? AND remaining = ?\")\n\t\t\t.run((remaining > 0n ? remaining : 0n).toString(), id, order.remaining)\n\t\t// A guard that fails is another writer moving `remaining` between the read\n\t\t// and the write, which only a second process on one database can do. Losing\n\t\t// it quietly would leave the order advertising output it has already paid,\n\t\t// so it is reported rather than swallowed.\n\t\tif (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid draw-down`)\n\t\treturn this.read(id)\n\t}\n\n\tasync clampRemaining(id: string, room: string): Promise<LimitOrder | null> {\n\t\tconst order = this.read(id)\n\t\tif (!order || order.status !== \"open\") return null\n\t\tconst sum = BigInt(room) + BigInt(order.reserved)\n\t\tconst target = sum > 0n ? sum : 0n\n\t\tif (target >= BigInt(order.remaining)) return null\n\n\t\t// Guarded on everything the decision was read against. A guard that fails\n\t\t// leaves the row as the other writer left it, which is no worse than not\n\t\t// having clamped, so it answers null and the next pass decides afresh.\n\t\tconst result = this.db\n\t\t\t.prepare(`\n\t\t\t\tUPDATE limit_orders SET remaining = ?, updated_at = datetime('now')\n\t\t\t\tWHERE id = ? AND remaining = ? AND reserved = ? AND status = 'open'\n\t\t\t`)\n\t\t\t.run(target.toString(), id, order.remaining, order.reserved)\n\t\treturn result.changes === 1 ? this.read(id) : null\n\t}\n\n\tasync release(id: string, amount: string): Promise<void> {\n\t\tconst order = this.read(id)\n\t\tif (!order) return\n\t\t// Floored at zero: a double release would otherwise leave a negative\n\t\t// reservation, which hands out capacity the order does not have.\n\t\tconst reserved = BigInt(order.reserved) - BigInt(amount)\n\t\tconst result = this.db\n\t\t\t.prepare(\"UPDATE limit_orders SET reserved = ?, updated_at = datetime('now') WHERE id = ? AND reserved = ?\")\n\t\t\t.run((reserved > 0n ? reserved : 0n).toString(), id, order.reserved)\n\t\tif (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid release`)\n\t}\n}\n","import { readFileSync, unlinkSync } from \"node:fs\"\nimport { resolve } from \"node:path\"\nimport type { DatabaseSync } from \"node:sqlite\"\nimport { defaultLoggerContext, type Logger, type LoggerContext } from \"@/services/Logger\"\nimport type { RuntimeState, StateStore } from \"@/data/types\"\n\n/**\n * Where operator state lived before it moved into the database, newest first:\n * beside the databases, then the pre-data-directory location. Imported once and\n * deleted — a paused filler that silently resumed on upgrade would start\n * committing capital the operator had stopped.\n */\nconst RETIRED_STATE_FILE = \"runtime-state.json\"\nconst RETIRED_STATE_DIR = \".filler-data\"\n\n/**\n * SQLite-backed {@link StateStore}, sharing `bids.db` with the bid store.\n *\n * `node:sqlite` is synchronous, so every method resolves immediately — the\n * promises satisfy the interface, they do not defer work.\n *\n * One row per {@link RuntimeState} key, JSON-encoded, so a write touches only\n * the keys it names. That is what the JSON file this replaces could not do: it\n * was rewritten whole and non-atomically, so a crash mid-write truncated it and\n * lost the operator's pause, the very thing it existed to carry across a\n * restart, while two overlapping read-modify-writes could drop one another's key.\n */\nexport class SqliteStateStore implements StateStore {\n\tprivate logger: Logger\n\n\tconstructor(\n\t\tprivate db: DatabaseSync,\n\t\tdataDir: string,\n\t\tloggers: LoggerContext = defaultLoggerContext(),\n\t) {\n\t\tthis.logger = loggers.get(\"state-storage\")\n\t\tthis.initializeSchema()\n\t\tthis.importRetiredStateFile(dataDir)\n\t}\n\n\tprivate initializeSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS runtime_state (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tvalue TEXT NOT NULL\n\t\t\t);\n\t\t`)\n\t}\n\n\t/**\n\t * Moves state out of the JSON file a previous version wrote, then removes\n\t * the copies it managed to read. Runs only against an empty table, so it can\n\t * never overwrite state this database already holds.\n\t */\n\tprivate importRetiredStateFile(dataDir: string): void {\n\t\tconst { rows } = this.db.prepare(\"SELECT COUNT(*) as rows FROM runtime_state\").get() as unknown as {\n\t\t\trows: number\n\t\t}\n\t\tif (rows > 0) return\n\n\t\t// Resolved and de-duplicated: `--data-dir .filler-data` makes both entries\n\t\t// the same file under different strings, which would import it twice and\n\t\t// then unlink it twice — the second failing, and logging that a stale file\n\t\t// survived when it did not.\n\t\tconst paths = [\n\t\t\t...new Set([\n\t\t\t\tresolve(dataDir, RETIRED_STATE_FILE),\n\t\t\t\tresolve(process.cwd(), RETIRED_STATE_DIR, RETIRED_STATE_FILE),\n\t\t\t]),\n\t\t]\n\t\tconst recovered: { path: string; state: RuntimeState }[] = []\n\n\t\tfor (const path of paths) {\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown\n\t\t\t\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\t\t\t\tthrow new Error(`expected a JSON object, got ${Array.isArray(parsed) ? \"an array\" : typeof parsed}`)\n\t\t\t\t}\n\t\t\t\trecovered.push({ path, state: parsed as RuntimeState })\n\t\t\t} catch (err) {\n\t\t\t\t// An absent file is the normal case and says nothing. Anything else —\n\t\t\t\t// no read permission, an I/O error, malformed JSON — may still hold the\n\t\t\t\t// operator's pause, so the file stays put for them to recover. Deleting\n\t\t\t\t// it would destroy the state this import exists to rescue.\n\t\t\t\tif ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n\t\t\t\t\tthis.logger.warn({ err, path }, \"Could not read the retired state file; leaving it in place\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst [first] = recovered\n\t\tif (first) {\n\t\t\tthis.write(first.state, true)\n\t\t\tthis.logger.info(\n\t\t\t\t{ path: first.path, keys: Object.keys(first.state) },\n\t\t\t\t\"Imported operator state from its retired JSON file\",\n\t\t\t)\n\t\t}\n\n\t\t// Delete only what was read back. A copy left behind is read again by the\n\t\t// next empty database, resurrecting a pause the operator has since lifted,\n\t\t// so every readable copy goes — not just the one that was imported.\n\t\tfor (const { path } of recovered) {\n\t\t\ttry {\n\t\t\t\tunlinkSync(path)\n\t\t\t} catch (err) {\n\t\t\t\tthis.logger.warn({ err, path }, \"Could not delete the retired state file\")\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate read(): RuntimeState {\n\t\tconst rows = this.db.prepare(\"SELECT key, value FROM runtime_state\").all() as unknown as {\n\t\t\tkey: string\n\t\t\tvalue: string\n\t\t}[]\n\t\tconst state: Record<string, unknown> = {}\n\t\tfor (const row of rows) state[row.key] = JSON.parse(row.value)\n\t\treturn state as RuntimeState\n\t}\n\n\t/**\n\t * Writes the keys `state` names. With `replace`, keys it does not name are\n\t * dropped — the whole-record semantics {@link StateStore.set} promises.\n\t */\n\tprivate write(state: Partial<RuntimeState>, replace: boolean): void {\n\t\tconst upsert = this.db.prepare(`\n\t\t\tINSERT INTO runtime_state (key, value) VALUES (?, ?)\n\t\t\tON CONFLICT(key) DO UPDATE SET value = excluded.value\n\t\t`)\n\t\tconst remove = this.db.prepare(\"DELETE FROM runtime_state WHERE key = ?\")\n\n\t\t// `node:sqlite` has no transaction helper of its own, so the statements are\n\t\t// bracketed by hand. Without this a `set` could be observed with the old\n\t\t// rows deleted and the new ones not yet written.\n\t\tthis.db.exec(\"BEGIN\")\n\t\ttry {\n\t\t\tif (replace) this.db.exec(\"DELETE FROM runtime_state\")\n\t\t\tfor (const [key, value] of Object.entries(state)) {\n\t\t\t\tif (value === undefined) remove.run(key)\n\t\t\t\telse upsert.run(key, JSON.stringify(value))\n\t\t\t}\n\t\t\tthis.db.exec(\"COMMIT\")\n\t\t} catch (err) {\n\t\t\t// Same shape as `SqliteActivityStore.attachOrder`, for the same reason: a\n\t\t\t// COMMIT that fails has already rolled back, so an unconditional ROLLBACK\n\t\t\t// throws `cannot rollback - no transaction is active` over the real cause\n\t\t\t// — and the real cause is the one worth reading, here more than there,\n\t\t\t// since `persist` only logs it and the migration's write fails a boot.\n\t\t\t// Compared against `false`, not truthiness: the Node 23 line never got\n\t\t\t// the property, and treating `undefined` as \"no transaction\" would skip\n\t\t\t// the ROLLBACK and wedge the connection mid-transaction for good.\n\t\t\tif (this.db.isTransaction !== false) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(\"ROLLBACK\")\n\t\t\t\t} catch {\n\t\t\t\t\t// Nothing to roll back, or the connection is already gone.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\t}\n\n\tasync get(): Promise<RuntimeState> {\n\t\treturn this.read()\n\t}\n\n\tasync set(state: RuntimeState): Promise<void> {\n\t\tthis.persist(() => this.write(state, true))\n\t}\n\n\t/**\n\t * Merges `patch` in one transaction, leaving every other key untouched. The\n\t * generic {@link StateStore} fallback reads and writes back the whole record,\n\t * which loses a concurrent writer's key; this cannot.\n\t *\n\t * The read-back is inside the guard with the write. Every production caller\n\t * reaches this store through here — `Simplex.pause/resume` and the CLI's\n\t * `setPaused` — so a throw is a pause that reports failure\n\t * while the filler is actually paused, which is the one thing {@link persist}\n\t * exists to prevent. With the database unreadable the requested patch is the\n\t * most that can honestly be said about the state; the failure is in the log,\n\t * and no caller reads this value.\n\t */\n\tasync patch(patch: Partial<RuntimeState>): Promise<RuntimeState> {\n\t\treturn (\n\t\t\tthis.persist(() => {\n\t\t\t\tthis.write(patch, false)\n\t\t\t\treturn this.read()\n\t\t\t}) ?? patch\n\t\t)\n\t}\n\n\t/**\n\t * Runs a write, logging rather than throwing if it fails, and returning\n\t * `undefined` in that case. A pause that cannot be persisted must still pause\n\t * the filler; only its survival across a restart is lost.\n\t */\n\tprivate persist<T>(work: () => T): T | undefined {\n\t\ttry {\n\t\t\treturn work()\n\t\t} catch (err) {\n\t\t\tthis.logger.warn({ err }, \"Could not persist operator state\")\n\t\t\treturn undefined\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -417,6 +417,12 @@ interface LimitOrder {
417
417
  commitment: string | null;
418
418
  /** Bumped on every repost, so each posting hashes differently. */
419
419
  orderNonce: string;
420
+ /**
421
+ * The EntryPoint the current posting's op was signed for. Null while nothing is
422
+ * posted, and on a posting stored before this was recorded, which was signed
423
+ * for v0.8.
424
+ */
425
+ entryPoint: string | null;
420
426
  /** When the current posting expires, as the orderbook reported it. */
421
427
  bookExpiresAt: string | null;
422
428
  /** `Order.price` from the orderbook, which shades `price` by the protocol fee. */
@@ -454,6 +460,7 @@ interface LimitOrderPosting {
454
460
  bookExpiresAt: string | null;
455
461
  bookPrice: string | null;
456
462
  orderNonce: string;
463
+ entryPoint: string | null;
457
464
  status: LimitOrderStatus;
458
465
  lastError: string | null;
459
466
  }