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,128 @@
|
|
|
1
|
+
import CryptoJS from 'crypto-js';
|
|
2
|
+
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'crypto';
|
|
3
|
+
import { EncryptedData } from '../types';
|
|
4
|
+
|
|
5
|
+
const ALGORITHM = 'aes-256-ctr';
|
|
6
|
+
const SCRYPT_COST = 16384; // 2^14
|
|
7
|
+
const SCRYPT_BLOCK_SIZE = 8;
|
|
8
|
+
const SCRYPT_PARALLELIZATION = 1;
|
|
9
|
+
const DKLEN = 32;
|
|
10
|
+
|
|
11
|
+
export function encryptPrivateKey(privateKey: string, password: string): EncryptedData {
|
|
12
|
+
const salt = randomBytes(32);
|
|
13
|
+
const iv = randomBytes(16);
|
|
14
|
+
|
|
15
|
+
const derivedKey = scryptSync(password, salt, DKLEN, {
|
|
16
|
+
cost: SCRYPT_COST,
|
|
17
|
+
blockSize: SCRYPT_BLOCK_SIZE,
|
|
18
|
+
parallelization: SCRYPT_PARALLELIZATION
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const cipher = createCipheriv(ALGORITHM, derivedKey, iv);
|
|
22
|
+
const ciphertext = Buffer.concat([
|
|
23
|
+
cipher.update(privateKey, 'utf8'),
|
|
24
|
+
cipher.final()
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const mac = CryptoJS.HmacSHA256(
|
|
28
|
+
ciphertext.toString('hex') + iv.toString('hex'),
|
|
29
|
+
derivedKey.toString('hex')
|
|
30
|
+
).toString();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
ciphertext: ciphertext.toString('hex'),
|
|
34
|
+
iv: iv.toString('hex'),
|
|
35
|
+
salt: salt.toString('hex'),
|
|
36
|
+
mac
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decryptPrivateKey(encrypted: EncryptedData, password: string): string {
|
|
41
|
+
const salt = Buffer.from(encrypted.salt, 'hex');
|
|
42
|
+
const iv = Buffer.from(encrypted.iv, 'hex');
|
|
43
|
+
const ciphertext = Buffer.from(encrypted.ciphertext, 'hex');
|
|
44
|
+
|
|
45
|
+
const derivedKey = scryptSync(password, salt, DKLEN, {
|
|
46
|
+
cost: SCRYPT_COST,
|
|
47
|
+
blockSize: SCRYPT_BLOCK_SIZE,
|
|
48
|
+
parallelization: SCRYPT_PARALLELIZATION
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const expectedMac = CryptoJS.HmacSHA256(
|
|
52
|
+
encrypted.ciphertext + encrypted.iv,
|
|
53
|
+
derivedKey.toString('hex')
|
|
54
|
+
).toString();
|
|
55
|
+
|
|
56
|
+
if (expectedMac !== encrypted.mac) {
|
|
57
|
+
throw new Error('Invalid password or corrupted data');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const decipher = createDecipheriv(ALGORITHM, derivedKey, iv);
|
|
61
|
+
const decrypted = Buffer.concat([
|
|
62
|
+
decipher.update(ciphertext),
|
|
63
|
+
decipher.final()
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return decrypted.toString('utf8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encrypt data using a seed phrase (mnemonic) as the key.
|
|
71
|
+
* Uses faster key derivation since mnemonic has high entropy.
|
|
72
|
+
*/
|
|
73
|
+
export function encryptWithSeed(data: string, seed: string): EncryptedData {
|
|
74
|
+
const iv = randomBytes(16);
|
|
75
|
+
// Use SHA-256 of seed as key (mnemonic has enough entropy, no need for slow scrypt)
|
|
76
|
+
const derivedKey = Buffer.from(
|
|
77
|
+
CryptoJS.SHA256(seed).toString(CryptoJS.enc.Hex),
|
|
78
|
+
'hex'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const cipher = createCipheriv(ALGORITHM, derivedKey, iv);
|
|
82
|
+
const ciphertext = Buffer.concat([
|
|
83
|
+
cipher.update(data, 'utf8'),
|
|
84
|
+
cipher.final()
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const mac = CryptoJS.HmacSHA256(
|
|
88
|
+
ciphertext.toString('hex') + iv.toString('hex'),
|
|
89
|
+
derivedKey.toString('hex')
|
|
90
|
+
).toString();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
ciphertext: ciphertext.toString('hex'),
|
|
94
|
+
iv: iv.toString('hex'),
|
|
95
|
+
salt: '', // Not needed for seed-based encryption
|
|
96
|
+
mac
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Decrypt data using a seed phrase (mnemonic) as the key.
|
|
102
|
+
*/
|
|
103
|
+
export function decryptWithSeed(encrypted: EncryptedData, seed: string): string {
|
|
104
|
+
const iv = Buffer.from(encrypted.iv, 'hex');
|
|
105
|
+
const ciphertext = Buffer.from(encrypted.ciphertext, 'hex');
|
|
106
|
+
|
|
107
|
+
const derivedKey = Buffer.from(
|
|
108
|
+
CryptoJS.SHA256(seed).toString(CryptoJS.enc.Hex),
|
|
109
|
+
'hex'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const expectedMac = CryptoJS.HmacSHA256(
|
|
113
|
+
encrypted.ciphertext + encrypted.iv,
|
|
114
|
+
derivedKey.toString('hex')
|
|
115
|
+
).toString();
|
|
116
|
+
|
|
117
|
+
if (expectedMac !== encrypted.mac) {
|
|
118
|
+
throw new Error('Invalid seed or corrupted data');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const decipher = createDecipheriv(ALGORITHM, derivedKey, iv);
|
|
122
|
+
const decrypted = Buffer.concat([
|
|
123
|
+
decipher.update(ciphertext),
|
|
124
|
+
decipher.final()
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
return decrypted.toString('utf8');
|
|
128
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error helpers used across routes and lib modules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract a human-readable message from an unknown catch value.
|
|
7
|
+
*/
|
|
8
|
+
export function getErrorMessage(error: unknown): string {
|
|
9
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An error with an HTTP status code, thrown by validation helpers
|
|
14
|
+
* so callers can respond with the correct status.
|
|
15
|
+
*/
|
|
16
|
+
export class HttpError extends Error {
|
|
17
|
+
constructor(public status: number, message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook emitter for notifying Next.js of wallet events
|
|
3
|
+
* Posts events to Next.js API which broadcasts to WebSocket clients
|
|
4
|
+
* Also stores all events in database for debugging/audit trail
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { prisma } from './db';
|
|
8
|
+
import { log } from './pino';
|
|
9
|
+
|
|
10
|
+
// Event types (mirrored from src/lib/events.ts)
|
|
11
|
+
export const WALLET_EVENTS = {
|
|
12
|
+
TOKEN_CREATED: 'token:created',
|
|
13
|
+
TOKEN_REVOKED: 'token:revoked',
|
|
14
|
+
TOKEN_SPENT: 'token:spent',
|
|
15
|
+
WALLET_CREATED: 'wallet:created',
|
|
16
|
+
WALLET_CHANGED: 'wallet:changed',
|
|
17
|
+
ASSET_CHANGED: 'asset:changed',
|
|
18
|
+
TX_CREATED: 'tx:created',
|
|
19
|
+
ACTION_CREATED: 'action:created',
|
|
20
|
+
ACTION_RESOLVED: 'action:resolved',
|
|
21
|
+
VAULT_UNLOCKED: 'vault:unlocked',
|
|
22
|
+
CREDENTIAL_CHANGED: 'credential:changed',
|
|
23
|
+
CREDENTIAL_ACCESSED: 'credential:accessed',
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type WalletEventType = (typeof WALLET_EVENTS)[keyof typeof WALLET_EVENTS];
|
|
27
|
+
|
|
28
|
+
interface WalletEvent<T = unknown> {
|
|
29
|
+
type: WalletEventType | string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
source: 'express' | 'nextjs';
|
|
32
|
+
data: T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// WebSocket server broadcast endpoint (runs on port 4748)
|
|
36
|
+
const WS_BROADCAST_URL = process.env.WS_BROADCAST_URL ?? 'http://localhost:4748/broadcast';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Store event in database (non-blocking)
|
|
40
|
+
* Automatically stores all events for debugging and audit purposes
|
|
41
|
+
*/
|
|
42
|
+
function storeEvent<T>(event: WalletEvent<T>): void {
|
|
43
|
+
prisma.event.create({
|
|
44
|
+
data: {
|
|
45
|
+
type: event.type,
|
|
46
|
+
source: event.source,
|
|
47
|
+
data: JSON.stringify(event.data),
|
|
48
|
+
timestamp: new Date(event.timestamp),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
.then(() => {
|
|
52
|
+
// Stored successfully
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
log.error({ err: err.message }, 'failed to store event in DB');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Emit a wallet event to Next.js webhook
|
|
61
|
+
* Non-blocking - failures are logged but don't affect the calling code
|
|
62
|
+
* Events are automatically stored in the database
|
|
63
|
+
*/
|
|
64
|
+
export function emitWalletEvent<T>(type: WalletEventType | string, data: T): void {
|
|
65
|
+
const event: WalletEvent<T> = {
|
|
66
|
+
type,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
source: 'express',
|
|
69
|
+
data,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Store in database (non-blocking)
|
|
73
|
+
storeEvent(event);
|
|
74
|
+
|
|
75
|
+
// Fire and forget broadcast to WebSocket server - don't block the request
|
|
76
|
+
// Skip when WS_BROADCAST_URL is empty (e.g. in tests)
|
|
77
|
+
if (!WS_BROADCAST_URL) return;
|
|
78
|
+
|
|
79
|
+
fetch(WS_BROADCAST_URL, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(event),
|
|
83
|
+
})
|
|
84
|
+
.then((res) => {
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
log.warn({ status: res.status, type }, 'WebSocket broadcast failed');
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.catch((err) => {
|
|
90
|
+
// Log but don't throw - this is non-critical
|
|
91
|
+
log.warn({ err: err.message, type }, 'failed to broadcast to WebSocket');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Type-safe event emitters for each event type
|
|
96
|
+
export const events = {
|
|
97
|
+
tokenCreated: (data: {
|
|
98
|
+
tokenHash: string;
|
|
99
|
+
agentId: string;
|
|
100
|
+
limit: number;
|
|
101
|
+
permissions: string[];
|
|
102
|
+
expiresAt: number;
|
|
103
|
+
}) => emitWalletEvent(WALLET_EVENTS.TOKEN_CREATED, data),
|
|
104
|
+
|
|
105
|
+
tokenRevoked: (data: { tokenHash: string }) =>
|
|
106
|
+
emitWalletEvent(WALLET_EVENTS.TOKEN_REVOKED, data),
|
|
107
|
+
|
|
108
|
+
tokenSpent: (data: {
|
|
109
|
+
tokenHash: string;
|
|
110
|
+
amount: number;
|
|
111
|
+
newSpent: number;
|
|
112
|
+
remaining: number;
|
|
113
|
+
limitType?: 'fund' | 'send' | 'swap';
|
|
114
|
+
}) => emitWalletEvent(WALLET_EVENTS.TOKEN_SPENT, data),
|
|
115
|
+
|
|
116
|
+
walletCreated: (data: {
|
|
117
|
+
address: string;
|
|
118
|
+
tier: 'hot' | 'temp';
|
|
119
|
+
chain: string;
|
|
120
|
+
name?: string;
|
|
121
|
+
tokenHash?: string;
|
|
122
|
+
}) => emitWalletEvent(WALLET_EVENTS.WALLET_CREATED, data),
|
|
123
|
+
|
|
124
|
+
walletChanged: (data: {
|
|
125
|
+
address: string;
|
|
126
|
+
reason: 'created' | 'updated';
|
|
127
|
+
}) => emitWalletEvent(WALLET_EVENTS.WALLET_CHANGED, data),
|
|
128
|
+
|
|
129
|
+
assetChanged: (data: {
|
|
130
|
+
walletAddress: string;
|
|
131
|
+
tokenAddress: string;
|
|
132
|
+
symbol?: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
poolAddress?: string;
|
|
135
|
+
poolVersion?: string;
|
|
136
|
+
icon?: string;
|
|
137
|
+
removed?: boolean;
|
|
138
|
+
}) => emitWalletEvent(WALLET_EVENTS.ASSET_CHANGED, data),
|
|
139
|
+
|
|
140
|
+
txCreated: (data: {
|
|
141
|
+
walletAddress: string;
|
|
142
|
+
id: string;
|
|
143
|
+
type: string;
|
|
144
|
+
txHash?: string;
|
|
145
|
+
amount?: string;
|
|
146
|
+
tokenAddress?: string;
|
|
147
|
+
tokenAmount?: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
}) => emitWalletEvent(WALLET_EVENTS.TX_CREATED, data),
|
|
150
|
+
|
|
151
|
+
actionCreated: (data: {
|
|
152
|
+
id: string;
|
|
153
|
+
type: string;
|
|
154
|
+
source: string;
|
|
155
|
+
summary: string;
|
|
156
|
+
expiresAt: number | null;
|
|
157
|
+
metadata?: Record<string, unknown>;
|
|
158
|
+
}) => emitWalletEvent(WALLET_EVENTS.ACTION_CREATED, data),
|
|
159
|
+
|
|
160
|
+
actionResolved: (data: {
|
|
161
|
+
id: string;
|
|
162
|
+
type: string;
|
|
163
|
+
approved: boolean;
|
|
164
|
+
resolvedBy: string;
|
|
165
|
+
}) => emitWalletEvent(WALLET_EVENTS.ACTION_RESOLVED, data),
|
|
166
|
+
|
|
167
|
+
vaultUnlocked: (data: { address: string; vaultId: string }) =>
|
|
168
|
+
emitWalletEvent(WALLET_EVENTS.VAULT_UNLOCKED, data),
|
|
169
|
+
|
|
170
|
+
credentialChanged: (data: {
|
|
171
|
+
credentialId: string;
|
|
172
|
+
vaultId: string;
|
|
173
|
+
change:
|
|
174
|
+
| 'created'
|
|
175
|
+
| 'updated'
|
|
176
|
+
| 'archived'
|
|
177
|
+
| 'moved_to_recently_deleted'
|
|
178
|
+
| 'restored_to_active'
|
|
179
|
+
| 'restored_to_archive'
|
|
180
|
+
| 'purged';
|
|
181
|
+
actorType: 'admin' | 'agent';
|
|
182
|
+
agentId?: string;
|
|
183
|
+
tokenHash?: string;
|
|
184
|
+
fromLocation?: 'active' | 'archive' | 'recently_deleted';
|
|
185
|
+
toLocation?: 'active' | 'archive' | 'recently_deleted';
|
|
186
|
+
}) => emitWalletEvent(WALLET_EVENTS.CREDENTIAL_CHANGED, data),
|
|
187
|
+
|
|
188
|
+
credentialAccessed: (data: {
|
|
189
|
+
credentialId: string;
|
|
190
|
+
vaultId: string;
|
|
191
|
+
action: 'credentials.read' | 'credentials.totp';
|
|
192
|
+
allowed: boolean;
|
|
193
|
+
reasonCode: string;
|
|
194
|
+
httpStatus: number;
|
|
195
|
+
actorType: 'admin' | 'agent';
|
|
196
|
+
agentId?: string;
|
|
197
|
+
tokenHash?: string;
|
|
198
|
+
}) => emitWalletEvent(WALLET_EVENTS.CREDENTIAL_ACCESSED, data),
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generic event emitter for custom/new event types
|
|
202
|
+
* Events are automatically stored in the database
|
|
203
|
+
*/
|
|
204
|
+
custom: <T>(type: string, data: T) => emitWalletEvent(type, data),
|
|
205
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { WalletInfo, EncryptedData } from '../types';
|
|
3
|
+
import { getMnemonic, isUnlocked, getVaultMnemonic, isVaultUnlocked, getPrimaryVaultId } from './cold';
|
|
4
|
+
import { encryptWithSeed, decryptWithSeed } from './encrypt';
|
|
5
|
+
import { prisma } from './db';
|
|
6
|
+
import { normalizeAddress, isSolanaChain } from './address';
|
|
7
|
+
import { createSolanaHotWallet, getSolanaKeypair, signSolanaTransaction } from './solana/wallet';
|
|
8
|
+
|
|
9
|
+
export interface HotWalletInfo extends WalletInfo {
|
|
10
|
+
name?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
emoji?: string;
|
|
14
|
+
hidden?: boolean;
|
|
15
|
+
tokenHash: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateHotWalletOptions {
|
|
19
|
+
tokenHash: string;
|
|
20
|
+
chain?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
color?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
emoji?: string;
|
|
25
|
+
hidden?: boolean;
|
|
26
|
+
coldWalletId?: string; // Which vault to encrypt with (null = primary)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new hot wallet with random keypair, encrypted with the seed phrase.
|
|
31
|
+
* The wallet is owned by the token that creates it.
|
|
32
|
+
*/
|
|
33
|
+
export async function createHotWallet(options: CreateHotWalletOptions): Promise<HotWalletInfo> {
|
|
34
|
+
const { chain = 'base', coldWalletId } = options;
|
|
35
|
+
|
|
36
|
+
// Delegate to Solana wallet creation if Solana chain
|
|
37
|
+
if (isSolanaChain(chain)) {
|
|
38
|
+
return createSolanaHotWallet(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get mnemonic from specific vault or primary
|
|
42
|
+
const mnemonic = coldWalletId ? getVaultMnemonic(coldWalletId) : getMnemonic();
|
|
43
|
+
if (!mnemonic) {
|
|
44
|
+
const target = coldWalletId ? `Vault ${coldWalletId}` : 'Cold wallet';
|
|
45
|
+
throw new Error(`${target} must be unlocked to create hot wallets`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { tokenHash, name, color, description, emoji, hidden = false } = options;
|
|
49
|
+
|
|
50
|
+
// Generate random wallet
|
|
51
|
+
const wallet = ethers.Wallet.createRandom();
|
|
52
|
+
|
|
53
|
+
// Encrypt private key with seed phrase
|
|
54
|
+
const encrypted = encryptWithSeed(wallet.privateKey, mnemonic);
|
|
55
|
+
|
|
56
|
+
// Store in DB (with coldWalletId reference)
|
|
57
|
+
const hotWallet = await prisma.hotWallet.create({
|
|
58
|
+
data: {
|
|
59
|
+
address: normalizeAddress(wallet.address, chain),
|
|
60
|
+
encryptedPrivateKey: JSON.stringify(encrypted),
|
|
61
|
+
tokenHash,
|
|
62
|
+
coldWalletId: coldWalletId || null,
|
|
63
|
+
name,
|
|
64
|
+
color,
|
|
65
|
+
description,
|
|
66
|
+
emoji,
|
|
67
|
+
hidden,
|
|
68
|
+
chain,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
address: hotWallet.address,
|
|
74
|
+
tier: 'hot',
|
|
75
|
+
chain: hotWallet.chain,
|
|
76
|
+
createdAt: hotWallet.createdAt.toISOString(),
|
|
77
|
+
name: hotWallet.name || undefined,
|
|
78
|
+
color: hotWallet.color || undefined,
|
|
79
|
+
description: hotWallet.description || undefined,
|
|
80
|
+
emoji: hotWallet.emoji || undefined,
|
|
81
|
+
hidden: hotWallet.hidden,
|
|
82
|
+
tokenHash: hotWallet.tokenHash,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* List hot wallets. If tokenHash is provided, filter to only wallets owned by that token.
|
|
88
|
+
* If not provided (human access), return all wallets.
|
|
89
|
+
* If includeHidden is false (default), hidden wallets are excluded.
|
|
90
|
+
*/
|
|
91
|
+
export async function listHotWallets(tokenHash?: string, includeHidden: boolean = false): Promise<HotWalletInfo[]> {
|
|
92
|
+
const where: { tokenHash?: string; hidden?: boolean } = {};
|
|
93
|
+
if (tokenHash) where.tokenHash = tokenHash;
|
|
94
|
+
if (!includeHidden) where.hidden = false;
|
|
95
|
+
|
|
96
|
+
const wallets = await prisma.hotWallet.findMany({
|
|
97
|
+
where,
|
|
98
|
+
orderBy: { createdAt: 'desc' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return wallets.map((w) => ({
|
|
102
|
+
address: w.address,
|
|
103
|
+
tier: 'hot' as const,
|
|
104
|
+
chain: w.chain,
|
|
105
|
+
createdAt: w.createdAt.toISOString(),
|
|
106
|
+
name: w.name || undefined,
|
|
107
|
+
color: w.color || undefined,
|
|
108
|
+
description: w.description || undefined,
|
|
109
|
+
emoji: w.emoji || undefined,
|
|
110
|
+
hidden: w.hidden,
|
|
111
|
+
tokenHash: w.tokenHash,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a hot wallet by address.
|
|
117
|
+
*/
|
|
118
|
+
export async function getHotWallet(address: string) {
|
|
119
|
+
// Try lowercase first (EVM), then exact match (Solana)
|
|
120
|
+
let wallet = await prisma.hotWallet.findUnique({
|
|
121
|
+
where: { address: address.toLowerCase() },
|
|
122
|
+
});
|
|
123
|
+
if (!wallet && address !== address.toLowerCase()) {
|
|
124
|
+
wallet = await prisma.hotWallet.findUnique({
|
|
125
|
+
where: { address },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!wallet) return null;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
address: wallet.address,
|
|
133
|
+
tokenHash: wallet.tokenHash,
|
|
134
|
+
coldWalletId: wallet.coldWalletId,
|
|
135
|
+
metadata: {
|
|
136
|
+
name: wallet.name,
|
|
137
|
+
color: wallet.color,
|
|
138
|
+
description: wallet.description,
|
|
139
|
+
emoji: wallet.emoji,
|
|
140
|
+
hidden: wallet.hidden,
|
|
141
|
+
chain: wallet.chain,
|
|
142
|
+
createdAt: wallet.createdAt.toISOString(),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sign and send a transaction from a hot wallet.
|
|
149
|
+
* Requires the cold wallet to be unlocked to decrypt the private key.
|
|
150
|
+
*/
|
|
151
|
+
export async function signWithHotWallet(
|
|
152
|
+
address: string,
|
|
153
|
+
transaction: ethers.TransactionRequest,
|
|
154
|
+
provider: ethers.Provider
|
|
155
|
+
): Promise<{ hash: string }> {
|
|
156
|
+
// Try lowercase first (EVM), then exact match (Solana)
|
|
157
|
+
let wallet = await prisma.hotWallet.findUnique({
|
|
158
|
+
where: { address: address.toLowerCase() },
|
|
159
|
+
});
|
|
160
|
+
if (!wallet && address !== address.toLowerCase()) {
|
|
161
|
+
wallet = await prisma.hotWallet.findUnique({
|
|
162
|
+
where: { address },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!wallet) {
|
|
167
|
+
throw new Error(`Hot wallet not found: ${address}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Get mnemonic from the vault this hot wallet belongs to
|
|
171
|
+
const mnemonic = wallet.coldWalletId
|
|
172
|
+
? getVaultMnemonic(wallet.coldWalletId)
|
|
173
|
+
: getMnemonic();
|
|
174
|
+
if (!mnemonic) {
|
|
175
|
+
const target = wallet.coldWalletId ? `Vault ${wallet.coldWalletId}` : 'Cold wallet';
|
|
176
|
+
throw new Error(`${target} must be unlocked to sign from hot wallet`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Decrypt the private key
|
|
180
|
+
const encrypted: EncryptedData = JSON.parse(wallet.encryptedPrivateKey);
|
|
181
|
+
const privateKey = decryptWithSeed(encrypted, mnemonic);
|
|
182
|
+
|
|
183
|
+
// Create signer and send transaction
|
|
184
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
185
|
+
const tx = await signer.sendTransaction(transaction);
|
|
186
|
+
|
|
187
|
+
return { hash: tx.hash };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Export a hot wallet's private key.
|
|
192
|
+
* Requires the cold wallet to be unlocked.
|
|
193
|
+
*/
|
|
194
|
+
export async function exportHotWallet(address: string): Promise<{ address: string; privateKey: string }> {
|
|
195
|
+
// Try lowercase first (EVM), then exact match (Solana)
|
|
196
|
+
let wallet = await prisma.hotWallet.findUnique({
|
|
197
|
+
where: { address: address.toLowerCase() },
|
|
198
|
+
});
|
|
199
|
+
if (!wallet && address !== address.toLowerCase()) {
|
|
200
|
+
wallet = await prisma.hotWallet.findUnique({
|
|
201
|
+
where: { address },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!wallet) {
|
|
206
|
+
throw new Error(`Hot wallet not found: ${address}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Get mnemonic from the vault this hot wallet belongs to
|
|
210
|
+
const mnemonic = wallet.coldWalletId
|
|
211
|
+
? getVaultMnemonic(wallet.coldWalletId)
|
|
212
|
+
: getMnemonic();
|
|
213
|
+
if (!mnemonic) {
|
|
214
|
+
const target = wallet.coldWalletId ? `Vault ${wallet.coldWalletId}` : 'Cold wallet';
|
|
215
|
+
throw new Error(`${target} must be unlocked to export hot wallet`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Decrypt the private key
|
|
219
|
+
const encrypted: EncryptedData = JSON.parse(wallet.encryptedPrivateKey);
|
|
220
|
+
const privateKey = decryptWithSeed(encrypted, mnemonic);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
address: wallet.address,
|
|
224
|
+
privateKey,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete a hot wallet.
|
|
230
|
+
*/
|
|
231
|
+
export async function deleteHotWallet(address: string): Promise<void> {
|
|
232
|
+
// Try lowercase first (EVM), then exact (Solana)
|
|
233
|
+
try {
|
|
234
|
+
await prisma.hotWallet.delete({ where: { address: address.toLowerCase() } });
|
|
235
|
+
} catch {
|
|
236
|
+
if (address !== address.toLowerCase()) {
|
|
237
|
+
await prisma.hotWallet.delete({ where: { address } }).catch(() => {});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update hot wallet metadata.
|
|
244
|
+
*/
|
|
245
|
+
export async function updateHotWallet(
|
|
246
|
+
address: string,
|
|
247
|
+
updates: { name?: string; color?: string; description?: string; emoji?: string; hidden?: boolean }
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
try {
|
|
250
|
+
await prisma.hotWallet.update({
|
|
251
|
+
where: { address: address.toLowerCase() },
|
|
252
|
+
data: updates,
|
|
253
|
+
});
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
// Try exact match (Solana addresses are case-sensitive)
|
|
257
|
+
if (address !== address.toLowerCase()) {
|
|
258
|
+
try {
|
|
259
|
+
await prisma.hotWallet.update({
|
|
260
|
+
where: { address },
|
|
261
|
+
data: updates,
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Search hot wallets by name, address, or description.
|
|
274
|
+
* If tokenHash is provided, filter to only wallets owned by that token.
|
|
275
|
+
* Always includes hidden wallets in search results.
|
|
276
|
+
*/
|
|
277
|
+
export async function searchHotWallets(query: string, tokenHash?: string): Promise<HotWalletInfo[]> {
|
|
278
|
+
const lowerQuery = query.toLowerCase();
|
|
279
|
+
|
|
280
|
+
const where: { tokenHash?: string; OR: Array<{ name?: { contains: string }; address?: { contains: string }; description?: { contains: string } }> } = {
|
|
281
|
+
OR: [
|
|
282
|
+
{ name: { contains: lowerQuery } },
|
|
283
|
+
{ address: { contains: lowerQuery } },
|
|
284
|
+
{ description: { contains: lowerQuery } },
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
if (tokenHash) where.tokenHash = tokenHash;
|
|
288
|
+
|
|
289
|
+
const wallets = await prisma.hotWallet.findMany({
|
|
290
|
+
where,
|
|
291
|
+
orderBy: { createdAt: 'desc' },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return wallets.map((w) => ({
|
|
295
|
+
address: w.address,
|
|
296
|
+
tier: 'hot' as const,
|
|
297
|
+
chain: w.chain,
|
|
298
|
+
createdAt: w.createdAt.toISOString(),
|
|
299
|
+
name: w.name || undefined,
|
|
300
|
+
color: w.color || undefined,
|
|
301
|
+
description: w.description || undefined,
|
|
302
|
+
emoji: w.emoji || undefined,
|
|
303
|
+
hidden: w.hidden,
|
|
304
|
+
tokenHash: w.tokenHash,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if a token owns a specific hot wallet.
|
|
310
|
+
*/
|
|
311
|
+
export async function tokenOwnsWallet(tokenHash: string, address: string): Promise<boolean> {
|
|
312
|
+
// Try lowercase first (EVM), then exact match (Solana)
|
|
313
|
+
let wallet = await prisma.hotWallet.findUnique({
|
|
314
|
+
where: { address: address.toLowerCase() },
|
|
315
|
+
select: { tokenHash: true },
|
|
316
|
+
});
|
|
317
|
+
if (!wallet && address !== address.toLowerCase()) {
|
|
318
|
+
wallet = await prisma.hotWallet.findUnique({
|
|
319
|
+
where: { address },
|
|
320
|
+
select: { tokenHash: true },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return wallet?.tokenHash === tokenHash;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if a token can access a specific wallet.
|
|
329
|
+
* A token can access a wallet if:
|
|
330
|
+
* 1. The token created the wallet (tokenHash match), OR
|
|
331
|
+
* 2. The wallet address is in the token's walletAccess array
|
|
332
|
+
*
|
|
333
|
+
* @param tokenHash - Hash of the token making the request
|
|
334
|
+
* @param walletAccess - Array of wallet addresses the token has been granted access to
|
|
335
|
+
* @param address - The wallet address to check access for
|
|
336
|
+
* @returns true if the token can access the wallet
|
|
337
|
+
*/
|
|
338
|
+
export async function tokenCanAccessWallet(
|
|
339
|
+
tokenHash: string,
|
|
340
|
+
walletAccess: string[] | undefined,
|
|
341
|
+
address: string,
|
|
342
|
+
chain?: string
|
|
343
|
+
): Promise<boolean> {
|
|
344
|
+
const normalized = normalizeAddress(address, chain);
|
|
345
|
+
|
|
346
|
+
// Check if wallet address is in the walletAccess grants
|
|
347
|
+
if (walletAccess && walletAccess.includes(normalized)) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
// Also check original address (for Solana addresses that may be stored as-is)
|
|
351
|
+
if (walletAccess && walletAccess.includes(address)) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fall back to checking ownership
|
|
356
|
+
return tokenOwnsWallet(tokenHash, address);
|
|
357
|
+
}
|