@gearbox-protocol/sdk 16.0.0-next.54 → 16.0.0-next.55

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 (51) hide show
  1. package/dist/cjs/dev/mode-parity/compareOpportunities.js +3 -4
  2. package/dist/cjs/dev/mode-parity/comparePositions.js +5 -6
  3. package/dist/cjs/dev/mode-parity/fieldDiff.js +4 -6
  4. package/dist/cjs/dev/mode-parity/scriptUtils.js +145 -5
  5. package/dist/cjs/rewards/errors.js +27 -0
  6. package/dist/cjs/rewards/index.js +6 -7
  7. package/dist/cjs/rewards/merkl-api.js +45 -0
  8. package/dist/cjs/rewards/multichain.js +40 -0
  9. package/dist/cjs/rewards/{rewards/api.js → toMerklRewards.js} +26 -28
  10. package/dist/esm/dev/mode-parity/compareOpportunities.js +3 -4
  11. package/dist/esm/dev/mode-parity/comparePositions.js +5 -6
  12. package/dist/esm/dev/mode-parity/fieldDiff.js +4 -6
  13. package/dist/esm/dev/mode-parity/scriptUtils.js +142 -6
  14. package/dist/esm/rewards/errors.js +26 -0
  15. package/dist/esm/rewards/index.js +4 -5
  16. package/dist/esm/rewards/merkl-api.js +42 -0
  17. package/dist/esm/rewards/multichain.js +39 -0
  18. package/dist/esm/rewards/{rewards/api.js → toMerklRewards.js} +26 -28
  19. package/dist/types/dev/mode-parity/compareOpportunities.d.ts +3 -7
  20. package/dist/types/dev/mode-parity/comparePositions.d.ts +4 -8
  21. package/dist/types/dev/mode-parity/fieldDiff.d.ts +9 -8
  22. package/dist/types/dev/mode-parity/scriptUtils.d.ts +26 -1
  23. package/dist/types/rewards/errors.d.ts +18 -0
  24. package/dist/types/rewards/index.d.ts +4 -7
  25. package/dist/types/rewards/merkl-api.d.ts +64 -0
  26. package/dist/types/rewards/multichain.d.ts +33 -0
  27. package/dist/types/rewards/toMerklRewards.d.ts +42 -0
  28. package/package.json +1 -3
  29. package/dist/cjs/_virtual/_rolldown/runtime.js +0 -23
  30. package/dist/cjs/rewards/apy/index.js +0 -3
  31. package/dist/cjs/rewards/apy/output-details.js +0 -1
  32. package/dist/cjs/rewards/apy/output.js +0 -1
  33. package/dist/cjs/rewards/rewards/common.js +0 -1
  34. package/dist/cjs/rewards/rewards/extra-apy.js +0 -71
  35. package/dist/cjs/rewards/rewards/index.js +0 -6
  36. package/dist/cjs/rewards/rewards/merkl-api.js +0 -22
  37. package/dist/esm/rewards/apy/index.js +0 -3
  38. package/dist/esm/rewards/apy/output-details.js +0 -1
  39. package/dist/esm/rewards/apy/output.js +0 -1
  40. package/dist/esm/rewards/rewards/common.js +0 -1
  41. package/dist/esm/rewards/rewards/extra-apy.js +0 -67
  42. package/dist/esm/rewards/rewards/index.js +0 -3
  43. package/dist/esm/rewards/rewards/merkl-api.js +0 -19
  44. package/dist/types/rewards/apy/index.d.ts +0 -3
  45. package/dist/types/rewards/apy/output-details.d.ts +0 -99
  46. package/dist/types/rewards/apy/output.d.ts +0 -25
  47. package/dist/types/rewards/rewards/api.d.ts +0 -60
  48. package/dist/types/rewards/rewards/common.d.ts +0 -9
  49. package/dist/types/rewards/rewards/extra-apy.d.ts +0 -37
  50. package/dist/types/rewards/rewards/index.d.ts +0 -3
  51. package/dist/types/rewards/rewards/merkl-api.d.ts +0 -54
@@ -1,4 +1,5 @@
1
1
  import { getAlchemyUrl } from "../providers.js";
2
+ import { appendFileSync } from "node:fs";
2
3
  import { pino } from "pino";
3
4
  //#region src/dev/mode-parity/scriptUtils.ts
4
5
  const NETWORKS = [
@@ -76,6 +77,40 @@ function worstColumns(worst) {
76
77
  };
77
78
  }
78
79
  /**
80
+ * Reasons a compare run should fail CI: unexpected membership or field diffs,
81
+ * both sources empty, a chain that could not be read, or wallets that failed
82
+ * to list. Expected diffs (mode-scoped, backend-preferred, tolerance) do not
83
+ * fail the run.
84
+ **/
85
+ function evaluateCompareFailure(report) {
86
+ const { summary } = report;
87
+ const reasons = [];
88
+ if (summary.onchainRows === 0 && summary.offchainRows === 0) reasons.push("both sources produced no data");
89
+ if (summary.different > 0) reasons.push(`${summary.different} matched rows are different`);
90
+ if (summary.onlyOnchain > 0) reasons.push(`${summary.onlyOnchain} rows only onchain`);
91
+ if (summary.onlyOffchain > 0) reasons.push(`${summary.onlyOffchain} rows only offchain`);
92
+ if ((summary.walletsFailed ?? 0) > 0) reasons.push(`${summary.walletsFailed} wallets failed to list`);
93
+ for (const chain of [...report.onchainChains, ...report.offchainChains]) if (chain.status === "error") reasons.push(`chain ${chain.chainId} failed on ${chain.source ?? "unknown"}: ${errorMessage(chain.error)}`);
94
+ return reasons;
95
+ }
96
+ function splitDiffs(summary) {
97
+ return {
98
+ unexpected: summary.diffsByPath.filter((entry) => entry.unexpected > 0),
99
+ expected: summary.diffsByPath.filter((entry) => entry.unexpected === 0 && entry.expected > 0)
100
+ };
101
+ }
102
+ function markdownCell(value) {
103
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
104
+ }
105
+ function markdownTable(headers, rows) {
106
+ const line = (cells) => `| ${cells.map(markdownCell).join(" | ")} |`;
107
+ return [
108
+ line(headers),
109
+ line(headers.map(() => "---")),
110
+ ...rows.map(line)
111
+ ].join("\n");
112
+ }
113
+ /**
79
114
  * Prints the shared membership table, the fields that differed most often,
80
115
  * and any chain that failed to answer.
81
116
  **/
@@ -87,15 +122,13 @@ function printCompareSummary(noun, report, extraLines = []) {
87
122
  onchain: chain.onchainRows,
88
123
  offchain: chain.offchainRows,
89
124
  matched: chain.matched,
90
- identical: chain.identical,
91
- clean: chain.clean,
92
- differing: chain.differing,
125
+ similar: chain.similar,
126
+ different: chain.different,
93
127
  "only onchain": chain.onlyOnchain,
94
128
  "only offchain": chain.onlyOffchain
95
129
  })));
96
130
  for (const line of extraLines) console.log(line);
97
- const unexpected = summary.diffsByPath.filter((entry) => entry.unexpected > 0);
98
- const expected = summary.diffsByPath.filter((entry) => entry.unexpected === 0 && entry.expected > 0);
131
+ const { unexpected, expected } = splitDiffs(summary);
99
132
  if (unexpected.length) {
100
133
  console.log("\nunexpected fields that differed most often:");
101
134
  console.table(unexpected.slice(0, 25).map((entry) => ({
@@ -117,5 +150,108 @@ function printCompareSummary(noun, report, extraLines = []) {
117
150
  }
118
151
  for (const chain of [...report.onchainChains, ...report.offchainChains]) if (chain.status === "error") console.log(`chain ${chain.chainId} failed on ${chain.source}:`, chain.error);
119
152
  }
153
+ /**
154
+ * Markdown of the same summary printed to the console, for GitHub job summaries.
155
+ **/
156
+ function formatCompareMarkdown(noun, report, extraLines = [], reasons = []) {
157
+ const { summary } = report;
158
+ const { unexpected, expected } = splitDiffs(summary);
159
+ const parts = [
160
+ `# ${noun} compare`,
161
+ "",
162
+ reasons.length ? "**Failed**" : "**Passed**",
163
+ "",
164
+ `${noun} from \`${report.backendUrl}\` vs the chain`,
165
+ ""
166
+ ];
167
+ if (reasons.length) {
168
+ parts.push("## Failures", "");
169
+ for (const reason of reasons) parts.push(`- ${reason}`);
170
+ parts.push("");
171
+ }
172
+ if (extraLines.length) {
173
+ for (const line of extraLines) parts.push(line);
174
+ parts.push("");
175
+ }
176
+ if (summary.byChain.length) parts.push(markdownTable([
177
+ "chain",
178
+ "onchain",
179
+ "offchain",
180
+ "matched",
181
+ "similar",
182
+ "different",
183
+ "only onchain",
184
+ "only offchain"
185
+ ], summary.byChain.map((chain) => [
186
+ chain.chainId,
187
+ chain.onchainRows,
188
+ chain.offchainRows,
189
+ chain.matched,
190
+ chain.similar,
191
+ chain.different,
192
+ chain.onlyOnchain,
193
+ chain.onlyOffchain
194
+ ])), "");
195
+ if (unexpected.length) parts.push("## Unexpected fields that differed most often", "", markdownTable([
196
+ "field",
197
+ "unexpected",
198
+ "expected",
199
+ "kinds",
200
+ "max diff",
201
+ "worst entity"
202
+ ], unexpected.slice(0, 25).map((entry) => {
203
+ const worst = worstColumns(entry.worstUnexpected);
204
+ return [
205
+ entry.path,
206
+ entry.unexpected,
207
+ entry.expected,
208
+ entry.kinds.join(", "),
209
+ worst["max diff"],
210
+ worst["worst entity"]
211
+ ];
212
+ })), "");
213
+ if (expected.length) parts.push("## Expected fields (mode-scoped, backend-preferred, or within tolerance)", "", markdownTable([
214
+ "field",
215
+ "rows",
216
+ "kinds",
217
+ "max diff",
218
+ "worst entity"
219
+ ], expected.slice(0, 25).map((entry) => {
220
+ const worst = worstColumns(entry.worstExpected);
221
+ return [
222
+ entry.path,
223
+ entry.expected,
224
+ entry.kinds.join(", "),
225
+ worst["max diff"],
226
+ worst["worst entity"]
227
+ ];
228
+ })), "");
229
+ const failedChains = [...report.onchainChains, ...report.offchainChains].filter((chain) => chain.status === "error");
230
+ if (failedChains.length) {
231
+ parts.push("## Chain errors", "");
232
+ for (const chain of failedChains) parts.push(`- chain ${chain.chainId} failed on ${chain.source ?? "unknown"}: ${errorMessage(chain.error)}`);
233
+ parts.push("");
234
+ }
235
+ return parts.join("\n");
236
+ }
237
+ /**
238
+ * Appends {@link formatCompareMarkdown} to `GITHUB_STEP_SUMMARY` when running
239
+ * in GitHub Actions. No-op locally.
240
+ **/
241
+ function writeGithubJobSummary(noun, report, extraLines = [], reasons = []) {
242
+ const path = process.env.GITHUB_STEP_SUMMARY;
243
+ if (!path) return;
244
+ appendFileSync(path, `${formatCompareMarkdown(noun, report, extraLines, reasons)}\n`);
245
+ }
246
+ /**
247
+ * Console + job-summary output for a compare run. Returns the failure reasons
248
+ * so the caller can exit 1 when the report is not similar.
249
+ **/
250
+ function reportCompare(noun, report, extraLines = []) {
251
+ printCompareSummary(noun, report, extraLines);
252
+ const reasons = evaluateCompareFailure(report);
253
+ writeGithubJobSummary(noun, report, extraLines, reasons);
254
+ return reasons;
255
+ }
120
256
  //#endregion
121
- export { BACKEND_URL, NETWORKS, TIMEOUT, createLogger, errorMessage, formatBpsAsPercent, mapPool, printCompareSummary, requireEnv, rpcUrls };
257
+ export { BACKEND_URL, NETWORKS, TIMEOUT, createLogger, errorMessage, evaluateCompareFailure, formatBpsAsPercent, formatCompareMarkdown, mapPool, printCompareSummary, reportCompare, requireEnv, rpcUrls, writeGithubJobSummary };
@@ -0,0 +1,26 @@
1
+ import { BaseError } from "viem";
2
+ //#region src/rewards/errors.ts
3
+ /**
4
+ * Thrown when none of Merkl's domains answered for a chain.
5
+ *
6
+ * Everything a reader needs is in the message: this error travels to consumers
7
+ * inside a chain's {@link ChainFailed} metadata, where it is typed `unknown`
8
+ * and may be serialised by something that turns an arbitrary object into `{}`.
9
+ */
10
+ var MerklRequestFailedError = class extends BaseError {
11
+ name = "MerklRequestFailedError";
12
+ chainId;
13
+ constructor(chainId, path, attempts) {
14
+ super(`Merkl could not be reached for chain ${chainId}.`, {
15
+ cause: attempts.find(([, c]) => c instanceof Error)?.[1],
16
+ metaMessages: attempts.map(([domain, cause]) => `${domain}${path} — ${describe(cause)}`)
17
+ });
18
+ this.chainId = chainId;
19
+ }
20
+ };
21
+ function describe(cause) {
22
+ if (cause instanceof Error) return cause.name === "TimeoutError" ? "timed out" : cause.message;
23
+ return String(cause);
24
+ }
25
+ //#endregion
26
+ export { MerklRequestFailedError };
@@ -1,5 +1,4 @@
1
- import "./apy/index.js";
2
- import { getMerklRewards } from "./rewards/api.js";
3
- import { PoolPointsAPI, getKeyForPoolPointsInfo } from "./rewards/extra-apy.js";
4
- import "./rewards/index.js";
5
- export { PoolPointsAPI, getKeyForPoolPointsInfo, getMerklRewards };
1
+ import { MerklRequestFailedError } from "./errors.js";
2
+ import { toMerklRewards } from "./toMerklRewards.js";
3
+ import { getMerklRewardsMultichain } from "./multichain.js";
4
+ export { MerklRequestFailedError, getMerklRewardsMultichain, toMerklRewards };
@@ -0,0 +1,42 @@
1
+ import { MerklRequestFailedError } from "./errors.js";
2
+ //#region src/rewards/merkl-api.ts
3
+ /**
4
+ * Merkl's own host and the Angle mirror, tried in this order.
5
+ */
6
+ const MERKL_DOMAINS = ["https://api.merkl.xyz", "https://api-merkl.angle.money"];
7
+ const MERKL_API_KEY_HEADER = "X-API-Key";
8
+ /**
9
+ * Per-attempt budget. Merkl has no timeout of its own, and a hung connection
10
+ * would otherwise stall its leg of a fan-out for as long as the socket lives.
11
+ */
12
+ const ATTEMPT_TIMEOUT = 1e4;
13
+ /**
14
+ * The wallet's raw Merkl rewards on one chain.
15
+ *
16
+ * Rejects with {@link MerklRequestFailedError} when neither domain answers, so
17
+ * a caller can tell an unreachable Merkl from a wallet with nothing to claim.
18
+ * A non-2xx counts as no answer and moves to the next domain: it carries no
19
+ * rewards either way, and treating it as success would report emptiness that
20
+ * was never established.
21
+ */
22
+ async function fetchMerklUserRewards({ chainId, user, apiKey }) {
23
+ const path = `/v4/users/${user}/rewards?chainId=${chainId}`;
24
+ const headers = apiKey ? { [MERKL_API_KEY_HEADER]: apiKey } : void 0;
25
+ const attempts = [];
26
+ for (const domain of MERKL_DOMAINS) try {
27
+ const response = await fetch(`${domain}${path}`, {
28
+ headers,
29
+ signal: AbortSignal.timeout(ATTEMPT_TIMEOUT)
30
+ });
31
+ if (!response.ok) {
32
+ attempts.push([domain, /* @__PURE__ */ new Error(`answered ${response.status}`)]);
33
+ continue;
34
+ }
35
+ return await response.json();
36
+ } catch (error) {
37
+ attempts.push([domain, error]);
38
+ }
39
+ throw new MerklRequestFailedError(chainId, path, attempts);
40
+ }
41
+ //#endregion
42
+ export { MERKL_API_KEY_HEADER, MERKL_DOMAINS, fetchMerklUserRewards };
@@ -0,0 +1,39 @@
1
+ import { MultichainConstruct } from "../onchain/base/MultichainConstruct.js";
2
+ import "../onchain/index.js";
3
+ import { fetchMerklUserRewards } from "./merkl-api.js";
4
+ import { toMerklRewards } from "./toMerklRewards.js";
5
+ import { getAddress } from "viem";
6
+ //#region src/rewards/multichain.ts
7
+ /**
8
+ * The fan-out itself. Private because rewards are not an SDK namespace yet:
9
+ * the read is a free function, and this is only how it reaches `queryChains`.
10
+ **/
11
+ var MerklRewardsFanOut = class extends MultichainConstruct {
12
+ async list(wallet, chainIds, apiKey) {
13
+ const user = getAddress(wallet);
14
+ return this.queryChains({
15
+ chainIds,
16
+ label: "list rewards",
17
+ block: "state",
18
+ run: async (sdk) => toMerklRewards(sdk, await fetchMerklUserRewards({
19
+ chainId: sdk.chainId,
20
+ user,
21
+ apiKey
22
+ }))
23
+ });
24
+ }
25
+ };
26
+ /**
27
+ * Every claimable Merkl reward a wallet holds, across the chains the handle
28
+ * carries.
29
+ *
30
+ * Answers the read model's own envelope, so a chain that could not be reached
31
+ * is `status: "error"` in `meta.chains` while a chain with nothing to claim is
32
+ * a `"success"` that contributed no rows. That distinction is the point: the
33
+ * single-chain read this replaces resolved empty either way.
34
+ **/
35
+ async function getMerklRewardsMultichain({ sdk, wallet, chainIds, apiKey }) {
36
+ return new MerklRewardsFanOut(sdk).list(wallet, chainIds, apiKey);
37
+ }
38
+ //#endregion
39
+ export { getMerklRewardsMultichain };
@@ -1,27 +1,16 @@
1
- import { AddressMap } from "../../onchain/utils/AddressMap.js";
2
- import { BigIntMath } from "../../onchain/utils/bigint-math.js";
3
- import { toBigInt } from "../../onchain/utils/formatter.js";
4
- import "../../onchain/index.js";
5
- import { MerkleXYZApi } from "./merkl-api.js";
1
+ import { AddressMap } from "../onchain/utils/AddressMap.js";
2
+ import { BigIntMath } from "../onchain/utils/bigint-math.js";
3
+ import { toBigInt } from "../onchain/utils/formatter.js";
4
+ import "../onchain/index.js";
6
5
  import { formatUnits, getAddress, isAddress } from "viem";
7
- //#region src/rewards/rewards/api.ts
6
+ //#region src/rewards/toMerklRewards.ts
8
7
  /**
9
- * The wallet's claimable Merkl rewards on one chain.
10
- *
11
- * Never rejects on a transport failure: the fetch is settled rather than
12
- * awaited, and a failure goes to `reportError` and yields an empty list. A
13
- * caller that must tell "this chain is down" from "this chain has no rewards"
14
- * has to watch that callback.
8
+ * Merkl's answer for one chain, turned into rows of the read model.
15
9
  */
16
- async function getMerklRewards({ sdk, account, reportError, apiKey }) {
17
- const [merkleXYZLMResponse] = await Promise.allSettled([MerkleXYZApi.fetchWithFallback(MerkleXYZApi.getUserRewardsUrl({ params: {
18
- chainId: sdk.chainId,
19
- user: getAddress(account)
20
- } }), apiKey)]);
21
- const merkleXYZLm = extractFulfilled(merkleXYZLMResponse, reportError, "merkleXYZLm")?.data;
10
+ function toMerklRewards(sdk, response) {
22
11
  const poolByItsToken = AddressMap.fromMappedArray(sdk.marketRegister.pools.map(({ pool }) => pool.address), (address) => address);
23
12
  const claimable = /* @__PURE__ */ new Map();
24
- for (const chainRewards of merkleXYZLm || []) for (const reward of chainRewards.rewards) {
13
+ for (const chainRewards of response) for (const reward of chainRewards.rewards) {
25
14
  if (!isAddress(reward.token.address, { strict: false })) continue;
26
15
  const rewardTokenAddress = getAddress(reward.token.address);
27
16
  for (const reason of reward.breakdowns) {
@@ -29,9 +18,9 @@ async function getMerklRewards({ sdk, account, reportError, apiKey }) {
29
18
  if (!isAddress(poolTokenAddress, { strict: false })) continue;
30
19
  const pool = poolByItsToken.get(poolTokenAddress);
31
20
  if (!pool) continue;
32
- const total = toBigInt(reason.amount || 0);
33
- const claimed = toBigInt(reason.claimed || 0);
34
- const amount = BigIntMath.max(total - claimed, 0n);
21
+ const amounts = toAmounts(reason);
22
+ if (!amounts) continue;
23
+ const amount = BigIntMath.max(amounts.total - amounts.claimed, 0n);
35
24
  if (amount === 0n) continue;
36
25
  const key = `${pool}_${rewardTokenAddress}`;
37
26
  const seen = claimable.get(key);
@@ -53,6 +42,20 @@ async function getMerklRewards({ sdk, account, reportError, apiKey }) {
53
42
  }
54
43
  return [...claimable.values()].map(toReward);
55
44
  }
45
+ /**
46
+ * `toBigInt` throws on anything `BigInt()` cannot parse, and Merkl's amounts
47
+ * are free-form strings.
48
+ */
49
+ function toAmounts(reason) {
50
+ try {
51
+ return {
52
+ total: toBigInt(reason.amount || 0),
53
+ claimed: toBigInt(reason.claimed || 0)
54
+ };
55
+ } catch {
56
+ return;
57
+ }
58
+ }
56
59
  function toReward({ price, token, value, ...rest }) {
57
60
  return {
58
61
  ...rest,
@@ -77,10 +80,5 @@ function toRewardToken(sdk, address, merkl) {
77
80
  decimals: merkl.decimals || 18
78
81
  };
79
82
  }
80
- function extractFulfilled(r, reportError, description) {
81
- if (r.status === "fulfilled") return r.value;
82
- if (reportError) reportError(r.reason, description);
83
- else console.error(r.reason);
84
- }
85
83
  //#endregion
86
- export { getMerklRewards };
84
+ export { toMerklRewards };
@@ -36,14 +36,10 @@ interface OpportunityMatch {
36
36
  **/
37
37
  onchainName: string;
38
38
  offchainName: string;
39
- /**
40
- * No diffs at all, including the documented offchain-only ones.
41
- **/
42
- identical: boolean;
43
39
  /**
44
40
  * No unexpected diffs: every disagreement is mode-scoped or within tolerance.
45
41
  **/
46
- clean: boolean;
42
+ similar: boolean;
47
43
  diffs: FieldDiff[];
48
44
  }
49
45
  /**
@@ -93,8 +89,8 @@ interface CompareOpportunitiesInput {
93
89
  *
94
90
  * Nothing is filtered out. A field only the backend can fill, or a USD value
95
91
  * that drifted within snapshot-lag noise, is still reported — tagged
96
- * {@link FieldDiff.expected} so that {@link CompareCounts.clean} can ignore it
97
- * while {@link CompareCounts.identical} stays strict.
92
+ * {@link FieldDiff.expected} so that {@link CompareCounts.similar} can ignore
93
+ * it.
98
94
  **/
99
95
  declare function compareOpportunities(input: CompareOpportunitiesInput): OpportunityCompareReport;
100
96
  /**
@@ -34,15 +34,11 @@ interface PositionMatch {
34
34
  chainId: ChainId;
35
35
  onchainName: string;
36
36
  offchainName: string;
37
- /**
38
- * No diffs at all, including the documented mode-scoped ones.
39
- **/
40
- identical: boolean;
41
37
  /**
42
38
  * No unexpected diffs: every disagreement is mode-scoped, backend-preferred,
43
39
  * or within tolerance.
44
40
  **/
45
- clean: boolean;
41
+ similar: boolean;
46
42
  diffs: FieldDiff[];
47
43
  }
48
44
  /**
@@ -78,7 +74,7 @@ interface PositionsCompareSummary extends CompareCounts {
78
74
  * Wallets whose listings were read and that have no membership gaps and no
79
75
  * unexpected field diffs.
80
76
  **/
81
- walletsClean: number;
77
+ walletsSimilar: number;
82
78
  walletsFailed: number;
83
79
  byChain: ChainCompareCounts[];
84
80
  byWallet: WalletCompareCounts[];
@@ -139,8 +135,8 @@ interface ComparePositionsInput {
139
135
  * Nothing is filtered out. A field only one mode can fill, a strategy field
140
136
  * both-mode merge overlays from the backend, or a USD value that drifted
141
137
  * within snapshot-lag noise, is still reported — tagged
142
- * {@link FieldDiff.expected} so that {@link CompareCounts.clean} can ignore it
143
- * while {@link CompareCounts.identical} stays strict.
138
+ * {@link FieldDiff.expected} so that {@link CompareCounts.similar} can ignore
139
+ * it.
144
140
  **/
145
141
  declare function comparePositions(input: ComparePositionsInput): PositionsCompareReport;
146
142
  /**
@@ -42,7 +42,7 @@ interface FieldDiff {
42
42
  kind: DiffKind;
43
43
  /**
44
44
  * Present when this disagreement is documented or within snapshot-lag noise,
45
- * so it does not keep the row from being counted as clean.
45
+ * so it does not keep the row from being counted as similar.
46
46
  **/
47
47
  expected?: true;
48
48
  reason?: ExpectedDiffReason;
@@ -102,12 +102,14 @@ interface CompareCounts {
102
102
  onchainRows: number;
103
103
  offchainRows: number;
104
104
  matched: number;
105
- identical: number;
106
105
  /**
107
- * Matched rows with no unexpected diffs, including the identical ones.
106
+ * Matched rows with no unexpected diffs.
108
107
  **/
109
- clean: number;
110
- differing: number;
108
+ similar: number;
109
+ /**
110
+ * Matched rows with at least one unexpected diff.
111
+ **/
112
+ different: number;
111
113
  onlyOnchain: number;
112
114
  onlyOffchain: number;
113
115
  }
@@ -157,7 +159,7 @@ declare function diffValue(path: string, onchain: unknown, offchain: unknown, ou
157
159
  **/
158
160
  declare function diffObjects(onchain: unknown, offchain: unknown, options?: DiffOptions): FieldDiff[];
159
161
  /**
160
- * Mark a diff expected, so a later `clean` count can ignore it.
162
+ * Mark a diff expected, so a later `similar` count can ignore it.
161
163
  **/
162
164
  declare function withExpected(diff: FieldDiff, reason: ExpectedDiffReason): FieldDiff;
163
165
  /**
@@ -174,8 +176,7 @@ declare function collapseArrayKeys(path: string): string;
174
176
  * Membership and match totals of one comparison, from already-built lists.
175
177
  **/
176
178
  declare function toCompareCounts(onchainRows: number, offchainRows: number, onlyOnchain: number, onlyOffchain: number, matched: ReadonlyArray<{
177
- identical: boolean;
178
- clean: boolean;
179
+ similar: boolean;
179
180
  }>): CompareCounts;
180
181
  /**
181
182
  * USD floats within {@link USD_RELATIVE_EPSILON}.
@@ -16,6 +16,10 @@ declare const TIMEOUT = 480000;
16
16
  interface PrintableCompareSummary extends CompareCounts {
17
17
  byChain: ChainCompareCounts[];
18
18
  diffsByPath: DiffPathCount[];
19
+ /**
20
+ * Present on position reports: wallets whose listings could not be read.
21
+ **/
22
+ walletsFailed?: number;
19
23
  }
20
24
  /**
21
25
  * Per-source chain metadata both compare reports carry.
@@ -38,10 +42,31 @@ declare function mapPool<T>(items: readonly T[], concurrency: number, fn: (item:
38
42
  * Formats a relative difference in bps as a percent, e.g. 12.5 → `"0.125%"`.
39
43
  **/
40
44
  declare function formatBpsAsPercent(bps: number): string;
45
+ /**
46
+ * Reasons a compare run should fail CI: unexpected membership or field diffs,
47
+ * both sources empty, a chain that could not be read, or wallets that failed
48
+ * to list. Expected diffs (mode-scoped, backend-preferred, tolerance) do not
49
+ * fail the run.
50
+ **/
51
+ declare function evaluateCompareFailure(report: PrintableCompareReport): string[];
41
52
  /**
42
53
  * Prints the shared membership table, the fields that differed most often,
43
54
  * and any chain that failed to answer.
44
55
  **/
45
56
  declare function printCompareSummary(noun: string, report: PrintableCompareReport, extraLines?: string[]): void;
57
+ /**
58
+ * Markdown of the same summary printed to the console, for GitHub job summaries.
59
+ **/
60
+ declare function formatCompareMarkdown(noun: string, report: PrintableCompareReport, extraLines?: string[], reasons?: string[]): string;
61
+ /**
62
+ * Appends {@link formatCompareMarkdown} to `GITHUB_STEP_SUMMARY` when running
63
+ * in GitHub Actions. No-op locally.
64
+ **/
65
+ declare function writeGithubJobSummary(noun: string, report: PrintableCompareReport, extraLines?: string[], reasons?: string[]): void;
66
+ /**
67
+ * Console + job-summary output for a compare run. Returns the failure reasons
68
+ * so the caller can exit 1 when the report is not similar.
69
+ **/
70
+ declare function reportCompare(noun: string, report: PrintableCompareReport, extraLines?: string[]): string[];
46
71
  //#endregion
47
- export { BACKEND_URL, ComparedNetwork, NETWORKS, PrintableCompareReport, PrintableCompareSummary, TIMEOUT, createLogger, errorMessage, formatBpsAsPercent, mapPool, printCompareSummary, requireEnv, rpcUrls };
72
+ export { BACKEND_URL, ComparedNetwork, NETWORKS, PrintableCompareReport, PrintableCompareSummary, TIMEOUT, createLogger, errorMessage, evaluateCompareFailure, formatBpsAsPercent, formatCompareMarkdown, mapPool, printCompareSummary, reportCompare, requireEnv, rpcUrls, writeGithubJobSummary };
@@ -0,0 +1,18 @@
1
+ import { ChainId } from "../model/primitives.js";
2
+ import "../model/index.js";
3
+ import { BaseError } from "viem";
4
+ //#region src/rewards/errors.d.ts
5
+ /**
6
+ * Thrown when none of Merkl's domains answered for a chain.
7
+ *
8
+ * Everything a reader needs is in the message: this error travels to consumers
9
+ * inside a chain's {@link ChainFailed} metadata, where it is typed `unknown`
10
+ * and may be serialised by something that turns an arbitrary object into `{}`.
11
+ */
12
+ declare class MerklRequestFailedError extends BaseError {
13
+ name: string;
14
+ readonly chainId: ChainId;
15
+ constructor(chainId: ChainId, path: string, attempts: ReadonlyArray<[domain: string, cause: unknown]>);
16
+ }
17
+ //#endregion
18
+ export { MerklRequestFailedError };
@@ -1,7 +1,4 @@
1
- import { Apy, ApyDetails, DebtReward, ExternalApy, ExtraCollateralAPY, ExtraCollateralPointsInfo, FarmInfo, GearAPY, GearAPYDetails, PointsInfo, PointsReward, PoolExtraApy, PoolOutputDetails, PoolPointsInfo, TokenOutputDetails } from "./apy/output-details.js";
2
- import { DataResult, Output } from "./apy/output.js";
3
- import "./apy/index.js";
4
- import { GetMerklRewardsProps, MerklReward, MerklRewardsSdk, getMerklRewards } from "./rewards/api.js";
5
- import { GetPointsByPoolProps, GetTotalTokensOnProtocolProps, PoolPointsAPI, PoolPointsBase, getKeyForPoolPointsInfo } from "./rewards/extra-apy.js";
6
- import "./rewards/index.js";
7
- export { Apy, ApyDetails, DataResult, DebtReward, ExternalApy, ExtraCollateralAPY, ExtraCollateralPointsInfo, FarmInfo, GearAPY, GearAPYDetails, GetMerklRewardsProps, GetPointsByPoolProps, GetTotalTokensOnProtocolProps, MerklReward, MerklRewardsSdk, Output, PointsInfo, PointsReward, PoolExtraApy, PoolOutputDetails, PoolPointsAPI, PoolPointsBase, PoolPointsInfo, TokenOutputDetails, getKeyForPoolPointsInfo, getMerklRewards };
1
+ import { MerklRequestFailedError } from "./errors.js";
2
+ import { MerklReward, MerklRewardsSdk, toMerklRewards } from "./toMerklRewards.js";
3
+ import { GetMerklRewardsMultichainProps, getMerklRewardsMultichain } from "./multichain.js";
4
+ export { GetMerklRewardsMultichainProps, MerklRequestFailedError, MerklReward, MerklRewardsSdk, getMerklRewardsMultichain, toMerklRewards };
@@ -0,0 +1,64 @@
1
+ import { ChainId } from "../model/primitives.js";
2
+ import "../model/index.js";
3
+ import { Address } from "viem";
4
+ //#region src/rewards/merkl-api.d.ts
5
+ interface MerkleXYZUserRewardsV4 {
6
+ chain: MerkleXYZChain;
7
+ rewards: Array<{
8
+ root: Address;
9
+ recipient: Address;
10
+ amount: string;
11
+ claimed: string;
12
+ pending: string;
13
+ proofs: Array<Address>;
14
+ token: {
15
+ address: Address;
16
+ chainId: number;
17
+ symbol: string;
18
+ decimals: number;
19
+ /**
20
+ * USD price of one whole token. Optional because Merkl omits the key
21
+ * outright for the tokens it does not price — points and the like —
22
+ * rather than sending a null.
23
+ */
24
+ price?: number;
25
+ };
26
+ breakdowns: Array<{
27
+ reason: string;
28
+ amount: string;
29
+ claimed: string;
30
+ pending: string;
31
+ campaignId: Address;
32
+ }>;
33
+ }>;
34
+ }
35
+ type MerkleXYZUserRewardsV4Response = Array<MerkleXYZUserRewardsV4>;
36
+ interface MerkleXYZChain {
37
+ id: number;
38
+ name: string;
39
+ icon: string;
40
+ }
41
+ /**
42
+ * Merkl's own host and the Angle mirror, tried in this order.
43
+ */
44
+ declare const MERKL_DOMAINS: readonly ["https://api.merkl.xyz", "https://api-merkl.angle.money"];
45
+ declare const MERKL_API_KEY_HEADER = "X-API-Key";
46
+ interface FetchMerklUserRewardsProps {
47
+ chainId: ChainId;
48
+ /** Checksummed by the caller — Merkl keys its answer on the exact string. */
49
+ user: Address;
50
+ /** Raises Merkl's rate limit; the keyless path answers too. */
51
+ apiKey?: string;
52
+ }
53
+ /**
54
+ * The wallet's raw Merkl rewards on one chain.
55
+ *
56
+ * Rejects with {@link MerklRequestFailedError} when neither domain answers, so
57
+ * a caller can tell an unreachable Merkl from a wallet with nothing to claim.
58
+ * A non-2xx counts as no answer and moves to the next domain: it carries no
59
+ * rewards either way, and treating it as success would report emptiness that
60
+ * was never established.
61
+ */
62
+ declare function fetchMerklUserRewards({ chainId, user, apiKey }: FetchMerklUserRewardsProps): Promise<MerkleXYZUserRewardsV4Response>;
63
+ //#endregion
64
+ export { FetchMerklUserRewardsProps, MERKL_API_KEY_HEADER, MERKL_DOMAINS, MerkleXYZUserRewardsV4, MerkleXYZUserRewardsV4Response, fetchMerklUserRewards };
@@ -0,0 +1,33 @@
1
+ import { ChainId } from "../model/primitives.js";
2
+ import { DataResponse } from "../model/response.js";
3
+ import "../model/index.js";
4
+ import { PluginsMap } from "../onchain/plugins/types.js";
5
+ import { MultichainSDK } from "../onchain/MultichainSDK.js";
6
+ import "../onchain/index.js";
7
+ import { MerklReward } from "./toMerklRewards.js";
8
+ import { Address } from "viem";
9
+ //#region src/rewards/multichain.d.ts
10
+ interface GetMerklRewardsMultichainProps<Plugins extends PluginsMap = {}> {
11
+ /** Handle whose chains are asked. */
12
+ sdk: MultichainSDK<Plugins>;
13
+ /** Wallet whose claimable rewards to list. */
14
+ wallet: Address;
15
+ /**
16
+ * Chains to ask, defaulting to every chain the handle carries.
17
+ **/
18
+ chainIds?: ChainId[];
19
+ /** Raises Merkl's rate limit; the keyless path answers too. */
20
+ apiKey?: string;
21
+ }
22
+ /**
23
+ * Every claimable Merkl reward a wallet holds, across the chains the handle
24
+ * carries.
25
+ *
26
+ * Answers the read model's own envelope, so a chain that could not be reached
27
+ * is `status: "error"` in `meta.chains` while a chain with nothing to claim is
28
+ * a `"success"` that contributed no rows. That distinction is the point: the
29
+ * single-chain read this replaces resolved empty either way.
30
+ **/
31
+ declare function getMerklRewardsMultichain<const Plugins extends PluginsMap = {}>({ sdk, wallet, chainIds, apiKey }: GetMerklRewardsMultichainProps<Plugins>): Promise<DataResponse<MerklReward[]>>;
32
+ //#endregion
33
+ export { GetMerklRewardsMultichainProps, getMerklRewardsMultichain };