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,378 @@
1
+ /**
2
+ * Batch RPC helper for fetching token balances and prices
3
+ * Minimizes RPC calls by batching all requests together
4
+ */
5
+
6
+ import { ethers } from 'ethers';
7
+ import { Connection, PublicKey } from '@solana/web3.js';
8
+
9
+ export interface TokenData {
10
+ balance: string; // Formatted balance with decimals
11
+ balanceRaw: string; // Raw balance in smallest unit
12
+ priceInEth: number | null; // Price in ETH (null if no pool)
13
+ }
14
+
15
+ export interface AssetInfo {
16
+ tokenAddress: string;
17
+ decimals: number;
18
+ poolAddress?: string | null;
19
+ poolVersion?: string | null;
20
+ }
21
+
22
+ // ABI fragments for encoding/decoding
23
+ const BALANCE_OF_SELECTOR = '0x70a08231'; // balanceOf(address)
24
+ const GET_RESERVES_SELECTOR = '0x0902f1ac'; // getReserves() for V2
25
+ const SLOT0_SELECTOR = '0x3850c7bd'; // slot0() for V3
26
+ const TOKEN0_SELECTOR = '0x0dfe1681'; // token0() for determining price direction
27
+
28
+ /**
29
+ * Encode balanceOf(address) call
30
+ */
31
+ function encodeBalanceOf(walletAddress: string): string {
32
+ const addressParam = ethers.zeroPadValue(walletAddress, 32).slice(2);
33
+ return BALANCE_OF_SELECTOR + addressParam;
34
+ }
35
+
36
+ /**
37
+ * Encode getReserves() call for V2 pools
38
+ */
39
+ function encodeGetReserves(): string {
40
+ return GET_RESERVES_SELECTOR;
41
+ }
42
+
43
+ /**
44
+ * Encode slot0() call for V3 pools
45
+ */
46
+ function encodeSlot0(): string {
47
+ return SLOT0_SELECTOR;
48
+ }
49
+
50
+ /**
51
+ * Encode token0() call
52
+ */
53
+ function encodeToken0(): string {
54
+ return TOKEN0_SELECTOR;
55
+ }
56
+
57
+ /**
58
+ * Execute batch JSON-RPC calls
59
+ */
60
+ async function batchRpcCall(
61
+ rpcUrl: string,
62
+ calls: { to: string; data: string }[]
63
+ ): Promise<(string | null)[]> {
64
+ if (calls.length === 0) return [];
65
+
66
+ const batch = calls.map((call, i) => ({
67
+ jsonrpc: '2.0',
68
+ id: i,
69
+ method: 'eth_call',
70
+ params: [{ to: call.to, data: call.data }, 'latest']
71
+ }));
72
+
73
+ try {
74
+ const res = await fetch(rpcUrl, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(batch)
78
+ });
79
+
80
+ const results = await res.json();
81
+
82
+ // Sort by id and extract results, handling errors
83
+ return results
84
+ .sort((a: { id: number }, b: { id: number }) => a.id - b.id)
85
+ .map((r: { result?: string; error?: unknown }) => r.result || null);
86
+ } catch (err) {
87
+ console.error('[tokenData] Batch RPC failed:', err);
88
+ return calls.map(() => null);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Parse V2 reserves to get price
94
+ * Returns price of token in ETH
95
+ */
96
+ function parseV2Price(
97
+ reservesData: string | null,
98
+ tokenIsToken0: boolean
99
+ ): number | null {
100
+ if (!reservesData || reservesData === '0x') return null;
101
+
102
+ try {
103
+ // getReserves() returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)
104
+ const reserve0 = BigInt('0x' + reservesData.slice(2, 66));
105
+ const reserve1 = BigInt('0x' + reservesData.slice(66, 130));
106
+
107
+ if (reserve0 === BigInt(0) || reserve1 === BigInt(0)) return null;
108
+
109
+ // Calculate price based on which token is token0
110
+ // If our token is token0, price = reserve1/reserve0 (ETH per token)
111
+ // If our token is token1, price = reserve0/reserve1
112
+ if (tokenIsToken0) {
113
+ return Number(reserve1) / Number(reserve0);
114
+ } else {
115
+ return Number(reserve0) / Number(reserve1);
116
+ }
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Parse V3 slot0 to get price
124
+ * Returns price of token in ETH
125
+ */
126
+ function parseV3Price(
127
+ slot0Data: string | null,
128
+ tokenIsToken0: boolean
129
+ ): number | null {
130
+ if (!slot0Data || slot0Data === '0x') return null;
131
+
132
+ try {
133
+ // slot0() returns (sqrtPriceX96, tick, observationIndex, ...)
134
+ // sqrtPriceX96 is the first 32 bytes
135
+ const sqrtPriceX96 = BigInt('0x' + slot0Data.slice(2, 66));
136
+
137
+ if (sqrtPriceX96 === BigInt(0)) return null;
138
+
139
+ // Convert sqrtPriceX96 to price
140
+ // price = (sqrtPriceX96 / 2^96)^2
141
+ const Q96 = BigInt(2) ** BigInt(96);
142
+ const priceRatio = Number(sqrtPriceX96) / Number(Q96);
143
+ const price = priceRatio * priceRatio;
144
+
145
+ // If token is token0, this gives us token1/token0 (ETH per token if ETH is token1)
146
+ // If token is token1, we need to invert
147
+ if (tokenIsToken0) {
148
+ return price;
149
+ } else {
150
+ return price > 0 ? 1 / price : null;
151
+ }
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Batch fetch balances AND prices for multiple tokens
159
+ * Returns a Map of tokenAddress (lowercase) to TokenData
160
+ */
161
+ export async function fetchTokenData(
162
+ walletAddress: string,
163
+ assets: AssetInfo[],
164
+ rpcUrl: string
165
+ ): Promise<Map<string, TokenData>> {
166
+ const result = new Map<string, TokenData>();
167
+
168
+ if (assets.length === 0) return result;
169
+
170
+ // Build all RPC calls
171
+ const calls: { to: string; data: string; type: 'balance' | 'reserves' | 'slot0' | 'token0'; assetIndex: number }[] = [];
172
+
173
+ // Add balance calls for all tokens
174
+ for (let i = 0; i < assets.length; i++) {
175
+ calls.push({
176
+ to: assets[i].tokenAddress,
177
+ data: encodeBalanceOf(walletAddress),
178
+ type: 'balance',
179
+ assetIndex: i
180
+ });
181
+ }
182
+
183
+ // Add price calls for assets with pools
184
+ for (let i = 0; i < assets.length; i++) {
185
+ const asset = assets[i];
186
+ if (!asset.poolAddress) continue;
187
+
188
+ if (asset.poolVersion === 'v2') {
189
+ calls.push({
190
+ to: asset.poolAddress,
191
+ data: encodeGetReserves(),
192
+ type: 'reserves',
193
+ assetIndex: i
194
+ });
195
+ // Also need token0 to determine price direction
196
+ calls.push({
197
+ to: asset.poolAddress,
198
+ data: encodeToken0(),
199
+ type: 'token0',
200
+ assetIndex: i
201
+ });
202
+ } else if (asset.poolVersion === 'v3' || asset.poolVersion === 'v4') {
203
+ calls.push({
204
+ to: asset.poolAddress,
205
+ data: encodeSlot0(),
206
+ type: 'slot0',
207
+ assetIndex: i
208
+ });
209
+ // Also need token0 to determine price direction
210
+ calls.push({
211
+ to: asset.poolAddress,
212
+ data: encodeToken0(),
213
+ type: 'token0',
214
+ assetIndex: i
215
+ });
216
+ }
217
+ }
218
+
219
+ // Execute batch RPC
220
+ const rpcCalls = calls.map(c => ({ to: c.to, data: c.data }));
221
+ const responses = await batchRpcCall(rpcUrl, rpcCalls);
222
+
223
+ // Parse results
224
+ // First, organize results by asset and type
225
+ const balances: (string | null)[] = new Array(assets.length).fill(null);
226
+ const priceData: { reserves?: string | null; slot0?: string | null; token0?: string | null }[] =
227
+ assets.map(() => ({}));
228
+
229
+ for (let i = 0; i < calls.length; i++) {
230
+ const call = calls[i];
231
+ const response = responses[i];
232
+
233
+ switch (call.type) {
234
+ case 'balance':
235
+ balances[call.assetIndex] = response;
236
+ break;
237
+ case 'reserves':
238
+ priceData[call.assetIndex].reserves = response;
239
+ break;
240
+ case 'slot0':
241
+ priceData[call.assetIndex].slot0 = response;
242
+ break;
243
+ case 'token0':
244
+ priceData[call.assetIndex].token0 = response;
245
+ break;
246
+ }
247
+ }
248
+
249
+ // Build result map
250
+ for (let i = 0; i < assets.length; i++) {
251
+ const asset = assets[i];
252
+ const balanceRaw = balances[i];
253
+
254
+ // Parse balance
255
+ let balance = '0';
256
+ let balanceRawStr = '0';
257
+ if (balanceRaw && balanceRaw !== '0x') {
258
+ try {
259
+ const balanceBigInt = BigInt(balanceRaw);
260
+ balanceRawStr = balanceBigInt.toString();
261
+ balance = ethers.formatUnits(balanceBigInt, asset.decimals);
262
+ } catch {
263
+ // Keep defaults
264
+ }
265
+ }
266
+
267
+ // Parse price
268
+ let priceInEth: number | null = null;
269
+ const pd = priceData[i];
270
+
271
+ if (pd.token0) {
272
+ // Determine if our token is token0
273
+ let tokenIsToken0 = false;
274
+ try {
275
+ const token0Address = '0x' + pd.token0.slice(26).toLowerCase();
276
+ tokenIsToken0 = token0Address === asset.tokenAddress.toLowerCase();
277
+ } catch {
278
+ // Assume false
279
+ }
280
+
281
+ if (asset.poolVersion === 'v2' && pd.reserves) {
282
+ priceInEth = parseV2Price(pd.reserves, tokenIsToken0);
283
+ } else if ((asset.poolVersion === 'v3' || asset.poolVersion === 'v4') && pd.slot0) {
284
+ priceInEth = parseV3Price(pd.slot0, tokenIsToken0);
285
+ }
286
+ }
287
+
288
+ result.set(asset.tokenAddress.toLowerCase(), {
289
+ balance,
290
+ balanceRaw: balanceRawStr,
291
+ priceInEth
292
+ });
293
+ }
294
+
295
+ return result;
296
+ }
297
+
298
+ /**
299
+ * Calculate USD value for a token
300
+ */
301
+ export function calculateUsdValue(
302
+ balance: string,
303
+ priceInEth: number | null,
304
+ ethPrice: number | null
305
+ ): number | null {
306
+ if (priceInEth === null || ethPrice === null) return null;
307
+
308
+ const balanceNum = parseFloat(balance);
309
+ if (isNaN(balanceNum) || balanceNum === 0) return null;
310
+
311
+ return balanceNum * priceInEth * ethPrice;
312
+ }
313
+
314
+ /**
315
+ * Format USD value for display
316
+ */
317
+ export function formatUsdValue(usdValue: number | null): string {
318
+ if (usdValue === null) return '';
319
+ if (usdValue < 0.01) return '<$0.01';
320
+ if (usdValue < 1) return `$${usdValue.toFixed(2)}`;
321
+ if (usdValue < 1000) return `$${usdValue.toFixed(2)}`;
322
+ if (usdValue < 10000) return `$${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
323
+ return `$${usdValue.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
324
+ }
325
+
326
+ // SPL Token Program ID
327
+ const SPL_TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
328
+
329
+ /**
330
+ * Fetch SPL token balances for Solana wallets.
331
+ * Uses getParsedTokenAccountsByOwner for a single batched RPC call.
332
+ * Returns Map keyed by mint address (case-sensitive base58).
333
+ */
334
+ export async function fetchSolanaTokenData(
335
+ walletAddress: string,
336
+ assets: AssetInfo[],
337
+ rpcUrl: string
338
+ ): Promise<Map<string, TokenData>> {
339
+ const result = new Map<string, TokenData>();
340
+ if (assets.length === 0) return result;
341
+
342
+ try {
343
+ const connection = new Connection(rpcUrl, 'confirmed');
344
+ const ownerPubkey = new PublicKey(walletAddress);
345
+
346
+ const response = await connection.getParsedTokenAccountsByOwner(ownerPubkey, {
347
+ programId: SPL_TOKEN_PROGRAM_ID,
348
+ });
349
+
350
+ // Build mint → balance map from all token accounts
351
+ const balanceByMint = new Map<string, { amount: string; uiAmount: string }>();
352
+ for (const { account } of response.value) {
353
+ const parsed = account.data.parsed;
354
+ if (parsed?.info?.mint) {
355
+ balanceByMint.set(parsed.info.mint, {
356
+ amount: parsed.info.tokenAmount.amount,
357
+ uiAmount: parsed.info.tokenAmount.uiAmountString || '0',
358
+ });
359
+ }
360
+ }
361
+
362
+ for (const asset of assets) {
363
+ const tokenBalance = balanceByMint.get(asset.tokenAddress);
364
+ result.set(asset.tokenAddress, {
365
+ balance: tokenBalance?.uiAmount || '0',
366
+ balanceRaw: tokenBalance?.amount || '0',
367
+ priceInEth: null, // Solana on-chain pricing would need Jupiter API
368
+ });
369
+ }
370
+ } catch (err) {
371
+ console.error('[tokenData] Solana token fetch failed:', err);
372
+ for (const asset of assets) {
373
+ result.set(asset.tokenAddress, { balance: '0', balanceRaw: '0', priceInEth: null });
374
+ }
375
+ }
376
+
377
+ return result;
378
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Browser RSA-OAEP keypair lifecycle + HybridEnvelope decryption.
3
+ *
4
+ * The private key lives in memory only — lost on page reload (intentional).
5
+ * This forces re-unlock on refresh, matching 1Password's behavior.
6
+ */
7
+
8
+ let privateKey: CryptoKey | null = null;
9
+ let publicKeyBase64: string | null = null;
10
+
11
+ function arrayBufferToBase64(buffer: ArrayBuffer): string {
12
+ const bytes = new Uint8Array(buffer);
13
+ let binary = '';
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ binary += String.fromCharCode(bytes[i]);
16
+ }
17
+ return btoa(binary);
18
+ }
19
+
20
+ function base64ToArrayBuffer(b64: string): ArrayBuffer {
21
+ const binary = atob(b64);
22
+ const bytes = new Uint8Array(binary.length);
23
+ for (let i = 0; i < binary.length; i++) {
24
+ bytes[i] = binary.charCodeAt(i);
25
+ }
26
+ return bytes.buffer;
27
+ }
28
+
29
+ /**
30
+ * Generate an RSA-OAEP 2048-bit keypair.
31
+ * Private key is non-extractable and stored in module state.
32
+ * Returns the public key as SPKI base64 for sending to the server.
33
+ */
34
+ export async function generateVaultKeypair(): Promise<{ publicKeyBase64: string }> {
35
+ const pair = await crypto.subtle.generateKey(
36
+ {
37
+ name: 'RSA-OAEP',
38
+ modulusLength: 2048,
39
+ publicExponent: new Uint8Array([1, 0, 1]),
40
+ hash: 'SHA-256',
41
+ },
42
+ false, // non-extractable private key
43
+ ['decrypt'],
44
+ );
45
+ privateKey = pair.privateKey;
46
+ const spki = await crypto.subtle.exportKey('spki', pair.publicKey);
47
+ publicKeyBase64 = arrayBufferToBase64(spki);
48
+ return { publicKeyBase64 };
49
+ }
50
+
51
+ /** Returns the in-memory private key, or null after reload/lock. */
52
+ export function getVaultPrivateKey(): CryptoKey | null {
53
+ return privateKey;
54
+ }
55
+
56
+ /** Returns the cached public key base64, or null. */
57
+ export function getVaultPublicKeyBase64(): string | null {
58
+ return publicKeyBase64;
59
+ }
60
+
61
+ /** Discard both keys (called on lock). */
62
+ export function discardVaultKeypair(): void {
63
+ privateKey = null;
64
+ publicKeyBase64 = null;
65
+ }
66
+
67
+ interface HybridEnvelope {
68
+ v: number;
69
+ alg: string;
70
+ key: string;
71
+ iv: string;
72
+ tag: string;
73
+ data: string;
74
+ }
75
+
76
+ /**
77
+ * Decrypt a HybridEnvelope from the server's credential read endpoint.
78
+ *
79
+ * Always expects hybrid format: RSA-OAEP wrapped AES-256-GCM session key.
80
+ * WebCrypto expects the GCM auth tag appended to the ciphertext (unlike
81
+ * Node.js which stores it separately), so we concatenate data + tag.
82
+ */
83
+ export async function decryptCredentialPayload(encryptedBase64: string): Promise<string> {
84
+ if (!privateKey) {
85
+ throw new Error('No vault keypair — unlock required');
86
+ }
87
+
88
+ // Decode the outer base64 → JSON envelope
89
+ const envelopeJson = atob(encryptedBase64);
90
+ const envelope: HybridEnvelope = JSON.parse(envelopeJson);
91
+
92
+ if (envelope.v !== 1 || envelope.alg !== 'RSA-OAEP/AES-256-GCM') {
93
+ throw new Error(`Unsupported envelope: v=${envelope.v} alg=${envelope.alg}`);
94
+ }
95
+
96
+ // 1. RSA-OAEP decrypt the wrapped AES session key
97
+ const wrappedKey = base64ToArrayBuffer(envelope.key);
98
+ const rawSessionKey = await crypto.subtle.decrypt(
99
+ { name: 'RSA-OAEP' },
100
+ privateKey,
101
+ wrappedKey,
102
+ );
103
+
104
+ // 2. Import the AES-256-GCM session key
105
+ const aesKey = await crypto.subtle.importKey(
106
+ 'raw',
107
+ rawSessionKey,
108
+ { name: 'AES-GCM' },
109
+ false,
110
+ ['decrypt'],
111
+ );
112
+
113
+ // 3. Concatenate ciphertext + auth tag (WebCrypto expects tag appended)
114
+ const ciphertext = base64ToArrayBuffer(envelope.data);
115
+ const authTag = base64ToArrayBuffer(envelope.tag);
116
+ const combined = new Uint8Array(ciphertext.byteLength + authTag.byteLength);
117
+ combined.set(new Uint8Array(ciphertext), 0);
118
+ combined.set(new Uint8Array(authTag), ciphertext.byteLength);
119
+
120
+ // 4. AES-256-GCM decrypt
121
+ const iv = base64ToArrayBuffer(envelope.iv);
122
+ const plaintext = await crypto.subtle.decrypt(
123
+ { name: 'AES-GCM', iv },
124
+ aesKey,
125
+ combined.buffer,
126
+ );
127
+
128
+ return new TextDecoder().decode(plaintext);
129
+ }