@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.
- package/dist/cjs/dev/mode-parity/compareOpportunities.js +3 -4
- package/dist/cjs/dev/mode-parity/comparePositions.js +5 -6
- package/dist/cjs/dev/mode-parity/fieldDiff.js +4 -6
- package/dist/cjs/dev/mode-parity/scriptUtils.js +145 -5
- package/dist/cjs/rewards/errors.js +27 -0
- package/dist/cjs/rewards/index.js +6 -7
- package/dist/cjs/rewards/merkl-api.js +45 -0
- package/dist/cjs/rewards/multichain.js +40 -0
- package/dist/cjs/rewards/{rewards/api.js → toMerklRewards.js} +26 -28
- package/dist/esm/dev/mode-parity/compareOpportunities.js +3 -4
- package/dist/esm/dev/mode-parity/comparePositions.js +5 -6
- package/dist/esm/dev/mode-parity/fieldDiff.js +4 -6
- package/dist/esm/dev/mode-parity/scriptUtils.js +142 -6
- package/dist/esm/rewards/errors.js +26 -0
- package/dist/esm/rewards/index.js +4 -5
- package/dist/esm/rewards/merkl-api.js +42 -0
- package/dist/esm/rewards/multichain.js +39 -0
- package/dist/esm/rewards/{rewards/api.js → toMerklRewards.js} +26 -28
- package/dist/types/dev/mode-parity/compareOpportunities.d.ts +3 -7
- package/dist/types/dev/mode-parity/comparePositions.d.ts +4 -8
- package/dist/types/dev/mode-parity/fieldDiff.d.ts +9 -8
- package/dist/types/dev/mode-parity/scriptUtils.d.ts +26 -1
- package/dist/types/rewards/errors.d.ts +18 -0
- package/dist/types/rewards/index.d.ts +4 -7
- package/dist/types/rewards/merkl-api.d.ts +64 -0
- package/dist/types/rewards/multichain.d.ts +33 -0
- package/dist/types/rewards/toMerklRewards.d.ts +42 -0
- package/package.json +1 -3
- package/dist/cjs/_virtual/_rolldown/runtime.js +0 -23
- package/dist/cjs/rewards/apy/index.js +0 -3
- package/dist/cjs/rewards/apy/output-details.js +0 -1
- package/dist/cjs/rewards/apy/output.js +0 -1
- package/dist/cjs/rewards/rewards/common.js +0 -1
- package/dist/cjs/rewards/rewards/extra-apy.js +0 -71
- package/dist/cjs/rewards/rewards/index.js +0 -6
- package/dist/cjs/rewards/rewards/merkl-api.js +0 -22
- package/dist/esm/rewards/apy/index.js +0 -3
- package/dist/esm/rewards/apy/output-details.js +0 -1
- package/dist/esm/rewards/apy/output.js +0 -1
- package/dist/esm/rewards/rewards/common.js +0 -1
- package/dist/esm/rewards/rewards/extra-apy.js +0 -67
- package/dist/esm/rewards/rewards/index.js +0 -3
- package/dist/esm/rewards/rewards/merkl-api.js +0 -19
- package/dist/types/rewards/apy/index.d.ts +0 -3
- package/dist/types/rewards/apy/output-details.d.ts +0 -99
- package/dist/types/rewards/apy/output.d.ts +0 -25
- package/dist/types/rewards/rewards/api.d.ts +0 -60
- package/dist/types/rewards/rewards/common.d.ts +0 -9
- package/dist/types/rewards/rewards/extra-apy.d.ts +0 -37
- package/dist/types/rewards/rewards/index.d.ts +0 -3
- 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
|
-
|
|
91
|
-
|
|
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
|
|
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 "./
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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 "
|
|
2
|
-
import { BigIntMath } from "
|
|
3
|
-
import { toBigInt } from "
|
|
4
|
-
import "
|
|
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/
|
|
6
|
+
//#region src/rewards/toMerklRewards.ts
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
33
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
97
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
143
|
-
*
|
|
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
|
|
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
|
|
106
|
+
* Matched rows with no unexpected diffs.
|
|
108
107
|
**/
|
|
109
|
-
|
|
110
|
-
|
|
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 `
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import "./
|
|
4
|
-
|
|
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 };
|