auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,356 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { ethers } from 'ethers';
3
+ import { getHotWallet, tokenCanAccessWallet } from '../lib/hot';
4
+ import { getTempWallet, hasTempWallet } from '../lib/temp';
5
+ import { resolveChain, getRpcUrl } from '../lib/config';
6
+ import { logger } from '../lib/logger';
7
+ import {
8
+ getDexAdapter,
9
+ detectBestDex,
10
+ listDexes,
11
+ } from '../lib/dex';
12
+ import { requireWalletAuth } from '../middleware/auth';
13
+ import { hasAnyPermission, isAdmin } from '../lib/permissions';
14
+ import { getDefault, getDefaultSync } from '../lib/defaults';
15
+ import { isSolanaChain, NATIVE_ADDRESSES } from '../lib/address';
16
+ import { getErrorMessage, HttpError } from '../lib/error';
17
+ import { getJupiterQuote } from '../lib/solana/jupiter';
18
+ import { getRelayPrice } from '../lib/dex/relay';
19
+ import { handleSolanaSwap } from './swap-solana';
20
+ import { handleEvmSwap } from './swap-evm';
21
+ import type { PoolKey } from '../lib/dex';
22
+
23
+ const router = Router();
24
+
25
+ export interface SwapRequest {
26
+ from: string;
27
+ token: string;
28
+ direction: 'buy' | 'sell';
29
+ amount: string;
30
+ minOut?: string;
31
+ slippage?: number;
32
+ dex?: string; // DEX to use (default: auto-detect)
33
+ version?: string; // Pool version override (v2, v3, v4, etc.)
34
+ poolFee?: number;
35
+ poolKey?: PoolKey;
36
+ hook?: string; // V4 hook name (e.g., 'clanker-static-fee-v2')
37
+ chain?: string;
38
+ chainOut?: string; // Destination chain for cross-chain swaps (Relay only)
39
+ description?: string; // Optional description for the transaction
40
+ }
41
+
42
+ // POST /swap - Execute token swap via DEX adapter
43
+ router.post('/', requireWalletAuth, async (req: Request, res: Response) => {
44
+ try {
45
+ const {
46
+ from,
47
+ token,
48
+ direction,
49
+ amount,
50
+ minOut,
51
+ slippage,
52
+ dex: requestedDex,
53
+ chain,
54
+ chainOut,
55
+ } = req.body as SwapRequest;
56
+
57
+ const auth = req.auth!;
58
+
59
+ // Validate required fields
60
+ if (!from || typeof from !== 'string') {
61
+ res.status(400).json({ error: 'from address is required' });
62
+ return;
63
+ }
64
+
65
+ if (!token || typeof token !== 'string') {
66
+ res.status(400).json({ error: 'token address is required' });
67
+ return;
68
+ }
69
+
70
+ if (!direction || (direction !== 'buy' && direction !== 'sell')) {
71
+ res.status(400).json({ error: 'direction must be "buy" or "sell"' });
72
+ return;
73
+ }
74
+
75
+ if (!amount || typeof amount !== 'string') {
76
+ res.status(400).json({ error: 'amount is required' });
77
+ return;
78
+ }
79
+
80
+ // Validate slippage/minOut - never allow 0 minOut (sandwich attack protection)
81
+ if (!minOut && (slippage === undefined || slippage === null)) {
82
+ res.status(400).json({ error: 'slippage is required (percentage, e.g. 1.0 for 1%)' });
83
+ return;
84
+ }
85
+
86
+ if (slippage !== undefined && slippage !== null) {
87
+ if (typeof slippage !== 'number' || isNaN(slippage) || slippage <= 0) {
88
+ res.status(400).json({ error: 'slippage must be a positive number (percentage)' });
89
+ return;
90
+ }
91
+ const maxSlippage = await getDefault<number>('swap.max_slippage', 50);
92
+ if (slippage > maxSlippage) {
93
+ res.status(400).json({ error: `slippage cannot exceed ${maxSlippage}%` });
94
+ return;
95
+ }
96
+ }
97
+
98
+ // Early validation: if explicit minOut is provided, check it against the slippage floor
99
+ // This runs before DEX detection so we fail fast on bad minOut values
100
+ if (minOut && minOut !== '0') {
101
+ const minSlippageEarly = isAdmin(auth)
102
+ ? await getDefault<number>('swap.min_slippage_admin', 0.5)
103
+ : await getDefault<number>('swap.min_slippage_agent', 1.0);
104
+ const amountBigEarly = BigInt(amount);
105
+ const floorBpsEarly = BigInt(Math.floor(minSlippageEarly * 100));
106
+ const floorMinOutEarly = amountBigEarly - (amountBigEarly * floorBpsEarly / 10000n);
107
+ const explicitMinOut = BigInt(minOut);
108
+ if (explicitMinOut < floorMinOutEarly) {
109
+ res.status(400).json({
110
+ error: `minOut too low: ${minOut} is below the ${minSlippageEarly}% slippage floor (min: ${floorMinOutEarly.toString()})`
111
+ });
112
+ return;
113
+ }
114
+ }
115
+
116
+ // Get chain config
117
+ const { targetChain, chainConfig } = resolveChain(chain);
118
+
119
+ // Resolve destination chain for cross-chain swaps
120
+ let destinationChainId: number | undefined;
121
+ let targetChainOut: string | undefined;
122
+ if (chainOut) {
123
+ try {
124
+ const { chainConfig: chainOutConfig } = resolveChain(chainOut);
125
+ destinationChainId = chainOutConfig.chainId;
126
+ } catch {
127
+ res.status(400).json({ error: `Unknown destination chain: ${chainOut}` });
128
+ return;
129
+ }
130
+ // Cross-chain only supported with Relay
131
+ if (requestedDex && requestedDex !== 'relay') {
132
+ res.status(400).json({ error: 'Cross-chain swaps are only supported with Relay (the default DEX)' });
133
+ return;
134
+ }
135
+ targetChainOut = chainOut;
136
+ }
137
+
138
+ // Dispatch to chain-specific handler
139
+ if (isSolanaChain(targetChain)) {
140
+ return await handleSolanaSwap(req, res, auth, targetChain, chainConfig);
141
+ }
142
+ return await handleEvmSwap(req, res, auth, targetChain, chainConfig, destinationChainId, targetChainOut);
143
+
144
+ } catch (error) {
145
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
146
+ res.status(400).json({ error: getErrorMessage(error) });
147
+ }
148
+ });
149
+
150
+ // POST /swap/quote - Get swap quote without executing
151
+ // Same auth and validation as /swap, but returns quote data only
152
+ router.post('/quote', requireWalletAuth, async (req: Request, res: Response) => {
153
+ try {
154
+ const {
155
+ from,
156
+ token,
157
+ direction,
158
+ amount,
159
+ slippage,
160
+ dex: requestedDex,
161
+ chain,
162
+ chainOut,
163
+ } = req.body as SwapRequest;
164
+
165
+ const auth = req.auth!;
166
+
167
+ // Validate required fields
168
+ if (!from || typeof from !== 'string') {
169
+ res.status(400).json({ error: 'from address is required' });
170
+ return;
171
+ }
172
+
173
+ if (!token || typeof token !== 'string') {
174
+ res.status(400).json({ error: 'token address is required' });
175
+ return;
176
+ }
177
+
178
+ if (!direction || (direction !== 'buy' && direction !== 'sell')) {
179
+ res.status(400).json({ error: 'direction must be "buy" or "sell"' });
180
+ return;
181
+ }
182
+
183
+ if (!amount || typeof amount !== 'string') {
184
+ res.status(400).json({ error: 'amount is required' });
185
+ return;
186
+ }
187
+
188
+ // Get chain config
189
+ const { targetChain, chainConfig } = resolveChain(chain);
190
+
191
+ // Resolve destination chain for cross-chain quotes
192
+ let destinationChainId: number | undefined;
193
+ if (chainOut) {
194
+ try {
195
+ const { chainConfig: chainOutConfig } = resolveChain(chainOut);
196
+ destinationChainId = chainOutConfig.chainId;
197
+ } catch {
198
+ res.status(400).json({ error: `Unknown destination chain: ${chainOut}` });
199
+ return;
200
+ }
201
+ if (requestedDex && requestedDex !== 'relay') {
202
+ res.status(400).json({ error: 'Cross-chain swaps are only supported with Relay' });
203
+ return;
204
+ }
205
+ }
206
+
207
+ // Permission check
208
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['swap'])) {
209
+ logger.permissionDenied('swap', auth.token.agentId, '/swap/quote');
210
+ res.status(403).json({ error: 'Token does not have swap permission' });
211
+ return;
212
+ }
213
+
214
+ // Wallet access check
215
+ const hotWallet = await getHotWallet(from);
216
+ const isTempWallet = isSolanaChain(targetChain) ? hasTempWallet(from) : !!getTempWallet(from);
217
+
218
+ if (!hotWallet && !isTempWallet) {
219
+ res.status(404).json({ error: 'Wallet not found' });
220
+ return;
221
+ }
222
+
223
+ if (hotWallet && !isAdmin(auth)) {
224
+ const chainParam = isSolanaChain(targetChain) ? targetChain : undefined;
225
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, from, chainParam);
226
+ if (!canAccess) {
227
+ logger.permissionDenied('wallet_access', auth.token.agentId, '/swap/quote');
228
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
229
+ return;
230
+ }
231
+ }
232
+
233
+ // Calculate effective slippage
234
+ const effectiveSlippage = slippage ?? getDefaultSync<number>('swap.min_slippage_agent', 1.0);
235
+ const slippageBps = Math.round(effectiveSlippage * 100);
236
+
237
+ // --- Solana/Jupiter quote ---
238
+ if (isSolanaChain(targetChain)) {
239
+ if (chainOut) {
240
+ res.status(400).json({ error: 'Cross-chain swaps are not supported on Solana' });
241
+ return;
242
+ }
243
+
244
+ const inputMint = direction === 'buy' ? NATIVE_ADDRESSES.SOL : token;
245
+ const outputMint = direction === 'buy' ? token : NATIVE_ADDRESSES.SOL;
246
+
247
+ const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
248
+
249
+ res.json({
250
+ success: true,
251
+ inputAmount: quote.inAmount,
252
+ outputAmount: quote.outAmount,
253
+ priceImpact: quote.priceImpactPct,
254
+ route: quote.routePlan.map((r: any) => ({
255
+ label: r.swapInfo.label,
256
+ percent: r.percent,
257
+ inAmount: r.swapInfo.inAmount,
258
+ outAmount: r.swapInfo.outAmount,
259
+ })),
260
+ slippage: effectiveSlippage,
261
+ dex: 'jupiter',
262
+ chain: targetChain,
263
+ });
264
+ return;
265
+ }
266
+
267
+ // --- EVM quote ---
268
+ // Determine DEX
269
+ const provider = new ethers.JsonRpcProvider(await getRpcUrl(targetChain));
270
+ let dexName: string;
271
+
272
+ if (requestedDex) {
273
+ const adapter = getDexAdapter(requestedDex);
274
+ if (!adapter) {
275
+ res.status(400).json({ error: `Unknown DEX: ${requestedDex}. Available: ${listDexes().join(', ')}` });
276
+ return;
277
+ }
278
+ if (!adapter.supportsChain(chainConfig.chainId)) {
279
+ res.status(400).json({ error: `${requestedDex} not supported on ${targetChain}` });
280
+ return;
281
+ }
282
+ dexName = adapter.name;
283
+ } else {
284
+ // Auto-detect
285
+ const result = await detectBestDex(token, provider, chainConfig.chainId);
286
+ if (!result) {
287
+ res.status(400).json({ error: 'No liquidity pool found for this token' });
288
+ return;
289
+ }
290
+ dexName = result.adapter.name;
291
+ }
292
+
293
+ // For Relay (default), use the lightweight /price endpoint
294
+ if (dexName === 'relay') {
295
+ const originCurrency = direction === 'buy'
296
+ ? '0x0000000000000000000000000000000000000000'
297
+ : token;
298
+ const destinationCurrency = direction === 'buy'
299
+ ? token
300
+ : '0x0000000000000000000000000000000000000000';
301
+
302
+ const price = await getRelayPrice({
303
+ user: from,
304
+ originChainId: chainConfig.chainId,
305
+ destinationChainId,
306
+ originCurrency,
307
+ destinationCurrency,
308
+ amount,
309
+ slippageBps,
310
+ });
311
+
312
+ res.json({
313
+ success: true,
314
+ inputAmount: price.details.sender.amount,
315
+ inputFormatted: price.details.sender.amountFormatted,
316
+ inputUsd: price.details.sender.amountUsd,
317
+ outputAmount: price.details.recipient.amount,
318
+ outputFormatted: price.details.recipient.amountFormatted,
319
+ outputUsd: price.details.recipient.amountUsd,
320
+ rate: price.details.rate,
321
+ fees: price.fees,
322
+ slippage: effectiveSlippage,
323
+ dex: 'relay',
324
+ chain: targetChain,
325
+ ...(chainOut && { chainOut }),
326
+ });
327
+ return;
328
+ }
329
+
330
+ // For Uniswap — return pool info (no on-chain quote endpoint in our adapter)
331
+ const adapter = getDexAdapter(dexName)!;
332
+ const pool = await adapter.detectPool(token, provider);
333
+
334
+ res.json({
335
+ success: true,
336
+ dex: dexName,
337
+ chain: targetChain,
338
+ pool: pool ? { version: pool.version, fee: pool.fee, poolAddress: pool.poolAddress } : null,
339
+ message: `Use POST /swap to execute. ${dexName} quote-only is not available — pool info returned instead.`,
340
+ });
341
+
342
+ } catch (error) {
343
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
344
+ res.status(400).json({ error: getErrorMessage(error) });
345
+ }
346
+ });
347
+
348
+ // GET /swap/dexes - List available DEXes
349
+ router.get('/dexes', (_req: Request, res: Response) => {
350
+ res.json({
351
+ success: true,
352
+ dexes: listDexes()
353
+ });
354
+ });
355
+
356
+ export default router;
@@ -0,0 +1,247 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { createPublicClient, http, erc20Abi, formatUnits, type Address, type Chain } from 'viem';
3
+ import { base, mainnet } from 'viem/chains';
4
+ import { PublicKey } from '@solana/web3.js';
5
+ import { searchTokens } from '../lib/token-search';
6
+ import { getTokenSafety } from '../lib/token-safety';
7
+ import { getRpcUrl, loadConfig } from '../lib/config';
8
+ import { isSolanaChain } from '../lib/address';
9
+ import { getSolanaConnection } from '../lib/solana/connection';
10
+ import { getTokenPrices } from '../lib/price';
11
+ import { getErrorMessage } from '../lib/error';
12
+
13
+ const router = Router();
14
+
15
+ // Map chain names to viem chain objects
16
+ const VIEM_CHAINS: Record<string, Chain> = {
17
+ base,
18
+ ethereum: mainnet,
19
+ };
20
+
21
+ // GET /token/search — Public endpoint (no auth required)
22
+ router.get('/search', async (req: Request, res: Response) => {
23
+ try {
24
+ const q = req.query.q as string | undefined;
25
+ if (!q || !q.trim()) {
26
+ res.status(400).json({ success: false, error: 'Missing required query parameter: q' });
27
+ return;
28
+ }
29
+
30
+ const chain = (req.query.chain as string) || undefined;
31
+ const parsedLimit = req.query.limit !== undefined ? parseInt(req.query.limit as string, 10) : NaN;
32
+ const limit = Number.isNaN(parsedLimit) ? 10 : Math.min(Math.max(parsedLimit, 1), 50);
33
+
34
+ const results = await searchTokens(q.trim(), { chain, limit });
35
+
36
+ res.json({
37
+ success: true,
38
+ query: q.trim(),
39
+ results,
40
+ });
41
+ } catch (error) {
42
+ const message = getErrorMessage(error);
43
+ res.status(500).json({ success: false, error: message });
44
+ }
45
+ });
46
+
47
+ // GET /token/safety/:address — Public endpoint (no auth required)
48
+ router.get('/safety/:address', async (req: Request, res: Response) => {
49
+ try {
50
+ const { address } = req.params;
51
+ const chain = (req.query.chain as string) || 'ethereum';
52
+
53
+ const result = await getTokenSafety(address, chain);
54
+
55
+ if (!result) {
56
+ res.status(404).json({
57
+ success: false,
58
+ error: `No safety data found for ${address} on ${chain}`,
59
+ });
60
+ return;
61
+ }
62
+
63
+ res.json({
64
+ success: true,
65
+ address,
66
+ chain,
67
+ safety: result,
68
+ });
69
+ } catch (error) {
70
+ const message = getErrorMessage(error);
71
+ res.status(500).json({ success: false, error: message });
72
+ }
73
+ });
74
+
75
+ // GET /token/holders/:address — Public endpoint (no auth required)
76
+ // Convenience endpoint that returns holder data from the same GoPlusLabs source
77
+ router.get('/holders/:address', async (req: Request, res: Response) => {
78
+ try {
79
+ const { address } = req.params;
80
+ const chain = (req.query.chain as string) || 'ethereum';
81
+
82
+ const result = await getTokenSafety(address, chain);
83
+
84
+ if (!result) {
85
+ res.status(404).json({
86
+ success: false,
87
+ error: `No holder data found for ${address} on ${chain}`,
88
+ });
89
+ return;
90
+ }
91
+
92
+ res.json({
93
+ success: true,
94
+ address,
95
+ chain,
96
+ tokenName: result.tokenName,
97
+ tokenSymbol: result.tokenSymbol,
98
+ holderCount: result.holderCount,
99
+ holders: result.holders,
100
+ });
101
+ } catch (error) {
102
+ const message = getErrorMessage(error);
103
+ res.status(500).json({ success: false, error: message });
104
+ }
105
+ });
106
+
107
+ // GET /token/:tokenAddress/balance/:walletAddress — Public endpoint (no auth required)
108
+ // Returns the on-chain token balance for any wallet address (not just registered wallets)
109
+ router.get('/:tokenAddress/balance/:walletAddress', async (req: Request, res: Response) => {
110
+ try {
111
+ const { tokenAddress, walletAddress } = req.params;
112
+ const config = loadConfig();
113
+ const chain = (req.query.chain as string) || config.defaultChain;
114
+
115
+ if (isSolanaChain(chain)) {
116
+ // ── Solana SPL token balance ──
117
+ try {
118
+ new PublicKey(walletAddress);
119
+ new PublicKey(tokenAddress);
120
+ } catch {
121
+ res.status(400).json({ success: false, error: 'Invalid Solana address format' });
122
+ return;
123
+ }
124
+
125
+ const connection = await getSolanaConnection(chain);
126
+ const { TOKEN_PROGRAM_ID, getAssociatedTokenAddress, getAccount } = await import('@solana/spl-token');
127
+ const mint = new PublicKey(tokenAddress);
128
+ const owner = new PublicKey(walletAddress);
129
+
130
+ // Get mint info for decimals
131
+ const mintInfo = await connection.getParsedAccountInfo(mint);
132
+ const mintData = (mintInfo.value?.data as any)?.parsed?.info;
133
+ if (!mintData) {
134
+ res.status(404).json({ success: false, error: `Token mint not found: ${tokenAddress}` });
135
+ return;
136
+ }
137
+ const decimals = mintData.decimals ?? 0;
138
+
139
+ // Get the associated token account balance
140
+ let balance = '0';
141
+ let formatted = '0';
142
+ try {
143
+ const ata = await getAssociatedTokenAddress(mint, owner);
144
+ const account = await getAccount(connection, ata);
145
+ balance = account.amount.toString();
146
+ formatted = formatUnits(account.amount, decimals);
147
+ } catch {
148
+ // No ATA = zero balance
149
+ }
150
+
151
+ // Fetch price
152
+ const priceMap = await getTokenPrices([{ address: tokenAddress, chain }]);
153
+ const cacheKey = `${chain}:${tokenAddress}`;
154
+ const price = priceMap.get(cacheKey);
155
+ const priceUsd = price ? parseFloat(price.priceUsd) : null;
156
+ const balanceNum = parseFloat(formatted) || 0;
157
+ const valueUsd = priceUsd !== null && balanceNum > 0 ? balanceNum * priceUsd : null;
158
+
159
+ res.json({
160
+ success: true,
161
+ tokenAddress,
162
+ walletAddress,
163
+ chain,
164
+ balance,
165
+ formatted,
166
+ decimals,
167
+ priceUsd: priceUsd !== null ? priceUsd.toString() : null,
168
+ valueUsd: valueUsd !== null ? valueUsd.toFixed(2) : null,
169
+ });
170
+ return;
171
+ }
172
+
173
+ // ── EVM ERC-20 balance ──
174
+ if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress)) {
175
+ res.status(400).json({ success: false, error: 'Invalid EVM token address format' });
176
+ return;
177
+ }
178
+ if (!/^0x[a-fA-F0-9]{40}$/.test(walletAddress)) {
179
+ res.status(400).json({ success: false, error: 'Invalid EVM wallet address format' });
180
+ return;
181
+ }
182
+
183
+ const rpcUrl = await getRpcUrl(chain);
184
+ const viemChain = VIEM_CHAINS[chain];
185
+ const tokenAddr = tokenAddress as Address;
186
+ const walletAddr = walletAddress as Address;
187
+
188
+ const client = createPublicClient({
189
+ chain: viemChain,
190
+ transport: http(rpcUrl),
191
+ batch: { multicall: true },
192
+ });
193
+
194
+ // Batch balanceOf, decimals, symbol, name via multicall
195
+ const contracts = [
196
+ { address: tokenAddr, abi: erc20Abi, functionName: 'balanceOf' as const, args: [walletAddr] as const },
197
+ { address: tokenAddr, abi: erc20Abi, functionName: 'decimals' as const },
198
+ { address: tokenAddr, abi: erc20Abi, functionName: 'symbol' as const },
199
+ { address: tokenAddr, abi: erc20Abi, functionName: 'name' as const },
200
+ ];
201
+
202
+ const results = await client.multicall({
203
+ contracts,
204
+ allowFailure: true,
205
+ });
206
+
207
+ const [balanceResult, decimalsResult, symbolResult, nameResult] = results;
208
+
209
+ if (balanceResult.status === 'failure') {
210
+ res.status(502).json({ success: false, error: 'Failed to query token balance from RPC' });
211
+ return;
212
+ }
213
+
214
+ const rawBalance = balanceResult.result as bigint;
215
+ const tokenDecimals = decimalsResult.status === 'success' ? (decimalsResult.result as number) : 18;
216
+ const tokenSymbol = symbolResult.status === 'success' ? (symbolResult.result as string) : null;
217
+ const tokenName = nameResult.status === 'success' ? (nameResult.result as string) : null;
218
+ const formatted = formatUnits(rawBalance, tokenDecimals);
219
+
220
+ // Fetch price
221
+ const priceMap = await getTokenPrices([{ address: tokenAddress, chain }]);
222
+ const cacheKey = `${chain}:${tokenAddress.toLowerCase()}`;
223
+ const price = priceMap.get(cacheKey);
224
+ const priceUsd = price ? parseFloat(price.priceUsd) : null;
225
+ const balanceNum = parseFloat(formatted) || 0;
226
+ const valueUsd = priceUsd !== null && balanceNum > 0 ? balanceNum * priceUsd : null;
227
+
228
+ res.json({
229
+ success: true,
230
+ tokenAddress,
231
+ walletAddress,
232
+ chain,
233
+ balance: rawBalance.toString(),
234
+ formatted,
235
+ decimals: tokenDecimals,
236
+ symbol: tokenSymbol,
237
+ name: tokenName,
238
+ priceUsd: priceUsd !== null ? priceUsd.toString() : null,
239
+ valueUsd: valueUsd !== null ? valueUsd.toFixed(2) : null,
240
+ });
241
+ } catch (error) {
242
+ const message = getErrorMessage(error);
243
+ res.status(500).json({ success: false, error: message });
244
+ }
245
+ });
246
+
247
+ export default router;