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.
- package/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- 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;
|