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,710 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { ethers } from 'ethers';
3
+ import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
4
+ import { isUnlocked, getColdWalletInfo, getColdWalletAddress, exportSeed, getSolanaColdAddress, listVaults, exportVaultSeed, getVaultAddress, isVaultUnlocked } from '../lib/cold';
5
+ import { createHotWallet, listHotWallets, updateHotWallet, exportHotWallet, tokenCanAccessWallet, searchHotWallets, getHotWallet } from '../lib/hot';
6
+ import { createTempWallet, listTempWallets } from '../lib/temp';
7
+ import { getRpcUrl } from '../lib/config';
8
+ import { events } from '../lib/events';
9
+ import { requireWalletAuth, optionalWalletAuth } from '../middleware/auth';
10
+ import { requireAdmin, hasAnyPermission, isAdmin } from '../lib/permissions';
11
+ import { prisma } from '../lib/db';
12
+ import { isSolanaChain, normalizeAddress } from '../lib/address';
13
+ import { getSolanaConnection } from '../lib/solana/connection';
14
+ import { logger } from '../lib/logger';
15
+ import { getErrorMessage } from '../lib/error';
16
+ import transactionRoutes from './wallet-transactions';
17
+ import assetRoutes from './wallet-assets';
18
+
19
+ /**
20
+ * Fetch balance for a single address using JSON-RPC
21
+ */
22
+ export async function fetchBalance(address: string, chain: string = 'base'): Promise<string> {
23
+ try {
24
+ // Solana balance fetch
25
+ if (isSolanaChain(chain)) {
26
+ const connection = await getSolanaConnection(chain);
27
+ const pubkey = new PublicKey(address);
28
+ const lamports = await connection.getBalance(pubkey);
29
+ return (lamports / LAMPORTS_PER_SOL).toString();
30
+ }
31
+
32
+ // EVM balance fetch
33
+ const rpcUrl = await getRpcUrl(chain);
34
+ const response = await fetch(rpcUrl, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({
38
+ jsonrpc: '2.0',
39
+ id: 1,
40
+ method: 'eth_getBalance',
41
+ params: [address, 'latest']
42
+ })
43
+ });
44
+ const data = await response.json();
45
+ if (data.result) {
46
+ return ethers.formatEther(data.result);
47
+ }
48
+ return '0';
49
+ } catch (error) {
50
+ console.error(`[Balance] Failed to fetch for ${address}:`, error);
51
+ return '0';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Batch fetch balances for multiple addresses.
57
+ * Groups by chain type: Solana uses getMultipleAccountsInfo, EVM uses JSON-RPC batch.
58
+ */
59
+ export async function fetchBalances(
60
+ addresses: string[],
61
+ chain: string = 'base',
62
+ addressChainMap?: Map<string, string>
63
+ ): Promise<Map<string, string>> {
64
+ const balances = new Map<string, string>();
65
+ if (addresses.length === 0) return balances;
66
+
67
+ // Separate Solana and EVM addresses
68
+ const solanaAddrs: string[] = [];
69
+ const evmAddrs: string[] = [];
70
+
71
+ for (const addr of addresses) {
72
+ const addrChain = addressChainMap?.get(addr) || chain;
73
+ if (isSolanaChain(addrChain)) {
74
+ solanaAddrs.push(addr);
75
+ } else {
76
+ evmAddrs.push(addr);
77
+ }
78
+ }
79
+
80
+ // Fetch Solana balances
81
+ if (solanaAddrs.length > 0) {
82
+ try {
83
+ const solChain = addressChainMap?.get(solanaAddrs[0]) || 'solana';
84
+ const connection = await getSolanaConnection(solChain);
85
+ const pubkeys = solanaAddrs.map(a => new PublicKey(a));
86
+ const accountInfos = await connection.getMultipleAccountsInfo(pubkeys);
87
+ accountInfos.forEach((info, i) => {
88
+ const lamports = info?.lamports || 0;
89
+ balances.set(solanaAddrs[i], (lamports / LAMPORTS_PER_SOL).toString());
90
+ });
91
+ } catch (error) {
92
+ console.error('[Balance] Solana batch fetch failed:', error);
93
+ solanaAddrs.forEach(addr => balances.set(addr, '0'));
94
+ }
95
+ }
96
+
97
+ // Fetch EVM balances
98
+ if (evmAddrs.length > 0) {
99
+ try {
100
+ const rpcUrl = await getRpcUrl(chain);
101
+ const batch = evmAddrs.map((address, index) => ({
102
+ jsonrpc: '2.0',
103
+ id: index,
104
+ method: 'eth_getBalance',
105
+ params: [address, 'latest']
106
+ }));
107
+
108
+ const response = await fetch(rpcUrl, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify(batch)
112
+ });
113
+ if (!response.ok) {
114
+ throw new Error(`RPC returned ${response.status}: ${response.statusText}`);
115
+ }
116
+ const text = await response.text();
117
+ if (!text || text.trim().length === 0) {
118
+ throw new Error('RPC returned empty response');
119
+ }
120
+ const results = JSON.parse(text);
121
+
122
+ if (Array.isArray(results)) {
123
+ results.forEach((result, index) => {
124
+ const address = evmAddrs[index];
125
+ if (result.result) {
126
+ balances.set(address.toLowerCase(), ethers.formatEther(result.result));
127
+ } else {
128
+ balances.set(address.toLowerCase(), '0');
129
+ }
130
+ });
131
+ }
132
+ } catch (error) {
133
+ console.error(`[Balance] EVM batch fetch failed:`, error);
134
+ evmAddrs.forEach(addr => balances.set(addr.toLowerCase(), '0'));
135
+ }
136
+ }
137
+
138
+ return balances;
139
+ }
140
+
141
+ const router = Router();
142
+
143
+ // GET /wallets - List wallets (optional auth for filtering)
144
+ // With agent token + wallet:list: returns all hot wallets + cold wallet (read-only) + agent info
145
+ // With agent token without wallet:list: returns only owned wallets + agent info
146
+ // Without token or admin: returns all wallets
147
+ // Query params: tier (cold|hot|temp), chain (base|solana|...), sortBy (balance|createdAt|name), sortDir (asc|desc)
148
+ router.get('/', optionalWalletAuth, async (req: Request, res: Response) => {
149
+ try {
150
+ const includeHidden = req.query.includeHidden === 'true';
151
+ const tierFilter = req.query.tier as string | undefined;
152
+ const chainFilter = req.query.chain as string | undefined;
153
+ const sortBy = req.query.sortBy as string | undefined;
154
+ const sortDir = (req.query.sortDir as string)?.toLowerCase() === 'asc' ? 'asc' : 'desc';
155
+ const auth = req.auth;
156
+
157
+ // Determine if filtering for agent (non-admin token provided)
158
+ const isAgent = auth && !isAdmin(auth);
159
+ // Agents with wallet:list permission can see all hot wallets + cold wallet info
160
+ const canListAll = isAgent && hasAnyPermission(auth.token.permissions, ['wallet:list']);
161
+ const tokenHash = (isAgent && !canListAll) ? auth.tokenHash : undefined;
162
+
163
+ // Get wallets (filtered by tokenHash for agents without wallet:list)
164
+ const hotWallets = await listHotWallets(tokenHash, includeHidden);
165
+ // Temp wallets are not associated with tokens, return empty for agents without wallet:list
166
+ const tempWallets = (isAgent && !canListAll) ? [] : listTempWallets();
167
+
168
+ // Agents with wallet:list can see cold wallet info (read-only: address + balance)
169
+ const coldInfo = (isAgent && !canListAll) ? null : getColdWalletInfo();
170
+
171
+ // Build chain map for multi-chain balance fetching
172
+ const allAddresses: string[] = [];
173
+ const addressChainMap = new Map<string, string>();
174
+
175
+ if (coldInfo) {
176
+ allAddresses.push(coldInfo.address);
177
+ // Cold wallet is EVM by default
178
+ }
179
+
180
+ // Also include Solana cold address if available
181
+ const solColdAddr = (isAgent && !canListAll) ? null : getSolanaColdAddress();
182
+ if (solColdAddr) {
183
+ allAddresses.push(solColdAddr);
184
+ addressChainMap.set(solColdAddr, 'solana');
185
+ }
186
+
187
+ hotWallets.forEach(w => {
188
+ allAddresses.push(w.address);
189
+ if (isSolanaChain(w.chain)) {
190
+ addressChainMap.set(w.address, w.chain);
191
+ }
192
+ });
193
+ tempWallets.forEach(w => {
194
+ allAddresses.push(w.address);
195
+ if (isSolanaChain(w.chain)) {
196
+ addressChainMap.set(w.address, w.chain);
197
+ }
198
+ });
199
+
200
+ // Try cached balances first, fall back to RPC
201
+ const cachedBalances = await prisma.nativeBalance.findMany({
202
+ where: { walletAddress: { in: allAddresses.map(a => normalizeAddress(a, addressChainMap.get(a) || 'base')) } },
203
+ });
204
+ const cachedMap = new Map<string, { balance: string; updatedAt: Date }>();
205
+ for (const cb of cachedBalances) {
206
+ cachedMap.set(`${cb.walletAddress}:${cb.chain}`, { balance: cb.balance, updatedAt: cb.updatedAt });
207
+ }
208
+
209
+ // Check which addresses have no cache — fetch those from RPC
210
+ const uncachedAddresses = allAddresses.filter(a => {
211
+ const chain = addressChainMap.get(a) || 'base';
212
+ return !cachedMap.has(`${normalizeAddress(a, chain)}:${chain}`);
213
+ });
214
+
215
+ let rpcBalances = new Map<string, string>();
216
+ if (uncachedAddresses.length > 0) {
217
+ rpcBalances = await fetchBalances(uncachedAddresses, 'base', addressChainMap);
218
+ }
219
+
220
+ // Helper to get balance (cached first, then RPC)
221
+ const getBalance = (address: string, chain: string): string => {
222
+ const norm = normalizeAddress(address, chain);
223
+ const cached = cachedMap.get(`${norm}:${chain}`);
224
+ if (cached) return cached.balance;
225
+ return rpcBalances.get(norm) || '0';
226
+ };
227
+
228
+ const getBalanceUpdatedAt = (address: string, chain: string): string | undefined => {
229
+ const norm = normalizeAddress(address, chain);
230
+ const cached = cachedMap.get(`${norm}:${chain}`);
231
+ return cached?.updatedAt?.toISOString();
232
+ };
233
+
234
+ // Add balances to wallets
235
+ let wallets = [
236
+ ...(coldInfo ? [{
237
+ ...coldInfo,
238
+ chain: 'base',
239
+ balance: getBalance(coldInfo.address, 'base'),
240
+ balanceUpdatedAt: getBalanceUpdatedAt(coldInfo.address, 'base'),
241
+ }] : []),
242
+ ...(solColdAddr ? [{
243
+ address: solColdAddr,
244
+ tier: 'cold' as const,
245
+ chain: 'solana',
246
+ balance: getBalance(solColdAddr, 'solana'),
247
+ balanceUpdatedAt: getBalanceUpdatedAt(solColdAddr, 'solana'),
248
+ createdAt: coldInfo?.createdAt,
249
+ }] : []),
250
+ ...hotWallets.map(w => ({
251
+ ...w,
252
+ balance: getBalance(w.address, w.chain),
253
+ balanceUpdatedAt: getBalanceUpdatedAt(w.address, w.chain),
254
+ })),
255
+ ...tempWallets.map(w => ({
256
+ ...w,
257
+ balance: getBalance(w.address, w.chain),
258
+ balanceUpdatedAt: getBalanceUpdatedAt(w.address, w.chain),
259
+ })),
260
+ ];
261
+
262
+ // Apply tier filter
263
+ if (tierFilter) {
264
+ wallets = wallets.filter(w => w.tier === tierFilter);
265
+ }
266
+
267
+ // Apply chain filter
268
+ if (chainFilter) {
269
+ wallets = wallets.filter(w => w.chain === chainFilter);
270
+ }
271
+
272
+ // Apply sorting
273
+ if (sortBy) {
274
+ wallets.sort((a, b) => {
275
+ let cmp = 0;
276
+ if (sortBy === 'balance') {
277
+ cmp = parseFloat(a.balance || '0') - parseFloat(b.balance || '0');
278
+ } else if (sortBy === 'createdAt') {
279
+ const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
280
+ const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
281
+ cmp = aTime - bTime;
282
+ } else if (sortBy === 'name') {
283
+ const aName = ('name' in a ? (a as { name?: string }).name : '') || '';
284
+ const bName = ('name' in b ? (b as { name?: string }).name : '') || '';
285
+ cmp = aName.localeCompare(bName);
286
+ }
287
+ return sortDir === 'asc' ? cmp : -cmp;
288
+ });
289
+ }
290
+
291
+ // Include vaults list for non-agent access
292
+ const vaults = isAgent ? [] : listVaults();
293
+
294
+ // Build response
295
+ const response: {
296
+ wallets: typeof wallets;
297
+ unlocked: boolean;
298
+ vaults: typeof vaults;
299
+ agent?: { id: string; remaining: number };
300
+ } = {
301
+ wallets,
302
+ unlocked: isUnlocked(),
303
+ vaults,
304
+ };
305
+
306
+ // Include agent info if authenticated as agent
307
+ if (isAgent && auth) {
308
+ const { getRemaining } = await import('../lib/sessions');
309
+ response.agent = {
310
+ id: auth.token.agentId,
311
+ remaining: getRemaining(auth.tokenHash, auth.token)
312
+ };
313
+ }
314
+
315
+ res.json(response);
316
+ } catch (error) {
317
+ const message = getErrorMessage(error);
318
+ res.status(400).json({ error: message });
319
+ }
320
+ });
321
+
322
+ // POST /wallet/create - Create hot/temp wallet (requires auth + permission)
323
+ router.post('/create', requireWalletAuth, async (req: Request, res: Response) => {
324
+ try {
325
+ const { tier, chain, name, color, description, emoji, hidden, vaultId } = req.body;
326
+ const auth = req.auth!;
327
+
328
+ if (!tier || !['hot', 'temp'].includes(tier)) {
329
+ res.status(400).json({ error: 'tier must be "hot" or "temp"' });
330
+ return;
331
+ }
332
+
333
+ if (tier === 'hot') {
334
+ // Check permission
335
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['wallet:create:hot'])) {
336
+ res.status(403).json({ error: 'Token does not have wallet:create:hot permission' });
337
+ return;
338
+ }
339
+
340
+ // Hot wallet creation requires unlocked cold wallet
341
+ if (!isUnlocked()) {
342
+ res.status(401).json({ error: 'Cold wallet must be unlocked to create hot wallets' });
343
+ return;
344
+ }
345
+
346
+ // If vaultId specified, verify that vault is unlocked
347
+ if (vaultId && !isVaultUnlocked(vaultId)) {
348
+ res.status(401).json({ error: `Vault ${vaultId} must be unlocked to create hot wallets` });
349
+ return;
350
+ }
351
+
352
+ const wallet = await createHotWallet({
353
+ tokenHash: auth.tokenHash,
354
+ chain,
355
+ name,
356
+ color,
357
+ description,
358
+ emoji,
359
+ hidden,
360
+ coldWalletId: vaultId || undefined,
361
+ });
362
+
363
+ // Emit WebSocket events
364
+ events.walletCreated({
365
+ address: wallet.address,
366
+ tier: 'hot',
367
+ chain: wallet.chain || 'base',
368
+ name: wallet.name,
369
+ tokenHash: auth.tokenHash,
370
+ });
371
+ events.walletChanged({ address: wallet.address, reason: 'created' });
372
+ logger.walletCreated(wallet.address, 'hot', isAdmin(auth) ? undefined : auth.token.agentId);
373
+
374
+ res.json({ success: true, wallet });
375
+ } else {
376
+ // Temp wallet
377
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['wallet:create:temp'])) {
378
+ res.status(403).json({ error: 'Token does not have wallet:create:temp permission' });
379
+ return;
380
+ }
381
+
382
+ const wallet = createTempWallet(chain);
383
+
384
+ // Emit WebSocket events
385
+ events.walletCreated({
386
+ address: wallet.address,
387
+ tier: 'temp',
388
+ chain: wallet.chain || 'base',
389
+ });
390
+ events.walletChanged({ address: wallet.address, reason: 'created' });
391
+ logger.walletCreated(wallet.address, 'temp', isAdmin(auth) ? undefined : auth.token.agentId);
392
+
393
+ res.json({ success: true, wallet });
394
+ }
395
+ } catch (error) {
396
+ const message = getErrorMessage(error);
397
+ res.status(400).json({ error: message });
398
+ }
399
+ });
400
+
401
+ // POST /wallet/rename - Update hot wallet metadata (requires auth)
402
+ router.post('/rename', requireWalletAuth, async (req: Request, res: Response) => {
403
+ try {
404
+ const { address, name, color, description, emoji, hidden } = req.body;
405
+ const auth = req.auth!;
406
+
407
+ if (!address || typeof address !== 'string') {
408
+ res.status(400).json({ error: 'address is required' });
409
+ return;
410
+ }
411
+
412
+ // Check permission (admin bypasses)
413
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['wallet:rename'])) {
414
+ // Fall back to checking wallet access for legacy compatibility
415
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, address);
416
+ if (!canAccess) {
417
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
418
+ return;
419
+ }
420
+ } else if (!isAdmin(auth)) {
421
+ // Has permission, but still need to verify wallet access
422
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, address);
423
+ if (!canAccess) {
424
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
425
+ return;
426
+ }
427
+ }
428
+
429
+ const updates: { name?: string; color?: string; description?: string; emoji?: string; hidden?: boolean } = {};
430
+ if (name !== undefined) updates.name = name || undefined;
431
+ if (color !== undefined) updates.color = color || undefined;
432
+ if (description !== undefined) updates.description = description || undefined;
433
+ if (emoji !== undefined) updates.emoji = emoji || undefined;
434
+ if (hidden !== undefined) updates.hidden = hidden;
435
+
436
+ const success = await updateHotWallet(address, updates);
437
+
438
+ if (!success) {
439
+ res.status(404).json({ error: 'Wallet not found' });
440
+ return;
441
+ }
442
+
443
+ events.walletChanged({ address, reason: 'updated' });
444
+ logger.walletRenamed(address, isAdmin(auth) ? undefined : auth.token.agentId);
445
+
446
+ res.json({ success: true });
447
+ } catch (error) {
448
+ const message = getErrorMessage(error);
449
+ res.status(400).json({ error: message });
450
+ }
451
+ });
452
+
453
+ // POST /wallet/:address/export - Export hot wallet private key
454
+ // Human (no token): allowed when unlocked
455
+ // Agent (token): requires wallet:export permission + wallet ownership
456
+ router.post('/:address/export', optionalWalletAuth, async (req: Request<{ address: string }>, res: Response) => {
457
+ try {
458
+ const { address } = req.params;
459
+ const auth = req.auth;
460
+
461
+ // Cold wallet must be unlocked
462
+ if (!isUnlocked()) {
463
+ res.status(401).json({ error: 'Wallet must be unlocked to export keys' });
464
+ return;
465
+ }
466
+
467
+ // If agent token provided, check permissions and ownership
468
+ if (auth && !isAdmin(auth)) {
469
+ // Check permission
470
+ if (!hasAnyPermission(auth.token.permissions, ['wallet:export'])) {
471
+ res.status(403).json({ error: 'Token does not have wallet:export permission' });
472
+ return;
473
+ }
474
+
475
+ // Verify wallet access
476
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, address);
477
+ if (!canAccess) {
478
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
479
+ return;
480
+ }
481
+ }
482
+ // No token = human access, allowed when unlocked (checked above)
483
+
484
+ const result = await exportHotWallet(address);
485
+ logger.walletExported(address, auth?.token?.agentId);
486
+
487
+ res.json({
488
+ success: true,
489
+ address: result.address,
490
+ privateKey: result.privateKey
491
+ });
492
+ } catch (error) {
493
+ const message = getErrorMessage(error);
494
+ res.status(400).json({ error: message });
495
+ }
496
+ });
497
+
498
+ // GET/POST /export-seed - Export mnemonic (requires admin)
499
+ // Supports optional ?vaultId query param to export a specific vault's seed
500
+ const handleExportSeed = (req: Request, res: Response) => {
501
+ try {
502
+ // Must have admin auth
503
+ if (!req.auth || !isAdmin(req.auth)) {
504
+ res.status(403).json({ success: false, error: 'Admin access required' });
505
+ return;
506
+ }
507
+
508
+ if (!isUnlocked()) {
509
+ res.status(401).json({ success: false, error: 'Wallet must be unlocked to export seed' });
510
+ return;
511
+ }
512
+
513
+ const vaultId = (req.query.vaultId || req.body?.vaultId) as string | undefined;
514
+
515
+ let mnemonic: string | null;
516
+ let address: string | null;
517
+
518
+ if (vaultId) {
519
+ mnemonic = exportVaultSeed(vaultId);
520
+ address = getVaultAddress(vaultId);
521
+ } else {
522
+ mnemonic = exportSeed();
523
+ address = getColdWalletAddress();
524
+ }
525
+
526
+ if (!mnemonic) {
527
+ res.status(400).json({ success: false, error: 'No mnemonic available. Is the vault unlocked?' });
528
+ return;
529
+ }
530
+
531
+ logger.seedExported(vaultId);
532
+
533
+ res.json({
534
+ success: true,
535
+ mnemonic,
536
+ address,
537
+ vaultId: vaultId || undefined,
538
+ warning: 'NEVER share this mnemonic. Anyone with it can access all your wallets.'
539
+ });
540
+ } catch (error) {
541
+ const message = getErrorMessage(error);
542
+ res.status(400).json({ success: false, error: message });
543
+ }
544
+ };
545
+
546
+ router.get('/export-seed', requireWalletAuth, requireAdmin, handleExportSeed);
547
+ router.post('/export-seed', requireWalletAuth, requireAdmin, handleExportSeed);
548
+
549
+ // GET /wallets/search - Search wallets by name/address (always includes hidden)
550
+ router.get('/search', optionalWalletAuth, async (req: Request, res: Response) => {
551
+ try {
552
+ const query = req.query.q as string;
553
+ if (!query || query.trim().length === 0) {
554
+ res.status(400).json({ error: 'Search query (q) is required' });
555
+ return;
556
+ }
557
+
558
+ const auth = req.auth;
559
+ // If agent, only search their wallets; if admin/no auth, search all
560
+ const tokenHash = auth && !isAdmin(auth) ? auth.tokenHash : undefined;
561
+ const hotWallets = await searchHotWallets(query, tokenHash);
562
+
563
+ // Batch fetch balances with chain-aware lookup
564
+ const addresses = hotWallets.map(w => w.address);
565
+ const searchChainMap = new Map<string, string>();
566
+ hotWallets.forEach(w => {
567
+ if (isSolanaChain(w.chain)) {
568
+ searchChainMap.set(w.address, w.chain);
569
+ }
570
+ });
571
+ const balances = await fetchBalances(addresses, 'base', searchChainMap);
572
+
573
+ // Add balances to wallets
574
+ const walletsWithBalances = hotWallets.map(w => ({
575
+ ...w,
576
+ balance: balances.get(normalizeAddress(w.address, w.chain)) || '0'
577
+ }));
578
+
579
+ // Also search address labels
580
+ const addressLabels = await prisma.addressLabel.findMany({
581
+ where: {
582
+ OR: [
583
+ { label: { contains: query } },
584
+ { address: { contains: query.toLowerCase() } },
585
+ ],
586
+ },
587
+ take: 10,
588
+ });
589
+
590
+ res.json({ wallets: walletsWithBalances, addressLabels });
591
+ } catch (error) {
592
+ const message = getErrorMessage(error);
593
+ res.status(400).json({ error: message });
594
+ }
595
+ });
596
+
597
+ // Mount sub-routers for transactions and assets
598
+ // IMPORTANT: These must come after named routes (like /transactions, /search, /export-seed)
599
+ // but before the /:address catch-all route, since Express matches in order.
600
+ router.use('/', transactionRoutes);
601
+ router.use('/', assetRoutes);
602
+
603
+ // GET /wallet/:address - Get single wallet details (requires auth for access check)
604
+ // NOTE: This must be LAST because /:address is a catch-all parameter route
605
+ router.get('/:address', optionalWalletAuth, async (req: Request<{ address: string }>, res: Response) => {
606
+ try {
607
+ const { address } = req.params;
608
+ const auth = req.auth;
609
+ const agentCanListAll = !!(
610
+ auth &&
611
+ !isAdmin(auth) &&
612
+ hasAnyPermission(auth.token.permissions, ['wallet:list'])
613
+ );
614
+
615
+ // 1) Hot wallet
616
+ const wallet = await getHotWallet(address);
617
+ if (wallet) {
618
+ // If agent (not admin), verify access unless it has wallet:list
619
+ if (auth && !isAdmin(auth) && !agentCanListAll) {
620
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, address);
621
+ if (!canAccess) {
622
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
623
+ return;
624
+ }
625
+ }
626
+
627
+ const chain = wallet.metadata.chain || 'base';
628
+ const balance = await fetchBalance(wallet.address, chain);
629
+
630
+ res.json({
631
+ address: wallet.address,
632
+ tier: 'hot',
633
+ chain,
634
+ name: wallet.metadata.name,
635
+ color: wallet.metadata.color,
636
+ description: wallet.metadata.description,
637
+ emoji: wallet.metadata.emoji,
638
+ hidden: wallet.metadata.hidden,
639
+ createdAt: wallet.metadata.createdAt,
640
+ tokenHash: wallet.tokenHash,
641
+ balance,
642
+ });
643
+ return;
644
+ }
645
+
646
+ // 2) Cold wallet (EVM or Solana)
647
+ const coldInfo = getColdWalletInfo();
648
+ const coldEvmAddress = getColdWalletAddress();
649
+ const coldSolAddress = getSolanaColdAddress();
650
+
651
+ const isColdEvm = coldEvmAddress
652
+ ? normalizeAddress(coldEvmAddress, 'base') === normalizeAddress(address, 'base')
653
+ : false;
654
+ const isColdSol = coldSolAddress ? coldSolAddress === address : false;
655
+
656
+ if (isColdEvm || isColdSol) {
657
+ if (auth && !isAdmin(auth) && !agentCanListAll) {
658
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
659
+ return;
660
+ }
661
+
662
+ const chain = isColdSol ? 'solana' : 'base';
663
+ const resolvedAddress = isColdSol
664
+ ? coldSolAddress!
665
+ : (coldEvmAddress || coldInfo?.address || address);
666
+ const balance = await fetchBalance(resolvedAddress, chain);
667
+
668
+ res.json({
669
+ address: resolvedAddress,
670
+ tier: 'cold',
671
+ chain,
672
+ name: 'Cold Wallet',
673
+ createdAt: coldInfo?.createdAt,
674
+ balance,
675
+ });
676
+ return;
677
+ }
678
+
679
+ // 3) Temp wallet
680
+ const tempWallet = listTempWallets().find((w) =>
681
+ normalizeAddress(w.address, w.chain) === normalizeAddress(address, w.chain)
682
+ );
683
+
684
+ if (tempWallet) {
685
+ if (auth && !isAdmin(auth) && !agentCanListAll) {
686
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
687
+ return;
688
+ }
689
+
690
+ const chain = tempWallet.chain === 'any' ? 'base' : tempWallet.chain;
691
+ const balance = await fetchBalance(tempWallet.address, chain);
692
+
693
+ res.json({
694
+ address: tempWallet.address,
695
+ tier: 'temp',
696
+ chain,
697
+ createdAt: tempWallet.createdAt,
698
+ balance,
699
+ });
700
+ return;
701
+ }
702
+
703
+ res.status(404).json({ error: 'Wallet not found' });
704
+ } catch (error) {
705
+ const message = getErrorMessage(error);
706
+ res.status(400).json({ error: message });
707
+ }
708
+ });
709
+
710
+ export default router;