nyxora 26.6.19 → 26.6.21

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.
Files changed (83) hide show
  1. package/README.md +18 -1
  2. package/bin/nyxora.mjs +32 -0
  3. package/dist/packages/core/src/agent/reasoning.js +11 -1
  4. package/dist/packages/core/src/config/parser.js +121 -7
  5. package/dist/packages/core/src/gateway/chat.js +82 -0
  6. package/dist/packages/core/src/gateway/cli.js +63 -0
  7. package/dist/packages/core/src/gateway/server.js +117 -56
  8. package/dist/packages/core/src/gateway/setup.js +39 -22
  9. package/dist/packages/core/src/utils/formatter.test.js +40 -0
  10. package/dist/packages/core/src/utils/skillManager.js +91 -0
  11. package/dist/packages/core/src/utils/userWhitelistManager.js +41 -36
  12. package/dist/packages/core/src/web3/aggregator/aggregatorMainnet.js +2 -2
  13. package/dist/packages/core/src/web3/aggregator/defiRouter.js +3 -0
  14. package/dist/packages/core/src/web3/skills/bridgeToken.js +4 -0
  15. package/dist/packages/core/src/web3/skills/checkRegistryStatus.js +13 -0
  16. package/dist/packages/core/src/web3/skills/customTx.js +2 -0
  17. package/dist/packages/core/src/web3/skills/defiLending.js +2 -0
  18. package/dist/packages/core/src/web3/skills/getPrice.js +11 -6
  19. package/dist/packages/core/src/web3/skills/manageCustomTokens.js +18 -32
  20. package/dist/packages/core/src/web3/skills/marketAnalysis.js +3 -1
  21. package/dist/packages/core/src/web3/skills/mintNft.js +2 -0
  22. package/dist/packages/core/src/web3/skills/provideLiquidity.js +2 -0
  23. package/dist/packages/core/src/web3/skills/revokeApprovals.js +2 -0
  24. package/dist/packages/core/src/web3/skills/swapToken.js +4 -2
  25. package/dist/packages/core/src/web3/skills/transfer.js +2 -0
  26. package/dist/packages/core/src/web3/skills/yieldVault.js +2 -0
  27. package/dist/packages/core/src/web3/utils/tokens.js +9 -1
  28. package/package.json +2 -1
  29. package/packages/core/package.json +1 -1
  30. package/packages/core/src/agent/reasoning.ts +12 -1
  31. package/packages/core/src/config/parser.ts +119 -9
  32. package/packages/core/src/gateway/chat.ts +85 -0
  33. package/packages/core/src/gateway/cli.ts +63 -0
  34. package/packages/core/src/gateway/server.ts +132 -60
  35. package/packages/core/src/gateway/setup.ts +39 -27
  36. package/packages/core/src/utils/formatter.test.ts +41 -0
  37. package/packages/core/src/utils/skillManager.ts +98 -0
  38. package/packages/core/src/utils/userWhitelistManager.ts +48 -39
  39. package/packages/core/src/web3/aggregator/aggregatorMainnet.ts +2 -2
  40. package/packages/core/src/web3/aggregator/defiRouter.ts +4 -0
  41. package/packages/core/src/web3/skills/bridgeToken.ts +3 -0
  42. package/packages/core/src/web3/skills/checkRegistryStatus.ts +13 -0
  43. package/packages/core/src/web3/skills/customTx.ts +1 -0
  44. package/packages/core/src/web3/skills/defiLending.ts +1 -0
  45. package/packages/core/src/web3/skills/getPrice.ts +11 -6
  46. package/packages/core/src/web3/skills/manageCustomTokens.ts +18 -29
  47. package/packages/core/src/web3/skills/marketAnalysis.ts +2 -1
  48. package/packages/core/src/web3/skills/mintNft.ts +1 -0
  49. package/packages/core/src/web3/skills/provideLiquidity.ts +1 -0
  50. package/packages/core/src/web3/skills/revokeApprovals.ts +1 -0
  51. package/packages/core/src/web3/skills/swapToken.ts +3 -2
  52. package/packages/core/src/web3/skills/transfer.ts +1 -0
  53. package/packages/core/src/web3/skills/yieldVault.ts +1 -0
  54. package/packages/core/src/web3/utils/tokens.ts +9 -1
  55. package/packages/dashboard/dist/assets/{index-DnQrbB4c.css → index-CQNHWZtN.css} +1 -1
  56. package/packages/dashboard/dist/assets/index-Di9x08yk.js +16 -0
  57. package/packages/dashboard/dist/index.html +2 -2
  58. package/packages/dashboard/package.json +1 -1
  59. package/packages/mcp-server/package.json +1 -1
  60. package/packages/policy/package.json +1 -1
  61. package/packages/signer/package.json +1 -1
  62. package/dist/packages/core/src/agent/limitOrderManager.js +0 -124
  63. package/dist/packages/core/src/system/pluginManager.js +0 -91
  64. package/dist/packages/core/src/system/skills/installSkill.js +0 -52
  65. package/dist/packages/core/src/test-all-routers.js +0 -81
  66. package/dist/packages/core/src/test-router.js +0 -38
  67. package/dist/packages/core/src/web3/skills/autonomousDefi.js +0 -191
  68. package/dist/packages/core/src/web3/skills/createWallet.js +0 -34
  69. package/dist/packages/core/src/web3/skills/limitOrder.js +0 -106
  70. package/dist/packages/core/src/web3/utils/protocolRegistry.js +0 -46
  71. package/dist/tsconfig.tsbuildinfo +0 -1
  72. package/packages/core/src/__tests__/reasoning.test.ts +0 -81
  73. package/packages/core/src/__tests__/tokens.test.ts +0 -55
  74. package/packages/core/src/__tests__/web3.test.ts +0 -50
  75. package/packages/core/src/agent/reasoning.d.ts.map +0 -1
  76. package/packages/core/src/config/parser.d.ts.map +0 -1
  77. package/packages/core/src/gateway/cli.d.ts.map +0 -1
  78. package/packages/core/src/memory/logger.d.ts.map +0 -1
  79. package/packages/core/src/test-all-routers.ts +0 -59
  80. package/packages/core/src/test-router.ts +0 -49
  81. package/packages/core/src/web3/config.d.ts.map +0 -1
  82. package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
  83. package/packages/dashboard/dist/assets/index-BAXifdMN.js +0 -16
@@ -1,8 +1,54 @@
1
1
  import fs from 'fs';
2
2
  import { getPath } from '../config/paths';
3
+ import { loadConfig, saveConfig } from '../config/parser';
3
4
 
4
5
  let disabledSkillsCache: string[] | null = null;
5
6
 
7
+ const reverseSkillMapping: Record<string, { category: 'os' | 'web3', name: string }> = {
8
+ // OS Skills
9
+ 'read_local_file': { category: 'os', name: 'readFile' },
10
+ 'write_local_file': { category: 'os', name: 'writeFile' },
11
+ 'edit_local_file': { category: 'os', name: 'editFile' },
12
+ 'generate_excel_file': { category: 'os', name: 'generateExcel' },
13
+ 'analyze_document': { category: 'os', name: 'analyzeDocument' },
14
+ 'run_terminal_command': { category: 'os', name: 'executeShell' },
15
+ 'browse_website': { category: 'os', name: 'browseWeb' },
16
+ 'search_web': { category: 'os', name: 'searchWeb' },
17
+ 'read_gmail_inbox': { category: 'os', name: 'readGmail' },
18
+ 'list_calendar_events': { category: 'os', name: 'listCalendar' },
19
+ 'append_row_to_sheets': { category: 'os', name: 'appendSheets' },
20
+ 'read_google_docs': { category: 'os', name: 'readDocs' },
21
+ 'read_google_form_responses': { category: 'os', name: 'readForms' },
22
+ 'execute_git_command': { category: 'os', name: 'gitManager' },
23
+ 'manage_twitter': { category: 'os', name: 'xManager' },
24
+ 'manage_notion': { category: 'os', name: 'notionWorkspace' },
25
+ 'transcribe_audio': { category: 'os', name: 'audioTranscribe' },
26
+ 'summarize_text': { category: 'os', name: 'summarizeText' },
27
+ 'update_security_policy': { category: 'os', name: 'updateSecurityPolicy' },
28
+
29
+ // Web3 Skills
30
+ 'transfer_token': { category: 'web3', name: 'transfer' },
31
+ 'transfer_native': { category: 'web3', name: 'transfer' },
32
+ 'swap_token': { category: 'web3', name: 'swap' },
33
+ 'bridge_token': { category: 'web3', name: 'bridge' },
34
+ 'mint_nft': { category: 'web3', name: 'mintNft' },
35
+ 'custom_tx': { category: 'web3', name: 'customTx' },
36
+ 'check_address': { category: 'web3', name: 'checkAddress' },
37
+ 'get_my_address': { category: 'web3', name: 'getMyAddress' },
38
+ 'check_token_security': { category: 'web3', name: 'checkSecurity' },
39
+ 'check_portfolio': { category: 'web3', name: 'checkPortfolio' },
40
+ 'analyze_market': { category: 'web3', name: 'marketAnalysis' },
41
+ 'manage_custom_tokens': { category: 'web3', name: 'manageCustomTokens' },
42
+ 'get_price': { category: 'web3', name: 'getPrice' },
43
+ 'supply_aave': { category: 'web3', name: 'aaveSupply' },
44
+ 'revoke_approval': { category: 'web3', name: 'revokeApproval' },
45
+ 'deposit_yield_vault': { category: 'web3', name: 'vaultDeposit' },
46
+ 'provide_liquidity_v3': { category: 'web3', name: 'provideLiquidity' },
47
+ 'get_tx_history': { category: 'web3', name: 'getTxHistory' },
48
+ 'check_registry_status': { category: 'web3', name: 'checkRegistryStatus' },
49
+ 'create_limit_order': { category: 'web3', name: 'createLimitOrder' }
50
+ };
51
+
6
52
  function getDisabledSkillsFile(): string {
7
53
  return getPath('disabled_skills.json');
8
54
  }
@@ -36,9 +82,61 @@ export function toggleSkill(skillName: string, active: boolean): void {
36
82
  }
37
83
  disabledSkillsCache = Array.from(current);
38
84
  fs.writeFileSync(getDisabledSkillsFile(), JSON.stringify(disabledSkillsCache, null, 2));
85
+
86
+ // Sync to config.yaml
87
+ const mapping = reverseSkillMapping[skillName];
88
+ if (mapping) {
89
+ const config = loadConfig();
90
+ if (!config.skills) config.skills = { web3: [], os: [] } as any;
91
+
92
+ const categoryArray = mapping.category === 'web3' ? config.skills!.web3 : config.skills!.os;
93
+
94
+ if (active && !categoryArray.includes(mapping.name)) {
95
+ categoryArray.push(mapping.name);
96
+ } else if (!active) {
97
+ const index = categoryArray.indexOf(mapping.name);
98
+ if (index !== -1) {
99
+ categoryArray.splice(index, 1);
100
+ }
101
+ }
102
+
103
+ saveConfig(config);
104
+ }
39
105
  }
40
106
 
41
107
  export function isSkillActive(skillName: string): boolean {
42
108
  const disabled = getDisabledSkills();
43
109
  return !disabled.includes(skillName);
44
110
  }
111
+
112
+ export function syncAllSkillsToConfig(): void {
113
+ const config = loadConfig();
114
+ if (!config.skills) config.skills = { web3: [], os: [] } as any;
115
+
116
+ const activeWeb3 = new Set<string>();
117
+ const activeOs = new Set<string>();
118
+
119
+ for (const [skillName, mapping] of Object.entries(reverseSkillMapping)) {
120
+ if (isSkillActive(skillName)) {
121
+ if (mapping.category === 'web3') {
122
+ activeWeb3.add(mapping.name);
123
+ } else {
124
+ activeOs.add(mapping.name);
125
+ }
126
+ }
127
+ }
128
+
129
+ // Check if arrays are different to avoid unnecessary saves
130
+ const currentWeb3 = config.skills!.web3 || [];
131
+ const currentOs = config.skills!.os || [];
132
+
133
+ const web3Changed = currentWeb3.length !== activeWeb3.size || !currentWeb3.every(s => activeWeb3.has(s));
134
+ const osChanged = currentOs.length !== activeOs.size || !currentOs.every(s => activeOs.has(s));
135
+
136
+ if (web3Changed || osChanged) {
137
+ config.skills!.web3 = Array.from(activeWeb3);
138
+ config.skills!.os = Array.from(activeOs);
139
+ saveConfig(config);
140
+ console.log('[SkillManager] Automatically synchronized active skills to config.yaml');
141
+ }
142
+ }
@@ -1,12 +1,17 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import yaml from 'yaml';
5
+ import { getPublicClient, ChainName } from '../web3/config';
6
+ import { getTokenMetadata } from '../web3/utils/tokens';
4
7
 
5
- const WHITELIST_FILE_PATH = path.join(os.homedir(), '.nyxora', 'user_whitelist.json');
8
+ const WHITELIST_FILE_PATH = path.join(os.homedir(), '.nyxora', 'user_whitelist.yaml');
6
9
 
7
10
  export interface WhitelistedToken {
8
11
  chainName: string;
9
12
  address: string;
13
+ symbol?: string;
14
+ decimals?: number;
10
15
  source: 'manual' | 'explorer' | 'swap';
11
16
  lastSeen: number;
12
17
  }
@@ -18,70 +23,74 @@ export interface UserWhitelist {
18
23
  export function getUserWhitelist(): UserWhitelist {
19
24
  try {
20
25
  if (!fs.existsSync(WHITELIST_FILE_PATH)) {
26
+ // Try migrating from JSON if YAML doesn't exist yet
27
+ const oldJsonPath = path.join(os.homedir(), '.nyxora', 'user_whitelist.json');
28
+ if (fs.existsSync(oldJsonPath)) {
29
+ const data = fs.readFileSync(oldJsonPath, 'utf-8');
30
+ const parsed = JSON.parse(data);
31
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(parsed), 'utf-8');
32
+ // Rename old file so it doesn't get used again
33
+ fs.renameSync(oldJsonPath, `${oldJsonPath}.bak`);
34
+ return parsed;
35
+ }
21
36
  return {};
22
37
  }
23
38
  const data = fs.readFileSync(WHITELIST_FILE_PATH, 'utf-8');
24
- const parsed = JSON.parse(data);
25
-
26
- // Auto-migrate legacy format
27
- let migrated = false;
28
- for (const addr in parsed) {
29
- if (parsed[addr] && !Array.isArray(parsed[addr])) {
30
- const newArray: WhitelistedToken[] = [];
31
- for (const chain in parsed[addr]) {
32
- const tokens = parsed[addr][chain];
33
- if (Array.isArray(tokens)) {
34
- for (const t of tokens) {
35
- newArray.push({
36
- chainName: chain,
37
- address: typeof t === 'string' ? t.toLowerCase() : (t.address || '').toLowerCase(),
38
- source: 'manual',
39
- lastSeen: Date.now()
40
- });
41
- }
42
- }
43
- }
44
- parsed[addr] = newArray;
45
- migrated = true;
46
- }
47
- }
48
-
49
- if (migrated) {
50
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(parsed, null, 2), 'utf-8');
51
- }
52
-
53
- return parsed;
39
+ return yaml.parse(data) || {};
54
40
  } catch (err) {
55
- console.error('[Whitelist] Error reading user_whitelist.json', err);
41
+ console.error('[Whitelist] Error reading user_whitelist.yaml', err);
56
42
  return {};
57
43
  }
58
44
  }
59
45
 
60
- export function saveTokenToWhitelist(walletAddress: string, chainName: string, tokenAddress: string, source: 'manual' | 'explorer' | 'swap' = 'manual') {
46
+ export async function saveTokenToWhitelist(
47
+ walletAddress: string,
48
+ chainName: ChainName,
49
+ tokenAddress: string,
50
+ source: 'manual' | 'explorer' | 'swap' = 'manual',
51
+ symbol?: string,
52
+ decimals?: number
53
+ ) {
61
54
  try {
62
55
  const whitelist = getUserWhitelist();
63
56
  const addr = walletAddress.toLowerCase();
64
57
  const tokenAddr = tokenAddress.toLowerCase();
65
58
 
59
+ // Auto-fetch metadata if not provided
60
+ if (!symbol || decimals === undefined) {
61
+ try {
62
+ const client = getPublicClient(chainName);
63
+ const metadata = await getTokenMetadata(client, tokenAddr as `0x${string}`);
64
+ symbol = metadata.symbol;
65
+ decimals = metadata.decimals;
66
+ } catch (err) {
67
+ console.warn(`[Whitelist] Could not fetch metadata for ${tokenAddr} on ${chainName}`, err);
68
+ }
69
+ }
70
+
66
71
  if (!whitelist[addr]) whitelist[addr] = [];
67
72
 
68
73
  const existingIndex = whitelist[addr].findIndex(t => t.chainName === chainName && t.address === tokenAddr);
69
74
 
70
75
  if (existingIndex >= 0) {
71
76
  whitelist[addr][existingIndex].lastSeen = Date.now();
77
+ if (symbol) whitelist[addr][existingIndex].symbol = symbol;
78
+ if (decimals !== undefined) whitelist[addr][existingIndex].decimals = decimals;
72
79
  } else {
73
80
  whitelist[addr].push({
74
81
  chainName,
75
82
  address: tokenAddr,
83
+ symbol,
84
+ decimals,
76
85
  source,
77
86
  lastSeen: Date.now()
78
87
  });
79
- console.log(`[Whitelist] Added ${tokenAddr} to ${chainName} for ${addr} via ${source}`);
88
+ console.log(`[Whitelist] Added ${symbol || tokenAddr} to ${chainName} for ${addr} via ${source}`);
80
89
  }
81
90
 
82
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(whitelist, null, 2), 'utf-8');
91
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(whitelist), 'utf-8');
83
92
  } catch (err) {
84
- console.error('[Whitelist] Error saving token to user_whitelist.json', err);
93
+ console.error('[Whitelist] Error saving token to user_whitelist.yaml', err);
85
94
  }
86
95
  }
87
96
 
@@ -97,11 +106,11 @@ export function removeTokenFromWhitelist(walletAddress: string, chainName: strin
97
106
  whitelist[addr] = whitelist[addr].filter(t => !(t.chainName === chainName && t.address === tokenAddr));
98
107
 
99
108
  if (whitelist[addr].length !== initialLength) {
100
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(whitelist, null, 2), 'utf-8');
101
- console.log(`[Whitelist] Removed garbage token ${tokenAddr} on ${chainName} for ${addr}`);
109
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(whitelist), 'utf-8');
110
+ console.log(`[Whitelist] Removed token ${tokenAddr} on ${chainName} for ${addr}`);
102
111
  }
103
112
  } catch (err) {
104
- console.error('[Whitelist] Error removing token from user_whitelist.json', err);
113
+ console.error('[Whitelist] Error removing token from user_whitelist.yaml', err);
105
114
  }
106
115
  }
107
116
 
@@ -74,8 +74,8 @@ export async function fetchMainnetBestRoute(
74
74
  promises.push(fetchOpenOcean(fromChain, toChain, fromToken, toToken, amountInWei, userAddress, slippageTolerance, keys.openocean_key));
75
75
  }
76
76
 
77
- // Provider 6: KyberSwap (Cross-chain & Same-chain)
78
- if (isProviderHealthy('kyberswap')) {
77
+ // Provider 6: KyberSwap (Same-chain ONLY)
78
+ if (!isCrossChain && isProviderHealthy('kyberswap')) {
79
79
  promises.push(fetchKyberSwap(fromChain, fromToken, toToken, amountInWei, userAddress, slippageTolerance));
80
80
  }
81
81
 
@@ -12,6 +12,10 @@ export async function routeTransaction(
12
12
  slippageTolerance: number | "auto" = "auto"
13
13
  ): Promise<RouteQuote> {
14
14
 
15
+ if (!fromChain || !toChain) {
16
+ throw new Error("Missing source or destination chain in routing.");
17
+ }
18
+
15
19
  // Auto-correct: If one is testnet and the other is mainnet, assume they meant testnet
16
20
  if (fromChain.includes('sepolia') && !toChain.includes('sepolia')) {
17
21
  if (toChain === 'base') toChain = 'base_sepolia';
@@ -15,6 +15,9 @@ export async function prepareBridgeToken(
15
15
  slippagePercent?: number | "auto"
16
16
  ): Promise<string> {
17
17
  try {
18
+ if (!fromChain || !toChain) throw new Error("Source or destination chain not provided by AI.");
19
+ if (!amountStr) throw new Error("Bridge amount not provided by AI.");
20
+
18
21
  const userAddress = await getAddress();
19
22
 
20
23
  // Auto-correct: If one is testnet and the other is mainnet, assume they meant testnet
@@ -83,3 +83,16 @@ export async function checkRegistryStatus(): Promise<{ isActive: boolean; reason
83
83
  return { isActive: true, reason: 'Registry check skipped due to network error.' };
84
84
  }
85
85
  }
86
+
87
+ export const checkRegistryStatusToolDefinition = {
88
+ type: "function",
89
+ function: {
90
+ name: "check_registry_status",
91
+ description: "Internal Security Middleware: Checks the On-Chain Kill Switch Registry before executing any transaction. If the agent is deactivated, the transaction will be strictly blocked for safety.",
92
+ parameters: {
93
+ type: "object",
94
+ properties: {},
95
+ required: []
96
+ }
97
+ }
98
+ };
@@ -10,6 +10,7 @@ export async function prepareCustomTx(
10
10
  description: string = "Custom transaction"
11
11
  ): Promise<string> {
12
12
  try {
13
+ if (!chainName || !toAddress || !data) throw new Error("Missing required parameters for custom transaction.");
13
14
  const tx = txManager.createPendingTransaction('custom', chainName, {
14
15
  toAddress,
15
16
  data,
@@ -31,6 +31,7 @@ const AAVE_V3_POOLS: Record<string, `0x${string}`> = {
31
31
 
32
32
  export async function prepareAaveSupply(chainName: ChainName, tokenAddressOrSymbol: string, amountStr: string): Promise<string> {
33
33
  try {
34
+ if (!chainName || !tokenAddressOrSymbol || !amountStr) throw new Error("Missing protocol/chain/token parameters for DeFi operation.");
34
35
  const publicClient = getPublicClient(chainName);
35
36
  const userAddress = await getAddress();
36
37
  const account = userAddress as `0x${string}`;
@@ -11,6 +11,10 @@ export const getPriceToolDefinition = {
11
11
  coinId: {
12
12
  type: "string",
13
13
  description: "The CoinGecko ID of the coin (e.g., 'ethereum', 'bitcoin', 'solana')."
14
+ },
15
+ currency: {
16
+ type: "string",
17
+ description: "The fiat currency to convert to (e.g., 'usd', 'idr', 'eur', 'jpy'). Defaults to 'usd'."
14
18
  }
15
19
  },
16
20
  required: ["coinId"]
@@ -18,25 +22,26 @@ export const getPriceToolDefinition = {
18
22
  }
19
23
  };
20
24
 
21
- export async function getPrice(coinId: string): Promise<string> {
25
+ export async function getPrice(coinId: string, currency: string = 'usd'): Promise<string> {
22
26
  try {
23
- const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd&include_24hr_change=true`;
27
+ const cur = currency.toLowerCase();
28
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=${cur}&include_24hr_change=true`;
24
29
  const data = await safeFetchJson<any>(url);
25
30
 
26
31
  if (!data[coinId]) {
27
32
  return `❌ Could not find price data for **${coinId.toUpperCase()}**. Please verify the coin name.`;
28
33
  }
29
34
 
30
- const price = data[coinId].usd;
31
- const change24h = data[coinId].usd_24h_change;
35
+ const price = data[coinId][cur];
36
+ const change24h = data[coinId][`${cur}_24h_change`];
32
37
 
33
38
  const isPositive = change24h >= 0;
34
39
  const arrow = isPositive ? '📈 🟩 +' : '📉 🟥 ';
35
- const priceFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price);
40
+ const priceFormatted = new Intl.NumberFormat(cur === 'idr' ? 'id-ID' : 'en-US', { style: 'currency', currency: cur.toUpperCase() }).format(price);
36
41
  const changeFormatted = change24h ? change24h.toFixed(2) : '0.00';
37
42
 
38
43
  return `💰 **Price Update for ${coinId.toUpperCase()}**\n\n` +
39
- `- **Price (USD)**: \`${priceFormatted}\`\n` +
44
+ `- **Price (${cur.toUpperCase()})**: \`${priceFormatted}\`\n` +
40
45
  `- **24h Change**: ${arrow}${changeFormatted}%`;
41
46
  } catch (error: any) {
42
47
  return `❌ **Failed to fetch price:** ${error.message}`;
@@ -1,13 +1,12 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { getPath } from '../../config/paths';
4
1
  import { SUPPORTED_CHAIN_NAMES, ChainName } from '../config';
2
+ import { saveTokenToWhitelist, removeTokenFromWhitelist } from '../../utils/userWhitelistManager';
3
+ import { getAddress } from '../utils/vaultClient';
5
4
 
6
5
  export const manageCustomTokensDefinition = {
7
6
  type: 'function',
8
7
  function: {
9
8
  name: 'manage_custom_tokens',
10
- description: 'Add or remove a custom ERC-20 token (like a memecoin or specific token) from the user\'s local portfolio watcher.',
9
+ description: 'Add or remove a custom ERC-20 token (like a memecoin or specific token) from the user\'s local portfolio watcher and swap whitelist.',
11
10
  parameters: {
12
11
  type: 'object',
13
12
  properties: {
@@ -27,10 +26,10 @@ export const manageCustomTokensDefinition = {
27
26
  },
28
27
  contract_address: {
29
28
  type: 'string',
30
- description: 'The smart contract address of the token. Only required for "add" action.'
29
+ description: 'The smart contract address of the token. Required for "add" action or "remove" action.'
31
30
  }
32
31
  },
33
- required: ['action', 'chain_name', 'symbol']
32
+ required: ['action', 'chain_name', 'symbol', 'contract_address']
34
33
  }
35
34
  }
36
35
  };
@@ -38,43 +37,33 @@ export const manageCustomTokensDefinition = {
38
37
  export async function executeManageCustomTokens(args: any): Promise<string> {
39
38
  const { action, chain_name, symbol, contract_address } = args;
40
39
 
41
- if (!SUPPORTED_CHAIN_NAMES.includes(chain_name)) {
42
- return `Error: Unsupported chain ${chain_name}.`;
40
+ if (!chain_name) {
41
+ throw new Error("Chain name is required.");
43
42
  }
44
43
 
45
- const customTokensPath = getPath('custom_tokens.json');
46
- let customTokens: Record<string, Record<string, string>> = {};
47
-
48
- if (fs.existsSync(customTokensPath)) {
49
- try {
50
- const data = fs.readFileSync(customTokensPath, 'utf8');
51
- customTokens = JSON.parse(data);
52
- } catch (e) {
53
- console.error('Error parsing custom_tokens.json', e);
54
- }
44
+ if (!symbol) {
45
+ throw new Error("Token symbol is required.");
55
46
  }
56
47
 
57
- if (!customTokens[chain_name]) {
58
- customTokens[chain_name] = {};
48
+ if (!SUPPORTED_CHAIN_NAMES.includes(chain_name as ChainName)) {
49
+ return `Error: Unsupported chain ${chain_name}.`;
59
50
  }
60
51
 
61
52
  const upperSymbol = symbol.toUpperCase();
53
+ const userAddress = await getAddress();
62
54
 
63
55
  if (action === 'add') {
64
56
  if (!contract_address || !contract_address.startsWith('0x')) {
65
57
  return `Error: Invalid or missing contract_address.`;
66
58
  }
67
- customTokens[chain_name][upperSymbol] = contract_address;
68
- fs.writeFileSync(customTokensPath, JSON.stringify(customTokens, null, 2));
69
- return `Successfully added custom token ${upperSymbol} to the ${chain_name} portfolio tracker.`;
59
+ await saveTokenToWhitelist(userAddress, chain_name as ChainName, contract_address, 'manual', upperSymbol);
60
+ return `Successfully added custom token ${upperSymbol} to the ${chain_name} portfolio tracker and swap whitelist.`;
70
61
  } else if (action === 'remove') {
71
- if (customTokens[chain_name][upperSymbol]) {
72
- delete customTokens[chain_name][upperSymbol];
73
- fs.writeFileSync(customTokensPath, JSON.stringify(customTokens, null, 2));
74
- return `Successfully removed custom token ${upperSymbol} from the ${chain_name} portfolio tracker.`;
75
- } else {
76
- return `Warning: Token ${upperSymbol} was not found in the custom portfolio tracker for ${chain_name}.`;
62
+ if (!contract_address || !contract_address.startsWith('0x')) {
63
+ return `Error: Invalid or missing contract_address for removal.`;
77
64
  }
65
+ removeTokenFromWhitelist(userAddress, chain_name, contract_address);
66
+ return `Successfully removed custom token ${upperSymbol} from the ${chain_name} whitelist.`;
78
67
  }
79
68
 
80
69
  return `Error: Invalid action.`;
@@ -47,7 +47,7 @@ async function fetchDexData(query: string, isCa: boolean, chainName?: ChainName)
47
47
  if (data && data.pairs && data.pairs.length > 0) {
48
48
  let pairs = data.pairs;
49
49
  if (chainName) {
50
- pairs = pairs.filter((p: any) => p.chainId.toLowerCase() === chainName.toLowerCase());
50
+ pairs = pairs.filter((p: any) => p.chainId?.toLowerCase() === chainName?.toLowerCase());
51
51
  if (pairs.length === 0) pairs = data.pairs;
52
52
  }
53
53
  return pairs.sort((a: any, b: any) => (b.volume?.h24 || 0) - (a.volume?.h24 || 0))[0];
@@ -82,6 +82,7 @@ async function fetchCexMomentum(symbol: string, currentP: number) {
82
82
 
83
83
  export async function analyzeMarket(chainName: ChainName, tokenAddressOrSymbol: string): Promise<string> {
84
84
  try {
85
+ if (!tokenAddressOrSymbol) throw new Error("Token symbol is invalid.");
85
86
  const cleanInput = tokenAddressOrSymbol.replace('$', '').toLowerCase();
86
87
  const isAddress = cleanInput.startsWith('0x') && cleanInput.length === 42;
87
88
 
@@ -10,6 +10,7 @@ export async function prepareMintNft(
10
10
  valueEth: string = "0"
11
11
  ): Promise<string> {
12
12
  try {
13
+ if (!chainName || !contractAddress || !functionSignature) throw new Error("Missing required parameters to mint NFT.");
13
14
  const publicClient = getPublicClient(chainName);
14
15
  const userAddress = await getAddress();
15
16
  const account = userAddress as `0x${string}`;
@@ -62,6 +62,7 @@ export async function prepareProvideLiquidity(
62
62
  slippagePercent?: number | "auto"
63
63
  ): Promise<string> {
64
64
  try {
65
+ if (!chainName || !token0AddressOrSymbol || !token1AddressOrSymbol || !amount0Str || !amount1Str) throw new Error("Missing protocol/chain/token parameters for DeFi operation.");
65
66
  let actualSlippage = slippagePercent;
66
67
  if (actualSlippage === undefined || actualSlippage === null || actualSlippage === "auto") {
67
68
  try {
@@ -7,6 +7,7 @@ import { resolveToken, ERC20_ABI, getTokenMetadata } from '../utils/tokens';
7
7
 
8
8
  export async function prepareRevokeApproval(chainName: ChainName, tokenAddressOrSymbol: string, spenderAddress: `0x${string}`): Promise<string> {
9
9
  try {
10
+ if (!chainName || !tokenAddressOrSymbol || !spenderAddress) throw new Error("Missing required parameters for revoking approval.");
10
11
  const publicClient = getPublicClient(chainName);
11
12
  const userAddress = await getAddress();
12
13
  const account = userAddress as `0x${string}`;
@@ -16,6 +16,7 @@ export async function prepareSwapToken(
16
16
  slippagePercent?: number | "auto"
17
17
  ): Promise<string> {
18
18
  try {
19
+ if (!chainName || !fromToken || !toToken || !amountStr) throw new Error("Missing required parameters for swap (chain, tokens, or amount).");
19
20
  const userAddress = await getAddress();
20
21
 
21
22
  const fromTokenAddress = resolveToken(fromToken, chainName);
@@ -23,9 +24,9 @@ export async function prepareSwapToken(
23
24
  const isNativeIn = fromTokenAddress === "0x0000000000000000000000000000000000000000";
24
25
 
25
26
  // Auto-save to Degen Whitelist
26
- if (!isNativeIn) saveTokenToWhitelist(userAddress, chainName, fromTokenAddress, 'swap');
27
+ if (!isNativeIn) await saveTokenToWhitelist(userAddress, chainName, fromTokenAddress, 'swap');
27
28
  if (toTokenAddress !== "0x0000000000000000000000000000000000000000") {
28
- saveTokenToWhitelist(userAddress, chainName, toTokenAddress, 'swap');
29
+ await saveTokenToWhitelist(userAddress, chainName, toTokenAddress, 'swap');
29
30
  }
30
31
 
31
32
  // Default to 18 decimals for formatting input, though Aggregator handles this if we pass raw
@@ -6,6 +6,7 @@ import { submitTransaction } from '../utils/vaultClient';
6
6
 
7
7
  export async function prepareTransfer(chainName: ChainName, toAddress: `0x${string}`, amountStr: string, token?: string): Promise<string> {
8
8
  try {
9
+ if (!chainName || !toAddress || !amountStr) throw new Error("Missing required parameters for transfer (chain, recipient, or amount).");
9
10
  const publicClient = getPublicClient(chainName);
10
11
  const userAddress = await getAddress();
11
12
  const account = userAddress as `0x${string}`;
@@ -23,6 +23,7 @@ const VAULT_ABI = [
23
23
 
24
24
  export async function prepareVaultDeposit(chainName: ChainName, protocol: string, vaultAddress: `0x${string}`, tokenAddressOrSymbol: string, amountStr: string): Promise<string> {
25
25
  try {
26
+ if (!chainName || !vaultAddress || !tokenAddressOrSymbol || !amountStr) throw new Error("Missing protocol/chain/token parameters for DeFi operation.");
26
27
  const publicClient = getPublicClient(chainName);
27
28
  const userAddress = await getAddress();
28
29
  const account = userAddress as `0x${string}`;
@@ -1,4 +1,5 @@
1
1
  import { ChainName } from '../config';
2
+ import { getUserWhitelist } from '../../utils/userWhitelistManager';
2
3
 
3
4
  export const ERC20_ABI = [
4
5
  {
@@ -72,7 +73,7 @@ export const TOKEN_MAP: Record<ChainName, Record<string, `0x${string}`>> = {
72
73
  ETH: "0x0000000000000000000000000000000000000000",
73
74
  WETH: "0x4200000000000000000000000000000000000006",
74
75
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
75
- USDT: "0xf55BEC9cbd4732f1F4143f647652e924540d9d64"
76
+ USDT: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"
76
77
  },
77
78
  optimism: {
78
79
  ETH: "0x0000000000000000000000000000000000000000",
@@ -127,6 +128,13 @@ export function resolveToken(tokenSymbolOrAddress: string, chainName: ChainName)
127
128
  return chainTokens[symbolUpper];
128
129
  }
129
130
 
131
+ const whitelistData = getUserWhitelist();
132
+ for (const addr in whitelistData) {
133
+ const tokens = whitelistData[addr];
134
+ const match = tokens.find(t => t.chainName === chainName && t.symbol === symbolUpper);
135
+ if (match) return match.address as `0x${string}`;
136
+ }
137
+
130
138
  throw new Error(`Token "${tokenSymbolOrAddress}" pada chain ${chainName} tidak ditemukan. Silakan gunakan alamat kontrak langsung (0x...).`);
131
139
  }
132
140