@earnforge/bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import { Bot } from "grammy";
2
+ import { EarnForge } from "@earnforge/sdk";
3
+
4
+ //#region src/index.d.ts
5
+ declare function createBot(token: string, forge: EarnForge): Bot;
6
+ declare function main(): Promise<void>;
7
+ //#endregion
8
+ export { createBot, main };
9
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/index.ts"],"mappings":";;;;iBA6HgB,SAAA,CAAU,KAAA,UAAe,KAAA,EAAO,SAAA,GAAY,GAAA;AAAA,iBA+NtC,IAAA,CAAA,GAAQ,OAAA"}
@@ -0,0 +1,235 @@
1
+ import { Bot } from "grammy";
2
+ import { createEarnForge, parseTvl } from "@earnforge/sdk";
3
+ //#region src/index.ts
4
+ const CHAIN_NAME_TO_ID = {
5
+ ethereum: 1,
6
+ optimism: 10,
7
+ bsc: 56,
8
+ gnosis: 100,
9
+ unichain: 130,
10
+ polygon: 137,
11
+ monad: 143,
12
+ sonic: 146,
13
+ mantle: 5e3,
14
+ base: 8453,
15
+ arbitrum: 42161,
16
+ celo: 42220,
17
+ avalanche: 43114,
18
+ linea: 59144,
19
+ berachain: 80094,
20
+ katana: 747474
21
+ };
22
+ const CHAIN_ID_TO_NAME = Object.fromEntries(Object.entries(CHAIN_NAME_TO_ID).map(([name, id]) => [id, name.charAt(0).toUpperCase() + name.slice(1)]));
23
+ function riskEmoji(label) {
24
+ if (label === "low") return "🟢";
25
+ if (label === "medium") return "🟡";
26
+ return "🔴";
27
+ }
28
+ function fmtApy(apy) {
29
+ return `${apy.toFixed(2)}%`;
30
+ }
31
+ function fmtTvl(vault) {
32
+ const tvl = parseTvl(vault.analytics.tvl).parsed;
33
+ if (tvl >= 1e9) return `$${(tvl / 1e9).toFixed(2)}B`;
34
+ if (tvl >= 1e6) return `$${(tvl / 1e6).toFixed(2)}M`;
35
+ if (tvl >= 1e3) return `$${(tvl / 1e3).toFixed(2)}K`;
36
+ return `$${tvl.toFixed(2)}`;
37
+ }
38
+ function fmtVaultLine(vault, risk, index) {
39
+ return `*${index}. ${escMd(vault.name)}*\n APY: \`${fmtApy(vault.analytics.apy.total)}\` | TVL: \`${fmtTvl(vault)}\`\n Protocol: \`${vault.protocol.name}\` | Chain: \`${CHAIN_ID_TO_NAME[vault.chainId] ?? vault.network}\`\n Risk: ${riskEmoji(risk.label)} \`${risk.score}/10\` (${risk.label})\n Slug: \`${vault.slug}\``;
40
+ }
41
+ function fmtRiskBreakdown(risk, vault) {
42
+ const b = risk.breakdown;
43
+ return `*Risk Score for* \`${escMd(vault.name)}\`\n\n${riskEmoji(risk.label)} *Overall: ${risk.score}/10* (${risk.label})\n\n*Breakdown:*\n TVL Magnitude: \`${b.tvl}/10\`\n APY Stability: \`${b.apyStability}/10\`\n Protocol Maturity: \`${b.protocol}/10\`\n Redeemability: \`${b.redeemability}/10\`\n Asset Type: \`${b.assetType}/10\`\n\n*Weights:* TVL 25%, APY 20%, Protocol 25%, Redeem 15%, Asset 15%`;
44
+ }
45
+ function fmtAllocation(alloc, index) {
46
+ return `*${index}. ${escMd(alloc.vault.name)}*\n Amount: \`$${alloc.amount.toFixed(2)}\` (${alloc.percentage.toFixed(1)}%)\n APY: \`${fmtApy(alloc.apy)}\` | Risk: ${riskEmoji(alloc.risk.label)} \`${alloc.risk.score}/10\`\n Chain: \`${CHAIN_ID_TO_NAME[alloc.vault.chainId] ?? alloc.vault.network}\` | Protocol: \`${alloc.vault.protocol.name}\``;
47
+ }
48
+ function fmtPreflightReport(report) {
49
+ const vault = report.vault;
50
+ let text = `*Preflight Check for* \`${escMd(vault.name)}\`\n\n`;
51
+ if (report.ok) text += "✅ *All checks passed*\n\n";
52
+ else text += "❌ *Issues found:*\n\n";
53
+ for (const issue of report.issues) {
54
+ const icon = issue.severity === "error" ? "🔴" : "🟡";
55
+ text += `${icon} \`${issue.code}\`: ${escMd(issue.message)}\n`;
56
+ }
57
+ if (report.issues.length === 0) text += "No issues detected\\.";
58
+ return text;
59
+ }
60
+ /**
61
+ * Escape special MarkdownV2 characters.
62
+ * grammy uses MarkdownV2 parse mode.
63
+ */
64
+ function escMd(text) {
65
+ return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
66
+ }
67
+ function createBot(token, forge) {
68
+ const bot = new Bot(token);
69
+ bot.command("start", async (ctx) => {
70
+ await ctx.reply("*Welcome to EarnForge Bot* 🏰\n\nDiscover, compare, and risk\\-score 623\\+ DeFi yield vaults across 16 chains via the LI\\.FI Earn API\\.\n\n*Commands:*\n/yield \\<asset\\> \\- Top 5 vaults for an asset by APY\n/top \\<chain\\> \\- Top 5 vaults on a chain\n/risk \\<slug\\> \\- Risk score breakdown for a vault\n/suggest \\<amount\\> \\<asset\\> \\- Portfolio allocation suggestion\n/doctor \\<slug\\> \\- Run preflight pitfall checks\n\n_Powered by @earnforge/sdk and LI\\.FI_", { parse_mode: "MarkdownV2" });
71
+ });
72
+ bot.command("yield", async (ctx) => {
73
+ const asset = (ctx.message?.text?.split(/\s+/).slice(1) ?? [])[0];
74
+ if (!asset) {
75
+ await ctx.reply("Usage: `/yield <asset>`\nExample: `/yield USDC`", { parse_mode: "MarkdownV2" });
76
+ return;
77
+ }
78
+ try {
79
+ const vaults = await forge.vaults.top({
80
+ asset: asset.toUpperCase(),
81
+ limit: 5
82
+ });
83
+ if (vaults.length === 0) {
84
+ await ctx.reply(`No vaults found for asset \`${escMd(asset.toUpperCase())}\`\\.`, { parse_mode: "MarkdownV2" });
85
+ return;
86
+ }
87
+ const lines = vaults.map((v, i) => fmtVaultLine(v, forge.riskScore(v), i + 1));
88
+ const text = `*Top ${vaults.length} vaults for ${escMd(asset.toUpperCase())}:*\n\n${lines.join("\n\n")}`;
89
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
90
+ } catch (err) {
91
+ await ctx.reply(`Error fetching vaults: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
92
+ }
93
+ });
94
+ bot.command("top", async (ctx) => {
95
+ const chainName = (ctx.message?.text?.split(/\s+/).slice(1) ?? [])[0]?.toLowerCase();
96
+ if (!chainName) {
97
+ const supported = Object.keys(CHAIN_NAME_TO_ID).join(", ");
98
+ await ctx.reply(`Usage: \`/top <chain>\`\nExample: \`/top base\`\n\nSupported: \`${escMd(supported)}\``, { parse_mode: "MarkdownV2" });
99
+ return;
100
+ }
101
+ const chainId = CHAIN_NAME_TO_ID[chainName];
102
+ if (chainId === void 0) {
103
+ const supported = Object.keys(CHAIN_NAME_TO_ID).join(", ");
104
+ await ctx.reply(`Unknown chain \`${escMd(chainName)}\`\\. Supported: \`${escMd(supported)}\``, { parse_mode: "MarkdownV2" });
105
+ return;
106
+ }
107
+ try {
108
+ const vaults = await forge.vaults.top({
109
+ chainId,
110
+ limit: 5
111
+ });
112
+ if (vaults.length === 0) {
113
+ await ctx.reply(`No vaults found on \`${escMd(chainName)}\`\\.`, { parse_mode: "MarkdownV2" });
114
+ return;
115
+ }
116
+ const lines = vaults.map((v, i) => fmtVaultLine(v, forge.riskScore(v), i + 1));
117
+ const header = CHAIN_ID_TO_NAME[chainId] ?? chainName;
118
+ const text = `*Top ${vaults.length} vaults on ${escMd(header)}:*\n\n${lines.join("\n\n")}`;
119
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
120
+ } catch (err) {
121
+ await ctx.reply(`Error fetching vaults: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
122
+ }
123
+ });
124
+ bot.command("risk", async (ctx) => {
125
+ const slug = (ctx.message?.text?.split(/\s+/).slice(1) ?? [])[0];
126
+ if (!slug) {
127
+ await ctx.reply("Usage: `/risk <slug>`\nExample: `/risk 8453\\-0xbeef\\.\\.\\.`", { parse_mode: "MarkdownV2" });
128
+ return;
129
+ }
130
+ try {
131
+ const vault = await forge.vaults.get(slug);
132
+ const text = fmtRiskBreakdown(forge.riskScore(vault), vault);
133
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
134
+ } catch (err) {
135
+ await ctx.reply(`Error: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
136
+ }
137
+ });
138
+ bot.command("suggest", async (ctx) => {
139
+ const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
140
+ const amountStr = args[0];
141
+ const asset = args[1];
142
+ const strategyArg = args[2]?.toLowerCase();
143
+ if (!amountStr || !asset) {
144
+ await ctx.reply("Usage: `/suggest <amount> <asset> [strategy]`\nExample: `/suggest 10000 USDC conservative`\nStrategies: conservative, max\\-apy, diversified, risk\\-adjusted", { parse_mode: "MarkdownV2" });
145
+ return;
146
+ }
147
+ const amount = Number(amountStr);
148
+ if (Number.isNaN(amount) || amount <= 0) {
149
+ await ctx.reply("Amount must be a positive number\\.", { parse_mode: "MarkdownV2" });
150
+ return;
151
+ }
152
+ const strategy = strategyArg && [
153
+ "conservative",
154
+ "max-apy",
155
+ "diversified",
156
+ "risk-adjusted"
157
+ ].includes(strategyArg) ? strategyArg : void 0;
158
+ try {
159
+ const result = await forge.suggest({
160
+ amount,
161
+ asset: asset.toUpperCase(),
162
+ maxVaults: 5,
163
+ strategy
164
+ });
165
+ if (result.allocations.length === 0) {
166
+ await ctx.reply(`No suitable vaults found for \`${escMd(asset.toUpperCase())}\`\\.`, { parse_mode: "MarkdownV2" });
167
+ return;
168
+ }
169
+ const lines = result.allocations.map((a, i) => fmtAllocation(a, i + 1));
170
+ const text = `*Portfolio Suggestion for $${escMd(amount.toLocaleString())} ${escMd(asset.toUpperCase())}:*\n\nExpected APY: \`${fmtApy(result.expectedApy)}\`\n\n` + lines.join("\n\n");
171
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
172
+ } catch (err) {
173
+ await ctx.reply(`Error: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
174
+ }
175
+ });
176
+ bot.command("doctor", async (ctx) => {
177
+ const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
178
+ const slug = args[0];
179
+ if (!slug) {
180
+ await ctx.reply("Usage: `/doctor <slug> [wallet]`\nExample: `/doctor 8453\\-0xbeef\\.\\.\\.`\nOptionally pass a wallet for balance checks\\.", { parse_mode: "MarkdownV2" });
181
+ return;
182
+ }
183
+ const wallet = args[1] && /^0x[0-9a-fA-F]{40}$/.test(args[1]) ? args[1] : "0x0000000000000000000000000000000000000000";
184
+ try {
185
+ const vault = await forge.vaults.get(slug);
186
+ const text = fmtPreflightReport(forge.preflight(vault, wallet));
187
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
188
+ } catch (err) {
189
+ await ctx.reply(`Error: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
190
+ }
191
+ });
192
+ bot.command("withdraw", async (ctx) => {
193
+ const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
194
+ const slug = args[0];
195
+ const amountStr = args[1];
196
+ if (!slug || !amountStr) {
197
+ await ctx.reply("Usage: `/withdraw <slug> <amount>`\nBuilds an unsigned redeem quote\\.", { parse_mode: "MarkdownV2" });
198
+ return;
199
+ }
200
+ try {
201
+ const vault = await forge.vaults.get(slug);
202
+ if (!vault.isRedeemable) {
203
+ await ctx.reply(`Vault \`${escMd(vault.name)}\` is not redeemable\\.`, { parse_mode: "MarkdownV2" });
204
+ return;
205
+ }
206
+ const result = await forge.buildRedeemQuote(vault, {
207
+ fromAmount: amountStr,
208
+ wallet: "0x0000000000000000000000000000000000000001"
209
+ });
210
+ const text = `*Redeem Quote* \\- \`${escMd(vault.name)}\`\n\nAmount: \`${escMd(result.humanAmount)}\` vault tokens\nReceive: \`${escMd(result.quote.estimate.toAmount)}\`\nMin: \`${escMd(result.quote.estimate.toAmountMin)}\`\nDuration: \`${result.quote.estimate.executionDuration}s\`\n\n_Sign the tx in your wallet to withdraw\\._`;
211
+ await ctx.reply(text, { parse_mode: "MarkdownV2" });
212
+ } catch (err) {
213
+ await ctx.reply(`Error: \`${escMd(String(err))}\``, { parse_mode: "MarkdownV2" });
214
+ }
215
+ });
216
+ return bot;
217
+ }
218
+ async function main() {
219
+ const token = process.env.TELEGRAM_BOT_TOKEN;
220
+ if (!token) {
221
+ console.error("TELEGRAM_BOT_TOKEN env var is required");
222
+ process.exit(1);
223
+ }
224
+ const bot = createBot(token, createEarnForge({ composerApiKey: process.env.LIFI_API_KEY }));
225
+ console.log("EarnForge Bot starting...");
226
+ bot.start();
227
+ }
228
+ if (typeof process !== "undefined" && process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) main().catch((err) => {
229
+ console.error("Fatal error:", err);
230
+ process.exit(1);
231
+ });
232
+ //#endregion
233
+ export { createBot, main };
234
+
235
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\nimport { Bot, Context } from 'grammy';\nimport {\n createEarnForge,\n type EarnForge,\n type Vault,\n type RiskScore,\n type Allocation,\n type PreflightReport,\n type StrategyPreset,\n parseTvl,\n} from '@earnforge/sdk';\n\n// ── Chain name → chainId mapping ──\n\nconst CHAIN_NAME_TO_ID: Record<string, number> = {\n ethereum: 1,\n optimism: 10,\n bsc: 56,\n gnosis: 100,\n unichain: 130,\n polygon: 137,\n monad: 143,\n sonic: 146,\n mantle: 5000,\n base: 8453,\n arbitrum: 42161,\n celo: 42220,\n avalanche: 43114,\n linea: 59144,\n berachain: 80094,\n katana: 747474,\n};\n\nconst CHAIN_ID_TO_NAME: Record<number, string> = Object.fromEntries(\n Object.entries(CHAIN_NAME_TO_ID).map(([name, id]) => [id, name.charAt(0).toUpperCase() + name.slice(1)]),\n);\n\n// ── Formatting helpers ──\n\nfunction riskEmoji(label: 'low' | 'medium' | 'high'): string {\n if (label === 'low') return '\\u{1F7E2}';\n if (label === 'medium') return '\\u{1F7E1}';\n return '\\u{1F534}';\n}\n\nfunction fmtApy(apy: number): string {\n return `${(apy).toFixed(2)}%`;\n}\n\nfunction fmtTvl(vault: Vault): string {\n const tvl = parseTvl(vault.analytics.tvl).parsed;\n if (tvl >= 1_000_000_000) return `$${(tvl / 1_000_000_000).toFixed(2)}B`;\n if (tvl >= 1_000_000) return `$${(tvl / 1_000_000).toFixed(2)}M`;\n if (tvl >= 1_000) return `$${(tvl / 1_000).toFixed(2)}K`;\n return `$${tvl.toFixed(2)}`;\n}\n\nfunction fmtVaultLine(vault: Vault, risk: RiskScore, index: number): string {\n return (\n `*${index}. ${escMd(vault.name)}*\\n` +\n ` APY: \\`${fmtApy(vault.analytics.apy.total)}\\` | TVL: \\`${fmtTvl(vault)}\\`\\n` +\n ` Protocol: \\`${vault.protocol.name}\\` | Chain: \\`${CHAIN_ID_TO_NAME[vault.chainId] ?? vault.network}\\`\\n` +\n ` Risk: ${riskEmoji(risk.label)} \\`${risk.score}/10\\` (${risk.label})\\n` +\n ` Slug: \\`${vault.slug}\\``\n );\n}\n\nfunction fmtRiskBreakdown(risk: RiskScore, vault: Vault): string {\n const b = risk.breakdown;\n return (\n `*Risk Score for* \\`${escMd(vault.name)}\\`\\n\\n` +\n `${riskEmoji(risk.label)} *Overall: ${risk.score}/10* (${risk.label})\\n\\n` +\n `*Breakdown:*\\n` +\n ` TVL Magnitude: \\`${b.tvl}/10\\`\\n` +\n ` APY Stability: \\`${b.apyStability}/10\\`\\n` +\n ` Protocol Maturity: \\`${b.protocol}/10\\`\\n` +\n ` Redeemability: \\`${b.redeemability}/10\\`\\n` +\n ` Asset Type: \\`${b.assetType}/10\\`\\n\\n` +\n `*Weights:* TVL 25%, APY 20%, Protocol 25%, Redeem 15%, Asset 15%`\n );\n}\n\nfunction fmtAllocation(alloc: Allocation, index: number): string {\n return (\n `*${index}. ${escMd(alloc.vault.name)}*\\n` +\n ` Amount: \\`$${alloc.amount.toFixed(2)}\\` (${alloc.percentage.toFixed(1)}%)\\n` +\n ` APY: \\`${fmtApy(alloc.apy)}\\` | Risk: ${riskEmoji(alloc.risk.label)} \\`${alloc.risk.score}/10\\`\\n` +\n ` Chain: \\`${CHAIN_ID_TO_NAME[alloc.vault.chainId] ?? alloc.vault.network}\\` | Protocol: \\`${alloc.vault.protocol.name}\\``\n );\n}\n\nfunction fmtPreflightReport(report: PreflightReport): string {\n const vault = report.vault;\n let text = `*Preflight Check for* \\`${escMd(vault.name)}\\`\\n\\n`;\n\n if (report.ok) {\n text += '\\u{2705} *All checks passed*\\n\\n';\n } else {\n text += '\\u{274C} *Issues found:*\\n\\n';\n }\n\n for (const issue of report.issues) {\n const icon = issue.severity === 'error' ? '\\u{1F534}' : '\\u{1F7E1}';\n text += `${icon} \\`${issue.code}\\`: ${escMd(issue.message)}\\n`;\n }\n\n if (report.issues.length === 0) {\n text += 'No issues detected\\\\.';\n }\n\n return text;\n}\n\n/**\n * Escape special MarkdownV2 characters.\n * grammy uses MarkdownV2 parse mode.\n */\nfunction escMd(text: string): string {\n return text.replace(/([_*[\\]()~`>#+\\-=|{}.!\\\\])/g, '\\\\$1');\n}\n\n// ── Bot setup ──\n\nexport function createBot(token: string, forge: EarnForge): Bot {\n const bot = new Bot(token);\n\n // /start\n bot.command('start', async (ctx: Context) => {\n const text =\n `*Welcome to EarnForge Bot* \\u{1F3F0}\\n\\n` +\n `Discover, compare, and risk\\\\-score 623\\\\+ DeFi yield vaults across 16 chains via the LI\\\\.FI Earn API\\\\.\\n\\n` +\n `*Commands:*\\n` +\n `/yield \\\\<asset\\\\> \\\\- Top 5 vaults for an asset by APY\\n` +\n `/top \\\\<chain\\\\> \\\\- Top 5 vaults on a chain\\n` +\n `/risk \\\\<slug\\\\> \\\\- Risk score breakdown for a vault\\n` +\n `/suggest \\\\<amount\\\\> \\\\<asset\\\\> \\\\- Portfolio allocation suggestion\\n` +\n `/doctor \\\\<slug\\\\> \\\\- Run preflight pitfall checks\\n\\n` +\n `_Powered by @earnforge/sdk and LI\\\\.FI_`;\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n });\n\n // /yield <asset>\n bot.command('yield', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const asset = args[0];\n if (!asset) {\n await ctx.reply('Usage: `/yield <asset>`\\nExample: `/yield USDC`', { parse_mode: 'MarkdownV2' });\n return;\n }\n\n try {\n const vaults = await forge.vaults.top({ asset: asset.toUpperCase(), limit: 5 });\n if (vaults.length === 0) {\n await ctx.reply(`No vaults found for asset \\`${escMd(asset.toUpperCase())}\\`\\\\.`, { parse_mode: 'MarkdownV2' });\n return;\n }\n\n const lines = vaults.map((v, i) => fmtVaultLine(v, forge.riskScore(v), i + 1));\n const text = `*Top ${vaults.length} vaults for ${escMd(asset.toUpperCase())}:*\\n\\n${lines.join('\\n\\n')}`;\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error fetching vaults: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n // /top <chain>\n bot.command('top', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const chainName = args[0]?.toLowerCase();\n if (!chainName) {\n const supported = Object.keys(CHAIN_NAME_TO_ID).join(', ');\n await ctx.reply(\n `Usage: \\`/top <chain>\\`\\nExample: \\`/top base\\`\\n\\nSupported: \\`${escMd(supported)}\\``,\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n const chainId = CHAIN_NAME_TO_ID[chainName];\n if (chainId === undefined) {\n const supported = Object.keys(CHAIN_NAME_TO_ID).join(', ');\n await ctx.reply(\n `Unknown chain \\`${escMd(chainName)}\\`\\\\. Supported: \\`${escMd(supported)}\\``,\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n try {\n const vaults = await forge.vaults.top({ chainId, limit: 5 });\n if (vaults.length === 0) {\n await ctx.reply(`No vaults found on \\`${escMd(chainName)}\\`\\\\.`, { parse_mode: 'MarkdownV2' });\n return;\n }\n\n const lines = vaults.map((v, i) => fmtVaultLine(v, forge.riskScore(v), i + 1));\n const header = CHAIN_ID_TO_NAME[chainId] ?? chainName;\n const text = `*Top ${vaults.length} vaults on ${escMd(header)}:*\\n\\n${lines.join('\\n\\n')}`;\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error fetching vaults: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n // /risk <slug>\n bot.command('risk', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const slug = args[0];\n if (!slug) {\n await ctx.reply('Usage: `/risk <slug>`\\nExample: `/risk 8453\\\\-0xbeef\\\\.\\\\.\\\\.`', { parse_mode: 'MarkdownV2' });\n return;\n }\n\n try {\n const vault = await forge.vaults.get(slug);\n const risk = forge.riskScore(vault);\n const text = fmtRiskBreakdown(risk, vault);\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n // /suggest <amount> <asset>\n // /suggest <amount> <asset> [strategy]\n bot.command('suggest', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const amountStr = args[0];\n const asset = args[1];\n const strategyArg = args[2]?.toLowerCase();\n\n if (!amountStr || !asset) {\n await ctx.reply(\n 'Usage: `/suggest <amount> <asset> [strategy]`\\nExample: `/suggest 10000 USDC conservative`\\nStrategies: conservative, max\\\\-apy, diversified, risk\\\\-adjusted',\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n const amount = Number(amountStr);\n if (Number.isNaN(amount) || amount <= 0) {\n await ctx.reply('Amount must be a positive number\\\\.', { parse_mode: 'MarkdownV2' });\n return;\n }\n\n const validStrategies = ['conservative', 'max-apy', 'diversified', 'risk-adjusted'];\n const strategy = strategyArg && validStrategies.includes(strategyArg)\n ? (strategyArg as StrategyPreset)\n : undefined;\n\n try {\n const result = await forge.suggest({ amount, asset: asset.toUpperCase(), maxVaults: 5, strategy });\n\n if (result.allocations.length === 0) {\n await ctx.reply(\n `No suitable vaults found for \\`${escMd(asset.toUpperCase())}\\`\\\\.`,\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n const lines = result.allocations.map((a, i) => fmtAllocation(a, i + 1));\n const text =\n `*Portfolio Suggestion for $${escMd(amount.toLocaleString())} ${escMd(asset.toUpperCase())}:*\\n\\n` +\n `Expected APY: \\`${fmtApy(result.expectedApy)}\\`\\n\\n` +\n lines.join('\\n\\n');\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n // /doctor <slug> [wallet]\n bot.command('doctor', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const slug = args[0];\n if (!slug) {\n await ctx.reply(\n 'Usage: `/doctor <slug> [wallet]`\\nExample: `/doctor 8453\\\\-0xbeef\\\\.\\\\.\\\\.`\\nOptionally pass a wallet for balance checks\\\\.',\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n const wallet = args[1] && /^0x[0-9a-fA-F]{40}$/.test(args[1])\n ? args[1]\n : '0x0000000000000000000000000000000000000000';\n\n try {\n const vault = await forge.vaults.get(slug);\n const report = forge.preflight(vault, wallet);\n const text = fmtPreflightReport(report);\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n // /withdraw <slug> <amount>\n bot.command('withdraw', async (ctx: Context) => {\n const args = ctx.message?.text?.split(/\\s+/).slice(1) ?? [];\n const slug = args[0];\n const amountStr = args[1];\n\n if (!slug || !amountStr) {\n await ctx.reply(\n 'Usage: `/withdraw <slug> <amount>`\\nBuilds an unsigned redeem quote\\\\.',\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n try {\n const vault = await forge.vaults.get(slug);\n if (!vault.isRedeemable) {\n await ctx.reply(\n `Vault \\`${escMd(vault.name)}\\` is not redeemable\\\\.`,\n { parse_mode: 'MarkdownV2' },\n );\n return;\n }\n\n const result = await forge.buildRedeemQuote(vault, {\n fromAmount: amountStr,\n wallet: '0x0000000000000000000000000000000000000001',\n });\n\n const text =\n `*Redeem Quote* \\\\- \\`${escMd(vault.name)}\\`\\n\\n` +\n `Amount: \\`${escMd(result.humanAmount)}\\` vault tokens\\n` +\n `Receive: \\`${escMd(result.quote.estimate.toAmount)}\\`\\n` +\n `Min: \\`${escMd(result.quote.estimate.toAmountMin)}\\`\\n` +\n `Duration: \\`${result.quote.estimate.executionDuration}s\\`\\n\\n` +\n `_Sign the tx in your wallet to withdraw\\\\._`;\n\n await ctx.reply(text, { parse_mode: 'MarkdownV2' });\n } catch (err) {\n await ctx.reply(`Error: \\`${escMd(String(err))}\\``, { parse_mode: 'MarkdownV2' });\n }\n });\n\n return bot;\n}\n\n// ── Main entry point ──\n\nexport async function main(): Promise<void> {\n const token = process.env.TELEGRAM_BOT_TOKEN;\n if (!token) {\n console.error('TELEGRAM_BOT_TOKEN env var is required');\n process.exit(1);\n }\n\n const forge = createEarnForge({\n composerApiKey: process.env.LIFI_API_KEY,\n });\n\n const bot = createBot(token, forge);\n\n console.log('EarnForge Bot starting...');\n bot.start();\n}\n\n// Auto-start when run directly (not when imported for testing)\nconst isMainModule =\n typeof process !== 'undefined' &&\n process.argv[1] &&\n import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\n\nif (isMainModule) {\n main().catch((err) => {\n console.error('Fatal error:', err);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAgBA,MAAM,mBAA2C;CAC/C,UAAU;CACV,UAAU;CACV,KAAK;CACL,QAAQ;CACR,UAAU;CACV,SAAS;CACT,OAAO;CACP,OAAO;CACP,QAAQ;CACR,MAAM;CACN,UAAU;CACV,MAAM;CACN,WAAW;CACX,OAAO;CACP,WAAW;CACX,QAAQ;CACT;AAED,MAAM,mBAA2C,OAAO,YACtD,OAAO,QAAQ,iBAAiB,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC,aAAa,GAAG,KAAK,MAAM,EAAE,CAAC,CAAC,CACzG;AAID,SAAS,UAAU,OAA0C;AAC3D,KAAI,UAAU,MAAO,QAAO;AAC5B,KAAI,UAAU,SAAU,QAAO;AAC/B,QAAO;;AAGT,SAAS,OAAO,KAAqB;AACnC,QAAO,GAAI,IAAK,QAAQ,EAAE,CAAC;;AAG7B,SAAS,OAAO,OAAsB;CACpC,MAAM,MAAM,SAAS,MAAM,UAAU,IAAI,CAAC;AAC1C,KAAI,OAAO,IAAe,QAAO,KAAK,MAAM,KAAe,QAAQ,EAAE,CAAC;AACtE,KAAI,OAAO,IAAW,QAAO,KAAK,MAAM,KAAW,QAAQ,EAAE,CAAC;AAC9D,KAAI,OAAO,IAAO,QAAO,KAAK,MAAM,KAAO,QAAQ,EAAE,CAAC;AACtD,QAAO,IAAI,IAAI,QAAQ,EAAE;;AAG3B,SAAS,aAAa,OAAc,MAAiB,OAAuB;AAC1E,QACE,IAAI,MAAM,IAAI,MAAM,MAAM,KAAK,CAAC,eACnB,OAAO,MAAM,UAAU,IAAI,MAAM,CAAC,cAAc,OAAO,MAAM,CAAC,qBACzD,MAAM,SAAS,KAAK,gBAAgB,iBAAiB,MAAM,YAAY,MAAM,QAAQ,eAC3F,UAAU,KAAK,MAAM,CAAC,KAAK,KAAK,MAAM,SAAS,KAAK,MAAM,gBACxD,MAAM,KAAK;;AAI7B,SAAS,iBAAiB,MAAiB,OAAsB;CAC/D,MAAM,IAAI,KAAK;AACf,QACE,sBAAsB,MAAM,MAAM,KAAK,CAAC,QACrC,UAAU,KAAK,MAAM,CAAC,aAAa,KAAK,MAAM,QAAQ,KAAK,MAAM,wCAE9C,EAAE,IAAI,4BACN,EAAE,aAAa,gCACX,EAAE,SAAS,4BACf,EAAE,cAAc,yBACnB,EAAE,UAAU;;AAKnC,SAAS,cAAc,OAAmB,OAAuB;AAC/D,QACE,IAAI,MAAM,IAAI,MAAM,MAAM,MAAM,KAAK,CAAC,mBACrB,MAAM,OAAO,QAAQ,EAAE,CAAC,MAAM,MAAM,WAAW,QAAQ,EAAE,CAAC,gBAC9D,OAAO,MAAM,IAAI,CAAC,aAAa,UAAU,MAAM,KAAK,MAAM,CAAC,KAAK,MAAM,KAAK,MAAM,qBAC/E,iBAAiB,MAAM,MAAM,YAAY,MAAM,MAAM,QAAQ,mBAAmB,MAAM,MAAM,SAAS,KAAK;;AAI7H,SAAS,mBAAmB,QAAiC;CAC3D,MAAM,QAAQ,OAAO;CACrB,IAAI,OAAO,2BAA2B,MAAM,MAAM,KAAK,CAAC;AAExD,KAAI,OAAO,GACT,SAAQ;KAER,SAAQ;AAGV,MAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,OAAO,MAAM,aAAa,UAAU,OAAc;AACxD,UAAQ,GAAG,KAAK,KAAK,MAAM,KAAK,MAAM,MAAM,MAAM,QAAQ,CAAC;;AAG7D,KAAI,OAAO,OAAO,WAAW,EAC3B,SAAQ;AAGV,QAAO;;;;;;AAOT,SAAS,MAAM,MAAsB;AACnC,QAAO,KAAK,QAAQ,+BAA+B,OAAO;;AAK5D,SAAgB,UAAU,OAAe,OAAuB;CAC9D,MAAM,MAAM,IAAI,IAAI,MAAM;AAG1B,KAAI,QAAQ,SAAS,OAAO,QAAiB;AAW3C,QAAM,IAAI,MATR,keASoB,EAAE,YAAY,cAAc,CAAC;GACnD;AAGF,KAAI,QAAQ,SAAS,OAAO,QAAiB;EAE3C,MAAM,SADO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,EACxC;AACnB,MAAI,CAAC,OAAO;AACV,SAAM,IAAI,MAAM,mDAAmD,EAAE,YAAY,cAAc,CAAC;AAChG;;AAGF,MAAI;GACF,MAAM,SAAS,MAAM,MAAM,OAAO,IAAI;IAAE,OAAO,MAAM,aAAa;IAAE,OAAO;IAAG,CAAC;AAC/E,OAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,+BAA+B,MAAM,MAAM,aAAa,CAAC,CAAC,QAAQ,EAAE,YAAY,cAAc,CAAC;AAC/G;;GAGF,MAAM,QAAQ,OAAO,KAAK,GAAG,MAAM,aAAa,GAAG,MAAM,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC;GAC9E,MAAM,OAAO,QAAQ,OAAO,OAAO,cAAc,MAAM,MAAM,aAAa,CAAC,CAAC,QAAQ,MAAM,KAAK,OAAO;AACtG,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,4BAA4B,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnG;AAGF,KAAI,QAAQ,OAAO,OAAO,QAAiB;EAEzC,MAAM,aADO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,EACpC,IAAI,aAAa;AACxC,MAAI,CAAC,WAAW;GACd,MAAM,YAAY,OAAO,KAAK,iBAAiB,CAAC,KAAK,KAAK;AAC1D,SAAM,IAAI,MACR,mEAAmE,MAAM,UAAU,CAAC,KACpF,EAAE,YAAY,cAAc,CAC7B;AACD;;EAGF,MAAM,UAAU,iBAAiB;AACjC,MAAI,YAAY,KAAA,GAAW;GACzB,MAAM,YAAY,OAAO,KAAK,iBAAiB,CAAC,KAAK,KAAK;AAC1D,SAAM,IAAI,MACR,mBAAmB,MAAM,UAAU,CAAC,qBAAqB,MAAM,UAAU,CAAC,KAC1E,EAAE,YAAY,cAAc,CAC7B;AACD;;AAGF,MAAI;GACF,MAAM,SAAS,MAAM,MAAM,OAAO,IAAI;IAAE;IAAS,OAAO;IAAG,CAAC;AAC5D,OAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,wBAAwB,MAAM,UAAU,CAAC,QAAQ,EAAE,YAAY,cAAc,CAAC;AAC9F;;GAGF,MAAM,QAAQ,OAAO,KAAK,GAAG,MAAM,aAAa,GAAG,MAAM,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC;GAC9E,MAAM,SAAS,iBAAiB,YAAY;GAC5C,MAAM,OAAO,QAAQ,OAAO,OAAO,aAAa,MAAM,OAAO,CAAC,QAAQ,MAAM,KAAK,OAAO;AACxF,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,4BAA4B,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnG;AAGF,KAAI,QAAQ,QAAQ,OAAO,QAAiB;EAE1C,MAAM,QADO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,EACzC;AAClB,MAAI,CAAC,MAAM;AACT,SAAM,IAAI,MAAM,kEAAkE,EAAE,YAAY,cAAc,CAAC;AAC/G;;AAGF,MAAI;GACF,MAAM,QAAQ,MAAM,MAAM,OAAO,IAAI,KAAK;GAE1C,MAAM,OAAO,iBADA,MAAM,UAAU,MAAM,EACC,MAAM;AAC1C,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,YAAY,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnF;AAIF,KAAI,QAAQ,WAAW,OAAO,QAAiB;EAC7C,MAAM,OAAO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;EAC3D,MAAM,YAAY,KAAK;EACvB,MAAM,QAAQ,KAAK;EACnB,MAAM,cAAc,KAAK,IAAI,aAAa;AAE1C,MAAI,CAAC,aAAa,CAAC,OAAO;AACxB,SAAM,IAAI,MACR,iKACA,EAAE,YAAY,cAAc,CAC7B;AACD;;EAGF,MAAM,SAAS,OAAO,UAAU;AAChC,MAAI,OAAO,MAAM,OAAO,IAAI,UAAU,GAAG;AACvC,SAAM,IAAI,MAAM,uCAAuC,EAAE,YAAY,cAAc,CAAC;AACpF;;EAIF,MAAM,WAAW,eADO;GAAC;GAAgB;GAAW;GAAe;GAAgB,CACnC,SAAS,YAAY,GAChE,cACD,KAAA;AAEJ,MAAI;GACF,MAAM,SAAS,MAAM,MAAM,QAAQ;IAAE;IAAQ,OAAO,MAAM,aAAa;IAAE,WAAW;IAAG;IAAU,CAAC;AAElG,OAAI,OAAO,YAAY,WAAW,GAAG;AACnC,UAAM,IAAI,MACR,kCAAkC,MAAM,MAAM,aAAa,CAAC,CAAC,QAC7D,EAAE,YAAY,cAAc,CAC7B;AACD;;GAGF,MAAM,QAAQ,OAAO,YAAY,KAAK,GAAG,MAAM,cAAc,GAAG,IAAI,EAAE,CAAC;GACvE,MAAM,OACJ,8BAA8B,MAAM,OAAO,gBAAgB,CAAC,CAAC,GAAG,MAAM,MAAM,aAAa,CAAC,CAAC,wBACxE,OAAO,OAAO,YAAY,CAAC,UAC9C,MAAM,KAAK,OAAO;AACpB,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,YAAY,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnF;AAGF,KAAI,QAAQ,UAAU,OAAO,QAAiB;EAC5C,MAAM,OAAO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;EAC3D,MAAM,OAAO,KAAK;AAClB,MAAI,CAAC,MAAM;AACT,SAAM,IAAI,MACR,+HACA,EAAE,YAAY,cAAc,CAC7B;AACD;;EAGF,MAAM,SAAS,KAAK,MAAM,sBAAsB,KAAK,KAAK,GAAG,GACzD,KAAK,KACL;AAEJ,MAAI;GACF,MAAM,QAAQ,MAAM,MAAM,OAAO,IAAI,KAAK;GAE1C,MAAM,OAAO,mBADE,MAAM,UAAU,OAAO,OAAO,CACN;AACvC,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,YAAY,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnF;AAGF,KAAI,QAAQ,YAAY,OAAO,QAAiB;EAC9C,MAAM,OAAO,IAAI,SAAS,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;EAC3D,MAAM,OAAO,KAAK;EAClB,MAAM,YAAY,KAAK;AAEvB,MAAI,CAAC,QAAQ,CAAC,WAAW;AACvB,SAAM,IAAI,MACR,0EACA,EAAE,YAAY,cAAc,CAC7B;AACD;;AAGF,MAAI;GACF,MAAM,QAAQ,MAAM,MAAM,OAAO,IAAI,KAAK;AAC1C,OAAI,CAAC,MAAM,cAAc;AACvB,UAAM,IAAI,MACR,WAAW,MAAM,MAAM,KAAK,CAAC,0BAC7B,EAAE,YAAY,cAAc,CAC7B;AACD;;GAGF,MAAM,SAAS,MAAM,MAAM,iBAAiB,OAAO;IACjD,YAAY;IACZ,QAAQ;IACT,CAAC;GAEF,MAAM,OACJ,wBAAwB,MAAM,MAAM,KAAK,CAAC,kBAC7B,MAAM,OAAO,YAAY,CAAC,8BACzB,MAAM,OAAO,MAAM,SAAS,SAAS,CAAC,aAC1C,MAAM,OAAO,MAAM,SAAS,YAAY,CAAC,kBACpC,OAAO,MAAM,SAAS,kBAAkB;AAGzD,SAAM,IAAI,MAAM,MAAM,EAAE,YAAY,cAAc,CAAC;WAC5C,KAAK;AACZ,SAAM,IAAI,MAAM,YAAY,MAAM,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,YAAY,cAAc,CAAC;;GAEnF;AAEF,QAAO;;AAKT,eAAsB,OAAsB;CAC1C,MAAM,QAAQ,QAAQ,IAAI;AAC1B,KAAI,CAAC,OAAO;AACV,UAAQ,MAAM,yCAAyC;AACvD,UAAQ,KAAK,EAAE;;CAOjB,MAAM,MAAM,UAAU,OAJR,gBAAgB,EAC5B,gBAAgB,QAAQ,IAAI,cAC7B,CAAC,CAEiC;AAEnC,SAAQ,IAAI,4BAA4B;AACxC,KAAI,OAAO;;AASb,IAJE,OAAO,YAAY,eACnB,QAAQ,KAAK,MACb,OAAO,KAAK,IAAI,SAAS,QAAQ,KAAK,GAAG,QAAQ,OAAO,IAAI,CAAC,CAG7D,OAAM,CAAC,OAAO,QAAQ;AACpB,SAAQ,MAAM,gBAAgB,IAAI;AAClC,SAAQ,KAAK,EAAE;EACf"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@earnforge/bot",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Telegram bot for the LI.FI Earn API — yield queries, risk scoring, and portfolio suggestions",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/esm/index.js",
10
+ "types": "./dist/esm/index.d.ts"
11
+ }
12
+ },
13
+ "files": ["dist", "README.md", "LICENSE"],
14
+ "scripts": {
15
+ "build": "tsdown src/index.ts --format esm --dts --out-dir dist/esm",
16
+ "start": "node dist/esm/index.js",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest run",
19
+ "test:unit": "vitest run",
20
+ "clean": "rm -rf dist .turbo"
21
+ },
22
+ "dependencies": {
23
+ "@earnforge/sdk": "workspace:*",
24
+ "grammy": "^1.35.0"
25
+ },
26
+ "devDependencies": {
27
+ "tsdown": "^0.21.7",
28
+ "typescript": "^5.9.3",
29
+ "vitest": "^4.1.4"
30
+ }
31
+ }