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