-
Notifications
You must be signed in to change notification settings - Fork 11
caLookupProvider (if not spartan-intel) #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
odilitime
wants to merge
3
commits into
1.x
Choose a base branch
from
odi-provider
base: 1.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import { describe, it, expect, beforeEach, mock } from 'bun:test'; | ||
| import type { IAgentRuntime, Memory, State } from '@elizaos/core'; | ||
| import { caLookupProvider } from '../../src/providers/ca-lookup'; | ||
| import { SOLANA_SERVICE_NAME } from '../../src/constants'; | ||
|
|
||
| describe('CA Lookup Provider', () => { | ||
| let mockRuntime: IAgentRuntime; | ||
| let mockMessage: Memory; | ||
| let mockState: State; | ||
|
|
||
| beforeEach(() => { | ||
| // Mock runtime | ||
| mockRuntime = { | ||
| logger: { | ||
| debug: mock(() => { }), | ||
| warn: mock(() => { }), | ||
| error: mock(() => { }), | ||
| log: mock(() => { }), | ||
| }, | ||
| getService: mock(() => null), | ||
| getSetting: mock(() => undefined), | ||
| } as any; | ||
|
|
||
| // Mock state | ||
| mockState = {} as State; | ||
|
|
||
| // Mock message | ||
| mockMessage = { | ||
| content: { | ||
| text: '', | ||
| }, | ||
| } as Memory; | ||
| }); | ||
|
|
||
| it('should return false when no addresses are found in message', async () => { | ||
| mockMessage.content.text = 'Hello world, no addresses here!'; | ||
|
|
||
| const detectPubkeys = mock(() => []); | ||
| const mockSolanaService = { | ||
| detectPubkeysFromString: detectPubkeys, | ||
| }; | ||
|
|
||
| mockRuntime.getService = mock(() => mockSolanaService); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(detectPubkeys).toHaveBeenCalledWith('Hello world, no addresses here!', false); | ||
| }); | ||
|
|
||
| it('should extract Solana addresses from message text', async () => { | ||
| const testAddress = 'So11111111111111111111111111111111111111112'; | ||
| mockMessage.content.text = `Check out this token: ${testAddress}`; | ||
|
|
||
| const mockSolanaService = { | ||
| detectPubkeysFromString: mock(() => [testAddress]), | ||
| getTokensSymbols: mock(async () => ({ [testAddress]: 'SOL' })), | ||
| getAddressesTypes: mock(async () => ({ [testAddress]: 'Token' })), | ||
| getDecimals: mock(async () => [9]), | ||
| getSupply: mock(async () => ({})), | ||
| }; | ||
|
|
||
| mockRuntime.getService = mock(() => mockSolanaService); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).not.toBe(false); | ||
| if (result !== false) { | ||
| expect(result.data.tokens.length).toBe(1); | ||
| expect(result.data.tokens[0].symbol).toBe('SOL'); | ||
| expect(result.data.tokens[0].address).toBe(testAddress); | ||
| expect(result.data.resolvedCount).toBe(1); | ||
| expect(result.text).toContain('SOL'); | ||
| expect(result.text).toContain(testAddress); | ||
| } | ||
| }); | ||
|
|
||
| it('should handle multiple addresses in message', async () => { | ||
| const addr1 = 'So11111111111111111111111111111111111111112'; | ||
| const addr2 = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; | ||
| mockMessage.content.text = `Compare ${addr1} and ${addr2}`; | ||
|
|
||
| const mockSolanaService = { | ||
| detectPubkeysFromString: mock(() => [addr1, addr2]), | ||
| getTokensSymbols: mock(async () => ({ | ||
| [addr1]: 'SOL', | ||
| [addr2]: 'USDC', | ||
| })), | ||
| getAddressesTypes: mock(async () => ({ | ||
| [addr1]: 'Token', | ||
| [addr2]: 'Token', | ||
| })), | ||
| getDecimals: mock(async () => [9, 6]), | ||
| getSupply: mock(async () => ({})), | ||
| }; | ||
|
|
||
| mockRuntime.getService = mock(() => mockSolanaService); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).not.toBe(false); | ||
| if (result !== false) { | ||
| expect(result.data.tokens.length).toBe(2); | ||
| expect(result.data.resolvedCount).toBe(2); | ||
| const symbols = result.data.tokens.map((t: any) => t.symbol); | ||
| expect(symbols).toContain('SOL'); | ||
| expect(symbols).toContain('USDC'); | ||
| } | ||
| }); | ||
|
|
||
| it('should return false when solana service is not available', async () => { | ||
| const testAddress = 'So11111111111111111111111111111111111111112'; | ||
| mockMessage.content.text = testAddress; | ||
|
|
||
| mockRuntime.getService = mock(() => null); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(mockRuntime.logger.warn).toHaveBeenCalled(); | ||
| expect(mockRuntime.getService).toHaveBeenCalledWith(SOLANA_SERVICE_NAME); | ||
| }); | ||
|
|
||
| it('should filter out non-token addresses', async () => { | ||
| const addr1 = 'So11111111111111111111111111111111111111112'; | ||
| const addr2 = 'WalletAddr11111111111111111111111111111111'; | ||
| mockMessage.content.text = `${addr1} ${addr2}`; | ||
|
|
||
| const mockSolanaService = { | ||
| detectPubkeysFromString: mock(() => [addr1, addr2]), | ||
| getTokensSymbols: mock(async () => ({ | ||
| [addr1]: 'SOL', | ||
| [addr2]: null, | ||
| })), | ||
| getAddressesTypes: mock(async () => ({ | ||
| [addr1]: 'Token', | ||
| [addr2]: 'Wallet', // Not a token | ||
| })), | ||
| getDecimals: mock(async () => [9, -1]), | ||
| getSupply: mock(async () => ({})), | ||
| }; | ||
|
|
||
| mockRuntime.getService = mock(() => mockSolanaService); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).not.toBe(false); | ||
| if (result !== false) { | ||
| // Only SOL token should be in results, wallet should be filtered | ||
| expect(result.data.tokens.length).toBe(1); | ||
| expect(result.data.tokens[0].symbol).toBe('SOL'); | ||
| expect(result.data.resolvedCount).toBe(1); | ||
| expect(result.data.lookupCount).toBe(2); | ||
| } | ||
| }); | ||
|
|
||
| it('should handle errors gracefully', async () => { | ||
| const testAddress = 'So11111111111111111111111111111111111111112'; | ||
| mockMessage.content.text = testAddress; | ||
|
|
||
| const mockSolanaService = { | ||
| detectPubkeysFromString: mock(() => [testAddress]), | ||
| getTokensSymbols: mock(async () => { | ||
| throw new Error('RPC error'); | ||
| }), | ||
| getAddressesTypes: mock(async () => ({})), | ||
| getDecimals: mock(async () => []), | ||
| getSupply: mock(async () => ({})), | ||
| }; | ||
|
|
||
| mockRuntime.getService = mock(() => mockSolanaService); | ||
|
|
||
| const result = await caLookupProvider.get(mockRuntime, mockMessage, mockState); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(mockRuntime.logger.error).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should have correct provider metadata', () => { | ||
| expect(caLookupProvider.name).toBe('SOLANA_CA_SYMBOL_LOOKUP'); | ||
| expect(caLookupProvider.description).toBeTruthy(); | ||
| expect(caLookupProvider.dynamic).toBe(true); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import type { IAgentRuntime, Memory, Provider, State } from '@elizaos/core'; | ||
| import { SOLANA_SERVICE_NAME } from '../constants'; | ||
|
|
||
| /** | ||
| * Provider for looking up comprehensive Solana token information | ||
| * | ||
| * This provider extracts contract addresses from messages and retrieves: | ||
| * - Symbol (from on-chain Metaplex/Token-2022 metadata) | ||
| * - Token type (Token, Token Account, Wallet, etc.) | ||
| * - Decimals | ||
| * - Total supply (when available) | ||
| * | ||
| * No LLM calls are made - this is pure data lookup from on-chain data. | ||
| */ | ||
| export const caLookupProvider: Provider = { | ||
| name: 'SOLANA_CA_SYMBOL_LOOKUP', | ||
| description: 'Looks up comprehensive Solana token information including symbol, type, decimals, and supply from on-chain data', | ||
| dynamic: true, | ||
| get: async (runtime: IAgentRuntime, message: Memory, state: State): Promise<any> => { | ||
| // Get Solana service | ||
| const solanaService = runtime.getService(SOLANA_SERVICE_NAME) as any; | ||
|
|
||
| if (!solanaService) { | ||
| runtime.logger.warn('CA lookup provider: solana service not available'); | ||
| return false; | ||
| } | ||
|
|
||
| // Extract potential addresses from the message content using Solana service's method | ||
| const messageText = message.content?.text || ''; | ||
| const addresses = solanaService.detectPubkeysFromString(messageText, false); | ||
|
|
||
| if (!addresses || addresses.length === 0) { | ||
| // No addresses found, return empty | ||
| return false; | ||
| } | ||
|
|
||
| runtime.logger.debug(`CA lookup provider found ${addresses.length} potential addresses`); | ||
|
|
||
| // Gather all token information in parallel | ||
| let symbolMap: Record<string, string | null> = {}; | ||
| let addressTypes: Record<string, string> = {}; | ||
| let decimals: number[] = []; | ||
| let supplyInfo: Record<string, { supply: bigint; decimals: number; human: any }> = {}; | ||
|
|
||
| try { | ||
| // Fetch all information in parallel for efficiency | ||
| [symbolMap, addressTypes, decimals, supplyInfo] = await Promise.all([ | ||
| solanaService.getTokensSymbols(addresses), | ||
| solanaService.getAddressesTypes(addresses), | ||
| solanaService.getDecimals(addresses), | ||
| solanaService.getSupply(addresses).catch((err: Error) => { | ||
| runtime.logger.warn(`Failed to fetch supply info: ${err.message}`); | ||
| return {}; | ||
| }) | ||
| ]); | ||
| } catch (error) { | ||
| runtime.logger.error( | ||
| `CA lookup error: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| // Build comprehensive token information | ||
| const tokenInfo: Array<{ | ||
| address: string; | ||
| symbol: string | null; | ||
| type: string; | ||
| decimals: number; | ||
| supply?: string; | ||
| }> = []; | ||
|
|
||
| for (let i = 0; i < addresses.length; i++) { | ||
| const ca = addresses[i]; | ||
| const type = addressTypes[ca] || 'Unknown'; | ||
|
|
||
| // Only include tokens (not wallets or other account types) | ||
| if (type === 'Token' || type.includes('Token')) { | ||
| const info: any = { | ||
| address: ca, | ||
| symbol: symbolMap[ca] || null, | ||
| type, | ||
| decimals: decimals[i] ?? -1, | ||
| }; | ||
|
|
||
| // Add supply if available | ||
| if (supplyInfo[ca]?.human) { | ||
| info.supply = supplyInfo[ca].human.toString(); | ||
| } | ||
|
|
||
| tokenInfo.push(info); | ||
| } | ||
| } | ||
|
|
||
| if (tokenInfo.length === 0) { | ||
| runtime.logger.debug('CA lookup: no valid tokens found'); | ||
| return false; | ||
| } | ||
|
|
||
| // Format output | ||
| let text = '\nSolana Token Information:\n'; | ||
| text += 'CA, Symbol, Type, Decimals, Supply\n'; | ||
|
|
||
| for (const info of tokenInfo) { | ||
| const parts = [ | ||
| info.address, | ||
| info.symbol || 'N/A', | ||
| info.type, | ||
| info.decimals >= 0 ? info.decimals.toString() : 'N/A', | ||
| info.supply || 'N/A' | ||
| ]; | ||
| text += parts.join(', ') + '\n'; | ||
| } | ||
|
|
||
| const data = { | ||
| tokens: tokenInfo, | ||
| lookupCount: addresses.length, | ||
| resolvedCount: tokenInfo.length, | ||
| }; | ||
|
|
||
| return { | ||
| data, | ||
| values: {}, | ||
| text, | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Bug
The
caLookupProvideraccesses token decimals by array index, while other token details use the contract address as a key. This assumes thegetDecimalsservice method preserves the input address order, which could lead to incorrect decimal assignments if not guaranteed.