UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera

158 lines (135 loc) 5.17 kB
import { StructuredTool } from '@langchain/core/tools'; import { z } from 'zod'; import { HederaAgentKit } from 'hedera-agent-kit'; import { Logger } from '@hashgraphonline/standards-sdk'; interface TokenInfo { decimals: number; [key: string]: unknown; } interface ToolWithCall { _call(input: unknown): Promise<string>; } interface AgentKitWithMirrorNode { mirrorNode?: { getTokenInfo(tokenId: string): Promise<TokenInfo>; }; network: string; } export class AirdropToolWrapper extends StructuredTool { name = 'hedera-hts-airdrop-token'; description = 'Airdrops fungible tokens to multiple recipients. Automatically converts human-readable amounts to smallest units based on token decimals.'; schema = z.object({ tokenId: z .string() .describe('The ID of the fungible token to airdrop (e.g., "0.0.yyyy").'), recipients: z .array( z.object({ accountId: z .string() .describe('Recipient account ID (e.g., "0.0.xxxx").'), amount: z .union([z.number(), z.string()]) .describe( 'Amount in human-readable format (e.g., "10" for 10 tokens).' ), }) ) .min(1) .describe('Array of recipient objects, each with accountId and amount.'), memo: z.string().optional().describe('Optional. Memo for the transaction.'), }); private originalTool: StructuredTool & ToolWithCall; private agentKit: HederaAgentKit & AgentKitWithMirrorNode; private logger: Logger; constructor(originalTool: StructuredTool, agentKit: unknown) { super(); this.originalTool = originalTool as StructuredTool & ToolWithCall; this.agentKit = agentKit as HederaAgentKit & AgentKitWithMirrorNode; this.logger = new Logger({ module: 'AirdropToolWrapper' }); } async _call(input: z.infer<typeof this.schema>): Promise<string> { try { this.logger.info( `Processing airdrop request for token ${input.tokenId} with ${input.recipients.length} recipients` ); const tokenInfo = await this.getTokenInfo(input.tokenId); const decimals = tokenInfo.decimals || 0; this.logger.info(`Token ${input.tokenId} has ${decimals} decimal places`); const convertedRecipients = input.recipients.map((recipient) => { const humanAmount = typeof recipient.amount === 'string' ? parseFloat(recipient.amount) : recipient.amount; const smallestUnitAmount = this.convertToSmallestUnits( humanAmount, decimals ); this.logger.info( `Converting amount for ${recipient.accountId}: ${humanAmount} tokens → ${smallestUnitAmount} smallest units` ); return { ...recipient, amount: smallestUnitAmount.toString(), }; }); const convertedInput = { ...input, recipients: convertedRecipients, }; this.logger.info(`Calling original airdrop tool with converted amounts`); return await this.originalTool._call(convertedInput); } catch (error) { this.logger.error('Error in airdrop tool wrapper:', error); throw error; } } private convertToSmallestUnits(amount: number, decimals: number): number { return Math.floor(amount * Math.pow(10, decimals)); } private async getTokenInfo(tokenId: string): Promise<TokenInfo> { return await this.queryTokenInfo(tokenId); } private async queryTokenInfo(tokenId: string): Promise<TokenInfo> { try { this.logger.info('Querying token info using mirror node'); const mirrorNode = this.agentKit.mirrorNode; if (!mirrorNode) { this.logger.info( 'MirrorNode not found in agentKit, attempting to access via fetch' ); const network = this.agentKit.network || 'testnet'; const mirrorNodeUrl = network === 'mainnet' ? 'https://mainnet.mirrornode.hedera.com' : 'https://testnet.mirrornode.hedera.com'; const response = await fetch( `${mirrorNodeUrl}/api/v1/tokens/${tokenId}` ); if (response.ok) { const tokenData = (await response.json()) as Record<string, unknown>; const decimals = parseInt(String(tokenData.decimals || '0')); this.logger.info( `Token ${tokenId} found with ${decimals} decimals via API` ); return { ...tokenData, decimals }; } } else { const tokenData = await mirrorNode.getTokenInfo(tokenId); if (tokenData && typeof tokenData.decimals !== 'undefined') { const decimals = parseInt(tokenData.decimals.toString()) || 0; this.logger.info(`Token ${tokenId} found with ${decimals} decimals`); return { ...tokenData, decimals }; } } throw new Error(`Token data not found or missing decimals field`); } catch (error) { this.logger.warn(`Failed to query token info for ${tokenId}:`, error); this.logger.info( 'Falling back to assumed 0 decimal places (smallest units)' ); return { decimals: 0 }; } } }