@gearbox-protocol/sdk 15.1.0-next.11 → 15.1.0-next.12

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.
Files changed (54) hide show
  1. package/dist/cjs/dev/mode-parity/compareOpportunities.js +105 -0
  2. package/dist/cjs/dev/{comparePositions.js → mode-parity/comparePositions.js} +23 -46
  3. package/dist/cjs/dev/mode-parity/compareRules.js +93 -0
  4. package/dist/cjs/dev/{fieldDiff.js → mode-parity/fieldDiff.js} +49 -2
  5. package/dist/cjs/dev/mode-parity/scriptUtils.js +131 -0
  6. package/dist/cjs/model/compare.schema.js +33 -0
  7. package/dist/cjs/model/curators.schema.js +2 -1
  8. package/dist/cjs/model/index.js +5 -0
  9. package/dist/cjs/model/opportunities.schema.js +13 -12
  10. package/dist/cjs/model/positions.schema.js +15 -14
  11. package/dist/cjs/model/primitives.schema.js +2 -1
  12. package/dist/esm/common-utils/index.js +3 -3
  13. package/dist/esm/common-utils/utils/index.js +1 -1
  14. package/dist/esm/dev/AccountOpener.js +1 -1
  15. package/dist/esm/dev/{compareOpportunities.js → mode-parity/compareOpportunities.js} +14 -46
  16. package/dist/esm/dev/{comparePositions.js → mode-parity/comparePositions.js} +16 -39
  17. package/dist/esm/dev/mode-parity/compareRules.js +91 -0
  18. package/dist/esm/dev/{fieldDiff.js → mode-parity/fieldDiff.js} +49 -3
  19. package/dist/esm/dev/mode-parity/scriptUtils.js +121 -0
  20. package/dist/esm/dev/withdrawalUtils.js +1 -1
  21. package/dist/esm/model/compare.schema.js +29 -0
  22. package/dist/esm/model/curators.schema.js +2 -1
  23. package/dist/esm/model/index.js +2 -1
  24. package/dist/esm/model/opportunities.schema.js +13 -12
  25. package/dist/esm/model/positions.schema.js +15 -14
  26. package/dist/esm/model/primitives.schema.js +2 -1
  27. package/dist/esm/plugins/adapters/contracts/ERC4626AdapterContract.js +1 -1
  28. package/dist/esm/plugins/apy/ApyPlugin.js +2 -2
  29. package/dist/esm/preview/simulate/simulatePoolOperation.js +1 -1
  30. package/dist/esm/preview/trace/extractTransfers.js +1 -1
  31. package/dist/esm/sdk/accounts/CreditAccountsServiceV310.js +2 -2
  32. package/dist/esm/sdk/accounts/liquidations/LiquidationsService.js +1 -1
  33. package/dist/esm/sdk/accounts/withdrawal-compressor/RedemptionLoggerV310Contract.js +1 -1
  34. package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV310Contract.js +1 -1
  35. package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV311Contract.js +1 -1
  36. package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +1 -1
  37. package/dist/esm/sdk/base/TokensMeta.js +3 -3
  38. package/dist/esm/sdk/chain/detectNetwork.js +1 -1
  39. package/dist/esm/sdk/core/createAddressProvider.js +1 -1
  40. package/dist/esm/sdk/market/credit/CreditFacadeV310BaseContract.js +1 -1
  41. package/dist/esm/sdk/market/pool/PoolV310Contract.js +1 -1
  42. package/dist/esm/sdk/market/zapper/IETHZapperContract.js +1 -1
  43. package/dist/esm/sdk/market/zapper/ZapperContract.js +1 -1
  44. package/dist/esm/sdk/pools/PoolService.js +1 -1
  45. package/dist/esm/sdk/utils/viem/simulateWithPriceUpdates.js +1 -1
  46. package/dist/types/dev/{compareOpportunities.d.ts → mode-parity/compareOpportunities.d.ts} +5 -5
  47. package/dist/types/dev/{comparePositions.d.ts → mode-parity/comparePositions.d.ts} +5 -5
  48. package/dist/types/dev/mode-parity/compareRules.d.ts +33 -0
  49. package/dist/types/dev/{fieldDiff.d.ts → mode-parity/fieldDiff.d.ts} +47 -5
  50. package/dist/types/dev/mode-parity/scriptUtils.d.ts +47 -0
  51. package/dist/types/model/compare.schema.d.ts +47 -0
  52. package/dist/types/model/index.d.ts +2 -1
  53. package/package.json +1 -1
  54. package/dist/cjs/dev/compareOpportunities.js +0 -137
@@ -0,0 +1,105 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_model_opportunities = require("../../model/opportunities.js");
3
+ const require_model_opportunities_schema = require("../../model/opportunities.schema.js");
4
+ require("../../model/index.js");
5
+ const require_dev_mode_parity_fieldDiff = require("./fieldDiff.js");
6
+ const require_dev_mode_parity_compareRules = require("./compareRules.js");
7
+ //#region src/dev/mode-parity/compareOpportunities.ts
8
+ const tagDiff = require_dev_mode_parity_compareRules.makeTagDiff({
9
+ pool: require_dev_mode_parity_compareRules.compileCompareRules(require_model_opportunities_schema.poolOpportunitySchema),
10
+ strategy: require_dev_mode_parity_compareRules.compileCompareRules(require_model_opportunities_schema.strategyOpportunitySchema)
11
+ });
12
+ /**
13
+ * Matches two opportunity listings by {@link opportunityId} and reports every
14
+ * field the two sources disagree on.
15
+ *
16
+ * Nothing is filtered out. A field only the backend can fill, or a USD value
17
+ * that drifted within snapshot-lag noise, is still reported — tagged
18
+ * {@link FieldDiff.expected} so that {@link CompareCounts.clean} can ignore it
19
+ * while {@link CompareCounts.identical} stays strict.
20
+ **/
21
+ function compareOpportunities(input) {
22
+ const onchainRows = indexById(input.onchain.data);
23
+ const offchainRows = indexById(input.offchain.data);
24
+ const onlyOnchain = [];
25
+ const onlyOffchain = [];
26
+ const matched = [];
27
+ for (const [id, row] of onchainRows) {
28
+ const counterpart = offchainRows.get(id);
29
+ if (!counterpart) {
30
+ onlyOnchain.push(toRef(row));
31
+ continue;
32
+ }
33
+ const diffs = diffOpportunity(row, counterpart);
34
+ matched.push({
35
+ id,
36
+ kind: row.kind,
37
+ chainId: row.chainId,
38
+ onchainName: row.name,
39
+ offchainName: counterpart.name,
40
+ identical: diffs.length === 0,
41
+ clean: diffs.every((diff) => diff.expected),
42
+ diffs
43
+ });
44
+ }
45
+ for (const [id, row] of offchainRows) if (!onchainRows.has(id)) onlyOffchain.push(toRef(row));
46
+ byId(onlyOnchain);
47
+ byId(onlyOffchain);
48
+ matched.sort((a, b) => a.id.localeCompare(b.id));
49
+ return {
50
+ generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
51
+ backendUrl: input.backendUrl,
52
+ networks: [...input.networks],
53
+ onchainChains: input.onchain.meta.chains,
54
+ offchainChains: input.offchain.meta.chains,
55
+ summary: summarize(input.onchain.data, input.offchain.data, onlyOnchain, onlyOffchain, matched),
56
+ onlyOnchain,
57
+ onlyOffchain,
58
+ matched
59
+ };
60
+ }
61
+ function indexById(rows) {
62
+ return new Map(rows.map((row) => [require_model_opportunities.opportunityId(row), row]));
63
+ }
64
+ function byId(refs) {
65
+ refs.sort((a, b) => a.id.localeCompare(b.id));
66
+ }
67
+ function toRef(row) {
68
+ const base = {
69
+ id: require_model_opportunities.opportunityId(row),
70
+ kind: row.kind,
71
+ chainId: row.chainId,
72
+ name: row.name
73
+ };
74
+ return row.kind === "pool" ? {
75
+ ...base,
76
+ pool: row.pool
77
+ } : {
78
+ ...base,
79
+ creditManager: row.creditManager,
80
+ targetCollateral: row.targetCollateral.address
81
+ };
82
+ }
83
+ /**
84
+ * Every field two versions of one opportunity disagree on.
85
+ **/
86
+ function diffOpportunity(onchain, offchain) {
87
+ return require_dev_mode_parity_fieldDiff.diffObjects(onchain, offchain).map((diff) => tagDiff(diff, onchain.kind));
88
+ }
89
+ function summarize(onchain, offchain, onlyOnchain, onlyOffchain, matched) {
90
+ const byChain = require_dev_mode_parity_fieldDiff.union(onchain.map((row) => String(row.chainId)), offchain.map((row) => String(row.chainId))).map((chainId) => ({
91
+ chainId: Number(chainId),
92
+ ...require_dev_mode_parity_fieldDiff.toCompareCounts(onchain.filter((row) => String(row.chainId) === chainId).length, offchain.filter((row) => String(row.chainId) === chainId).length, onlyOnchain.filter((ref) => String(ref.chainId) === chainId).length, onlyOffchain.filter((ref) => String(ref.chainId) === chainId).length, matched.filter((match) => String(match.chainId) === chainId))
93
+ })).sort((a, b) => a.chainId - b.chainId);
94
+ return {
95
+ ...require_dev_mode_parity_fieldDiff.toCompareCounts(onchain.length, offchain.length, onlyOnchain.length, onlyOffchain.length, matched),
96
+ byChain,
97
+ diffsByPath: require_dev_mode_parity_fieldDiff.countPaths(matched.flatMap((match) => match.diffs.map((diff) => ({
98
+ id: match.id,
99
+ diff
100
+ }))))
101
+ };
102
+ }
103
+ //#endregion
104
+ exports.compareOpportunities = compareOpportunities;
105
+ exports.diffOpportunity = diffOpportunity;
@@ -1,8 +1,16 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_model_positions = require("../model/positions.js");
3
- require("../model/index.js");
4
- const require_dev_fieldDiff = require("./fieldDiff.js");
5
- //#region src/dev/comparePositions.ts
2
+ const require_model_liquidations_schema = require("../../model/liquidations.schema.js");
3
+ const require_model_positions = require("../../model/positions.js");
4
+ const require_model_positions_schema = require("../../model/positions.schema.js");
5
+ require("../../model/index.js");
6
+ const require_dev_mode_parity_fieldDiff = require("./fieldDiff.js");
7
+ const require_dev_mode_parity_compareRules = require("./compareRules.js");
8
+ //#region src/dev/mode-parity/comparePositions.ts
9
+ const tagDiff = require_dev_mode_parity_compareRules.makeTagDiff({
10
+ pool: require_dev_mode_parity_compareRules.compileCompareRules(require_model_positions_schema.poolPositionSchema),
11
+ strategy: require_dev_mode_parity_compareRules.compileCompareRules(require_model_positions_schema.strategyPositionSchema),
12
+ liquidation: require_dev_mode_parity_compareRules.compileCompareRules(require_model_liquidations_schema.liquidationPositionSchema)
13
+ });
6
14
  /**
7
15
  * Matches two position listings per wallet by {@link positionId} and reports
8
16
  * every field the two sources disagree on.
@@ -36,7 +44,7 @@ function comparePositions(input) {
36
44
  * Every field two versions of one position disagree on.
37
45
  **/
38
46
  function diffPosition(onchain, offchain) {
39
- return require_dev_fieldDiff.diffObjects(onchain, offchain, { keyOf: keyOfPositionArray }).map((diff) => tagDiff(diff, onchain.kind));
47
+ return require_dev_mode_parity_fieldDiff.diffObjects(onchain, offchain, { keyOf: keyOfPositionArray }).map((diff) => tagDiff(diff, onchain.kind));
40
48
  }
41
49
  function compareWallet(input) {
42
50
  const onchainRows = indexById(input.onchain.data);
@@ -101,43 +109,9 @@ function toRef(row) {
101
109
  * address. Rewards-points programs already identify themselves by `id`.
102
110
  **/
103
111
  function keyOfPositionArray(path, value) {
104
- if (path !== "collaterals" || !require_dev_fieldDiff.isRecord(value) || !require_dev_fieldDiff.isRecord(value.collateral)) return;
112
+ if (path !== "collaterals" || !require_dev_mode_parity_fieldDiff.isRecord(value) || !require_dev_mode_parity_fieldDiff.isRecord(value.collateral)) return;
105
113
  const token = value.collateral.token;
106
- return require_dev_fieldDiff.isRecord(token) && typeof token.address === "string" ? token.address : void 0;
107
- }
108
- const BPS_RATE_PATHS = /* @__PURE__ */ new Set([
109
- "borrowApy",
110
- "apy.organicApy",
111
- "healthFactor"
112
- ]);
113
- /**
114
- * Amount fields whose bigint `value` moves with interest accrual between the
115
- * backend's last sync and the current block.
116
- **/
117
- function isLagAmountPath(path) {
118
- return path === "totalDebt.value" || path === "totalValue.value" || path === "netValue.value" || path.endsWith(".collateral.value") || path.endsWith(".quota.value");
119
- }
120
- /**
121
- * Fields documented `@mode offchain` or `@mode onchain` in the model.
122
- **/
123
- function isModeScoped(path, kind) {
124
- if (path === "pnl" || path.startsWith("pnl.")) return true;
125
- if (kind === "pool") return path === "apy.totalApy" || path.startsWith("apy.rewards");
126
- if (kind === "strategy") return path === "netApy" || path.startsWith("netApy.") || path === "borrowRate" || path.startsWith("borrowRate.") || path === "timeToLiquidation" || path === "liquidationPrice";
127
- return false;
128
- }
129
- function tagDiff(diff, kind) {
130
- if (isModeScoped(diff.path, kind)) return require_dev_fieldDiff.withExpected(diff, "mode-scoped");
131
- if (withinTolerance(diff)) return require_dev_fieldDiff.withExpected(diff, "tolerance");
132
- return diff;
133
- }
134
- function withinTolerance(diff) {
135
- if (diff.kind === "usd") return require_dev_fieldDiff.isUsdWithinTolerance(diff.onchain, diff.offchain);
136
- if (diff.kind !== "numeric") return false;
137
- if (diff.path === "leverage") return require_dev_fieldDiff.withinRelative(require_dev_fieldDiff.asFiniteNumber(diff.onchain), require_dev_fieldDiff.asFiniteNumber(diff.offchain), require_dev_fieldDiff.USD_RELATIVE_EPSILON);
138
- if (BPS_RATE_PATHS.has(diff.path)) return require_dev_fieldDiff.isBpsWithinTolerance(diff.onchain, diff.offchain);
139
- if (isLagAmountPath(diff.path)) return require_dev_fieldDiff.isAmountWithinTolerance(diff.onchain, diff.offchain);
140
- return false;
114
+ return require_dev_mode_parity_fieldDiff.isRecord(token) && typeof token.address === "string" ? token.address : void 0;
141
115
  }
142
116
  function summarize(inputs, wallets, failed) {
143
117
  const compared = wallets.filter((wallet) => !wallet.error);
@@ -146,24 +120,27 @@ function summarize(inputs, wallets, failed) {
146
120
  const onlyOnchain = compared.flatMap((wallet) => wallet.onlyOnchain);
147
121
  const onlyOffchain = compared.flatMap((wallet) => wallet.onlyOffchain);
148
122
  const matched = compared.flatMap((wallet) => wallet.matched);
149
- const byChain = require_dev_fieldDiff.union(allOnchain.map((row) => String(row.chainId)), allOffchain.map((row) => String(row.chainId))).map((chainId) => ({
123
+ const byChain = require_dev_mode_parity_fieldDiff.union(allOnchain.map((row) => String(row.chainId)), allOffchain.map((row) => String(row.chainId))).map((chainId) => ({
150
124
  chainId: Number(chainId),
151
- ...require_dev_fieldDiff.toCompareCounts(allOnchain.filter((row) => String(row.chainId) === chainId).length, allOffchain.filter((row) => String(row.chainId) === chainId).length, onlyOnchain.filter((ref) => String(ref.chainId) === chainId).length, onlyOffchain.filter((ref) => String(ref.chainId) === chainId).length, matched.filter((match) => String(match.chainId) === chainId))
125
+ ...require_dev_mode_parity_fieldDiff.toCompareCounts(allOnchain.filter((row) => String(row.chainId) === chainId).length, allOffchain.filter((row) => String(row.chainId) === chainId).length, onlyOnchain.filter((ref) => String(ref.chainId) === chainId).length, onlyOffchain.filter((ref) => String(ref.chainId) === chainId).length, matched.filter((match) => String(match.chainId) === chainId))
152
126
  })).sort((a, b) => a.chainId - b.chainId);
153
127
  const byWallet = wallets.map((wallet) => ({
154
128
  wallet: wallet.wallet,
155
129
  ...wallet.error ? { error: wallet.error } : {},
156
- ...require_dev_fieldDiff.toCompareCounts(wallet.matched.length + wallet.onlyOnchain.length, wallet.matched.length + wallet.onlyOffchain.length, wallet.onlyOnchain.length, wallet.onlyOffchain.length, wallet.matched)
130
+ ...require_dev_mode_parity_fieldDiff.toCompareCounts(wallet.matched.length + wallet.onlyOnchain.length, wallet.matched.length + wallet.onlyOffchain.length, wallet.onlyOnchain.length, wallet.onlyOffchain.length, wallet.matched)
157
131
  }));
158
132
  const walletsClean = compared.filter((wallet) => wallet.onlyOnchain.length === 0 && wallet.onlyOffchain.length === 0 && wallet.matched.every((match) => match.clean)).length;
159
133
  return {
160
- ...require_dev_fieldDiff.toCompareCounts(allOnchain.length, allOffchain.length, onlyOnchain.length, onlyOffchain.length, matched),
134
+ ...require_dev_mode_parity_fieldDiff.toCompareCounts(allOnchain.length, allOffchain.length, onlyOnchain.length, onlyOffchain.length, matched),
161
135
  wallets: wallets.length,
162
136
  walletsClean,
163
137
  walletsFailed: failed,
164
138
  byChain,
165
139
  byWallet,
166
- diffsByPath: require_dev_fieldDiff.countPaths(matched.flatMap((match) => match.diffs))
140
+ diffsByPath: require_dev_mode_parity_fieldDiff.countPaths(matched.flatMap((match) => match.diffs.map((diff) => ({
141
+ id: match.id,
142
+ diff
143
+ }))))
167
144
  };
168
145
  }
169
146
  /**
@@ -0,0 +1,93 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_model_compare_schema = require("../../model/compare.schema.js");
3
+ const require_dev_mode_parity_fieldDiff = require("./fieldDiff.js");
4
+ //#region src/dev/mode-parity/compareRules.ts
5
+ /**
6
+ * Walks a schema and records every field that carries compare metadata.
7
+ *
8
+ * `"amount"` on an object (an Amount / TokenAmount) is stored
9
+ * at `<path>.value`; every other tag is stored at the field's own path.
10
+ **/
11
+ function compileCompareRules(schema) {
12
+ const rules = /* @__PURE__ */ new Map();
13
+ walk(schema, "", /* @__PURE__ */ new Set(), rules);
14
+ return rules;
15
+ }
16
+ /**
17
+ * Tags diffs using the rules compiled for each row kind.
18
+ *
19
+ * Mode tags match the path or anything nested under it. Tolerance tags match
20
+ * the path exactly and dispatch on {@link CompareTolerance}.
21
+ **/
22
+ function makeTagDiff(rulesByKind) {
23
+ return (diff, kind) => {
24
+ const rules = rulesByKind[kind];
25
+ if (!rules) return diff;
26
+ const path = require_dev_mode_parity_fieldDiff.collapseArrayKeys(diff.path);
27
+ if (isModeScoped(path, rules)) return require_dev_mode_parity_fieldDiff.withExpected(diff, "mode-scoped");
28
+ const tag = rules.get(path);
29
+ if (tag && typeof tag === "object" && withinTolerance(tag.tolerance, diff)) return require_dev_mode_parity_fieldDiff.withExpected(diff, "tolerance");
30
+ return diff;
31
+ };
32
+ }
33
+ function isModeScoped(path, rules) {
34
+ for (const [rulePath, tag] of rules) {
35
+ if (tag !== "offchainOnly" && tag !== "onchainOnly") continue;
36
+ if (path === rulePath || path.startsWith(`${rulePath}.`) || path.startsWith(`${rulePath}[`)) return true;
37
+ }
38
+ return false;
39
+ }
40
+ function withinTolerance(kind, diff) {
41
+ switch (kind) {
42
+ case "usd": return require_dev_mode_parity_fieldDiff.isUsdWithinTolerance(diff.onchain, diff.offchain);
43
+ case "bps": return require_dev_mode_parity_fieldDiff.isBpsWithinTolerance(diff.onchain, diff.offchain);
44
+ case "amount": return require_dev_mode_parity_fieldDiff.isAmountWithinTolerance(diff.onchain, diff.offchain);
45
+ case "float": return require_dev_mode_parity_fieldDiff.withinRelative(require_dev_mode_parity_fieldDiff.asFiniteNumber(diff.onchain), require_dev_mode_parity_fieldDiff.asFiniteNumber(diff.offchain), require_dev_mode_parity_fieldDiff.USD_RELATIVE_EPSILON);
46
+ }
47
+ }
48
+ function walk(schema, path, seen, rules) {
49
+ if (seen.has(schema)) return;
50
+ const nextSeen = new Set(seen);
51
+ nextSeen.add(schema);
52
+ const tag = require_model_compare_schema.compareTagOf(schema);
53
+ if (tag) record(path, tag, schema, rules);
54
+ const def = schema.def;
55
+ switch (def.type) {
56
+ case "optional":
57
+ case "nullable":
58
+ case "default":
59
+ case "prefault":
60
+ case "readonly":
61
+ if (def.innerType) walk(def.innerType, path, nextSeen, rules);
62
+ return;
63
+ case "array":
64
+ if (def.element) walk(def.element, `${path}[]`, nextSeen, rules);
65
+ return;
66
+ case "object":
67
+ if (def.shape) for (const [key, field] of Object.entries(def.shape)) walk(field, join(path, key), nextSeen, rules);
68
+ return;
69
+ case "union":
70
+ for (const option of def.options ?? []) walk(option, path, nextSeen, rules);
71
+ return;
72
+ case "pipe":
73
+ if (def.out) walk(def.out, path, nextSeen, rules);
74
+ return;
75
+ case "lazy":
76
+ if (def.getter) walk(def.getter(), path, nextSeen, rules);
77
+ return;
78
+ default: return;
79
+ }
80
+ }
81
+ function record(path, tag, schema, rules) {
82
+ if (typeof tag === "object" && tag.tolerance === "amount" && schema.def.type === "object") {
83
+ rules.set(join(path, "value"), tag);
84
+ return;
85
+ }
86
+ rules.set(path, tag);
87
+ }
88
+ function join(path, key) {
89
+ return path ? `${path}.${key}` : key;
90
+ }
91
+ //#endregion
92
+ exports.compileCompareRules = compileCompareRules;
93
+ exports.makeTagDiff = makeTagDiff;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/dev/fieldDiff.ts
2
+ //#region src/dev/mode-parity/fieldDiff.ts
3
3
  /**
4
4
  * Relative drift allowed on {@link Amount.valueUsd} before a USD float is a
5
5
  * real disagreement: 0.1%.
@@ -70,7 +70,7 @@ function withExpected(diff, reason) {
70
70
  **/
71
71
  function countPaths(diffs) {
72
72
  const counts = /* @__PURE__ */ new Map();
73
- for (const diff of diffs) {
73
+ for (const { id, diff } of diffs) {
74
74
  const path = collapseArrayKeys(diff.path);
75
75
  const entry = counts.get(path) ?? {
76
76
  path,
@@ -83,10 +83,31 @@ function countPaths(diffs) {
83
83
  if (diff.expected) entry.expected += 1;
84
84
  else entry.unexpected += 1;
85
85
  if (!entry.kinds.includes(diff.kind)) entry.kinds.push(diff.kind);
86
+ recordWorst(entry, id, diff);
86
87
  counts.set(path, entry);
87
88
  }
88
89
  return [...counts.values()].sort((a, b) => b.unexpected - a.unexpected || b.count - a.count || a.path.localeCompare(b.path));
89
90
  }
91
+ function recordWorst(entry, id, diff) {
92
+ const bps = relativeDiffBps(diff.onchain, diff.offchain);
93
+ if (bps === void 0) return;
94
+ const worst = {
95
+ id,
96
+ path: diff.path,
97
+ bps,
98
+ onchain: diff.onchain,
99
+ offchain: diff.offchain
100
+ };
101
+ if (diff.expected) {
102
+ if (isWorse(worst, entry.worstExpected)) entry.worstExpected = worst;
103
+ return;
104
+ }
105
+ if (isWorse(worst, entry.worstUnexpected)) entry.worstUnexpected = worst;
106
+ }
107
+ function isWorse(candidate, current) {
108
+ if (!current) return true;
109
+ return candidate.bps > current.bps || candidate.bps === current.bps && candidate.id.localeCompare(current.id) < 0;
110
+ }
90
111
  /**
91
112
  * Collapse `collateralTokens[0xa0b8...].symbol` to `collateralTokens[].symbol`.
92
113
  **/
@@ -152,6 +173,31 @@ function withinRelativeBigint(onchain, offchain, epsilon) {
152
173
  function asFiniteNumber(value) {
153
174
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
154
175
  }
176
+ /**
177
+ * Relative difference of two numbers or bigints, in bps:
178
+ * `|a − b| / max(|a|, |b|) * 10_000`. `undefined` when the values are not
179
+ * a comparable pair of finite numbers or of bigints.
180
+ **/
181
+ function relativeDiffBps(onchain, offchain) {
182
+ if (typeof onchain === "bigint" && typeof offchain === "bigint") return relativeDiffBpsBigint(onchain, offchain);
183
+ const left = asFiniteNumber(onchain);
184
+ const right = asFiniteNumber(offchain);
185
+ if (left === void 0 || right === void 0) return;
186
+ const scale = Math.max(Math.abs(left), Math.abs(right));
187
+ return scale === 0 ? 0 : Math.abs(left - right) / scale * 1e4;
188
+ }
189
+ /**
190
+ * Same formula as {@link relativeDiffBps} for bigints, computed in integer
191
+ * arithmetic to milli-bps so a 1e18-scale amount does not round through
192
+ * `Number`.
193
+ **/
194
+ function relativeDiffBpsBigint(onchain, offchain) {
195
+ if (onchain === offchain) return 0;
196
+ const diff = onchain > offchain ? onchain - offchain : offchain - onchain;
197
+ const scale = abs(onchain) > abs(offchain) ? abs(onchain) : abs(offchain);
198
+ if (scale === 0n) return 0;
199
+ return Number(diff * 10000n * 1000n / scale) / 1e3;
200
+ }
155
201
  function union(left, right) {
156
202
  return [.../* @__PURE__ */ new Set([...left, ...right])];
157
203
  }
@@ -234,6 +280,7 @@ exports.isAmountWithinTolerance = isAmountWithinTolerance;
234
280
  exports.isBpsWithinTolerance = isBpsWithinTolerance;
235
281
  exports.isRecord = isRecord;
236
282
  exports.isUsdWithinTolerance = isUsdWithinTolerance;
283
+ exports.relativeDiffBps = relativeDiffBps;
237
284
  exports.toCompareCounts = toCompareCounts;
238
285
  exports.union = union;
239
286
  exports.withExpected = withExpected;
@@ -0,0 +1,131 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_dev_providers = require("../providers.js");
3
+ let pino = require("pino");
4
+ //#region src/dev/mode-parity/scriptUtils.ts
5
+ const NETWORKS = [
6
+ "Mainnet",
7
+ "Monad",
8
+ "Plasma",
9
+ "Somnia",
10
+ "Etherlink"
11
+ ];
12
+ const BACKEND_URL = process.env.BACKEND_URL ?? "https://api.gear-dev.dev";
13
+ const TIMEOUT = 48e4;
14
+ function requireEnv(name) {
15
+ const value = process.env[name];
16
+ if (!value) throw new Error(`${name} is required to reach the chains this script reads`);
17
+ return value;
18
+ }
19
+ function rpcUrls() {
20
+ const alchemyKey = requireEnv("ALCHEMY_KEY");
21
+ const alchemy = (network) => {
22
+ const url = require_dev_providers.getAlchemyUrl(network, alchemyKey);
23
+ if (!url) throw new Error(`Alchemy serves no URL for ${network}`);
24
+ return url;
25
+ };
26
+ return {
27
+ Mainnet: alchemy("Mainnet"),
28
+ Monad: alchemy("Monad"),
29
+ Plasma: alchemy("Plasma"),
30
+ Somnia: requireEnv("SOMNIA_PROVIDER"),
31
+ Etherlink: requireEnv("ETHERLINK_PROVIDER")
32
+ };
33
+ }
34
+ function createLogger() {
35
+ return (0, pino.pino)({
36
+ level: process.env.LOG_LEVEL ?? "info",
37
+ formatters: {
38
+ bindings: () => ({}),
39
+ level: (label) => ({ level: label })
40
+ }
41
+ });
42
+ }
43
+ function errorMessage(error) {
44
+ return error instanceof Error ? error.message : String(error);
45
+ }
46
+ /**
47
+ * Runs `fn` over `items` with at most `concurrency` in flight.
48
+ **/
49
+ async function mapPool(items, concurrency, fn) {
50
+ let next = 0;
51
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
52
+ while (true) {
53
+ const index = next;
54
+ next += 1;
55
+ if (index >= items.length) return;
56
+ await fn(items[index]);
57
+ }
58
+ });
59
+ await Promise.all(workers);
60
+ }
61
+ /**
62
+ * Formats a relative difference in bps as a percent, e.g. 12.5 → `"0.125%"`.
63
+ **/
64
+ function formatBpsAsPercent(bps) {
65
+ if (!Number.isFinite(bps)) return "";
66
+ const percent = bps / 100;
67
+ if (percent === 0) return "0%";
68
+ return `${Math.abs(percent) >= 1 ? trimTrailingZeros(percent.toFixed(3)) : trimTrailingZeros(percent.toPrecision(4))}%`;
69
+ }
70
+ function trimTrailingZeros(value) {
71
+ return value.includes(".") ? value.replace(/\.?0+$/, "") : value;
72
+ }
73
+ function worstColumns(worst) {
74
+ return {
75
+ "max diff": worst ? formatBpsAsPercent(worst.bps) : "",
76
+ "worst entity": worst?.id ?? ""
77
+ };
78
+ }
79
+ /**
80
+ * Prints the shared membership table, the fields that differed most often,
81
+ * and any chain that failed to answer.
82
+ **/
83
+ function printCompareSummary(noun, report, extraLines = []) {
84
+ const { summary } = report;
85
+ console.log(`\n${noun} from ${report.backendUrl} vs the chain`);
86
+ console.table(summary.byChain.map((chain) => ({
87
+ chain: chain.chainId,
88
+ onchain: chain.onchainRows,
89
+ offchain: chain.offchainRows,
90
+ matched: chain.matched,
91
+ identical: chain.identical,
92
+ clean: chain.clean,
93
+ differing: chain.differing,
94
+ "only onchain": chain.onlyOnchain,
95
+ "only offchain": chain.onlyOffchain
96
+ })));
97
+ for (const line of extraLines) console.log(line);
98
+ const unexpected = summary.diffsByPath.filter((entry) => entry.unexpected > 0);
99
+ const expected = summary.diffsByPath.filter((entry) => entry.unexpected === 0 && entry.expected > 0);
100
+ if (unexpected.length) {
101
+ console.log("\nunexpected fields that differed most often:");
102
+ console.table(unexpected.slice(0, 25).map((entry) => ({
103
+ field: entry.path,
104
+ unexpected: entry.unexpected,
105
+ expected: entry.expected,
106
+ kinds: entry.kinds.join(", "),
107
+ ...worstColumns(entry.worstUnexpected)
108
+ })));
109
+ }
110
+ if (expected.length) {
111
+ console.log("\nexpected fields (mode-scoped or within tolerance):");
112
+ console.table(expected.slice(0, 25).map((entry) => ({
113
+ field: entry.path,
114
+ rows: entry.expected,
115
+ kinds: entry.kinds.join(", "),
116
+ ...worstColumns(entry.worstExpected)
117
+ })));
118
+ }
119
+ for (const chain of [...report.onchainChains, ...report.offchainChains]) if (chain.status === "error") console.log(`chain ${chain.chainId} failed on ${chain.source}:`, chain.error);
120
+ }
121
+ //#endregion
122
+ exports.BACKEND_URL = BACKEND_URL;
123
+ exports.NETWORKS = NETWORKS;
124
+ exports.TIMEOUT = TIMEOUT;
125
+ exports.createLogger = createLogger;
126
+ exports.errorMessage = errorMessage;
127
+ exports.formatBpsAsPercent = formatBpsAsPercent;
128
+ exports.mapPool = mapPool;
129
+ exports.printCompareSummary = printCompareSummary;
130
+ exports.requireEnv = requireEnv;
131
+ exports.rpcUrls = rpcUrls;
@@ -0,0 +1,33 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/model/compare.schema.ts
3
+ /**
4
+ * Marks a field that only the backend fills.
5
+ **/
6
+ function offchainOnly(schema) {
7
+ return schema.meta({ compare: "offchainOnly" });
8
+ }
9
+ /**
10
+ * Marks a field that only the chain fills.
11
+ **/
12
+ function onchainOnly(schema) {
13
+ return schema.meta({ compare: "onchainOnly" });
14
+ }
15
+ /**
16
+ * Marks a numeric field whose two sources may drift within {@link kind}.
17
+ **/
18
+ function tolerance(schema, kind) {
19
+ return schema.meta({ compare: { tolerance: kind } });
20
+ }
21
+ /**
22
+ * The compare tag registered on a schema, if any.
23
+ **/
24
+ function compareTagOf(schema) {
25
+ const meta = schema.meta();
26
+ if (!meta || !("compare" in meta)) return;
27
+ return meta.compare;
28
+ }
29
+ //#endregion
30
+ exports.compareTagOf = compareTagOf;
31
+ exports.offchainOnly = offchainOnly;
32
+ exports.onchainOnly = onchainOnly;
33
+ exports.tolerance = tolerance;
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_sdk_utils_zod = require("../sdk/utils/zod.js");
3
+ const require_model_compare_schema = require("./compare.schema.js");
3
4
  let zod_v4 = require("zod/v4");
4
5
  //#region src/model/curators.schema.ts
5
6
  /**
@@ -31,7 +32,7 @@ const curatorNameSchema = zod_v4.z.enum([
31
32
  const curatorSchema = zod_v4.z.object({
32
33
  address: require_sdk_utils_zod.ZodAddress(),
33
34
  name: curatorNameSchema.optional(),
34
- url: zod_v4.z.string().nullable()
35
+ url: require_model_compare_schema.offchainOnly(zod_v4.z.string().nullable())
35
36
  });
36
37
  //#endregion
37
38
  exports.curatorNameSchema = curatorNameSchema;
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_model_charts = require("./charts.js");
3
+ const require_model_compare_schema = require("./compare.schema.js");
3
4
  const require_model_primitives_schema = require("./primitives.schema.js");
4
5
  const require_model_charts_schema = require("./charts.schema.js");
5
6
  require("./curators.js");
@@ -45,6 +46,7 @@ exports.chartRangeSchema = require_model_charts_schema.chartRangeSchema;
45
46
  exports.chartSeriesSchema = require_model_charts_schema.chartSeriesSchema;
46
47
  exports.chartValueSchema = require_model_charts_schema.chartValueSchema;
47
48
  exports.chartWindowSchema = require_model_charts_schema.chartWindowSchema;
49
+ exports.compareTagOf = require_model_compare_schema.compareTagOf;
48
50
  exports.curatorNameSchema = require_model_curators_schema.curatorNameSchema;
49
51
  exports.curatorSchema = require_model_curators_schema.curatorSchema;
50
52
  exports.dataSourceSchema = require_model_response_schema.dataSourceSchema;
@@ -66,6 +68,8 @@ exports.matchesOpportunityFilter = require_model_opportunities.matchesOpportunit
66
68
  exports.matchesPositionFilter = require_model_positions.matchesPositionFilter;
67
69
  exports.noticeKindSchema = require_model_notices_schema.noticeKindSchema;
68
70
  exports.noticeSchema = require_model_notices_schema.noticeSchema;
71
+ exports.offchainOnly = require_model_compare_schema.offchainOnly;
72
+ exports.onchainOnly = require_model_compare_schema.onchainOnly;
69
73
  exports.opportunityBaseSchema = require_model_opportunities_schema.opportunityBaseSchema;
70
74
  exports.opportunityDetailSchema = require_model_opportunities_schema.opportunityDetailSchema;
71
75
  exports.opportunityFilterQueryParamsSchema = require_model_opportunities_schema.opportunityFilterQueryParamsSchema;
@@ -124,4 +128,5 @@ exports.tokenAmountSchema = require_model_primitives_schema.tokenAmountSchema;
124
128
  exports.tokenRewardsPnLSchema = require_model_positions_schema.tokenRewardsPnLSchema;
125
129
  exports.tokenRewardsSchema = require_model_opportunities_schema.tokenRewardsSchema;
126
130
  exports.tokenSchema = require_model_primitives_schema.tokenSchema;
131
+ exports.tolerance = require_model_compare_schema.tolerance;
127
132
  exports.txCallSchema = require_model_primitives_schema.txCallSchema;
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_sdk_utils_zod = require("../sdk/utils/zod.js");
3
+ const require_model_compare_schema = require("./compare.schema.js");
3
4
  const require_model_primitives_schema = require("./primitives.schema.js");
4
5
  const require_model_curators_schema = require("./curators.schema.js");
5
6
  const require_model_filters = require("./filters.js");
@@ -46,9 +47,9 @@ const rewardsSchema = zod_v4.z.discriminatedUnion("kind", [tokenRewardsSchema, p
46
47
  * {@link ApyBreakdown}
47
48
  **/
48
49
  const apyBreakdownSchema = zod_v4.z.object({
49
- totalApy: require_model_primitives_schema.bpsSchema.optional(),
50
- organicApy: require_model_primitives_schema.bpsSchema,
51
- rewards: zod_v4.z.array(rewardsSchema).optional()
50
+ totalApy: require_model_compare_schema.offchainOnly(require_model_primitives_schema.bpsSchema).optional(),
51
+ organicApy: require_model_compare_schema.tolerance(require_model_primitives_schema.bpsSchema, "bps"),
52
+ rewards: require_model_compare_schema.offchainOnly(zod_v4.z.array(rewardsSchema)).optional()
52
53
  });
53
54
  /**
54
55
  * {@link OpportunityBase}
@@ -71,9 +72,9 @@ const poolOpportunitySchema = zod_v4.z.object({
71
72
  ...opportunityBaseSchema.shape,
72
73
  kind: zod_v4.z.literal("pool"),
73
74
  pool: require_sdk_utils_zod.ZodAddress(),
74
- totalSupply: require_model_primitives_schema.amountSchema,
75
- availableLiquidity: require_model_primitives_schema.amountSchema,
76
- utilization: require_model_primitives_schema.bpsSchema,
75
+ totalSupply: require_model_compare_schema.tolerance(require_model_primitives_schema.amountSchema, "amount"),
76
+ availableLiquidity: require_model_compare_schema.tolerance(require_model_primitives_schema.amountSchema, "amount"),
77
+ utilization: require_model_compare_schema.tolerance(require_model_primitives_schema.bpsSchema, "bps"),
77
78
  supplyApy: apyBreakdownSchema
78
79
  });
79
80
  /**
@@ -88,12 +89,12 @@ const strategyOpportunitySchema = zod_v4.z.object({
88
89
  liquidationPremium: require_model_primitives_schema.bpsSchema,
89
90
  liquidationFee: require_model_primitives_schema.bpsSchema,
90
91
  expirationDate: require_model_primitives_schema.timestampSchema.nullable(),
91
- collateralApy: apyBreakdownSchema.optional(),
92
- maxLeverageApy: apyBreakdownSchema.optional(),
93
- borrowApy: require_model_primitives_schema.bpsSchema.optional(),
94
- additionalBorrowApy: require_model_primitives_schema.bpsSchema.optional(),
95
- totalValue: require_model_primitives_schema.amountSchema.optional(),
96
- utilization: require_model_primitives_schema.bpsSchema.optional(),
92
+ collateralApy: require_model_compare_schema.offchainOnly(apyBreakdownSchema).optional(),
93
+ maxLeverageApy: require_model_compare_schema.offchainOnly(apyBreakdownSchema).optional(),
94
+ borrowApy: require_model_compare_schema.tolerance(require_model_primitives_schema.bpsSchema, "bps").optional(),
95
+ additionalBorrowApy: require_model_compare_schema.tolerance(require_model_primitives_schema.bpsSchema, "bps").optional(),
96
+ totalValue: require_model_compare_schema.offchainOnly(require_model_primitives_schema.amountSchema).optional(),
97
+ utilization: require_model_compare_schema.offchainOnly(require_model_primitives_schema.bpsSchema).optional(),
97
98
  maxBorrowAmount: require_model_primitives_schema.amountSchema,
98
99
  maxLeverage: require_model_primitives_schema.leverageSchema
99
100
  });