diff --git a/.changeset/dull-suits-act.md b/.changeset/dull-suits-act.md new file mode 100644 index 0000000..ed60dcb --- /dev/null +++ b/.changeset/dull-suits-act.md @@ -0,0 +1,8 @@ +--- +'@pubkey-cache/resolver': patch +'@pubkey-cache/core': patch +'@pubkey-cache/react': patch +'@pubkey-cache/server': patch +--- + +change resolvers to use handler for storing data diff --git a/examples/node-redis/src/commands/command-collection-holders.ts b/examples/node-redis/src/commands/command-collection-holders.ts deleted file mode 100644 index 3ea1b10..0000000 --- a/examples/node-redis/src/commands/command-collection-holders.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { resolverHeliusCollectionHolders } from '@pubkey-cache/resolver' -import prompts from 'prompts' - -import { ensureValidPublicKey } from '../lib/ensure-valid-public-key' -import { getConfig } from '../lib/get-config' -import { Context } from '../lib/get-context' -import { Command } from './command' - -let previousValue: string | undefined -const config = getConfig() - -export const commandCollectionHolders: Command = { - action: async (ctx: Context) => { - const { collection } = await prompts({ - initial: previousValue, - message: 'Enter the public key of the collection', - name: 'collection', - type: 'text', - validate: (publicKey) => { - try { - ensureValidPublicKey(publicKey) - return true - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error: unknown) { - return false - } - }, - }) - if (collection !== previousValue) { - previousValue = collection - } - try { - ensureValidPublicKey(collection) - const result = await resolverHeliusCollectionHolders({ - collection, - helius: ctx.helius, - verbose: config.verbose, - }) - - return [null, result] - } catch (error) { - return [new Error(error as string), null] - } - }, - description: 'Get the holders of an NFT collection', - name: 'collection-holders', -} diff --git a/examples/node-redis/src/commands/command-resolver-sync-all.ts b/examples/node-redis/src/commands/command-resolver-sync-all.ts index 6f414c3..28742e7 100644 --- a/examples/node-redis/src/commands/command-resolver-sync-all.ts +++ b/examples/node-redis/src/commands/command-resolver-sync-all.ts @@ -1,30 +1,45 @@ -import { resolve, ResolverType } from '@pubkey-cache/resolver' +import { ResolverType } from '@pubkey-cache/resolver' +import { createResolverContextHelius } from '@pubkey-cache/resolver/src' +import { createResolverContextHeliusInstance } from '@pubkey-cache/resolver/src/resolvers/helius/create-resolver-context-helius-instance' import { getConfig } from '../lib/get-config' import { getContext } from '../lib/get-context' import { Command } from './command' +import { resolve } from './resolve' export const commandResolverSyncAll: Command = { action: async () => { - const { resolvers, verbose } = getConfig() + const { resolvers, verbose, heliusApiKey } = getConfig() const context = getContext() const startTime = new Date().getTime() const results: string[] = [] + const config = createResolverContextHelius({ heliusApiKey }) + const instance = createResolverContextHeliusInstance(config) try { for (const resolver of resolvers) { const startTimeResolver = new Date().getTime() - + const items: unknown[] = [] if ( resolver.type === ResolverType['helius-collection-holders'] || resolver.type === ResolverType['helius-token-holders'] ) { - await resolve({ context, resolver, verbose }) + await resolve({ + handler: (data) => { + console.log(`Handling data:`, data) + items.push(data) + return true + }, + instance, + resolver, + }) const endTimeResolver = new Date().getTime() const durationResolver = endTimeResolver - startTimeResolver - results.push(`Synced resolver ${resolver.id} took (${durationResolver / 1000} seconds)`) + results.push( + `Synced resolver ${resolver.id} took (${durationResolver / 1000} seconds) ${items.length} items resolved`, + ) await context.discordLog({ level: 'info', message: `Synced resolver ${resolver.id} took (${durationResolver / 1000} seconds)`, diff --git a/examples/node-redis/src/commands/command-resolver-sync.ts b/examples/node-redis/src/commands/command-resolver-sync.ts index 39ed71a..52b6910 100644 --- a/examples/node-redis/src/commands/command-resolver-sync.ts +++ b/examples/node-redis/src/commands/command-resolver-sync.ts @@ -1,14 +1,14 @@ -import { resolve } from '@pubkey-cache/resolver' +import { createResolverContextHelius } from '@pubkey-cache/resolver/src' +import { createResolverContextHeliusInstance } from '@pubkey-cache/resolver/src/resolvers/helius/create-resolver-context-helius-instance' import prompts from 'prompts' import { getConfig } from '../lib/get-config' -import { getContext } from '../lib/get-context' import { Command } from './command' +import { resolve } from './resolve' export const commandResolverSync: Command = { action: async () => { - const { resolvers, verbose } = getConfig() - const context = getContext() + const { resolvers, heliusApiKey } = getConfig() try { const { selected } = await prompts({ @@ -25,9 +25,21 @@ export const commandResolverSync: Command = { return [new Error(`Resolver not found: ${selected}`), null] } - await resolve({ context, resolver, verbose }) + const config = createResolverContextHelius({ heliusApiKey }) + const instance = createResolverContextHeliusInstance(config) - return [null, `Synced resolver ${resolver.id}`] + const items: unknown[] = [] + const result = await resolve({ + handler: (data) => { + console.log(`Handling data:`, data) + items.push(data) + return true + }, + instance, + resolver, + }) + + return [null, JSON.stringify({ items, result }, null, 2)] } catch (error) { return [new Error(error as string), null] } diff --git a/examples/node-redis/src/commands/command-token-holders.ts b/examples/node-redis/src/commands/command-token-holders.ts deleted file mode 100644 index 7891648..0000000 --- a/examples/node-redis/src/commands/command-token-holders.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { resolverHeliusTokenHolders } from '@pubkey-cache/resolver' -import prompts from 'prompts' - -import { ensureValidPublicKey } from '../lib/ensure-valid-public-key' -import { Context } from '../lib/get-context' -import { Command } from './command' - -let previousValue: string | undefined - -export const commandTokenHolders: Command = { - action: async (ctx: Context) => { - const { mint } = await prompts({ - initial: previousValue, - message: 'Enter the public key of the token mint', - name: 'mint', - type: 'text', - validate: (publicKey) => { - try { - ensureValidPublicKey(publicKey) - return true - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error: unknown) { - return false - } - }, - }) - if (mint !== previousValue) { - previousValue = mint - } - try { - ensureValidPublicKey(mint) - const result = await resolverHeliusTokenHolders({ - helius: ctx.helius, - mint, - }) - - return [null, result] - } catch (error) { - return [new Error(error as string), null] - } - }, - description: 'Get the holders of a token', - name: 'token-holders', -} diff --git a/examples/node-redis/src/commands/index.ts b/examples/node-redis/src/commands/index.ts index dec5c00..767fb01 100644 --- a/examples/node-redis/src/commands/index.ts +++ b/examples/node-redis/src/commands/index.ts @@ -1,6 +1,5 @@ import { Command } from './command' import { commandBalance } from './command-balance' -import { commandCollectionHolders } from './command-collection-holders' import { commandDiscordLog } from './command-discord-log' import { commandGenesisHash } from './command-genesis-hash' import { commandHello } from './command-hello' @@ -12,14 +11,11 @@ import { commandStorageGet } from './command-storage-get' import { commandStorageKeys } from './command-storage-keys' import { commandStorageRemove } from './command-storage-remove' import { commandStorageSet } from './command-storage-set' -import { commandTokenHolders } from './command-token-holders' export const commands: Record = { 'a-resolver-sync-all': commandResolverSyncAll, 'a-resolver-sync-one': commandResolverSync, 'a-resolvers': commandResolvers, - 'b-collection-holders': commandCollectionHolders, - 'b-token-holders': commandTokenHolders, 'storage-get': commandStorageGet, 'storage-keys': commandStorageKeys, 'storage-remove': commandStorageRemove, diff --git a/examples/node-redis/src/commands/resolve.ts b/examples/node-redis/src/commands/resolve.ts new file mode 100644 index 0000000..04918b6 --- /dev/null +++ b/examples/node-redis/src/commands/resolve.ts @@ -0,0 +1,29 @@ +import { resolveHeliusCollectionAssets, resolveHeliusTokenAccounts, Resolver } from '@pubkey-cache/resolver' +import { ResolverContextHeliusInstance } from '@pubkey-cache/resolver/src/resolvers/helius/types/resolver-context-helius-instance' + +export async function resolve({ + handler, + instance, + resolver, +}: { + handler: (data: unknown) => boolean + instance: ResolverContextHeliusInstance + resolver: Resolver +}) { + switch (resolver.type) { + case 'helius-collection-assets': + return await resolveHeliusCollectionAssets({ + handler, + instance, + params: { collection: resolver.address }, + }) + case 'helius-token-accounts': + return await resolveHeliusTokenAccounts({ + handler, + instance, + params: { mint: resolver.address }, + }) + default: + throw new Error(`Unknown resolver type: ${resolver.type}`) + } +} diff --git a/packages/resolver/package.json b/packages/resolver/package.json index 49b8163..c2aad2b 100644 --- a/packages/resolver/package.json +++ b/packages/resolver/package.json @@ -74,7 +74,9 @@ "maintained node versions" ], "dependencies": { - "@solana/kit": "^2.1.0" + "@solana/kit": "^2.1.0", + "gill": "^0.9.0", + "zod": "^3.24.3" }, "devDependencies": { "helius-sdk": "^1.4.2", diff --git a/packages/resolver/src/__setup__/create-mock-helius-instance.ts b/packages/resolver/src/__setup__/create-mock-helius-instance.ts new file mode 100644 index 0000000..846463f --- /dev/null +++ b/packages/resolver/src/__setup__/create-mock-helius-instance.ts @@ -0,0 +1,13 @@ +import { createSolanaClient } from 'gill'; +import { Helius } from 'helius-sdk'; + +import { createResolverContextHelius } from '../resolvers/helius/create-resolver-context-helius'; +import { ResolverContextHeliusInstance } from '../resolvers/helius/types/resolver-context-helius-instance'; + +export function createMockHeliusInstance(helius: unknown): ResolverContextHeliusInstance { + return { + context: createResolverContextHelius({ heliusApiKey: '00000000-0000-0000-0000-000000000000' }), + helius: helius as Helius, + solanaClient: createSolanaClient({ urlOrMoniker: 'localnet' }), + }; +} diff --git a/packages/resolver/src/__tests__/create-resolver-context-helius-instance-test.ts b/packages/resolver/src/__tests__/create-resolver-context-helius-instance-test.ts new file mode 100644 index 0000000..b6ceb3b --- /dev/null +++ b/packages/resolver/src/__tests__/create-resolver-context-helius-instance-test.ts @@ -0,0 +1,25 @@ +import { Helius } from 'helius-sdk'; + +import { createResolverContextHelius } from '../resolvers/helius/create-resolver-context-helius'; +import { createResolverContextHeliusInstance } from '../resolvers/helius/create-resolver-context-helius-instance'; +import { type ResolverContextHelius } from '../resolvers/helius/types/resolver-context-helius'; + +describe('create-resolver-context-helius-instance', () => { + const heliusApiKey = '00000000-0000-0000-0000-000000000000'; + + describe('expected usage', () => { + it('should create an instance with minimal config', () => { + expect.assertions(3); + // ARRANGE + const context: ResolverContextHelius = createResolverContextHelius({ + heliusApiKey, + }); + // ACT + const instance = createResolverContextHeliusInstance(context); + // ASSERT + expect(instance.context).toEqual(context); + expect(instance.helius).toBeInstanceOf(Helius); + expect(Object.keys(instance)).toEqual(['context', 'helius', 'solanaClient']); + }); + }); +}); diff --git a/packages/resolver/src/__tests__/create-resolver-context-helius-test.ts b/packages/resolver/src/__tests__/create-resolver-context-helius-test.ts new file mode 100644 index 0000000..f91e82d --- /dev/null +++ b/packages/resolver/src/__tests__/create-resolver-context-helius-test.ts @@ -0,0 +1,113 @@ +import { createResolverContextHelius } from '../resolvers/helius/create-resolver-context-helius'; +import { ResolverConfigHeliusInput } from '../resolvers/helius/types/resolver-config-helius-input'; + +describe('create-resolver-context-helius', () => { + const heliusApiKey = '00000000-0000-0000-0000-000000000000'; + + describe('expected usage', () => { + it('should create a minimal config', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigHeliusInput = { heliusApiKey }; + // ACT + const resolver = createResolverContextHelius(config); + // ASSERT + expect(resolver).toMatchInlineSnapshot(` + { + "clusters": [ + "SolanaMainnet", + "SolanaDevnet", + ], + "config": { + "heliusApiKey": "00000000-0000-0000-0000-000000000000", + "heliusCluster": "mainnet-beta", + "type": "Helius", + }, + "provides": [ + "Helius", + "SolanaClient", + ], + } + `); + }); + + it('should create a minimal config with custom cluster', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigHeliusInput = { + heliusApiKey, + heliusCluster: 'devnet', + }; + // ACT + const resolver = createResolverContextHelius(config); + // ASSERT + expect(resolver).toMatchInlineSnapshot(` + { + "clusters": [ + "SolanaMainnet", + "SolanaDevnet", + ], + "config": { + "heliusApiKey": "00000000-0000-0000-0000-000000000000", + "heliusCluster": "devnet", + "type": "Helius", + }, + "provides": [ + "Helius", + "SolanaClient", + ], + } + `); + }); + }); + + describe('unexpected usage', () => { + it('should thrown an error with an invalid api key', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigHeliusInput = { heliusApiKey: '00000000000000000000000000000000' }; + + // ASSERT + expect(() => createResolverContextHelius(config)).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "validation": "uuid", + "code": "invalid_string", + "message": "Invalid uuid", + "path": [ + "heliusApiKey" + ] + } + ]" + `); + }); + + it('should thrown an error with an invalid cluster', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigHeliusInput = { + heliusApiKey, + // @ts-expect-error we are passing in corrupt data + heliusCluster: 'testnet', + }; + + // ASSERT + expect(() => createResolverContextHelius(config)).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "received": "testnet", + "code": "invalid_enum_value", + "options": [ + "mainnet-beta", + "devnet" + ], + "path": [ + "heliusCluster" + ], + "message": "Invalid enum value. Expected 'mainnet-beta' | 'devnet', received 'testnet'" + } + ]" + `); + }); + }); +}); diff --git a/packages/resolver/src/__tests__/create-resolver-context-solana-client-instance-test.ts b/packages/resolver/src/__tests__/create-resolver-context-solana-client-instance-test.ts new file mode 100644 index 0000000..cd21825 --- /dev/null +++ b/packages/resolver/src/__tests__/create-resolver-context-solana-client-instance-test.ts @@ -0,0 +1,22 @@ +import { createResolverContextSolanaClient } from '../resolvers/solana-client/create-resolver-context-solana-client'; +import { createResolverContextSolanaClientInstance } from '../resolvers/solana-client/create-resolver-context-solana-client-instance'; +import { type ResolverContextSolanaClient } from '../resolvers/solana-client/types/resolver-context-solana-client'; + +describe('create-resolver-context-solana-client-instance', () => { + const endpoint = 'http://localhost:8899'; + + describe('expected usage', () => { + it('should create an instance with minimal config', () => { + expect.assertions(2); + // ARRANGE + const context: ResolverContextSolanaClient = createResolverContextSolanaClient({ + endpoint, + }); + // ACT + const instance = createResolverContextSolanaClientInstance(context); + // ASSERT + expect(instance.context).toEqual(context); + expect(Object.keys(instance)).toEqual(['context', 'solanaClient']); + }); + }); +}); diff --git a/packages/resolver/src/__tests__/create-resolver-context-solana-client-test.ts b/packages/resolver/src/__tests__/create-resolver-context-solana-client-test.ts new file mode 100644 index 0000000..8fc5d32 --- /dev/null +++ b/packages/resolver/src/__tests__/create-resolver-context-solana-client-test.ts @@ -0,0 +1,143 @@ +import { createResolverContextSolanaClient } from '../resolvers/solana-client/create-resolver-context-solana-client'; +import { ResolverConfigSolanaClientInput } from '../resolvers/solana-client/types/resolver-config-solana-client-input'; + +describe('create-resolver-context-solana-client', () => { + const endpoint = 'https://api.mainnet-beta.solana.com'; + + describe('expected usage', () => { + it('should create a minimal context', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigSolanaClientInput = { endpoint }; + // ACT + const context = createResolverContextSolanaClient(config); + // ASSERT + expect(context).toMatchInlineSnapshot(` + { + "clusters": [ + "SolanaCustom", + "SolanaDevnet", + "SolanaMainnet", + "SolanaTestnet", + ], + "config": { + "cluster": "mainnet", + "endpoint": "https://api.mainnet-beta.solana.com", + "endpointWs": "wss://api.mainnet-beta.solana.com", + "type": "SolanaClient", + }, + "provides": [ + "SolanaClient", + ], + } + `); + }); + + it('should create a minimal context with custom cluster', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigSolanaClientInput = { cluster: 'localnet', endpoint: 'http://localhost:8899' }; + // ACT + const context = createResolverContextSolanaClient(config); + // ASSERT + expect(context).toMatchInlineSnapshot(` + { + "clusters": [ + "SolanaCustom", + "SolanaDevnet", + "SolanaMainnet", + "SolanaTestnet", + ], + "config": { + "cluster": "localnet", + "endpoint": "http://localhost:8899", + "endpointWs": "ws://localhost:8899", + "type": "SolanaClient", + }, + "provides": [ + "SolanaClient", + ], + } + `); + }); + }); + + describe('unexpected usage', () => { + it('should thrown an error with an invalid endpoint', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigSolanaClientInput = { endpoint: 'this is not a url' }; + + // ASSERT + expect(() => createResolverContextSolanaClient(config)).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "validation": "url", + "code": "invalid_string", + "message": "Invalid url", + "path": [ + "endpoint" + ] + }, + { + "validation": "url", + "code": "invalid_string", + "message": "Invalid url", + "path": [ + "endpointWs" + ] + } + ]" + `); + }); + it('should thrown an error with an invalid endpointWs', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigSolanaClientInput = { endpoint, endpointWs: 'not a ws endpoint' }; + + // ASSERT + expect(() => createResolverContextSolanaClient(config)).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "validation": "url", + "code": "invalid_string", + "message": "Invalid url", + "path": [ + "endpointWs" + ] + } + ]" + `); + }); + + it('should thrown an error with an invalid cluster', () => { + expect.assertions(1); + // ARRANGE + const config: ResolverConfigSolanaClientInput = { + // @ts-expect-error we are passing in corrupt data + cluster: 'random', + endpoint, + }; + + // ASSERT + expect(() => createResolverContextSolanaClient(config)).toThrowErrorMatchingInlineSnapshot(` + "[ + { + "received": "random", + "code": "invalid_enum_value", + "options": [ + "mainnet", + "devnet", + "testnet", + "localnet" + ], + "path": [ + "cluster" + ], + "message": "Invalid enum value. Expected 'mainnet' | 'devnet' | 'testnet' | 'localnet', received 'random'" + } + ]" + `); + }); + }); +}); diff --git a/packages/resolver/src/__tests__/resolver-helius-nft-collection-test.ts b/packages/resolver/src/__tests__/resolve-helius-nft-collection-test.ts similarity index 60% rename from packages/resolver/src/__tests__/resolver-helius-nft-collection-test.ts rename to packages/resolver/src/__tests__/resolve-helius-nft-collection-test.ts index b8d5d5f..9ee5052 100644 --- a/packages/resolver/src/__tests__/resolver-helius-nft-collection-test.ts +++ b/packages/resolver/src/__tests__/resolve-helius-nft-collection-test.ts @@ -1,15 +1,16 @@ -import { Helius } from 'helius-sdk'; +import { DAS } from 'helius-sdk'; +import { createMockHeliusInstance } from '../__setup__/create-mock-helius-instance'; import { - HeliusCollectionAssets, - resolverHeliusCollectionAssets, - ResolverHeliusCollectionAssetsOptions, -} from '../resolvers/resolver-helius-collection-assets'; + resolveHeliusCollectionAssets, + ResolveHeliusCollectionAssetsParams, +} from '../resolvers/helius/resolve-helius-collection-assets'; +import { ResolveResult } from '../types/resolve-result'; -describe('resolverHeliusNftCollection', () => { +describe('resolve-helius-collection-assets', () => { // Test case 1: Multiple pages (1500 items total) it('fetches all assets across multiple pages', async () => { - expect.assertions(7); + expect.assertions(9); // Arrange: Prepare mock data and behavior const collection = 'test-collection'; const limit = 1000; @@ -29,22 +30,31 @@ describe('resolverHeliusNftCollection', () => { return Promise.resolve({ items: [], total: 0 }); }); - const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius; - - const options: ResolverHeliusCollectionAssetsOptions = { - collection, - helius: mockHelius, // Type cast since Helius has more properties we don’t need to mock - verbose: true, - }; + const items: DAS.GetAssetResponse[] = []; // Act: Call the function - const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options); + const result: ResolveResult = await resolveHeliusCollectionAssets({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getAssetsByGroup: mockGetAssetsByGroup } }), + params: { collection }, + }); // Assert: Verify the results - expect(result.items).toHaveLength(1500); // 1000 + 500 items + expect(items).toHaveLength(1500); // 1000 + 500 items expect(result.limit).toBe(limit); - expect(result.page).toBe(2); // Last page fetched was 2 (returned as page - 1) + expect(result.pages).toBe(2); // Last page fetched was 2 (returned as page - 1) expect(result.total).toBe(1500); // Accumulated total from mock responses + expect(result.errors).toMatchInlineSnapshot(`[]`); + expect(result.logs).toMatchInlineSnapshot(` + [ + "resolveHeliusCollectionAssets [test-collection] => Fetching page 1...", + "resolveHeliusCollectionAssets [test-collection] => Fetching page 2...", + "resolveHeliusCollectionAssets [test-collection] => No more assets found for collection test-collection", + ] + `); // Verify mock calls expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(2); @@ -68,20 +78,21 @@ describe('resolverHeliusNftCollection', () => { const collection = 'empty-collection'; const limit = 1000; const mockResponse = { items: [], total: 0 }; - const mockGetAssetsByGroup = jest.fn().mockResolvedValue(mockResponse); - const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius; - const options: ResolverHeliusCollectionAssetsOptions = { - collection, - helius: mockHelius, - verbose: true, - }; - - const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options); + const items: DAS.GetAssetResponse[] = []; + + const result: ResolveResult = await resolveHeliusCollectionAssets({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getAssetsByGroup: mockGetAssetsByGroup } }), + params: { collection }, + }); - expect(result.items).toHaveLength(0); + expect(items).toHaveLength(0); expect(result.limit).toBe(limit); - expect(result.page).toBe(0); // Page 1 - 1, since no items were fetched + expect(result.pages).toBe(0); // Page 1 - 1, since no items were fetched expect(result.total).toBe(0); expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(1); @@ -100,20 +111,22 @@ describe('resolverHeliusNftCollection', () => { const limit = 1000; const mockItems = Array.from({ length: 800 }, (_, i) => ({ id: `asset${i}` })); const mockResponse = { items: mockItems, total: 800 }; - const mockGetAssetsByGroup = jest.fn().mockResolvedValue(mockResponse); - const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius; - const options: ResolverHeliusCollectionAssetsOptions = { - collection, - helius: mockHelius, - verbose: true, - }; - - const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options); + const items: DAS.GetAssetResponse[] = []; + const params: ResolveHeliusCollectionAssetsParams = { collection }; + + const result: ResolveResult = await resolveHeliusCollectionAssets({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getAssetsByGroup: mockGetAssetsByGroup } }), + params, + }); - expect(result.items).toHaveLength(800); + expect(items).toHaveLength(800); expect(result.limit).toBe(limit); - expect(result.page).toBe(1); // Only fetched page 1 + expect(result.pages).toBe(1); // Only fetched page 1 expect(result.total).toBe(800); expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(1); @@ -139,18 +152,22 @@ describe('resolverHeliusNftCollection', () => { if (options.page === 1) return Promise.resolve(mockResponsePage1); return Promise.resolve(mockResponsePage2); }); - const mockHelius = { rpc: { getAssetsByGroup: mockGetAssetsByGroup } } as unknown as Helius; - const options: ResolverHeliusCollectionAssetsOptions = { - collection, - helius: mockHelius, - verbose: true, - }; - const result: HeliusCollectionAssets = await resolverHeliusCollectionAssets(options); + const items: DAS.GetAssetResponse[] = []; + const params: ResolveHeliusCollectionAssetsParams = { collection }; + + const result: ResolveResult = await resolveHeliusCollectionAssets({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getAssetsByGroup: mockGetAssetsByGroup } }), + params, + }); - expect(result.items).toHaveLength(1000); + expect(items).toHaveLength(1000); expect(result.limit).toBe(limit); - expect(result.page).toBe(1); // Fetched page 2 and got 0 items + expect(result.pages).toBe(1); // Fetched page 2 and got 0 items expect(result.total).toBe(1000); expect(mockGetAssetsByGroup).toHaveBeenCalledTimes(2); diff --git a/packages/resolver/src/__tests__/resolver-helius-token-accounts-test.ts b/packages/resolver/src/__tests__/resolve-helius-token-accounts-test.ts similarity index 60% rename from packages/resolver/src/__tests__/resolver-helius-token-accounts-test.ts rename to packages/resolver/src/__tests__/resolve-helius-token-accounts-test.ts index 15398dc..f5822bb 100644 --- a/packages/resolver/src/__tests__/resolver-helius-token-accounts-test.ts +++ b/packages/resolver/src/__tests__/resolve-helius-token-accounts-test.ts @@ -1,15 +1,13 @@ -import { Helius } from 'helius-sdk'; +import { DAS } from 'helius-sdk'; -import { - HeliusGetTokenAccountsOptions, - HeliusTokenAccounts, - resolverHeliusTokenAccounts, -} from '../resolvers/resolver-helius-token-accounts'; // Adjust path as needed +import { createMockHeliusInstance } from '../__setup__/create-mock-helius-instance'; // Adjust path as needed +import { resolveHeliusTokenAccounts } from '../resolvers/helius/resolve-helius-token-accounts'; +import { ResolveResult } from '../types/resolve-result'; -describe('resolverHeliusTokenAccounts', () => { +describe('resolve-helius-token-accounts', () => { // Test case 1: Multiple pages (1500 token accounts total) it('fetches all token accounts across multiple pages', async () => { - expect.assertions(7); + expect.assertions(9); // Arrange: Prepare mock data and behavior const mint = 'test-mint'; const limit = 1000; @@ -29,22 +27,31 @@ describe('resolverHeliusTokenAccounts', () => { return Promise.resolve({ token_accounts: [], total: 0 }); }); - const mockHelius = { rpc: { getTokenAccounts: mockGetTokenAccounts } } as unknown as Helius; - - const options: HeliusGetTokenAccountsOptions = { - helius: mockHelius, - mint, - verbose: true, - }; + const items: DAS.TokenAccounts[] = []; // Act: Call the function - const result: HeliusTokenAccounts = await resolverHeliusTokenAccounts(options); + const result: ResolveResult = await resolveHeliusTokenAccounts({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getTokenAccounts: mockGetTokenAccounts } }), + params: { mint }, + }); // Assert: Verify the results - expect(result.items).toHaveLength(1500); // 1000 + 500 token accounts + expect(items).toHaveLength(1500); // 1000 + 500 token accounts expect(result.limit).toBe(limit); - expect(result.page).toBe(2); // Last page fetched was 2 (returned as page - 1) + expect(result.pages).toBe(2); // Last page fetched was 2 (returned as page - 1) expect(result.total).toBe(1500); // Accumulated total from mock responses + expect(result.errors).toMatchInlineSnapshot(`[]`); + expect(result.logs).toMatchInlineSnapshot(` + [ + "resolveHeliusTokenAccounts [test-mint] => Fetching page 1...", + "resolveHeliusTokenAccounts [test-mint] => Fetching page 2...", + "resolveHeliusTokenAccounts [test-mint] => No more token accounts found for mint test-mint", + ] + `); // Verify mock calls expect(mockGetTokenAccounts).toHaveBeenCalledTimes(2); @@ -68,18 +75,20 @@ describe('resolverHeliusTokenAccounts', () => { const mockResponse = { token_accounts: [], total: 0 }; const mockGetTokenAccounts = jest.fn().mockResolvedValue(mockResponse); - const mockHelius = { rpc: { getTokenAccounts: mockGetTokenAccounts } } as unknown as Helius; - const options: HeliusGetTokenAccountsOptions = { - helius: mockHelius, - mint, - verbose: true, - }; - const result: HeliusTokenAccounts = await resolverHeliusTokenAccounts(options); + const items: DAS.TokenAccounts[] = []; + const result: ResolveResult = await resolveHeliusTokenAccounts({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getTokenAccounts: mockGetTokenAccounts } }), + params: { mint }, + }); - expect(result.items).toHaveLength(0); + expect(items).toHaveLength(0); expect(result.limit).toBe(limit); - expect(result.page).toBe(0); // Page 1 - 1, since no items were fetched + expect(result.pages).toBe(0); // Page 1 - 1, since no items were fetched expect(result.total).toBe(0); expect(mockGetTokenAccounts).toHaveBeenCalledTimes(1); @@ -99,18 +108,20 @@ describe('resolverHeliusTokenAccounts', () => { const mockResponse = { token_accounts: mockAccounts, total: 800 }; const mockGetTokenAccounts = jest.fn().mockResolvedValue(mockResponse); - const mockHelius = { rpc: { getTokenAccounts: mockGetTokenAccounts } } as unknown as Helius; - const options: HeliusGetTokenAccountsOptions = { - helius: mockHelius, - mint, - verbose: true, - }; - const result: HeliusTokenAccounts = await resolverHeliusTokenAccounts(options); + const items: DAS.TokenAccounts[] = []; + const result: ResolveResult = await resolveHeliusTokenAccounts({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getTokenAccounts: mockGetTokenAccounts } }), + params: { mint }, + }); - expect(result.items).toHaveLength(800); + expect(items).toHaveLength(800); expect(result.limit).toBe(limit); - expect(result.page).toBe(1); // Only fetched page 1 + expect(result.pages).toBe(1); // Only fetched page 1 expect(result.total).toBe(800); expect(mockGetTokenAccounts).toHaveBeenCalledTimes(1); @@ -135,18 +146,19 @@ describe('resolverHeliusTokenAccounts', () => { if (options.page === 1) return Promise.resolve(mockResponsePage1); return Promise.resolve(mockResponsePage2); }); - const mockHelius = { rpc: { getTokenAccounts: mockGetTokenAccounts } } as unknown as Helius; - const options: HeliusGetTokenAccountsOptions = { - helius: mockHelius, - mint, - verbose: true, - }; - - const result: HeliusTokenAccounts = await resolverHeliusTokenAccounts(options); + const items: DAS.TokenAccounts[] = []; + const result: ResolveResult = await resolveHeliusTokenAccounts({ + handler: page => { + items.push(...page.items); + return true; + }, + instance: createMockHeliusInstance({ rpc: { getTokenAccounts: mockGetTokenAccounts } }), + params: { mint }, + }); - expect(result.items).toHaveLength(1000); + expect(items).toHaveLength(1000); expect(result.limit).toBe(limit); - expect(result.page).toBe(1); // Fetched page 2 and got 0 items + expect(result.pages).toBe(1); // Fetched page 2 and got 0 items expect(result.total).toBe(1000); expect(mockGetTokenAccounts).toHaveBeenCalledTimes(2); diff --git a/packages/resolver/src/index.ts b/packages/resolver/src/index.ts index 9ecbe08..96c6f7e 100644 --- a/packages/resolver/src/index.ts +++ b/packages/resolver/src/index.ts @@ -5,14 +5,21 @@ export * from './get-resolver-path-owner'; export * from './get-resolver-path-snapshot-json'; export * from './get-resolver-snapshot'; export * from './parse-resolver-string'; -export * from './resolve'; -export * from './resolvers/resolver-helius-collection-assets'; -export * from './resolvers/resolver-helius-collection-holders'; -export * from './resolvers/resolver-helius-token-accounts'; -export * from './resolvers/resolver-helius-token-holders'; +export * from './resolve-helius'; +export * from './resolvers/helius/create-resolver-context-helius'; +export * from './resolvers/helius/resolve-helius-collection-assets'; +export * from './resolvers/helius/resolve-helius-token-accounts'; +export * from './resolvers/helius/types/resolver-config-helius'; +export * from './resolvers/solana-client/create-resolver-context-solana-client'; +export * from './resolvers/solana-client/types/resolver-config-solana-client'; export * from './sort-resolver-result-map'; export * from './store-resolver-result-map'; +export * from './types/network-cluster'; +export * from './types/resolve-result'; +export * from './types/resolve-result-page'; export * from './types/resolver'; +export * from './types/resolver-config'; +export * from './types/resolver-config-type'; export * from './types/resolver-context'; export * from './types/resolver-result'; export * from './types/resolver-result-map'; diff --git a/packages/resolver/src/resolve-helius.ts b/packages/resolver/src/resolve-helius.ts new file mode 100644 index 0000000..30d9570 --- /dev/null +++ b/packages/resolver/src/resolve-helius.ts @@ -0,0 +1,27 @@ +import { createResolverContextHelius } from './resolvers/helius/create-resolver-context-helius'; +import { createResolverContextHeliusInstance } from './resolvers/helius/create-resolver-context-helius-instance'; +import { resolveHeliusCollectionAssets } from './resolvers/helius/resolve-helius-collection-assets'; +import { resolveHeliusTokenAccounts } from './resolvers/helius/resolve-helius-token-accounts'; +import { ResolverConfigHelius } from './resolvers/helius/types/resolver-config-helius'; +import { Resolver } from './types/resolver'; + +export function resolveHelius({ config, resolver }: { config: ResolverConfigHelius; resolver: Resolver }) { + const context = createResolverContextHelius(config); + const instance = createResolverContextHeliusInstance(context); + switch (resolver.type) { + case 'helius-collection-assets': + return resolveHeliusCollectionAssets({ + handler: () => true, + instance, + params: { collection: resolver.address }, + }); + case 'helius-token-accounts': + return resolveHeliusTokenAccounts({ + handler: () => true, + instance, + params: { mint: resolver.address }, + }); + default: + throw new Error(`Unknown resolver type: ${resolver.type}`); + } +} diff --git a/packages/resolver/src/resolve.ts b/packages/resolver/src/resolve.ts deleted file mode 100644 index 67a53dd..0000000 --- a/packages/resolver/src/resolve.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { resolverHeliusCollectionAssets } from './resolvers/resolver-helius-collection-assets'; -import { resolverHeliusCollectionHolders } from './resolvers/resolver-helius-collection-holders'; -import { resolverHeliusTokenAccounts } from './resolvers/resolver-helius-token-accounts'; -import { resolverHeliusTokenHolders } from './resolvers/resolver-helius-token-holders'; -import { Resolver } from './types/resolver'; -import { ResolverContext } from './types/resolver-context'; - -export function resolve({ - context, - resolver, - verbose, -}: { - context: ResolverContext; - resolver: Resolver; - verbose?: boolean; -}) { - switch (resolver.type) { - case 'helius-collection-assets': - return resolverHeliusCollectionAssets({ collection: resolver.address, helius: context.helius, verbose }); - case 'helius-collection-holders': - return resolverHeliusCollectionHolders({ collection: resolver.address, helius: context.helius, verbose }); - case 'helius-token-accounts': - return resolverHeliusTokenAccounts({ helius: context.helius, mint: resolver.address, verbose }); - case 'helius-token-holders': - return resolverHeliusTokenHolders({ helius: context.helius, mint: resolver.address, verbose }); - default: - throw new Error(`Unknown resolver type: ${resolver.type}`); - } -} diff --git a/packages/resolver/src/resolvers/helius/create-resolver-context-helius-instance.ts b/packages/resolver/src/resolvers/helius/create-resolver-context-helius-instance.ts new file mode 100644 index 0000000..cbd538c --- /dev/null +++ b/packages/resolver/src/resolvers/helius/create-resolver-context-helius-instance.ts @@ -0,0 +1,18 @@ +import { createSolanaClient } from 'gill'; +import { Helius } from 'helius-sdk'; + +import { ResolverConfigType } from '../../types/resolver-config-type'; +import { ResolverContextHelius } from './types/resolver-context-helius'; +import { ResolverContextHeliusInstance } from './types/resolver-context-helius-instance'; + +export function createResolverContextHeliusInstance(context: ResolverContextHelius): ResolverContextHeliusInstance { + if (!context.provides.includes(ResolverConfigType.Helius)) { + throw new Error(`Context does not provide ResolverConfigType ${ResolverConfigType.Helius}`); + } + const helius = new Helius(context.config.heliusApiKey, context.config.heliusCluster); + return { + context, + helius, + solanaClient: createSolanaClient({ urlOrMoniker: helius.endpoints.rpc }), + }; +} diff --git a/packages/resolver/src/resolvers/helius/create-resolver-context-helius.ts b/packages/resolver/src/resolvers/helius/create-resolver-context-helius.ts new file mode 100644 index 0000000..8522f82 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/create-resolver-context-helius.ts @@ -0,0 +1,14 @@ +import { NetworkCluster } from '../../types/network-cluster'; +import { ResolverConfigType } from '../../types/resolver-config-type'; +import { ResolverConfigHeliusInput } from './types/resolver-config-helius-input'; +import { ResolverContextHelius } from './types/resolver-context-helius'; +import { validateResolverConfigHelius } from './validate-resolver-config-helius'; + +export function createResolverContextHelius(input: ResolverConfigHeliusInput): ResolverContextHelius { + const config = validateResolverConfigHelius(input); + return { + clusters: [NetworkCluster.SolanaMainnet, NetworkCluster.SolanaDevnet], + config, + provides: [ResolverConfigType.Helius, ResolverConfigType.SolanaClient], + }; +} diff --git a/packages/resolver/src/resolvers/helius/resolve-helius-collection-assets.ts b/packages/resolver/src/resolvers/helius/resolve-helius-collection-assets.ts new file mode 100644 index 0000000..b9b294b --- /dev/null +++ b/packages/resolver/src/resolvers/helius/resolve-helius-collection-assets.ts @@ -0,0 +1,59 @@ +import { DAS } from 'helius-sdk'; + +import { ResolveResult } from '../../types/resolve-result'; +import { ResolveResultPage } from '../../types/resolve-result-page'; +import { ResolverContextHeliusInstance } from './types/resolver-context-helius-instance'; + +export interface ResolveHeliusCollectionAssetsParams { + collection: string; +} + +export async function resolveHeliusCollectionAssets({ + handler, + instance, + params, +}: { + handler: (page: ResolveResultPage) => Promise | boolean; + instance: ResolverContextHeliusInstance; + params: ResolveHeliusCollectionAssetsParams; +}): Promise { + const tag = `resolveHeliusCollectionAssets [${params.collection}]`; + const result: ResolveResult = { + errors: [], + limit: 1000, + logs: [], + pages: 0, + total: 0, + }; + + // Loop through the pages of results + let page = 1; + while (result.total < page * result.limit) { + result.logs.push(`${tag} => Fetching page ${page}...`); + const assets = await instance.helius.rpc.getAssetsByGroup({ + groupKey: 'collection', + groupValue: params.collection, + limit: result.limit, + page: page, + }); + if (assets.items.length === 0) { + result.logs.push(`${tag} => No ${page > 1 ? 'more' : ''} assets found for collection ${params.collection}`); + break; + } + const handled = await handler({ items: assets.items, page }); + if (!handled) { + result.errors.push(`${tag} Error handling results for page ${page}`); + } + result.pages++; + result.total += assets.total; + page++; + + // If we got less than `list.limit` items, we're done + if (assets.items.length < result.limit) { + result.logs.push(`${tag} => No more assets found for collection ${params.collection}`); + break; + } + } + + return result; +} diff --git a/packages/resolver/src/resolvers/helius/resolve-helius-token-accounts.ts b/packages/resolver/src/resolvers/helius/resolve-helius-token-accounts.ts new file mode 100644 index 0000000..525e292 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/resolve-helius-token-accounts.ts @@ -0,0 +1,59 @@ +import { DAS } from 'helius-sdk'; + +import { ResolveResult } from '../../types/resolve-result'; +import { ResolveResultPage } from '../../types/resolve-result-page'; +import { ResolverContextHeliusInstance } from './types/resolver-context-helius-instance'; + +export interface ResolveHeliusTokenAccountsParams { + mint: string; +} + +export async function resolveHeliusTokenAccounts({ + handler, + instance, + params, +}: { + handler: (page: ResolveResultPage) => Promise | boolean; + instance: ResolverContextHeliusInstance; + params: ResolveHeliusTokenAccountsParams; +}): Promise { + const tag = `resolveHeliusTokenAccounts [${params.mint}]`; + const result: ResolveResult = { + errors: [], + limit: 1000, + logs: [], + pages: 0, + total: 0, + }; + + // Loop through the pages of results + let page = 1; + while (result.total < page * result.limit) { + result.logs.push(`${tag} => Fetching page ${page}...`); + const assets = await instance.helius.rpc.getTokenAccounts({ + limit: result.limit, + mint: params.mint, + page: page, + }); + + if (!assets?.token_accounts?.length) { + result.logs.push(`${tag} => No ${page > 1 ? 'more' : ''} token accounts found for mint ${params.mint}`); + break; + } + const handled = await handler({ items: assets.token_accounts, page }); + if (!handled) { + result.errors.push(`${tag} Error handling results for page ${page}`); + } + result.pages++; + result.total += assets.token_accounts.length; + page++; + + // If we got less than `result.limit` items, we're done + if (assets.token_accounts.length < result.limit) { + result.logs.push(`${tag} => No more token accounts found for mint ${params.mint}`); + break; + } + } + + return result; +} diff --git a/packages/resolver/src/resolvers/helius/resolver-config-helius-schema.ts b/packages/resolver/src/resolvers/helius/resolver-config-helius-schema.ts new file mode 100644 index 0000000..a6336b9 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/resolver-config-helius-schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +import { ResolverConfigType } from '../../types/resolver-config-type'; + +export const ResolverConfigHeliusSchema = z.object({ + heliusApiKey: z.string().uuid(), + heliusCluster: z.enum(['mainnet-beta', 'devnet']).default('mainnet-beta'), + type: z.literal(ResolverConfigType.Helius).default(ResolverConfigType.Helius), +}); diff --git a/packages/resolver/src/resolvers/helius/types/resolver-config-helius-input.ts b/packages/resolver/src/resolvers/helius/types/resolver-config-helius-input.ts new file mode 100644 index 0000000..1b6ef03 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/types/resolver-config-helius-input.ts @@ -0,0 +1,3 @@ +import { ResolverConfigHeliusType } from './resolver-config-helius-type'; + +export type ResolverConfigHeliusInput = Partial> & { heliusApiKey: string }; diff --git a/packages/resolver/src/resolvers/helius/types/resolver-config-helius-type.ts b/packages/resolver/src/resolvers/helius/types/resolver-config-helius-type.ts new file mode 100644 index 0000000..d688698 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/types/resolver-config-helius-type.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +import { ResolverConfigHeliusSchema } from '../resolver-config-helius-schema'; + +export type ResolverConfigHeliusType = z.infer; diff --git a/packages/resolver/src/resolvers/helius/types/resolver-config-helius.ts b/packages/resolver/src/resolvers/helius/types/resolver-config-helius.ts new file mode 100644 index 0000000..c3b307b --- /dev/null +++ b/packages/resolver/src/resolvers/helius/types/resolver-config-helius.ts @@ -0,0 +1,6 @@ +import { ResolverConfigType } from '../../../types/resolver-config-type'; +import { ResolverConfigHeliusType } from './resolver-config-helius-type'; + +export interface ResolverConfigHelius extends ResolverConfigHeliusType { + type: ResolverConfigType.Helius; +} diff --git a/packages/resolver/src/resolvers/helius/types/resolver-context-helius-instance.ts b/packages/resolver/src/resolvers/helius/types/resolver-context-helius-instance.ts new file mode 100644 index 0000000..01256bc --- /dev/null +++ b/packages/resolver/src/resolvers/helius/types/resolver-context-helius-instance.ts @@ -0,0 +1,10 @@ +import { SolanaClient } from 'gill'; +import { Helius } from 'helius-sdk'; + +import { ResolverContextHelius } from './resolver-context-helius'; + +export interface ResolverContextHeliusInstance { + context: ResolverContextHelius; + helius: Helius; + solanaClient: SolanaClient; +} diff --git a/packages/resolver/src/resolvers/helius/types/resolver-context-helius.ts b/packages/resolver/src/resolvers/helius/types/resolver-context-helius.ts new file mode 100644 index 0000000..b1379e4 --- /dev/null +++ b/packages/resolver/src/resolvers/helius/types/resolver-context-helius.ts @@ -0,0 +1,4 @@ +import { ResolverContext } from '../../../types/resolver-context'; +import { ResolverConfigHelius } from './resolver-config-helius'; + +export type ResolverContextHelius = ResolverContext; diff --git a/packages/resolver/src/resolvers/helius/validate-resolver-config-helius.ts b/packages/resolver/src/resolvers/helius/validate-resolver-config-helius.ts new file mode 100644 index 0000000..fbfee2f --- /dev/null +++ b/packages/resolver/src/resolvers/helius/validate-resolver-config-helius.ts @@ -0,0 +1,7 @@ +import { ResolverConfigHeliusSchema } from './resolver-config-helius-schema'; +import { ResolverConfigHelius } from './types/resolver-config-helius'; +import { ResolverConfigHeliusInput } from './types/resolver-config-helius-input'; + +export function validateResolverConfigHelius(input: ResolverConfigHeliusInput): ResolverConfigHelius { + return ResolverConfigHeliusSchema.parse(input); +} diff --git a/packages/resolver/src/resolvers/resolver-helius-collection-assets.ts b/packages/resolver/src/resolvers/resolver-helius-collection-assets.ts deleted file mode 100644 index 059cd23..0000000 --- a/packages/resolver/src/resolvers/resolver-helius-collection-assets.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { DAS, Helius } from 'helius-sdk'; - -export interface ResolverHeliusCollectionAssetsOptions { - collection: string; - helius: Helius; - verbose?: boolean; -} - -export interface HeliusCollectionAssets { - items: DAS.GetAssetResponse[]; - limit: number; - page: number; - total: number; -} - -/** - * Get all assets by collection - * @param options HeliusGetNftCollectionOptions - */ -export async function resolverHeliusCollectionAssets( - options: ResolverHeliusCollectionAssetsOptions, -): Promise { - let page = 1; - // Create a response list similar to the one returned by the API - const list: HeliusCollectionAssets = { items: [], limit: 1000, page, total: 0 }; - - // Loop through all pages of assets - while (list.total < page * list.limit) { - if (options.verbose) { - console.log(` => heliusGetNftCollection [${options.collection}] => Fetching page ${page}...`); - } - const assets = await options.helius.rpc.getAssetsByGroup({ - groupKey: 'collection', - groupValue: options.collection, - limit: list.limit, - page: page, - }); - if (assets.items.length === 0) { - if (options.verbose) { - console.log( - ` => heliusGetNftCollection [${options.collection}] => No ${page > 1 ? 'more' : ''} assets found for collection ${options.collection}`, - ); - } - break; - } - list.items.push(...assets.items); - list.total += assets.total; - page++; - - // If we got less than `list.limit` items, we're done - if (assets.items.length < list.limit) { - if (options.verbose) { - console.log( - ` => heliusGetNftCollection [${options.collection}] => No more assets found for collection ${options.collection}`, - ); - } - break; - } - } - - // Filter the assets by owner - const items = list.items?.length ? list?.items : []; - - // Return the list with the page offset by 1 - return { ...list, items, page: page - 1 }; -} diff --git a/packages/resolver/src/resolvers/resolver-helius-collection-holders.ts b/packages/resolver/src/resolvers/resolver-helius-collection-holders.ts deleted file mode 100644 index b11d50b..0000000 --- a/packages/resolver/src/resolvers/resolver-helius-collection-holders.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ResolverResultAsset } from '../types/resolver-result'; -import { ResolverResultMap } from '../types/resolver-result-map'; -import { - resolverHeliusCollectionAssets, - ResolverHeliusCollectionAssetsOptions, -} from './resolver-helius-collection-assets'; - -export async function resolverHeliusCollectionHolders( - options: ResolverHeliusCollectionAssetsOptions, -): Promise { - const assets = await resolverHeliusCollectionAssets(options); - - const result: ResolverResultMap = {}; - - for (const asset of assets.items) { - if (asset.ownership.owner in result) { - result[asset.ownership.owner].amount += 1; - result[asset.ownership.owner].addresses.push(asset.id); - result[asset.ownership.owner].assets.push(asset as unknown as ResolverResultAsset); - if (options.verbose) { - console.log( - ` => resolverHeliusNftHolders [${options.collection}] => Owner ${asset.ownership.owner} has multiple addresses, adding ${asset.id}`, - ); - } - continue; - } - result[asset.ownership.owner] = { - addresses: [asset.id], - amount: 1, - assets: [asset as unknown as ResolverResultAsset], - owner: asset.ownership.owner, - }; - } - for (const owner in result) { - result[owner].assets = result[owner].assets.sort((a, b) => { - const aName = a.content?.metadata?.name ?? ''; - const bName = b.content?.metadata?.name ?? ''; - if (aName < bName) { - return -1; - } - if (aName > bName) { - return 1; - } - return 0; - }); - } - - return result; -} diff --git a/packages/resolver/src/resolvers/resolver-helius-token-accounts.ts b/packages/resolver/src/resolvers/resolver-helius-token-accounts.ts deleted file mode 100644 index 554738e..0000000 --- a/packages/resolver/src/resolvers/resolver-helius-token-accounts.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { DAS, Helius } from 'helius-sdk'; - -export interface HeliusGetTokenAccountsOptions { - helius: Helius; - mint: string; - verbose?: boolean; -} - -export interface HeliusTokenAccounts { - items: DAS.TokenAccounts[]; - limit: number; - page: number; - total: number; -} - -/** - * Get token holders - * @param options HeliusGetTokenAccountsOptions - */ -export async function resolverHeliusTokenAccounts( - options: HeliusGetTokenAccountsOptions, -): Promise { - let page = 1; - // Create a response list similar to the one returned by the API - const list: HeliusTokenAccounts = { - items: [], - limit: 1000, - page, - total: 0, - }; - - // Loop through all pages of assets - while (list.total < page * list.limit) { - if (options.verbose) { - console.log(` => heliusGetTokenAccounts [${options.mint}] => Fetching page ${page}...`); - } - const assets = await options.helius.rpc.getTokenAccounts({ limit: list.limit, mint: options.mint, page: page }); - - if (!assets?.token_accounts?.length) { - if (options.verbose) { - console.log( - ` => heliusGetTokenAccounts [${options.mint}] => No ${page > 1 ? 'more' : ''} token accounts found for mint ${options.mint}`, - ); - } - break; - } - - if (assets?.token_accounts?.length === 0) { - break; - } - list.items.push(...assets.token_accounts); - list.total += assets.total ?? 0; - page++; - - // If we got less than `list.limit` items, we're done - if (assets.token_accounts.length < list.limit) { - if (options.verbose) { - console.log( - ` => heliusGetTokenAccounts [${options.mint}] => No more token accounts found for mint ${options.mint}`, - ); - } - break; - } - } - - // Filter the assets by owner - const items = list.items?.length ? list?.items : []; - - // Return the list with the page offset by 1 - return { ...list, items, page: page - 1 }; -} diff --git a/packages/resolver/src/resolvers/resolver-helius-token-holders.ts b/packages/resolver/src/resolvers/resolver-helius-token-holders.ts deleted file mode 100644 index 612410e..0000000 --- a/packages/resolver/src/resolvers/resolver-helius-token-holders.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ResolverResultAsset } from '../types/resolver-result'; -import { ResolverResultMap } from '../types/resolver-result-map'; -import { HeliusGetTokenAccountsOptions, resolverHeliusTokenAccounts } from './resolver-helius-token-accounts'; - -export async function resolverHeliusTokenHolders(options: HeliusGetTokenAccountsOptions): Promise { - const assets = await resolverHeliusTokenAccounts(options); - - const holderMap: ResolverResultMap = {}; - for (const asset of assets.items) { - if (!asset.owner || !asset.address) { - continue; - } - const amount = Number(asset.amount ?? '0'); - if (asset.owner in holderMap) { - holderMap[asset.owner].amount += amount; - holderMap[asset.owner].addresses.push(asset.address); - holderMap[asset.owner].assets.push(asset as unknown as ResolverResultAsset); - if (options.verbose) { - console.log( - ` => resolverHeliusTokenHolders [${options.mint}] => Owner ${asset.owner} has multiple addresses, adding ${asset.address}`, - ); - } - continue; - } - holderMap[asset.owner] = { - addresses: [asset.address], - amount, - assets: [asset as unknown as ResolverResultAsset], - owner: asset.owner, - }; - } - - return holderMap; -} diff --git a/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client-instance.ts b/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client-instance.ts new file mode 100644 index 0000000..9f7d94d --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client-instance.ts @@ -0,0 +1,17 @@ +import { createSolanaClient } from 'gill'; + +import { ResolverConfigType } from '../../types/resolver-config-type'; +import { ResolverContextSolanaClient } from './types/resolver-context-solana-client'; +import { ResolverContextSolanaClientInstance } from './types/resolver-context-solana-client-instance'; + +export function createResolverContextSolanaClientInstance( + context: ResolverContextSolanaClient, +): ResolverContextSolanaClientInstance { + if (!context.provides.includes(ResolverConfigType.SolanaClient)) { + throw new Error(`Context does not provide ResolverConfigType ${ResolverConfigType.SolanaClient}`); + } + return { + context, + solanaClient: createSolanaClient({ urlOrMoniker: context.config.endpoint }), + }; +} diff --git a/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client.ts b/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client.ts new file mode 100644 index 0000000..8f56424 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/create-resolver-context-solana-client.ts @@ -0,0 +1,23 @@ +import { NetworkCluster } from '../../types/network-cluster'; +import { ResolverConfigType } from '../../types/resolver-config-type'; +import { ResolverContext } from '../../types/resolver-context'; +import { ResolverConfigSolanaClient } from './types/resolver-config-solana-client'; +import { ResolverConfigSolanaClientInput } from './types/resolver-config-solana-client-input'; +import { validateResolverConfigSolanaClient } from './validate-resolver-config-solana-client'; + +export function createResolverContextSolanaClient( + input: ResolverConfigSolanaClientInput, +): ResolverContext { + const config = validateResolverConfigSolanaClient(input); + + return { + clusters: [ + NetworkCluster.SolanaCustom, + NetworkCluster.SolanaDevnet, + NetworkCluster.SolanaMainnet, + NetworkCluster.SolanaTestnet, + ], + config, + provides: [ResolverConfigType.SolanaClient], + }; +} diff --git a/packages/resolver/src/resolvers/solana-client/resolver-config-solana-client-schema.ts b/packages/resolver/src/resolvers/solana-client/resolver-config-solana-client-schema.ts new file mode 100644 index 0000000..ce502d5 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/resolver-config-solana-client-schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +import { ResolverConfigType } from '../../types/resolver-config-type'; + +export const ResolverConfigSolanaClientSchema = z.object({ + cluster: z.enum(['mainnet', 'devnet', 'testnet', 'localnet']).default('mainnet'), + endpoint: z.string().url(), + endpointWs: z.string().url().optional(), + type: z.literal(ResolverConfigType.SolanaClient).default(ResolverConfigType.SolanaClient), +}); diff --git a/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-input.ts b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-input.ts new file mode 100644 index 0000000..ded2c88 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-input.ts @@ -0,0 +1,5 @@ +import { ResolverConfigSolanaClientType } from './resolver-config-solana-client-type'; + +export type ResolverConfigSolanaClientInput = Partial> & { + endpoint: string; +}; diff --git a/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-type.ts b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-type.ts new file mode 100644 index 0000000..8f1800a --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client-type.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +import { ResolverConfigSolanaClientSchema } from '../resolver-config-solana-client-schema'; + +export type ResolverConfigSolanaClientType = z.infer; diff --git a/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client.ts b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client.ts new file mode 100644 index 0000000..6b99122 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/types/resolver-config-solana-client.ts @@ -0,0 +1,6 @@ +import { ResolverConfigType } from '../../../types/resolver-config-type'; +import { ResolverConfigSolanaClientType } from './resolver-config-solana-client-type'; + +export interface ResolverConfigSolanaClient extends ResolverConfigSolanaClientType { + type: ResolverConfigType.SolanaClient; +} diff --git a/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client-instance.ts b/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client-instance.ts new file mode 100644 index 0000000..0acdf58 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client-instance.ts @@ -0,0 +1,8 @@ +import { SolanaClient } from 'gill'; + +import { ResolverContextSolanaClient } from './resolver-context-solana-client'; + +export interface ResolverContextSolanaClientInstance { + context: ResolverContextSolanaClient; + solanaClient: SolanaClient; +} diff --git a/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client.ts b/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client.ts new file mode 100644 index 0000000..7339572 --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/types/resolver-context-solana-client.ts @@ -0,0 +1,4 @@ +import { ResolverContext } from '../../../types/resolver-context'; +import { ResolverConfigSolanaClient } from './resolver-config-solana-client'; + +export type ResolverContextSolanaClient = ResolverContext; diff --git a/packages/resolver/src/resolvers/solana-client/validate-resolver-config-solana-client.ts b/packages/resolver/src/resolvers/solana-client/validate-resolver-config-solana-client.ts new file mode 100644 index 0000000..ffd2cef --- /dev/null +++ b/packages/resolver/src/resolvers/solana-client/validate-resolver-config-solana-client.ts @@ -0,0 +1,10 @@ +import { ResolverConfigSolanaClientSchema } from './resolver-config-solana-client-schema'; +import { ResolverConfigSolanaClient } from './types/resolver-config-solana-client'; +import { ResolverConfigSolanaClientInput } from './types/resolver-config-solana-client-input'; + +export function validateResolverConfigSolanaClient(input: ResolverConfigSolanaClientInput): ResolverConfigSolanaClient { + return ResolverConfigSolanaClientSchema.parse({ + ...input, + endpointWs: input.endpointWs ?? input.endpoint.replace('http', 'ws'), + }); +} diff --git a/packages/resolver/src/types/network-cluster.ts b/packages/resolver/src/types/network-cluster.ts new file mode 100644 index 0000000..f43b380 --- /dev/null +++ b/packages/resolver/src/types/network-cluster.ts @@ -0,0 +1,6 @@ +export enum NetworkCluster { + SolanaCustom = 'SolanaCustom', + SolanaDevnet = 'SolanaDevnet', + SolanaMainnet = 'SolanaMainnet', + SolanaTestnet = 'SolanaTestnet', +} diff --git a/packages/resolver/src/types/resolve-result-page.ts b/packages/resolver/src/types/resolve-result-page.ts new file mode 100644 index 0000000..463d76a --- /dev/null +++ b/packages/resolver/src/types/resolve-result-page.ts @@ -0,0 +1,4 @@ +export interface ResolveResultPage { + items: T[]; + page: number; +} diff --git a/packages/resolver/src/types/resolve-result.ts b/packages/resolver/src/types/resolve-result.ts new file mode 100644 index 0000000..facfd9c --- /dev/null +++ b/packages/resolver/src/types/resolve-result.ts @@ -0,0 +1,7 @@ +export interface ResolveResult { + errors: string[]; + limit: number; + logs: string[]; + pages: number; + total: number; +} diff --git a/packages/resolver/src/types/resolver-config-type.ts b/packages/resolver/src/types/resolver-config-type.ts new file mode 100644 index 0000000..c0fa079 --- /dev/null +++ b/packages/resolver/src/types/resolver-config-type.ts @@ -0,0 +1,4 @@ +export enum ResolverConfigType { + Helius = 'Helius', + SolanaClient = 'SolanaClient', +} diff --git a/packages/resolver/src/types/resolver-config.ts b/packages/resolver/src/types/resolver-config.ts new file mode 100644 index 0000000..7447345 --- /dev/null +++ b/packages/resolver/src/types/resolver-config.ts @@ -0,0 +1,4 @@ +import { ResolverConfigHelius } from '../resolvers/helius/types/resolver-config-helius'; +import { ResolverConfigSolanaClient } from '../resolvers/solana-client/types/resolver-config-solana-client'; + +export type ResolverConfig = ResolverConfigHelius | ResolverConfigSolanaClient; diff --git a/packages/resolver/src/types/resolver-context.ts b/packages/resolver/src/types/resolver-context.ts index 1a1bbcc..280c5f9 100644 --- a/packages/resolver/src/types/resolver-context.ts +++ b/packages/resolver/src/types/resolver-context.ts @@ -1,5 +1,9 @@ -import { Helius } from 'helius-sdk'; +import { NetworkCluster } from './network-cluster'; +import { ResolverConfig } from './resolver-config'; +import { ResolverConfigType } from './resolver-config-type'; -export interface ResolverContext { - helius: Helius; +export interface ResolverContext { + clusters: NetworkCluster[]; + config: T; + provides: ResolverConfigType[]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df4d736..36a9029 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,10 +297,16 @@ importers: dependencies: '@solana/kit': specifier: ^2.1.0 - version: 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + version: 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + gill: + specifier: ^0.9.0 + version: 0.9.0(@solana/sysvars@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) typescript: specifier: '>=5' version: 5.7.2 + zod: + specifier: ^3.24.3 + version: 3.24.3 devDependencies: helius-sdk: specifier: ^1.4.2 @@ -2575,6 +2581,27 @@ packages: '@sinonjs/fake-timers@8.1.0': resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} + '@solana-program/address-lookup-table@0.7.0': + resolution: {integrity: sha512-dzCeIO5LtiK3bIg0AwO+TPeGURjSG2BKt0c4FRx7105AgLy7uzTktpUzUj6NXAK9SzbirI8HyvHUvw1uvL8O9A==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/compute-budget@0.7.0': + resolution: {integrity: sha512-/JJSE1fKO5zx7Z55Z2tLGWBDDi7tUE+xMlK8qqkHlY51KpqksMsIBzQMkG9Dqhoe2Cnn5/t3QK1nJKqW6eHzpg==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/system@0.7.0': + resolution: {integrity: sha512-FKTBsKHpvHHNc1ATRm7SlC5nF/VdJtOSjldhcyfMN9R7xo712Mo2jHIzvBgn8zQO5Kg0DcWuKB7268Kv1ocicw==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/token-2022@0.4.0': + resolution: {integrity: sha512-rLcYyjeRq/dW62ju9X+gFYqIIRGuD4vXq6EwM9oQBoURFbFzyo12VUi6v0hNh0dRcru+kUx321qVCAfsWWV/ug==} + peerDependencies: + '@solana/kit': ^2.1.0 + '@solana/sysvars': ^2.1.0 + '@solana/accounts@2.1.0': resolution: {integrity: sha512-1JOBiLFeIeHmGx7k1b23UWF9vM1HAh9GBMCzr5rBPrGSBs+QUgxBJ3+yrRg+UPEOSELubqo7qoOVFUKYsb1nXw==} engines: {node: '>=20.18.0'} @@ -4511,6 +4538,12 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + gill@0.9.0: + resolution: {integrity: sha512-PQzwZI83mUoUJs9hoUNjzyQZb8+NyeFHwuF3SnaZNshi8YzRrgqgHSwmjOkIkqD/6ukyRMivq7XW704tPVvVAQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5' + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6932,6 +6965,9 @@ packages: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} + zod@3.24.3: + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + snapshots: '@ampproject/remapping@2.2.1': @@ -9649,6 +9685,23 @@ snapshots: dependencies: '@sinonjs/commons': 1.8.6 + '@solana-program/address-lookup-table@0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + + '@solana-program/compute-budget@0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + + '@solana-program/system@0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + + '@solana-program/token-2022@0.4.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)))(@solana/sysvars@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2))': + dependencies: + '@solana/kit': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/sysvars': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/accounts@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)': dependencies: '@solana/addresses': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) @@ -9817,6 +9870,31 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/addresses': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/codecs': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/errors': 2.1.0(typescript@5.7.2) + '@solana/functional': 2.1.0(typescript@5.7.2) + '@solana/instructions': 2.1.0(typescript@5.7.2) + '@solana/keys': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/programs': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/rpc': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/rpc-parsed-types': 2.1.0(typescript@5.7.2) + '@solana/rpc-spec-types': 2.1.0(typescript@5.7.2) + '@solana/rpc-subscriptions': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/signers': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/sysvars': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/transaction-confirmation': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/transactions': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) @@ -9924,6 +10002,15 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-subscriptions-channel-websocket@2.1.0(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.1.0(typescript@5.7.2) + '@solana/functional': 2.1.0(typescript@5.7.2) + '@solana/rpc-subscriptions-spec': 2.1.0(typescript@5.7.2) + '@solana/subscribable': 2.1.0(typescript@5.7.2) + typescript: 5.7.2 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@2.1.0(typescript@5.7.2)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.1.0(typescript@5.7.2) @@ -9941,6 +10028,24 @@ snapshots: '@solana/subscribable': 2.1.0(typescript@5.7.2) typescript: 5.7.2 + '@solana/rpc-subscriptions@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.1.0(typescript@5.7.2) + '@solana/fast-stable-stringify': 2.1.0(typescript@5.7.2) + '@solana/functional': 2.1.0(typescript@5.7.2) + '@solana/promises': 2.1.0(typescript@5.7.2) + '@solana/rpc-spec-types': 2.1.0(typescript@5.7.2) + '@solana/rpc-subscriptions-api': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/rpc-subscriptions-channel-websocket': 2.1.0(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.1.0(typescript@5.7.2) + '@solana/rpc-transformers': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/rpc-types': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/subscribable': 2.1.0(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.1.0(typescript@5.7.2) @@ -10053,6 +10158,23 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transaction-confirmation@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/codecs-strings': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/errors': 2.1.0(typescript@5.7.2) + '@solana/keys': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/promises': 2.1.0(typescript@5.7.2) + '@solana/rpc': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/rpc-subscriptions': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/transaction-messages': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/transactions': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-confirmation@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) @@ -12000,6 +12122,22 @@ snapshots: resolve-pkg-maps: 1.0.0 optional: true + gill@0.9.0(@solana/sysvars@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + '@solana-program/address-lookup-table': 0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))) + '@solana-program/compute-budget': 0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))) + '@solana-program/system': 0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)))(@solana/sysvars@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)) + '@solana/assertions': 2.1.0(typescript@5.7.2) + '@solana/codecs': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/kit': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + typescript: 5.7.2 + transitivePeerDependencies: + - '@solana/sysvars' + - fastestsmallesttextencoderdecoder + - ws + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -14761,3 +14899,5 @@ snapshots: nanoclone: 0.2.1 property-expr: 2.0.6 toposort: 2.0.2 + + zod@3.24.3: {}