mirror of
https://github.com/iamlukethedev/Claw3D.git
synced 2026-07-30 19:22:28 +00:00
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:
Generated
+615
-13
@@ -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
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user