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,217 @@
1
+ /**
2
+ * Portfolio endpoint — cross-wallet asset aggregation.
3
+ * Returns native balances by chain + token balances aggregated across wallets.
4
+ * Supports filtering by token address or symbol for per-wallet breakdown.
5
+ */
6
+
7
+ import { Router, Request, Response } from 'express';
8
+ import { optionalWalletAuth } from '../middleware/auth';
9
+ import { isAdmin, hasAnyPermission } from '../lib/permissions';
10
+ import { prisma } from '../lib/db';
11
+ import { listHotWallets } from '../lib/hot';
12
+ import { getEthToUsd, getSolToUsd } from '../lib/prices';
13
+ import { getTokenPrices } from '../lib/price';
14
+ import { getNativeCurrency } from '../lib/address';
15
+ import { getErrorMessage } from '../lib/error';
16
+
17
+ const router = Router();
18
+
19
+ // GET /portfolio — aggregated balances across all wallets
20
+ // Requires wallet:list permission for agents (admin always OK, no-auth OK)
21
+ // Query params:
22
+ // token — filter by token contract address (returns per-wallet breakdown)
23
+ // symbol — filter by token symbol, case-insensitive (returns per-wallet breakdown)
24
+ // chain — filter by chain name
25
+ router.get('/', optionalWalletAuth, async (req: Request, res: Response) => {
26
+ try {
27
+ const auth = req.auth;
28
+ const isAgent = auth && !isAdmin(auth);
29
+ const canListAll = isAgent && hasAnyPermission(auth.token.permissions, ['wallet:list']);
30
+
31
+ // Agents without wallet:list can only see their own wallets
32
+ let walletFilter: string[] | undefined;
33
+ if (isAgent && !canListAll) {
34
+ const owned = await listHotWallets(auth.tokenHash);
35
+ walletFilter = owned.map((w) => w.address.toLowerCase());
36
+ if (walletFilter.length === 0) {
37
+ res.json({ success: true, byChain: [], byToken: [] });
38
+ return;
39
+ }
40
+ }
41
+
42
+ const { token, symbol, chain } = req.query as Record<string, string | undefined>;
43
+
44
+ // Native balances by chain
45
+ const nativeWhere: Record<string, unknown> = {};
46
+ if (walletFilter) nativeWhere.walletAddress = { in: walletFilter };
47
+ if (chain) nativeWhere.chain = chain;
48
+
49
+ const nativeBalances = await prisma.nativeBalance.findMany({
50
+ where: nativeWhere,
51
+ });
52
+
53
+ // Group native balances by chain
54
+ const byChainMap = new Map<string, { chain: string; totalBalance: number; walletCount: number }>();
55
+ for (const nb of nativeBalances) {
56
+ const entry = byChainMap.get(nb.chain) || { chain: nb.chain, totalBalance: 0, walletCount: 0 };
57
+ entry.totalBalance += parseFloat(nb.balance) || 0;
58
+ entry.walletCount += 1;
59
+ byChainMap.set(nb.chain, entry);
60
+ }
61
+
62
+ // Token balances — build filter (exclude bookmarks with null walletAddress)
63
+ const assetWhere: Record<string, unknown> = {};
64
+ if (walletFilter) {
65
+ assetWhere.walletAddress = { in: walletFilter };
66
+ } else {
67
+ assetWhere.walletAddress = { not: null };
68
+ }
69
+ assetWhere.lastBalance = { not: null };
70
+ if (chain) assetWhere.chain = chain;
71
+ if (token) assetWhere.tokenAddress = token.toLowerCase();
72
+ if (symbol) assetWhere.symbol = symbol.toUpperCase();
73
+
74
+ const trackedAssets = await prisma.trackedAsset.findMany({
75
+ where: assetWhere,
76
+ select: {
77
+ tokenAddress: true,
78
+ symbol: true,
79
+ name: true,
80
+ decimals: true,
81
+ lastBalance: true,
82
+ chain: true,
83
+ walletAddress: true,
84
+ },
85
+ });
86
+
87
+ // Batch-lookup TokenMetadata for enrichment
88
+ const uniqueTokenKeys = [...new Set(trackedAssets.map(a => `${a.tokenAddress}:${a.chain}`))];
89
+ const tokenMetaList = uniqueTokenKeys.length > 0
90
+ ? await prisma.tokenMetadata.findMany({
91
+ where: {
92
+ OR: uniqueTokenKeys.map(k => {
93
+ const [addr, ch] = k.split(':');
94
+ return { tokenAddress: addr, chain: ch };
95
+ }),
96
+ },
97
+ })
98
+ : [];
99
+ const metaMap = new Map(tokenMetaList.map(m => [`${m.tokenAddress}:${m.chain}`, m]));
100
+
101
+ // Group by tokenAddress+chain
102
+ const tokenKey = (addr: string, c: string) => `${addr.toLowerCase()}:${c}`;
103
+ const byTokenMap = new Map<string, {
104
+ tokenAddress: string;
105
+ chain: string;
106
+ symbol: string | null;
107
+ name: string | null;
108
+ decimals: number;
109
+ totalBalance: number;
110
+ walletCount: number;
111
+ }>();
112
+
113
+ // When filtering by token/symbol, also collect per-wallet breakdown
114
+ const isFiltered = !!(token || symbol);
115
+ const walletBreakdown: { walletAddress: string; chain: string; balance: number }[] = [];
116
+
117
+ for (const asset of trackedAssets) {
118
+ const key = tokenKey(asset.tokenAddress, asset.chain);
119
+ const meta = metaMap.get(`${asset.tokenAddress}:${asset.chain}`);
120
+ const existing = byTokenMap.get(key);
121
+ const balance = parseFloat(asset.lastBalance || '0') || 0;
122
+
123
+ if (isFiltered && asset.walletAddress) {
124
+ walletBreakdown.push({
125
+ walletAddress: asset.walletAddress,
126
+ chain: asset.chain,
127
+ balance,
128
+ });
129
+ }
130
+
131
+ if (existing) {
132
+ existing.totalBalance += balance;
133
+ existing.walletCount += 1;
134
+ if (!existing.symbol && (meta?.symbol || asset.symbol)) existing.symbol = meta?.symbol ?? asset.symbol;
135
+ if (!existing.name && (meta?.name || asset.name)) existing.name = meta?.name ?? asset.name;
136
+ } else {
137
+ byTokenMap.set(key, {
138
+ tokenAddress: asset.tokenAddress,
139
+ chain: asset.chain,
140
+ symbol: meta?.symbol ?? asset.symbol,
141
+ name: meta?.name ?? asset.name,
142
+ decimals: meta?.decimals ?? asset.decimals,
143
+ totalBalance: balance,
144
+ walletCount: 1,
145
+ });
146
+ }
147
+ }
148
+
149
+ // Fetch cached native prices
150
+ const [ethPrice, solPrice] = await Promise.all([getEthToUsd(), getSolToUsd()]);
151
+ const nativePrices: Record<string, number | null> = { ETH: ethPrice, SOL: solPrice };
152
+
153
+ // Batch-fetch USD prices for all tracked tokens
154
+ const tokenEntries = Array.from(byTokenMap.values());
155
+ const priceMap = await getTokenPrices(
156
+ tokenEntries.map((t) => ({ address: t.tokenAddress, chain: t.chain })),
157
+ );
158
+
159
+ // Enrich tokens with USD values
160
+ let totalValueUsd = 0;
161
+ const byToken = tokenEntries.map((t) => {
162
+ const cacheKey = `${t.chain}:${t.tokenAddress.toLowerCase()}`;
163
+ const price = priceMap.get(cacheKey);
164
+ const priceUsd = price ? parseFloat(price.priceUsd) : null;
165
+ const valueUsd = priceUsd !== null ? t.totalBalance * priceUsd : null;
166
+ if (valueUsd !== null) totalValueUsd += valueUsd;
167
+ return {
168
+ ...t,
169
+ priceUsd: priceUsd !== null ? priceUsd.toString() : null,
170
+ valueUsd: valueUsd !== null ? valueUsd.toFixed(2) : null,
171
+ };
172
+ }).sort((a, b) => {
173
+ const aVal = a.valueUsd ? parseFloat(a.valueUsd) : -1;
174
+ const bVal = b.valueUsd ? parseFloat(b.valueUsd) : -1;
175
+ return bVal - aVal;
176
+ });
177
+
178
+ // Enrich chains with USD values
179
+ const byChain = Array.from(byChainMap.values()).map((c) => {
180
+ const currency = getNativeCurrency(c.chain);
181
+ const nativePrice = nativePrices[currency];
182
+ const valueUsd = nativePrice !== null ? c.totalBalance * nativePrice : null;
183
+ if (valueUsd !== null) totalValueUsd += valueUsd;
184
+ return {
185
+ ...c,
186
+ valueUsd: valueUsd !== null ? valueUsd.toFixed(2) : null,
187
+ };
188
+ });
189
+
190
+ // Enrich wallet breakdown with USD when filtered
191
+ let wallets: { walletAddress: string; chain: string; balance: number; valueUsd: string | null }[] | undefined;
192
+ if (isFiltered && walletBreakdown.length > 0) {
193
+ // Use first token's price for all wallet entries (same token)
194
+ const firstPrice = byToken[0]?.priceUsd ? parseFloat(byToken[0].priceUsd) : null;
195
+ wallets = walletBreakdown.map((w) => ({
196
+ ...w,
197
+ valueUsd: firstPrice !== null ? (w.balance * firstPrice).toFixed(2) : null,
198
+ }));
199
+ }
200
+
201
+ const response: Record<string, unknown> = {
202
+ success: true,
203
+ byChain,
204
+ byToken,
205
+ prices: { ETH: ethPrice, SOL: solPrice },
206
+ totalValueUsd: totalValueUsd.toFixed(2),
207
+ };
208
+ if (wallets) response.wallets = wallets;
209
+
210
+ res.json(response);
211
+ } catch (error) {
212
+ const message = getErrorMessage(error);
213
+ res.status(500).json({ success: false, error: message });
214
+ }
215
+ });
216
+
217
+ export default router;
@@ -0,0 +1,63 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { getTokenPrice } from '../lib/price';
3
+ import { loadConfig } from '../lib/config';
4
+ import { isSolanaChain } from '../lib/address';
5
+ import { getErrorMessage } from '../lib/error';
6
+
7
+ const router = Router();
8
+
9
+ // GET /price/:address — Public endpoint (no auth required)
10
+ router.get('/:address', async (req: Request, res: Response) => {
11
+ try {
12
+ const { address } = req.params;
13
+ const config = loadConfig();
14
+ const chain = (req.query.chain as string) || config.defaultChain;
15
+
16
+ // Validate chain
17
+ if (!config.chains[chain]) {
18
+ res.status(400).json({ success: false, error: `Unknown chain: ${chain}` });
19
+ return;
20
+ }
21
+
22
+ // Validate address format (skip for 'native')
23
+ if (address !== 'native') {
24
+ if (isSolanaChain(chain)) {
25
+ // Base58: alphanumeric (no 0, O, I, l), 32-44 chars
26
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
27
+ res.status(400).json({ success: false, error: 'Invalid Solana address format' });
28
+ return;
29
+ }
30
+ } else {
31
+ // EVM: 0x-prefixed hex, 42 chars
32
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
33
+ res.status(400).json({ success: false, error: 'Invalid EVM address format' });
34
+ return;
35
+ }
36
+ }
37
+ }
38
+
39
+ const result = await getTokenPrice(address, chain);
40
+
41
+ if (!result) {
42
+ res.status(404).json({
43
+ success: false,
44
+ error: `No price found for ${address} on ${chain}`,
45
+ });
46
+ return;
47
+ }
48
+
49
+ res.json({
50
+ success: true,
51
+ token: address,
52
+ chain,
53
+ priceUsd: result.priceUsd,
54
+ source: result.source,
55
+ cached: result.cached,
56
+ });
57
+ } catch (error) {
58
+ const message = getErrorMessage(error);
59
+ res.status(500).json({ success: false, error: message });
60
+ }
61
+ });
62
+
63
+ export default router;
@@ -0,0 +1,31 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { resolveName } from '../lib/resolve';
3
+ import { getErrorMessage } from '../lib/error';
4
+
5
+ const router = Router();
6
+
7
+ // GET /resolve/:name - Resolve ENS name to address
8
+ // Public endpoint (no auth required)
9
+ router.get('/:name', async (req: Request, res: Response) => {
10
+ try {
11
+ const { name } = req.params;
12
+
13
+ if (!name) {
14
+ res.status(400).json({ error: 'Name parameter is required' });
15
+ return;
16
+ }
17
+
18
+ const result = await resolveName(name);
19
+
20
+ res.json({
21
+ success: true,
22
+ address: result.address,
23
+ name: result.name,
24
+ });
25
+ } catch (error) {
26
+ const message = getErrorMessage(error);
27
+ res.status(400).json({ error: message });
28
+ }
29
+ });
30
+
31
+ export default router;
@@ -0,0 +1,45 @@
1
+ import { Request, Response, Router } from 'express';
2
+ import { getErrorMessage } from '../lib/error';
3
+ import { requireAdmin, requireWalletAuth } from '../middleware/auth';
4
+ import {
5
+ listNoisyCredentials,
6
+ listNoisyCredentialTokens,
7
+ listRecentCredentialAccess,
8
+ } from '../lib/credential-access-audit';
9
+
10
+ const router = Router();
11
+ router.use(requireWalletAuth, requireAdmin);
12
+
13
+ router.get('/credential-access/recent', async (req: Request, res: Response) => {
14
+ try {
15
+ const limit = Number.parseInt(String(req.query.limit ?? '50'), 10);
16
+ const rows = await listRecentCredentialAccess(limit);
17
+ res.json({ success: true, rows });
18
+ } catch (error) {
19
+ res.status(500).json({ success: false, error: getErrorMessage(error) });
20
+ }
21
+ });
22
+
23
+ router.get('/credential-access/noisy-credentials', async (req: Request, res: Response) => {
24
+ try {
25
+ const windowMs = Number.parseInt(String(req.query.windowMs ?? 3600000), 10);
26
+ const limit = Number.parseInt(String(req.query.limit ?? '20'), 10);
27
+ const rows = await listNoisyCredentials(windowMs, limit);
28
+ res.json({ success: true, windowMs, rows });
29
+ } catch (error) {
30
+ res.status(500).json({ success: false, error: getErrorMessage(error) });
31
+ }
32
+ });
33
+
34
+ router.get('/credential-access/noisy-tokens', async (req: Request, res: Response) => {
35
+ try {
36
+ const windowMs = Number.parseInt(String(req.query.windowMs ?? 3600000), 10);
37
+ const limit = Number.parseInt(String(req.query.limit ?? '20'), 10);
38
+ const rows = await listNoisyCredentialTokens(windowMs, limit);
39
+ res.json({ success: true, windowMs, rows });
40
+ } catch (error) {
41
+ res.status(500).json({ success: false, error: getErrorMessage(error) });
42
+ }
43
+ });
44
+
45
+ export default router;
@@ -0,0 +1,241 @@
1
+ import { Request, Response } from 'express';
2
+ import { ethers } from 'ethers';
3
+ import { getHotWallet, signWithHotWallet, tokenCanAccessWallet } from '../lib/hot';
4
+ import { getTempWallet, signWithTempWallet } from '../lib/temp';
5
+ import { isUnlocked, getColdWalletAddress, listVaults } from '../lib/cold';
6
+ import { reserveSpend, releaseSpend } from '../lib/sessions';
7
+ import { getRpcUrl } from '../lib/config';
8
+ import { logger } from '../lib/logger';
9
+ import { hasAnyPermission, isAdmin } from '../lib/permissions';
10
+ import { getErrorMessage, HttpError } from '../lib/error';
11
+ import { recordTransaction, autoTrackToken } from '../lib/transactions';
12
+ import type { AuthInfo } from '../middleware/auth';
13
+ import type { ChainConfig } from '../lib/config';
14
+
15
+ export async function handleEvmSend(
16
+ req: Request,
17
+ res: Response,
18
+ auth: AuthInfo,
19
+ targetChain: string,
20
+ chainConfig: ChainConfig
21
+ ): Promise<void> {
22
+ try {
23
+ const {
24
+ from,
25
+ amount,
26
+ value,
27
+ data,
28
+ gasLimit,
29
+ gasPrice,
30
+ maxFeePerGas,
31
+ maxPriorityFeePerGas,
32
+ nonce,
33
+ tokenAddress,
34
+ description: userDescription
35
+ } = req.body;
36
+
37
+ // 'to' may have been resolved by the dispatcher (ENS resolution)
38
+ const to = req.body.to;
39
+ const rawValue = amount || value;
40
+
41
+ // Parse value
42
+ let valueWei = BigInt(0);
43
+ let valueEth = 0;
44
+ if (rawValue) {
45
+ if (typeof rawValue === 'string') {
46
+ valueWei = BigInt(rawValue);
47
+ } else if (typeof rawValue === 'number') {
48
+ valueWei = BigInt(rawValue);
49
+ }
50
+ valueEth = parseFloat(ethers.formatEther(valueWei));
51
+ }
52
+
53
+ const provider = new ethers.JsonRpcProvider(await getRpcUrl(targetChain));
54
+
55
+ // Determine wallet type
56
+ const hotWallet = await getHotWallet(from);
57
+ const tempWallet = getTempWallet(from);
58
+
59
+ let txHash: string;
60
+
61
+ // Build transaction object
62
+ const tx: ethers.TransactionRequest = {
63
+ from,
64
+ value: valueWei
65
+ };
66
+
67
+ if (to) tx.to = to;
68
+ if (data) tx.data = data;
69
+ if (gasLimit) tx.gasLimit = BigInt(gasLimit);
70
+ if (nonce !== undefined) tx.nonce = nonce;
71
+
72
+ // Gas price settings (EIP-1559 or legacy)
73
+ if (maxFeePerGas) {
74
+ tx.maxFeePerGas = BigInt(maxFeePerGas);
75
+ if (maxPriorityFeePerGas) {
76
+ tx.maxPriorityFeePerGas = BigInt(maxPriorityFeePerGas);
77
+ }
78
+ } else if (gasPrice) {
79
+ tx.gasPrice = BigInt(gasPrice);
80
+ }
81
+
82
+ // ERC-20 token send: encode transfer(to, amount) calldata
83
+ const isTokenSend = !!tokenAddress;
84
+ if (isTokenSend) {
85
+ if (!to) {
86
+ res.status(400).json({ error: 'to address is required for token sends' });
87
+ return;
88
+ }
89
+ const iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
90
+ tx.data = iface.encodeFunctionData('transfer', [to, valueWei]);
91
+ tx.to = tokenAddress; // send to the token contract
92
+ tx.value = 0n; // no native value
93
+ }
94
+
95
+ // Check if this send is going to any vault's EVM address (returning funds to vault bypasses limit)
96
+ const coldAddress = getColdWalletAddress();
97
+ let isSendToVault = to && coldAddress && to.toLowerCase() === coldAddress.toLowerCase();
98
+ if (!isSendToVault && to) {
99
+ const vaults = listVaults();
100
+ isSendToVault = vaults.some(v => v.address.toLowerCase() === to.toLowerCase());
101
+ }
102
+
103
+ if (hotWallet) {
104
+ // Check permission
105
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['send:hot'])) {
106
+ logger.permissionDenied('send:hot', auth.token.agentId, '/send');
107
+ res.status(403).json({ error: 'Token does not have send:hot permission' });
108
+ return;
109
+ }
110
+
111
+ // Verify token can access this wallet
112
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, from);
113
+ if (!isAdmin(auth) && !canAccess) {
114
+ logger.permissionDenied('wallet_access', auth.token.agentId, '/send');
115
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
116
+ return;
117
+ }
118
+
119
+ // Reserve spending atomically (prevents TOCTOU race between concurrent requests)
120
+ // Token sends skip native spending limits (spending tokens, not ETH)
121
+ const needsHotLimit = !isAdmin(auth) && !isSendToVault && !isTokenSend && valueEth > 0;
122
+ if (needsHotLimit) {
123
+ const reserve = reserveSpend(auth.tokenHash, auth.token, 'send', valueEth);
124
+ if (!reserve.ok) {
125
+ logger.limitExceeded(auth.token.agentId, 'send', valueEth, reserve.remaining);
126
+ res.status(403).json({ error: 'Amount exceeds remaining send limit', remaining: reserve.remaining, requested: valueEth });
127
+ return;
128
+ }
129
+ }
130
+
131
+ if (!isUnlocked()) {
132
+ if (needsHotLimit) releaseSpend(auth.tokenHash, 'send', valueEth);
133
+ logger.authFailed('Cold wallet locked', '/send');
134
+ res.status(401).json({ error: 'Cold wallet must be unlocked to send from hot wallet' });
135
+ return;
136
+ }
137
+
138
+ try {
139
+ const result = await signWithHotWallet(from, tx, provider);
140
+ txHash = result.hash;
141
+ } catch (err) {
142
+ if (needsHotLimit) releaseSpend(auth.tokenHash, 'send', valueEth);
143
+ throw err;
144
+ }
145
+ } else if (tempWallet) {
146
+ // Check permission
147
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['send:temp'])) {
148
+ logger.permissionDenied('send:temp', auth.token.agentId, '/send');
149
+ res.status(403).json({ error: 'Token does not have send:temp permission' });
150
+ return;
151
+ }
152
+
153
+ // Reserve spending atomically for temp wallets too
154
+ // Token sends skip native spending limits (spending tokens, not ETH)
155
+ const needsTempLimit = !isAdmin(auth) && !isSendToVault && !isTokenSend && valueEth > 0;
156
+ if (needsTempLimit) {
157
+ const reserve = reserveSpend(auth.tokenHash, auth.token, 'send', valueEth);
158
+ if (!reserve.ok) {
159
+ logger.limitExceeded(auth.token.agentId, 'send', valueEth, reserve.remaining);
160
+ res.status(403).json({ error: 'Amount exceeds remaining send limit', remaining: reserve.remaining, requested: valueEth });
161
+ return;
162
+ }
163
+ }
164
+
165
+ try {
166
+ txHash = await signWithTempWallet(from, tx, provider);
167
+ } catch (err) {
168
+ if (needsTempLimit) releaseSpend(auth.tokenHash, 'send', valueEth);
169
+ throw err;
170
+ }
171
+ } else {
172
+ res.status(404).json({ error: 'Wallet not found' });
173
+ return;
174
+ }
175
+
176
+ // Log the transaction
177
+ const tokenAmountStr = rawValue || '0';
178
+ const txType = data && !isTokenSend ? 'contract' : 'send';
179
+ const description = userDescription || (isTokenSend
180
+ ? `Sent ${tokenAmountStr} tokens of ${tokenAddress} to ${to}`
181
+ : data
182
+ ? `Contract call to ${to || 'deploy'} with ${ethers.formatEther(valueWei)} ETH`
183
+ : `Sent ${ethers.formatEther(valueWei)} ETH to ${to}`);
184
+
185
+ await recordTransaction({
186
+ walletAddress: from,
187
+ txHash,
188
+ type: txType,
189
+ amount: isTokenSend ? undefined : ethers.formatEther(valueWei),
190
+ tokenAddress: isTokenSend ? tokenAddress : undefined,
191
+ tokenAmount: isTokenSend ? tokenAmountStr : undefined,
192
+ from,
193
+ to,
194
+ description,
195
+ chain: targetChain,
196
+ logTitle: isTokenSend ? 'Token Send' : data ? 'Contract Transaction' : 'Send Transaction',
197
+ });
198
+
199
+ // Auto-track token after successful ERC-20 send
200
+ if (isTokenSend) {
201
+ await autoTrackToken({
202
+ walletAddress: from,
203
+ tokenAddress,
204
+ chain: targetChain,
205
+ });
206
+ }
207
+
208
+ // Log send event (only for simple sends and token sends, not contract calls)
209
+ if ((!data || isTokenSend) && to) {
210
+ const agentId = !isAdmin(auth) ? auth.token.agentId : undefined;
211
+ const amountStr = isTokenSend ? `${tokenAmountStr} tokens` : ethers.formatEther(valueWei);
212
+ logger.send(from, to, amountStr, txHash, agentId);
213
+ }
214
+
215
+ const response: Record<string, unknown> = {
216
+ success: true,
217
+ hash: txHash,
218
+ from,
219
+ to: to || null,
220
+ chain: targetChain
221
+ };
222
+
223
+ if (isTokenSend) {
224
+ response.tokenAddress = tokenAddress;
225
+ response.tokenAmount = tokenAmountStr;
226
+ } else {
227
+ response.amount = ethers.formatEther(valueWei);
228
+ response.value = valueWei.toString();
229
+ }
230
+
231
+ if (data && !isTokenSend) {
232
+ response.type = 'contract';
233
+ response.data = data.slice(0, 10) + '...'; // Just show selector
234
+ }
235
+
236
+ res.json(response);
237
+ } catch (error) {
238
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
239
+ res.status(400).json({ error: getErrorMessage(error) });
240
+ }
241
+ }