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,281 @@
1
+ import { Request, Response } from 'express';
2
+ import { PublicKey, VersionedTransaction } from '@solana/web3.js';
3
+ import { getMint } from '@solana/spl-token';
4
+ import { getHotWallet, tokenCanAccessWallet } from '../lib/hot';
5
+ import { getTempSolanaKeypair, hasTempWallet } from '../lib/temp';
6
+ import { isUnlocked, getSolanaColdAddress, listVaults } from '../lib/cold';
7
+ import { reserveSpend, releaseSpend } from '../lib/sessions';
8
+ import { logger } from '../lib/logger';
9
+ import { hasAnyPermission, isAdmin } from '../lib/permissions';
10
+ import { getNativeAddress, getNativeCurrency } from '../lib/address';
11
+ import { getSolanaConnection } from '../lib/solana/connection';
12
+ import { getSolanaKeypair } from '../lib/solana/wallet';
13
+ import { buildSolTransfer, buildSplTransfer, sendSolanaTransaction } from '../lib/solana/transfer';
14
+ import { getErrorMessage, HttpError } from '../lib/error';
15
+ import { recordTransaction, autoTrackToken } from '../lib/transactions';
16
+ import type { AuthInfo } from '../middleware/auth';
17
+ import type { ChainConfig } from '../lib/config';
18
+
19
+ export async function handleSolanaSend(
20
+ req: Request,
21
+ res: Response,
22
+ auth: AuthInfo,
23
+ targetChain: string,
24
+ chainConfig: ChainConfig
25
+ ): Promise<void> {
26
+ try {
27
+ const {
28
+ from,
29
+ amount,
30
+ value,
31
+ data,
32
+ chain,
33
+ tokenAddress,
34
+ transaction: rawTransaction,
35
+ description: userDescription
36
+ } = req.body;
37
+
38
+ // 'to' may have been resolved by the dispatcher (ENS resolution)
39
+ const to = req.body.to;
40
+ const rawValue = amount || value;
41
+
42
+ // Parse value for limit checks
43
+ let valueWei = BigInt(0);
44
+ if (rawValue) {
45
+ valueWei = BigInt(rawValue);
46
+ }
47
+
48
+ // 'to' is required for simple sends, but not for raw VersionedTransaction
49
+ if (!rawTransaction && !to) {
50
+ res.status(400).json({ error: 'to address is required for Solana sends' });
51
+ return;
52
+ }
53
+
54
+ const currency = getNativeAddress(targetChain);
55
+ const nativeCurrency = getNativeCurrency(targetChain);
56
+
57
+ // Determine wallet type
58
+ const hotWallet = await getHotWallet(from);
59
+ const isTempWallet = hasTempWallet(from);
60
+
61
+ if (!hotWallet && !isTempWallet) {
62
+ res.status(404).json({ error: 'Wallet not found' });
63
+ return;
64
+ }
65
+
66
+ // Permission checks
67
+ if (hotWallet) {
68
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['send:hot'])) {
69
+ logger.permissionDenied('send:hot', auth.token.agentId, '/send');
70
+ res.status(403).json({ error: 'Token does not have send:hot permission' });
71
+ return;
72
+ }
73
+ const canAccess = await tokenCanAccessWallet(auth.tokenHash, auth.token.walletAccess, from, targetChain);
74
+ if (!isAdmin(auth) && !canAccess) {
75
+ logger.permissionDenied('wallet_access', auth.token.agentId, '/send');
76
+ res.status(403).json({ error: 'Token does not have access to this wallet' });
77
+ return;
78
+ }
79
+ } else if (isTempWallet) {
80
+ if (!isAdmin(auth) && !hasAnyPermission(auth.token.permissions, ['send:temp'])) {
81
+ logger.permissionDenied('send:temp', auth.token.agentId, '/send');
82
+ res.status(403).json({ error: 'Token does not have send:temp permission' });
83
+ return;
84
+ }
85
+ }
86
+
87
+ if (!isUnlocked()) {
88
+ logger.authFailed('Cold wallet locked', '/send');
89
+ res.status(401).json({ error: 'Cold wallet must be unlocked to send from hot wallet' });
90
+ return;
91
+ }
92
+
93
+ // Get signer keypair (shared by both raw tx and simple transfer paths)
94
+ let signerKeypair;
95
+ if (hotWallet) {
96
+ signerKeypair = await getSolanaKeypair(from);
97
+ } else {
98
+ signerKeypair = getTempSolanaKeypair(from);
99
+ if (!signerKeypair) {
100
+ res.status(404).json({ error: 'Temp Solana wallet not found' });
101
+ return;
102
+ }
103
+ }
104
+
105
+ // --- Raw VersionedTransaction path ---
106
+ if (rawTransaction) {
107
+ if (typeof rawTransaction !== 'string') {
108
+ res.status(400).json({ error: 'transaction must be a base64-encoded string' });
109
+ return;
110
+ }
111
+
112
+ let tx: VersionedTransaction;
113
+ try {
114
+ tx = VersionedTransaction.deserialize(Buffer.from(rawTransaction, 'base64'));
115
+ } catch (err) {
116
+ res.status(400).json({ error: 'Invalid transaction: failed to deserialize VersionedTransaction' });
117
+ return;
118
+ }
119
+
120
+ const connection = await getSolanaConnection(targetChain);
121
+
122
+ // Sign and submit
123
+ tx.sign([signerKeypair]);
124
+ const signature = await connection.sendRawTransaction(tx.serialize(), {
125
+ skipPreflight: false,
126
+ maxRetries: 2,
127
+ });
128
+ await connection.confirmTransaction(signature, 'confirmed');
129
+
130
+ // Log
131
+ const description = userDescription || `Program transaction from ${from}`;
132
+ await recordTransaction({
133
+ walletAddress: from,
134
+ txHash: signature,
135
+ type: 'contract',
136
+ amount: '0',
137
+ from,
138
+ to: to || undefined,
139
+ description,
140
+ chain: targetChain,
141
+ logTitle: 'Program Transaction',
142
+ });
143
+
144
+ res.json({
145
+ success: true,
146
+ hash: signature,
147
+ from,
148
+ chain: targetChain,
149
+ type: 'program'
150
+ });
151
+ return;
152
+ }
153
+
154
+ // --- SPL Token transfer path ---
155
+ if (tokenAddress) {
156
+ const connection = await getSolanaConnection(targetChain);
157
+ const fromPubkey = new PublicKey(from);
158
+ const toPubkey = new PublicKey(to!);
159
+ const mint = new PublicKey(tokenAddress);
160
+
161
+ // Resolve decimals from on-chain mint account
162
+ const mintInfo = await getMint(connection, mint);
163
+ const decimals = mintInfo.decimals;
164
+
165
+ // Token sends skip native spending limits (spending tokens, not SOL)
166
+ let txHash: string;
167
+ try {
168
+ const tx = await buildSplTransfer(connection, fromPubkey, toPubkey, mint, BigInt(rawValue || '0'), decimals);
169
+ txHash = await sendSolanaTransaction(connection, tx, signerKeypair);
170
+ } catch (err) {
171
+ throw err;
172
+ }
173
+
174
+ // Log
175
+ const tokenAmountStr = rawValue || '0';
176
+ const description = userDescription || `Sent ${tokenAmountStr} tokens of ${tokenAddress} to ${to}`;
177
+ await recordTransaction({
178
+ walletAddress: from,
179
+ txHash,
180
+ type: 'send',
181
+ tokenAddress,
182
+ tokenAmount: tokenAmountStr,
183
+ from,
184
+ to: to!,
185
+ description,
186
+ chain: targetChain,
187
+ logTitle: 'Token Send',
188
+ });
189
+
190
+ await autoTrackToken({
191
+ walletAddress: from,
192
+ tokenAddress,
193
+ chain: targetChain,
194
+ });
195
+
196
+ const agentId = !isAdmin(auth) ? auth.token.agentId : undefined;
197
+ logger.send(from, to!, `${tokenAmountStr} tokens`, txHash, agentId);
198
+
199
+ res.json({
200
+ success: true,
201
+ hash: txHash,
202
+ from,
203
+ to,
204
+ tokenAddress,
205
+ tokenAmount: tokenAmountStr,
206
+ chain: targetChain
207
+ });
208
+ return;
209
+ }
210
+
211
+ // --- Simple SOL transfer path ---
212
+
213
+ // Check if send is going to any vault's Solana address (vault bypass)
214
+ const solColdAddress = getSolanaColdAddress();
215
+ let isSendToVault = solColdAddress && to === solColdAddress;
216
+ if (!isSendToVault) {
217
+ const vaults = listVaults();
218
+ isSendToVault = vaults.some(v => v.solanaAddress === to);
219
+ }
220
+
221
+ // For Solana, convert lamports to SOL for limit checks (9 decimals, not 18)
222
+ const valueSol = Number(valueWei) / 1e9;
223
+
224
+ // Reserve spending atomically (prevents TOCTOU race between concurrent requests)
225
+ const needsLimit = !isAdmin(auth) && !isSendToVault && valueSol > 0;
226
+ if (needsLimit) {
227
+ const reserve = reserveSpend(auth.tokenHash, auth.token, 'send', valueSol, currency);
228
+ if (!reserve.ok) {
229
+ logger.limitExceeded(auth.token.agentId, 'send', valueSol, reserve.remaining);
230
+ res.status(403).json({ error: 'Amount exceeds remaining send limit', remaining: reserve.remaining, requested: valueSol });
231
+ return;
232
+ }
233
+ }
234
+
235
+ let txHash: string;
236
+ try {
237
+ const connection = await getSolanaConnection(targetChain);
238
+ const fromPubkey = new PublicKey(from);
239
+ const toPubkey = new PublicKey(to!);
240
+
241
+ // Build SOL transfer -- pass lamports directly
242
+ const tx = await buildSolTransfer(connection, fromPubkey, toPubkey, Number(valueWei));
243
+
244
+ txHash = await sendSolanaTransaction(connection, tx, signerKeypair);
245
+ } catch (err) {
246
+ if (needsLimit) releaseSpend(auth.tokenHash, 'send', valueSol, currency);
247
+ throw err;
248
+ }
249
+
250
+ // Log
251
+ const description = userDescription || `Sent ${valueSol} ${nativeCurrency} to ${to}`;
252
+ await recordTransaction({
253
+ walletAddress: from,
254
+ txHash,
255
+ type: 'send',
256
+ amount: valueSol.toString(),
257
+ from,
258
+ to: to!,
259
+ description,
260
+ chain: targetChain,
261
+ logTitle: 'Send Transaction',
262
+ });
263
+
264
+ if (!data) {
265
+ const agentId = !isAdmin(auth) ? auth.token.agentId : undefined;
266
+ logger.send(from, to!, valueSol.toString(), txHash, agentId);
267
+ }
268
+
269
+ res.json({
270
+ success: true,
271
+ hash: txHash,
272
+ from,
273
+ to,
274
+ amount: valueSol.toString(),
275
+ chain: targetChain
276
+ });
277
+ } catch (error) {
278
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
279
+ res.status(400).json({ error: getErrorMessage(error) });
280
+ }
281
+ }
@@ -0,0 +1,178 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { ethers } from 'ethers';
3
+ import { resolveChain, getRpcUrl } from '../lib/config';
4
+ import { requireWalletAuth } from '../middleware/auth';
5
+ import { isSolanaChain } from '../lib/address';
6
+ import { resolveName, looksLikeName } from '../lib/resolve';
7
+ import { getErrorMessage, HttpError } from '../lib/error';
8
+ import { handleSolanaSend } from './send-solana';
9
+ import { handleEvmSend } from './send-evm';
10
+
11
+ const router = Router();
12
+
13
+ // POST /send - Generic transaction endpoint
14
+ // Supports simple ETH sends and complex contract calls
15
+ //
16
+ // Simple send: { from, to, amount: "100000000000000000", chain } // amount in wei or lamports
17
+ // Contract call: { from, to, value, data, gasLimit, ... }
18
+ //
19
+ // Requires Bearer token authentication
20
+ //
21
+ // Spending limit enforced for agent tokens (send limit).
22
+ // Sends to the cold wallet address bypass the limit (returning funds to vault).
23
+ // Admin tokens bypass all limits.
24
+ router.post('/', requireWalletAuth, async (req: Request, res: Response) => {
25
+ try {
26
+ const {
27
+ from,
28
+ to: rawTo,
29
+ amount, // Amount in wei (EVM) or lamports (Solana)
30
+ value, // Advanced mode: wei (alias for amount)
31
+ data,
32
+ tokenAddress, // Optional: ERC-20/SPL token contract address
33
+ transaction: rawTransaction, // Solana: base64-encoded VersionedTransaction
34
+ } = req.body;
35
+
36
+ const auth = req.auth!;
37
+
38
+ if (!from || typeof from !== 'string') {
39
+ res.status(400).json({ error: 'from address is required' });
40
+ return;
41
+ }
42
+
43
+ // Token sends require 'to'
44
+ if (tokenAddress && !rawTo) {
45
+ res.status(400).json({ error: 'to address is required for token sends' });
46
+ return;
47
+ }
48
+
49
+ // Resolve ENS names (e.g. "vitalik.eth" -> "0x...")
50
+ let to = rawTo;
51
+ if (to && typeof to === 'string' && looksLikeName(to)) {
52
+ try {
53
+ const resolved = await resolveName(to);
54
+ to = resolved.address;
55
+ } catch (err) {
56
+ const msg = getErrorMessage(err);
57
+ res.status(400).json({ error: msg });
58
+ return;
59
+ }
60
+ }
61
+
62
+ // 'to' is optional for contract deployment
63
+ if (to && typeof to !== 'string') {
64
+ res.status(400).json({ error: 'to must be a valid address' });
65
+ return;
66
+ }
67
+
68
+ // For simple sends, 'to' is required (raw Solana transactions don't need 'to')
69
+ if (!data && !rawTransaction && !to) {
70
+ res.status(400).json({ error: 'to address is required for simple sends' });
71
+ return;
72
+ }
73
+
74
+ // Parse value - amount must be in wei (EVM) or lamports (Solana).
75
+ // Callers must convert before calling.
76
+ let valueWei = BigInt(0);
77
+ let valueEth = 0;
78
+ const rawValue = amount || value;
79
+
80
+ if (rawValue) {
81
+ if (typeof rawValue === 'string') {
82
+ valueWei = BigInt(rawValue);
83
+ } else if (typeof rawValue === 'number') {
84
+ valueWei = BigInt(rawValue);
85
+ }
86
+ valueEth = parseFloat(ethers.formatEther(valueWei)); // for limits only
87
+ }
88
+
89
+ // For simple native sends without data, require some value (raw Solana transactions carry value in instructions)
90
+ // Token sends use amount as token units, not native currency, so skip this check
91
+ if (!data && !rawTransaction && !tokenAddress && valueEth <= 0) {
92
+ res.status(400).json({ error: 'amount is required for simple sends' });
93
+ return;
94
+ }
95
+
96
+ // For token sends, require amount
97
+ if (tokenAddress && (!rawValue || BigInt(rawValue) <= 0n)) {
98
+ res.status(400).json({ error: 'amount is required for token sends' });
99
+ return;
100
+ }
101
+
102
+ // Validate data if provided
103
+ if (data && typeof data === 'string' && !data.startsWith('0x')) {
104
+ res.status(400).json({ error: 'data must be hex-encoded (start with 0x)' });
105
+ return;
106
+ }
107
+
108
+ const { targetChain, chainConfig } = resolveChain(req.body.chain);
109
+
110
+ // Mutate req.body.to so handlers see the resolved address
111
+ req.body.to = to;
112
+
113
+ // Dispatch to chain-specific handler
114
+ if (isSolanaChain(targetChain)) {
115
+ return handleSolanaSend(req, res, auth, targetChain, chainConfig);
116
+ }
117
+ return handleEvmSend(req, res, auth, targetChain, chainConfig);
118
+ } catch (error) {
119
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
120
+ res.status(400).json({ error: getErrorMessage(error) });
121
+ }
122
+ });
123
+
124
+ // POST /send/estimate - Estimate gas for a transaction
125
+ router.post('/estimate', async (req: Request, res: Response) => {
126
+ try {
127
+ const { from, to, amount, value, data, chain } = req.body;
128
+
129
+ if (!from || typeof from !== 'string') {
130
+ res.status(400).json({ error: 'from address is required' });
131
+ return;
132
+ }
133
+
134
+ const { targetChain } = resolveChain(chain);
135
+
136
+ const provider = new ethers.JsonRpcProvider(await getRpcUrl(targetChain));
137
+
138
+ // Build transaction for estimation
139
+ const tx: ethers.TransactionRequest = { from };
140
+ if (to) tx.to = to;
141
+
142
+ const rawValue = amount || value;
143
+ if (rawValue) {
144
+ tx.value = BigInt(rawValue);
145
+ }
146
+ if (data) tx.data = data;
147
+
148
+ // Estimate gas
149
+ const gasEstimate = await provider.estimateGas(tx);
150
+ const feeData = await provider.getFeeData();
151
+
152
+ const response: Record<string, unknown> = {
153
+ success: true,
154
+ gasLimit: gasEstimate.toString(),
155
+ gasPrice: feeData.gasPrice?.toString() || null,
156
+ maxFeePerGas: feeData.maxFeePerGas?.toString() || null,
157
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.toString() || null
158
+ };
159
+
160
+ // Calculate estimated cost
161
+ if (feeData.maxFeePerGas) {
162
+ const estimatedCost = gasEstimate * feeData.maxFeePerGas;
163
+ response.estimatedCostWei = estimatedCost.toString();
164
+ response.estimatedCostEth = ethers.formatEther(estimatedCost);
165
+ } else if (feeData.gasPrice) {
166
+ const estimatedCost = gasEstimate * feeData.gasPrice;
167
+ response.estimatedCostWei = estimatedCost.toString();
168
+ response.estimatedCostEth = ethers.formatEther(estimatedCost);
169
+ }
170
+
171
+ res.json(response);
172
+ } catch (error) {
173
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
174
+ res.status(400).json({ error: getErrorMessage(error) });
175
+ }
176
+ });
177
+
178
+ export default router;
@@ -0,0 +1,210 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import {
3
+ createColdWallet,
4
+ hasColdWallet,
5
+ isUnlocked,
6
+ getColdWalletAddress,
7
+ createVault,
8
+ importVault,
9
+ listVaults,
10
+ rotatePrimaryVaultPassword,
11
+ } from '../lib/cold';
12
+ import { createAdminToken } from '../lib/auth';
13
+ import { parseEncryptedPassword } from '../lib/transport';
14
+ import { prisma } from '../lib/db';
15
+ import { loadConfig } from '../lib/config';
16
+ import { logger } from '../lib/logger';
17
+ import { requireWalletAuth } from '../middleware/auth';
18
+ import { requireAdmin } from '../lib/permissions';
19
+ import { isValidAgentPubkey, normalizeAgentPubkey } from '../lib/credential-transport';
20
+ import { getErrorMessage, HttpError } from '../lib/error';
21
+
22
+ const router = Router();
23
+
24
+ // POST /setup - Create cold wallet with encrypted password
25
+ router.post('/', async (req: Request, res: Response) => {
26
+ try {
27
+ const pubkey = typeof req.body?.pubkey === 'string' ? req.body.pubkey : '';
28
+ if (!pubkey.trim()) {
29
+ res.status(400).json({ error: 'pubkey is required' });
30
+ return;
31
+ }
32
+ if (!isValidAgentPubkey(pubkey)) {
33
+ res.status(400).json({ error: 'pubkey must be a valid RSA public key (PEM or base64)' });
34
+ return;
35
+ }
36
+ const normalizedPubkey = normalizeAgentPubkey(pubkey);
37
+ const password = parseEncryptedPassword(req.body.encrypted);
38
+
39
+ const result = createColdWallet(password);
40
+
41
+ // Log the setup event
42
+ logger.setup(result.address);
43
+
44
+ // Create admin token (vault is auto-unlocked after creation)
45
+ const token = await createAdminToken(normalizedPubkey);
46
+
47
+ res.json({
48
+ success: true,
49
+ address: result.address,
50
+ mnemonic: result.mnemonic,
51
+ token,
52
+ message: 'Cold wallet created. SAVE YOUR MNEMONIC SECURELY. It will not be shown again.'
53
+ });
54
+ } catch (error) {
55
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
56
+ res.status(400).json({ error: getErrorMessage(error) });
57
+ }
58
+ });
59
+
60
+ // GET /setup - Check setup status (includes adapter/key status for agents)
61
+ router.get('/', async (_req: Request, res: Response) => {
62
+ try {
63
+ const hasWallet = hasColdWallet();
64
+ const unlocked = isUnlocked();
65
+ const address = getColdWalletAddress();
66
+ const config = loadConfig();
67
+
68
+ // Check which API keys are configured
69
+ const apiKeys = await prisma.apiKey.findMany({
70
+ where: { isActive: true },
71
+ select: { service: true },
72
+ });
73
+ const apiKeyServices = new Set(apiKeys.map((k) => k.service));
74
+
75
+ // Check adapter config
76
+ let telegramEnabled = false;
77
+ let webhookEnabled = false;
78
+ try {
79
+ const appConfig = await prisma.appConfig.findUnique({ where: { id: 'global' } });
80
+ if (appConfig?.adapterConfig) {
81
+ const parsed = JSON.parse(appConfig.adapterConfig);
82
+ const adapters: Array<{ type: string; enabled: boolean }> = parsed.adapters || [];
83
+ telegramEnabled = adapters.some((a) => a.type === 'telegram' && a.enabled);
84
+ webhookEnabled = adapters.some((a) => a.type === 'webhook' && a.enabled);
85
+ }
86
+ } catch {
87
+ // Ignore parse errors
88
+ }
89
+
90
+ res.json({
91
+ hasWallet,
92
+ unlocked,
93
+ address,
94
+ adapters: {
95
+ telegram: telegramEnabled,
96
+ webhook: webhookEnabled,
97
+ },
98
+ apiKeys: {
99
+ alchemy: apiKeyServices.has('alchemy'),
100
+ anthropic: apiKeyServices.has('anthropic'),
101
+ },
102
+ defaultChain: config.defaultChain,
103
+ });
104
+ } catch (error) {
105
+ res.status(500).json({ error: getErrorMessage(error) });
106
+ }
107
+ });
108
+
109
+ // POST /setup/vault - Create additional vault (requires admin + unlocked primary)
110
+ router.post('/vault', requireWalletAuth, requireAdmin, (req: Request, res: Response) => {
111
+ try {
112
+ const { name } = req.body;
113
+ const password = parseEncryptedPassword(req.body.encrypted);
114
+
115
+ if (!isUnlocked()) {
116
+ res.status(401).json({ error: 'Primary vault must be unlocked to create additional vaults' });
117
+ return;
118
+ }
119
+
120
+ const result = createVault(password, name);
121
+
122
+ logger.setup(result.address);
123
+
124
+ res.json({
125
+ success: true,
126
+ id: result.id,
127
+ address: result.address,
128
+ solanaAddress: result.solanaAddress,
129
+ mnemonic: result.mnemonic,
130
+ name: result.name,
131
+ message: 'Vault created. SAVE YOUR MNEMONIC SECURELY. It will not be shown again.'
132
+ });
133
+ } catch (error) {
134
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
135
+ res.status(400).json({ error: getErrorMessage(error) });
136
+ }
137
+ });
138
+
139
+ // POST /setup/vault/import - Import vault from seed (requires admin + unlocked primary)
140
+ router.post('/vault/import', requireWalletAuth, requireAdmin, (req: Request, res: Response) => {
141
+ try {
142
+ const { mnemonic, name } = req.body;
143
+
144
+ if (!mnemonic || typeof mnemonic !== 'string') {
145
+ res.status(400).json({ error: 'Seed phrase (mnemonic) is required' });
146
+ return;
147
+ }
148
+
149
+ const password = parseEncryptedPassword(req.body.encrypted);
150
+
151
+ if (!isUnlocked()) {
152
+ res.status(401).json({ error: 'Primary vault must be unlocked to import additional vaults' });
153
+ return;
154
+ }
155
+
156
+ const result = importVault(mnemonic, password, name);
157
+
158
+ logger.setup(result.address);
159
+
160
+ res.json({
161
+ success: true,
162
+ id: result.id,
163
+ address: result.address,
164
+ solanaAddress: result.solanaAddress,
165
+ name: result.name,
166
+ message: 'Vault imported successfully'
167
+ });
168
+ } catch (error) {
169
+ if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
170
+ res.status(400).json({ error: getErrorMessage(error) });
171
+ }
172
+ });
173
+
174
+ // GET /setup/vaults - List all vaults
175
+ router.get('/vaults', (_req: Request, res: Response) => {
176
+ const vaults = listVaults();
177
+ res.json({ vaults });
178
+ });
179
+
180
+ // POST /setup/password - Rotate primary vault password (admin)
181
+ router.post('/password', requireWalletAuth, requireAdmin, (req: Request, res: Response) => {
182
+ try {
183
+ const currentPassword = parseEncryptedPassword(req.body.currentEncrypted);
184
+ const newPassword = parseEncryptedPassword(req.body.newEncrypted);
185
+
186
+ if (newPassword.length < 8) {
187
+ res.status(400).json({ error: 'Password must be at least 8 characters' });
188
+ return;
189
+ }
190
+
191
+ const changed = rotatePrimaryVaultPassword(currentPassword, newPassword);
192
+ if (!changed) {
193
+ res.status(401).json({ error: 'Invalid current password' });
194
+ return;
195
+ }
196
+
197
+ res.json({
198
+ success: true,
199
+ message: 'Primary vault password updated',
200
+ });
201
+ } catch (error) {
202
+ if (error instanceof HttpError) {
203
+ res.status(error.status).json({ error: error.message });
204
+ return;
205
+ }
206
+ res.status(400).json({ error: getErrorMessage(error) });
207
+ }
208
+ });
209
+
210
+ export default router;