Skip to content
Open
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
95 changes: 80 additions & 15 deletions packages/bot/src/adapters/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Client, GatewayIntentBits, Message, TextChannel } from 'discord.js';
import { TransactionNotificationData } from './types';
import { createTrustlineOperation } from '@chen-pilot/sdk-core';

const BACKEND_URL = process.env.NODE_URL || 'http://localhost:3000';

export class DiscordAdapter {
private client: Client;
private userChannels: Map<string, string> = new Map(); // userId -> channelId
Expand Down Expand Up @@ -32,13 +34,16 @@ export class DiscordAdapter {
this.client.on("messageCreate", async (message: Message) => {
if (message.author.bot) return;

if (message.content === "!start") {
const content = message.content;

if (content === "!start") {
await message.reply(
"Welcome to Chen Pilot! I am your AI-powered Stellar DeFi assistant."
);
return;
}

if (message.content === "!sponsor") {
if (content === "!sponsor") {
const userId = message.author.id;
await message.reply("⏳ Requesting account sponsorship...");

Expand Down Expand Up @@ -69,42 +74,102 @@ export class DiscordAdapter {
"❌ Could not reach the sponsorship service. Please try again later."
);
}
return;
}

if (message.content.startsWith('!trustline')) {
const args = message.content.split(' ').slice(1);
if (args.length < 1) {
return message.reply('Usage: !trustline <assetCode> [issuerDomain|issuerAddress]\nExample: !trustline USDC circle.com');
if (content.startsWith('!trustline')) {
const text = content.split(' ').slice(1).join(' ');
if (!text) {
return message.reply('Usage: !trustline <assetCode> [issuerDomain|issuerAddress] OR !trustline <description>\nExample: !trustline USDC circle.com OR !trustline the dollar stablecoin');
}

const assetCode = args[0];
const assetIssuer = args[1];

if (!assetIssuer) {
return message.reply(`Please provide an issuer domain or address for ${assetCode}.`);
}
const args = text.split(' ');
let assetCode = args[0];
let assetIssuer = args[1];

try {
await message.reply(`🔍 Looking up asset ${assetCode} from ${assetIssuer}...`);
const op = await createTrustlineOperation(assetCode, assetIssuer);
// If we only have one arg or it doesn't look like a code + issuer, try AI recognition
if (!assetIssuer || assetCode.length > 12) {
await message.reply(`🔍 AI is identifying the asset: "${text}"...`);
const recognized = await this.recognizeAsset(text, message.author.id);

if (recognized) {
assetCode = recognized.assetCode;
assetIssuer = recognized.issuer;
await message.reply(`💡 AI recognized this as **${assetCode}**${assetIssuer ? ` from \`${assetIssuer}\`` : ''}.\n${recognized.description}`);
} else if (!assetIssuer) {
return message.reply(`❌ Could not recognize asset from "${text}". Please provide an asset code and issuer address/domain.`);
}
}

if (!assetIssuer && assetCode !== 'XLM') {
return message.reply(`Please provide an issuer domain or address for ${assetCode}.`);
}

await message.reply(`🔍 Looking up asset ${assetCode}${assetIssuer ? ` from ${assetIssuer}` : ''}...`);
const op = await createTrustlineOperation(assetCode, assetIssuer || 'native');

let response = `✅ Found asset ${assetCode}!\n\n`;
response += `To add this trustline, you can use the following details in your wallet:\n`;
response += `**Asset:** ${assetCode}\n`;
response += `**Issuer:** \`${(op as any).asset.issuer}\`\n\n`;
response += `**Issuer:** \`${(op as any).asset.issuer || 'native'}\`\n\n`;
response += `*Note: In a future update, I will provide a direct signing link.*`;

await message.reply(response);
} catch (error) {
await message.reply(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
}
return;
}

// Handle natural language asset recognition
if (!content.startsWith('!')) {
const keywords = ['add', 'trustline', 'asset', 'coin', 'stablecoin', 'token'];
const lowercaseText = content.toLowerCase();

if (keywords.some(k => lowercaseText.includes(k))) {
try {
const recognized = await this.recognizeAsset(content, message.author.id);
if (recognized && recognized.confidence > 0.8) {
let response = `🤖 It sounds like you're talking about **${recognized.assetCode}**!\n\n`;
response += `${recognized.description}\n\n`;
response += `Would you like to add a trustline for this asset? Use \`!trustline ${recognized.assetCode} ${recognized.issuer || ''}\``;

await message.reply(response);
}
} catch (error) {
console.error("Passive AI recognition error:", error);
}
}
}
});

await this.client.login(token);
console.log("✅ Discord bot initialized.");
}

/**
* Calls the backend AI asset recognition service
*/
private async recognizeAsset(query: string, userId: string): Promise<any> {
try {
const response = await fetch(`${BACKEND_URL}/api/assets/recognize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, query })
});

const data = await response.json() as any;
if (data.success) {
return data.asset;
}
return null;
} catch (error) {
console.error("Error calling asset recognition API:", error);
return null;
}
}

/**
* Register a user to receive notifications
*/
Expand Down
94 changes: 80 additions & 14 deletions packages/bot/src/adapters/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Telegraf } from 'telegraf';
import { TransactionNotificationData } from './types';
import { createTrustlineOperation } from '@chen-pilot/sdk-core';

const BACKEND_URL = process.env.NODE_URL || 'http://localhost:3000';

export class TelegramAdapter {
private bot: Telegraf | undefined;
private token: string;
Expand All @@ -23,28 +25,41 @@ export class TelegramAdapter {
this.bot.help((ctx) => ctx.reply('Commands: /start, /balance, /swap, /trustline'));

this.bot.command('trustline', async (ctx) => {
const args = ctx.message.text.split(' ').slice(1);
if (args.length < 1) {
return ctx.reply('Usage: /trustline <assetCode> [issuerDomain|issuerAddress]\nExample: /trustline USDC circle.com');
const text = ctx.message.text.split(' ').slice(1).join(' ');
if (!text) {
return ctx.reply('Usage: /trustline <assetCode> [issuerDomain|issuerAddress] OR /trustline <description>\nExample: /trustline USDC circle.com OR /trustline the dollar stablecoin');
}

const assetCode = args[0];
const assetIssuer = args[1];

if (!assetIssuer) {
return ctx.reply(`Please provide an issuer domain or address for ${assetCode}.`);
}
const args = text.split(' ');
let assetCode = args[0];
let assetIssuer = args[1];

try {
await ctx.reply(`🔍 Looking up asset ${assetCode} from ${assetIssuer}...`);
const op = await createTrustlineOperation(assetCode, assetIssuer);
// If we only have one arg or it doesn't look like a code + issuer, try AI recognition
if (!assetIssuer || assetCode.length > 12) {
await ctx.reply(`🔍 AI is identifying the asset: "${text}"...`);
const recognized = await this.recognizeAsset(text, ctx.from.id.toString());

if (recognized) {
assetCode = recognized.assetCode;
assetIssuer = recognized.issuer;
await ctx.reply(`💡 AI recognized this as <b>${assetCode}</b>${assetIssuer ? ` from <code>${assetIssuer}</code>` : ''}.\n${recognized.description}`, { parse_mode: 'HTML' });
} else if (!assetIssuer) {
return ctx.reply(`❌ Could not recognize asset from "${text}". Please provide an asset code and issuer address/domain.`);
}
}

if (!assetIssuer && assetCode !== 'XLM') {
return ctx.reply(`Please provide an issuer domain or address for ${assetCode}.`);
}

await ctx.reply(`🔍 Looking up asset ${assetCode}${assetIssuer ? ` from ${assetIssuer}` : ''}...`);
const op = await createTrustlineOperation(assetCode, assetIssuer || 'native');

// In a real scenario, we would generate a signing link (e.g., Albedo or Stellar Laboratory)
// For now, we'll return the operation details
let message = `✅ Found asset ${assetCode}!\n\n`;
message += `To add this trustline, you can use the following details in your wallet:\n`;
message += `<b>Asset:</b> ${assetCode}\n`;
message += `<b>Issuer:</b> <code>${(op as any).asset.issuer}</code>\n\n`;
message += `<b>Issuer:</b> <code>${(op as any).asset.issuer || 'native'}</code>\n\n`;
message += `<i>Note: In a future update, I will provide a direct signing link.</i>`;

await ctx.reply(message, { parse_mode: 'HTML' });
Expand All @@ -53,10 +68,61 @@ export class TelegramAdapter {
}
});

// Handle natural language asset recognition
this.bot.on('text', async (ctx, next) => {
const text = ctx.message.text;
if (text.startsWith('/') || text.toLowerCase().includes('trustline')) {
return next();
}

// Simple heuristic: if message mentions "add", "trustline", "asset", or "coin"
const keywords = ['add', 'trustline', 'asset', 'coin', 'stablecoin', 'token'];
const lowercaseText = text.toLowerCase();

if (keywords.some(k => lowercaseText.includes(k))) {
try {
const recognized = await this.recognizeAsset(text, ctx.from.id.toString());
if (recognized && recognized.confidence > 0.8) {
let message = `🤖 It sounds like you're talking about <b>${recognized.assetCode}</b>!\n\n`;
message += `${recognized.description}\n\n`;
message += `Would you like to add a trustline for this asset? Use <code>/trustline ${recognized.assetCode} ${recognized.issuer || ''}</code>`;

await ctx.reply(message, { parse_mode: 'HTML' });
}
} catch (error) {
// Silent error for passive recognition
console.error("Passive AI recognition error:", error);
}
}
return next();
});

this.bot.launch();
console.log("✅ Telegram bot initialized.");
}

/**
* Calls the backend AI asset recognition service
*/
private async recognizeAsset(query: string, userId: string): Promise<any> {
try {
const response = await fetch(`${BACKEND_URL}/api/assets/recognize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, query })
});

const data = await response.json() as any;
if (data.success) {
return data.asset;
}
return null;
} catch (error) {
console.error("Error calling asset recognition API:", error);
return null;
}
}

/**
* Register a user to receive notifications
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Gateway/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { container } from "tsyringe";
import swaggerUi from "swagger-ui-express";
import routes from "./routes";
import authRoutes from "./auth.routes";
import assetRoutes from "./asset.routes";
import { swaggerSpec } from "./swagger";
import requestLogger from "../middleware/requestLogger";
import { ipBlacklistMiddleware, ipBlacklistRoutes } from "../Security";
Expand Down Expand Up @@ -186,6 +187,7 @@ app.post("/query", sensitiveLimiter, async (req, res, next) => {
});

app.use("/api", routes);
app.use("/api/assets", assetRoutes);
app.use("/api/security/blacklist", ipBlacklistRoutes);
app.use("/api/prompts", promptRoutes);

Expand Down
79 changes: 79 additions & 0 deletions src/Gateway/asset.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Router } from "express";
import { aiAssetRecognitionService } from "../services/aiAssetRecognition.service";
import { authenticate } from "../Auth/auth";
import { UnauthorizedError } from "../utils/error";

const router = Router();

/**
* @swagger
* /api/assets/recognize:
* post:
* summary: Recognize a Stellar asset from a description or name using AI
* tags: [Assets]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - userId
* - query
* properties:
* userId:
* type: string
* description: ID of the user
* query:
* type: string
* description: Description or name of the asset
* responses:
* 200:
* description: Asset recognized successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* asset:
* type: object
* properties:
* assetCode:
* type: string
* issuer:
* type: string
* confidence:
* type: number
* description:
* type: string
* 401:
* description: Unauthorized
*/
router.post("/recognize", async (req, res, next) => {
try {
const { userId, query } = req.body;

const user = await authenticate(userId);
if (!user) throw new UnauthorizedError("invalid credentials");

const recognizedAsset = await aiAssetRecognitionService.recognizeAsset(query, userId);

if (recognizedAsset) {
res.json({
success: true,
asset: recognizedAsset
});
} else {
res.json({
success: false,
message: "Could not recognize asset"
});
}
} catch (error) {
next(error);
}
});

export default router;
Loading