@berachain/berajs 0.2.9 → 0.2.10

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 (59) hide show
  1. package/dist/actions/clients/exports.d.ts +77 -1
  2. package/dist/actions/clients/exports.mjs +13 -4
  3. package/dist/actions/exports.d.ts +59 -27
  4. package/dist/actions/exports.mjs +23 -23
  5. package/dist/actions/governance/exports.mjs +3 -3
  6. package/dist/actions/server/exports.mjs +4 -4
  7. package/dist/{chunk-4Z4AK6SH.mjs → chunk-3JJLQ2JX.mjs} +3 -3
  8. package/dist/{chunk-EXIUPSFN.mjs → chunk-7YVNSDXG.mjs} +2 -2
  9. package/dist/{chunk-WXXOISTU.mjs → chunk-AUOPN6NK.mjs} +1 -1
  10. package/dist/{chunk-75M6TF7M.mjs → chunk-DQRH5VE3.mjs} +1 -1
  11. package/dist/{chunk-CDFWPU2R.mjs → chunk-E7YFXBBQ.mjs} +0 -124
  12. package/dist/{chunk-AFN4CVD3.mjs → chunk-GUURQAME.mjs} +1 -1
  13. package/dist/{chunk-KQUMKB66.mjs → chunk-GY6B3PD5.mjs} +1 -1
  14. package/dist/{chunk-HSSJKHZ4.mjs → chunk-HYDP32P6.mjs} +3 -3
  15. package/dist/{chunk-NPBQLVL3.mjs → chunk-IXIBY5FP.mjs} +2 -2
  16. package/dist/{chunk-J5I45WGQ.mjs → chunk-KHXJDYA4.mjs} +7 -0
  17. package/dist/chunk-P5WXXULM.mjs +54 -0
  18. package/dist/{chunk-QJIXTYTZ.mjs → chunk-QBBOWFMH.mjs} +105 -30
  19. package/dist/{chunk-FFB5LFDW.mjs → chunk-QVHEM4BG.mjs} +2 -2
  20. package/dist/{chunk-3EARVV7K.mjs → chunk-WNBWX23Q.mjs} +17 -5
  21. package/dist/chunk-Y6THHG77.mjs +126 -0
  22. package/dist/{chunk-XIYN6AL6.mjs → chunk-ZLTMIFCZ.mjs} +10 -5
  23. package/dist/contexts/exports.mjs +8 -8
  24. package/dist/errors/exports.mjs +5 -5
  25. package/dist/{getValidatorQueuedOperatorAddress-Dw5KN5sh.d.ts → getValidatorQueuedOperatorAddress-DphU3qhE.d.ts} +1 -1
  26. package/dist/hooks/exports.d.ts +17 -18
  27. package/dist/hooks/exports.mjs +161 -132
  28. package/dist/hooks/governance/exports.mjs +4 -4
  29. package/dist/{pol.d-CqPA9K6m.d.ts → pol.d-Dw5SQcRX.d.ts} +15 -4
  30. package/dist/types/exports.d.ts +1 -1
  31. package/dist/utils/exports.mjs +11 -9
  32. package/package.json +4 -4
  33. package/src/actions/clients/exports.ts +3 -0
  34. package/src/actions/clients/fetchBeep.ts +34 -0
  35. package/src/actions/clients/fetchOpenApi.ts +93 -0
  36. package/src/actions/clients/fetchOpenApi.unit.test.ts +223 -0
  37. package/src/actions/clients/fetchRailwayBackend.ts +34 -0
  38. package/src/actions/enso/getEnsoUserTokensWithBalances.ts +18 -0
  39. package/src/actions/exports.ts +1 -0
  40. package/src/actions/honey/getPythLatestPrices.ts +7 -0
  41. package/src/actions/pol/__tests__/rewardVaults.integration.test.ts +1 -1
  42. package/src/actions/pol/getAutoclaimedIncentives.ts +21 -10
  43. package/src/actions/pol/getAutoclaimedIncentivesTxHash.ts +41 -0
  44. package/src/actions/pol/getBgtIncentiveDistributorPaused.ts +5 -12
  45. package/src/actions/pol/getEarnedStakedBeraVault.ts +20 -16
  46. package/src/actions/pol/getRewardVaults.ts +4 -4
  47. package/src/actions/pol/getStakingDailyAssets.ts +18 -14
  48. package/src/actions/validators/utils/getValidatorBoostApy.ts +1 -1
  49. package/src/errors/RequestError.ts +12 -3
  50. package/src/errors/RequestError.unit.test.ts +55 -0
  51. package/src/errors/errorMap.ts +8 -0
  52. package/src/hooks/exports.ts +1 -0
  53. package/src/hooks/pol/useAutoclaimedIncentives.ts +1 -10
  54. package/src/hooks/pol/useAutoclaimedIncentivesTxHash.ts +45 -0
  55. package/src/hooks/validators/useValidator.ts +4 -8
  56. package/src/types/bribe-boost.d.ts +14 -3
  57. package/src/utils/polyfillAbortSignalAny.ts +53 -0
  58. package/src/utils/polyfillAbortSignalAny.unit.test.ts +81 -0
  59. /package/dist/{exports-BcUTGFUb.d.ts → getApolloClient-BcUTGFUb.d.ts} +0 -0
@@ -0,0 +1,41 @@
1
+ import type { Address } from "viem";
2
+
3
+ import { RequestError } from "../../errors/RequestError";
4
+ import type { AutoclaimedIncentivesTxHashResponse } from "../../types/bribe-boost";
5
+ import { beraFetchJson } from "../../utils/beraFetch";
6
+ import { parseBaseArgs } from "../../utils/parseBaseArgs";
7
+
8
+ /**
9
+ * Companion to `getAutoclaimedIncentives` that fetches the on-chain tx hash
10
+ * for each autoclaimed token. Same nested shape as `/autoclaimed`, but each
11
+ * token leaf is `{ txhash }` where the hash may be absent or empty while the
12
+ * bot is still in flight.
13
+ *
14
+ * Returns `null` on 404 (bot has not run yet for this wallet).
15
+ */
16
+ export async function getAutoclaimedIncentivesTxHash({
17
+ account,
18
+ ...args
19
+ }: {
20
+ account: Address;
21
+ } & BeraJS.BaseFunctionArgs): Promise<AutoclaimedIncentivesTxHashResponse | null> {
22
+ const { config } = parseBaseArgs(args);
23
+
24
+ try {
25
+ return await beraFetchJson<AutoclaimedIncentivesTxHashResponse>(
26
+ {
27
+ url: `${
28
+ config.pol.bribeBoostApi
29
+ }/api/v1/wallets/${account.toLowerCase()}/autoclaimed/txhash`,
30
+ name: "pol-autoclaimed-incentives-txhash",
31
+ type: "rest",
32
+ },
33
+ { cache: "no-store" },
34
+ );
35
+ } catch (error) {
36
+ if (error instanceof RequestError && error.statusCode === 404) {
37
+ return null;
38
+ }
39
+ throw error;
40
+ }
41
+ }
@@ -9,8 +9,6 @@ import { parseBaseArgs } from "../../utils/parseBaseArgs";
9
9
  * Reads `paused()` on the BGT incentive distributor. Used to gate the
10
10
  * autoclaim banner — when `true`, users can no longer self-claim and the
11
11
  * bot is responsible for distributing incentives on their behalf.
12
- *
13
- * Fails closed: any read error returns `false` so the banner stays hidden.
14
12
  */
15
13
  export async function getBgtIncentiveDistributorPaused({
16
14
  publicClient,
@@ -22,14 +20,9 @@ export async function getBgtIncentiveDistributorPaused({
22
20
 
23
21
  const { config } = parseBaseArgs(args);
24
22
 
25
- try {
26
- const paused = await publicClient.readContract({
27
- address: config.pol.bgtIncentiveDistributor,
28
- abi: bgtIncentiveDistributorAbi,
29
- functionName: "paused",
30
- });
31
- return paused;
32
- } catch {
33
- return false;
34
- }
23
+ return publicClient.readContract({
24
+ address: config.pol.bgtIncentiveDistributor,
25
+ abi: bgtIncentiveDistributorAbi,
26
+ functionName: "paused",
27
+ });
35
28
  }
@@ -1,11 +1,8 @@
1
1
  import type { Address } from "viem";
2
2
 
3
- import { getUriFromLink } from "@berachain/config";
4
-
5
- import type { VaultEarningResponse } from "@berachain/graphql/api";
6
-
7
- import { beraFetchJson } from "../../utils/beraFetch";
8
3
  import { parseBaseArgs } from "../../utils/parseBaseArgs";
4
+ import { fetchBeep } from "../clients/fetchBeep";
5
+ import { fetchRailwayBackend } from "../clients/fetchRailwayBackend";
9
6
 
10
7
  type GetEarnedStakedBeraVaultArgs = {
11
8
  /**
@@ -21,17 +18,24 @@ export async function getEarnedStakedBeraVault({
21
18
  address,
22
19
  account,
23
20
  ...args
24
- }: BeraJS.BaseFunctionArgs &
25
- GetEarnedStakedBeraVaultArgs): Promise<VaultEarningResponse> {
21
+ }: BeraJS.BaseFunctionArgs & GetEarnedStakedBeraVaultArgs) {
26
22
  const { config } = parseBaseArgs(args);
27
23
 
28
- const url = `${getUriFromLink(config.backend)}/vaults/${address}/earnings/${account}`;
29
- return beraFetchJson<VaultEarningResponse>({
30
- url,
31
- name:
32
- typeof config.backend === "string"
33
- ? "backend-railway"
34
- : config.backend.name,
35
- type: "rest",
36
- });
24
+ // TODO(beep): remove this entire block once `beep` is deployed on every
25
+ // chain. It is the only legacy-Railway-specific code left here; deleting it
26
+ // leaves the `beep` path below as the sole implementation.
27
+ if (!config.beep) {
28
+ return fetchRailwayBackend(
29
+ "/vaults/{vault}/earnings/{owner}",
30
+ { path: { vault: address, owner: account } },
31
+ args,
32
+ );
33
+ }
34
+
35
+ // beep backend (next-gen), typed against `@berachain/graphql/beep`.
36
+ return fetchBeep(
37
+ "/v0/stake/{vault}/earnings/{owner}",
38
+ { path: { vault: address, owner: account } },
39
+ args,
40
+ );
37
41
  }
@@ -87,13 +87,13 @@ export async function getRewardVaults({
87
87
  ...vault,
88
88
  dynamicData: {
89
89
  ...vault.dynamicData,
90
- allTimeReceivedBGTAmount:
91
- vault.dynamicData?.allTimeReceivedBGTAmount ?? "0",
92
- bgtCapturePercentage: vault.dynamicData?.bgtCapturePercentage ?? "0",
90
+ allTimeRewards: vault.dynamicData?.allTimeRewards ?? "0",
91
+ rewardCapturePercentage:
92
+ vault.dynamicData?.rewardCapturePercentage ?? 0,
93
93
  activeIncentivesValueUsd: totalIncentiveInUsdc.toString(),
94
94
  activeIncentivesRateUsd:
95
95
  vault.dynamicData?.activeIncentivesRateUsd ?? "0",
96
- bgtCapturePerBlock: vault.dynamicData?.bgtCapturePerBlock ?? "0",
96
+ rewardCapturePerBlock: vault.dynamicData?.rewardCapturePerBlock ?? 0,
97
97
  },
98
98
  activeIncentives: incentivesArray,
99
99
  } satisfies typeof vault;
@@ -1,11 +1,8 @@
1
1
  import type { Address } from "viem";
2
2
 
3
- import { getUriFromLink } from "@berachain/config";
4
-
5
- import type { VaultStatsByDayResponse } from "@berachain/graphql/api";
6
-
7
- import { beraFetchJson } from "../../utils/beraFetch";
8
3
  import { parseBaseArgs } from "../../utils/parseBaseArgs";
4
+ import { fetchBeep } from "../clients/fetchBeep";
5
+ import { fetchRailwayBackend } from "../clients/fetchRailwayBackend";
9
6
 
10
7
  export async function getStakingDailyAssets({
11
8
  address,
@@ -13,15 +10,22 @@ export async function getStakingDailyAssets({
13
10
  }: {
14
11
  address: Address;
15
12
  range: 30 | 60 | 90;
16
- }): Promise<VaultStatsByDayResponse> {
13
+ }) {
17
14
  const { config } = parseBaseArgs({});
18
- const url = `${getUriFromLink(config.backend)}/vaults/${address}/stats-by-day?days=${range}`;
19
- return beraFetchJson<VaultStatsByDayResponse>({
20
- url,
21
- name:
22
- typeof config.backend === "string"
23
- ? "backend-railway"
24
- : config.backend.name,
25
- type: "rest",
15
+
16
+ // TODO(beep): remove this entire block once `beep` is deployed on every
17
+ // chain. It is the only legacy-Railway-specific code left here; deleting it
18
+ // leaves the `beep` path below as the sole implementation.
19
+ if (!config.beep) {
20
+ return fetchRailwayBackend("/vaults/{vault}/stats-by-day", {
21
+ path: { vault: address },
22
+ query: { days: `${range}` },
23
+ });
24
+ }
25
+
26
+ // beep backend (next-gen), typed against `@berachain/graphql/beep`.
27
+ return fetchBeep("/v0/stake/{vault}/stats-by-day", {
28
+ path: { vault: address },
29
+ query: { days: `${range}` },
26
30
  });
27
31
  }
@@ -51,7 +51,7 @@ export function getValidatorBoostApy({
51
51
 
52
52
  const boostApy =
53
53
  (returnPerBgt *
54
- Number(validator.dynamicData?.lastDayDistributedBGTAmount ?? 0) *
54
+ Number(validator.dynamicData?.lastDayDistributedRewards ?? 0) *
55
55
  365) /
56
56
  (validatorActiveBoostAmount * bgtPrice);
57
57
 
@@ -1,6 +1,7 @@
1
1
  import type { ErrorLike } from "@apollo/client";
2
2
 
3
3
  import { getUriFromLink } from "@berachain/config";
4
+ import { bepolia } from "@berachain/config/bepolia";
4
5
  import { mainnet } from "@berachain/config/mainnet";
5
6
 
6
7
  import { BeraError, type IBeraErrorArgs } from "./BeraError";
@@ -33,12 +34,16 @@ export class RequestError extends BeraError {
33
34
  */
34
35
  private queryRichfulDomains = [
35
36
  mainnet.bex.aggregatorsProxyUrl,
36
- "https://api.fly.trade/aggregator/quote",
37
+ // covers both /aggregator/quote and /aggregator/transaction?quoteId=...
38
+ // (the transaction endpoint's quoteId would otherwise fragment grouping)
39
+ "https://api.fly.trade/aggregator",
37
40
  "https://open-api.openocean.finance/v4/bera/swap",
38
41
  // vercel internal proxy
39
42
  "/api/aggregators?aggregator",
40
43
  // bonder endpoints have date-based query params (e.g. ?start=2025-11-28)
41
44
  mainnet.backend,
45
+ // beep (next-gen backend) on testnet — same date-based bonder query params
46
+ bepolia.beep,
42
47
  ];
43
48
 
44
49
  statusCode: number | undefined;
@@ -208,14 +213,18 @@ export class RequestError extends BeraError {
208
213
  // - have a network error
209
214
  // - have CORS errors
210
215
  // the list is empirically collected from sentry error logs.
216
+ // The trailing "(host)" suffix is optional: some browsers/transports omit
217
+ // it (e.g. bare "Failed to fetch" / "Load failed"). We normalize all of
218
+ // them to a single NETWORK_ERROR reason so the variants group together,
219
+ // falling back to the endpoint url when no host is present in the message.
211
220
  const failedToFetchMatch = message.match(
212
- /^(?:NetworkError when attempting to fetch resource\.|Failed to fetch|Load failed) \(([a-z0-9-.]+)\)$/,
221
+ /^(?:NetworkError when attempting to fetch resource\.|Failed to fetch|Load failed)(?: \(([a-z0-9-.]+)\))?$/,
213
222
  );
214
223
  if (failedToFetchMatch) {
215
224
  return {
216
225
  reason: RequestError.REASON_MAP.NETWORK_ERROR,
217
226
  message: message,
218
- url: failedToFetchMatch[1],
227
+ url: failedToFetchMatch[1] ?? this.endpoint.url,
219
228
  };
220
229
  }
221
230
  }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { RequestError } from "./RequestError.js";
4
+
5
+ describe("RequestError endpoint normalization (grouping)", () => {
6
+ it("strips query params from the fly.trade transaction endpoint so quoteId no longer fragments groups", () => {
7
+ const base = "https://api.fly.trade/aggregator/transaction";
8
+ const a = new RequestError({
9
+ response: undefined,
10
+ endpoint: {
11
+ url: `${base}?quoteId=aaaa-1111&estimateGas=false`,
12
+ type: "rest",
13
+ },
14
+ });
15
+ const b = new RequestError({
16
+ response: undefined,
17
+ endpoint: {
18
+ url: `${base}?quoteId=bbbb-2222&estimateGas=false`,
19
+ type: "rest",
20
+ },
21
+ });
22
+
23
+ expect(a.endpoint.url).toBe(base);
24
+ expect(b.endpoint.url).toBe(base);
25
+ expect(a.endpoint.url).toBe(b.endpoint.url);
26
+ });
27
+ });
28
+
29
+ describe("RequestError network-error normalization (grouping)", () => {
30
+ it.each([
31
+ "Failed to fetch",
32
+ "Load failed",
33
+ "NetworkError when attempting to fetch resource.",
34
+ "Failed to fetch (api.berachain.com)",
35
+ ])("normalizes %s to NETWORK_ERROR reason", (message) => {
36
+ const err = new RequestError({
37
+ response: undefined,
38
+ cause: new TypeError(message),
39
+ endpoint: { url: "https://api.berachain.com", type: "graphql" },
40
+ });
41
+
42
+ expect(err.reason).toBe(RequestError.REASON_MAP.NETWORK_ERROR);
43
+ expect(err.level).toBe("warning");
44
+ });
45
+
46
+ it("does not treat unrelated messages as network errors", () => {
47
+ const err = new RequestError({
48
+ response: undefined,
49
+ cause: new TypeError("Cannot read properties of undefined"),
50
+ endpoint: { url: "https://api.berachain.com", type: "graphql" },
51
+ });
52
+
53
+ expect(err.reason).not.toBe(RequestError.REASON_MAP.NETWORK_ERROR);
54
+ });
55
+ });
@@ -89,6 +89,14 @@ export const errorMsgMap = {
89
89
  errorMSG:
90
90
  "Nonce is too low. Your wallet might have unconfirmed transactions.",
91
91
  },
92
+ REPLACEMENT_UNDERPRICED: {
93
+ // Transient resubmission issue (gas bump too low). Not function-specific —
94
+ // normalizing the raw RPC string to this key collapses the per-function
95
+ // Sentry groups (functionName is still kept as a tag).
96
+ keywords: ["replacement transaction underpriced"],
97
+ errorMSG:
98
+ "Your wallet tried to replace a pending transaction without raising the gas enough. Please wait for the previous transaction to finish, or try again.",
99
+ },
92
100
  GAS_PRICE: {
93
101
  keywords: ["gasLimit"],
94
102
  errorMSG:
@@ -49,6 +49,7 @@ export * from "./honey/useIsBasketModeEnabled";
49
49
  export * from "./honey/usePythLatestPrices";
50
50
  export * from "./perps/usePythUpdateFee";
51
51
  export { useAutoclaimedIncentives } from "./pol/useAutoclaimedIncentives";
52
+ export { useAutoclaimedIncentivesTxHash } from "./pol/useAutoclaimedIncentivesTxHash";
52
53
  export * from "./pol/useBgtAprSimulation";
53
54
  export { useBgtUnstakedBalance } from "./pol/useBgtUnstakedBalance";
54
55
  export { useClaimableFees } from "./pol/useClaimableFees";
@@ -3,7 +3,6 @@ import useSWR from "swr";
3
3
  import { useBeraWallet } from "@berachain/wagmi/hooks";
4
4
 
5
5
  import { getAutoclaimedIncentives } from "../../actions/pol/getAutoclaimedIncentives";
6
- import { BeraError } from "../../errors/BeraError";
7
6
  import type { AutoclaimedIncentivesResponse } from "../../types/bribe-boost";
8
7
  import type { DefaultHookReturnType } from "../../types/global";
9
8
 
@@ -26,15 +25,7 @@ export function useAutoclaimedIncentives({
26
25
 
27
26
  const swrResponse = useSWR(
28
27
  QUERY_KEY,
29
- async ([, account]) => {
30
- if (!account) {
31
- throw new BeraError({
32
- message: "useAutoclaimedIncentives needs a logged in account",
33
- level: "error",
34
- });
35
- }
36
- return await getAutoclaimedIncentives({ account });
37
- },
28
+ async ([, account]) => getAutoclaimedIncentives({ account }),
38
29
  {
39
30
  revalidateOnFocus: false,
40
31
  revalidateOnReconnect: false,
@@ -0,0 +1,45 @@
1
+ import useSWR from "swr";
2
+
3
+ import { useBeraWallet } from "@berachain/wagmi/hooks";
4
+
5
+ import { getAutoclaimedIncentivesTxHash } from "../../actions/pol/getAutoclaimedIncentivesTxHash";
6
+ import { POLLING } from "../../enum/polling";
7
+ import type { AutoclaimedIncentivesTxHashResponse } from "../../types/bribe-boost";
8
+ import type { DefaultHookReturnType } from "../../types/global";
9
+
10
+ interface UseAutoclaimedIncentivesTxHashArgs {
11
+ // Hard gate, identical to `useAutoclaimedIncentives` — only flip true once
12
+ // both `!isBgtPolRewardToken` and `bgtIncentiveDistributor.paused()` hold.
13
+ enabled: boolean;
14
+ }
15
+
16
+ export function useAutoclaimedIncentivesTxHash({
17
+ enabled,
18
+ }: UseAutoclaimedIncentivesTxHashArgs): DefaultHookReturnType<AutoclaimedIncentivesTxHashResponse | null> {
19
+ const { address: account } = useBeraWallet();
20
+
21
+ const QUERY_KEY =
22
+ enabled && account
23
+ ? (["useAutoclaimedIncentivesTxHash", account] as const)
24
+ : null;
25
+
26
+ const swrResponse = useSWR(
27
+ QUERY_KEY,
28
+ async ([, account]) => getAutoclaimedIncentivesTxHash({ account }),
29
+ {
30
+ // txhashes flip from absent → present as the bot processes each token,
31
+ // so we need to actually poll. Cadence matches the bot's tx cadence
32
+ // (minutes, not seconds) and lets the user watch pending → claimed
33
+ // transitions without reloading the page.
34
+ refreshInterval: POLLING.NORMAL,
35
+ revalidateOnFocus: true,
36
+ revalidateOnReconnect: true,
37
+ revalidateIfStale: false,
38
+ },
39
+ );
40
+
41
+ return {
42
+ ...swrResponse,
43
+ refresh: () => swrResponse?.mutate?.(),
44
+ };
45
+ }
@@ -40,12 +40,8 @@ export function useValidator(
40
40
  "",
41
41
  queuedBoostAmount:
42
42
  indexerValidator?.dynamicData?.queuedBoostAmount ?? "",
43
- usersActiveBoostCount:
44
- indexerValidator?.dynamicData?.usersActiveBoostCount ?? 0,
45
- usersQueuedBoostCount:
46
- indexerValidator?.dynamicData?.usersQueuedBoostCount ?? 0,
47
- allTimeDistributedBGTAmount:
48
- indexerValidator?.dynamicData?.allTimeDistributedBGTAmount ??
43
+ allTimeDistributedRewards:
44
+ indexerValidator?.dynamicData?.allTimeDistributedRewards ??
49
45
  "0",
50
46
  rewardRate:
51
47
  onChainValidator?.dynamicData?.rewardRate ??
@@ -53,8 +49,8 @@ export function useValidator(
53
49
  "",
54
50
  stakedBeraAmount:
55
51
  indexerValidator?.dynamicData?.stakedBeraAmount ?? "",
56
- lastDayDistributedBGTAmount:
57
- indexerValidator?.dynamicData?.lastDayDistributedBGTAmount ??
52
+ lastDayDistributedRewards:
53
+ indexerValidator?.dynamicData?.lastDayDistributedRewards ??
58
54
  "",
59
55
  lastDayProposedBlockCount:
60
56
  indexerValidator?.dynamicData?.lastDayProposedBlockCount ?? 0,
@@ -33,11 +33,22 @@ export interface BribeBoostRewardProof {
33
33
  }
34
34
 
35
35
  // Response shape of GET /api/v1/wallets/:wallet/autoclaimed:
36
- // { [wallet]: { [validatorPubkey]: { [tokenAddress]: rawAmount (stringified bigint) } } }
37
- // Top-level wallet key is the lowercase wallet address that was queried.
36
+ // { [wallet]: { [validatorPubkey]: { [tokenAddress]: { amount } } } }
37
+ // `amount` is a stringified bigint in token base units. The backend now
38
+ // aggregates across validators under a single fake pubkey (`0x000…000`),
39
+ // so consumers can collapse the validator dimension blindly.
38
40
  export type AutoclaimedIncentivesResponse = Record<
39
41
  Address,
40
- Record<Address, Record<Address, string>>
42
+ Record<Address, Record<Address, { amount: string }>>
43
+ >;
44
+
45
+ // Response shape of GET /api/v1/wallets/:wallet/autoclaimed/txhash: same
46
+ // nesting, but each token leaf is `{ txhash }`. Tokens still pending are
47
+ // simply absent — when a token from `/autoclaimed` has no entry here, the
48
+ // bot hasn't distributed it yet.
49
+ export type AutoclaimedIncentivesTxHashResponse = Record<
50
+ Address,
51
+ Record<Address, Record<Address, { txhash: string }>>
41
52
  >;
42
53
 
43
54
  export interface BribeBoostValidatorApr {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Polyfill for `AbortSignal.any`.
3
+ *
4
+ * `AbortSignal.any` ships in Chrome 116+, Safari 17.4+, Firefox 124+. Older
5
+ * environments — notably Android WebViews bundled with old Chromium (e.g.
6
+ * Chrome Mobile WebView 114) — lack it, and third-party libraries that call it
7
+ * (e.g. @pythnetwork/hermes-client) throw "AbortSignal.any is not a function".
8
+ *
9
+ * This installs a spec-compatible-enough fallback: it returns a controller's
10
+ * signal that aborts as soon as any of the input signals abort (propagating the
11
+ * abort reason). It is idempotent and a no-op when the native API exists.
12
+ */
13
+ export function polyfillAbortSignalAny(): void {
14
+ if (typeof AbortSignal === "undefined") return;
15
+ if (typeof (AbortSignal as { any?: unknown }).any === "function") return;
16
+
17
+ (
18
+ AbortSignal as unknown as { any: (signals: AbortSignal[]) => AbortSignal }
19
+ ).any = (signals: AbortSignal[]): AbortSignal => {
20
+ const controller = new AbortController();
21
+
22
+ // Phase 1: if any signal is already aborted, short-circuit before attaching
23
+ // any listeners (attaching to the others would leave dangling listeners
24
+ // that never fire — the leak the WeakRef/FinalizationRegistry versions guard
25
+ // against; checking first avoids it without that machinery).
26
+ for (const signal of signals) {
27
+ if (signal.aborted) {
28
+ controller.abort(signal.reason);
29
+ return controller.signal;
30
+ }
31
+ }
32
+
33
+ // Phase 2: forward the first abort, then detach every listener so none
34
+ // outlive the combined signal. Safe for our short-lived usage (per-request
35
+ // fetch signals that settle and are collected together).
36
+ const listeners: Array<() => void> = [];
37
+ const cleanup = () => {
38
+ for (let i = 0; i < signals.length; i++) {
39
+ signals[i].removeEventListener("abort", listeners[i]);
40
+ }
41
+ };
42
+ for (const signal of signals) {
43
+ const onAbort = () => {
44
+ if (!controller.signal.aborted) controller.abort(signal.reason);
45
+ cleanup();
46
+ };
47
+ listeners.push(onAbort);
48
+ signal.addEventListener("abort", onAbort);
49
+ }
50
+
51
+ return controller.signal;
52
+ };
53
+ }
@@ -0,0 +1,81 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+
3
+ import { polyfillAbortSignalAny } from "./polyfillAbortSignalAny.js";
4
+
5
+ describe("polyfillAbortSignalAny", () => {
6
+ const original = (AbortSignal as { any?: unknown }).any;
7
+
8
+ afterEach(() => {
9
+ // restore whatever the runtime had (native or undefined)
10
+ (AbortSignal as { any?: unknown }).any = original;
11
+ });
12
+
13
+ it("does not overwrite a native AbortSignal.any", () => {
14
+ const native = () => new AbortController().signal;
15
+ (AbortSignal as { any?: unknown }).any = native;
16
+ polyfillAbortSignalAny();
17
+ expect((AbortSignal as { any?: unknown }).any).toBe(native);
18
+ });
19
+
20
+ it("installs a fallback when AbortSignal.any is missing", () => {
21
+ (AbortSignal as { any?: unknown }).any = undefined;
22
+ polyfillAbortSignalAny();
23
+ expect(typeof (AbortSignal as { any?: unknown }).any).toBe("function");
24
+ });
25
+
26
+ it("aborts the combined signal when one input aborts", () => {
27
+ (AbortSignal as { any?: unknown }).any = undefined;
28
+ polyfillAbortSignalAny();
29
+
30
+ const a = new AbortController();
31
+ const b = new AbortController();
32
+ const combined = (
33
+ AbortSignal as unknown as { any: (s: AbortSignal[]) => AbortSignal }
34
+ ).any([a.signal, b.signal]);
35
+
36
+ expect(combined.aborted).toBe(false);
37
+ b.abort("boom");
38
+ expect(combined.aborted).toBe(true);
39
+ expect(combined.reason).toBe("boom");
40
+ });
41
+
42
+ it("removes listeners from all inputs once the combined signal aborts", () => {
43
+ (AbortSignal as { any?: unknown }).any = undefined;
44
+ polyfillAbortSignalAny();
45
+
46
+ const a = new AbortController();
47
+ const b = new AbortController();
48
+ const removed: AbortSignal[] = [];
49
+ for (const c of [a, b]) {
50
+ const orig = c.signal.removeEventListener.bind(c.signal);
51
+ c.signal.removeEventListener = ((...args: Parameters<typeof orig>) => {
52
+ removed.push(c.signal);
53
+ return orig(...args);
54
+ }) as typeof orig;
55
+ }
56
+
57
+ (AbortSignal as unknown as { any: (s: AbortSignal[]) => AbortSignal }).any([
58
+ a.signal,
59
+ b.signal,
60
+ ]);
61
+
62
+ a.abort("x");
63
+ // both inputs should have had their abort listener detached, not just the firing one
64
+ expect(removed).toContain(a.signal);
65
+ expect(removed).toContain(b.signal);
66
+ });
67
+
68
+ it("is already aborted if an input is already aborted", () => {
69
+ (AbortSignal as { any?: unknown }).any = undefined;
70
+ polyfillAbortSignalAny();
71
+
72
+ const a = new AbortController();
73
+ a.abort("early");
74
+ const combined = (
75
+ AbortSignal as unknown as { any: (s: AbortSignal[]) => AbortSignal }
76
+ ).any([a.signal]);
77
+
78
+ expect(combined.aborted).toBe(true);
79
+ expect(combined.reason).toBe("early");
80
+ });
81
+ });