From 7228643131af02b59f6ebcc004f2d5507c294e65 Mon Sep 17 00:00:00 2001 From: iamlukethedev Date: Sat, 21 Mar 2026 20:49:29 -0500 Subject: [PATCH 1/6] Crypto --- .env.example | 3 + package-lock.json | 509 +++++- package.json | 1 + src/app/api/crypto/pair/[pairId]/route.ts | 149 ++ src/app/api/crypto/quote/route.ts | 54 + src/app/api/crypto/swap/route.ts | 66 + .../components/CryptoImmersiveScreen.tsx | 1373 +++++++++++++++++ .../crypto/hooks/useCryptoRoomState.ts | 724 +++++++++ src/features/crypto/lib/constants.ts | 21 + src/features/crypto/lib/pnl.ts | 93 ++ src/features/crypto/lib/solana.ts | 314 ++++ src/features/crypto/lib/storage.ts | 111 ++ src/features/crypto/types.ts | 174 +++ src/features/retro-office/RetroOffice3D.tsx | 224 ++- src/features/retro-office/core/constants.ts | 1 + .../retro-office/core/furnitureDefaults.ts | 110 +- src/features/retro-office/core/geometry.ts | 4 + src/features/retro-office/core/persistence.ts | 8 + .../retro-office/objects/machines.tsx | 384 ++++- tests/unit/cryptoPnl.test.ts | 87 ++ 20 files changed, 4397 insertions(+), 13 deletions(-) create mode 100644 src/app/api/crypto/pair/[pairId]/route.ts create mode 100644 src/app/api/crypto/quote/route.ts create mode 100644 src/app/api/crypto/swap/route.ts create mode 100644 src/features/crypto/components/CryptoImmersiveScreen.tsx create mode 100644 src/features/crypto/hooks/useCryptoRoomState.ts create mode 100644 src/features/crypto/lib/constants.ts create mode 100644 src/features/crypto/lib/pnl.ts create mode 100644 src/features/crypto/lib/solana.ts create mode 100644 src/features/crypto/lib/storage.ts create mode 100644 src/features/crypto/types.ts create mode 100644 tests/unit/cryptoPnl.test.ts diff --git a/.env.example b/.env.example index 1f0005b5..3056e30b 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,6 @@ DEBUG=true # ELEVENLABS_API_KEY= # ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM # ELEVENLABS_MODEL_ID=eleven_flash_v2_5 + +# Optional: Solana RPC for the crypto room (free tier from Helius, Alchemy, etc.) +# NEXT_PUBLIC_SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY diff --git a/package-lock.json b/package-lock.json index 5cc8ff77..484f1e3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@noble/ed25519": "^3.0.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "@solana/web3.js": "^1.98.4", "@vercel/otel": "^2.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1896,6 +1897,21 @@ "node": ">= 10" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/ed25519": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz", @@ -1905,6 +1921,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2551,6 +2579,103 @@ "dev": true, "license": "MIT" }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2961,6 +3086,15 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3040,7 +3174,6 @@ "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3107,6 +3240,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -3117,7 +3256,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3862,6 +4000,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4163,6 +4313,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4201,6 +4360,23 @@ "require-from-string": "^2.0.2" } }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4259,6 +4435,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -4283,6 +4468,20 @@ "ieee754": "^1.2.1" } }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4517,6 +4716,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4781,6 +4989,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5089,6 +5309,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -5636,6 +5871,14 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5687,6 +5930,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -6206,6 +6455,15 @@ "node": ">= 14" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6810,6 +7068,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6840,6 +7107,74 @@ "react": "^19.0.0" } }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6944,6 +7279,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -8462,6 +8803,60 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -9301,6 +9696,42 @@ "fsevents": "~2.3.2" } }, + "node_modules/rpc-websockets": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.6.tgz", + "integrity": "sha512-RzuOQDGd+EtR/cBYQAH/0jjaBzhyvXXGROhxigGJPf+q3XKyvtelZCucylzxiq5MaGlfBx1075djTsxFsFDgrA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^11.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9345,6 +9776,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -9700,6 +10151,21 @@ "node": ">= 0.4" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -9904,6 +10370,15 @@ } } }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9977,6 +10452,11 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/three": { "version": "0.183.2", "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", @@ -10386,7 +10866,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10443,7 +10922,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -10618,6 +11096,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/utility-types": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", @@ -10627,6 +11119,15 @@ "node": ">= 4" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 7270432a..299c7d57 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@noble/ed25519": "^3.0.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "@solana/web3.js": "^1.98.4", "@vercel/otel": "^2.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/api/crypto/pair/[pairId]/route.ts b/src/app/api/crypto/pair/[pairId]/route.ts new file mode 100644 index 00000000..47b84dee --- /dev/null +++ b/src/app/api/crypto/pair/[pairId]/route.ts @@ -0,0 +1,149 @@ +import { NextResponse } from "next/server"; + +const PAIR_ID_RE = /^[a-zA-Z0-9]{16,128}$/; + +type DexScreenerPair = { + chainId?: string; + pairAddress?: string; + dexId?: string; + url?: string; + priceUsd?: string; + priceNative?: string; + fdv?: number; + marketCap?: number; + liquidity?: { + usd?: number; + }; + volume?: { + h24?: number; + }; + txns?: { + h24?: { + buys?: number; + sells?: number; + }; + }; + baseToken?: { + address?: string; + symbol?: string; + name?: string; + }; + quoteToken?: { + address?: string; + symbol?: string; + name?: string; + }; + priceChange?: { + m5?: number; + h1?: number; + h6?: number; + h24?: number; + }; + info?: { + imageUrl?: string; + }; +}; + +const toResponsePair = (pair: DexScreenerPair) => ({ + pairAddress: pair.pairAddress!, + dexId: pair.dexId ?? null, + url: pair.url ?? `https://dexscreener.com/solana/${pair.pairAddress}`, + priceUsd: Number(pair.priceUsd ?? "0"), + priceNative: pair.priceNative ? Number(pair.priceNative) : null, + fdv: pair.fdv ?? null, + marketCap: pair.marketCap ?? null, + liquidityUsd: pair.liquidity?.usd ?? null, + volume24hUsd: pair.volume?.h24 ?? null, + buys24h: pair.txns?.h24?.buys ?? null, + sells24h: pair.txns?.h24?.sells ?? null, + pairLabel: `${pair.baseToken?.symbol ?? "TOKEN"}/${pair.quoteToken?.symbol ?? "TOKEN"}`, + chainId: pair.chainId ?? "solana", + quoteToken: { + address: pair.quoteToken!.address!, + symbol: pair.quoteToken?.symbol ?? "TOKEN", + name: pair.quoteToken?.name ?? pair.quoteToken?.symbol ?? "Token", + }, + baseToken: { + address: pair.baseToken!.address!, + symbol: pair.baseToken?.symbol ?? "TOKEN", + name: pair.baseToken?.name ?? pair.baseToken?.symbol ?? "Token", + }, + priceChangePct: { + m5: pair.priceChange?.m5 ?? null, + h1: pair.priceChange?.h1 ?? null, + h6: pair.priceChange?.h6 ?? null, + h24: pair.priceChange?.h24 ?? null, + }, + imageUrl: pair.info?.imageUrl ?? null, + loadedAt: Date.now(), +}); + +const isCompletePair = (pair: DexScreenerPair | undefined): pair is DexScreenerPair => + Boolean(pair?.pairAddress && pair.baseToken?.address && pair.quoteToken?.address); + +const getBestPair = (pairs: DexScreenerPair[]) => + pairs + .filter(isCompletePair) + .sort((left, right) => (right.liquidity?.usd ?? 0) - (left.liquidity?.usd ?? 0))[0]; + +export async function GET( + _request: Request, + context: { params: Promise<{ pairId: string }> }, +) { + const { pairId } = await context.params; + if (!PAIR_ID_RE.test(pairId)) { + return NextResponse.json({ error: "Invalid pair id." }, { status: 400 }); + } + + try { + const requestOptions = { + headers: { + accept: "application/json", + }, + cache: "no-store" as const, + }; + + const pairUpstream = await fetch( + `https://api.dexscreener.com/latest/dex/pairs/solana/${pairId}`, + requestOptions, + ); + + let pair: DexScreenerPair | undefined; + if (pairUpstream.ok) { + const payload = (await pairUpstream.json()) as { + pairs?: DexScreenerPair[]; + }; + pair = getBestPair(payload.pairs ?? []); + } + + if (!pair) { + const tokenUpstream = await fetch( + `https://api.dexscreener.com/tokens/v1/solana/${pairId}`, + requestOptions, + ); + if (!tokenUpstream.ok) { + throw new Error(`DexScreener request failed with ${tokenUpstream.status}.`); + } + const tokenPayload = (await tokenUpstream.json()) as DexScreenerPair[]; + pair = getBestPair(tokenPayload); + } + + if (!pair) { + return NextResponse.json({ error: "Pair data was not available." }, { status: 404 }); + } + + return NextResponse.json({ + pair: toResponsePair(pair), + }); + } catch (error) { + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Unable to load the requested pair.", + }, + { status: 502 }, + ); + } +} diff --git a/src/app/api/crypto/quote/route.ts b/src/app/api/crypto/quote/route.ts new file mode 100644 index 00000000..774a7418 --- /dev/null +++ b/src/app/api/crypto/quote/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from "next/server"; + +const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,64}$/; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const inputMint = searchParams.get("inputMint")?.trim() ?? ""; + const outputMint = searchParams.get("outputMint")?.trim() ?? ""; + const amount = searchParams.get("amount")?.trim() ?? ""; + const slippageBps = Number(searchParams.get("slippageBps") ?? "150"); + + if (!MINT_RE.test(inputMint) || !MINT_RE.test(outputMint)) { + return NextResponse.json({ error: "Invalid token mint." }, { status: 400 }); + } + if (!/^\d+$/.test(amount) || Number(amount) <= 0) { + return NextResponse.json({ error: "Invalid swap amount." }, { status: 400 }); + } + if (!Number.isFinite(slippageBps) || slippageBps < 10 || slippageBps > 5_000) { + return NextResponse.json({ error: "Slippage must be between 10 and 5000 bps." }, { status: 400 }); + } + + try { + const query = new URLSearchParams({ + inputMint, + outputMint, + amount, + slippageBps: String(slippageBps), + restrictIntermediateTokens: "true", + }); + const upstream = await fetch(`https://api.jup.ag/swap/v1/quote?${query.toString()}`, { + headers: { + accept: "application/json", + }, + cache: "no-store", + }); + const payload = await upstream.json(); + if (!upstream.ok) { + throw new Error( + typeof payload?.error === "string" + ? payload.error + : "Quote request failed.", + ); + } + return NextResponse.json({ quote: payload }); + } catch (error) { + return NextResponse.json( + { + error: + error instanceof Error ? error.message : "Unable to request a Jupiter quote.", + }, + { status: 502 }, + ); + } +} diff --git a/src/app/api/crypto/swap/route.ts b/src/app/api/crypto/swap/route.ts new file mode 100644 index 00000000..1944f24e --- /dev/null +++ b/src/app/api/crypto/swap/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from "next/server"; + +const PUBLIC_KEY_RE = /^[1-9A-HJ-NP-Za-km-z]{32,64}$/; + +export async function POST(request: Request) { + try { + const payload = (await request.json()) as { + quoteResponse?: unknown; + userPublicKey?: string; + }; + const userPublicKey = payload.userPublicKey?.trim() ?? ""; + if (!PUBLIC_KEY_RE.test(userPublicKey)) { + return NextResponse.json({ error: "Invalid wallet public key." }, { status: 400 }); + } + if (!payload.quoteResponse || typeof payload.quoteResponse !== "object") { + return NextResponse.json({ error: "Missing quote payload." }, { status: 400 }); + } + + const upstream = await fetch("https://api.jup.ag/swap/v1/swap", { + method: "POST", + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + body: JSON.stringify({ + quoteResponse: payload.quoteResponse, + userPublicKey, + dynamicComputeUnitLimit: true, + dynamicSlippage: true, + prioritizationFeeLamports: { + priorityLevelWithMaxLamports: { + maxLamports: 1_000_000, + priorityLevel: "veryHigh", + }, + }, + }), + cache: "no-store", + }); + const upstreamPayload = await upstream.json(); + if (!upstream.ok || typeof upstreamPayload?.swapTransaction !== "string") { + throw new Error( + typeof upstreamPayload?.error === "string" + ? upstreamPayload.error + : "Unable to build a swap transaction.", + ); + } + + return NextResponse.json({ + swapTransaction: upstreamPayload.swapTransaction, + lastValidBlockHeight: + typeof upstreamPayload.lastValidBlockHeight === "number" + ? upstreamPayload.lastValidBlockHeight + : null, + }); + } catch (error) { + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Unable to prepare the Solana swap transaction.", + }, + { status: 502 }, + ); + } +} diff --git a/src/features/crypto/components/CryptoImmersiveScreen.tsx b/src/features/crypto/components/CryptoImmersiveScreen.tsx new file mode 100644 index 00000000..ad829c4c --- /dev/null +++ b/src/features/crypto/components/CryptoImmersiveScreen.tsx @@ -0,0 +1,1373 @@ +"use client"; + +import { + Activity, + Bot, + Coins, + CandlestickChart, + ExternalLink, + LineChart, + RefreshCw, + Shield, + Sparkles, + Wallet, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { + CRYPTO_ROOM_DEXSCREENER_URL, + CRYPTO_ROOM_PAIR_ADDRESS, +} from "@/features/crypto/lib/constants"; +import { useCryptoRoomState } from "@/features/crypto/hooks/useCryptoRoomState"; +import type { CryptoAgentTradeMode, CryptoTrackedPair } from "@/features/crypto/types"; +import type { OfficeAgent } from "@/features/retro-office/core/types"; + +const currency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 2, +}); + +const compactCurrency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + notation: "compact", + maximumFractionDigits: 2, +}); + +const number = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 4, +}); + +type TabKey = "market" | "trade" | "ledger" | "agents"; + +const DEXSCREENER_SOLANA_URL_RE = + /^https?:\/\/(?:www\.)?dexscreener\.com\/solana\/([a-zA-Z0-9]{16,128})(?:[/?#].*)?$/i; + +const parseMonitorTarget = (raw: string) => { + const trimmed = raw.trim(); + if (!trimmed) { + return { + url: CRYPTO_ROOM_DEXSCREENER_URL, + lookupId: CRYPTO_ROOM_PAIR_ADDRESS, + }; + } + const directMatch = trimmed.match(DEXSCREENER_SOLANA_URL_RE); + if (directMatch) { + return { + url: trimmed, + lookupId: directMatch[1] ?? null, + }; + } + if (/^[a-zA-Z0-9]{16,128}$/.test(trimmed)) { + return { + url: `https://dexscreener.com/solana/${trimmed}`, + lookupId: trimmed, + }; + } + return { + url: trimmed, + lookupId: null, + }; +}; + +export function CryptoImmersiveScreen({ agents }: { agents: OfficeAgent[] }) { + const [activeTab, setActiveTab] = useState("market"); + const [iframeSrc, setIframeSrc] = useState(CRYPTO_ROOM_DEXSCREENER_URL); + const [addressInput, setAddressInput] = useState(CRYPTO_ROOM_DEXSCREENER_URL); + const [pairLookupInput, setPairLookupInput] = useState(""); + const [browsedPair, setBrowsedPair] = useState(null); + const [browsedPairLoading, setBrowsedPairLoading] = useState(false); + const [browsedPairError, setBrowsedPairError] = useState(null); + const state = useCryptoRoomState(agents); + + const pendingApprovals = useMemo( + () => state.approvals.filter((approval) => approval.status === "pending"), + [state.approvals], + ); + + const syncBrowsedPair = async (lookupId: string | null) => { + if (!lookupId || lookupId === CRYPTO_ROOM_PAIR_ADDRESS) { + setBrowsedPair(null); + setBrowsedPairError(null); + setBrowsedPairLoading(false); + return; + } + try { + setBrowsedPairLoading(true); + setBrowsedPairError(null); + const response = await fetch(`/api/crypto/pair/${lookupId}`, { + cache: "no-store", + }); + const payload = (await response.json()) as { + pair?: CryptoTrackedPair; + error?: string; + }; + if (!response.ok || !payload.pair) { + throw new Error(payload.error?.trim() || "Unable to load DexScreener pair data."); + } + setBrowsedPair(payload.pair); + } catch (error) { + setBrowsedPair(null); + setBrowsedPairError( + error instanceof Error ? error.message : "Unable to load DexScreener pair data.", + ); + } finally { + setBrowsedPairLoading(false); + } + }; + + const navigateMonitor = async (raw: string) => { + const target = parseMonitorTarget(raw); + setIframeSrc(target.url); + setAddressInput(target.url); + setPairLookupInput(target.lookupId ?? ""); + await syncBrowsedPair(target.lookupId); + }; + + return ( +
+
+
+
+
+
+
+ + Claw3D Crypto Room +
+

+ Solana Trading Desk +

+

+ The art room now runs as a monitored Solana room with DexScreener + on the wall, Phantom-only signing, local trade reports, and agent + trade queues that still stop for explicit wallet approval. +

+
+ +
+
+
+
+
+ Safety posture +
+
+ Phantom-only custody +
+
+
+ +
+
+
+ + + +
+
+ + + + +
+ {state.walletError ? ( + + {state.walletError} + + ) : null} +
+ +
+
+
+
+ Wallet holdings +
+
+ {state.walletLoading + ? "Loading..." + : `${number.format(state.wallet.solBalance)} SOL`} +
+
+
+ +
+
+ {!state.wallet.connected ? ( + + Connect Phantom to see holdings. + + ) : ( + <> +
+ {state.wallet.tokenHoldings.length} token{state.wallet.tokenHoldings.length !== 1 ? "s" : ""} found +
+
+ {state.wallet.tokenHoldings.length === 0 ? ( +
No SPL tokens found.
+ ) : ( + state.wallet.tokenHoldings.map((holding) => ( + + )) + )} +
+ {state.wallet.lastUpdatedAt ? ( +
+ Updated {new Date(state.wallet.lastUpdatedAt).toLocaleTimeString()}. +
+ ) : null} + + )} +
+ +
+
+
+
+ Local ledger +
+
+ {currency.format(state.report.totalPnlUsd)} PnL +
+
+
+ +
+
+
+ + + + +
+
+ {state.ledger.length === 0 ? ( +
No trades recorded yet.
+ ) : ( + state.ledger.slice(0, 10).map((trade) => ( +
+
+
+ {trade.side === "buy" ? "Bought" : "Sold"} {trade.tokenSymbol} +
+
+ {trade.source === "agent" ? trade.agentName ?? "Agent" : "User"} ยท {trade.status} +
+
+
+ {currency.format(trade.notionalUsd)} +
+
+ )) + )} +
+
+
+
+ +
+ {[ + { + key: "market" as const, + label: "Market", + icon: , + }, + { + key: "trade" as const, + label: "Trade", + icon: , + }, + { + key: "ledger" as const, + label: "Ledger", + icon: , + }, + { + key: "agents" as const, + label: "Agents", + icon: , + }, + ].map((tab) => ( + + ))} +
+ +
+ {activeTab === "market" ? ( + navigateMonitor(pairLookupInput)} + /> + ) : activeTab === "trade" ? ( + + ) : activeTab === "ledger" ? ( + + ) : ( + + )} +
+
+
+ ); +} + +function MarketTab({ + state, + iframeSrc, + addressInput, + pairLookupInput, + browsedPair, + browsedPairLoading, + browsedPairError, + onAddressInputChange, + onPairLookupInputChange, + onNavigate, + onLookup, +}: { + state: ReturnType; + iframeSrc: string; + addressInput: string; + pairLookupInput: string; + browsedPair: CryptoTrackedPair | null; + browsedPairLoading: boolean; + browsedPairError: string | null; + onAddressInputChange: (value: string) => void; + onPairLookupInputChange: (value: string) => void; + onNavigate: (value: string) => Promise; + onLookup: () => Promise; +}) { + const snapshotPair = browsedPair ?? state.pair; + const snapshotLoading = browsedPairLoading || (!browsedPair && state.pairLoading); + const snapshotError = browsedPairError ?? (!browsedPair ? state.pairError : null); + + return ( +
+ + window.open(iframeSrc, "_blank", "noopener,noreferrer") + } + className="rounded-full border border-white/12 bg-white/6 px-4 py-2 text-[11px] uppercase tracking-[0.18em] text-white/75 transition-colors hover:bg-white/10" + > + Open full browser + + } + > +
+
+
+ + + +
+ onAddressInputChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void onNavigate(addressInput); + }} + placeholder="Paste a token address or DexScreener URL..." + className="min-w-0 flex-1 rounded-full border border-white/10 bg-black/30 px-4 py-2 font-mono text-[13px] text-white/80 outline-none transition-colors placeholder:text-white/30 focus:border-cyan-400/30" + /> + + +
+