@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
text/typescript
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 };
}
}
}