@hyperbridge/simplex 0.16.12 → 0.16.13

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.
@@ -29007,17 +29007,17 @@ function cosh(x4) {
29007
29007
  }
29008
29008
  function clone(obj) {
29009
29009
  var i6, p3, ps;
29010
- function Decimal2(v7) {
29010
+ function Decimal3(v7) {
29011
29011
  var e4, i7, t6, x4 = this;
29012
- if (!(x4 instanceof Decimal2)) return new Decimal2(v7);
29013
- x4.constructor = Decimal2;
29012
+ if (!(x4 instanceof Decimal3)) return new Decimal3(v7);
29013
+ x4.constructor = Decimal3;
29014
29014
  if (isDecimalInstance(v7)) {
29015
29015
  x4.s = v7.s;
29016
29016
  if (external) {
29017
- if (!v7.d || v7.e > Decimal2.maxE) {
29017
+ if (!v7.d || v7.e > Decimal3.maxE) {
29018
29018
  x4.e = NaN;
29019
29019
  x4.d = null;
29020
- } else if (v7.e < Decimal2.minE) {
29020
+ } else if (v7.e < Decimal3.minE) {
29021
29021
  x4.e = 0;
29022
29022
  x4.d = [0];
29023
29023
  } else {
@@ -29047,10 +29047,10 @@ function clone(obj) {
29047
29047
  if (v7 === ~~v7 && v7 < 1e7) {
29048
29048
  for (e4 = 0, i7 = v7; i7 >= 10; i7 /= 10) e4++;
29049
29049
  if (external) {
29050
- if (e4 > Decimal2.maxE) {
29050
+ if (e4 > Decimal3.maxE) {
29051
29051
  x4.e = NaN;
29052
29052
  x4.d = null;
29053
- } else if (e4 < Decimal2.minE) {
29053
+ } else if (e4 < Decimal3.minE) {
29054
29054
  x4.e = 0;
29055
29055
  x4.d = [0];
29056
29056
  } else {
@@ -29092,58 +29092,58 @@ function clone(obj) {
29092
29092
  }
29093
29093
  throw Error(invalidArgument + v7);
29094
29094
  }
29095
- Decimal2.prototype = P;
29096
- Decimal2.ROUND_UP = 0;
29097
- Decimal2.ROUND_DOWN = 1;
29098
- Decimal2.ROUND_CEIL = 2;
29099
- Decimal2.ROUND_FLOOR = 3;
29100
- Decimal2.ROUND_HALF_UP = 4;
29101
- Decimal2.ROUND_HALF_DOWN = 5;
29102
- Decimal2.ROUND_HALF_EVEN = 6;
29103
- Decimal2.ROUND_HALF_CEIL = 7;
29104
- Decimal2.ROUND_HALF_FLOOR = 8;
29105
- Decimal2.EUCLID = 9;
29106
- Decimal2.config = Decimal2.set = config;
29107
- Decimal2.clone = clone;
29108
- Decimal2.isDecimal = isDecimalInstance;
29109
- Decimal2.abs = abs;
29110
- Decimal2.acos = acos;
29111
- Decimal2.acosh = acosh;
29112
- Decimal2.add = add2;
29113
- Decimal2.asin = asin;
29114
- Decimal2.asinh = asinh;
29115
- Decimal2.atan = atan;
29116
- Decimal2.atanh = atanh;
29117
- Decimal2.atan2 = atan2;
29118
- Decimal2.cbrt = cbrt;
29119
- Decimal2.ceil = ceil;
29120
- Decimal2.clamp = clamp;
29121
- Decimal2.cos = cos;
29122
- Decimal2.cosh = cosh;
29123
- Decimal2.div = div;
29124
- Decimal2.exp = exp;
29125
- Decimal2.floor = floor;
29126
- Decimal2.hypot = hypot;
29127
- Decimal2.ln = ln;
29128
- Decimal2.log = log;
29129
- Decimal2.log10 = log10;
29130
- Decimal2.log2 = log2;
29131
- Decimal2.max = max;
29132
- Decimal2.min = min;
29133
- Decimal2.mod = mod3;
29134
- Decimal2.mul = mul;
29135
- Decimal2.pow = pow;
29136
- Decimal2.random = random;
29137
- Decimal2.round = round;
29138
- Decimal2.sign = sign;
29139
- Decimal2.sin = sin;
29140
- Decimal2.sinh = sinh;
29141
- Decimal2.sqrt = sqrt;
29142
- Decimal2.sub = sub;
29143
- Decimal2.sum = sum;
29144
- Decimal2.tan = tan;
29145
- Decimal2.tanh = tanh;
29146
- Decimal2.trunc = trunc;
29095
+ Decimal3.prototype = P;
29096
+ Decimal3.ROUND_UP = 0;
29097
+ Decimal3.ROUND_DOWN = 1;
29098
+ Decimal3.ROUND_CEIL = 2;
29099
+ Decimal3.ROUND_FLOOR = 3;
29100
+ Decimal3.ROUND_HALF_UP = 4;
29101
+ Decimal3.ROUND_HALF_DOWN = 5;
29102
+ Decimal3.ROUND_HALF_EVEN = 6;
29103
+ Decimal3.ROUND_HALF_CEIL = 7;
29104
+ Decimal3.ROUND_HALF_FLOOR = 8;
29105
+ Decimal3.EUCLID = 9;
29106
+ Decimal3.config = Decimal3.set = config;
29107
+ Decimal3.clone = clone;
29108
+ Decimal3.isDecimal = isDecimalInstance;
29109
+ Decimal3.abs = abs;
29110
+ Decimal3.acos = acos;
29111
+ Decimal3.acosh = acosh;
29112
+ Decimal3.add = add2;
29113
+ Decimal3.asin = asin;
29114
+ Decimal3.asinh = asinh;
29115
+ Decimal3.atan = atan;
29116
+ Decimal3.atanh = atanh;
29117
+ Decimal3.atan2 = atan2;
29118
+ Decimal3.cbrt = cbrt;
29119
+ Decimal3.ceil = ceil;
29120
+ Decimal3.clamp = clamp;
29121
+ Decimal3.cos = cos;
29122
+ Decimal3.cosh = cosh;
29123
+ Decimal3.div = div;
29124
+ Decimal3.exp = exp;
29125
+ Decimal3.floor = floor;
29126
+ Decimal3.hypot = hypot;
29127
+ Decimal3.ln = ln;
29128
+ Decimal3.log = log;
29129
+ Decimal3.log10 = log10;
29130
+ Decimal3.log2 = log2;
29131
+ Decimal3.max = max;
29132
+ Decimal3.min = min;
29133
+ Decimal3.mod = mod3;
29134
+ Decimal3.mul = mul;
29135
+ Decimal3.pow = pow;
29136
+ Decimal3.random = random;
29137
+ Decimal3.round = round;
29138
+ Decimal3.sign = sign;
29139
+ Decimal3.sin = sin;
29140
+ Decimal3.sinh = sinh;
29141
+ Decimal3.sqrt = sqrt;
29142
+ Decimal3.sub = sub;
29143
+ Decimal3.sum = sum;
29144
+ Decimal3.tan = tan;
29145
+ Decimal3.tanh = tanh;
29146
+ Decimal3.trunc = trunc;
29147
29147
  if (obj === void 0) obj = {};
29148
29148
  if (obj) {
29149
29149
  if (obj.defaults !== true) {
@@ -29151,8 +29151,8 @@ function clone(obj) {
29151
29151
  for (i6 = 0; i6 < ps.length; ) if (!obj.hasOwnProperty(p3 = ps[i6++])) obj[p3] = this[p3];
29152
29152
  }
29153
29153
  }
29154
- Decimal2.config(obj);
29155
- return Decimal2;
29154
+ Decimal3.config(obj);
29155
+ return Decimal3;
29156
29156
  }
29157
29157
  function div(x4, y5) {
29158
29158
  return new this(x4).div(y5);
@@ -51069,7 +51069,7 @@ var require_bn = __commonJS({
51069
51069
  }
51070
51070
  return this;
51071
51071
  };
51072
- BN3.prototype.add = function add3(num2) {
51072
+ BN3.prototype.add = function add4(num2) {
51073
51073
  var res;
51074
51074
  if (num2.negative !== 0 && this.negative === 0) {
51075
51075
  num2.negative = 0;
@@ -52906,7 +52906,7 @@ var require_bn = __commonJS({
52906
52906
  }
52907
52907
  return this.m.sub(a6)._forceRed(this);
52908
52908
  };
52909
- Red.prototype.add = function add3(a6, b5) {
52909
+ Red.prototype.add = function add4(a6, b5) {
52910
52910
  this._verify2(a6, b5);
52911
52911
  var res = a6.add(b5);
52912
52912
  if (res.cmp(this.m) >= 0) {
@@ -55786,10 +55786,10 @@ var init_fflate = __esm({
55786
55786
  lm = void 0;
55787
55787
  break;
55788
55788
  } else {
55789
- let add3 = sym - 254;
55789
+ let add4 = sym - 254;
55790
55790
  if (sym > 264) {
55791
55791
  const i6 = sym - 257, b5 = fleb[i6];
55792
- add3 = bits(dat, pos, (1 << b5) - 1) + fl[i6];
55792
+ add4 = bits(dat, pos, (1 << b5) - 1) + fl[i6];
55793
55793
  pos += b5;
55794
55794
  }
55795
55795
  const d5 = dm[bits16(dat, pos) & dms], dsym = d5 >>> 4;
@@ -55805,7 +55805,7 @@ var init_fflate = __esm({
55805
55805
  throw "unexpected EOF";
55806
55806
  if (noBuf)
55807
55807
  cbuf(bt + 131072);
55808
- const end = bt + add3;
55808
+ const end = bt + add4;
55809
55809
  for (; bt < end; bt += 4) {
55810
55810
  buf[bt] = buf[bt - dt];
55811
55811
  buf[bt + 1] = buf[bt + 1 - dt];
@@ -83509,15 +83509,15 @@ var require_fromEvent = __commonJS({
83509
83509
  return function(handler) {
83510
83510
  return target[methodName](eventName, handler, options);
83511
83511
  };
83512
- }) : isNodeStyleEventEmitter(target) ? nodeEventEmitterMethods.map(toCommonHandlerRegistry(target, eventName)) : isJQueryStyleEventEmitter(target) ? jqueryMethods.map(toCommonHandlerRegistry(target, eventName)) : [], 2), add3 = _a10[0], remove = _a10[1];
83513
- if (!add3) {
83512
+ }) : isNodeStyleEventEmitter(target) ? nodeEventEmitterMethods.map(toCommonHandlerRegistry(target, eventName)) : isJQueryStyleEventEmitter(target) ? jqueryMethods.map(toCommonHandlerRegistry(target, eventName)) : [], 2), add4 = _a10[0], remove = _a10[1];
83513
+ if (!add4) {
83514
83514
  if (isArrayLike_1.isArrayLike(target)) {
83515
83515
  return mergeMap_1.mergeMap(function(subTarget) {
83516
83516
  return fromEvent(subTarget, eventName, options);
83517
83517
  })(innerFrom_1.innerFrom(target));
83518
83518
  }
83519
83519
  }
83520
- if (!add3) {
83520
+ if (!add4) {
83521
83521
  throw new TypeError("Invalid event target");
83522
83522
  }
83523
83523
  return new Observable_1.Observable(function(subscriber) {
@@ -83528,7 +83528,7 @@ var require_fromEvent = __commonJS({
83528
83528
  }
83529
83529
  return subscriber.next(1 < args.length ? args : args[0]);
83530
83530
  };
83531
- add3(handler);
83531
+ add4(handler);
83532
83532
  return function() {
83533
83533
  return remove(handler);
83534
83534
  };
@@ -233930,8 +233930,8 @@ function addToExternrefTable0(obj) {
233930
233930
  function passArrayJsValueToWasm0(array, malloc) {
233931
233931
  const ptr = malloc(array.length * 4, 4) >>> 0;
233932
233932
  for (let i6 = 0; i6 < array.length; i6++) {
233933
- const add3 = addToExternrefTable0(array[i6]);
233934
- getDataViewMemory0().setUint32(ptr + 4 * i6, add3, true);
233933
+ const add4 = addToExternrefTable0(array[i6]);
233934
+ getDataViewMemory0().setUint32(ptr + 4 * i6, add4, true);
233935
233935
  }
233936
233936
  WASM_VECTOR_LEN = array.length;
233937
233937
  return ptr;
@@ -247568,9 +247568,17 @@ query OrderStatus($commitment: String!) {
247568
247568
  * order. Frontends can use `relayerFee` to approve the ERC-20 spend before
247569
247569
  * submitting the cancel transaction.
247570
247570
  *
247571
+ * A cross-chain cancel is paid on the chain that receives the cancel
247572
+ * transaction: the source chain for the source route, the destination chain
247573
+ * for the destination route. When that chain is a testnet, the quote has no
247574
+ * native value, since testnet hosts have no router to swap it into the fee
247575
+ * token. The caller must first approve `relayerFee` of that host's fee token
247576
+ * to that chain's IntentGateway.
247577
+ *
247571
247578
  * @param order - The order to quote.
247572
247579
  * @param options - Choose the initiation side. Defaults to source-side cancellation.
247573
- * @returns `{ nativeValue }` — native token amount (wei) to send as `value`;
247580
+ * @returns `{ nativeValue }` — native token amount (wei) to send as `value`,
247581
+ * or `0n` when the relayer fee must be paid in the fee token;
247574
247582
  * `{ relayerFee }` — relayer incentive denominated in the chain's fee token.
247575
247583
  */
247576
247584
  async quoteCancelOrder(order, options = {}) {
@@ -247584,11 +247592,12 @@ query OrderStatus($commitment: String!) {
247584
247592
  *
247585
247593
  * Constructs the ISMP GET request the source gateway will dispatch, one
247586
247594
  * `_partialFills` key per leg, and calls `quoteNative` on the source host to
247587
- * obtain the dispatch fee.
247595
+ * obtain the dispatch fee. Returns a zero `nativeValue` when the source is a
247596
+ * testnet chain, leaving the relayer fee to be paid in its fee token.
247588
247597
  * Returns 0 for same-chain orders (no ISMP call needed).
247589
247598
  *
247590
247599
  * @param order - The order to quote.
247591
- * @returns The native token dispatch fee in wei.
247600
+ * @returns The native token dispatch fee in wei and the relayer fee.
247592
247601
  */
247593
247602
  async quoteCancelFromSource(order) {
247594
247603
  const sourceStateMachine = normalizeStateMachineId(order.source);
@@ -247596,6 +247605,14 @@ query OrderStatus($commitment: String!) {
247596
247605
  if (sourceStateMachine === destStateMachine) {
247597
247606
  return { nativeValue: 0n, relayerFee: 0n };
247598
247607
  }
247608
+ const feeInSourceFeeToken = await convertGasToFeeToken(
247609
+ this.ctx,
247610
+ _a6.SOURCE_GET_RESPONSE_GAS,
247611
+ "source",
247612
+ sourceStateMachine
247613
+ );
247614
+ const relayerFee = feeInSourceFeeToken * 1005n / 1000n;
247615
+ if (TESTNET_CHAINS.has(sourceStateMachine)) return { nativeValue: 0n, relayerFee };
247599
247616
  const height = order.deadline + 1n;
247600
247617
  const destIntentGateway = this.ctx.dest.configService.getIntentGatewayAddress(destStateMachine);
247601
247618
  const commitment = this.orderId(order);
@@ -247604,20 +247621,13 @@ query OrderStatus($commitment: String!) {
247604
247621
  const getRequest = {
247605
247622
  source: sourceStateMachine,
247606
247623
  dest: destStateMachine,
247607
- from: this.ctx.source.configService.getIntentGatewayAddress(destStateMachine),
247624
+ from: this.ctx.source.configService.getIntentGatewayAddress(sourceStateMachine),
247608
247625
  nonce: await this.ctx.source.getHostNonce(),
247609
247626
  height,
247610
247627
  keys: keys3,
247611
247628
  timeoutTimestamp: 0n,
247612
247629
  context
247613
247630
  };
247614
- const feeInSourceFeeToken = await convertGasToFeeToken(
247615
- this.ctx,
247616
- _a6.SOURCE_GET_RESPONSE_GAS,
247617
- "source",
247618
- sourceStateMachine
247619
- );
247620
- const relayerFee = feeInSourceFeeToken * 1005n / 1000n;
247621
247631
  const nativeValue = await this.ctx.source.quoteNative(getRequest, relayerFee) * 101n / 100n;
247622
247632
  return { nativeValue, relayerFee };
247623
247633
  }
@@ -247638,7 +247648,10 @@ query OrderStatus($commitment: String!) {
247638
247648
  * cancellation. Same-chain orders always use the direct local route.
247639
247649
  * @yields {@link CancelEvent} objects describing each stage of the
247640
247650
  * cancellation lifecycle. The caller signs or broadcasts the yielded
247641
- * transaction, so its signer may differ from `order.user` after expiry.
247651
+ * transaction, and the gateway decides who may sign it. A same-chain cancel
247652
+ * or a cross-chain cancel from the destination accepts only `order.user`
247653
+ * through the deadline and any signer strictly after it. A cross-chain
247654
+ * cancel from the source accepts only `order.user`, before or after expiry.
247642
247655
  */
247643
247656
  async *cancelOrder(order, indexerClient, options = {}) {
247644
247657
  const sourceStateMachine = normalizeStateMachineId(order.source);
@@ -247858,11 +247871,13 @@ query OrderStatus($commitment: String!) {
247858
247871
  *
247859
247872
  * Estimates the relayer fee for delivering the refund POST request from the
247860
247873
  * destination chain back to the source chain, converts it to the destination
247861
- * fee token, and calls `quoteNative` on the destination host.
247874
+ * fee token, and calls `quoteNative` on the destination host. Returns a zero
247875
+ * `nativeValue` when the destination is a testnet chain, leaving the relayer
247876
+ * fee to be paid in its fee token.
247862
247877
  * Returns 0 for same-chain orders.
247863
247878
  *
247864
247879
  * @param order - The order to quote.
247865
- * @returns The native token dispatch fee in wei.
247880
+ * @returns The native token dispatch fee in wei and the relayer fee.
247866
247881
  */
247867
247882
  async quoteCancelFromDest(order) {
247868
247883
  const sourceStateMachine = normalizeStateMachineId(order.source);
@@ -247873,6 +247888,7 @@ query OrderStatus($commitment: String!) {
247873
247888
  const destIntentGateway = this.ctx.dest.configService.getIntentGatewayAddress(destStateMachine);
247874
247889
  const sourceIntentGateway = this.ctx.source.configService.getIntentGatewayAddress(sourceStateMachine);
247875
247890
  const relayerFee = await this.estimateRelayerFee(sourceStateMachine, destStateMachine);
247891
+ if (TESTNET_CHAINS.has(destStateMachine)) return { nativeValue: 0n, relayerFee };
247876
247892
  const body = constructRefundEscrowRequestBody(order, order.user);
247877
247893
  const postRequest = {
247878
247894
  source: destStateMachine,
@@ -250232,6 +250248,8 @@ query QuotePessimistic($route: RouteInput!, $amountIn: BigInt!) {
250232
250248
  * order. Use `relayerFee` to approve the ERC-20 spend before submitting.
250233
250249
  * Same-chain orders use the direct local route for either `from` option and
250234
250250
  * return `{ nativeValue: 0n, relayerFee: 0n }`.
250251
+ * When the chain receiving the cancel transaction is a testnet, `nativeValue` is `0n` and the caller
250252
+ * approves `relayerFee` of that host's fee token to that chain's gateway.
250235
250253
  *
250236
250254
  * Delegates to {@link OrderCanceller.quoteCancelOrder}.
250237
250255
  *
@@ -254232,7 +254250,7 @@ var require_bn2 = __commonJS({
254232
254250
  }
254233
254251
  return this;
254234
254252
  };
254235
- BN3.prototype.add = function add3(num2) {
254253
+ BN3.prototype.add = function add4(num2) {
254236
254254
  var res;
254237
254255
  if (num2.negative !== 0 && this.negative === 0) {
254238
254256
  num2.negative = 0;
@@ -256059,7 +256077,7 @@ var require_bn2 = __commonJS({
256059
256077
  }
256060
256078
  return this.m.sub(a6)._forceRed(this);
256061
256079
  };
256062
- Red.prototype.add = function add3(a6, b5) {
256080
+ Red.prototype.add = function add4(a6, b5) {
256063
256081
  this._verify2(a6, b5);
256064
256082
  var res = a6.add(b5);
256065
256083
  if (res.cmp(this.m) >= 0) {
@@ -267070,7 +267088,7 @@ var require_namespace = __commonJS({
267070
267088
  return this.nested[name3].values;
267071
267089
  throw Error("no such enum: " + name3);
267072
267090
  };
267073
- Namespace.prototype.add = function add3(object) {
267091
+ Namespace.prototype.add = function add4(object) {
267074
267092
  if (!(object instanceof Field6 && object.extend !== void 0 || object instanceof Type2 || object instanceof OneOf || object instanceof Enum3 || object instanceof Service || object instanceof Namespace))
267075
267093
  throw TypeError("object must be a valid nested object");
267076
267094
  if (!this.nested)
@@ -267450,7 +267468,7 @@ var require_service2 = __commonJS({
267450
267468
  });
267451
267469
  return this;
267452
267470
  };
267453
- Service.prototype.add = function add3(object) {
267471
+ Service.prototype.add = function add4(object) {
267454
267472
  if (this.get(object.name))
267455
267473
  throw Error("duplicate name '" + object.name + "' in " + this);
267456
267474
  if (object instanceof Method2) {
@@ -268155,7 +268173,7 @@ var require_type2 = __commonJS({
268155
268173
  Type2.prototype.get = function get4(name3) {
268156
268174
  return this.fields[name3] || this.oneofs && this.oneofs[name3] || this.nested && this.nested[name3] || null;
268157
268175
  };
268158
- Type2.prototype.add = function add3(object) {
268176
+ Type2.prototype.add = function add4(object) {
268159
268177
  if (this.get(object.name))
268160
268178
  throw Error("duplicate name '" + object.name + "' in " + this);
268161
268179
  if (object instanceof Field6 && object.extend === void 0) {
@@ -269066,7 +269084,7 @@ var require_oneof = __commonJS({
269066
269084
  oneof.parent.add(oneof.fieldsArray[i6]);
269067
269085
  }
269068
269086
  }
269069
- OneOf.prototype.add = function add3(field) {
269087
+ OneOf.prototype.add = function add4(field) {
269070
269088
  if (!(field instanceof Field6))
269071
269089
  throw TypeError("field must be a Field");
269072
269090
  if (field.parent && field.parent !== this.parent)
@@ -269395,7 +269413,7 @@ var require_enum = __commonJS({
269395
269413
  keepComments ? this.comments : void 0
269396
269414
  ]);
269397
269415
  };
269398
- Enum3.prototype.add = function add3(name3, id2, comment, options) {
269416
+ Enum3.prototype.add = function add4(name3, id2, comment, options) {
269399
269417
  if (!util7.isString(name3))
269400
269418
  throw TypeError("name must be a string");
269401
269419
  if (!util7.isInteger(id2))
@@ -273872,7 +273890,7 @@ var require_umd = __commonJS({
273872
273890
  return this.not().add(ONE2);
273873
273891
  };
273874
273892
  LongPrototype.neg = LongPrototype.negate;
273875
- LongPrototype.add = function add3(addend) {
273893
+ LongPrototype.add = function add4(addend) {
273876
273894
  if (!isLong(addend)) addend = fromValue(addend);
273877
273895
  var a48 = this.high >>> 16;
273878
273896
  var a32 = this.high & 65535;
@@ -340232,6 +340250,35 @@ var LIMITS_QUERY = `
340232
340250
  chains { id name tokens { symbol decimals } }
340233
340251
  }
340234
340252
  `;
340253
+ var LEVEL_FIELDS = "fillChain priceBucket price worstPrice baseSize quoteSize orderCount solverCount";
340254
+ var SNAPSHOT_QUERY = `
340255
+ query OrderbookSnapshot($id: ID!, $sourceChain: String, $fillChain: String) {
340256
+ book(id: $id) {
340257
+ id base quote
340258
+ bids: levels(side: BID, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
340259
+ asks: levels(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
340260
+ bidLiquidity: availableLiquidity(side: BID, sourceChain: $sourceChain, fillChain: $fillChain)
340261
+ askLiquidity: availableLiquidity(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain)
340262
+ }
340263
+ serverInfo { priceGranularities { book granularity } }
340264
+ }
340265
+ `;
340266
+ var LEVEL_ORDERS_QUERY = `
340267
+ query OrderbookLevelOrders($id: ID!, $side: Side!, $fillChain: String, $sourceChain: String, $after: String) {
340268
+ book(id: $id) {
340269
+ orders(side: $side, fillChain: $fillChain, sourceChain: $sourceChain, first: 50, after: $after) {
340270
+ edges {
340271
+ node {
340272
+ solver { address }
340273
+ commitment fillChain price priceBucket advertisedSize quotedAmount resized expiresAt acceptedSources
340274
+ }
340275
+ }
340276
+ pageInfo { hasNextPage endCursor }
340277
+ }
340278
+ }
340279
+ }
340280
+ `;
340281
+ var LEVEL_ORDER_PAGES = 20;
340235
340282
  var POSTED_ORDER_FIELDS = "commitment side status price quotedAmount advertisedSize expiresAt acceptedSources";
340236
340283
  var SUBMIT_ORDER_MUTATION = `
340237
340284
  mutation SubmitOrder($userOp: Bytes!) {
@@ -340296,6 +340343,44 @@ var OrderbookClient = class {
340296
340343
  async limits() {
340297
340344
  return this.request(LIMITS_QUERY, {});
340298
340345
  }
340346
+ /** The public book across all solvers, restricted to the selected route. */
340347
+ async snapshot(id2, filters = {}) {
340348
+ const { book, serverInfo } = await this.request(SNAPSHOT_QUERY, {
340349
+ id: id2,
340350
+ sourceChain: filters.sourceChain || null,
340351
+ fillChain: filters.fillChain || null
340352
+ });
340353
+ if (!book) return null;
340354
+ const granularity = serverInfo.priceGranularities.find((entry) => entry.book === book.id)?.granularity ?? null;
340355
+ return { ...book, granularity };
340356
+ }
340357
+ /**
340358
+ * The orders behind one price level. The orderbook filters orders by side and route but not by
340359
+ * price bucket, so this walks the side's orders on the level's fill chain and keeps the bucket.
340360
+ * Null when the book does not exist.
340361
+ */
340362
+ async levelOrders(id2, level) {
340363
+ const found = [];
340364
+ let after = null;
340365
+ for (let page = 0; page < LEVEL_ORDER_PAGES; page++) {
340366
+ const { book } = await this.request(LEVEL_ORDERS_QUERY, {
340367
+ id: id2,
340368
+ side: level.side,
340369
+ fillChain: level.fillChain,
340370
+ sourceChain: level.sourceChain || null,
340371
+ after
340372
+ });
340373
+ if (!book) return null;
340374
+ for (const { node } of book.orders.edges) {
340375
+ if (node.priceBucket !== level.priceBucket) continue;
340376
+ const { solver, priceBucket: _bucket, ...order } = node;
340377
+ found.push({ ...order, solver: solver.address });
340378
+ }
340379
+ if (!book.orders.pageInfo.hasNextPage || !book.orders.pageInfo.endCursor) break;
340380
+ after = book.orders.pageInfo.endCursor;
340381
+ }
340382
+ return found;
340383
+ }
340299
340384
  async submitOrder(userOp) {
340300
340385
  const { submitOrder } = await this.request(SUBMIT_ORDER_MUTATION, { userOp });
340301
340386
  switch (submitOrder.__typename) {
@@ -340484,16 +340569,16 @@ function signedAmounts(params) {
340484
340569
  function offerFor(params) {
340485
340570
  const { side, inputAmount, price, outputDecimals } = params;
340486
340571
  if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
340487
- const scaled = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE2 : inputAmount * ORDERBOOK_SCALE2 / price;
340572
+ const scaled2 = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE2 : inputAmount * ORDERBOOK_SCALE2 / price;
340488
340573
  const unit = outputDecimals >= 18 ? 1n : 10n ** BigInt(18 - outputDecimals);
340489
- return scaled / unit * unit;
340574
+ return scaled2 / unit * unit;
340490
340575
  }
340491
340576
  function inputFor(params) {
340492
340577
  const { side, outputAmount, price, inputDecimals } = params;
340493
340578
  if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
340494
- const scaled = side === "BID" ? divCeil2(outputAmount * ORDERBOOK_SCALE2, price) : divCeil2(outputAmount * price, ORDERBOOK_SCALE2);
340579
+ const scaled2 = side === "BID" ? divCeil2(outputAmount * ORDERBOOK_SCALE2, price) : divCeil2(outputAmount * price, ORDERBOOK_SCALE2);
340495
340580
  const unit = inputDecimals >= 18 ? 1n : 10n ** BigInt(18 - inputDecimals);
340496
- return divCeil2(scaled, unit) * unit;
340581
+ return divCeil2(scaled2, unit) * unit;
340497
340582
  }
340498
340583
 
340499
340584
  // src/orderbook/matching.ts
@@ -340751,6 +340836,14 @@ var LimitOrderService = class {
340751
340836
  throw err;
340752
340837
  }
340753
340838
  }
340839
+ /** Public market levels read through the configured orderbook client. */
340840
+ orderbookSnapshot(book, filters = {}) {
340841
+ return this.client.snapshot(book, filters);
340842
+ }
340843
+ /** Every solver's order behind one public price level. */
340844
+ orderbookLevelOrders(book, level) {
340845
+ return this.client.levelOrders(book, level);
340846
+ }
340754
340847
  /**
340755
340848
  * The pairs an order can be written against, the smallest payout each token may carry, and
340756
340849
  * the tokens the orderbook registers on each chain.
@@ -340805,16 +340898,16 @@ var LimitOrderService = class {
340805
340898
  if (!this.configService.getConfiguredChainIds().includes(getChainId3(request3.fillChain) ?? -1)) {
340806
340899
  throw new LimitOrderValidationError(`'${request3.fillChain}' is not a chain this filler is configured for`);
340807
340900
  }
340808
- const scaled = { amountIn: 0n, amountOut: 0n };
340901
+ const scaled2 = { amountIn: 0n, amountOut: 0n };
340809
340902
  for (const name3 of ["amountIn", "amountOut"]) {
340810
340903
  try {
340811
- scaled[name3] = fromHuman(request3[name3] ?? "");
340904
+ scaled2[name3] = fromHuman(request3[name3] ?? "");
340812
340905
  } catch (err) {
340813
340906
  throw new LimitOrderValidationError(
340814
340907
  `${name3} must be an amount in whole tokens, like "1000" or "1500.25"; ${err.message}`
340815
340908
  );
340816
340909
  }
340817
- if (scaled[name3] <= 0n) throw new LimitOrderValidationError(`${name3} must be greater than zero`);
340910
+ if (scaled2[name3] <= 0n) throw new LimitOrderValidationError(`${name3} must be greater than zero`);
340818
340911
  }
340819
340912
  const sources = request3.acceptedSources ?? [];
340820
340913
  if (sources.length === 0) {
@@ -340861,7 +340954,7 @@ var LimitOrderService = class {
340861
340954
  );
340862
340955
  }
340863
340956
  const floor2 = limits.serverInfo.minOrderSizes.find((entry) => entry.symbol === request3.tokenOut);
340864
- if (floor2 && scaled.amountOut < BigInt(floor2.size)) {
340957
+ if (floor2 && scaled2.amountOut < BigInt(floor2.size)) {
340865
340958
  throw new LimitOrderValidationError(
340866
340959
  `amountOut is below the orderbook's dust floor for ${request3.tokenOut} (${toHuman(BigInt(floor2.size))})`
340867
340960
  );
@@ -340871,7 +340964,7 @@ var LimitOrderService = class {
340871
340964
  throw new LimitOrderValidationError(`'${symbol3}' does not resolve to a token address on ${request3.fillChain}`);
340872
340965
  }
340873
340966
  }
340874
- return { amountIn: scaled.amountIn, amountOut: scaled.amountOut };
340967
+ return { amountIn: scaled2.amountIn, amountOut: scaled2.amountOut };
340875
340968
  }
340876
340969
  /**
340877
340970
  * Refuses an order the wallet cannot pay out.
@@ -341397,6 +341490,459 @@ function underFunded(entry) {
341397
341490
  return entry.resized ? `UNDER_FUNDED: the orderbook is advertising ${entry.advertisedSize} of the ${entry.quotedAmount} quoted` : "UNDER_FUNDED: the solver's balance does not cover this posting";
341398
341491
  }
341399
341492
 
341493
+ // src/orderbook/profitability.ts
341494
+ init_esm_shims();
341495
+ init_decimal();
341496
+ init_asset_registry();
341497
+
341498
+ // src/data/inventory.ts
341499
+ init_esm_shims();
341500
+ var INVENTORY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
341501
+ function inventoryOf(snapshot) {
341502
+ if (snapshot.status !== "fresh") return null;
341503
+ const held = {};
341504
+ for (const chain2 of snapshot.chains) {
341505
+ for (const asset of chain2.assets) {
341506
+ if (asset.total === null) return null;
341507
+ held[asset.symbol] = (held[asset.symbol] ?? 0) + asset.total;
341508
+ }
341509
+ }
341510
+ return Object.keys(held).length > 0 ? held : null;
341511
+ }
341512
+ function parseUtc(value) {
341513
+ return Date.parse(`${value.replace(" ", "T")}Z`);
341514
+ }
341515
+ var InventoryRecorder = class {
341516
+ constructor(store, balances, logger8, now = Date.now) {
341517
+ this.store = store;
341518
+ this.balances = balances;
341519
+ this.logger = logger8;
341520
+ this.now = now;
341521
+ this.recording = false;
341522
+ this.onSnapshot = (snapshot) => void this.consider(snapshot);
341523
+ }
341524
+ start() {
341525
+ this.balances.on("snapshot", this.onSnapshot);
341526
+ void this.consider(this.balances.getSnapshot());
341527
+ }
341528
+ stop() {
341529
+ this.balances.off("snapshot", this.onSnapshot);
341530
+ }
341531
+ /** Takes a snapshot from `snapshot` if one is due and the read is complete. Never rejects. */
341532
+ async consider(snapshot) {
341533
+ if (this.recording) return;
341534
+ const held = inventoryOf(snapshot);
341535
+ if (!held) return;
341536
+ this.recording = true;
341537
+ try {
341538
+ if (this.lastTakenAt === void 0) {
341539
+ const latest2 = await this.store.latest();
341540
+ this.lastTakenAt = latest2 ? parseUtc(latest2.takenAt) : 0;
341541
+ }
341542
+ if (this.now() - this.lastTakenAt < INVENTORY_INTERVAL_MS) return;
341543
+ await this.store.record(held);
341544
+ this.lastTakenAt = this.now();
341545
+ this.logger.info({ tokens: Object.keys(held).length }, "Recorded an inventory snapshot");
341546
+ } catch (err) {
341547
+ this.logger.warn({ err }, "Could not record an inventory snapshot");
341548
+ } finally {
341549
+ this.recording = false;
341550
+ }
341551
+ }
341552
+ };
341553
+
341554
+ // src/orderbook/usd.ts
341555
+ init_esm_shims();
341556
+ init_decimal();
341557
+ init_asset_registry();
341558
+ function limitOrderUsdEdges(orders) {
341559
+ return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
341560
+ base: normalizeSymbol(order.base),
341561
+ quote: normalizeSymbol(order.quote),
341562
+ rate: new Decimal(order.price).div(ORDERBOOK_SCALE2.toString())
341563
+ })).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
341564
+ }
341565
+ function usdFactorsFrom(edges) {
341566
+ const factors = /* @__PURE__ */ new Map();
341567
+ for (const symbol3 of USD_STABLE_SYMBOLS) factors.set(symbol3, new Decimal(1));
341568
+ const ordered = [...edges].sort((a6, b5) => a6.base.localeCompare(b5.base) || a6.quote.localeCompare(b5.quote));
341569
+ let grew = true;
341570
+ while (grew) {
341571
+ grew = false;
341572
+ for (const edge of ordered) {
341573
+ const usdBase = factors.get(edge.base);
341574
+ const usdQuote = factors.get(edge.quote);
341575
+ if (usdBase && !usdQuote) {
341576
+ factors.set(edge.quote, usdBase.div(edge.rate));
341577
+ grew = true;
341578
+ } else if (usdQuote && !usdBase) {
341579
+ factors.set(edge.base, usdQuote.mul(edge.rate));
341580
+ grew = true;
341581
+ }
341582
+ }
341583
+ }
341584
+ return factors;
341585
+ }
341586
+ function usdValueOf(factors, symbol3, amount) {
341587
+ const factor = factors.get(normalizeSymbol(symbol3));
341588
+ return factor ? amount.mul(factor) : null;
341589
+ }
341590
+
341591
+ // src/orderbook/profitability.ts
341592
+ var Decimal2 = Decimal.clone({ precision: 40 });
341593
+ var PROFIT_PERIODS = {
341594
+ "7d": { bucket: "day", count: 7 },
341595
+ "30d": { bucket: "day", count: 30 },
341596
+ "12w": { bucket: "week", count: 12 },
341597
+ "12m": { bucket: "month", count: 12 },
341598
+ all: { bucket: "year", count: null }
341599
+ };
341600
+ function isProfitPeriod(value) {
341601
+ return value in PROFIT_PERIODS;
341602
+ }
341603
+ var MINUTE = 6e4;
341604
+ var DAY = 864e5;
341605
+ var BALANCES_REACH_MS = 7 * DAY;
341606
+ function bucketStart(local, bucket) {
341607
+ const date = new Date(local);
341608
+ const year = date.getUTCFullYear();
341609
+ const month = date.getUTCMonth();
341610
+ if (bucket === "year") return Date.UTC(year, 0, 1);
341611
+ if (bucket === "month") return Date.UTC(year, month, 1);
341612
+ const day = Date.UTC(year, month, date.getUTCDate());
341613
+ return bucket === "day" ? day : day - (date.getUTCDay() + 6) % 7 * DAY;
341614
+ }
341615
+ function shiftBucket(start, bucket, steps) {
341616
+ if (bucket === "day") return start + steps * DAY;
341617
+ if (bucket === "week") return start + steps * 7 * DAY;
341618
+ const date = new Date(start);
341619
+ return bucket === "month" ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + steps, 1) : Date.UTC(date.getUTCFullYear() + steps, 0, 1);
341620
+ }
341621
+ function scaled(value) {
341622
+ return new Decimal2(value).div(ORDERBOOK_SCALE2.toString());
341623
+ }
341624
+ function tradeOf(fill, at) {
341625
+ try {
341626
+ const paid = scaled(fill.amount);
341627
+ const taken = fill.amountIn === null ? null : scaled(fill.amountIn);
341628
+ const price = scaled(fill.price);
341629
+ if (!paid.gt(0) || !price.gt(0) || taken !== null && !taken.gt(0)) return null;
341630
+ const buying = fill.side === "BID";
341631
+ const [baseAmount, quoteAmount] = buying ? [taken ?? paid.div(price), paid] : [paid, taken ?? paid.mul(price)];
341632
+ return {
341633
+ at,
341634
+ book: fill.book,
341635
+ base: normalizeSymbol(fill.base),
341636
+ quote: normalizeSymbol(fill.quote),
341637
+ buying,
341638
+ baseAmount,
341639
+ quoteAmount,
341640
+ estimated: taken === null
341641
+ };
341642
+ } catch {
341643
+ return null;
341644
+ }
341645
+ }
341646
+ function add3(holdings, symbol3, amount) {
341647
+ holdings.set(symbol3, (holdings.get(symbol3) ?? new Decimal2(0)).add(amount));
341648
+ }
341649
+ function apply3(holdings, trade, sign6) {
341650
+ const base4 = trade.buying ? trade.baseAmount : trade.baseAmount.neg();
341651
+ const quote = trade.buying ? trade.quoteAmount.neg() : trade.quoteAmount;
341652
+ add3(holdings, trade.base, base4.mul(sign6));
341653
+ add3(holdings, trade.quote, quote.mul(sign6));
341654
+ }
341655
+ var Sums = class {
341656
+ constructor() {
341657
+ this.change = /* @__PURE__ */ new Map();
341658
+ this.boughtUsd = new Decimal2(0);
341659
+ this.soldUsd = new Decimal2(0);
341660
+ this.buys = 0;
341661
+ this.sells = 0;
341662
+ }
341663
+ };
341664
+ var BookLedger = class {
341665
+ constructor(id2, base4, quote) {
341666
+ this.id = id2;
341667
+ this.base = base4;
341668
+ this.quote = quote;
341669
+ this.bought = new Decimal2(0);
341670
+ this.boughtQuote = new Decimal2(0);
341671
+ this.sold = new Decimal2(0);
341672
+ this.soldQuote = new Decimal2(0);
341673
+ /** The rates of the book's latest buy and latest sell, over all of its history. */
341674
+ this.latestBuy = null;
341675
+ this.latestSell = null;
341676
+ this.inPeriod = false;
341677
+ }
341678
+ /**
341679
+ * The rate the book is valued at. Midway between the latest buy and the latest sell, because
341680
+ * either alone sits half a spread to one side and would price open volume against whichever
341681
+ * way the book happened to trade last.
341682
+ */
341683
+ rate() {
341684
+ if (this.latestBuy && this.latestSell) return this.latestBuy.add(this.latestSell).div(2);
341685
+ return this.latestBuy ?? this.latestSell;
341686
+ }
341687
+ };
341688
+ function readingOf(reading) {
341689
+ const held = /* @__PURE__ */ new Map();
341690
+ const cover = /* @__PURE__ */ new Map();
341691
+ for (const [symbol3, amount] of Object.entries(reading.balances)) {
341692
+ const chains2 = reading.chains[symbol3];
341693
+ if (!Number.isFinite(amount) || !chains2?.length) continue;
341694
+ add3(held, normalizeSymbol(symbol3), new Decimal2(amount));
341695
+ cover.set(normalizeSymbol(symbol3), [...chains2].sort().join(","));
341696
+ }
341697
+ return { held, cover };
341698
+ }
341699
+ function movedBetween(earlier, later) {
341700
+ if (!earlier || !later) return null;
341701
+ const moved = /* @__PURE__ */ new Map();
341702
+ for (const [symbol3, cover] of later.cover) {
341703
+ if (earlier.cover.get(symbol3) !== cover) continue;
341704
+ const before = earlier.held.get(symbol3) ?? new Decimal2(0);
341705
+ moved.set(symbol3, (later.held.get(symbol3) ?? new Decimal2(0)).sub(before));
341706
+ }
341707
+ return moved;
341708
+ }
341709
+ function nearestRecord(instant, records) {
341710
+ let record;
341711
+ let gap = Number.POSITIVE_INFINITY;
341712
+ for (const candidate of records) {
341713
+ const distance = Math.abs(candidate.at - instant);
341714
+ if (candidate.live && distance > BALANCES_REACH_MS) continue;
341715
+ if (distance < gap || distance === gap && record?.live && !candidate.live) {
341716
+ record = candidate;
341717
+ gap = distance;
341718
+ }
341719
+ }
341720
+ return record;
341721
+ }
341722
+ function inventoryAt(instant, records, trades, outflows, readings) {
341723
+ const record = nearestRecord(instant, records);
341724
+ if (!record) return null;
341725
+ const recorded = /* @__PURE__ */ new Map();
341726
+ for (const [symbol3, amount] of Object.entries(record.balances)) {
341727
+ if (Number.isFinite(amount)) add3(recorded, normalizeSymbol(symbol3), new Decimal2(amount));
341728
+ }
341729
+ const held = new Map(recorded);
341730
+ const after = record.at >= instant;
341731
+ const [from32, to] = after ? [instant, record.at] : [record.at, instant];
341732
+ const sign6 = after ? -1 : 1;
341733
+ for (const trade of trades) {
341734
+ if (trade.at >= from32 && trade.at < to) apply3(held, trade, sign6);
341735
+ }
341736
+ for (const outflow of outflows) {
341737
+ if (outflow.at >= from32 && outflow.at < to) {
341738
+ add3(held, normalizeSymbol(outflow.symbol), new Decimal2(outflow.amount).mul(-sign6));
341739
+ }
341740
+ }
341741
+ const moved = movedBetween(readings.get(from32), readings.get(to));
341742
+ if (moved) {
341743
+ for (const [symbol3, amount] of moved) {
341744
+ held.set(symbol3, (recorded.get(symbol3) ?? new Decimal2(0)).add(amount.mul(sign6)));
341745
+ }
341746
+ }
341747
+ for (const [symbol3, amount] of held) if (amount.isNegative()) held.set(symbol3, new Decimal2(0));
341748
+ return { held, record };
341749
+ }
341750
+ function tradesOf(fills, now) {
341751
+ const trades = [];
341752
+ for (const fill of fills) {
341753
+ const at = parseUtc(fill.filledAt);
341754
+ if (!Number.isFinite(at) || at > now) continue;
341755
+ const trade = tradeOf(fill, at);
341756
+ if (trade) trades.push(trade);
341757
+ }
341758
+ return trades.sort((a6, b5) => a6.at - b5.at);
341759
+ }
341760
+ function bucketStarts(period, now, offset, firstAt) {
341761
+ const { bucket, count } = PROFIT_PERIODS[period];
341762
+ const last = bucketStart(now - offset, bucket);
341763
+ const first3 = count === null ? bucketStart((firstAt ?? now) - offset, bucket) : shiftBucket(last, bucket, 1 - count);
341764
+ const starts = [];
341765
+ for (let start = first3; start <= last; start = shiftBucket(start, bucket, 1)) starts.push(start);
341766
+ return starts;
341767
+ }
341768
+ function inventoryInstants(fills, options) {
341769
+ const { period, now } = options;
341770
+ const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
341771
+ const records = (options.inventory ?? []).filter((record) => record.at <= now);
341772
+ const instants = /* @__PURE__ */ new Set([now]);
341773
+ for (const start of bucketStarts(period, now, offset, tradesOf(fills, now)[0]?.at)) {
341774
+ instants.add(start + offset);
341775
+ const record = nearestRecord(start + offset, records);
341776
+ if (record) instants.add(record.at);
341777
+ }
341778
+ return [...instants].sort((a6, b5) => a6 - b5);
341779
+ }
341780
+ function summarizeProfit(fills, options) {
341781
+ const { period, now } = options;
341782
+ const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
341783
+ const { bucket } = PROFIT_PERIODS[period];
341784
+ const records = (options.inventory ?? []).filter((record) => record.at <= now);
341785
+ const outflows = options.outflows ?? [];
341786
+ const readings = new Map((options.readings ?? []).map((reading) => [reading.at, readingOf(reading)]));
341787
+ const trades = tradesOf(fills, now);
341788
+ const starts = bucketStarts(period, now, offset, trades[0]?.at);
341789
+ const first3 = starts[0];
341790
+ const books = /* @__PURE__ */ new Map();
341791
+ const spelling2 = /* @__PURE__ */ new Map();
341792
+ for (const record of records) {
341793
+ for (const symbol3 of Object.keys(record.balances)) {
341794
+ if (!spelling2.has(normalizeSymbol(symbol3))) spelling2.set(normalizeSymbol(symbol3), symbol3);
341795
+ }
341796
+ }
341797
+ for (const fill of fills) {
341798
+ for (const symbol3 of [fill.base, fill.quote]) {
341799
+ if (!spelling2.has(normalizeSymbol(symbol3))) spelling2.set(normalizeSymbol(symbol3), symbol3);
341800
+ }
341801
+ }
341802
+ for (const trade of trades) {
341803
+ let book = books.get(trade.book);
341804
+ if (!book) {
341805
+ book = new BookLedger(trade.book, trade.base, trade.quote);
341806
+ books.set(trade.book, book);
341807
+ }
341808
+ const rate = trade.quoteAmount.div(trade.baseAmount);
341809
+ if (trade.buying) book.latestBuy = rate;
341810
+ else book.latestSell = rate;
341811
+ }
341812
+ const edges = [];
341813
+ for (const book of books.values()) {
341814
+ const rate = book.rate();
341815
+ if (rate && book.base !== book.quote) {
341816
+ edges.push({ base: book.base, quote: book.quote, rate: new Decimal(rate.toString()) });
341817
+ }
341818
+ }
341819
+ const factors = usdFactorsFrom(edges);
341820
+ const priceOf = (symbol3) => {
341821
+ const factor = factors.get(symbol3);
341822
+ return factor ? new Decimal2(factor.toString()) : null;
341823
+ };
341824
+ const unpriced = /* @__PURE__ */ new Set();
341825
+ const valueOf = (holdings) => {
341826
+ let total = new Decimal2(0);
341827
+ for (const [symbol3, amount] of holdings) {
341828
+ if (amount.isZero()) continue;
341829
+ const price = priceOf(symbol3);
341830
+ if (price) total = total.add(amount.mul(price));
341831
+ else unpriced.add(symbol3);
341832
+ }
341833
+ return total;
341834
+ };
341835
+ const totals = new Sums();
341836
+ const buckets = new Map(starts.map((start) => [start, new Sums()]));
341837
+ let estimatedFills = 0;
341838
+ for (const trade of trades) {
341839
+ const sums = buckets.get(bucketStart(trade.at - offset, bucket));
341840
+ if (!sums) continue;
341841
+ const book = books.get(trade.book);
341842
+ if (!book) continue;
341843
+ book.inPeriod = true;
341844
+ if (trade.estimated) estimatedFills += 1;
341845
+ if (trade.buying) {
341846
+ book.bought = book.bought.add(trade.baseAmount);
341847
+ book.boughtQuote = book.boughtQuote.add(trade.quoteAmount);
341848
+ } else {
341849
+ book.sold = book.sold.add(trade.baseAmount);
341850
+ book.soldQuote = book.soldQuote.add(trade.quoteAmount);
341851
+ }
341852
+ const basePrice = priceOf(trade.base);
341853
+ const quotePrice = priceOf(trade.quote);
341854
+ const volume = basePrice ? trade.baseAmount.mul(basePrice) : quotePrice ? trade.quoteAmount.mul(quotePrice) : new Decimal2(0);
341855
+ for (const sum2 of [totals, sums]) {
341856
+ apply3(sum2.change, trade, 1);
341857
+ if (trade.buying) {
341858
+ sum2.buys += 1;
341859
+ sum2.boughtUsd = sum2.boughtUsd.add(volume);
341860
+ } else {
341861
+ sum2.sells += 1;
341862
+ sum2.soldUsd = sum2.soldUsd.add(volume);
341863
+ }
341864
+ }
341865
+ }
341866
+ const figures = (sums, startsAt, endsAt) => {
341867
+ const profit = valueOf(sums.change);
341868
+ const began2 = inventoryAt(startsAt, records, trades, outflows, readings);
341869
+ const inventory = began2 ? valueOf(began2.held) : null;
341870
+ const moved = movedBetween(readings.get(startsAt), readings.get(endsAt));
341871
+ if (moved) {
341872
+ for (const [symbol3, amount] of sums.change) {
341873
+ if (moved.has(symbol3)) moved.set(symbol3, (moved.get(symbol3) ?? new Decimal2(0)).sub(amount));
341874
+ }
341875
+ }
341876
+ return {
341877
+ profitUsd: profit.toNumber(),
341878
+ startInventoryUsd: inventory ? inventory.toNumber() : null,
341879
+ returnPct: inventory?.gt(0) ? profit.div(inventory).mul(100).toNumber() : null,
341880
+ transfersUsd: moved && moved.size > 0 ? valueOf(moved).toNumber() : null,
341881
+ boughtUsd: sums.boughtUsd.toNumber(),
341882
+ soldUsd: sums.soldUsd.toNumber(),
341883
+ buys: sums.buys,
341884
+ sells: sums.sells
341885
+ };
341886
+ };
341887
+ const from32 = first3 + offset;
341888
+ const began = inventoryAt(from32, records, trades, outflows, readings);
341889
+ const startInventory = began && {
341890
+ at: from32,
341891
+ source: began.record.live ? "balances" : "snapshot",
341892
+ recordedAt: began.record.at,
341893
+ usd: valueOf(began.held).toNumber(),
341894
+ tokens: [...began.held].filter(([, amount]) => amount.gt(0)).map(([symbol3, amount]) => {
341895
+ const price = priceOf(symbol3);
341896
+ return {
341897
+ symbol: spelling2.get(symbol3) ?? symbol3,
341898
+ amount: amount.toNumber(),
341899
+ usd: price ? amount.mul(price).toNumber() : null
341900
+ };
341901
+ }).sort((a6, b5) => (b5.usd ?? 0) - (a6.usd ?? 0) || a6.symbol.localeCompare(b5.symbol))
341902
+ };
341903
+ const rows = [];
341904
+ for (const book of books.values()) {
341905
+ if (!book.inPeriod) continue;
341906
+ const averageBuy = book.bought.gt(0) ? book.boughtQuote.div(book.bought) : null;
341907
+ const averageSell = book.sold.gt(0) ? book.soldQuote.div(book.sold) : null;
341908
+ const baseChange = book.bought.sub(book.sold);
341909
+ const quoteChange = book.soldQuote.sub(book.boughtQuote);
341910
+ const basePrice = priceOf(book.base);
341911
+ const quotePrice = priceOf(book.quote);
341912
+ rows.push({
341913
+ book: book.id,
341914
+ base: spelling2.get(book.base) ?? book.base,
341915
+ quote: spelling2.get(book.quote) ?? book.quote,
341916
+ bought: book.bought.toNumber(),
341917
+ sold: book.sold.toNumber(),
341918
+ averageBuy: averageBuy ? averageBuy.toNumber() : null,
341919
+ averageSell: averageSell ? averageSell.toNumber() : null,
341920
+ spreadPct: averageBuy && averageSell ? averageSell.div(averageBuy).sub(1).mul(100).toNumber() : null,
341921
+ baseChange: baseChange.toNumber(),
341922
+ quoteChange: quoteChange.toNumber(),
341923
+ rate: book.rate()?.toNumber() ?? null,
341924
+ profitUsd: basePrice && quotePrice ? baseChange.mul(basePrice).add(quoteChange.mul(quotePrice)).toNumber() : null
341925
+ });
341926
+ }
341927
+ rows.sort((a6, b5) => (b5.profitUsd ?? 0) - (a6.profitUsd ?? 0) || a6.book.localeCompare(b5.book));
341928
+ return {
341929
+ period,
341930
+ bucket,
341931
+ from: from32,
341932
+ to: now,
341933
+ totals: figures(totals, from32, now),
341934
+ series: starts.map((start, index3) => {
341935
+ const sums = buckets.get(start) ?? new Sums();
341936
+ const endsAt = index3 + 1 < starts.length ? starts[index3 + 1] + offset : now;
341937
+ return { start: start + offset, ...figures(sums, start + offset, endsAt) };
341938
+ }),
341939
+ books: rows,
341940
+ startInventory,
341941
+ estimatedFills,
341942
+ unpricedTokens: [...unpriced].map((symbol3) => spelling2.get(symbol3) ?? symbol3).sort()
341943
+ };
341944
+ }
341945
+
341400
341946
  // src/services/server/UiServer.ts
341401
341947
  init_Logger();
341402
341948
 
@@ -343639,6 +344185,55 @@ var UiServer = class {
343639
344185
  if (method !== "GET") return sendJson(res, 405, { error: "Method not allowed" });
343640
344186
  return this.handleLimitOrders(res, () => this.operator.limitOrders.books());
343641
344187
  }
344188
+ if (path2 === "/api/analytics/profitability") {
344189
+ if (this.mode !== "operator") return sendJson(res, 409, { error: "Filler is not running" });
344190
+ if (method !== "GET") return sendJson(res, 405, { error: "Method not allowed" });
344191
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
344192
+ const period = params.get("period") ?? "7d";
344193
+ if (!isProfitPeriod(period)) return sendJson(res, 400, { error: "period must be 7d, 30d, 12w, 12m or all" });
344194
+ const tz = Number(params.get("tz") ?? 0);
344195
+ if (!Number.isInteger(tz) || Math.abs(tz) > 14 * 60) {
344196
+ return sendJson(res, 400, { error: "tz must be a whole number of minutes" });
344197
+ }
344198
+ return this.handleLimitOrders(res, () => this.operator.limitOrders.profitability(period, tz));
344199
+ }
344200
+ if (path2 === "/api/orderbook/snapshot") {
344201
+ if (this.mode !== "operator") return sendJson(res, 409, { error: "Filler is not running" });
344202
+ if (method !== "GET") return sendJson(res, 405, { error: "Method not allowed" });
344203
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
344204
+ const book = params.get("book")?.trim();
344205
+ if (!book) return sendJson(res, 400, { error: "A book is required" });
344206
+ return this.handleLimitOrders(
344207
+ res,
344208
+ () => this.operator.limitOrders.orderbookSnapshot(book, {
344209
+ sourceChain: params.get("sourceChain") || void 0,
344210
+ fillChain: params.get("fillChain") || void 0
344211
+ })
344212
+ );
344213
+ }
344214
+ if (path2 === "/api/orderbook/level-orders") {
344215
+ if (this.mode !== "operator") return sendJson(res, 409, { error: "Filler is not running" });
344216
+ if (method !== "GET") return sendJson(res, 405, { error: "Method not allowed" });
344217
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
344218
+ const book = params.get("book")?.trim();
344219
+ const side = params.get("side");
344220
+ const fillChain = params.get("fillChain")?.trim();
344221
+ const priceBucket = params.get("priceBucket")?.trim();
344222
+ if (!book) return sendJson(res, 400, { error: "A book is required" });
344223
+ if (side !== "BID" && side !== "ASK") return sendJson(res, 400, { error: "side must be BID or ASK" });
344224
+ if (!fillChain) return sendJson(res, 400, { error: "A fill chain is required" });
344225
+ if (!priceBucket || !/^\d+$/.test(priceBucket))
344226
+ return sendJson(res, 400, { error: "priceBucket must be an integer" });
344227
+ return this.handleLimitOrders(
344228
+ res,
344229
+ () => this.operator.limitOrders.orderbookLevelOrders(book, {
344230
+ side,
344231
+ fillChain,
344232
+ priceBucket,
344233
+ sourceChain: params.get("sourceChain") || void 0
344234
+ })
344235
+ );
344236
+ }
343642
344237
  const limitOrderMatch = path2.match(/^\/api\/limit-orders\/([\w-]+)$/);
343643
344238
  if (limitOrderMatch) {
343644
344239
  if (this.mode !== "operator") return sendJson(res, 409, { error: "Filler is not running" });
@@ -345000,6 +345595,7 @@ var CacheService = class {
345000
345595
  })),
345001
345596
  partialFill: plan.partialFill,
345002
345597
  profit: plan.profit,
345598
+ ...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
345003
345599
  ...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
345004
345600
  }));
345005
345601
  } catch (error) {
@@ -345030,6 +345626,7 @@ var CacheService = class {
345030
345626
  })),
345031
345627
  partialFill: plan.partialFill,
345032
345628
  profit: plan.profit,
345629
+ ...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
345033
345630
  ...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
345034
345631
  })),
345035
345632
  timestamp: Date.now()
@@ -384432,7 +385029,12 @@ var IntentFiller = class {
384432
385029
  else cache.clearFundingPrepends(order.id);
384433
385030
  }
384434
385031
  reservation = await this.holdAll([
384435
- { limitOrderId: plan.limitOrderId, payout: plan.payout, take: plan.fillerInputs[plan.leg]?.amount }
385032
+ {
385033
+ limitOrderId: plan.limitOrderId,
385034
+ payout: plan.payout,
385035
+ take: plan.fillerInputs[plan.leg]?.amount,
385036
+ takeDecimals: plan.inputDecimals
385037
+ }
384436
385038
  ]);
384437
385039
  if (reservation.length === 0) {
384438
385040
  this.logger.info(
@@ -384602,7 +385204,8 @@ var IntentFiller = class {
384602
385204
  taken.push({
384603
385205
  limitOrderId: hold.limitOrderId,
384604
385206
  amount,
384605
- ...hold.take !== void 0 ? { take: hold.take.toString() } : {}
385207
+ ...hold.take !== void 0 ? { take: hold.take.toString() } : {},
385208
+ ...hold.takeDecimals !== void 0 ? { takeDecimals: hold.takeDecimals } : {}
384606
385209
  });
384607
385210
  }
384608
385211
  return taken;
@@ -384629,6 +385232,7 @@ var IntentFiller = class {
384629
385232
  }
384630
385233
  const settled = executedHold(claimed, delivered.scaled, released);
384631
385234
  const charged = chargedFor(settled, delivered, released);
385235
+ const takenIn = released > 0n && settled.takeDecimals !== void 0 ? toScaled(released, settled.takeDecimals) : null;
384632
385236
  const settledBid = rows.find(
384633
385237
  (row) => row.reservations.some(
384634
385238
  (hold) => hold.limitOrderId === settled.limitOrderId && hold.amount === settled.amount && hold.take === settled.take
@@ -384647,6 +385251,7 @@ var IntentFiller = class {
384647
385251
  commitment,
384648
385252
  bid: settledBid,
384649
385253
  amount: share.toString(),
385254
+ amountIn: takenIn?.toString() ?? null,
384650
385255
  transactionHash: transactionHash ?? null
384651
385256
  });
384652
385257
  }
@@ -384799,45 +385404,6 @@ init_esm();
384799
385404
  init_Logger();
384800
385405
  init_asset_registry();
384801
385406
  init_decimal();
384802
-
384803
- // src/orderbook/usd.ts
384804
- init_esm_shims();
384805
- init_decimal();
384806
- init_asset_registry();
384807
- function limitOrderUsdEdges(orders) {
384808
- return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
384809
- base: normalizeSymbol(order.base),
384810
- quote: normalizeSymbol(order.quote),
384811
- rate: new Decimal(order.price).div(ORDERBOOK_SCALE2.toString())
384812
- })).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
384813
- }
384814
- function usdFactorsFrom(edges) {
384815
- const factors = /* @__PURE__ */ new Map();
384816
- for (const symbol3 of USD_STABLE_SYMBOLS) factors.set(symbol3, new Decimal(1));
384817
- const ordered = [...edges].sort((a6, b5) => a6.base.localeCompare(b5.base) || a6.quote.localeCompare(b5.quote));
384818
- let grew = true;
384819
- while (grew) {
384820
- grew = false;
384821
- for (const edge of ordered) {
384822
- const usdBase = factors.get(edge.base);
384823
- const usdQuote = factors.get(edge.quote);
384824
- if (usdBase && !usdQuote) {
384825
- factors.set(edge.quote, usdBase.div(edge.rate));
384826
- grew = true;
384827
- } else if (usdQuote && !usdBase) {
384828
- factors.set(edge.base, usdQuote.mul(edge.rate));
384829
- grew = true;
384830
- }
384831
- }
384832
- }
384833
- return factors;
384834
- }
384835
- function usdValueOf(factors, symbol3, amount) {
384836
- const factor = factors.get(normalizeSymbol(symbol3));
384837
- return factor ? amount.mul(factor) : null;
384838
- }
384839
-
384840
- // src/strategies/fx.ts
384841
385407
  function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
384842
385408
  if (fromDecimals === toDecimals) return amount;
384843
385409
  if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
@@ -385318,6 +385884,7 @@ var FXFiller = class _FXFiller {
385318
385884
  payout: toScaled(finalOutputAmount, outputDecimals),
385319
385885
  fillerOutputs,
385320
385886
  fillerInputs,
385887
+ inputDecimals,
385321
385888
  fundingCalls: [...fundingCalls],
385322
385889
  partialFill,
385323
385890
  profit: totalProfit,
@@ -386812,9 +387379,10 @@ async function bootFiller(config5, options) {
386812
387379
  const activity = new ActivityRecorder(options.data.activity, options.loggers, { describeToken });
386813
387380
  activity.attach(intentFiller.monitor);
386814
387381
  const network = resolvedChains.some((chain2) => chainByChainId(chain2.chainId)?.network === "testnet") ? "testnet" : "mainnet";
387382
+ const indexerUrl = config5.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network];
386815
387383
  void backfillOrderSummaries({
386816
387384
  store: options.data.activity,
386817
- indexerUrl: config5.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network],
387385
+ indexerUrl,
386818
387386
  fillerAddress: runtimeSigner.address,
386819
387387
  describeToken,
386820
387388
  onUpdated: (rows) => {
@@ -386880,6 +387448,15 @@ async function bootFiller(config5, options) {
386880
387448
  vaultVenue?.startSweeping();
386881
387449
  started.push(() => balanceProvider.stop());
386882
387450
  await balanceProvider.start();
387451
+ if (options.signer) {
387452
+ const inventoryRecorder = new InventoryRecorder(
387453
+ options.data.inventory,
387454
+ balanceProvider,
387455
+ moduleLogger(options.loggers, "inventory")
387456
+ );
387457
+ started.push(() => inventoryRecorder.stop());
387458
+ inventoryRecorder.start();
387459
+ }
386883
387460
  const watchOnlyChains = watchOnlyConfig ? Object.entries(watchOnlyConfig).filter(([, value]) => value === true).map(([chainId]) => Number.parseInt(chainId, 10)) : [];
386884
387461
  logger8.info(
386885
387462
  {
@@ -386925,6 +387502,8 @@ async function bootFiller(config5, options) {
386925
387502
  rebalancingService,
386926
387503
  resolvedChains,
386927
387504
  fillerAddress: runtimeSigner.address,
387505
+ indexerUrl,
387506
+ describeToken,
386928
387507
  watchOnly: watchOnlyConfig,
386929
387508
  config: config5,
386930
387509
  configPath: options.configPath,
@@ -387208,10 +387787,20 @@ var MemoryLimitOrderStore = class {
387208
387787
  commitment: fill.commitment,
387209
387788
  bid: fill.bid ?? null,
387210
387789
  amount: fill.amount,
387790
+ amountIn: fill.amountIn ?? null,
387211
387791
  transactionHash: fill.transactionHash ?? null,
387212
387792
  filledAt: sqliteDatetime(/* @__PURE__ */ new Date())
387213
387793
  });
387214
387794
  }
387795
+ async fillHistory() {
387796
+ return this.fillRows.flatMap((fill) => {
387797
+ const order = this.orders.get(fill.limitOrderId);
387798
+ if (!order) return [];
387799
+ const { id: id2, limitOrderId, amount, amountIn, filledAt } = fill;
387800
+ const { book, base: base4, quote, side, price } = order;
387801
+ return [{ id: id2, limitOrderId, book, base: base4, quote, side, price, amount, amountIn, filledAt }];
387802
+ });
387803
+ }
387215
387804
  async fills(limitOrderId, limit = 100) {
387216
387805
  return this.fillRows.filter((fill) => fill.limitOrderId === limitOrderId).slice(-capLimit(limit)).reverse().map((fill) => ({ ...fill }));
387217
387806
  }
@@ -387318,12 +387907,29 @@ var MemoryStateStore = class {
387318
387907
  this.state = { ...state };
387319
387908
  }
387320
387909
  };
387910
+ var MemoryInventoryStore = class {
387911
+ constructor() {
387912
+ this.rows = [];
387913
+ }
387914
+ async record(balances) {
387915
+ this.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(/* @__PURE__ */ new Date()), balances: { ...balances } });
387916
+ }
387917
+ async latest() {
387918
+ const last = this.rows[this.rows.length - 1];
387919
+ return last ? { ...last, balances: { ...last.balances } } : null;
387920
+ }
387921
+ async since(from32) {
387922
+ const cutoff = sqliteDatetime(from32);
387923
+ return this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }));
387924
+ }
387925
+ };
387321
387926
  var MemoryDataStore = class {
387322
387927
  constructor() {
387323
387928
  this.bids = new MemoryBidStore();
387324
387929
  this.activity = new MemoryActivityStore();
387325
387930
  this.state = new MemoryStateStore();
387326
387931
  this.limitOrders = new MemoryLimitOrderStore();
387932
+ this.inventory = new MemoryInventoryStore();
387327
387933
  }
387328
387934
  };
387329
387935
 
@@ -387935,6 +388541,107 @@ var OrderScanner = class _OrderScanner {
387935
388541
  }
387936
388542
  };
387937
388543
 
388544
+ // src/data/indexer-inventory.ts
388545
+ init_esm_shims();
388546
+ init_esm();
388547
+ var READ_TIMEOUT_MS = 1e4;
388548
+ var SETTLED_AFTER_MS = 10 * 6e4;
388549
+ var BATCH = 40;
388550
+ var MAX_KEPT = 2e3;
388551
+ var IndexerInventory = class {
388552
+ constructor(options) {
388553
+ this.options = options;
388554
+ this.kept = /* @__PURE__ */ new Map();
388555
+ this.described = /* @__PURE__ */ new Map();
388556
+ this.fetch = options.fetch ?? fetch;
388557
+ }
388558
+ /**
388559
+ * A reading for each of `instants` the indexer could answer for. Never rejects: an instant it
388560
+ * could not be asked about is simply missing from the answer.
388561
+ */
388562
+ async at(instants, now) {
388563
+ const readings = [];
388564
+ const wanted = [];
388565
+ for (const instant of new Set(instants)) {
388566
+ const kept = this.kept.get(instant);
388567
+ if (kept) readings.push(kept);
388568
+ else wanted.push(instant);
388569
+ }
388570
+ try {
388571
+ for (let index3 = 0; index3 < wanted.length; index3 += BATCH) {
388572
+ for (const reading of await this.read(wanted.slice(index3, index3 + BATCH))) {
388573
+ readings.push(reading);
388574
+ if (reading.at <= now - SETTLED_AFTER_MS) this.keep(reading);
388575
+ }
388576
+ }
388577
+ } catch (err) {
388578
+ this.options.logger.warn(
388579
+ { err: err instanceof Error ? err.message : String(err) },
388580
+ "Could not read inventory from the indexer; carrying records by fills and sends alone"
388581
+ );
388582
+ }
388583
+ return readings;
388584
+ }
388585
+ keep(reading) {
388586
+ if (this.kept.size >= MAX_KEPT) this.kept.clear();
388587
+ this.kept.set(reading.at, reading);
388588
+ }
388589
+ /** One request for several instants, each its own aliased query at that moment in history. */
388590
+ async read(instants) {
388591
+ const queries = instants.map(
388592
+ (instant, index3) => `r${index3}: solverInventories(filter: { solver: { equalTo: $solver } }, blockHeight: "${Math.floor(instant)}", first: 1000) { nodes { chain tokenAddress balance } }`
388593
+ );
388594
+ const response = await this.fetch(this.options.indexerUrl, {
388595
+ method: "POST",
388596
+ headers: { "content-type": "application/json" },
388597
+ body: JSON.stringify({
388598
+ query: `query SolverInventory($solver: String!) {
388599
+ ${queries.join("\n")}
388600
+ }`,
388601
+ variables: { solver: this.options.solver.toLowerCase() }
388602
+ }),
388603
+ signal: AbortSignal.timeout(READ_TIMEOUT_MS)
388604
+ });
388605
+ if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
388606
+ const body = await response.json();
388607
+ if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
388608
+ const readings = [];
388609
+ for (const [index3, instant] of instants.entries()) {
388610
+ const rows = body.data?.[`r${index3}`]?.nodes;
388611
+ if (rows) readings.push(await this.readingOf(instant, rows));
388612
+ }
388613
+ return readings;
388614
+ }
388615
+ async readingOf(at, rows) {
388616
+ const balances = {};
388617
+ const chains2 = {};
388618
+ for (const row of rows) {
388619
+ const token = await this.describe(row.chain, row.tokenAddress);
388620
+ if (!token) continue;
388621
+ let amount;
388622
+ try {
388623
+ amount = Number(formatUnits(BigInt(row.balance), token.decimals));
388624
+ } catch {
388625
+ continue;
388626
+ }
388627
+ balances[token.symbol] = (balances[token.symbol] ?? 0) + amount;
388628
+ chains2[token.symbol] = [...chains2[token.symbol] ?? [], row.chain];
388629
+ }
388630
+ return { at, balances, chains: chains2 };
388631
+ }
388632
+ /** A token's symbol and decimals, remembered once both are known. */
388633
+ async describe(chain2, token) {
388634
+ const key = `${chain2}:${token.toLowerCase()}`;
388635
+ const known3 = this.described.get(key);
388636
+ if (known3) return known3;
388637
+ const { symbol: symbol3, decimals } = await this.options.describeToken(chain2, token);
388638
+ if (!symbol3 || decimals === null) return null;
388639
+ const description = { symbol: symbol3, decimals };
388640
+ this.described.set(key, description);
388641
+ return description;
388642
+ }
388643
+ };
388644
+
387938
388645
  // src/simplex.ts
387939
388646
  var EVENT_MAP = {
387940
388647
  newOrder: "order:detected",
@@ -387980,10 +388687,66 @@ var LimitOrderController = class {
387980
388687
  ]);
387981
388688
  return { order, fills, bids };
387982
388689
  }
388690
+ /**
388691
+ * What the operator's buys and sells earned over `period`, bucketed on a clock
388692
+ * `tzOffsetMinutes` behind UTC.
388693
+ *
388694
+ * Profit is the change the period's fills made to what the solver holds, and
388695
+ * it is compared with the inventory the period began with. That inventory
388696
+ * comes from the stored snapshots and, when the balances have been read in
388697
+ * full, from what is held now, each carried to the period's start by what
388698
+ * moved between. The indexer says what moved, deposits and withdrawals
388699
+ * included, wherever it has followed the solver; elsewhere the fills and
388700
+ * sends on record stand in. It answers whether or not an orderbook is
388701
+ * configured, and whether or not the indexer does.
388702
+ */
388703
+ async profitability(period, tzOffsetMinutes = 0) {
388704
+ const now = Date.now();
388705
+ const { data: data4, balanceProvider } = this.runtime;
388706
+ const [fills, snapshots, ledger] = await Promise.all([
388707
+ data4.limitOrders.fillHistory(),
388708
+ data4.inventory.since(/* @__PURE__ */ new Date(0)),
388709
+ data4.activity.walletTxs(500)
388710
+ ]);
388711
+ const inventory = snapshots.map((snapshot) => ({
388712
+ at: parseUtc(snapshot.takenAt),
388713
+ balances: snapshot.balances
388714
+ }));
388715
+ const held = inventoryOf(balanceProvider.getSnapshot());
388716
+ if (held) inventory.push({ at: now, balances: held, live: true });
388717
+ const outflows = [];
388718
+ for (const tx of ledger) {
388719
+ const amount = Number(tx.amount);
388720
+ if (tx.kind === "send" && tx.token && Number.isFinite(amount) && amount > 0) {
388721
+ outflows.push({ at: tx.ts, symbol: tx.token, amount });
388722
+ }
388723
+ }
388724
+ const instants = inventoryInstants(fills, { period, now, tzOffsetMinutes, inventory });
388725
+ const readings = await this.indexerInventory().at(instants, now);
388726
+ return summarizeProfit(fills, { period, now, tzOffsetMinutes, inventory, outflows, readings });
388727
+ }
388728
+ /** One reader for the life of the filler, so the readings it has already fetched are kept. */
388729
+ indexerInventory() {
388730
+ this.indexed ?? (this.indexed = new IndexerInventory({
388731
+ indexerUrl: this.runtime.indexerUrl,
388732
+ solver: this.runtime.fillerAddress,
388733
+ describeToken: this.runtime.describeToken,
388734
+ logger: this.runtime.loggers.get("inventory")
388735
+ }));
388736
+ return this.indexed;
388737
+ }
387983
388738
  /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
387984
388739
  books() {
387985
388740
  return this.service.books();
387986
388741
  }
388742
+ /** Public bids and asks across solvers, optionally restricted to a route. */
388743
+ orderbookSnapshot(book, filters = {}) {
388744
+ return this.service.orderbookSnapshot(book, filters);
388745
+ }
388746
+ /** Every solver's order behind one public price level. */
388747
+ orderbookLevelOrders(book, level) {
388748
+ return this.service.orderbookLevelOrders(book, level);
388749
+ }
387987
388750
  /** Creates the order, posts it, and reports what the orderbook made of it. */
387988
388751
  async create(request3) {
387989
388752
  const posted = await this.service.create(request3);
@@ -389074,6 +389837,40 @@ var SqliteBidStore = class {
389074
389837
  }
389075
389838
  };
389076
389839
 
389840
+ // src/data/sqlite/inventory.ts
389841
+ init_esm_shims();
389842
+ var SqliteInventoryStore = class {
389843
+ constructor(db) {
389844
+ this.db = db;
389845
+ this.db.exec(`
389846
+ CREATE TABLE IF NOT EXISTS inventory_snapshots (
389847
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
389848
+ taken_at TEXT NOT NULL DEFAULT (datetime('now')),
389849
+ balances TEXT NOT NULL
389850
+ );
389851
+
389852
+ CREATE INDEX IF NOT EXISTS idx_inventory_snapshots_taken_at ON inventory_snapshots(taken_at);
389853
+ `);
389854
+ }
389855
+ toSnapshot(row) {
389856
+ return { id: row.id, takenAt: row.takenAt, balances: JSON.parse(row.balances) };
389857
+ }
389858
+ async record(balances) {
389859
+ this.db.prepare("INSERT INTO inventory_snapshots (balances) VALUES (?)").run(JSON.stringify(balances));
389860
+ }
389861
+ async latest() {
389862
+ const row = this.db.prepare("SELECT id, taken_at as takenAt, balances FROM inventory_snapshots ORDER BY id DESC LIMIT 1").get();
389863
+ return row ? this.toSnapshot(row) : null;
389864
+ }
389865
+ async since(from32) {
389866
+ const cutoff = from32.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
389867
+ const rows = this.db.prepare(
389868
+ "SELECT id, taken_at as takenAt, balances FROM inventory_snapshots WHERE taken_at >= ? ORDER BY taken_at, id"
389869
+ ).all(cutoff);
389870
+ return rows.map((row) => this.toSnapshot(row));
389871
+ }
389872
+ };
389873
+
389077
389874
  // src/data/sqlite/limit-orders.ts
389078
389875
  init_esm_shims();
389079
389876
  init_Logger();
@@ -389147,12 +389944,17 @@ var SqliteLimitOrderStore = class {
389147
389944
  commitment TEXT NOT NULL,
389148
389945
  bid TEXT,
389149
389946
  amount TEXT NOT NULL,
389947
+ amount_in TEXT,
389150
389948
  transaction_hash TEXT,
389151
389949
  filled_at TEXT NOT NULL DEFAULT (datetime('now'))
389152
389950
  );
389153
389951
 
389154
389952
  CREATE INDEX IF NOT EXISTS idx_limit_order_fills_order ON limit_order_fills(limit_order_id);
389155
389953
  `);
389954
+ if (!columnNames(this.db, "limit_order_fills").has("amount_in")) {
389955
+ this.db.exec("ALTER TABLE limit_order_fills ADD COLUMN amount_in TEXT");
389956
+ this.logger.info({ column: "amount_in" }, "Migrated limit order fill schema");
389957
+ }
389156
389958
  }
389157
389959
  // biome-ignore lint/suspicious/noExplicitAny: raw sqlite row
389158
389960
  toLimitOrder(row) {
@@ -389262,12 +390064,27 @@ var SqliteLimitOrderStore = class {
389262
390064
  }
389263
390065
  async recordFill(fill) {
389264
390066
  this.db.prepare(
389265
- "INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, transaction_hash) VALUES (?, ?, ?, ?, ?)"
389266
- ).run(fill.limitOrderId, fill.commitment, fill.bid ?? null, fill.amount, fill.transactionHash ?? null);
390067
+ "INSERT INTO limit_order_fills (limit_order_id, commitment, bid, amount, amount_in, transaction_hash) VALUES (?, ?, ?, ?, ?, ?)"
390068
+ ).run(
390069
+ fill.limitOrderId,
390070
+ fill.commitment,
390071
+ fill.bid ?? null,
390072
+ fill.amount,
390073
+ fill.amountIn ?? null,
390074
+ fill.transactionHash ?? null
390075
+ );
390076
+ }
390077
+ async fillHistory() {
390078
+ return this.db.prepare(
390079
+ `SELECT f.id, f.limit_order_id as limitOrderId, o.book, o.base, o.quote, o.side, o.price,
390080
+ f.amount, f.amount_in as amountIn, f.filled_at as filledAt
390081
+ FROM limit_order_fills f JOIN limit_orders o ON o.id = f.limit_order_id
390082
+ ORDER BY f.filled_at, f.id`
390083
+ ).all();
389267
390084
  }
389268
390085
  async fills(limitOrderId, limit = 100) {
389269
390086
  return this.db.prepare(
389270
- `SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, transaction_hash as transactionHash,
390087
+ `SELECT id, limit_order_id as limitOrderId, commitment, bid, amount, amount_in as amountIn, transaction_hash as transactionHash,
389271
390088
  filled_at as filledAt
389272
390089
  FROM limit_order_fills WHERE limit_order_id = ? ORDER BY id DESC LIMIT ?`
389273
390090
  ).all(limitOrderId, Math.min(Math.max(limit, 1), 500));
@@ -389456,6 +390273,7 @@ var SqliteDataStore = class {
389456
390273
  this.activity = new SqliteActivityStore(activityDb, loggers);
389457
390274
  this.state = new SqliteStateStore(bidsDb, dataDir, loggers);
389458
390275
  this.limitOrders = new SqliteLimitOrderStore(bidsDb, loggers);
390276
+ this.inventory = new SqliteInventoryStore(activityDb);
389459
390277
  this.logger.info({ dataDir }, "SQLite data store opened");
389460
390278
  }
389461
390279
  /**