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
694 changes: 593 additions & 101 deletions packages/bot/src/adapters/discord.js

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion packages/bot/src/adapters/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,19 @@ export class DiscordAdapter {
await message.reply(
`🔍 Looking up asset ${assetCode} from ${assetIssuer}...`
);

const gate = await this.verificationService.canExecuteTrustline(
assetCode,
assetIssuer
);

if (!gate.allowed) {
await message.reply(
`🚫 Trustline blocked: ${gate.reason || gate.trustResult.details}`
);
return;
}

const op = await createTrustlineOperation(assetCode, assetIssuer);

let response = `✅ Found asset ${assetCode}!\n\n`;
Expand All @@ -462,11 +475,15 @@ export class DiscordAdapter {
response += `**Issuer:** \`${(op as any).asset.issuer}\`\n\n`;
response += `*Note: In a future update, I will provide a direct signing link.*`;

if (gate.trustResult.status === 'UNVERIFIED') {
response += `\n\n⚠️ This asset remains unverified. Proceed with caution.`;
}

await message.reply(response);
await this.logAuditAction({
action: 'TRUSTLINE_LOOKUP',
triggeredBy: message.author.id,
details: `Asset: ${assetCode}, Issuer: ${assetIssuer}`,
details: `Asset: ${assetCode}, Issuer: ${assetIssuer}, TrustStatus: ${gate.trustResult.status}`,
success: true,
timestamp: new Date().toISOString(),
});
Expand Down
171 changes: 117 additions & 54 deletions packages/bot/src/adapters/telegram.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@ exports.TelegramAdapter = void 0;
const telegraf_1 = require("telegraf");
const sdk_core_1 = require("@chen-pilot/sdk-core");
const assetVerification_1 = require("../assetVerification");
const DASHBOARD_URL = process.env.DASHBOARD_URL || `${process.env.API_BASE_URL || 'http://localhost:2333'}/dashboard`;
const rateLimiter_1 = require("../rateLimiter");
const performanceProfiler_1 = require("../performanceProfiler");
const multisigWizard_1 = require("../multisigWizard");
const BACKEND_URL = process.env.BACKEND_URL || process.env.API_BASE_URL || 'http://localhost:2333';
const DASHBOARD_URL = process.env.DASHBOARD_URL || `${BACKEND_URL}/dashboard`;
const HORIZON_URL = process.env.STELLAR_HORIZON_URL || 'https://horizon-testnet.stellar.org';
const DEBOUNCE_MS = 1000; // 1 second debounce between commands
// Commands that involve personal account data and must only be used in DMs
const DM_ONLY_COMMANDS = ['/balance'];
// Commands that start a wizard
const WIZARD_COMMANDS = ['/multisig'];
// Commands that require stricter rate limiting
const SENSITIVE_COMMANDS = ['/trustline', '/validate'];
function isDM(ctx) {
var _a;
return ((_a = ctx.chat) === null || _a === void 0 ? void 0 : _a.type) === 'private';
Expand All @@ -33,6 +42,11 @@ class TelegramAdapter {
this.lastCommandTime = new Map();
this.token = token;
this.verificationService = new assetVerification_1.AssetVerificationService(HORIZON_URL);
// #125: Initialize multisig wizard
this.multisigWizard = new multisigWizard_1.MultisigWizard();
// #123: Initialize rate limiters
this.defaultRateLimiter = new rateLimiter_1.RateLimiter(rateLimiter_1.DEFAULT_RATE_LIMIT);
this.strictRateLimiter = new rateLimiter_1.RateLimiter(rateLimiter_1.STRICT_RATE_LIMIT);
}
// #145: Returns true if the user is flooding (within debounce window)
isFlooding(userId) {
Expand All @@ -44,6 +58,21 @@ class TelegramAdapter {
this.lastCommandTime.set(userId, now);
return false;
}
// #123: Check rate limit for a user and command
checkRateLimit(userId, command) {
// Determine which rate limiter to use based on command
const isSensitive = SENSITIVE_COMMANDS.some(cmd => command.startsWith(cmd));
const rateLimiter = isSensitive ? this.strictRateLimiter : this.defaultRateLimiter;
const status = rateLimiter.check(String(userId));
if (!status.allowed) {
const retryAfter = status.retryAfter || 60;
return {
allowed: false,
message: `⏳ Rate limit exceeded. Please wait ${retryAfter} seconds before trying again.`
};
}
return { allowed: true };
}
init() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.token) {
Expand All @@ -53,40 +82,20 @@ class TelegramAdapter {
this.bot = new telegraf_1.Telegraf(this.token);
// #145: Middleware to debounce all incoming messages/commands
this.bot.use((ctx, next) => __awaiter(this, void 0, void 0, function* () {
var _a;
var _a, _b, _c;
const userId = (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id;
if (userId && this.isFlooding(userId)) {
yield ctx.reply("⏳ Please wait a moment before sending another command.");
return;
}
return next();
}));
this.bot.start((ctx) => ctx.reply('Welcome to Chen Pilot! I am your AI-powered Stellar DeFi assistant.'));
this.bot.help((ctx) => ctx.reply('Commands: /start, /balance, /swap, /trustline, /dashboard, /validate'));
this.bot.command('trustline', (ctx) => __awaiter(this, void 0, void 0, function* () {
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 assetCode = args[0];
const assetIssuer = args[1];
if (!assetIssuer) {
return ctx.reply(`Please provide an issuer domain or address for ${assetCode}.`);
}
try {
yield ctx.reply(`🔍 Looking up asset ${assetCode} from ${assetIssuer}...`);
const op = yield (0, sdk_core_1.createTrustlineOperation)(assetCode, assetIssuer);
// 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.asset.issuer}</code>\n\n`;
message += `<i>Note: In a future update, I will provide a direct signing link.</i>`;
yield ctx.reply(message, { parse_mode: "HTML" });
}
catch (error) {
yield ctx.reply(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
// #123: Rate limit check
const command = ((_c = (_b = ctx.message) === null || _b === void 0 ? void 0 : _b.text) === null || _c === void 0 ? void 0 : _c.split(' ')[0]) || '';
if (userId) {
const rateLimitResult = this.checkRateLimit(userId, command);
if (!rateLimitResult.allowed) {
yield ctx.reply(rateLimitResult.message);
return;
}
}
return next();
}));
Expand Down Expand Up @@ -131,6 +140,34 @@ class TelegramAdapter {
}
}))();
}));
// #134: Ping command — measure end-to-end latency
this.bot.command('ping', (ctx) => __awaiter(this, void 0, void 0, function* () {
var _a;
const userId = String(((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) || 'unknown');
yield (0, performanceProfiler_1.withPerformanceProfiling)('/ping', 'telegram', userId, () => __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = yield fetch(`${BACKEND_URL}/api/health`, {
method: 'GET',
signal: controller.signal,
});
clearTimeout(timeout);
const roundtripMs = Date.now() - startTime;
if (response.ok) {
yield ctx.reply(`🏓 <b>Pong!</b>\n\n📡 <b>End-to-End Latency:</b> ${roundtripMs}ms\n✅ Backend: Online`, { parse_mode: 'HTML' });
}
else {
yield ctx.reply(`🏓 <b>Pong!</b>\n\n📡 <b>End-to-End Latency:</b> ${roundtripMs}ms\n⚠️ Backend: Returned HTTP ${response.status}`, { parse_mode: 'HTML' });
}
}
catch (_a) {
const roundtripMs = Date.now() - startTime;
yield ctx.reply(`🏓 <b>Pong!</b>\n\n📡 <b>End-to-End Latency:</b> ${roundtripMs}ms\n❌ Backend: Unreachable`, { parse_mode: 'HTML' });
}
}))();
}));
// #146: Dashboard command
this.bot.command('dashboard', (ctx) => __awaiter(this, void 0, void 0, function* () {
var _a;
Expand All @@ -139,33 +176,58 @@ class TelegramAdapter {
yield ctx.reply(`📊 <b>Chen Pilot Dashboard</b>\n\nAccess your admin dashboard here:\n🔗 <a href="${DASHBOARD_URL}">Open Dashboard</a>\n\n<i>Note: You must be logged in to view the dashboard.</i>`, { parse_mode: 'HTML' });
}))();
}));
// #146: Dashboard command
this.bot.command('dashboard', (ctx) => __awaiter(this, void 0, void 0, function* () {
yield ctx.reply(`📊 <b>Chen Pilot Dashboard</b>\n\nAccess your admin dashboard here:\n🔗 <a href="${DASHBOARD_URL}">Open Dashboard</a>\n\n<i>Note: You must be logged in to view the dashboard.</i>`, { parse_mode: 'HTML' });
}));
// #148: /validate command for Stellar asset verification
this.bot.command('validate', (ctx) => __awaiter(this, void 0, void 0, function* () {
const args = ctx.message.text.split(' ').slice(1);
if (args.length < 2) {
return ctx.reply('Usage: /validate <assetCode> <issuerAddress>\nExample: /validate USDC GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5');
}
const [assetCode, issuerAddress] = args;
yield ctx.reply(`🔍 Verifying asset <b>${assetCode}</b> from issuer <code>${issuerAddress.slice(0, 8)}...</code>`, { parse_mode: 'HTML' });
try {
const result = yield this.verificationService.verifyAsset(assetCode, issuerAddress);
const statusEmoji = result.status === 'VERIFIED' ? '✅' : result.status === 'MALICIOUS' ? '🚨' : '⚠️';
let reply = `${statusEmoji} <b>Asset Verification: ${result.status}</b>\n\n`;
reply += `<b>Asset:</b> ${assetCode}\n`;
reply += `<b>Issuer:</b> <code>${issuerAddress}</code>\n`;
if (result.domain)
reply += `<b>Domain:</b> ${result.domain}\n`;
if (result.details)
reply += `<b>Details:</b> ${result.details}\n`;
reply += `\n<b>Safe to use:</b> ${result.isSafe ? 'Yes ✅' : 'No ❌'}`;
yield ctx.reply(reply, { parse_mode: 'HTML' });
var _a;
const userId = String(((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) || 'unknown');
const commandName = (0, performanceProfiler_1.extractCommandName)(ctx.message.text, 'telegram');
yield (0, performanceProfiler_1.withPerformanceProfiling)(commandName, 'telegram', userId, () => __awaiter(this, void 0, void 0, function* () {
const args = ctx.message.text.split(' ').slice(1);
if (args.length < 2) {
return ctx.reply('Usage: /validate <assetCode> <issuerAddress>\nExample: /validate USDC GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5');
}
const [assetCode, issuerAddress] = args;
yield ctx.reply(`🔍 Verifying asset <b>${assetCode}</b> from issuer <code>${issuerAddress.slice(0, 8)}...</code>`, { parse_mode: 'HTML' });
try {
const result = yield this.verificationService.verifyAsset(assetCode, issuerAddress);
const statusEmoji = result.status === 'VERIFIED' ? '✅' : result.status === 'MALICIOUS' ? '🚨' : '⚠️';
let reply = `${statusEmoji} <b>Asset Verification: ${result.status}</b>\n\n`;
reply += `<b>Asset:</b> ${assetCode}\n`;
reply += `<b>Issuer:</b> <code>${issuerAddress}</code>\n`;
if (result.domain)
reply += `<b>Domain:</b> ${result.domain}\n`;
if (result.details)
reply += `<b>Details:</b> ${result.details}\n`;
reply += `\n<b>Safe to use:</b> ${result.isSafe ? 'Yes ✅' : 'No ❌'}`;
yield ctx.reply(reply, { parse_mode: 'HTML' });
}
catch (error) {
yield ctx.reply(`❌ Verification error: ${error instanceof Error ? error.message : String(error)}`);
}
}))();
}));
// #125: Multisig wizard command
this.bot.command('multisig', (ctx) => __awaiter(this, void 0, void 0, function* () {
var _a;
const userId = String(((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) || 'unknown');
if (!isDM(ctx)) {
yield rejectPublicChannel(ctx);
return;
}
catch (error) {
yield ctx.reply(`❌ Verification error: ${error instanceof Error ? error.message : String(error)}`);
const response = this.multisigWizard.startWizard(userId, 'telegram');
yield ctx.reply(response.message);
}));
// #125: Handle wizard input (for active wizard sessions)
this.bot.use((ctx, next) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const userId = String(((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) || 'unknown');
const text = ((_b = ctx.message) === null || _b === void 0 ? void 0 : _b.text) || '';
const command = text.split(' ')[0];
const wizardState = this.multisigWizard.getWizardState(userId, 'telegram');
if (wizardState && !WIZARD_COMMANDS.includes(command)) {
const response = this.multisigWizard.processInput(userId, 'telegram', text);
yield ctx.reply(response.message);
return;
}
return next();
}));
Expand All @@ -175,6 +237,7 @@ class TelegramAdapter {
{ command: "balance", description: "Check wallet balance" },
{ command: "swap", description: "Swap assets" },
{ command: "trustline", description: "Add trustline" },
{ command: "multisig", description: "Setup multisig wallet" },
{ command: "help", description: "Show help" },
]);
this.bot.launch();
Expand Down
14 changes: 14 additions & 0 deletions packages/bot/src/adapters/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ export class TelegramAdapter {
await ctx.reply(
`🔍 Looking up asset ${assetCode} from ${assetIssuer}...`
);

const gate = await this.verificationService.canExecuteTrustline(
assetCode,
assetIssuer
);

if (!gate.allowed) {
await ctx.reply(`🚫 Trustline blocked: ${gate.reason || gate.trustResult.details}`);
return;
}

const op = await createTrustlineOperation(assetCode, assetIssuer);

// In a real scenario, we would generate a signing link (e.g., Albedo or Stellar Laboratory)
Expand All @@ -150,6 +161,9 @@ export class TelegramAdapter {
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`;
if (gate.trustResult.status === 'UNVERIFIED') {
message += `<b>Warning:</b> This asset is unverified. Proceed with caution.\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 Down
Loading