-
-
-
- Asset
- Chain
- Claimable
- Pending
- Claimed
- Total
- Action
-
-
- {filteredRewardTokens
- .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined)
- .map((tokenReward, index) => {
- // try find the reward token, default to 18 decimals for unknown tokens
- const matchedToken = findToken(tokenReward.asset.address, tokenReward.asset.chain_id) ?? {
- symbol: 'Unknown',
- img: undefined,
- decimals: 18,
- };
-
- const total = tokenReward.total.claimable + tokenReward.total.pendingAmount + tokenReward.total.claimed;
-
- const distribution = distributions.find(
- (d) =>
- d.asset.address.toLowerCase() === tokenReward.asset.address.toLowerCase() &&
- d.asset.chain_id === tokenReward.asset.chain_id,
- );
-
- const isMerklReward = tokenReward.programs.includes('merkl');
-
- return (
-
-
+
+
+
+ ASSET
+ CHAIN
+ CLAIMABLE
+ CAMPAIGN
+ ACTIONS
+
+
+ {filteredRewardTokens
+ .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined)
+ .map((tokenReward, index) => {
+ // try find the reward token, default to 18 decimals for unknown tokens
+ const matchedToken = findToken(tokenReward.asset.address, tokenReward.asset.chain_id) ?? {
+ symbol: 'Unknown',
+ img: undefined,
+ decimals: 18,
+ };
+
+ const distribution = distributions.find(
+ (d) =>
+ d.asset.address.toLowerCase() === tokenReward.asset.address.toLowerCase() &&
+ d.asset.chain_id === tokenReward.asset.chain_id,
+ );
+
+ const isMerklReward = tokenReward.programs.includes('merkl');
+
+ // Find matching campaign for this reward
+ const matchedCampaign = campaigns.find(
+ (c) =>
+ c.rewardToken.address.toLowerCase() === tokenReward.asset.address.toLowerCase() &&
+ c.chainId === tokenReward.asset.chain_id,
+ );
+
+ // Create unique key for tracking claim status
+ const rewardKey = `${tokenReward.asset.address.toLowerCase()}-${tokenReward.asset.chain_id}`;
+ const isThisRewardClaiming = claimingRewardKey === rewardKey;
+
+ return (
+
+
+ e.stopPropagation()}
+ >
+ {matchedToken.symbol}
+
+
+
+
+ {getNetworkImg(tokenReward.asset.chain_id) ? (
+
+ ) : (
+
+ )}
+
+
+
+ {formatSimple(formatBalance(tokenReward.total.claimable, matchedToken.decimals))}
+
+
+
+
+ {matchedCampaign ? (
e.stopPropagation()}
+ className="inline-flex items-center gap-1 text-sm hover:opacity-80 no-underline"
>
- {matchedToken.symbol}
-
+ Details
+
-
-
-
- {getNetworkImg(tokenReward.asset.chain_id) ? (
-
- ) : (
-
- )}
-
-
-
-
-
{formatSimple(formatBalance(tokenReward.total.claimable, matchedToken.decimals))}
-
-
-
-
-
-
{formatSimple(formatBalance(tokenReward.total.pendingAmount, matchedToken.decimals))}
-
-
-
-
-
-
{formatSimple(formatBalance(tokenReward.total.claimed, matchedToken.decimals))}
-
-
-
-
-
-
{formatSimple(formatBalance(total, matchedToken.decimals))}
-
-
-
-
-
- {isMerklReward ? (
-
-
-
- ) : (
- handleClaim(distribution)}
- variant="surface"
- size="sm"
- disabled={tokenReward.total.claimable === BigInt(0) || distribution === undefined}
- >
- Claim
-
- )}
-
-
-
- );
- })}
-
-
-
+ ) : (
+ -
+ )}
+
+
+ {isMerklReward ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
);
}
diff --git a/package.json b/package.json
index 81d641e8..372b5515 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@heroui/theme": "^2.2.6",
"@heroui/tooltip": "^2.0.36",
"@internationalized/date": "^3.8.2",
+ "@merkl/api": "^1.7.0",
"@morpho-org/blue-sdk": "^5.3.0",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 48ce890b..e694ab6c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@internationalized/date':
specifier: ^3.8.2
version: 3.8.2
+ '@merkl/api':
+ specifier: ^1.7.0
+ version: 1.7.0(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3)
'@morpho-org/blue-sdk':
specifier: ^5.3.0
version: 5.3.0(@morpho-org/morpho-ts@2.4.4)
@@ -856,6 +859,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@borewit/text-codec@0.1.1':
+ resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
+
'@clack/core@0.5.0':
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
@@ -900,6 +906,11 @@ packages:
peerDependencies:
'@noble/ciphers': ^1.0.0
+ '@elysiajs/eden@1.4.0':
+ resolution: {integrity: sha512-Elubsibe0mGK1TLsCrG+fXHorxVfS/YvWP/uDv/8bniideMgREH3yp4Hua5zQkejM3HG9nv2EfajsJ/wfqszuA==}
+ peerDependencies:
+ elysia: '>= 1.4.0-exp.0'
+
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
@@ -2040,6 +2051,9 @@ packages:
'@lit/reactive-element@2.1.1':
resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==}
+ '@merkl/api@1.7.0':
+ resolution: {integrity: sha512-wBzXyCyde3UVimuPH6dHrAJxS8vbjHchU3Ux1IMVSSy7Y/xEHpV16Bqz0WcT6BprlG6MOh5w9+PdGi+x6LIBkw==}
+
'@metamask/json-rpc-engine@8.0.2':
resolution: {integrity: sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==}
engines: {node: '>=16.0.0'}
@@ -3141,6 +3155,9 @@ packages:
'@scure/bip39@1.6.0':
resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==}
+ '@sinclair/typebox@0.34.41':
+ resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
+
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -3542,6 +3559,13 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
+ '@tokenizer/inflate@0.4.1':
+ resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
+ engines: {node: '>=18'}
+
+ '@tokenizer/token@0.3.0':
+ resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
+
'@trpc/server@11.7.2':
resolution: {integrity: sha512-AgB26PXY69sckherIhCacKLY49rxE2XP5h38vr/KMZTbLCL1p8IuIoKPjALTcugC2kbyQ7Lbqo2JDVfRSmPmfQ==}
peerDependencies:
@@ -4125,6 +4149,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
core-js-compat@3.45.0:
resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==}
@@ -4254,6 +4282,15 @@ packages:
supports-color:
optional: true
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
@@ -4350,6 +4387,20 @@ packages:
elliptic@6.6.1:
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
+ elysia@1.4.3-beta.0:
+ resolution: {integrity: sha512-UzanGZSqoKKcKgg+I4YB+ZOGeJNA7mlA8JPj7klYi0LDVcj/vtXKGRmsp4jEzZO+LjSpFuYTZcwHi4rCnvJj1w==}
+ peerDependencies:
+ exact-mirror: '>= 0.0.9'
+ file-type: '>= 20.0.0'
+ typescript: '>= 5.0.0'
+
+ elysia@1.4.5:
+ resolution: {integrity: sha512-slSMNyAuh6lFEjEwSkSkpzbUOLCtx6hOw4AYhpGOHqczu27eqxHbNtRtIPQuXDq6sqJ1ezXgw3gUjoasZozWRg==}
+ peerDependencies:
+ exact-mirror: '>= 0.0.9'
+ file-type: '>= 20.0.0'
+ typescript: '>= 5.0.0'
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -4447,6 +4498,14 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
+ exact-mirror@0.2.5:
+ resolution: {integrity: sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ==}
+ peerDependencies:
+ '@sinclair/typebox': ^0.34.15
+ peerDependenciesMeta:
+ '@sinclair/typebox':
+ optional: true
+
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
@@ -4464,6 +4523,9 @@ packages:
fast-copy@4.0.1:
resolution: {integrity: sha512-+uUOQlhsaswsizHFmEFAQhB3lSiQ+lisxl50N6ZP0wywlZeWsIESxSi9ftPEps8UGfiBzyYP7x27zA674WUvXw==}
+ fast-decode-uri-component@1.0.1:
+ resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
+
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -4501,6 +4563,10 @@ packages:
file-entry-cache@10.1.4:
resolution: {integrity: sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==}
+ file-type@21.1.1:
+ resolution: {integrity: sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==}
+ engines: {node: '>=20'}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -5253,6 +5319,9 @@ packages:
openapi-fetch@0.13.8:
resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==}
+ openapi-types@12.1.3:
+ resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
+
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
@@ -5832,6 +5901,10 @@ packages:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
+ strtok3@10.3.4:
+ resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
+ engines: {node: '>=18'}
+
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -5949,6 +6022,10 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ token-types@6.1.1:
+ resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
+ engines: {node: '>=14.16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -5999,6 +6076,10 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
+ uint8array-extras@1.5.0:
+ resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
+ engines: {node: '>=18'}
+
uint8arrays@3.1.0:
resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==}
@@ -7192,6 +7273,8 @@ snapshots:
'@biomejs/cli-win32-x64@2.3.8':
optional: true
+ '@borewit/text-codec@0.1.1': {}
+
'@clack/core@0.5.0':
dependencies:
picocolors: 1.1.1
@@ -7263,6 +7346,10 @@ snapshots:
'@noble/ciphers': 1.3.0
optional: true
+ '@elysiajs/eden@1.4.0(elysia@1.4.5(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3))':
+ dependencies:
+ elysia: 1.4.5(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3)
+
'@emnapi/runtime@1.4.5':
dependencies:
tslib: 2.8.1
@@ -8921,6 +9008,15 @@ snapshots:
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
+ '@merkl/api@1.7.0(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3)':
+ dependencies:
+ '@elysiajs/eden': 1.4.0(elysia@1.4.5(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3))
+ elysia: 1.4.5(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3)
+ transitivePeerDependencies:
+ - exact-mirror
+ - file-type
+ - typescript
+
'@metamask/json-rpc-engine@8.0.2':
dependencies:
'@metamask/rpc-errors': 6.4.0
@@ -10983,6 +11079,9 @@ snapshots:
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
+ '@sinclair/typebox@0.34.41':
+ optional: true
+
'@socket.io/component-emitter@3.1.2':
optional: true
@@ -11571,6 +11670,15 @@ snapshots:
dependencies:
'@testing-library/dom': 9.3.4
+ '@tokenizer/inflate@0.4.1':
+ dependencies:
+ debug: 4.4.3
+ token-types: 6.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tokenizer/token@0.3.0': {}
+
'@trpc/server@11.7.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -12761,6 +12869,8 @@ snapshots:
cookie@0.7.2: {}
+ cookie@1.1.1: {}
+
core-js-compat@3.45.0:
dependencies:
browserslist: 4.25.3
@@ -12872,6 +12982,10 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decamelize@1.2.0: {}
decimal.js-light@2.5.1: {}
@@ -12990,6 +13104,29 @@ snapshots:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
+ elysia@1.4.3-beta.0(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3):
+ dependencies:
+ cookie: 1.1.1
+ exact-mirror: 0.2.5(@sinclair/typebox@0.34.41)
+ fast-decode-uri-component: 1.0.1
+ file-type: 21.1.1
+ typescript: 5.9.3
+ optionalDependencies:
+ '@sinclair/typebox': 0.34.41
+ openapi-types: 12.1.3
+
+ elysia@1.4.5(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3):
+ dependencies:
+ cookie: 1.1.1
+ elysia: 1.4.3-beta.0(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(typescript@5.9.3)
+ exact-mirror: 0.2.5(@sinclair/typebox@0.34.41)
+ fast-decode-uri-component: 1.0.1
+ file-type: 21.1.1
+ typescript: 5.9.3
+ optionalDependencies:
+ '@sinclair/typebox': 0.34.41
+ openapi-types: 12.1.3
+
emoji-regex@8.0.0: {}
encode-utf8@1.0.3: {}
@@ -13160,6 +13297,10 @@ snapshots:
events@3.3.0: {}
+ exact-mirror@0.2.5(@sinclair/typebox@0.34.41):
+ optionalDependencies:
+ '@sinclair/typebox': 0.34.41
+
exsolve@1.0.8: {}
extend@3.0.2: {}
@@ -13175,6 +13316,8 @@ snapshots:
fast-copy@4.0.1: {}
+ fast-decode-uri-component@1.0.1: {}
+
fast-deep-equal@3.1.3: {}
fast-equals@5.2.2: {}
@@ -13210,6 +13353,15 @@ snapshots:
dependencies:
flat-cache: 6.1.13
+ file-type@21.1.1:
+ dependencies:
+ '@tokenizer/inflate': 0.4.1
+ strtok3: 10.3.4
+ token-types: 6.1.1
+ uint8array-extras: 1.5.0
+ transitivePeerDependencies:
+ - supports-color
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -13905,6 +14057,9 @@ snapshots:
openapi-typescript-helpers: 0.0.15
optional: true
+ openapi-types@12.1.3:
+ optional: true
+
openapi-typescript-helpers@0.0.15:
optional: true
@@ -14668,6 +14823,10 @@ snapshots:
strip-json-comments@5.0.3: {}
+ strtok3@10.3.4:
+ dependencies:
+ '@tokenizer/token': 0.3.0
+
styled-jsx@5.1.6(@babel/core@7.28.3)(react@18.3.1):
dependencies:
client-only: 0.0.1
@@ -14803,6 +14962,12 @@ snapshots:
dependencies:
is-number: 7.0.0
+ token-types@6.1.1:
+ dependencies:
+ '@borewit/text-codec': 0.1.1
+ '@tokenizer/token': 0.3.0
+ ieee754: 1.2.1
+
tr46@0.0.3: {}
trough@2.2.0: {}
@@ -14829,6 +14994,8 @@ snapshots:
ufo@1.6.1: {}
+ uint8array-extras@1.5.0: {}
+
uint8arrays@3.1.0:
dependencies:
multiformats: 9.9.0
diff --git a/src/abis/merkl-distributor.ts b/src/abis/merkl-distributor.ts
new file mode 100644
index 00000000..78833139
--- /dev/null
+++ b/src/abis/merkl-distributor.ts
@@ -0,0 +1,33 @@
+// Merkl Distributor ABI
+// Same distributor contract address across all supported chains
+// Address: 0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae
+
+// Details in the future: https://app.merkl.xyz/status
+
+export const merklDistributorAbi = [
+ {
+ inputs: [
+ { internalType: 'address[]', name: 'users', type: 'address[]' },
+ { internalType: 'address[]', name: 'tokens', type: 'address[]' },
+ { internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' },
+ { internalType: 'bytes32[][]', name: 'proofs', type: 'bytes32[][]' },
+ ],
+ name: 'claim',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'user', type: 'address' },
+ { internalType: 'address', name: 'token', type: 'address' },
+ ],
+ name: 'claimed',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
+
+// Universal Merkl Distributor address - same across all chains
+export const MERKL_DISTRIBUTOR_ADDRESS = '0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae' as const;
diff --git a/src/contexts/MerklCampaignsContext.tsx b/src/contexts/MerklCampaignsContext.tsx
index e53c350c..270d8760 100644
--- a/src/contexts/MerklCampaignsContext.tsx
+++ b/src/contexts/MerklCampaignsContext.tsx
@@ -1,8 +1,8 @@
'use client';
-import { createContext, useContext, useState, useEffect, useCallback, type ReactNode, useMemo } from 'react';
-import { merklApiClient, simplifyMerklCampaign } from '@/utils/merklApi';
-import type { MerklCampaign, SimplifiedCampaign } from '@/utils/merklTypes';
+import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode, useMemo } from 'react';
+import { fetchActiveCampaigns, simplifyMerklCampaign } from '@/utils/merklApi';
+import type { SimplifiedCampaign } from '@/utils/merklTypes';
type MerklCampaignsContextType = {
campaigns: SimplifiedCampaign[];
@@ -21,33 +21,26 @@ export function MerklCampaignsProvider({ children }: MerklCampaignsProviderProps
const [campaigns, setCampaigns] = useState
([]);
const [loading, setLoading] = useState(true); // Start as true like MarketsContext
const [error, setError] = useState(null);
+ const hasInitialized = useRef(false);
const fetchCampaigns = useCallback(async () => {
setLoading(true);
setError(null);
try {
- const allRawCampaigns: MerklCampaign[] = [];
-
- // Fetch both MORPHOSUPPLY and MORPHOSUPPLY_SINGLETOKEN campaigns
- const supplyCampaigns = await merklApiClient.fetchActiveCampaigns({
- type: 'MORPHOSUPPLY',
- });
- const singleTokenCampaigns = await merklApiClient.fetchActiveCampaigns({
- type: 'MORPHOSUPPLY_SINGLETOKEN',
- });
- allRawCampaigns.push(...supplyCampaigns, ...singleTokenCampaigns);
-
- // Convert to simplified campaigns and normalize market IDs
- const simplifiedCampaigns = allRawCampaigns.map((campaign) => {
- const simplified = simplifyMerklCampaign(campaign);
- return {
- ...simplified,
- marketId: simplified.marketId.toLowerCase(), // Normalize to lowercase
- };
- });
+ // Fetch both MORPHOSUPPLY and MORPHOSUPPLY_SINGLETOKEN campaigns using SDK
+ const [supplyCampaigns, singleTokenCampaigns] = await Promise.all([
+ fetchActiveCampaigns({ type: 'MORPHOSUPPLY' }),
+ fetchActiveCampaigns({ type: 'MORPHOSUPPLY_SINGLETOKEN' }),
+ ]);
+
+ const allRawCampaigns = [...supplyCampaigns, ...singleTokenCampaigns];
+
+ // Convert to simplified campaigns
+ const simplifiedCampaigns = allRawCampaigns.map((campaign) => simplifyMerklCampaign(campaign));
setCampaigns(simplifiedCampaigns);
+ hasInitialized.current = true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch campaigns';
setError(errorMessage);
@@ -58,11 +51,11 @@ export function MerklCampaignsProvider({ children }: MerklCampaignsProviderProps
}, []); // Empty deps like MarketsContext
useEffect(() => {
- // Simple condition like MarketsContext - run if we have no campaigns
- if (campaigns.length === 0) {
+ // Only fetch once on mount
+ if (!hasInitialized.current) {
void fetchCampaigns().catch(console.error);
}
- }, [fetchCampaigns, campaigns.length]);
+ }, [fetchCampaigns]);
const refetch = useCallback(async () => {
await fetchCampaigns();
diff --git a/src/hooks/useClaimMerklRewards.ts b/src/hooks/useClaimMerklRewards.ts
new file mode 100644
index 00000000..b9ae35ef
--- /dev/null
+++ b/src/hooks/useClaimMerklRewards.ts
@@ -0,0 +1,142 @@
+import { useState, useCallback } from 'react';
+import { useConnection, useWriteContract, useWaitForTransactionReceipt, useSwitchChain, useChainId } from 'wagmi';
+import type { Address } from 'viem';
+import { merklDistributorAbi, MERKL_DISTRIBUTOR_ADDRESS } from '@/abis/merkl-distributor';
+import type { MerklRewardWithProofs } from './useRewards';
+
+type ClaimStatus = 'idle' | 'preparing' | 'switching' | 'pending' | 'success' | 'error';
+
+type ClaimResult = {
+ status: ClaimStatus;
+ txHash?: Address;
+ error?: Error;
+};
+
+export function useClaimMerklRewards() {
+ const { address } = useConnection();
+ const currentChainId = useChainId();
+ const [claimStatus, setClaimStatus] = useState('idle');
+ const [txHash, setTxHash] = useState(undefined);
+
+ const { mutateAsync: writeContractAsync, error: writeError, isPending: isWritePending } = useWriteContract();
+ const { mutateAsync: switchChainAsync } = useSwitchChain();
+
+ const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
+ hash: txHash,
+ });
+
+ const claimRewards = useCallback(
+ async (rewards: MerklRewardWithProofs[]): Promise => {
+ if (!address) {
+ return {
+ status: 'error',
+ error: new Error('Wallet not connected'),
+ };
+ }
+
+ if (rewards.length === 0) {
+ return {
+ status: 'error',
+ error: new Error('No rewards provided'),
+ };
+ }
+
+ // Get the target chain ID from the first reward (all rewards passed should be for the same chain)
+ const targetChainId = rewards[0].chainId;
+
+ // Filter rewards for the target chain and with claimable amounts
+ const claimableRewards = rewards.filter((reward) => {
+ const claimable = BigInt(reward.amount) - BigInt(reward.claimed);
+ return reward.chainId === targetChainId && claimable > 0n && reward.proofs.length > 0;
+ });
+
+ if (claimableRewards.length === 0) {
+ return {
+ status: 'error',
+ error: new Error('No claimable rewards for this chain'),
+ };
+ }
+
+ try {
+ setClaimStatus('preparing');
+
+ // Check if we need to switch chains
+ if (currentChainId !== targetChainId) {
+ setClaimStatus('switching');
+ try {
+ await switchChainAsync({ chainId: targetChainId });
+ } catch (switchError) {
+ // User rejected chain switch or other error
+ setClaimStatus('error');
+ return {
+ status: 'error',
+ error: switchError instanceof Error ? switchError : new Error('Chain switch failed'),
+ };
+ }
+ }
+
+ // Prepare claim data
+ const users: Address[] = [];
+ const tokens: Address[] = [];
+ const amounts: bigint[] = [];
+ const proofs: `0x${string}`[][] = [];
+
+ for (const reward of claimableRewards) {
+ users.push(address);
+ tokens.push(reward.tokenAddress);
+ amounts.push(BigInt(reward.amount));
+ proofs.push(reward.proofs as `0x${string}`[]);
+ }
+
+ setClaimStatus('pending');
+
+ // Execute claim transaction - same distributor address on all chains
+ const hash = await writeContractAsync({
+ address: MERKL_DISTRIBUTOR_ADDRESS,
+ abi: merklDistributorAbi,
+ functionName: 'claim',
+ args: [users, tokens, amounts, proofs],
+ });
+
+ // Store hash for transaction receipt tracking
+ setTxHash(hash);
+ setClaimStatus('success');
+
+ return {
+ status: 'success',
+ txHash: hash,
+ };
+ } catch (_err) {
+ setClaimStatus('error');
+ return {
+ status: 'error',
+ error: new Error('Claiming failed or canceled, please try again later'),
+ };
+ }
+ },
+ [address, currentChainId, switchChainAsync, writeContractAsync],
+ );
+
+ const claimSingleReward = useCallback(
+ async (reward: MerklRewardWithProofs): Promise => {
+ return claimRewards([reward]);
+ },
+ [claimRewards],
+ );
+
+ const reset = useCallback(() => {
+ setClaimStatus('idle');
+ }, []);
+
+ return {
+ claimRewards,
+ claimSingleReward,
+ claimStatus,
+ isWritePending,
+ isConfirming,
+ isConfirmed,
+ txHash,
+ error: writeError,
+ reset,
+ };
+}
diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts
index ee91420d..d0e05284 100644
--- a/src/hooks/useRewards.ts
+++ b/src/hooks/useRewards.ts
@@ -1,8 +1,9 @@
import { useState, useEffect, useCallback } from 'react';
import type { Address } from 'viem';
-import type { MerklChain, MerklToken } from '@/utils/merklTypes';
+import { merklClient } from '@/utils/merklApi';
import type { RewardResponseType } from '@/utils/types';
import { URLS } from '@/utils/urls';
+import { ALL_SUPPORTED_NETWORKS } from '@/utils/networks';
export type DistributionResponseType = {
user: Address;
@@ -21,38 +22,44 @@ export type DistributionResponseType = {
tx_data: string;
};
-type MerklReward = {
- distributionChainId: number;
- root: string;
- recipient: string;
+// Extended reward type with claiming data
+export type MerklRewardWithProofs = {
+ tokenAddress: Address;
+ chainId: number;
amount: string;
claimed: string;
pending: string;
proofs: string[];
- token: Pick;
+ symbol: string;
+ decimals: number;
};
-type MerklApiResponse = {
- chain: MerklChain;
- rewards: MerklReward[];
-}[];
-
-async function fetchMerklRewards(userAddress: string): Promise {
+async function fetchMerklRewards(
+ userAddress: string,
+): Promise<{ rewards: RewardResponseType[]; rewardsWithProofs: MerklRewardWithProofs[] }> {
try {
const rewardsList: RewardResponseType[] = [];
- const chainIds = [1, 8453]; // Mainnet and Base
-
- for (const chainId of chainIds) {
- const url = `https://api.merkl.xyz/v4/users/${userAddress}/rewards?chainId=${chainId}&reloadChainId=${chainId}&test=false&claimableOnly=false&breakdownPage=0&type=TOKEN`;
- const response = await fetch(url);
-
- if (!response.ok) {
- console.error(`Merkl API error for chain ${chainId}:`, response.status, response.statusText);
+ const rewardsWithProofsList: MerklRewardWithProofs[] = [];
+
+ // Scan all supported networks for Merkl rewards
+ for (const chainId of ALL_SUPPORTED_NETWORKS) {
+ // Use Merkl SDK to fetch rewards with proofs
+ const { data, error, status } = await merklClient.v4.users({ address: userAddress }).rewards.get({
+ query: {
+ chainId: [chainId.toString()],
+ reloadChainId: chainId,
+ test: false,
+ claimableOnly: false,
+ breakdownPage: 0,
+ type: 'TOKEN',
+ },
+ });
+
+ if (error ?? status !== 200) {
+ console.error(`Merkl API error for chain ${chainId}:`, status, error);
continue;
}
- const data = (await response.json()) as MerklApiResponse;
-
if (!Array.isArray(data) || data.length === 0) {
console.warn(`No rewards data for chain ${chainId}`);
continue;
@@ -63,7 +70,10 @@ async function fetchMerklRewards(userAddress: string): Promise = {};
+ const tokenAggregation: Record<
+ string,
+ { pending: bigint; amount: bigint; claimed: bigint; proofs: string[]; symbol: string; decimals: number }
+ > = {};
for (const reward of chainData.rewards) {
const tokenAddress = reward.token.address;
@@ -73,11 +83,14 @@ async function fetchMerklRewards(userAddress: string): Promise claimed ? amount - claimed : 0n;
tokenAggregation[tokenAddress].pending += pending;
@@ -86,6 +99,7 @@ async function fetchMerklRewards(userAddress: string): Promise {
+ try {
+ const [totalRewardsRes, distributionRes] = await Promise.all([
+ fetch(`${URLS.MORPHO_REWARDS_API}/users/${userAddress}/rewards`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }),
+ fetch(`${URLS.MORPHO_REWARDS_API}/users/${userAddress}/distributions`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }),
+ ]);
+
+ const morphoRewards = (await totalRewardsRes.json()).data as RewardResponseType[];
+ const distributions = (await distributionRes.json()).data as DistributionResponseType[];
+
+ return {
+ rewards: Array.isArray(morphoRewards) ? morphoRewards : [],
+ distributions: Array.isArray(distributions) ? distributions : [],
+ };
+ } catch (error) {
+ console.error('Error fetching Morpho rewards:', error);
+ return { rewards: [], distributions: [] };
}
}
@@ -116,6 +174,7 @@ const useUserRewards = (user: string | undefined) => {
const [loading, setLoading] = useState(true);
const [rewards, setRewards] = useState([]);
const [distributions, setDistributions] = useState([]);
+ const [merklRewardsWithProofs, setMerklRewardsWithProofs] = useState([]);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
@@ -126,37 +185,20 @@ const useUserRewards = (user: string | undefined) => {
try {
setLoading(true);
- const [totalRewardsRes, distributionRes, merklRewards] = await Promise.all([
- fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/rewards`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- }),
- fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/distributions`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- }),
- fetchMerklRewards(user),
- ]);
-
- const morphoRewards = (await totalRewardsRes.json()).data as RewardResponseType[];
- const newDistributions = (await distributionRes.json()).data as DistributionResponseType[];
+ const [morphoRewardsData, merklRewardsData] = await Promise.all([fetchMorphoRewards(user), fetchMerklRewards(user)]);
// Combine Morpho and Merkl rewards
- const combinedRewards = [...(Array.isArray(morphoRewards) ? morphoRewards : []), ...merklRewards];
+ const combinedRewards = [...morphoRewardsData.rewards, ...merklRewardsData.rewards];
- if (Array.isArray(newDistributions)) {
- setDistributions(newDistributions);
- }
+ setDistributions(morphoRewardsData.distributions);
setRewards(combinedRewards);
+ setMerklRewardsWithProofs(merklRewardsData.rewardsWithProofs);
setError(null);
} catch (err) {
setError(err);
setRewards([]);
setDistributions([]);
+ setMerklRewardsWithProofs([]);
} finally {
setLoading(false);
}
@@ -169,6 +211,7 @@ const useUserRewards = (user: string | undefined) => {
return {
rewards,
distributions,
+ merklRewardsWithProofs,
loading,
error,
refresh: fetchData,
diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx
index 1b91b3f1..5b9b6b0d 100644
--- a/src/hooks/useTransactionWithToast.tsx
+++ b/src/hooks/useTransactionWithToast.tsx
@@ -27,7 +27,7 @@ export function useTransactionWithToast({
successDescription,
onSuccess,
}: UseTransactionWithToastProps) {
- const { data: hash, sendTransaction, error: txError, sendTransactionAsync } = useSendTransaction();
+ const { data: hash, mutate: sendTransaction, error: txError, mutateAsync: sendTransactionAsync } = useSendTransaction();
const {
isLoading: isConfirming,
isSuccess: isConfirmed,
diff --git a/src/utils/merklApi.ts b/src/utils/merklApi.ts
index 4bd90409..f31ef3c5 100644
--- a/src/utils/merklApi.ts
+++ b/src/utils/merklApi.ts
@@ -1,58 +1,73 @@
-import type { MerklCampaignsResponse, MerklApiParams, MerklCampaign, SimplifiedCampaign } from './merklTypes';
-
-const MERKL_API_BASE_URL = 'https://api.merkl.xyz/v4';
-
-export class MerklApiClient {
- private readonly baseUrl: string;
-
- constructor(baseUrl: string = MERKL_API_BASE_URL) {
- this.baseUrl = baseUrl;
- }
-
- private buildUrl(endpoint: string, params: MerklApiParams = {}): string {
- const url = new URL(`${this.baseUrl}${endpoint}`);
-
- Object.entries(params).forEach(([key, value]) => {
- if (value !== undefined && value !== null) {
- url.searchParams.append(key, value.toString());
- }
+import { MerklApi } from '@merkl/api';
+import type { MerklCampaign, SimplifiedCampaign, MerklApiParams } from './merklTypes';
+
+const MERKL_API_BASE_URL = 'https://api.merkl.xyz';
+
+// Initialize the Merkl SDK singleton
+export const merklClient = MerklApi(MERKL_API_BASE_URL);
+
+// Helper function to fetch campaigns using the SDK with Adapter pattern
+export async function fetchCampaigns(params: MerklApiParams = {}): Promise {
+ try {
+ const queryParams: Record = {};
+
+ if (params.type) queryParams.type = params.type;
+ if (params.chainId !== undefined) queryParams.chainId = params.chainId;
+ if (params.items !== undefined) queryParams.items = params.items;
+ if (params.page !== undefined) queryParams.page = params.page;
+ if (params.startTimestamp !== undefined) queryParams.startTimestamp = params.startTimestamp;
+ if (params.endTimestamp !== undefined) queryParams.endTimestamp = params.endTimestamp;
+
+ const { data, error, status } = await merklClient.v4.campaigns.get({
+ query: {
+ ...queryParams,
+ mainProtocolId: 'morpho',
+ },
});
- return url.toString();
- }
-
- async fetchCampaigns(params: MerklApiParams = {}): Promise {
- const url = this.buildUrl('/campaigns', params);
-
- try {
- const response = await fetch(url);
-
- if (!response.ok) {
- throw new Error(`Merkl API error: ${response.status} ${response.statusText}`);
- }
-
- const data = (await response.json()) as MerklCampaignsResponse;
- return data;
- } catch (error) {
- console.error('Error fetching Merkl campaigns:', error);
- throw error;
+ if (error ?? status !== 200) {
+ throw new Error(`Merkl API error: ${status} ${error}`);
}
+
+ // The SDK returns data that's compatible with our MerklCampaign type
+ return data as unknown as MerklCampaign[];
+ } catch (err) {
+ console.error('Error fetching Merkl campaigns:', err);
+ throw err;
}
+}
- async fetchActiveCampaigns(params: Omit = {}): Promise {
- const now = Math.floor(Date.now() / 1000);
+// Helper function to fetch active campaigns with full pagination
+export async function fetchActiveCampaigns(params: Omit = {}): Promise {
+ const now = Math.floor(Date.now() / 1000);
+ const pageSize = params.items ?? 100; // Use provided items or default to 100
+ const allCampaigns: MerklCampaign[] = [];
+ let currentPage = 0;
+ let hasMore = true;
- // Single API call with reasonable limit, no pagination loop
- return this.fetchCampaigns({
+ while (hasMore) {
+ const batch = await fetchCampaigns({
...params,
- items: 100, // Get up to 100 campaigns in one call
- page: 0,
+ items: pageSize,
+ page: currentPage,
startTimestamp: 0,
endTimestamp: now,
});
+
+ allCampaigns.push(...batch);
+
+ // If we got fewer results than pageSize, we've reached the end
+ if (batch.length < pageSize) {
+ hasMore = false;
+ } else {
+ currentPage++;
+ }
}
+
+ return allCampaigns;
}
+// Adapter function to convert SDK campaign to SimplifiedCampaign
export function simplifyMerklCampaign(campaign: MerklCampaign): SimplifiedCampaign {
const now = Math.floor(Date.now() / 1000);
const isActive = campaign.startTimestamp <= now && campaign.endTimestamp > now;
@@ -96,5 +111,3 @@ export function simplifyMerklCampaign(campaign: MerklCampaign): SimplifiedCampai
return baseResult;
}
-
-export const merklApiClient = new MerklApiClient();
diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts
index 2aff2591..190dc433 100644
--- a/src/utils/tokens.ts
+++ b/src/utils/tokens.ts
@@ -751,6 +751,19 @@ const supportedTokens = [
peg: TokenPeg.USD,
},
// End of hyperEvm
+
+ // Monad
+ {
+ symbol: 'WMON',
+ img: require('../imgs/chains/monad.svg') as string,
+ decimals: 18,
+ networks: [
+ {
+ address: '0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A', // not used in "reward" as MORPHO_ARBITRUM just yet
+ chain: monad,
+ },
+ ],
+ },
{
symbol: 'MORPHO',
img: require('../imgs/tokens/morpho.svg') as string,
diff --git a/src/utils/urls.ts b/src/utils/urls.ts
index ae61adcc..252158c6 100644
--- a/src/utils/urls.ts
+++ b/src/utils/urls.ts
@@ -2,6 +2,8 @@ import { SupportedNetworks } from './networks';
export const URLS = {
MORPHO_BLUE_API: 'https://blue-api.morpho.org/graphql',
+
+ // only returns morpho reward, won't include merkl rewards
MORPHO_REWARDS_API: 'https://rewards.morpho.org/v1',
} as const;