Skip to content
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

fix: quick-intel plugin optimizations & fixes. #3208

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 134 additions & 16 deletions packages/plugin-quick-intel/src/actions/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ class TokenAuditAction {
this.apiKey = apiKey;
}

private getGeckoChainId(chain: string): string {
// Only include mappings that differ from our standard chain names
const geckoSpecificMappings: { [key: string]: string } = {
'polygon': 'polygon_pos',
'avalanche': 'avax',
'gnosis': 'xdai',
'arbitrum_nova': 'arbitrum_nova',
'polygonzkevm': 'polygon-zkevm',
'ethereumpow': 'ethw'
};

const normalizedChain = chain.toLowerCase();

// Return specific mapping if it exists, otherwise return the normalized chain
return geckoSpecificMappings[normalizedChain] || normalizedChain;
}

async audit(chain: string, tokenAddress: string): Promise<AuditResponse> {
elizaLogger.log("Auditing token:", { chain, tokenAddress });
const myHeaders = new Headers();
Expand All @@ -58,40 +75,136 @@ class TokenAuditAction {
return await response.json();
}

async fetchDexData(tokenAddress: string, chain: string): Promise<DexResponse | null> {
elizaLogger.log("Fetching DEX data:", { tokenAddress, chain });
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

private async fetchDexScreenerData(tokenAddress: string, chain: string): Promise<DexResponse | null> {
elizaLogger.log("Fetching DexScreener data:", { tokenAddress, chain });
const requestOptions: RequestInit = {
method: "GET",
headers: myHeaders,
headers: { "Content-Type": "application/json" },
};

const response = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`, requestOptions);
if (!response.ok) {
throw new Error(`DexScreener API failed with status ${response.status}`);
return null;
}

const data = await response.json();
if (!data?.pairs?.length) {
return null;
}

// Filter pairs for the target chain
const chainPairs = data.pairs.filter((pair: DexPair) =>
pair.chainId.toLowerCase() === chain.toLowerCase() || pair.chainId.toLowerCase().includes(chain.toLowerCase())
pair.chainId.toLowerCase() === chain.toLowerCase() ||
pair.chainId.toLowerCase().includes(chain.toLowerCase())
);

const otherChains = data.pairs
.filter((pair: DexPair) => pair.chainId.toLowerCase() !== chain.toLowerCase())
.map((pair: DexPair) => pair.chainId) as string[];
.map((pair: DexPair): string => pair.chainId);

return {
pairs: chainPairs,
otherChains: [...new Set(otherChains)]
otherChains: Array.from(new Set(otherChains))
};
}

private async fetchGeckoTerminalData(tokenAddress: string, chain: string): Promise<DexResponse | null> {
elizaLogger.log("Fetching GeckoTerminal data:", { tokenAddress, chain });

// Convert chain to GeckoTerminal format
const geckoChain = this.getGeckoChainId(chain);

const requestOptions: RequestInit = {
method: "GET",
headers: { "Content-Type": "application/json" },
};

try {
const response = await fetch(
`https://api.geckoterminal.com/api/v2/networks/${geckoChain}/tokens/${tokenAddress}/pools?include=included&page=1`,
requestOptions
);

if (!response.ok) {
return null;
}

const data = await response.json();
if (!data?.data?.length) {
return null;
}

// Transform GeckoTerminal data to match DexScreener format
const pairs = data.data.map((pool: any) => ({
chainId: chain,
pairAddress: pool.attributes.address,
dexId: pool.relationships.dex.data.id,
pairCreatedAt: pool.attributes.pool_created_at,
priceUsd: pool.attributes.base_token_price_usd,
priceChange: {
h24: pool.attributes.price_change_percentage.h24,
h6: pool.attributes.price_change_percentage.h6,
h1: pool.attributes.price_change_percentage.h1,
m5: pool.attributes.price_change_percentage.m5
},
liquidity: {
usd: pool.attributes.reserve_in_usd
},
volume: {
h24: pool.attributes.volume_usd.h24,
h6: pool.attributes.volume_usd.h6,
h1: pool.attributes.volume_usd.h1,
m5: pool.attributes.volume_usd.m5
},
txns: {
h24: {
buys: pool.attributes.transactions.h24.buys,
sells: pool.attributes.transactions.h24.sells
},
h1: {
buys: pool.attributes.transactions.h1.buys,
sells: pool.attributes.transactions.h1.sells
}
},
baseToken: {
address: pool.relationships.base_token.data.id.split('_')[1],
name: pool.attributes.name.split(' / ')[0]
},
quoteToken: {
address: pool.relationships.quote_token.data.id.split('_')[1],
name: pool.attributes.name.split(' / ')[1].split(' ')[0]
},
fdv: pool.attributes.fdv_usd,
marketCap: pool.attributes.market_cap_usd
}));

return {
pairs,
otherChains: [] // GeckoTerminal API is chain-specific, so no other chains
};
} catch (error) {
elizaLogger.error("Error fetching GeckoTerminal data:", error);
return null;
}
}

async fetchDexData(tokenAddress: string, chain: string): Promise<DexResponse | null> {
elizaLogger.log("Fetching DEX data:", { tokenAddress, chain });

// Try DexScreener first
const dexScreenerData = await this.fetchDexScreenerData(tokenAddress, chain);
if (dexScreenerData?.pairs?.length) {
return dexScreenerData;
}

// If DexScreener returns no results, try GeckoTerminal
const geckoData = await this.fetchGeckoTerminalData(tokenAddress, chain);
if (geckoData?.pairs?.length) {
return geckoData;
}

// If both APIs return no results, return null
return null;
}
}

export const auditAction: Action = {
Expand Down Expand Up @@ -125,18 +238,23 @@ export const auditAction: Action = {
throw new Error("Could not determine chain and token address. Please specify both the chain and token address.");
}

let dexData = null;

// Perform audit
elizaLogger.log("Performing audit for:", { chain, tokenAddress });
const action = new TokenAuditAction(apiKey);
const [auditData, dexData] = await Promise.all([
action.audit(chain, tokenAddress),
action.fetchDexData(tokenAddress, chain)
]);
const auditData = await action.audit(chain, tokenAddress);

if(auditData) {
try {
dexData = await action.fetchDexData(tokenAddress, chain);
} catch(error) {}
}

const newState = await runtime.composeState(message, {
...state,
auditData: JSON.stringify(auditData, null, 2),
marketData: auditData?.tokenDetails?.tokenName ? JSON.stringify(dexData, null, 2) : null,
marketData: auditData?.tokenDetails?.tokenName && dexData ? JSON.stringify(dexData, null, 2) : null,
});


Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-quick-intel/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Market Data Results:
* Audit tools may not support all DEXs (Decentralized Exchanges).
* Buy or sell problems could cause false positives.
* Real-time data can sometimes be unreliable, or outdated.
5. **Slippage Consideration:** When a very low buy/sell tax is detected (e.g., below 1%), state in the response that this *might* be due to slippage during the trades and not necessarily a tax encoded in the contract.
5. **Slippage Consideration:** When a very low buy/sell tax is detected (e.g., taxes with decimals), state in the response that this *might* be due to slippage during the trades and not necessarily a tax encoded in the contract, but only if relevant.
6. **Character Focus & Structure:** Infuse the response with the persona of {{agentName}} using details from {{bio}} and {{lore}}. This includes determining the structure, tone, and overall presentation style. You are free to use the basic data points (risks, findings, liquidity, market, link) in a format that is appropriate for the character. The format should be a natural conversation, and not always a strict list. The user should still be able to determine the risk clearly, and any key findings should still be highlighted, but in a more dynamic format.
7. **Security Analysis (if data exists):**
* Provide an overall security assessment using simple language. Use metaphors for easier understanding where appropriate.
Expand All @@ -41,6 +41,6 @@ Market Data Results:
8. **Quick Intel link** If security data is present, include the following link for further investigation, replacing {{chain}} and {{token}} with the relevant values, and make sure it's well placed within the text:
https://app.quickintel.io/scanner?type=token&chain={{chain}}&contractAddress={{token}}
9. **No Hypotheticals:** Don't explore hypothetical or "what if" scenarios. Stick to the data you are given, and avoid speculation.
10. **User Friendly:** Format your response as a clear security analysis suitable for users, in an easy-to-understand manner, avoiding overly technical language.
10. **User Friendly:** Format your response as a clear security analysis suitable for users, in an easy-to-understand manner, avoiding overly technical language, ensuring to highlight and focus on any high risk items the user should be aware of as a focul point.

# Instructions: Based on the context above, provide your response, inline with the character {{agentName}}.` + messageCompletionFooter;
36 changes: 16 additions & 20 deletions packages/plugin-quick-intel/src/utils/chain-detection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const CHAIN_MAPPINGS = {
ethereum: ['eth', 'ethereum', 'ether', 'mainnet'],
eth: ['eth', 'ethereum', 'ether', 'mainnet'],
bsc: ['bsc', 'binance', 'bnb', 'binance smart chain', 'smartchain'],
polygon: ['polygon', 'matic', 'poly'],
arbitrum: ['arbitrum', 'arb', 'arbitrum one'],
Expand All @@ -21,7 +21,7 @@ const CHAIN_MAPPINGS = {
okex: ['okex', 'okexchain', 'okc'],
zkera: ['zkera', 'zksync era', 'era'],
zksync: ['zksync', 'zks'],
polygon_zkevm: ['polygon zkevm', 'zkevm'],
polygonzkevm: ['polygon zkevm', 'zkevm'],
linea: ['linea'],
mantle: ['mantle'],
scroll: ['scroll'],
Expand All @@ -46,7 +46,7 @@ const CHAIN_MAPPINGS = {
wanchain: ['wanchain', 'wan'],
gochain: ['gochain'],
ethereumpow: ['ethereumpow', 'ethw'],
pulsechain: ['pulsechain', 'pls'],
pulse: ['pulsechain', 'pls'],
kava: ['kava'],
milkomeda: ['milkomeda'],
nahmii: ['nahmii'],
Expand Down Expand Up @@ -88,11 +88,12 @@ function normalizeChainName(chain: string): string | null {
const normalizedInput = chain.toLowerCase().trim();

for (const [standardName, variations] of Object.entries(CHAIN_MAPPINGS)) {
if (variations.some(v => normalizedInput.includes(v))) {
if (variations.some((v: string) => normalizedInput.includes(v))) {
return standardName;
}
}
return null;

return normalizedInput;
}

export function extractTokenInfo(message: string): TokenInfo {
Expand All @@ -110,21 +111,17 @@ export function extractTokenInfo(message: string): TokenInfo {
const prepositionMatch = cleanMessage.match(prepositionPattern);

// 2. Look for chain names anywhere in the message
if (!result.chain) {
for (const [chainName, variations] of Object.entries(CHAIN_MAPPINGS)) {
if (variations.some(v => cleanMessage.includes(v))) {
result.chain = chainName;
break;
}
for (const [chainName, variations] of Object.entries(CHAIN_MAPPINGS)) {
if (variations.some((v: string) => cleanMessage.includes(v))) {
result.chain = chainName;
break;
}
}

// If chain was found through preposition pattern, normalize it
if (prepositionMatch?.[1]) {
const normalizedChain = normalizeChainName(prepositionMatch[1]);
if (normalizedChain) {
result.chain = normalizedChain;
}
// If chain wasn't found in mappings but was found through preposition pattern,
// use the exact chain name provided
if (!result.chain && prepositionMatch?.[1]) {
result.chain = normalizeChainName(prepositionMatch[1]);
}

// Find token address using different patterns
Expand All @@ -143,9 +140,8 @@ export function extractTokenInfo(message: string): TokenInfo {

// If we still don't have a chain but have an EVM address, default to ethereum
if (!result.chain && result.tokenAddress?.startsWith('0x')) {
result.chain = 'ethereum';
result.chain = 'eth';
}

return result;
}

}
Loading