Add Pump.fun token launch flow.

Wire a shared launch backend into the crypto room and agent chat so users can create tokens manually or through a guided conversation, with wallet-approved and server-side execution paths.

Made-with: Cursor
This commit is contained in:
iamlukethedev
2026-04-01 17:41:23 -05:00
committed by iamlukethedev
parent f8f8491f50
commit b2b798a28b
15 changed files with 2438 additions and 21 deletions
+615 -13
View File
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0",
"@pump-fun/pump-sdk": "^1.32.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@solana/web3.js": "^1.98.4",
@@ -27,7 +28,8 @@
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.4.0",
"three": "^0.183.2",
"ws": "^8.18.3"
"ws": "^8.18.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@playwright/test": "^1.58.0",
@@ -382,6 +384,85 @@
"node": ">=6.9.0"
}
},
"node_modules/@coral-xyz/anchor": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz",
"integrity": "sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA==",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@coral-xyz/anchor-errors": "^0.31.1",
"@coral-xyz/borsh": "^0.31.1",
"@noble/hashes": "^1.3.1",
"@solana/web3.js": "^1.69.0",
"bn.js": "^5.1.2",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.2",
"camelcase": "^6.3.0",
"cross-fetch": "^3.1.5",
"eventemitter3": "^4.0.7",
"pako": "^2.0.3",
"superstruct": "^0.15.4",
"toml": "^3.0.0"
},
"engines": {
"node": ">=17"
}
},
"node_modules/@coral-xyz/anchor-errors": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz",
"integrity": "sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/@coral-xyz/anchor/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/@coral-xyz/anchor/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/@coral-xyz/anchor/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/@coral-xyz/anchor/node_modules/superstruct": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz",
"integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==",
"license": "MIT"
},
"node_modules/@coral-xyz/borsh": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.31.1.tgz",
"integrity": "sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw==",
"license": "Apache-2.0",
"dependencies": {
"bn.js": "^5.1.2",
"buffer-layout": "^1.2.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@solana/web3.js": "^1.69.0"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
@@ -2296,6 +2377,36 @@
"node": ">=18"
}
},
"node_modules/@pump-fun/pump-sdk": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/@pump-fun/pump-sdk/-/pump-sdk-1.32.0.tgz",
"integrity": "sha512-ptqaz119m80w9ICjFFQLMO6WJw4DSZacj5Gzi4nSZEDlf7mLbgj5RbPzGZ3Eud+s+mTFVxJmztPjFVr/hpDI+Q==",
"license": "MIT",
"dependencies": {
"@coral-xyz/anchor": "^0.31.1",
"@pump-fun/pump-swap-sdk": "^1.13.0",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "^1.98.2",
"bn.js": "^5.2.2",
"undici-types": "^6.20.0"
}
},
"node_modules/@pump-fun/pump-swap-sdk": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@pump-fun/pump-swap-sdk/-/pump-swap-sdk-1.14.1.tgz",
"integrity": "sha512-U3vFL1Plhl/sBYUAHQhrFkmzXLxJp/gmuqHDn+odgwC1cRj15DJLVlRxnksGBMnf8dlScX9I+DdVng6rGbUnzg==",
"license": "MIT",
"dependencies": {
"@coral-xyz/anchor": "^0.31.1",
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.98.2",
"bn.js": "^5.2.2",
"bs58": "^6.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@react-three/drei": {
"version": "10.7.7",
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
@@ -2753,6 +2864,37 @@
"node": ">=5.10"
}
},
"node_modules/@solana/buffer-layout-utils": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz",
"integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==",
"license": "Apache-2.0",
"dependencies": {
"@solana/buffer-layout": "^4.0.0",
"@solana/web3.js": "^1.32.0",
"bigint-buffer": "^1.1.5",
"bignumber.js": "^9.0.1"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/@solana/codecs": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz",
"integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/codecs-data-structures": "2.0.0-rc.1",
"@solana/codecs-numbers": "2.0.0-rc.1",
"@solana/codecs-strings": "2.0.0-rc.1",
"@solana/options": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-core": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
@@ -2768,6 +2910,82 @@
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/codecs-data-structures": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz",
"integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/codecs-numbers": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-data-structures/node_modules/@solana/codecs-core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz",
"integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-data-structures/node_modules/@solana/codecs-numbers": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz",
"integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-data-structures/node_modules/@solana/errors": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz",
"integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-data-structures/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/codecs-data-structures/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@solana/codecs-numbers": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
@@ -2784,6 +3002,145 @@
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/codecs-strings": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz",
"integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/codecs-numbers": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"fastestsmallesttextencoderdecoder": "^1.0.22",
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-strings/node_modules/@solana/codecs-core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz",
"integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-strings/node_modules/@solana/codecs-numbers": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz",
"integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-strings/node_modules/@solana/errors": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz",
"integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs-strings/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/codecs-strings/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@solana/codecs/node_modules/@solana/codecs-core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz",
"integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs/node_modules/@solana/codecs-numbers": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz",
"integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs/node_modules/@solana/errors": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz",
"integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/codecs/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/codecs/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@solana/errors": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
@@ -2815,6 +3172,133 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@solana/options": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz",
"integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/codecs-data-structures": "2.0.0-rc.1",
"@solana/codecs-numbers": "2.0.0-rc.1",
"@solana/codecs-strings": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/options/node_modules/@solana/codecs-core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz",
"integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/options/node_modules/@solana/codecs-numbers": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz",
"integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.0.0-rc.1",
"@solana/errors": "2.0.0-rc.1"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/options/node_modules/@solana/errors": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz",
"integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"peerDependencies": {
"typescript": ">=5"
}
},
"node_modules/@solana/options/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/options/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@solana/spl-token": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.14.tgz",
"integrity": "sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==",
"license": "Apache-2.0",
"dependencies": {
"@solana/buffer-layout": "^4.0.0",
"@solana/buffer-layout-utils": "^0.2.0",
"@solana/spl-token-group": "^0.0.7",
"@solana/spl-token-metadata": "^0.1.6",
"buffer": "^6.0.3"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@solana/web3.js": "^1.95.5"
}
},
"node_modules/@solana/spl-token-group": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz",
"integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==",
"license": "Apache-2.0",
"dependencies": {
"@solana/codecs": "2.0.0-rc.1"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@solana/web3.js": "^1.95.3"
}
},
"node_modules/@solana/spl-token-metadata": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz",
"integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==",
"license": "Apache-2.0",
"dependencies": {
"@solana/codecs": "2.0.0-rc.1"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@solana/web3.js": "^1.95.3"
}
},
"node_modules/@solana/web3.js": {
"version": "1.98.4",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
@@ -2838,6 +3322,24 @@
"superstruct": "^2.0.2"
}
},
"node_modules/@solana/web3.js/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/@solana/web3.js/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/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -4558,13 +5060,10 @@
"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"
}
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -4607,6 +5106,37 @@
"require-from-string": "^2.0.2"
}
},
"node_modules/bigint-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
"integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bindings": "^1.3.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bn.js": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
@@ -4624,6 +5154,24 @@
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/borsh/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/borsh/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/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -4683,12 +5231,12 @@
}
},
"node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
"base-x": "^5.0.0"
}
},
"node_modules/buffer": {
@@ -4715,6 +5263,15 @@
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-layout": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz",
"integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==",
"license": "MIT",
"engines": {
"node": ">=4.5"
}
},
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
@@ -4799,6 +5356,18 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camera-controls": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
@@ -5024,6 +5593,15 @@
"yarn": ">=1"
}
},
"node_modules/cross-fetch": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -6203,6 +6781,13 @@
"integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
"license": "MIT"
},
"node_modules/fastestsmallesttextencoderdecoder": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz",
"integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==",
"license": "CC0-1.0",
"peer": true
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -6232,6 +6817,12 @@
"node": ">=16.0.0"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -9333,6 +9924,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -10942,6 +11539,12 @@
"node": ">=8.0"
}
},
"node_modules/toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
@@ -11978,7 +12581,6 @@
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+3 -1
View File
@@ -19,6 +19,7 @@
},
"dependencies": {
"@noble/ed25519": "^3.0.0",
"@pump-fun/pump-sdk": "^1.32.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@solana/web3.js": "^1.98.4",
@@ -36,7 +37,8 @@
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.4.0",
"three": "^0.183.2",
"ws": "^8.18.3"
"ws": "^8.18.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@playwright/test": "^1.58.0",
@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { ZodError } from "zod";
import { normalizeLaunchDraft, cryptoLaunchPrepareSchema } from "@/features/crypto/lib/launchSchema";
import { prepareCryptoLaunch } from "@/features/crypto/server/launch/service";
export async function POST(request: Request) {
try {
const payload = cryptoLaunchPrepareSchema.parse(await request.json());
const prepared = await prepareCryptoLaunch({
draft: normalizeLaunchDraft(payload.draft),
creatorPublicKey: payload.creatorPublicKey,
});
return NextResponse.json({ prepared });
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{
error: error.issues[0]?.message ?? "Invalid launch payload.",
issues: error.issues,
},
{ status: 400 },
);
}
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Unable to prepare the Pump.fun launch.",
},
{ status: 502 },
);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { ZodError } from "zod";
import { cryptoLaunchSubmitSchema } from "@/features/crypto/lib/launchSchema";
import { submitCryptoLaunch } from "@/features/crypto/server/launch/service";
export async function POST(request: Request) {
try {
const payload = cryptoLaunchSubmitSchema.parse(await request.json());
const result = await submitCryptoLaunch(payload);
return NextResponse.json({ result });
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{
error: error.issues[0]?.message ?? "Invalid launch submission payload.",
issues: error.issues,
},
{ status: 400 },
);
}
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Unable to submit the Pump.fun launch.",
},
{ status: 502 },
);
}
}
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import { fetchJitoTipFloor } from "@/features/crypto/server/launch/service";
export async function GET() {
try {
const tipFloor = await fetchJitoTipFloor();
if (!tipFloor) {
return NextResponse.json({ error: "Tip floor unavailable." }, { status: 503 });
}
return NextResponse.json(tipFloor);
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Unable to load the Jito tip floor.",
},
{ status: 502 },
);
}
}
@@ -0,0 +1,356 @@
import {
executeServerSideLaunch,
executeWalletApprovedLaunch,
} from "@/features/crypto/lib/launchClient";
import {
buildInitialCryptoLaunchState,
loadCryptoLaunchState,
saveCryptoLaunchState,
} from "@/features/crypto/lib/launchStorage";
import {
getLaunchFieldLabel,
getMissingRequiredLaunchField,
isSkipAnswer,
normalizeLaunchDraft,
parseExecutionModeAnswer,
parseNetworkAnswer,
} from "@/features/crypto/lib/launchSchema";
import type { AgentState } from "@/features/agents/state/store";
import type {
CryptoLaunchDraft,
CryptoLaunchDraftState,
CryptoLaunchQuestionId,
} from "@/features/crypto/types";
type ChatInteractionDispatchAction =
| { type: "updateAgent"; agentId: string; patch: Partial<AgentState> }
| { type: "appendOutput"; agentId: string; line: string };
const QUESTION_ORDER: readonly CryptoLaunchQuestionId[] = [
"network",
"executionMode",
"name",
"symbol",
"description",
"logoUrl",
"website",
"twitter",
"telegram",
"discord",
"confirm",
] as const;
const START_RE =
/\b(create|launch|make)\b[\s\S]*\b(token|coin|memecoin|meme coin)\b/i;
const isAffirmative = (value: string) => /^(yes|y|launch|create it|ship it|do it|confirm)$/i.test(value.trim());
const isNegative = (value: string) => /^(no|n|cancel|stop|abort|not now)$/i.test(value.trim());
const wantsOpenRoom = (value: string) =>
/(open|show).*(crypto room|launch tab)|crypto room|manual/i.test(value.trim());
function getNextQuestion(current: CryptoLaunchQuestionId | null): CryptoLaunchQuestionId {
if (!current) return QUESTION_ORDER[0];
const index = QUESTION_ORDER.indexOf(current);
return QUESTION_ORDER[Math.min(index + 1, QUESTION_ORDER.length - 1)]!;
}
function buildQuestion(field: CryptoLaunchQuestionId): string {
switch (field) {
case "network":
return "Which network should I use: `devnet` or `mainnet`?";
case "executionMode":
return "Should I use `user-approved wallet` mode or `server-side` mode?";
case "name":
return "What is the token name?";
case "symbol":
return "What is the token symbol? Keep it under 10 characters.";
case "description":
return "What description should I publish for the token?";
case "logoUrl":
return "What is the logo URL? If you want to upload an image manually instead, say `open crypto room`.";
case "website":
return "What website should I attach? Reply with a URL or `skip`.";
case "twitter":
return "What Twitter/X link or handle should I attach? Reply with a value or `skip`.";
case "telegram":
return "What Telegram link or handle should I attach? Reply with a value or `skip`.";
case "discord":
return "What Discord invite or handle should I attach? Reply with a value or `skip`.";
case "confirm":
return "Everything is collected. Reply `launch` to create the token now, or say `open crypto room` if you want to review it manually first.";
}
}
function buildSummary(draft: CryptoLaunchDraft): string {
return [
"Token draft ready:",
`- Network: ${draft.network}`,
`- Mode: ${draft.executionMode === "server_side" ? "server-side" : "user-approved wallet"}`,
`- Name: ${draft.name}`,
`- Symbol: ${draft.symbol}`,
`- Description: ${draft.description}`,
`- Logo: ${draft.logoUrl}`,
`- Website: ${draft.website || "skip"}`,
`- Twitter/X: ${draft.twitter || "skip"}`,
`- Telegram: ${draft.telegram || "skip"}`,
`- Discord: ${draft.discord || "skip"}`,
].join("\n");
}
function persistState(state: CryptoLaunchDraftState) {
saveCryptoLaunchState(state);
}
function appendAssistant(dispatch: (action: ChatInteractionDispatchAction) => void, agentId: string, line: string) {
dispatch({ type: "appendOutput", agentId, line });
}
function updateDraftForAnswer(
field: CryptoLaunchQuestionId,
answer: string,
draft: CryptoLaunchDraft,
): { draft: CryptoLaunchDraft; error: string | null } {
const trimmed = answer.trim();
switch (field) {
case "network": {
const network = parseNetworkAnswer(trimmed);
if (!network) {
return { draft, error: "Please reply with `devnet` or `mainnet`." };
}
return { draft: { ...draft, network }, error: null };
}
case "executionMode": {
const executionMode = parseExecutionModeAnswer(trimmed);
if (!executionMode) {
return {
draft,
error: "Please reply with `user-approved wallet` or `server-side`.",
};
}
return { draft: { ...draft, executionMode }, error: null };
}
case "name":
return trimmed ? { draft: { ...draft, name: trimmed }, error: null } : { draft, error: "The token name cannot be empty." };
case "symbol":
return trimmed
? { draft: { ...draft, symbol: trimmed.toUpperCase() }, error: null }
: { draft, error: "The token symbol cannot be empty." };
case "description":
return trimmed
? { draft: { ...draft, description: trimmed }, error: null }
: { draft, error: "The token description cannot be empty." };
case "logoUrl":
if (!trimmed) {
return { draft, error: "A logo URL is required for the chat launch flow." };
}
return { draft: { ...draft, logoUrl: trimmed }, error: null };
case "website":
return { draft: { ...draft, website: isSkipAnswer(trimmed) ? "" : trimmed }, error: null };
case "twitter":
return { draft: { ...draft, twitter: isSkipAnswer(trimmed) ? "" : trimmed }, error: null };
case "telegram":
return { draft: { ...draft, telegram: isSkipAnswer(trimmed) ? "" : trimmed }, error: null };
case "discord":
return { draft: { ...draft, discord: isSkipAnswer(trimmed) ? "" : trimmed }, error: null };
case "confirm":
return { draft, error: null };
}
}
async function launchFromDraft(params: {
state: CryptoLaunchDraftState;
}) {
const draft = normalizeLaunchDraft(params.state.draft);
const outcome =
draft.executionMode === "server_side"
? await executeServerSideLaunch(draft)
: await executeWalletApprovedLaunch(draft);
const nextState: CryptoLaunchDraftState = {
...params.state,
draft: {
...draft,
creatorWallet:
draft.executionMode === "user_approved" ? outcome.result.creatorPublicKey : draft.creatorWallet,
},
conversation: {
active: false,
agentId: null,
awaitingField: null,
lastUpdatedAt: Date.now(),
},
lastPrepared: outcome.prepared,
lastResult: outcome.result,
};
persistState(nextState);
return outcome.result;
}
export async function maybeHandleCryptoLaunchChat(params: {
agentId: string;
message: string;
dispatch: (action: ChatInteractionDispatchAction) => void;
}): Promise<boolean> {
const trimmed = params.message.trim();
if (!trimmed) return false;
const existing = loadCryptoLaunchState();
const activeForAgent =
existing.conversation.active && existing.conversation.agentId === params.agentId;
const shouldStart = START_RE.test(trimmed);
if (!activeForAgent && !shouldStart) {
return false;
}
appendAssistant(params.dispatch, params.agentId, `> ${trimmed}`);
if (shouldStart && !activeForAgent) {
const nextState: CryptoLaunchDraftState = {
...buildInitialCryptoLaunchState(),
conversation: {
active: true,
agentId: params.agentId,
awaitingField: QUESTION_ORDER[0],
lastUpdatedAt: Date.now(),
},
lastPrepared: existing.lastPrepared,
lastResult: existing.lastResult,
};
persistState(nextState);
appendAssistant(
params.dispatch,
params.agentId,
`I can handle that. ${buildQuestion(QUESTION_ORDER[0])}`,
);
return true;
}
if (!activeForAgent) {
appendAssistant(
params.dispatch,
params.agentId,
"Another agent already owns the active token launch draft. Open the crypto room to review it or cancel that flow first.",
);
return true;
}
if (isNegative(trimmed)) {
const nextState: CryptoLaunchDraftState = {
...existing,
conversation: {
active: false,
agentId: null,
awaitingField: null,
lastUpdatedAt: Date.now(),
},
};
persistState(nextState);
appendAssistant(
params.dispatch,
params.agentId,
"Token launch cancelled. The draft stays available in the crypto room if you want to resume manually.",
);
return true;
}
const awaitingField = existing.conversation.awaitingField ?? QUESTION_ORDER[0];
if (awaitingField === "confirm") {
if (wantsOpenRoom(trimmed)) {
persistState({
...existing,
conversation: {
...existing.conversation,
active: false,
agentId: null,
awaitingField: null,
lastUpdatedAt: Date.now(),
},
});
appendAssistant(
params.dispatch,
params.agentId,
"The launch draft is ready in the crypto room Launch tab. Review it there and launch whenever you want.",
);
return true;
}
if (!isAffirmative(trimmed)) {
appendAssistant(
params.dispatch,
params.agentId,
"Reply `launch` to create the token now, or say `open crypto room` to finish it manually.",
);
return true;
}
try {
const result = await launchFromDraft({ state: existing });
appendAssistant(
params.dispatch,
params.agentId,
[
`Token created on ${result.network}.`,
`Mint: ${result.mintAddress}`,
`Creator: ${result.creatorPublicKey}`,
`Transaction: ${result.explorerTxUrl}`,
].join("\n"),
);
} catch (error) {
appendAssistant(
params.dispatch,
params.agentId,
error instanceof Error
? `Launch failed: ${error.message}`
: "Launch failed. Try again or open the crypto room to review the draft.",
);
}
return true;
}
if (awaitingField === "logoUrl" && wantsOpenRoom(trimmed)) {
appendAssistant(
params.dispatch,
params.agentId,
"The draft is waiting in the crypto room Launch tab. Upload the logo there, then finish the launch manually or come back here and say `launch`.",
);
return true;
}
const nextDraft = updateDraftForAnswer(awaitingField, trimmed, normalizeLaunchDraft(existing.draft));
if (nextDraft.error) {
appendAssistant(params.dispatch, params.agentId, nextDraft.error);
return true;
}
const candidateNextField = getNextQuestion(awaitingField);
const nextState: CryptoLaunchDraftState = {
...existing,
draft: nextDraft.draft,
conversation: {
active: true,
agentId: params.agentId,
awaitingField: candidateNextField,
lastUpdatedAt: Date.now(),
},
};
if (candidateNextField === "confirm") {
const missingField = getMissingRequiredLaunchField(nextState.draft);
if (missingField) {
nextState.conversation.awaitingField = missingField;
persistState(nextState);
appendAssistant(
params.dispatch,
params.agentId,
`I still need the ${getLaunchFieldLabel(missingField)}. ${buildQuestion(missingField)}`,
);
return true;
}
persistState(nextState);
appendAssistant(params.dispatch, params.agentId, buildSummary(nextState.draft));
appendAssistant(params.dispatch, params.agentId, buildQuestion("confirm"));
return true;
}
persistState(nextState);
appendAssistant(params.dispatch, params.agentId, buildQuestion(candidateNextField));
return true;
}
@@ -7,6 +7,7 @@ import {
planNewSessionIntent,
planStopRunIntent,
} from "@/features/agents/operations/chatInteractionWorkflow";
import { maybeHandleCryptoLaunchChat } from "@/features/agents/operations/cryptoLaunchChat";
import { sendChatMessageViaStudio } from "@/features/agents/operations/chatSendOperation";
import { mergePendingLivePatch } from "@/features/agents/state/livePatchQueue";
import { buildNewSessionAgentPatch, type AgentState } from "@/features/agents/state/store";
@@ -207,6 +208,20 @@ export function useChatInteractionController(
});
return;
}
if (
await maybeHandleCryptoLaunchChat({
agentId,
message: trimmed,
dispatch: params.dispatch,
})
) {
params.dispatch({
type: "updateAgent",
agentId,
patch: { draft: "", lastActivityAt: Date.now() },
});
return;
}
if (agent.status === "running") {
params.dispatch({
type: "enqueueQueuedMessage",
@@ -12,12 +12,14 @@ import {
Sparkles,
Wallet,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useEffect, 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 { useCryptoLaunchState } from "@/features/crypto/hooks/useCryptoLaunchState";
import { getLaunchFieldLabel, getMissingRequiredLaunchField } from "@/features/crypto/lib/launchSchema";
import type { CryptoAgentTradeMode, CryptoTrackedPair } from "@/features/crypto/types";
import type { OfficeAgent } from "@/features/retro-office/core/types";
@@ -38,7 +40,7 @@ const number = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 4,
});
type TabKey = "market" | "trade" | "ledger" | "agents";
type TabKey = "market" | "trade" | "ledger" | "agents" | "launch";
const DEXSCREENER_SOLANA_URL_RE =
/^https?:\/\/(?:www\.)?dexscreener\.com\/solana\/([a-zA-Z0-9]{16,128})(?:[/?#].*)?$/i;
@@ -79,6 +81,7 @@ export function CryptoImmersiveScreen({ agents }: { agents: OfficeAgent[] }) {
const [browsedPairLoading, setBrowsedPairLoading] = useState(false);
const [browsedPairError, setBrowsedPairError] = useState<string | null>(null);
const state = useCryptoRoomState(agents);
const launch = useCryptoLaunchState();
const pendingApprovals = useMemo(
() => state.approvals.filter((approval) => approval.status === "pending"),
@@ -380,6 +383,11 @@ export function CryptoImmersiveScreen({ agents }: { agents: OfficeAgent[] }) {
label: "Agents",
icon: <Bot className="h-4 w-4" />,
},
{
key: "launch" as const,
label: "Launch",
icon: <Sparkles className="h-4 w-4" />,
},
].map((tab) => (
<button
key={tab.key}
@@ -416,6 +424,8 @@ export function CryptoImmersiveScreen({ agents }: { agents: OfficeAgent[] }) {
<TradeTab state={state} pendingApprovals={pendingApprovals} />
) : activeTab === "ledger" ? (
<LedgerTab state={state} />
) : activeTab === "launch" ? (
<LaunchTab launch={launch} />
) : (
<AgentsTab state={state} />
)}
@@ -425,6 +435,20 @@ export function CryptoImmersiveScreen({ agents }: { agents: OfficeAgent[] }) {
);
}
const readFileAsDataUrl = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error("Unable to read the selected image file."));
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
return;
}
reject(new Error("Unable to read the selected image file."));
};
reader.readAsDataURL(file);
});
function MarketTab({
state,
iframeSrc,
@@ -620,6 +644,363 @@ function MarketTab({
);
}
function LaunchTab({
launch,
}: {
launch: ReturnType<typeof useCryptoLaunchState>;
}) {
const [tipFloor, setTipFloor] = useState<number | null>(null);
const [tipFloorError, setTipFloorError] = useState<string | null>(null);
const missingField = getMissingRequiredLaunchField(launch.draft);
const effectiveTipFloor = launch.draft.network === "mainnet" ? tipFloor : null;
const effectiveTipFloorError =
launch.draft.network === "mainnet" ? tipFloorError : null;
useEffect(() => {
if (launch.draft.network !== "mainnet") {
return;
}
let cancelled = false;
void fetch("/api/crypto/launch/tip-floor", { cache: "no-store" })
.then(async (response) => {
const payload = (await response.json().catch(() => null)) as {
landed_tips_50th_percentile?: number;
error?: string;
} | null;
if (!response.ok) {
throw new Error(payload?.error?.trim() || "Unable to load the Jito tip floor.");
}
if (!cancelled) {
setTipFloor(payload?.landed_tips_50th_percentile ?? null);
setTipFloorError(null);
}
})
.catch((error) => {
if (!cancelled) {
setTipFloor(null);
setTipFloorError(error instanceof Error ? error.message : "Unable to load the Jito tip floor.");
}
});
return () => {
cancelled = true;
};
}, [launch.draft.network]);
return (
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1.15fr)_420px]">
<SectionCard
title="Pump.fun launcher"
subtitle="Build the token metadata here, choose how execution works, then launch from the same draft the chat flow uses."
action={
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={launch.resetDraft}
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"
>
Reset draft
</button>
<button
type="button"
onClick={() => {
launch.clearLaunchError();
void launch.submitLaunch().catch(() => {});
}}
disabled={launch.launchBusy || Boolean(missingField)}
className="rounded-full border border-cyan-300/25 bg-cyan-300/12 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-cyan-100 transition-colors hover:bg-cyan-300/16 disabled:cursor-not-allowed disabled:opacity-50"
>
{launch.launchBusy ? "Launching" : "Launch token"}
</button>
</div>
}
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<LabeledField label="Network">
<select
value={launch.draft.network}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
network: event.target.value === "mainnet" ? "mainnet" : "devnet",
}))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors focus:border-cyan-300/35"
>
<option value="devnet">Devnet</option>
<option value="mainnet">Mainnet</option>
</select>
</LabeledField>
<LabeledField label="Execution mode">
<select
value={launch.draft.executionMode}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
executionMode:
event.target.value === "server_side" ? "server_side" : "user_approved",
}))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors focus:border-cyan-300/35"
>
<option value="user_approved">User-approved wallet</option>
<option value="server_side">Server-side signer</option>
</select>
</LabeledField>
<LabeledField label="Token name">
<input
value={launch.draft.name}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, name: event.target.value }))
}
maxLength={32}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="Claw Coin"
/>
</LabeledField>
<LabeledField label="Token symbol">
<input
value={launch.draft.symbol}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
symbol: event.target.value.toUpperCase(),
}))
}
maxLength={10}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] uppercase text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="CLAW"
/>
</LabeledField>
</div>
<div className="mt-4">
<LabeledField label="Description">
<textarea
value={launch.draft.description}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
description: event.target.value,
}))
}
rows={5}
maxLength={280}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="Tell the story behind the token."
/>
</LabeledField>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<LabeledField label="Logo URL">
<input
value={launch.draft.logoUrl}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, logoUrl: event.target.value }))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="https://..."
/>
</LabeledField>
<LabeledField label="Upload logo">
<input
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={(event) => {
const file = event.target.files?.[0];
if (!file) return;
void readFileAsDataUrl(file)
.then((logoUrl) => {
launch.setDraft((current) => ({ ...current, logoUrl }));
})
.catch(() => {});
}}
className="block w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white file:mr-3 file:rounded-full file:border-0 file:bg-cyan-300/14 file:px-3 file:py-1 file:text-[11px] file:font-semibold file:uppercase file:tracking-[0.14em] file:text-cyan-100"
/>
</LabeledField>
<LabeledField label="Website">
<input
value={launch.draft.website}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, website: event.target.value }))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="https://..."
/>
</LabeledField>
<LabeledField label="Twitter/X">
<input
value={launch.draft.twitter}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, twitter: event.target.value }))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="https://x.com/..."
/>
</LabeledField>
<LabeledField label="Telegram">
<input
value={launch.draft.telegram}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, telegram: event.target.value }))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="https://t.me/..."
/>
</LabeledField>
<LabeledField label="Discord">
<input
value={launch.draft.discord}
onChange={(event) =>
launch.setDraft((current) => ({ ...current, discord: event.target.value }))
}
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors placeholder:text-white/30 focus:border-cyan-300/35"
placeholder="https://discord.gg/..."
/>
</LabeledField>
<LabeledField label="Priority fee (SOL)">
<input
value={launch.draft.priorityFeeSol}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
priorityFeeSol: Number(event.target.value) || 0,
}))
}
inputMode="decimal"
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors focus:border-cyan-300/35"
/>
</LabeledField>
<LabeledField label="Compute units">
<input
value={launch.draft.computeUnitLimit}
onChange={(event) =>
launch.setDraft((current) => ({
...current,
computeUnitLimit: Number(event.target.value) || 0,
}))
}
inputMode="numeric"
className="w-full rounded-2xl border border-white/12 bg-[#071019] px-4 py-3 text-[13px] text-white outline-none transition-colors focus:border-cyan-300/35"
/>
</LabeledField>
</div>
</SectionCard>
<div className="space-y-6">
<SectionCard
title="Launch review"
subtitle="Required fields, runtime notes, and the last prepared or submitted launch."
>
<div className="space-y-3">
{launch.conversation.active ? (
<Banner tone="neutral">
An agent is currently building this token draft and is waiting for{" "}
{launch.conversation.awaitingField
? getLaunchFieldLabel(launch.conversation.awaitingField)
: "confirmation"}
.
</Banner>
) : null}
{missingField ? (
<Banner tone="danger">
Fill in the {getLaunchFieldLabel(missingField)} field before launching.
</Banner>
) : (
<Banner tone="success">
The required token fields are complete and ready for launch review.
</Banner>
)}
{launch.draft.executionMode === "user_approved" ? (
<Banner tone="neutral">
Wallet-approved launches will connect to Phantom, prepare the Pump.fun transaction, and ask for an explicit signature before submission.
</Banner>
) : (
<Banner tone="danger">
Server-side launches require `PUMPFUN_SERVER_SECRET_KEY` on the server and will use that wallet as the token creator.
</Banner>
)}
{launch.draft.network === "mainnet" ? (
effectiveTipFloor !== null ? (
<Banner tone="neutral">
Current Jito p50 tip floor: {effectiveTipFloor.toFixed(6)} SOL.
</Banner>
) : effectiveTipFloorError ? (
<Banner tone="danger">{effectiveTipFloorError}</Banner>
) : null
) : (
<Banner tone="neutral">
Devnet is the safest place to validate the full flow before launching on mainnet.
</Banner>
)}
{launch.launchError ? <Banner tone="danger">{launch.launchError}</Banner> : null}
</div>
<div className="mt-4 space-y-2">
<StatRow label="Mode" value={launch.draft.executionMode === "server_side" ? "Server-side" : "User-approved"} />
<StatRow label="Network" value={launch.draft.network} />
<StatRow
label="Creator wallet"
value={launch.lastResult?.creatorPublicKey || launch.draft.creatorWallet || "Resolved at launch time"}
/>
<StatRow label="Token name" value={launch.draft.name || "Pending"} />
<StatRow label="Ticker" value={launch.draft.symbol || "Pending"} />
</div>
</SectionCard>
<SectionCard
title="Latest launch"
subtitle="This reflects the most recent prepared or submitted token launch from this browser."
>
{launch.lastResult ? (
<div className="space-y-3">
<Banner tone="success">
Launch submitted for {launch.lastResult.mintAddress}. You can review the mint and transaction links below.
</Banner>
<StatRow label="Mint address" value={shorten(launch.lastResult.mintAddress)} />
<StatRow label="Creator" value={shorten(launch.lastResult.creatorPublicKey)} />
<StatRow label="Submitted" value={new Date(launch.lastResult.submittedAt).toLocaleString()} />
<a
href={launch.lastResult.explorerTokenUrl}
target="_blank"
rel="noreferrer"
className="block rounded-[16px] border border-white/10 bg-white/[0.03] px-3 py-2 text-[12px] text-cyan-100 transition-colors hover:bg-white/[0.06]"
>
Open token explorer
</a>
<a
href={launch.lastResult.explorerTxUrl}
target="_blank"
rel="noreferrer"
className="block rounded-[16px] border border-white/10 bg-white/[0.03] px-3 py-2 text-[12px] text-cyan-100 transition-colors hover:bg-white/[0.06]"
>
Open transaction explorer
</a>
</div>
) : launch.lastPrepared ? (
<div className="space-y-3">
<Banner tone="neutral">
The last prepared launch is waiting for{" "}
{launch.lastPrepared.executionMode === "server_side"
? "server-side submission"
: "wallet approval"}
.
</Banner>
<StatRow label="Mint address" value={shorten(launch.lastPrepared.mintAddress)} />
<StatRow label="Metadata URI" value={shorten(launch.lastPrepared.metadataUri)} />
<StatRow
label="Expires"
value={new Date(launch.lastPrepared.expiresAt).toLocaleTimeString()}
/>
</div>
) : (
<Banner tone="neutral">
No Pump.fun token has been prepared in this browser yet.
</Banner>
)}
</SectionCard>
</div>
</div>
);
}
function TradeTab({
state,
pendingApprovals,
@@ -0,0 +1,109 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import {
buildInitialCryptoLaunchState,
loadCryptoLaunchState,
saveCryptoLaunchState,
subscribeToCryptoLaunchUpdates,
} from "@/features/crypto/lib/launchStorage";
import { executeServerSideLaunch, executeWalletApprovedLaunch } from "@/features/crypto/lib/launchClient";
import { normalizeLaunchDraft } from "@/features/crypto/lib/launchSchema";
import type { CryptoLaunchDraft, CryptoLaunchDraftState } from "@/features/crypto/types";
export function useCryptoLaunchState() {
const [launchState, setLaunchState] = useState<CryptoLaunchDraftState>(() => loadCryptoLaunchState());
const [launchBusy, setLaunchBusy] = useState(false);
const [launchError, setLaunchError] = useState<string | null>(null);
useEffect(() => subscribeToCryptoLaunchUpdates(() => setLaunchState(loadCryptoLaunchState())), []);
const persistState = useCallback((next: CryptoLaunchDraftState) => {
setLaunchState(next);
saveCryptoLaunchState(next);
}, []);
const setDraft = useCallback(
(updater: CryptoLaunchDraft | ((draft: CryptoLaunchDraft) => CryptoLaunchDraft)) => {
persistState({
...launchState,
draft:
typeof updater === "function"
? normalizeLaunchDraft((updater as (draft: CryptoLaunchDraft) => CryptoLaunchDraft)(launchState.draft))
: normalizeLaunchDraft(updater),
});
},
[launchState, persistState],
);
const setConversation = useCallback(
(conversation: CryptoLaunchDraftState["conversation"]) => {
persistState({
...launchState,
conversation,
});
},
[launchState, persistState],
);
const resetDraft = useCallback(() => {
persistState({
...buildInitialCryptoLaunchState(),
lastPrepared: launchState.lastPrepared,
lastResult: launchState.lastResult,
});
setLaunchError(null);
}, [launchState.lastPrepared, launchState.lastResult, persistState]);
const submitLaunch = useCallback(async () => {
setLaunchBusy(true);
setLaunchError(null);
try {
const draft = normalizeLaunchDraft(launchState.draft);
const outcome =
draft.executionMode === "server_side"
? await executeServerSideLaunch(draft)
: await executeWalletApprovedLaunch(draft);
const nextState: CryptoLaunchDraftState = {
...launchState,
draft: {
...draft,
creatorWallet:
draft.executionMode === "user_approved" ? outcome.result.creatorPublicKey : draft.creatorWallet,
},
conversation: {
active: false,
agentId: null,
awaitingField: null,
lastUpdatedAt: Date.now(),
},
lastPrepared: outcome.prepared,
lastResult: outcome.result,
};
persistState(nextState);
return outcome.result;
} catch (error) {
const message =
error instanceof Error ? error.message : "Unable to complete the Pump.fun launch.";
setLaunchError(message);
throw error;
} finally {
setLaunchBusy(false);
}
}, [launchState, persistState]);
return {
launchState,
draft: launchState.draft,
conversation: launchState.conversation,
lastPrepared: launchState.lastPrepared,
lastResult: launchState.lastResult,
launchBusy,
launchError,
setDraft,
setConversation,
resetDraft,
submitLaunch,
clearLaunchError: () => setLaunchError(null),
};
}
+138
View File
@@ -0,0 +1,138 @@
import type { CryptoLaunchDraft, CryptoLaunchPrepared, CryptoLaunchResult } from "@/features/crypto/types";
import { getPhantomProvider } from "@/features/crypto/lib/solana";
function deserializeTransaction(base64Transaction: string) {
return Uint8Array.from(atob(base64Transaction), (char) => char.charCodeAt(0));
}
function serializeSignedTransaction(bytes: Uint8Array): string {
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
async function readJson(response: Response) {
const payload = (await response.json().catch(() => null)) as
| { error?: string; prepared?: CryptoLaunchPrepared; result?: CryptoLaunchResult }
| null;
if (!response.ok) {
throw new Error(payload?.error?.trim() || `Request failed with status ${response.status}.`);
}
return payload;
}
function isPhantomUnauthorizedError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /not been authorized|not authorized by the user|unauthorized/i.test(message);
}
function isPhantomUserRejection(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /user rejected|user denied|cancelled|canceled/i.test(message);
}
async function connectAuthorizedPhantom(provider: NonNullable<ReturnType<typeof getPhantomProvider>>) {
try {
return await provider.connect();
} catch (error) {
if (!isPhantomUnauthorizedError(error)) {
throw error;
}
await provider.disconnect().catch(() => {});
return provider.connect();
}
}
export async function prepareCryptoLaunch(
draft: CryptoLaunchDraft,
creatorPublicKey?: string,
): Promise<CryptoLaunchPrepared> {
const response = await fetch("/api/crypto/launch/prepare", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
draft,
creatorPublicKey,
}),
});
const payload = await readJson(response);
if (!payload?.prepared) {
throw new Error("The launch preparation response was incomplete.");
}
return payload.prepared;
}
export async function submitCryptoLaunch(params: {
launchId: string;
executionMode: CryptoLaunchDraft["executionMode"];
signedTransaction?: string;
}): Promise<CryptoLaunchResult> {
const response = await fetch("/api/crypto/launch/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
});
const payload = await readJson(response);
if (!payload?.result) {
throw new Error("The launch submission response was incomplete.");
}
return payload.result;
}
export async function executeWalletApprovedLaunch(
draft: CryptoLaunchDraft,
): Promise<{ prepared: CryptoLaunchPrepared; result: CryptoLaunchResult; creatorPublicKey: string }> {
const provider = getPhantomProvider();
if (!provider) {
throw new Error("Phantom was not detected in this browser.");
}
const connected = provider.publicKey
? { publicKey: provider.publicKey }
: await connectAuthorizedPhantom(provider);
const creatorPublicKey = connected.publicKey.toBase58();
const prepared = await prepareCryptoLaunch(draft, creatorPublicKey);
if (!prepared.serializedTransaction) {
throw new Error("The launch is missing a transaction to sign.");
}
const mod = await import("@solana/web3.js");
const transaction = mod.VersionedTransaction.deserialize(
deserializeTransaction(prepared.serializedTransaction),
);
let signed: InstanceType<typeof mod.VersionedTransaction>;
try {
signed = await provider.signTransaction(transaction);
} catch (error) {
if (isPhantomUnauthorizedError(error)) {
await provider.disconnect().catch(() => {});
throw new Error(
"Phantom is no longer authorized for this site. Reconnect your wallet and try launching again.",
);
}
if (isPhantomUserRejection(error)) {
await provider.disconnect().catch(() => {});
throw new Error(
"Phantom approval was cancelled. Reconnect your wallet and try launching again.",
);
}
throw error;
}
const result = await submitCryptoLaunch({
launchId: prepared.launchId,
executionMode: "user_approved",
signedTransaction: serializeSignedTransaction(signed.serialize()),
});
return { prepared, result, creatorPublicKey };
}
export async function executeServerSideLaunch(
draft: CryptoLaunchDraft,
): Promise<{ prepared: CryptoLaunchPrepared; result: CryptoLaunchResult }> {
const prepared = await prepareCryptoLaunch(draft);
const result = await submitCryptoLaunch({
launchId: prepared.launchId,
executionMode: "server_side",
});
return { prepared, result };
}
+154
View File
@@ -0,0 +1,154 @@
import { z } from "zod";
import type {
CryptoLaunchDraft,
CryptoLaunchExecutionMode,
CryptoLaunchQuestionId,
} from "@/features/crypto/types";
const dataUrlSchema = z.string().regex(/^data:[^;]+;base64,/i, "Invalid data URL.");
const urlSchema = z.string().url();
const optionalUrlSchema = z.union([z.literal(""), urlSchema]);
const optionalHandleSchema = z.string().trim().max(240);
const optionalAssetSchema = z.union([z.literal(""), urlSchema, dataUrlSchema]);
export const CRYPTO_LAUNCH_REQUIRED_FIELDS: readonly CryptoLaunchQuestionId[] = [
"network",
"executionMode",
"name",
"symbol",
"description",
"logoUrl",
] as const;
export const cryptoLaunchDraftSchema = z.object({
network: z.enum(["devnet", "mainnet"]),
executionMode: z.enum(["user_approved", "server_side"]),
name: z.string().trim().min(1).max(32),
symbol: z
.string()
.trim()
.min(1)
.max(10)
.regex(/^[A-Za-z0-9]+$/, "Token symbol must be letters and numbers only."),
description: z.string().trim().min(1).max(280),
logoUrl: optionalAssetSchema,
website: optionalUrlSchema,
twitter: optionalHandleSchema,
telegram: optionalHandleSchema,
discord: optionalHandleSchema,
creatorWallet: z.string().trim().default(""),
priorityFeeSol: z.number().min(0).max(0.1),
computeUnitLimit: z.number().int().min(200_000).max(1_600_000),
});
export const cryptoLaunchPrepareSchema = z
.object({
draft: cryptoLaunchDraftSchema,
creatorPublicKey: z.string().trim().optional(),
})
.superRefine((payload, ctx) => {
if (payload.draft.executionMode === "user_approved" && !payload.creatorPublicKey?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "A connected wallet is required for user-approved launches.",
path: ["creatorPublicKey"],
});
}
});
export const cryptoLaunchSubmitSchema = z
.object({
launchId: z.string().trim().min(1),
executionMode: z.enum(["user_approved", "server_side"]),
signedTransaction: z.string().trim().optional(),
})
.superRefine((payload, ctx) => {
if (payload.executionMode === "user_approved" && !payload.signedTransaction?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "A signed transaction is required for wallet-approved launches.",
path: ["signedTransaction"],
});
}
});
export const normalizeLaunchDraft = (draft: Partial<CryptoLaunchDraft> | null | undefined): CryptoLaunchDraft => ({
network: draft?.network === "mainnet" ? "mainnet" : "devnet",
executionMode: draft?.executionMode === "server_side" ? "server_side" : "user_approved",
name: draft?.name?.trim() ?? "",
symbol: draft?.symbol?.trim().toUpperCase() ?? "",
description: draft?.description?.trim() ?? "",
logoUrl: draft?.logoUrl?.trim() ?? "",
website: draft?.website?.trim() ?? "",
twitter: draft?.twitter?.trim() ?? "",
telegram: draft?.telegram?.trim() ?? "",
discord: draft?.discord?.trim() ?? "",
creatorWallet: draft?.creatorWallet?.trim() ?? "",
priorityFeeSol:
typeof draft?.priorityFeeSol === "number" && Number.isFinite(draft.priorityFeeSol)
? draft.priorityFeeSol
: 0.0005,
computeUnitLimit:
typeof draft?.computeUnitLimit === "number" && Number.isFinite(draft.computeUnitLimit)
? Math.trunc(draft.computeUnitLimit)
: 500_000,
});
const FIELD_LABELS: Record<CryptoLaunchQuestionId, string> = {
network: "network",
executionMode: "execution mode",
name: "token name",
symbol: "token symbol",
description: "token description",
logoUrl: "logo URL",
website: "website URL",
twitter: "Twitter/X URL",
telegram: "Telegram URL",
discord: "Discord URL",
confirm: "launch confirmation",
};
export const getLaunchFieldLabel = (field: CryptoLaunchQuestionId): string => FIELD_LABELS[field];
export const getMissingRequiredLaunchField = (
draft: CryptoLaunchDraft,
): Exclude<CryptoLaunchQuestionId, "confirm"> | null => {
for (const field of CRYPTO_LAUNCH_REQUIRED_FIELDS) {
const value = draft[field as keyof CryptoLaunchDraft];
if (typeof value !== "string" || value.trim().length === 0) {
return field as Exclude<CryptoLaunchQuestionId, "confirm">;
}
}
return null;
};
export const isSkipAnswer = (value: string): boolean => /^(skip|none|n\/a|na)$/i.test(value.trim());
export const parseNetworkAnswer = (value: string): CryptoLaunchDraft["network"] | null => {
const normalized = value.trim().toLowerCase();
if (normalized.includes("main")) return "mainnet";
if (normalized.includes("dev")) return "devnet";
return null;
};
export const parseExecutionModeAnswer = (
value: string,
): CryptoLaunchExecutionMode | null => {
const normalized = value.trim().toLowerCase();
if (
normalized.includes("server") ||
normalized.includes("automatic") ||
normalized.includes("automated")
) {
return "server_side";
}
if (
normalized.includes("wallet") ||
normalized.includes("approve") ||
normalized.includes("phantom") ||
normalized.includes("manual")
) {
return "user_approved";
}
return null;
};
+96
View File
@@ -0,0 +1,96 @@
import type {
CryptoLaunchConversationState,
CryptoLaunchDraftState,
CryptoLaunchPrepared,
CryptoLaunchResult,
} from "@/features/crypto/types";
import { normalizeLaunchDraft } from "@/features/crypto/lib/launchSchema";
const CRYPTO_LAUNCH_STORAGE_KEY = "openclaw-crypto-launch-v1";
const CRYPTO_LAUNCH_EVENT = "openclaw:crypto-launch-updated";
const buildDefaultConversation = (): CryptoLaunchConversationState => ({
active: false,
agentId: null,
awaitingField: null,
lastUpdatedAt: null,
});
export const buildInitialCryptoLaunchState = (): CryptoLaunchDraftState => ({
draft: normalizeLaunchDraft(null),
conversation: buildDefaultConversation(),
lastPrepared: null,
lastResult: null,
});
const canUseDom = () => typeof window !== "undefined";
export const loadCryptoLaunchState = (): CryptoLaunchDraftState => {
if (!canUseDom()) return buildInitialCryptoLaunchState();
try {
const raw = window.localStorage.getItem(CRYPTO_LAUNCH_STORAGE_KEY);
if (!raw) return buildInitialCryptoLaunchState();
const parsed = JSON.parse(raw) as Partial<CryptoLaunchDraftState> | null;
if (!parsed || typeof parsed !== "object") return buildInitialCryptoLaunchState();
const fallback = buildInitialCryptoLaunchState();
return {
draft: normalizeLaunchDraft(parsed.draft),
conversation:
parsed.conversation && typeof parsed.conversation === "object"
? {
active: Boolean(parsed.conversation.active),
agentId:
typeof parsed.conversation.agentId === "string"
? parsed.conversation.agentId
: null,
awaitingField:
typeof parsed.conversation.awaitingField === "string"
? parsed.conversation.awaitingField
: null,
lastUpdatedAt:
typeof parsed.conversation.lastUpdatedAt === "number"
? parsed.conversation.lastUpdatedAt
: null,
}
: fallback.conversation,
lastPrepared:
parsed.lastPrepared && typeof parsed.lastPrepared === "object"
? (parsed.lastPrepared as CryptoLaunchPrepared)
: null,
lastResult:
parsed.lastResult && typeof parsed.lastResult === "object"
? (parsed.lastResult as CryptoLaunchResult)
: null,
};
} catch {
return buildInitialCryptoLaunchState();
}
};
const emitCryptoLaunchUpdate = () => {
if (!canUseDom()) return;
window.dispatchEvent(new CustomEvent(CRYPTO_LAUNCH_EVENT));
};
export const saveCryptoLaunchState = (state: CryptoLaunchDraftState) => {
if (!canUseDom()) return;
try {
window.localStorage.setItem(CRYPTO_LAUNCH_STORAGE_KEY, JSON.stringify(state));
emitCryptoLaunchUpdate();
} catch {
/* ignore */
}
};
export const subscribeToCryptoLaunchUpdates = (listener: () => void) => {
if (!canUseDom()) return () => {};
const handleStorage = (event: StorageEvent) => {
if (event.key === CRYPTO_LAUNCH_STORAGE_KEY) listener();
};
window.addEventListener("storage", handleStorage);
window.addEventListener(CRYPTO_LAUNCH_EVENT, listener);
return () => {
window.removeEventListener("storage", handleStorage);
window.removeEventListener(CRYPTO_LAUNCH_EVENT, listener);
};
};
@@ -0,0 +1,398 @@
import { randomUUID } from "node:crypto";
import {
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import type {
CryptoLaunchDraft,
CryptoLaunchExecutionMode,
CryptoLaunchNetwork,
CryptoLaunchPrepared,
CryptoLaunchResult,
} from "@/features/crypto/types";
const PUMP_IPFS_ENDPOINT = "https://pump.fun/api/ipfs";
const PREPARED_LAUNCH_TTL_MS = 5 * 60 * 1000;
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
type NetworkPreset = {
rpcUrl: string;
explorerBaseUrl: string;
explorerCluster: string | null;
};
type PreparedLaunchContext = {
prepared: CryptoLaunchPrepared;
draft: CryptoLaunchDraft;
serializedTransaction: string;
blockhash: string;
lastValidBlockHeight: number;
};
const preparedLaunches = new Map<string, PreparedLaunchContext>();
let cachedPumpSdk:
| {
createV2Instruction: (params: {
mint: PublicKey;
name: string;
symbol: string;
uri: string;
creator: PublicKey;
user: PublicKey;
mayhemMode: boolean;
cashback: boolean;
}) => Promise<import("@solana/web3.js").TransactionInstruction>;
extendAccountInstruction: (params: {
account: PublicKey;
user: PublicKey;
}) => Promise<import("@solana/web3.js").TransactionInstruction>;
}
| null = null;
async function getPumpSdk() {
if (!cachedPumpSdk) {
const mod = await import("@pump-fun/pump-sdk");
cachedPumpSdk = mod.PUMP_SDK;
}
return cachedPumpSdk;
}
function bondingCurvePda(mint: PublicKey): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("bonding-curve"), mint.toBuffer()],
new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"),
)[0];
}
function getNetworkPreset(network: CryptoLaunchNetwork): NetworkPreset {
if (network === "mainnet") {
return {
rpcUrl:
process.env.HELIUS_MAINNET_RPC_URL ??
process.env.NEXT_PUBLIC_HELIUS_MAINNET_RPC_URL ??
process.env.NEXT_PUBLIC_SOLANA_RPC_URL ??
"https://api.mainnet-beta.solana.com",
explorerBaseUrl: "https://solscan.io",
explorerCluster: null,
};
}
return {
rpcUrl:
process.env.HELIUS_DEVNET_RPC_URL ??
process.env.NEXT_PUBLIC_HELIUS_DEVNET_RPC_URL ??
"https://api.devnet.solana.com",
explorerBaseUrl: "https://solscan.io",
explorerCluster: "devnet",
};
}
function buildExplorerUrl(baseUrl: string, kind: "token" | "tx", value: string, cluster: string | null) {
const path = kind === "token" ? `token/${value}` : `tx/${value}`;
if (!cluster) return `${baseUrl}/${path}`;
return `${baseUrl}/${path}?cluster=${cluster}`;
}
function dataUriToBlob(dataUri: string): { blob: Blob; mimeType: string } {
const match = dataUri.match(/^data:([^;]+);base64,([\s\S]+)$/);
if (!match) {
throw new Error("Invalid token logo data URL.");
}
const mimeType = match[1]!;
const base64 = match[2]!;
const bytes = Buffer.from(base64, "base64");
return { blob: new Blob([bytes], { type: mimeType }), mimeType };
}
function decodeBase58(value: string): Uint8Array {
const bytes = [0];
for (const char of value) {
const charIndex = BASE58_ALPHABET.indexOf(char);
if (charIndex < 0) {
throw new Error("PUMPFUN_SERVER_SECRET_KEY is not a valid base58 string.");
}
let carry = charIndex;
for (let index = 0; index < bytes.length; index += 1) {
carry += bytes[index]! * 58;
bytes[index] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
for (const char of value) {
if (char !== "1") break;
bytes.push(0);
}
return Uint8Array.from(bytes.reverse());
}
async function uploadMetadata(draft: CryptoLaunchDraft): Promise<string> {
const formData = new FormData();
if (draft.logoUrl.startsWith("data:")) {
const { blob, mimeType } = dataUriToBlob(draft.logoUrl);
const ext = mimeType === "image/png" ? "png" : "jpg";
formData.append("file", blob, `token-logo.${ext}`);
} else if (draft.logoUrl) {
const imageResponse = await fetch(draft.logoUrl);
if (!imageResponse.ok) {
throw new Error(`Failed to fetch the token logo from ${draft.logoUrl}.`);
}
formData.append("file", await imageResponse.blob(), "token-logo.png");
}
formData.append("name", draft.name);
formData.append("symbol", draft.symbol);
formData.append("description", draft.description);
formData.append("showName", "true");
if (draft.twitter) formData.append("twitter", draft.twitter);
if (draft.telegram) formData.append("telegram", draft.telegram);
if (draft.website) formData.append("website", draft.website);
if (draft.discord) formData.append("discord", draft.discord);
const response = await fetch(PUMP_IPFS_ENDPOINT, {
method: "POST",
body: formData,
});
const payload = (await response.json().catch(() => null)) as {
metadataUri?: string;
message?: string;
} | null;
if (!response.ok || !payload?.metadataUri) {
throw new Error(
payload?.message?.trim() || `Pump.fun metadata upload failed with status ${response.status}.`,
);
}
return payload.metadataUri;
}
function decodeServerSecret(raw: string): Uint8Array {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("PUMPFUN_SERVER_SECRET_KEY is not configured.");
}
if (trimmed.startsWith("[")) {
return Uint8Array.from(JSON.parse(trimmed) as number[]);
}
return decodeBase58(trimmed);
}
function getServerSigner(): Keypair {
return Keypair.fromSecretKey(decodeServerSecret(process.env.PUMPFUN_SERVER_SECRET_KEY ?? ""));
}
function serializeTransaction(transaction: VersionedTransaction): string {
return Buffer.from(transaction.serialize()).toString("base64");
}
function deserializeTransaction(serialized: string): VersionedTransaction {
return VersionedTransaction.deserialize(Buffer.from(serialized, "base64"));
}
function pruneExpiredLaunches() {
const now = Date.now();
for (const [launchId, context] of preparedLaunches.entries()) {
if (context.prepared.expiresAt <= now) {
preparedLaunches.delete(launchId);
}
}
}
function resolveCreatorPublicKey(params: {
draft: CryptoLaunchDraft;
creatorPublicKey?: string;
}): PublicKey {
if (params.draft.executionMode === "server_side") {
return getServerSigner().publicKey;
}
return new PublicKey(params.creatorPublicKey!.trim());
}
function buildPreparedLaunch(params: {
launchId: string;
draft: CryptoLaunchDraft;
creatorPublicKey: string;
mintAddress: string;
metadataUri: string;
serializedTransaction: string;
expiresAt: number;
}): CryptoLaunchPrepared {
const preset = getNetworkPreset(params.draft.network);
return {
launchId: params.launchId,
network: params.draft.network,
executionMode: params.draft.executionMode,
mintAddress: params.mintAddress,
metadataUri: params.metadataUri,
creatorPublicKey: params.creatorPublicKey,
explorerBaseUrl: preset.explorerBaseUrl,
explorerCluster: preset.explorerCluster,
explorerTokenUrl: buildExplorerUrl(
preset.explorerBaseUrl,
"token",
params.mintAddress,
preset.explorerCluster,
),
explorerTxUrl: null,
expiresAt: params.expiresAt,
serializedTransaction:
params.draft.executionMode === "user_approved" ? params.serializedTransaction : null,
status:
params.draft.executionMode === "user_approved"
? "awaiting_signature"
: "ready_for_server_submit",
};
}
export async function prepareCryptoLaunch(params: {
draft: CryptoLaunchDraft;
creatorPublicKey?: string;
}): Promise<CryptoLaunchPrepared> {
pruneExpiredLaunches();
const creator = resolveCreatorPublicKey(params);
const metadataUri = await uploadMetadata(params.draft);
const connection = new Connection(getNetworkPreset(params.draft.network).rpcUrl, "confirmed");
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
const mintKeypair = Keypair.generate();
const sdk = await getPumpSdk();
const instructions = [
ComputeBudgetProgram.setComputeUnitLimit({
units: params.draft.computeUnitLimit,
}),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: Math.max(0, Math.round(params.draft.priorityFeeSol * 1_000_000_000)),
}),
await sdk.createV2Instruction({
mint: mintKeypair.publicKey,
name: params.draft.name,
symbol: params.draft.symbol,
uri: metadataUri,
creator,
user: creator,
mayhemMode: false,
cashback: false,
}),
await sdk.extendAccountInstruction({
account: bondingCurvePda(mintKeypair.publicKey),
user: creator,
}),
];
const transaction = new VersionedTransaction(
new TransactionMessage({
payerKey: creator,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message(),
);
transaction.sign([mintKeypair]);
const launchId = randomUUID();
const expiresAt = Date.now() + PREPARED_LAUNCH_TTL_MS;
const serializedTransaction = serializeTransaction(transaction);
const prepared = buildPreparedLaunch({
launchId,
draft: params.draft,
creatorPublicKey: creator.toBase58(),
mintAddress: mintKeypair.publicKey.toBase58(),
metadataUri,
serializedTransaction,
expiresAt,
});
preparedLaunches.set(launchId, {
prepared,
draft: params.draft,
serializedTransaction,
blockhash,
lastValidBlockHeight,
});
return prepared;
}
export async function submitCryptoLaunch(params: {
launchId: string;
executionMode: CryptoLaunchExecutionMode;
signedTransaction?: string;
}): Promise<CryptoLaunchResult> {
pruneExpiredLaunches();
const context = preparedLaunches.get(params.launchId);
if (!context) {
throw new Error("Prepared launch was not found or has expired. Please prepare it again.");
}
if (context.draft.executionMode !== params.executionMode) {
throw new Error("Launch execution mode does not match the prepared request.");
}
const preset = getNetworkPreset(context.draft.network);
const connection = new Connection(preset.rpcUrl, "confirmed");
const transaction =
params.executionMode === "server_side"
? deserializeTransaction(context.serializedTransaction)
: deserializeTransaction(params.signedTransaction!.trim());
if (params.executionMode === "server_side") {
transaction.sign([getServerSigner()]);
}
const signature = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight: false,
maxRetries: 3,
});
const confirmation = await connection.confirmTransaction(
{
signature,
blockhash: context.blockhash,
lastValidBlockHeight: context.lastValidBlockHeight,
},
"confirmed",
);
if (confirmation.value.err) {
throw new Error(`Launch transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
const result: CryptoLaunchResult = {
launchId: context.prepared.launchId,
network: context.prepared.network,
executionMode: context.prepared.executionMode,
mintAddress: context.prepared.mintAddress,
creatorPublicKey: context.prepared.creatorPublicKey,
metadataUri: context.prepared.metadataUri,
signature,
explorerBaseUrl: context.prepared.explorerBaseUrl,
explorerCluster: context.prepared.explorerCluster,
explorerTokenUrl: context.prepared.explorerTokenUrl,
explorerTxUrl: buildExplorerUrl(
context.prepared.explorerBaseUrl,
"tx",
signature,
context.prepared.explorerCluster,
),
confirmed: true,
submittedAt: new Date().toISOString(),
};
preparedLaunches.delete(params.launchId);
return result;
}
export async function fetchJitoTipFloor() {
const response = await fetch("https://bundles.jito.wtf/api/v1/bundles/tip_floor", {
cache: "no-store",
});
if (!response.ok) {
throw new Error(`Unable to load the Jito tip floor (${response.status}).`);
}
const payload = (await response.json()) as Array<{
landed_tips_50th_percentile?: number;
landed_tips_75th_percentile?: number;
landed_tips_95th_percentile?: number;
}>;
return payload[0] ?? null;
}
+79
View File
@@ -149,6 +149,85 @@ export type CryptoRoomSettings = {
agentSettings: CryptoAgentSetting[];
};
export type CryptoLaunchNetwork = "devnet" | "mainnet";
export type CryptoLaunchExecutionMode = "user_approved" | "server_side";
export type CryptoLaunchDraft = {
network: CryptoLaunchNetwork;
executionMode: CryptoLaunchExecutionMode;
name: string;
symbol: string;
description: string;
logoUrl: string;
website: string;
twitter: string;
telegram: string;
discord: string;
creatorWallet: string;
priorityFeeSol: number;
computeUnitLimit: number;
};
export type CryptoLaunchQuestionId =
| "network"
| "executionMode"
| "name"
| "symbol"
| "description"
| "logoUrl"
| "website"
| "twitter"
| "telegram"
| "discord"
| "confirm";
export type CryptoLaunchConversationState = {
active: boolean;
agentId: string | null;
awaitingField: CryptoLaunchQuestionId | null;
lastUpdatedAt: number | null;
};
export type CryptoLaunchPrepared = {
launchId: string;
network: CryptoLaunchNetwork;
executionMode: CryptoLaunchExecutionMode;
mintAddress: string;
metadataUri: string;
creatorPublicKey: string;
explorerBaseUrl: string;
explorerCluster: string | null;
explorerTokenUrl: string;
explorerTxUrl: string | null;
expiresAt: number;
serializedTransaction: string | null;
status: "awaiting_signature" | "ready_for_server_submit";
};
export type CryptoLaunchResult = {
launchId: string;
network: CryptoLaunchNetwork;
executionMode: CryptoLaunchExecutionMode;
mintAddress: string;
creatorPublicKey: string;
metadataUri: string;
signature: string;
explorerBaseUrl: string;
explorerCluster: string | null;
explorerTokenUrl: string;
explorerTxUrl: string;
confirmed: boolean;
submittedAt: string;
};
export type CryptoLaunchDraftState = {
draft: CryptoLaunchDraft;
conversation: CryptoLaunchConversationState;
lastPrepared: CryptoLaunchPrepared | null;
lastResult: CryptoLaunchResult | null;
};
export type CryptoReportSnapshot = {
trackedTokenSymbol: string;
tradeCount: number;
+11 -5
View File
@@ -5251,13 +5251,19 @@ export function RetroOffice3D({
]);
useEffect(() => {
const isTypingTarget = (target: EventTarget | null): boolean => {
if (!(target instanceof HTMLElement)) return false;
const tagName = target.tagName;
return (
tagName === "INPUT" ||
tagName === "TEXTAREA" ||
tagName === "SELECT" ||
target.isContentEditable
);
};
const onDown = (e: KeyboardEvent) => {
if (e.code !== "Space") return;
if (
document.activeElement?.tagName === "INPUT" ||
document.activeElement?.tagName === "TEXTAREA"
)
return;
if (isTypingTarget(e.target) || isTypingTarget(document.activeElement)) return;
e.preventDefault();
setSpaceDown(true);
};