Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit 1cf547a

Browse files
authored
Merge pull request #66 from ethereumfollowprotocol/leaderboard-and-slots
leaderboard fixes and lookup by slot
2 parents 5538861 + 44a76a3 commit 1cf547a

11 files changed

Lines changed: 228 additions & 125 deletions

File tree

src/router/api/v1/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { leaderboard } from './leaderboard'
1313
import { lists } from './lists'
1414
import { minters } from './minters'
1515
import { serviceHealth } from './serviceHealth'
16+
import { slots } from './slots'
1617
import { stats } from './stats'
1718
import { token } from './token'
1819
import { users } from './users'
@@ -45,6 +46,7 @@ export function api(services: Services): Hono<{ Bindings: Environment }> {
4546
api.route('/leaderboard', leaderboard(services))
4647
api.route('/lists', lists(services))
4748
api.route('/minters', minters(services))
49+
api.route('/slots', slots(services))
4850
api.route('/stats', stats(services))
4951
api.route('/token', token(services))
5052
api.route('/users', users(services))

src/router/api/v1/leaderboard/blocked/index.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function blocked(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* Same as /followers, but for blocked.
19-
*/
2014
leaderboard.get('/blocked', limitValidator, includeValidator, async context => {
21-
const { include, limit } = context.req.valid('query')
15+
const { limit, offset, cache } = context.req.valid('query')
2216
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
23-
let mostBlocked: { address: string; blocked_by_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/blocked?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
28+
const mostBlocked: { address: string; blocked_by_count: number }[] = await services
2429
.efp(env(context))
2530
.getLeaderboardBlocked(parsedLimit)
26-
if (include?.includes('ens')) {
27-
const ens = services.ens(env(context))
28-
const ensProfiles = await Promise.all(mostBlocked.map(user => ens.getENSProfile(user.address)))
29-
mostBlocked = mostBlocked.map((user, index) => ({
30-
...user,
31-
ens: ensProfiles[index]
32-
}))
33-
}
34-
return context.json(mostBlocked, 200)
31+
32+
const packagedResponse = mostBlocked
33+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
34+
return context.json(packagedResponse, 200)
3535
})
3636
}

src/router/api/v1/leaderboard/blocks/index.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function blocks(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* Same as /followers, but for following.
19-
*/
20-
leaderboard.get('/blocks/:addressOrENS?', limitValidator, includeValidator, async context => {
21-
const { include, limit } = context.req.valid('query')
14+
leaderboard.get('/blocks', limitValidator, includeValidator, async context => {
15+
const { limit, offset, cache } = context.req.valid('query')
2216
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
23-
let mostBlocks: { address: string; blocks_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/blocks?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
28+
const mostBlocks: { address: string; blocks_count: number }[] = await services
2429
.efp(env(context))
2530
.getLeaderboardBlocks(parsedLimit)
26-
if (include?.includes('ens')) {
27-
const ens = services.ens(env(context))
28-
const ensProfiles = await Promise.all(mostBlocks.map(user => ens.getENSProfile(user.address)))
29-
mostBlocks = mostBlocks.map((user, index) => ({
30-
...user,
31-
ens: ensProfiles[index]
32-
}))
33-
}
34-
return context.json(mostBlocks, 200)
31+
32+
const packagedResponse = mostBlocks
33+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
34+
return context.json(packagedResponse, 200)
3535
})
3636
}

src/router/api/v1/leaderboard/count/index.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,21 @@ export function count(
1313
includeValidator: IncludeValidator
1414
) {
1515
leaderboard.get('/count', limitValidator, includeValidator, async context => {
16-
const efp = await services.efp(env(context))
16+
const { cache } = context.req.valid('query')
17+
18+
const cacheService = services.cache(env(context))
19+
const cacheTarget = `leaderboard/count`
20+
if (cache !== 'fresh') {
21+
const cacheHit = await cacheService.get(cacheTarget)
22+
if (cacheHit) {
23+
return context.json({ ...cacheHit }, 200)
24+
}
25+
}
26+
const efp = services.efp(env(context))
1727
const leaderboardCount: number = await efp.getLeaderboardCount()
1828

19-
return context.json({ leaderboardCount }, 200)
29+
const packagedResponse = { leaderboardCount }
30+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
31+
return context.json(packagedResponse, 200)
2032
})
2133
}

src/router/api/v1/leaderboard/followers/index.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,31 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function followers(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* By default, only returns leaderboard with address and followers_count/following_count of each user.
19-
* If include=ens, also returns ens profile of each user.
20-
* If include=muted, also returns how many users each user has muted.
21-
* If include=blocked, also returns how many users each user has blocked.
22-
* If addressOrENS path param is provided AND include=mutuals query param is provided, returns mutuals between addressOrENS and each user.
23-
*/
24-
leaderboard.get('/followers/:addressOrENS?', limitValidator, includeValidator, async context => {
25-
const { include, limit } = context.req.valid('query')
14+
leaderboard.get('/followers', limitValidator, includeValidator, async context => {
15+
const { limit, offset, cache } = context.req.valid('query')
2616
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
27-
let mostFollowers: { address: string; followers_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/followers?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
const mostFollowers: { address: string; followers_count: number }[] = await services
2828
.efp(env(context))
2929
.getLeaderboardFollowers(parsedLimit)
30-
if (include?.includes('ens')) {
31-
const ens = services.ens(env(context))
32-
const ensProfiles = await Promise.all(mostFollowers.map(user => ens.getENSProfile(user.address)))
33-
mostFollowers = mostFollowers.map((user, index) => ({
34-
...user,
35-
ens: ensProfiles[index]
36-
}))
37-
}
38-
return context.json(mostFollowers, 200)
30+
31+
const packagedResponse = mostFollowers
32+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
33+
return context.json(packagedResponse, 200)
3934
})
4035
}

src/router/api/v1/leaderboard/following/index.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function following(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* Same as /followers, but for following.
19-
*/
20-
leaderboard.get('/following/:addressOrENS?', limitValidator, includeValidator, async context => {
21-
const { include, limit } = context.req.valid('query')
14+
leaderboard.get('/following', limitValidator, includeValidator, async context => {
15+
const { limit, offset, cache } = context.req.valid('query')
2216
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
23-
let mostFollowing: { address: string; following_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/following?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
28+
const mostFollowing: { address: string; following_count: number }[] = await services
2429
.efp(env(context))
2530
.getLeaderboardFollowing(parsedLimit)
26-
if (include?.includes('ens')) {
27-
const ens = services.ens(env(context))
28-
const ensProfiles = await Promise.all(mostFollowing.map(user => ens.getENSProfile(user.address)))
29-
mostFollowing = mostFollowing.map((user, index) => ({
30-
...user,
31-
ens: ensProfiles[index]
32-
}))
33-
}
34-
return context.json(mostFollowing, 200)
31+
32+
const packagedResponse = mostFollowing
33+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
34+
return context.json(packagedResponse, 200)
3535
})
3636
}

src/router/api/v1/leaderboard/muted/index.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function muted(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* Same as /followers, but for muted.
19-
*/
20-
leaderboard.get('/muted?', limitValidator, includeValidator, async context => {
21-
const { include, limit } = context.req.valid('query')
14+
leaderboard.get('/muted', limitValidator, includeValidator, async context => {
15+
const { limit, offset, cache } = context.req.valid('query')
2216
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
23-
let mostMuted: { address: string; muted_by_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/muted?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
28+
const mostMuted: { address: string; muted_by_count: number }[] = await services
2429
.efp(env(context))
2530
.getLeaderboardMuted(parsedLimit)
26-
if (include?.includes('ens')) {
27-
const ens = services.ens(env(context))
28-
const ensProfiles = await Promise.all(mostMuted.map(user => ens.getENSProfile(user.address)))
29-
mostMuted = mostMuted.map((user, index) => ({
30-
...user,
31-
ens: ensProfiles[index]
32-
}))
33-
}
34-
return context.json(mostMuted, 200)
31+
32+
const packagedResponse = mostMuted
33+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
34+
return context.json(packagedResponse, 200)
3535
})
3636
}

src/router/api/v1/leaderboard/mutes/index.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,31 @@ import type { Services } from '#/service'
55
import type { Environment } from '#/types'
66
import type { IncludeValidator, LimitValidator } from '../validators'
77

8-
/**
9-
* TODO: add support for whether :addressOrENS is followed, is following, is muting, is blocking, is blocked by
10-
*/
118
export function mutes(
129
leaderboard: Hono<{ Bindings: Environment }>,
1310
services: Services,
1411
limitValidator: LimitValidator,
1512
includeValidator: IncludeValidator
1613
) {
17-
/**
18-
* Same as /followers, but for following.
19-
*/
20-
leaderboard.get('/mutes/:addressOrENS?', limitValidator, includeValidator, async context => {
21-
const { include, limit } = context.req.valid('query')
14+
leaderboard.get('/mutes', limitValidator, includeValidator, async context => {
15+
const { limit, offset, cache } = context.req.valid('query')
2216
const parsedLimit = Number.parseInt(limit?.toString() || '10', 10)
23-
let mostMutes: { address: string; mutes_count: number }[] = await services
17+
const parsedOffset = Number.parseInt(offset?.toString() || '0', 10)
18+
19+
const cacheService = services.cache(env(context))
20+
const cacheTarget = `leaderboard/mutes?limit=${parsedLimit}&offset=${parsedOffset}`
21+
if (cache !== 'fresh') {
22+
const cacheHit = await cacheService.get(cacheTarget)
23+
if (cacheHit) {
24+
return context.json({ ...cacheHit }, 200)
25+
}
26+
}
27+
const mostMutes: { address: string; mutes_count: number }[] = await services
2428
.efp(env(context))
2529
.getLeaderboardMutes(parsedLimit)
26-
if (include?.includes('ens')) {
27-
const ens = services.ens(env(context))
28-
const ensProfiles = await Promise.all(mostMutes.map(user => ens.getENSProfile(user.address)))
29-
mostMutes = mostMutes.map((user, index) => ({
30-
...user,
31-
ens: ensProfiles[index]
32-
}))
33-
}
34-
return context.json(mostMutes, 200)
30+
31+
const packagedResponse = mostMutes
32+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
33+
return context.json(packagedResponse, 200)
3534
})
3635
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Hono } from 'hono'
2+
import { env } from 'hono/adapter'
3+
4+
import type { Services } from '#/service'
5+
import type { Environment } from '#/types'
6+
7+
export function details(slots: Hono<{ Bindings: Environment }>, services: Services) {
8+
slots.get('/:chain_id/:contract/:slot/details', async context => {
9+
const { chain_id, contract, slot } = context.req.param()
10+
const { cache } = context.req.query()
11+
const cacheService = services.cache(env(context))
12+
const cacheTarget = `slots/${slot}/details`
13+
if (cache !== 'fresh') {
14+
const cacheHit = await cacheService.get(cacheTarget)
15+
if (cacheHit) {
16+
return context.json({ ...cacheHit }, 200)
17+
}
18+
}
19+
20+
// const slotDetails: {}[] = ['details not implemented']
21+
const slotDetails = await services.efp(env(context)).getListDetailsBySlot(Number(chain_id), contract, slot)
22+
23+
if (!slotDetails) {
24+
return context.json({ response: 'No List Found' }, 404)
25+
}
26+
27+
const packagedResponse = { slotDetails }
28+
await cacheService.put(cacheTarget, JSON.stringify(packagedResponse))
29+
return context.json(packagedResponse, 200)
30+
})
31+
}

src/router/api/v1/slots/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Hono } from 'hono'
2+
3+
import type { Services } from '#/service'
4+
import type { Environment } from '#/types'
5+
6+
import { details } from './details'
7+
8+
export function slots(services: Services): Hono<{ Bindings: Environment }> {
9+
const slots = new Hono<{ Bindings: Environment }>()
10+
details(slots, services)
11+
12+
return slots
13+
}

0 commit comments

Comments
 (0)