@hyperbridge/simplex 0.18.1 → 0.18.3

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
@@ -442,18 +442,31 @@ var BID_COLUMNS = `
442
442
  retracted_at as retractedAt,
443
443
  retract_extrinsic_hash as retractExtrinsicHash,
444
444
  dead,
445
- bid,
445
+ on_orderbook as onOrderbook,
446
+ priced_by as pricedBy,
446
447
  reservations
447
448
  `;
448
- function parseHolds(raw) {
449
- if (!raw) return [];
449
+ function parseJson(raw) {
450
+ if (!raw) return null;
450
451
  try {
451
- const parsed = JSON.parse(raw);
452
- return Array.isArray(parsed) ? parsed : [];
452
+ return JSON.parse(raw);
453
453
  } catch {
454
- return [];
454
+ return null;
455
455
  }
456
456
  }
457
+ function pricedBy(row) {
458
+ const current = parseJson(row.pricedBy);
459
+ if (current && typeof current === "object") return current;
460
+ const legacy = parseJson(row.reservations);
461
+ const first = Array.isArray(legacy) ? legacy[0] : null;
462
+ if (!first || typeof first !== "object" || typeof first.limitOrderId !== "string") return null;
463
+ return {
464
+ limitOrderId: first.limitOrderId,
465
+ payout: String(first.payout ?? first.amount),
466
+ ...first.take !== void 0 ? { take: String(first.take) } : {},
467
+ ...first.takeDecimals !== void 0 ? { takeDecimals: Number(first.takeDecimals) } : {}
468
+ };
469
+ }
457
470
  var SqliteBidStore = class {
458
471
  constructor(db, loggers = defaultLoggerContext()) {
459
472
  this.db = db;
@@ -476,7 +489,11 @@ var SqliteBidStore = class {
476
489
  retracted_at TEXT,
477
490
  retract_extrinsic_hash TEXT,
478
491
  dead INTEGER NOT NULL DEFAULT 0,
479
- reservations TEXT
492
+ on_orderbook INTEGER NOT NULL DEFAULT 0,
493
+ -- Only read, as the provenance of a bid written before priced_by. Kept, and
494
+ -- never written, so a rollback to a build that still reads it finds the column.
495
+ reservations TEXT,
496
+ priced_by TEXT
480
497
  );
481
498
 
482
499
  CREATE INDEX IF NOT EXISTS idx_bids_commitment ON bids(commitment);
@@ -485,12 +502,12 @@ var SqliteBidStore = class {
485
502
  CREATE INDEX IF NOT EXISTS idx_bids_created_at ON bids(created_at);
486
503
  `);
487
504
  const columns = columnNames(this.db, "bids");
488
- for (const column of ["dead", "pending"]) {
505
+ for (const column of ["dead", "pending", "on_orderbook"]) {
489
506
  if (columns.has(column)) continue;
490
507
  this.db.exec(`ALTER TABLE bids ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
491
508
  this.logger.info({ column }, "Migrated bid storage schema");
492
509
  }
493
- for (const column of ["bid", "reservations"]) {
510
+ for (const column of ["bid", "reservations", "priced_by"]) {
494
511
  if (columns.has(column)) continue;
495
512
  this.db.exec(`ALTER TABLE bids ADD COLUMN ${column} TEXT`);
496
513
  this.logger.info({ column }, "Migrated bid storage schema");
@@ -499,19 +516,21 @@ var SqliteBidStore = class {
499
516
  }
500
517
  // biome-ignore lint/suspicious/noExplicitAny: raw sqlite row
501
518
  toStoredBid(row) {
519
+ const { reservations: _legacy, ...rest } = row;
502
520
  return {
503
- ...row,
521
+ ...rest,
504
522
  success: Boolean(row.success),
505
523
  pending: Boolean(row.pending),
506
524
  retracted: Boolean(row.retracted),
507
525
  dead: Boolean(row.dead),
508
- reservations: parseHolds(row.reservations)
526
+ onOrderbook: Boolean(row.onOrderbook),
527
+ pricedBy: pricedBy(row)
509
528
  };
510
529
  }
511
530
  async store(bid) {
512
531
  const result = this.db.prepare(`
513
- INSERT INTO bids (commitment, bid, extrinsic_hash, block_hash, success, pending, error, reservations)
514
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
532
+ INSERT INTO bids (commitment, bid, extrinsic_hash, block_hash, success, pending, error, on_orderbook, priced_by)
533
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
515
534
  `).run(
516
535
  bid.commitment,
517
536
  bid.bid ?? null,
@@ -520,7 +539,8 @@ var SqliteBidStore = class {
520
539
  bid.success ? 1 : 0,
521
540
  bid.pending ? 1 : 0,
522
541
  bid.error || null,
523
- bid.reservations?.length ? JSON.stringify(bid.reservations) : null
542
+ bid.onOrderbook ? 1 : 0,
543
+ bid.pricedBy ? JSON.stringify(bid.pricedBy) : null
524
544
  );
525
545
  this.logger.debug({ id: result.lastInsertRowid, commitment: bid.commitment, success: bid.success }, "Bid stored");
526
546
  }
@@ -546,37 +566,33 @@ var SqliteBidStore = class {
546
566
  const rows = this.db.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC`).all(sqliteDatetime(from), sqliteDatetime(to));
547
567
  return rows.map((row) => this.toStoredBid(row));
548
568
  }
549
- async markRetracted(commitment, retractExtrinsicHash) {
569
+ async markRetracted(commitment, retractExtrinsicHash, rows) {
570
+ if (rows?.length === 0) return false;
550
571
  const result = this.db.prepare(`
551
572
  UPDATE bids
552
573
  SET retracted = 1, retracted_at = datetime('now'), retract_extrinsic_hash = ?
553
- WHERE commitment = ? AND retracted = 0
554
- `).run(retractExtrinsicHash, commitment);
574
+ WHERE commitment = ? AND retracted = 0${rows ? ` AND id IN (${rows.map(() => "?").join(",")})` : ""}
575
+ `).run(retractExtrinsicHash, commitment, ...rows ?? []);
555
576
  if (result.changes > 0) {
556
577
  this.logger.info({ commitment, retractExtrinsicHash }, "Bid marked as retracted");
557
578
  return true;
558
579
  }
559
580
  return false;
560
581
  }
561
- async claimReservation(commitment, bid) {
562
- const rows = bid === void 0 ? this.db.prepare("SELECT id, reservations FROM bids WHERE commitment = ? AND reservations IS NOT NULL ORDER BY id").all(commitment) : this.db.prepare(
563
- "SELECT id, reservations FROM bids WHERE commitment = ? AND bid = ? AND reservations IS NOT NULL ORDER BY id"
564
- ).all(commitment, bid);
565
- const claimed = [];
566
- for (const row of rows) {
567
- const result = this.db.prepare("UPDATE bids SET reservations = NULL WHERE id = ? AND reservations = ?").run(row.id, row.reservations);
568
- if (result.changes === 1) claimed.push(...parseHolds(row.reservations));
569
- }
570
- return claimed;
571
- }
572
582
  async byLimitOrder(limitOrderId, limit = 100) {
573
- const rows = this.db.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE reservations LIKE ? ORDER BY id DESC LIMIT ?`).all(`%${limitOrderId}%`, Math.min(Math.max(limit, 1), 500));
574
- return rows.map((row) => this.toStoredBid(row)).filter((bid) => bid.reservations.some((hold) => hold.limitOrderId === limitOrderId));
583
+ const pattern = `%${limitOrderId}%`;
584
+ const rows = this.db.prepare(
585
+ `SELECT ${BID_COLUMNS} FROM bids WHERE priced_by LIKE ? OR (priced_by IS NULL AND reservations LIKE ?) ORDER BY id DESC LIMIT ?`
586
+ ).all(pattern, pattern, Math.min(Math.max(limit, 1), 500));
587
+ return rows.map((row) => this.toStoredBid(row)).filter((bid) => bid.pricedBy?.limitOrderId === limitOrderId);
575
588
  }
576
- async markDead(commitment) {
577
- const result = this.db.prepare("UPDATE bids SET dead = 1 WHERE commitment = ? AND retracted = 0 AND dead = 0").run(commitment);
589
+ async markDead(commitment, rows) {
590
+ if (rows?.length === 0) return false;
591
+ const result = this.db.prepare(
592
+ `UPDATE bids SET dead = 1 WHERE commitment = ? AND retracted = 0 AND dead = 0${rows ? ` AND id IN (${rows.map(() => "?").join(",")})` : ""}`
593
+ ).run(commitment, ...rows ?? []);
578
594
  if (result.changes > 0) {
579
- this.logger.debug({ commitment }, "Bid marked as dead (order filled on-chain)");
595
+ this.logger.debug({ commitment }, "Bid marked dead, due for retraction on the next sweep");
580
596
  return true;
581
597
  }
582
598
  return false;
@@ -585,8 +601,8 @@ var SqliteBidStore = class {
585
601
  const stats = this.db.prepare(`
586
602
  SELECT
587
603
  COUNT(*) as total,
588
- SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,
589
- SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed,
604
+ SUM(CASE WHEN success = 1 OR on_orderbook = 1 THEN 1 ELSE 0 END) as successful,
605
+ SUM(CASE WHEN success = 0 AND on_orderbook = 0 THEN 1 ELSE 0 END) as failed,
590
606
  SUM(CASE WHEN retracted = 1 THEN 1 ELSE 0 END) as retracted,
591
607
  SUM(CASE WHEN (success = 1 OR pending = 1) AND retracted = 0 THEN 1 ELSE 0 END) as pendingRetraction
592
608
  FROM bids
@@ -600,7 +616,7 @@ var SqliteBidStore = class {
600
616
  };
601
617
  }
602
618
  async failed(limit = 100) {
603
- const rows = this.db.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE success = 0 ORDER BY created_at DESC LIMIT ?`).all(limit);
619
+ const rows = this.db.prepare(`SELECT ${BID_COLUMNS} FROM bids WHERE success = 0 AND on_orderbook = 0 ORDER BY created_at DESC LIMIT ?`).all(limit);
604
620
  return rows.map((row) => this.toStoredBid(row));
605
621
  }
606
622
  async byCommitments(commitments) {
@@ -660,7 +676,6 @@ var LIMIT_ORDER_COLUMNS = `
660
676
  price,
661
677
  size,
662
678
  remaining,
663
- reserved,
664
679
  accepted_sources as acceptedSources,
665
680
  ttl_secs as ttlSecs,
666
681
  expires_at as expiresAt,
@@ -674,11 +689,13 @@ var LIMIT_ORDER_COLUMNS = `
674
689
  created_at as createdAt,
675
690
  updated_at as updatedAt
676
691
  `;
677
- var LimitOrderWriteError = class extends Error {
678
- };
679
692
  function statusGuard(only) {
680
693
  return only && only.length > 0 ? ` AND status IN (${only.map(() => "?").join(", ")})` : "";
681
694
  }
695
+ function clampable(only) {
696
+ const live = ["open", "resizing"];
697
+ return only && only.length > 0 ? live.filter((status) => only.includes(status)) : live;
698
+ }
682
699
  var SqliteLimitOrderStore = class {
683
700
  constructor(db, loggers = defaultLoggerContext()) {
684
701
  this.db = db;
@@ -697,6 +714,9 @@ var SqliteLimitOrderStore = class {
697
714
  price TEXT NOT NULL,
698
715
  size TEXT NOT NULL,
699
716
  remaining TEXT NOT NULL,
717
+ -- Never read or written: limit orders keep no local holds. Kept, and reset to
718
+ -- '0' on every start, so a rollback to a build that still reads it finds the
719
+ -- column and no stale hold.
700
720
  reserved TEXT NOT NULL DEFAULT '0',
701
721
  accepted_sources TEXT NOT NULL,
702
722
  ttl_secs INTEGER NOT NULL,
@@ -724,11 +744,15 @@ var SqliteLimitOrderStore = class {
724
744
  amount TEXT NOT NULL,
725
745
  amount_in TEXT,
726
746
  transaction_hash TEXT,
747
+ log_index INTEGER,
727
748
  filled_at TEXT NOT NULL DEFAULT (datetime('now'))
728
749
  );
729
750
 
730
751
  CREATE INDEX IF NOT EXISTS idx_limit_order_fills_order ON limit_order_fills(limit_order_id);
731
752
  `);
753
+ if (columnNames(this.db, "limit_orders").has("reserved") && this.db.prepare("SELECT 1 FROM limit_orders WHERE reserved <> '0' LIMIT 1").get()) {
754
+ this.db.exec("UPDATE limit_orders SET reserved = '0' WHERE reserved <> '0'");
755
+ }
732
756
  if (!columnNames(this.db, "limit_orders").has("entry_point")) {
733
757
  this.db.exec("ALTER TABLE limit_orders ADD COLUMN entry_point TEXT");
734
758
  this.logger.info({ column: "entry_point" }, "Migrated limit order storage schema");
@@ -737,6 +761,10 @@ var SqliteLimitOrderStore = class {
737
761
  this.db.exec("ALTER TABLE limit_order_fills ADD COLUMN amount_in TEXT");
738
762
  this.logger.info({ column: "amount_in" }, "Migrated limit order fill schema");
739
763
  }
764
+ if (!columnNames(this.db, "limit_order_fills").has("log_index")) {
765
+ this.db.exec("ALTER TABLE limit_order_fills ADD COLUMN log_index INTEGER");
766
+ this.logger.info({ column: "log_index" }, "Migrated limit order fill schema");
767
+ }
740
768
  }
741
769
  // biome-ignore lint/suspicious/noExplicitAny: raw sqlite row
742
770
  toLimitOrder(row) {
@@ -818,43 +846,17 @@ var SqliteLimitOrderStore = class {
818
846
  ).run(status, lastError, id, ...only ?? []);
819
847
  return result.changes === 1 ? this.read(id) : null;
820
848
  }
821
- async reserve(id, amount) {
822
- const order = this.read(id);
823
- if (!order || order.status !== "open") return false;
824
- if (BigInt(amount) > BigInt(order.remaining)) return false;
825
- const reserved = BigInt(order.reserved) + BigInt(amount);
826
- const result = this.db.prepare(`
827
- UPDATE limit_orders SET reserved = ?, updated_at = datetime('now')
828
- WHERE id = ? AND reserved = ? AND status = 'open'
829
- `).run(reserved.toString(), id, order.reserved);
830
- return result.changes === 1;
831
- }
832
- async transaction(settle) {
833
- this.db.exec("BEGIN");
834
- try {
835
- const result = await settle();
836
- this.db.exec("COMMIT");
837
- return result;
838
- } catch (err) {
839
- if (this.db.isTransaction !== false) {
840
- try {
841
- this.db.exec("ROLLBACK");
842
- } catch {
843
- }
844
- }
845
- throw err;
846
- }
847
- }
848
849
  async recordFill(fill) {
849
850
  this.db.prepare(
850
- "INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, amount_in, transaction_hash) VALUES (?, ?, ?, ?, ?, ?)"
851
+ "INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, amount_in, transaction_hash, log_index) VALUES (?, ?, ?, ?, ?, ?, ?)"
851
852
  ).run(
852
853
  fill.limitOrderId,
853
854
  fill.commitment,
854
855
  fill.bid ?? null,
855
856
  fill.amount,
856
857
  fill.amountIn ?? null,
857
- fill.transactionHash ?? null
858
+ fill.transactionHash ?? null,
859
+ fill.logIndex ?? null
858
860
  );
859
861
  }
860
862
  async fillHistory() {
@@ -867,38 +869,23 @@ var SqliteLimitOrderStore = class {
867
869
  }
868
870
  async fills(limitOrderId, limit = 100) {
869
871
  return this.db.prepare(
870
- `SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, amount_in as amountIn, transaction_hash as transactionHash,
872
+ `SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, amount_in as amountIn, transaction_hash as transactionHash, log_index as logIndex,
871
873
  filled_at as filledAt
872
874
  FROM limit_order_fills WHERE limit_order_id = ? ORDER BY id DESC LIMIT ?`
873
875
  ).all(limitOrderId, Math.min(Math.max(limit, 1), 500));
874
876
  }
875
- async drawDown(id, amount) {
876
- const order = this.read(id);
877
- if (!order) return null;
878
- const remaining = BigInt(order.remaining) - BigInt(amount);
879
- const result = this.db.prepare("UPDATE limit_orders SET remaining = ?, updated_at = datetime('now') WHERE id = ? AND remaining = ?").run((remaining > 0n ? remaining : 0n).toString(), id, order.remaining);
880
- if (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid draw-down`);
881
- return this.read(id);
882
- }
883
- async clampRemaining(id, room) {
877
+ async clampRemaining(id, room, only) {
878
+ const statuses = clampable(only);
884
879
  const order = this.read(id);
885
- if (!order || order.status !== "open") return null;
886
- const sum = BigInt(room) + BigInt(order.reserved);
887
- const target = sum > 0n ? sum : 0n;
880
+ if (!order || !statuses.includes(order.status)) return null;
881
+ const target = BigInt(room) > 0n ? BigInt(room) : 0n;
888
882
  if (target >= BigInt(order.remaining)) return null;
889
883
  const result = this.db.prepare(`
890
884
  UPDATE limit_orders SET remaining = ?, updated_at = datetime('now')
891
- WHERE id = ? AND remaining = ? AND reserved = ? AND status = 'open'
892
- `).run(target.toString(), id, order.remaining, order.reserved);
885
+ WHERE id = ? AND remaining = ? AND status IN (${statuses.map(() => "?").join(", ")})
886
+ `).run(target.toString(), id, order.remaining, ...statuses);
893
887
  return result.changes === 1 ? this.read(id) : null;
894
888
  }
895
- async release(id, amount) {
896
- const order = this.read(id);
897
- if (!order) return;
898
- const reserved = BigInt(order.reserved) - BigInt(amount);
899
- const result = this.db.prepare("UPDATE limit_orders SET reserved = ?, updated_at = datetime('now') WHERE id = ? AND reserved = ?").run((reserved > 0n ? reserved : 0n).toString(), id, order.reserved);
900
- if (result.changes !== 1) throw new LimitOrderWriteError(`Another writer moved limit order '${id}' mid release`);
901
- }
902
889
  };
903
890
 
904
891
  // src/data/sqlite/state.ts