@gearbox-protocol/sdk 17.1.2 → 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.
- package/dist/cjs/rewards/errors.js +11 -0
- package/dist/cjs/rewards/index.js +2 -3
- package/dist/cjs/rewards/multichain.js +41 -21
- package/dist/cjs/rewards/toMerklRewards.js +8 -7
- package/dist/cjs/rewards/toTurtleRewards.js +77 -0
- package/dist/cjs/rewards/turtle-api.js +50 -0
- package/dist/esm/rewards/errors.js +11 -1
- package/dist/esm/rewards/index.js +3 -4
- package/dist/esm/rewards/multichain.js +41 -21
- package/dist/esm/rewards/toMerklRewards.js +8 -8
- package/dist/esm/rewards/toTurtleRewards.js +76 -0
- package/dist/esm/rewards/turtle-api.js +48 -0
- package/dist/types/rewards/errors.d.ts +6 -1
- package/dist/types/rewards/index.d.ts +6 -4
- package/dist/types/rewards/multichain.d.ts +12 -17
- package/dist/types/rewards/toMerklRewards.d.ts +19 -7
- package/dist/types/rewards/toTurtleRewards.d.ts +10 -0
- package/dist/types/rewards/turtle-api.d.ts +62 -0
- package/package.json +1 -1
|
@@ -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.
|
|
7
|
-
exports.
|
|
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
|
-
|
|
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) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
29
|
-
* carries.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
37
|
-
return new
|
|
56
|
+
async function getRewardsMultichain({ sdk, ...props }) {
|
|
57
|
+
return new RewardsFanOut(sdk).list(props);
|
|
38
58
|
}
|
|
39
59
|
//#endregion
|
|
40
|
-
exports.
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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,
|
|
75
|
+
function toRewardToken(sdk, address, named) {
|
|
76
76
|
return sdk.tokensMeta.getToken(address) ?? {
|
|
77
77
|
chainId: sdk.chainId,
|
|
78
78
|
address,
|
|
79
|
-
symbol:
|
|
80
|
-
name:
|
|
81
|
-
decimals:
|
|
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;
|
|
@@ -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 {
|
|
3
|
-
|
|
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
|
-
|
|
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) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
28
|
-
* carries.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
36
|
-
return new
|
|
55
|
+
async function getRewardsMultichain({ sdk, ...props }) {
|
|
56
|
+
return new RewardsFanOut(sdk).list(props);
|
|
37
57
|
}
|
|
38
58
|
//#endregion
|
|
39
|
-
export {
|
|
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
|
-
*
|
|
71
|
-
*
|
|
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,
|
|
74
|
+
function toRewardToken(sdk, address, named) {
|
|
75
75
|
return sdk.tokensMeta.getToken(address) ?? {
|
|
76
76
|
chainId: sdk.chainId,
|
|
77
77
|
address,
|
|
78
|
-
symbol:
|
|
79
|
-
name:
|
|
80
|
-
decimals:
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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 {
|
|
7
|
+
import { Reward } from "./toMerklRewards.js";
|
|
8
8
|
import { Address } from "viem";
|
|
9
9
|
//#region src/rewards/multichain.d.ts
|
|
10
|
-
interface
|
|
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
|
-
|
|
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
|
|
24
|
-
* carries.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
26
|
+
declare function getRewardsMultichain<const Plugins extends PluginsMap = {}>({ sdk, ...props }: GetRewardsMultichainProps<Plugins>): Promise<DataResponse<Reward[]>>;
|
|
32
27
|
//#endregion
|
|
33
|
-
export {
|
|
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
|
-
*
|
|
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
|
-
|
|
43
|
+
declare function toMerklRewards(sdk: RewardsSdk, response: MerkleXYZUserRewardsV4Response): MerklReward[];
|
|
37
44
|
/**
|
|
38
|
-
*
|
|
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
|
|
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,
|
|
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 };
|