Skip to content

Commit fa46f15

Browse files
committed
feat: expose helius-collection-assets and helius-token-accounts resolvers
1 parent 9cfafda commit fa46f15

16 files changed

+417
-73
lines changed

examples/node-redis/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ HELIUS_API_KEY=foo-bar-baz-qux
33
HOSTNAME=localhost
44
PORT=3080
55
REDIS_URL=redis://localhost:6379
6-
RESOLVERS=helius-nft-holders:5FusHaKEKjfKsmQwXNrhFcFABGGxu7iYCdbvyVSRe3Ri|helius-token-holders:Ds52CDgqdWbTWsua1hgT3AuSSy4FNx2Ezge1br3jQ14a
6+
RESOLVERS=helius-collection-holders:5FusHaKEKjfKsmQwXNrhFcFABGGxu7iYCdbvyVSRe3Ri|helius-token-holders:Ds52CDgqdWbTWsua1hgT3AuSSy4FNx2Ezge1br3jQ14a
77
VERBOSE=true

examples/node-redis/src/commands/command-nft-holders.ts renamed to examples/node-redis/src/commands/command-collection-holders.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolverHeliusNftHolders } from '@pubkey-cache/resolver'
1+
import { resolverHeliusCollectionHolders } from '@pubkey-cache/resolver'
22
import prompts from 'prompts'
33

44
import { ensureValidPublicKey } from '../lib/ensure-valid-public-key'
@@ -9,7 +9,7 @@ import { Command } from './command'
99
let previousValue: string | undefined
1010
const config = getConfig()
1111

12-
export const commandNftHolders: Command = {
12+
export const commandCollectionHolders: Command = {
1313
action: async (ctx: Context) => {
1414
const { collection } = await prompts({
1515
initial: previousValue,
@@ -31,7 +31,7 @@ export const commandNftHolders: Command = {
3131
}
3232
try {
3333
ensureValidPublicKey(collection)
34-
const result = await resolverHeliusNftHolders({
34+
const result = await resolverHeliusCollectionHolders({
3535
collection,
3636
helius: ctx.helius,
3737
verbose: config.verbose,
@@ -43,5 +43,5 @@ export const commandNftHolders: Command = {
4343
}
4444
},
4545
description: 'Get the holders of an NFT collection',
46-
name: 'nft-holders',
46+
name: 'collection-holders',
4747
}

examples/node-redis/src/commands/command-resolver-sync-all.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolve, storeResolverResultMap } from '@pubkey-cache/resolver'
1+
import { resolve, ResolverType } from '@pubkey-cache/resolver'
22

33
import { getConfig } from '../lib/get-config'
44
import { getContext } from '../lib/get-context'
@@ -14,25 +14,24 @@ export const commandResolverSyncAll: Command = {
1414
try {
1515
for (const resolver of resolvers) {
1616
const startTimeResolver = new Date().getTime()
17-
const resultMap = await resolve({ context, resolver, verbose })
18-
19-
const { writeCount } = await storeResolverResultMap({
20-
path: resolver.id,
21-
resultMap,
22-
storage: context.storage,
23-
})
24-
const endTimeResolver = new Date().getTime()
25-
const durationResolver = endTimeResolver - startTimeResolver
26-
27-
results.push(
28-
`Synced resolver ${resolver.id}, wrote ${writeCount} items to storage (${durationResolver / 1000} seconds)`,
29-
)
30-
await context.discordLog({
31-
level: 'info',
32-
message: `Wrote ${writeCount} items to storage (${durationResolver / 1000} seconds)`,
33-
title: `Synced ${resolver.type} ${resolver.address}`,
34-
url: `https://explorer.solana.com/address/${resolver.address}`,
35-
})
17+
18+
if (
19+
resolver.type === ResolverType['helius-collection-holders'] ||
20+
resolver.type === ResolverType['helius-token-holders']
21+
) {
22+
await resolve({ context, resolver, verbose })
23+
24+
const endTimeResolver = new Date().getTime()
25+
const durationResolver = endTimeResolver - startTimeResolver
26+
27+
results.push(`Synced resolver ${resolver.id} took (${durationResolver / 1000} seconds)`)
28+
await context.discordLog({
29+
level: 'info',
30+
message: `Synced resolver ${resolver.id} took (${durationResolver / 1000} seconds)`,
31+
title: `Synced ${resolver.type} ${resolver.address}`,
32+
url: `https://explorer.solana.com/address/${resolver.address}`,
33+
})
34+
}
3635
}
3736

3837
const endTime = new Date().getTime()

examples/node-redis/src/commands/command-resolver-sync.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolve, storeResolverResultMap } from '@pubkey-cache/resolver'
1+
import { resolve } from '@pubkey-cache/resolver'
22
import prompts from 'prompts'
33

44
import { getConfig } from '../lib/get-config'
@@ -25,15 +25,9 @@ export const commandResolverSync: Command = {
2525
return [new Error(`Resolver not found: ${selected}`), null]
2626
}
2727

28-
const resultMap = await resolve({ context, resolver, verbose })
28+
await resolve({ context, resolver, verbose })
2929

30-
const { writeCount } = await storeResolverResultMap({
31-
path: resolver.id,
32-
resultMap,
33-
storage: context.storage,
34-
})
35-
36-
return [null, `Synced resolver ${resolver.id}, wrote ${writeCount} items to storage`]
30+
return [null, `Synced resolver ${resolver.id}`]
3731
} catch (error) {
3832
return [new Error(error as string), null]
3933
}

examples/node-redis/src/commands/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Command } from './command'
22
import { commandBalance } from './command-balance'
3+
import { commandCollectionHolders } from './command-collection-holders'
34
import { commandDiscordLog } from './command-discord-log'
45
import { commandGenesisHash } from './command-genesis-hash'
56
import { commandHello } from './command-hello'
67
import { commandHelp } from './command-help'
7-
import { commandNftHolders } from './command-nft-holders'
88
import { commandResolverSync } from './command-resolver-sync'
99
import { commandResolverSyncAll } from './command-resolver-sync-all'
1010
import { commandResolvers } from './command-resolvers'
@@ -18,7 +18,7 @@ export const commands: Record<string, Command> = {
1818
'a-resolver-sync-all': commandResolverSyncAll,
1919
'a-resolver-sync-one': commandResolverSync,
2020
'a-resolvers': commandResolvers,
21-
'b-nft-holders': commandNftHolders,
21+
'b-collection-holders': commandCollectionHolders,
2222
'b-token-holders': commandTokenHolders,
2323
'storage-get': commandStorageGet,
2424
'storage-keys': commandStorageKeys,

packages/resolver/src/__tests__/foo-test.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Helius } from 'helius-sdk';
2+
3+
import {
4+
HeliusCollectionAssets,
5+
resolverHeliusCollectionAssets,
6+
ResolverHeliusCollectionAssetsOptions,
7+
} from '../resolvers/resolver-helius-collection-assets';
8+
9+
describe('resolverHeliusNftCollection', () => {
10+
// Test case 1: Multiple pages (1500 items total)
11+
it('fetches all assets across multiple pages', async () => {
12+
expect.assertions(7);
13+
// Arrange: Prepare mock data and behavior
14+
const collection = 'test-collection';
15+
const limit = 1000;
16+
17+
// Mock items for page 1 (1000 items) and page 2 (500 items)
18+
const mockItemsPage1 = Array.from({ length: 1000 }, (_, i) => ({ id: `asset${i}` }));
19+
const mockItemsPage2 = Array.from({ length: 500 }, (_, i) => ({ id: `asset${1000 + i}` }));
20+
const mockResponsePage1 = { items: mockItemsPage1, total: 1000 };
21+
const mockResponsePage2 = { items: mockItemsPage2, total: 500 };
22+
23+
// Mock getAssetsByGroup to return different responses based on page
24+
const mockGetAssetsByGroup = jest.fn().mockImplementation(options => {
25+
// eslint-disable-next-line jest/no-conditional-in-test
26+
if (options.page === 1) return Promise.resolve(mockResponsePage1);
27+
// eslint-disable-next-line jest/no-conditional-in-test
28+
if (options.page === 2) return Promise.resolve(mockResponsePage2);
29+
return Promise.resolve({ items: [], total: 0 });
30+
});
31+
32+
const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius;
33+
34+
const options: ResolverHeliusCollectionAssetsOptions = {
35+
collection,
36+
helius: mockHelius, // Type cast since Helius has more properties we don’t need to mock
37+
verbose: true,
38+
};
39+
40+
// Act: Call the function
41+
const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options);
42+
43+
// Assert: Verify the results
44+
expect(result.items).toHaveLength(1500); // 1000 + 500 items
45+
expect(result.limit).toBe(limit);
46+
expect(result.page).toBe(2); // Last page fetched was 2 (returned as page - 1)
47+
expect(result.total).toBe(1500); // Accumulated total from mock responses
48+
49+
// Verify mock calls
50+
expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(2);
51+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
52+
groupKey: 'collection',
53+
groupValue: collection,
54+
limit,
55+
page: 1,
56+
});
57+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
58+
groupKey: 'collection',
59+
groupValue: collection,
60+
limit,
61+
page: 2,
62+
});
63+
});
64+
65+
// Test case 2: No assets
66+
it('handles a collection with no assets', async () => {
67+
expect.assertions(6);
68+
const collection = 'empty-collection';
69+
const limit = 1000;
70+
const mockResponse = { items: [], total: 0 };
71+
72+
const mockGetAssetsByGroup = jest.fn().mockResolvedValue(mockResponse);
73+
const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius;
74+
const options: ResolverHeliusCollectionAssetsOptions = {
75+
collection,
76+
helius: mockHelius,
77+
verbose: true,
78+
};
79+
80+
const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options);
81+
82+
expect(result.items).toHaveLength(0);
83+
expect(result.limit).toBe(limit);
84+
expect(result.page).toBe(0); // Page 1 - 1, since no items were fetched
85+
expect(result.total).toBe(0);
86+
87+
expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(1);
88+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
89+
groupKey: 'collection',
90+
groupValue: collection,
91+
limit,
92+
page: 1,
93+
});
94+
});
95+
96+
// Test case 3: Exactly one page (less than limit)
97+
it('handles a single page with fewer items than the limit', async () => {
98+
expect.assertions(6);
99+
const collection = 'small-collection';
100+
const limit = 1000;
101+
const mockItems = Array.from({ length: 800 }, (_, i) => ({ id: `asset${i}` }));
102+
const mockResponse = { items: mockItems, total: 800 };
103+
104+
const mockGetAssetsByGroup = jest.fn().mockResolvedValue(mockResponse);
105+
const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius;
106+
const options: ResolverHeliusCollectionAssetsOptions = {
107+
collection,
108+
helius: mockHelius,
109+
verbose: true,
110+
};
111+
112+
const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options);
113+
114+
expect(result.items).toHaveLength(800);
115+
expect(result.limit).toBe(limit);
116+
expect(result.page).toBe(1); // Only fetched page 1
117+
expect(result.total).toBe(800);
118+
119+
expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(1);
120+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
121+
groupKey: 'collection',
122+
groupValue: collection,
123+
limit,
124+
page: 1,
125+
});
126+
});
127+
128+
// Test case 4: Exactly the limit (1000 items)
129+
it('handles a collection with exactly the limit number of items', async () => {
130+
expect.assertions(7);
131+
const collection = 'exact-limit-collection';
132+
const limit = 1000;
133+
const mockItems = Array.from({ length: 1000 }, (_, i) => ({ id: `asset${i}` }));
134+
const mockResponsePage1 = { items: mockItems, total: 1000 };
135+
const mockResponsePage2 = { items: [], total: 0 };
136+
137+
const mockGetAssetsByGroup = jest.fn().mockImplementation(options => {
138+
// eslint-disable-next-line jest/no-conditional-in-test
139+
if (options.page === 1) return Promise.resolve(mockResponsePage1);
140+
return Promise.resolve(mockResponsePage2);
141+
});
142+
const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius;
143+
const options: ResolverHeliusCollectionAssetsOptions = {
144+
collection,
145+
helius: mockHelius,
146+
verbose: true,
147+
};
148+
149+
const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options);
150+
151+
expect(result.items).toHaveLength(1000);
152+
expect(result.limit).toBe(limit);
153+
expect(result.page).toBe(1); // Fetched page 2 and got 0 items
154+
expect(result.total).toBe(1000);
155+
156+
expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(2);
157+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
158+
groupKey: 'collection',
159+
groupValue: collection,
160+
limit,
161+
page: 1,
162+
});
163+
expect(mockGetAssetsByGroup).toHaveBeenCalledWith({
164+
groupKey: 'collection',
165+
groupValue: collection,
166+
limit,
167+
page: 2,
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)