@gearbox-protocol/sdk 17.1.1 → 17.2.0

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.
@@ -69,6 +69,7 @@ const chains = {
69
69
  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "ETH",
70
70
  "0x18084fbA666a33d37592fA2633fD49a74DD93a88": "BTC",
71
71
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": "Stable",
72
+ "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29": "Stable",
72
73
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": "BTC"
73
74
  }),
74
75
  rwaTokens: new require_onchain_utils_AddressSet.AddressSet([
@@ -23,5 +23,16 @@ function describe(cause) {
23
23
  if (cause instanceof Error) return cause.name === "TimeoutError" ? "timed out" : cause.message;
24
24
  return String(cause);
25
25
  }
26
+ /** Turtle answers for every chain at once, so this names no chain. */
27
+ var TurtleRequestFailedError = class extends viem.BaseError {
28
+ name = "TurtleRequestFailedError";
29
+ constructor(path, cause) {
30
+ super("Turtle could not be reached.", {
31
+ cause: cause instanceof Error ? cause : void 0,
32
+ metaMessages: [`${path} — ${describe(cause)}`]
33
+ });
34
+ }
35
+ };
26
36
  //#endregion
27
37
  exports.MerklRequestFailedError = MerklRequestFailedError;
38
+ exports.TurtleRequestFailedError = TurtleRequestFailedError;
@@ -1,7 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rewards_errors = require("./errors.js");
3
- const require_rewards_toMerklRewards = require("./toMerklRewards.js");
4
3
  const require_rewards_multichain = require("./multichain.js");
5
4
  exports.MerklRequestFailedError = require_rewards_errors.MerklRequestFailedError;
6
- exports.getMerklRewardsMultichain = require_rewards_multichain.getMerklRewardsMultichain;
7
- exports.toMerklRewards = require_rewards_toMerklRewards.toMerklRewards;
5
+ exports.TurtleRequestFailedError = require_rewards_errors.TurtleRequestFailedError;
6
+ exports.getRewardsMultichain = require_rewards_multichain.getRewardsMultichain;
@@ -3,38 +3,58 @@ const require_onchain_base_MultichainConstruct = require("../onchain/base/Multic
3
3
  require("../onchain/index.js");
4
4
  const require_rewards_merkl_api = require("./merkl-api.js");
5
5
  const require_rewards_toMerklRewards = require("./toMerklRewards.js");
6
+ const require_rewards_toTurtleRewards = require("./toTurtleRewards.js");
7
+ const require_rewards_turtle_api = require("./turtle-api.js");
6
8
  let viem = require("viem");
7
9
  //#region src/rewards/multichain.ts
8
- /**
9
- * The fan-out itself. Private because rewards are not an SDK namespace yet:
10
- * the read is a free function, and this is only how it reaches `queryChains`.
11
- **/
12
- var MerklRewardsFanOut = class extends require_onchain_base_MultichainConstruct.MultichainConstruct {
13
- async list(wallet, chainIds, apiKey) {
10
+ var RewardsFanOut = class extends require_onchain_base_MultichainConstruct.MultichainConstruct {
11
+ async list({ wallet, chainIds, merklApiKey, turtleApiKey }) {
14
12
  const user = (0, viem.getAddress)(wallet);
13
+ const turtle = turtleApiKey ? require_rewards_turtle_api.fetchTurtleWalletRewards(user, turtleApiKey) : void 0;
14
+ turtle?.catch(() => {});
15
15
  return this.queryChains({
16
16
  chainIds,
17
17
  label: "list rewards",
18
18
  block: "state",
19
- run: async (sdk) => require_rewards_toMerklRewards.toMerklRewards(sdk, await require_rewards_merkl_api.fetchMerklUserRewards({
20
- chainId: sdk.chainId,
21
- user,
22
- apiKey
23
- }))
19
+ run: async (sdk, block) => {
20
+ const sources = [require_rewards_merkl_api.fetchMerklUserRewards({
21
+ chainId: sdk.chainId,
22
+ user,
23
+ apiKey: merklApiKey
24
+ }).then((response) => require_rewards_toMerklRewards.toMerklRewards(sdk, response))];
25
+ if (turtle) sources.push(turtle.then(async (rewards) => require_rewards_toTurtleRewards.toTurtleRewards(sdk, rewards, await readClaimed(sdk, user, rewards, block.blockNumber))));
26
+ const settled = await Promise.allSettled(sources);
27
+ const failed = settled.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
28
+ if (failed.length === settled.length) throw failed.length === 1 ? failed[0] : new AggregateError(failed, "no rewards source answered");
29
+ for (const reason of failed) (sdk.logger ?? this.sdk.logger)?.warn(reason, `rewards source failed on chain ${sdk.chainId}`);
30
+ return settled.flatMap((r) => r.status === "fulfilled" ? r.value : []);
31
+ }
24
32
  });
25
33
  }
26
34
  };
35
+ async function readClaimed(sdk, user, { proofs }, blockNumber) {
36
+ const onChain = proofs.filter((p) => p.chainId === sdk.chainId);
37
+ if (onChain.length === 0) return /* @__PURE__ */ new Map();
38
+ const claimed = await sdk.client.multicall({
39
+ contracts: onChain.map((p) => ({
40
+ address: p.contractAddress,
41
+ abi: require_rewards_turtle_api.turtleStreamAbi,
42
+ functionName: "getClaimedRewards",
43
+ args: [user]
44
+ })),
45
+ allowFailure: false,
46
+ blockNumber
47
+ });
48
+ return new Map(onChain.map((p, i) => [p.streamId, claimed[i]]));
49
+ }
27
50
  /**
28
- * Every claimable Merkl reward a wallet holds, across the chains the handle
29
- * carries.
30
- *
31
- * Answers the read model's own envelope, so a chain that could not be reached
32
- * is `status: "error"` in `meta.chains` while a chain with nothing to claim is
33
- * a `"success"` that contributed no rows. That distinction is the point: the
34
- * single-chain read this replaces resolved empty either way.
51
+ * Every claimable reward a wallet holds — Merkl campaigns and the Gearbox
52
+ * organisation's Turtle streams — across the chains the handle carries.
53
+ * A chain is `status: "error"` only when every source failed on it; a chain
54
+ * with nothing to claim is a `"success"` with no rows.
35
55
  **/
36
- async function getMerklRewardsMultichain({ sdk, wallet, chainIds, apiKey }) {
37
- return new MerklRewardsFanOut(sdk).list(wallet, chainIds, apiKey);
56
+ async function getRewardsMultichain({ sdk, ...props }) {
57
+ return new RewardsFanOut(sdk).list(props);
38
58
  }
39
59
  //#endregion
40
- exports.getMerklRewardsMultichain = getMerklRewardsMultichain;
60
+ exports.getRewardsMultichain = getRewardsMultichain;
@@ -60,6 +60,7 @@ function toAmounts(reason) {
60
60
  function toReward({ price, token, value, ...rest }) {
61
61
  return {
62
62
  ...rest,
63
+ source: "merkl",
63
64
  amount: {
64
65
  token,
65
66
  value,
@@ -68,18 +69,18 @@ function toReward({ price, token, value, ...rest }) {
68
69
  };
69
70
  }
70
71
  /**
71
- * A campaign's incentive token is not protocol collateral, so the registry
72
- * usually has no entry for it — and Merkl always names it. The one place the
73
- * two sources are reconciled.
72
+ * An incentive token is rarely protocol collateral, so the registry usually
73
+ * has no entry for it — and both sources always name it.
74
74
  */
75
- function toRewardToken(sdk, address, merkl) {
75
+ function toRewardToken(sdk, address, named) {
76
76
  return sdk.tokensMeta.getToken(address) ?? {
77
77
  chainId: sdk.chainId,
78
78
  address,
79
- symbol: merkl.symbol,
80
- name: merkl.symbol,
81
- decimals: merkl.decimals || 18
79
+ symbol: named.symbol,
80
+ name: named.name ?? named.symbol,
81
+ decimals: named.decimals || 18
82
82
  };
83
83
  }
84
84
  //#endregion
85
85
  exports.toMerklRewards = toMerklRewards;
86
+ exports.toRewardToken = toRewardToken;
@@ -0,0 +1,77 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_onchain_utils_AddressMap = require("../onchain/utils/AddressMap.js");
3
+ const require_onchain_utils_bigint_math = require("../onchain/utils/bigint-math.js");
4
+ const require_onchain_utils_formatter = require("../onchain/utils/formatter.js");
5
+ require("../onchain/index.js");
6
+ const require_rewards_toMerklRewards = require("./toMerklRewards.js");
7
+ let viem = require("viem");
8
+ //#region src/rewards/toTurtleRewards.ts
9
+ /**
10
+ * Rows for the streams targeting a pool on the SDK's chain, merged per pool
11
+ * and incentive. A token row is what is committed and not yet claimed.
12
+ */
13
+ function toTurtleRewards(sdk, { streams, proofs }, claimed) {
14
+ const pools = require_onchain_utils_AddressMap.AddressMap.fromMappedArray(sdk.marketRegister.pools.map(({ pool }) => pool.address), (address) => address);
15
+ const proofByStream = new Map(proofs.map((p) => [p.streamId, p]));
16
+ const rows = /* @__PURE__ */ new Map();
17
+ for (const { streamId, snapshots, stream } of streams) {
18
+ const target = stream.customArgs.targetToken;
19
+ if (!target || Number(target.chain.chainId) !== sdk.chainId) continue;
20
+ if (!(0, viem.isAddress)(target.address, { strict: false })) continue;
21
+ const pool = pools.get(target.address);
22
+ const poolToken = pool && sdk.tokensMeta.getToken(pool);
23
+ if (!pool || !poolToken) continue;
24
+ const base = {
25
+ source: "turtle",
26
+ chainId: sdk.chainId,
27
+ pool,
28
+ poolToken
29
+ };
30
+ const { point, rewardToken } = stream;
31
+ if (point) {
32
+ const value = parse(snapshots.at(-1)?.rewardsAccumulated);
33
+ if (!value) continue;
34
+ const key = `${pool}_${point.id}`;
35
+ const seen = rows.get(key);
36
+ rows.set(key, {
37
+ ...base,
38
+ points: {
39
+ id: point.id,
40
+ name: point.name,
41
+ multiplier: null,
42
+ value: (seen?.points.value ?? 0) + Number((0, viem.formatUnits)(value, point.decimals))
43
+ }
44
+ });
45
+ continue;
46
+ }
47
+ if (!rewardToken || !(0, viem.isAddress)(rewardToken.address, { strict: false })) continue;
48
+ const committed = parse(proofByStream.get(streamId)?.amount);
49
+ const done = claimed.get(streamId);
50
+ if (committed === void 0 || done === void 0) continue;
51
+ const value = require_onchain_utils_bigint_math.BigIntMath.max(committed - done, 0n);
52
+ if (value === 0n) continue;
53
+ const token = require_rewards_toMerklRewards.toRewardToken(sdk, (0, viem.getAddress)(rewardToken.address), rewardToken);
54
+ const key = `${pool}_${token.address}`;
55
+ const total = (rows.get(key)?.amount.value ?? 0n) + value;
56
+ const price = Number(stream.lastSnapshot?.rewardTokenPrice ?? NaN);
57
+ rows.set(key, {
58
+ ...base,
59
+ amount: {
60
+ token,
61
+ value: total,
62
+ valueUsd: Number.isFinite(price) ? Number((0, viem.formatUnits)(total, token.decimals)) * price : null
63
+ }
64
+ });
65
+ }
66
+ return [...rows.values()];
67
+ }
68
+ function parse(amount) {
69
+ if (amount === void 0) return void 0;
70
+ try {
71
+ return require_onchain_utils_formatter.toBigInt(amount);
72
+ } catch {
73
+ return;
74
+ }
75
+ }
76
+ //#endregion
77
+ exports.toTurtleRewards = toTurtleRewards;
@@ -0,0 +1,50 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rewards_errors = require("./errors.js");
3
+ //#region src/rewards/turtle-api.ts
4
+ const TURTLE_API_URL = "https://earn.turtle.xyz";
5
+ const GEARBOX_ORG_ID = "d171ba3d-ff89-4e6c-8f13-d94840b06edd";
6
+ const TIMEOUT = 1e4;
7
+ const turtleStreamAbi = [{
8
+ type: "function",
9
+ name: "getClaimedRewards",
10
+ stateMutability: "view",
11
+ inputs: [{
12
+ name: "user",
13
+ type: "address"
14
+ }],
15
+ outputs: [{
16
+ name: "",
17
+ type: "uint256"
18
+ }]
19
+ }];
20
+ async function fetchTurtleWalletRewards(user, apiKey) {
21
+ const { streams: all } = await get(`/v2/streams/wallets/${user}`, apiKey);
22
+ const streams = all.filter((s) => s.stream.orgId === GEARBOX_ORG_ID);
23
+ const tokenStreamIds = streams.filter((s) => s.stream.rewardToken && s.stream.contractAddress).map((s) => s.streamId);
24
+ if (tokenStreamIds.length === 0) return {
25
+ streams,
26
+ proofs: []
27
+ };
28
+ const query = new URLSearchParams({ wallet: user });
29
+ for (const id of tokenStreamIds) query.append("streamIds", id);
30
+ const { proofs } = await get(`/v2/streams/merkle_proofs?${query}`, apiKey);
31
+ return {
32
+ streams,
33
+ proofs
34
+ };
35
+ }
36
+ async function get(path, apiKey) {
37
+ try {
38
+ const response = await fetch(`${TURTLE_API_URL}${path}`, {
39
+ headers: { "X-API-Key": apiKey },
40
+ signal: AbortSignal.timeout(TIMEOUT)
41
+ });
42
+ if (!response.ok) throw new Error(`answered ${response.status}`);
43
+ return await response.json();
44
+ } catch (error) {
45
+ throw new require_rewards_errors.TurtleRequestFailedError(path, error);
46
+ }
47
+ }
48
+ //#endregion
49
+ exports.fetchTurtleWalletRewards = fetchTurtleWalletRewards;
50
+ exports.turtleStreamAbi = turtleStreamAbi;
@@ -68,6 +68,7 @@ const chains = {
68
68
  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "ETH",
69
69
  "0x18084fbA666a33d37592fA2633fD49a74DD93a88": "BTC",
70
70
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": "Stable",
71
+ "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29": "Stable",
71
72
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": "BTC"
72
73
  }),
73
74
  rwaTokens: new AddressSet([
@@ -22,5 +22,15 @@ function describe(cause) {
22
22
  if (cause instanceof Error) return cause.name === "TimeoutError" ? "timed out" : cause.message;
23
23
  return String(cause);
24
24
  }
25
+ /** Turtle answers for every chain at once, so this names no chain. */
26
+ var TurtleRequestFailedError = class extends BaseError {
27
+ name = "TurtleRequestFailedError";
28
+ constructor(path, cause) {
29
+ super("Turtle could not be reached.", {
30
+ cause: cause instanceof Error ? cause : void 0,
31
+ metaMessages: [`${path} — ${describe(cause)}`]
32
+ });
33
+ }
34
+ };
25
35
  //#endregion
26
- export { MerklRequestFailedError };
36
+ export { MerklRequestFailedError, TurtleRequestFailedError };
@@ -1,4 +1,3 @@
1
- import { MerklRequestFailedError } from "./errors.js";
2
- import { toMerklRewards } from "./toMerklRewards.js";
3
- import { getMerklRewardsMultichain } from "./multichain.js";
4
- export { MerklRequestFailedError, getMerklRewardsMultichain, toMerklRewards };
1
+ import { MerklRequestFailedError, TurtleRequestFailedError } from "./errors.js";
2
+ import { getRewardsMultichain } from "./multichain.js";
3
+ export { MerklRequestFailedError, TurtleRequestFailedError, getRewardsMultichain };
@@ -2,38 +2,58 @@ import { MultichainConstruct } from "../onchain/base/MultichainConstruct.js";
2
2
  import "../onchain/index.js";
3
3
  import { fetchMerklUserRewards } from "./merkl-api.js";
4
4
  import { toMerklRewards } from "./toMerklRewards.js";
5
+ import { toTurtleRewards } from "./toTurtleRewards.js";
6
+ import { fetchTurtleWalletRewards, turtleStreamAbi } from "./turtle-api.js";
5
7
  import { getAddress } from "viem";
6
8
  //#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) {
9
+ var RewardsFanOut = class extends MultichainConstruct {
10
+ async list({ wallet, chainIds, merklApiKey, turtleApiKey }) {
13
11
  const user = getAddress(wallet);
12
+ const turtle = turtleApiKey ? fetchTurtleWalletRewards(user, turtleApiKey) : void 0;
13
+ turtle?.catch(() => {});
14
14
  return this.queryChains({
15
15
  chainIds,
16
16
  label: "list rewards",
17
17
  block: "state",
18
- run: async (sdk) => toMerklRewards(sdk, await fetchMerklUserRewards({
19
- chainId: sdk.chainId,
20
- user,
21
- apiKey
22
- }))
18
+ run: async (sdk, block) => {
19
+ const sources = [fetchMerklUserRewards({
20
+ chainId: sdk.chainId,
21
+ user,
22
+ apiKey: merklApiKey
23
+ }).then((response) => toMerklRewards(sdk, response))];
24
+ if (turtle) sources.push(turtle.then(async (rewards) => toTurtleRewards(sdk, rewards, await readClaimed(sdk, user, rewards, block.blockNumber))));
25
+ const settled = await Promise.allSettled(sources);
26
+ const failed = settled.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
27
+ if (failed.length === settled.length) throw failed.length === 1 ? failed[0] : new AggregateError(failed, "no rewards source answered");
28
+ for (const reason of failed) (sdk.logger ?? this.sdk.logger)?.warn(reason, `rewards source failed on chain ${sdk.chainId}`);
29
+ return settled.flatMap((r) => r.status === "fulfilled" ? r.value : []);
30
+ }
23
31
  });
24
32
  }
25
33
  };
34
+ async function readClaimed(sdk, user, { proofs }, blockNumber) {
35
+ const onChain = proofs.filter((p) => p.chainId === sdk.chainId);
36
+ if (onChain.length === 0) return /* @__PURE__ */ new Map();
37
+ const claimed = await sdk.client.multicall({
38
+ contracts: onChain.map((p) => ({
39
+ address: p.contractAddress,
40
+ abi: turtleStreamAbi,
41
+ functionName: "getClaimedRewards",
42
+ args: [user]
43
+ })),
44
+ allowFailure: false,
45
+ blockNumber
46
+ });
47
+ return new Map(onChain.map((p, i) => [p.streamId, claimed[i]]));
48
+ }
26
49
  /**
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.
50
+ * Every claimable reward a wallet holds — Merkl campaigns and the Gearbox
51
+ * organisation's Turtle streams — across the chains the handle carries.
52
+ * A chain is `status: "error"` only when every source failed on it; a chain
53
+ * with nothing to claim is a `"success"` with no rows.
34
54
  **/
35
- async function getMerklRewardsMultichain({ sdk, wallet, chainIds, apiKey }) {
36
- return new MerklRewardsFanOut(sdk).list(wallet, chainIds, apiKey);
55
+ async function getRewardsMultichain({ sdk, ...props }) {
56
+ return new RewardsFanOut(sdk).list(props);
37
57
  }
38
58
  //#endregion
39
- export { getMerklRewardsMultichain };
59
+ export { getRewardsMultichain };
@@ -59,6 +59,7 @@ function toAmounts(reason) {
59
59
  function toReward({ price, token, value, ...rest }) {
60
60
  return {
61
61
  ...rest,
62
+ source: "merkl",
62
63
  amount: {
63
64
  token,
64
65
  value,
@@ -67,18 +68,17 @@ function toReward({ price, token, value, ...rest }) {
67
68
  };
68
69
  }
69
70
  /**
70
- * A campaign's incentive token is not protocol collateral, so the registry
71
- * usually has no entry for it — and Merkl always names it. The one place the
72
- * two sources are reconciled.
71
+ * An incentive token is rarely protocol collateral, so the registry usually
72
+ * has no entry for it — and both sources always name it.
73
73
  */
74
- function toRewardToken(sdk, address, merkl) {
74
+ function toRewardToken(sdk, address, named) {
75
75
  return sdk.tokensMeta.getToken(address) ?? {
76
76
  chainId: sdk.chainId,
77
77
  address,
78
- symbol: merkl.symbol,
79
- name: merkl.symbol,
80
- decimals: merkl.decimals || 18
78
+ symbol: named.symbol,
79
+ name: named.name ?? named.symbol,
80
+ decimals: named.decimals || 18
81
81
  };
82
82
  }
83
83
  //#endregion
84
- export { toMerklRewards };
84
+ export { toMerklRewards, toRewardToken };
@@ -0,0 +1,76 @@
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 { toRewardToken } from "./toMerklRewards.js";
6
+ import { formatUnits, getAddress, isAddress } from "viem";
7
+ //#region src/rewards/toTurtleRewards.ts
8
+ /**
9
+ * Rows for the streams targeting a pool on the SDK's chain, merged per pool
10
+ * and incentive. A token row is what is committed and not yet claimed.
11
+ */
12
+ function toTurtleRewards(sdk, { streams, proofs }, claimed) {
13
+ const pools = AddressMap.fromMappedArray(sdk.marketRegister.pools.map(({ pool }) => pool.address), (address) => address);
14
+ const proofByStream = new Map(proofs.map((p) => [p.streamId, p]));
15
+ const rows = /* @__PURE__ */ new Map();
16
+ for (const { streamId, snapshots, stream } of streams) {
17
+ const target = stream.customArgs.targetToken;
18
+ if (!target || Number(target.chain.chainId) !== sdk.chainId) continue;
19
+ if (!isAddress(target.address, { strict: false })) continue;
20
+ const pool = pools.get(target.address);
21
+ const poolToken = pool && sdk.tokensMeta.getToken(pool);
22
+ if (!pool || !poolToken) continue;
23
+ const base = {
24
+ source: "turtle",
25
+ chainId: sdk.chainId,
26
+ pool,
27
+ poolToken
28
+ };
29
+ const { point, rewardToken } = stream;
30
+ if (point) {
31
+ const value = parse(snapshots.at(-1)?.rewardsAccumulated);
32
+ if (!value) continue;
33
+ const key = `${pool}_${point.id}`;
34
+ const seen = rows.get(key);
35
+ rows.set(key, {
36
+ ...base,
37
+ points: {
38
+ id: point.id,
39
+ name: point.name,
40
+ multiplier: null,
41
+ value: (seen?.points.value ?? 0) + Number(formatUnits(value, point.decimals))
42
+ }
43
+ });
44
+ continue;
45
+ }
46
+ if (!rewardToken || !isAddress(rewardToken.address, { strict: false })) continue;
47
+ const committed = parse(proofByStream.get(streamId)?.amount);
48
+ const done = claimed.get(streamId);
49
+ if (committed === void 0 || done === void 0) continue;
50
+ const value = BigIntMath.max(committed - done, 0n);
51
+ if (value === 0n) continue;
52
+ const token = toRewardToken(sdk, getAddress(rewardToken.address), rewardToken);
53
+ const key = `${pool}_${token.address}`;
54
+ const total = (rows.get(key)?.amount.value ?? 0n) + value;
55
+ const price = Number(stream.lastSnapshot?.rewardTokenPrice ?? NaN);
56
+ rows.set(key, {
57
+ ...base,
58
+ amount: {
59
+ token,
60
+ value: total,
61
+ valueUsd: Number.isFinite(price) ? Number(formatUnits(total, token.decimals)) * price : null
62
+ }
63
+ });
64
+ }
65
+ return [...rows.values()];
66
+ }
67
+ function parse(amount) {
68
+ if (amount === void 0) return void 0;
69
+ try {
70
+ return toBigInt(amount);
71
+ } catch {
72
+ return;
73
+ }
74
+ }
75
+ //#endregion
76
+ export { toTurtleRewards };
@@ -0,0 +1,48 @@
1
+ import { TurtleRequestFailedError } from "./errors.js";
2
+ //#region src/rewards/turtle-api.ts
3
+ const TURTLE_API_URL = "https://earn.turtle.xyz";
4
+ const GEARBOX_ORG_ID = "d171ba3d-ff89-4e6c-8f13-d94840b06edd";
5
+ const TIMEOUT = 1e4;
6
+ const turtleStreamAbi = [{
7
+ type: "function",
8
+ name: "getClaimedRewards",
9
+ stateMutability: "view",
10
+ inputs: [{
11
+ name: "user",
12
+ type: "address"
13
+ }],
14
+ outputs: [{
15
+ name: "",
16
+ type: "uint256"
17
+ }]
18
+ }];
19
+ async function fetchTurtleWalletRewards(user, apiKey) {
20
+ const { streams: all } = await get(`/v2/streams/wallets/${user}`, apiKey);
21
+ const streams = all.filter((s) => s.stream.orgId === GEARBOX_ORG_ID);
22
+ const tokenStreamIds = streams.filter((s) => s.stream.rewardToken && s.stream.contractAddress).map((s) => s.streamId);
23
+ if (tokenStreamIds.length === 0) return {
24
+ streams,
25
+ proofs: []
26
+ };
27
+ const query = new URLSearchParams({ wallet: user });
28
+ for (const id of tokenStreamIds) query.append("streamIds", id);
29
+ const { proofs } = await get(`/v2/streams/merkle_proofs?${query}`, apiKey);
30
+ return {
31
+ streams,
32
+ proofs
33
+ };
34
+ }
35
+ async function get(path, apiKey) {
36
+ try {
37
+ const response = await fetch(`${TURTLE_API_URL}${path}`, {
38
+ headers: { "X-API-Key": apiKey },
39
+ signal: AbortSignal.timeout(TIMEOUT)
40
+ });
41
+ if (!response.ok) throw new Error(`answered ${response.status}`);
42
+ return await response.json();
43
+ } catch (error) {
44
+ throw new TurtleRequestFailedError(path, error);
45
+ }
46
+ }
47
+ //#endregion
48
+ export { fetchTurtleWalletRewards, turtleStreamAbi };
@@ -14,5 +14,10 @@ declare class MerklRequestFailedError extends BaseError {
14
14
  readonly chainId: ChainId;
15
15
  constructor(chainId: ChainId, path: string, attempts: ReadonlyArray<[domain: string, cause: unknown]>);
16
16
  }
17
+ /** Turtle answers for every chain at once, so this names no chain. */
18
+ declare class TurtleRequestFailedError extends BaseError {
19
+ name: string;
20
+ constructor(path: string, cause: unknown);
21
+ }
17
22
  //#endregion
18
- export { MerklRequestFailedError };
23
+ export { MerklRequestFailedError, TurtleRequestFailedError };
@@ -1,4 +1,6 @@
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 };
1
+ import { MerklRequestFailedError, TurtleRequestFailedError } from "./errors.js";
2
+ import { MerkleXYZUserRewardsV4Response } from "./merkl-api.js";
3
+ import { MerklReward, PointsReward, Reward } from "./toMerklRewards.js";
4
+ import { GetRewardsMultichainProps, getRewardsMultichain } from "./multichain.js";
5
+ import { TurtleWalletRewards } from "./turtle-api.js";
6
+ export { GetRewardsMultichainProps, MerklRequestFailedError, type MerklReward, type MerkleXYZUserRewardsV4Response, type PointsReward, type Reward, TurtleRequestFailedError, type TurtleWalletRewards, getRewardsMultichain };
@@ -4,30 +4,25 @@ import "../model/index.js";
4
4
  import { PluginsMap } from "../onchain/plugins/types.js";
5
5
  import { MultichainSDK } from "../onchain/MultichainSDK.js";
6
6
  import "../onchain/index.js";
7
- import { MerklReward } from "./toMerklRewards.js";
7
+ import { Reward } from "./toMerklRewards.js";
8
8
  import { Address } from "viem";
9
9
  //#region src/rewards/multichain.d.ts
10
- interface GetMerklRewardsMultichainProps<Plugins extends PluginsMap = {}> {
11
- /** Handle whose chains are asked. */
10
+ interface GetRewardsMultichainProps<Plugins extends PluginsMap = {}> {
12
11
  sdk: MultichainSDK<Plugins>;
13
- /** Wallet whose claimable rewards to list. */
14
12
  wallet: Address;
15
- /**
16
- * Chains to ask, defaulting to every chain the handle carries.
17
- **/
13
+ /** Defaults to every chain the handle carries. */
18
14
  chainIds?: ChainId[];
19
15
  /** Raises Merkl's rate limit; the keyless path answers too. */
20
- apiKey?: string;
16
+ merklApiKey?: string;
17
+ /** Turtle is skipped without one: its API answers no keyless request. */
18
+ turtleApiKey?: string;
21
19
  }
22
20
  /**
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.
21
+ * Every claimable reward a wallet holds — Merkl campaigns and the Gearbox
22
+ * organisation's Turtle streams — across the chains the handle carries.
23
+ * A chain is `status: "error"` only when every source failed on it; a chain
24
+ * with nothing to claim is a `"success"` with no rows.
30
25
  **/
31
- declare function getMerklRewardsMultichain<const Plugins extends PluginsMap = {}>({ sdk, wallet, chainIds, apiKey }: GetMerklRewardsMultichainProps<Plugins>): Promise<DataResponse<MerklReward[]>>;
26
+ declare function getRewardsMultichain<const Plugins extends PluginsMap = {}>({ sdk, ...props }: GetRewardsMultichainProps<Plugins>): Promise<DataResponse<Reward[]>>;
32
27
  //#endregion
33
- export { GetMerklRewardsMultichainProps, getMerklRewardsMultichain };
28
+ export { GetRewardsMultichainProps, getRewardsMultichain };
@@ -1,4 +1,5 @@
1
1
  import { ChainId, Token, TokenAmount } from "../model/primitives.js";
2
+ import { PointsProgramPnL } from "../model/positions.js";
2
3
  import "../model/index.js";
3
4
  import { OnchainSDK } from "../onchain/OnchainSDK.js";
4
5
  import "../onchain/index.js";
@@ -12,6 +13,8 @@ import { Address } from "viem";
12
13
  * reassembles one out of the loose fields Merkl sends.
13
14
  */
14
15
  interface MerklReward {
16
+ /** Where the reward is claimed. */
17
+ readonly source: "merkl" | "turtle";
15
18
  readonly chainId: ChainId;
16
19
  /** Market pool whose depositors the campaign rewards. */
17
20
  readonly pool: Address;
@@ -28,15 +31,24 @@ interface MerklReward {
28
31
  */
29
32
  readonly amount: TokenAmount;
30
33
  }
34
+ /** Points have no token and no price, and are not claimed. */
35
+ interface PointsReward extends Omit<MerklReward, "amount"> {
36
+ readonly points: PointsProgramPnL;
37
+ }
38
+ type Reward = MerklReward | PointsReward;
39
+ type RewardsSdk = Pick<OnchainSDK, "chainId" | "marketRegister" | "tokensMeta">;
31
40
  /**
32
- * What the mapping needs off a chain's SDK: which chain the rows belong to,
33
- * the pools a campaign can be keyed on, and the registry that names their
34
- * tokens. Nothing else, and nothing asynchronous.
41
+ * Merkl's answer for one chain, turned into rows of the read model.
35
42
  */
36
- type MerklRewardsSdk = Pick<OnchainSDK, "chainId" | "marketRegister" | "tokensMeta">;
43
+ declare function toMerklRewards(sdk: RewardsSdk, response: MerkleXYZUserRewardsV4Response): MerklReward[];
37
44
  /**
38
- * Merkl's answer for one chain, turned into rows of the read model.
45
+ * An incentive token is rarely protocol collateral, so the registry usually
46
+ * has no entry for it — and both sources always name it.
39
47
  */
40
- declare function toMerklRewards(sdk: MerklRewardsSdk, response: MerkleXYZUserRewardsV4Response): MerklReward[];
48
+ declare function toRewardToken(sdk: RewardsSdk, address: Address, named: {
49
+ symbol: string;
50
+ name?: string;
51
+ decimals: number;
52
+ }): Token;
41
53
  //#endregion
42
- export { MerklReward, MerklRewardsSdk, toMerklRewards };
54
+ export { MerklReward, PointsReward, Reward, RewardsSdk, toMerklRewards, toRewardToken };
@@ -0,0 +1,10 @@
1
+ import { Reward, RewardsSdk } from "./toMerklRewards.js";
2
+ import { TurtleWalletRewards } from "./turtle-api.js";
3
+ //#region src/rewards/toTurtleRewards.d.ts
4
+ /**
5
+ * Rows for the streams targeting a pool on the SDK's chain, merged per pool
6
+ * and incentive. A token row is what is committed and not yet claimed.
7
+ */
8
+ declare function toTurtleRewards(sdk: RewardsSdk, { streams, proofs }: TurtleWalletRewards, claimed: ReadonlyMap<string, bigint>): Reward[];
9
+ //#endregion
10
+ export { toTurtleRewards };
@@ -0,0 +1,62 @@
1
+ import { Address } from "viem";
2
+ //#region src/rewards/turtle-api.d.ts
3
+ interface TurtleWalletStream {
4
+ streamId: string;
5
+ snapshots: Array<{
6
+ rewardsAccumulated: string;
7
+ }>;
8
+ stream: {
9
+ orgId: string;
10
+ contractAddress: Address | null;
11
+ /** For Gearbox the target is a pool's share token. */
12
+ customArgs: {
13
+ targetToken?: {
14
+ address: Address;
15
+ chain: {
16
+ chainId: string;
17
+ };
18
+ };
19
+ };
20
+ point?: {
21
+ id: string;
22
+ name: string;
23
+ decimals: number;
24
+ } | null;
25
+ rewardToken: {
26
+ address: Address;
27
+ symbol: string;
28
+ name: string;
29
+ decimals: number;
30
+ } | null;
31
+ lastSnapshot: {
32
+ rewardTokenPrice: string | null;
33
+ } | null;
34
+ };
35
+ }
36
+ interface TurtleMerkleProof {
37
+ streamId: string;
38
+ chainId: number;
39
+ contractAddress: Address;
40
+ /** Cumulative committed allocation, not what is left to claim. */
41
+ amount: string;
42
+ }
43
+ interface TurtleWalletRewards {
44
+ streams: TurtleWalletStream[];
45
+ proofs: TurtleMerkleProof[];
46
+ }
47
+ declare const turtleStreamAbi: readonly [{
48
+ readonly type: "function";
49
+ readonly name: "getClaimedRewards";
50
+ readonly stateMutability: "view";
51
+ readonly inputs: readonly [{
52
+ readonly name: "user";
53
+ readonly type: "address";
54
+ }];
55
+ readonly outputs: readonly [{
56
+ readonly name: "";
57
+ readonly type: "uint256";
58
+ }];
59
+ }];
60
+ declare function fetchTurtleWalletRewards(user: Address, apiKey: string): Promise<TurtleWalletRewards>;
61
+ //#endregion
62
+ export { TurtleMerkleProof, TurtleWalletRewards, TurtleWalletStream, fetchTurtleWalletRewards, turtleStreamAbi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/sdk",
3
- "version": "17.1.1",
3
+ "version": "17.2.0",
4
4
  "description": "Gearbox SDK",
5
5
  "license": "MIT",
6
6
  "repository": {