├── README.md ├── agent-launchpad-starter.code-workspace ├── agent-tee-phala ├── .DS_Store ├── .tee-cloud │ └── compose-files │ │ └── tee-compose.yaml └── image │ ├── .gitignore │ ├── Dockerfile │ ├── agent │ ├── elizaos │ │ ├── actions.ts │ │ ├── index.ts │ │ └── wallet.ts │ ├── index.ts │ └── vercel-ai │ │ ├── index.ts │ │ └── wallet.ts │ ├── package.json │ ├── patches │ └── @elizaos__core@0.25.6-alpha.1.patch │ ├── pnpm-lock.yaml │ ├── server │ └── src │ │ └── index.ts │ └── tsconfig.json ├── biome.json └── launchpad-starter-next-app ├── .env.example ├── .gitignore ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── agents.png ├── ai-agent.png ├── fireworks.gif └── icons │ ├── eth.png │ └── sol.png ├── src ├── app │ ├── _actions │ │ ├── get-agents.ts │ │ ├── solana │ │ │ └── sign-solana-message.ts │ │ ├── submit-signature-approval.ts │ │ └── submit-txn-approval.ts │ ├── agents │ │ ├── agent-card.tsx │ │ └── page.tsx │ ├── api │ │ ├── deploy │ │ │ ├── route.ts │ │ │ └── stop │ │ │ │ └── route.ts │ │ └── health │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── providers │ │ ├── providers.tsx │ │ └── wallet-provider.tsx │ └── types │ │ ├── phala.ts │ │ └── wallet.ts ├── components │ ├── avatar.tsx │ ├── button.tsx │ ├── deploy-agent-button.tsx │ ├── dropdown-menu.tsx │ ├── fireworks.tsx │ ├── header.tsx │ ├── powered-by-crossmint.tsx │ ├── signin-auth-button.tsx │ ├── skeleton.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── typography.tsx │ ├── use-toast.tsx │ └── wallet-type-selector.tsx ├── icons │ ├── crossmint-leaf.tsx │ ├── logo.tsx │ ├── logout.tsx │ ├── passkey.tsx │ └── spinner.tsx ├── lib │ └── utils.ts └── server │ └── services │ ├── container.ts │ ├── delegated-signer.ts │ └── phala │ └── phala-cloud.ts ├── tailwind.config.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |

Agent Launchpad Starter Kit

8 | 9 |

10 | Example webapp that showcases how to deploy AI agents with non-custodial wallets. It uses Crossmint smart wallets and deploys agents in a TEE for secure key management. 11 |
12 | Report Bug 13 | · 14 | Request Feature 15 |

16 |
17 | 18 | > **ℹ️ Beta Software Notice** 19 | > 20 | > This codebase is currently in beta and has not undergone formal security audits. It serves as an illustration and blueprint for implementing non-custodial wallet architectures in AI agent systems. Before using this in production: 21 | > 22 | > 1. **Conduct Security Audits**: Thoroughly review and audit the codebase, especially the wallet management and TEE deployment components 23 | > 2. **Stay Updated**: Star and watch this repository to receive updates as we add functionality and enhance security measures 24 | > 25 | > We are actively improving the security and functionality of this codebase. Your feedback and contributions are welcome! 26 | 27 | 28 |
29 | Table of Contents 30 |
    31 |
  1. 32 | Roadmap 33 |
  2. 34 |
  3. 35 | About The Project 36 | 41 |
  4. 42 |
  5. 43 | Get Started 44 | 52 |
  6. 53 |
  7. 54 | Deploying to Production 55 | 59 |
  8. 60 |
61 |
62 | 63 | 64 | 65 | ## Roadmap 66 | 67 | - [x] Solana Smart Wallets (~Feb 10) 68 | - [ ] Passkey support for Solana Smart Wallets (~Mar 10) 69 | - [ ] Add support for more TEE networks: Marlin, Lit, etc (~Mar 10) 70 | - [ ] Sample code for user-initiated wallet actions 71 | - [ ] Non-custodial agent software updates 72 | 73 | ## About The Project 74 | 75 | ![Crossmint Agent Wallets](https://github.com/user-attachments/assets/10bdd357-38bb-4661-8d01-0568d0440263) 76 | 77 | The goal of this project is to help launchpads and other agent hosting patforms to easily 78 | deploy AI agents with wallets, following an architecture that is non-custodial for the launchpad, 79 | yet allows the agent owner and user to control the wallet. 80 | 81 | It implements the architecture proposed on [this blog](https://article.app/alfonso/agent-launchpad-wallet-architecture). 82 | 83 | ### System Architecture 84 | ![System Architecture](https://github.com/user-attachments/assets/2570bb5d-144d-420a-84b2-8290f8a2f7f2) 85 | 86 | 87 | ### Key Features: 88 | 89 | - Agent framework agnostic. Compatible with ElizaOS, Zerepy, GAME, Langchain, Vercel AI, and more. 90 | - Chain agnostic. Currently works for all EVM chains, with Solana coming soon. 91 | - Non-custodial for the launchpad: launchpad owner can't access the agent's funds / wallet. Required 92 | for regulatory compliance in the US. 93 | - Dual-key architecture. Both agent owner and agent itself can control the wallet. 94 | - The frontend uses NextJS and the agents are deployed into a TEE by Phala network 95 | 96 | ### Why Non-Custodial? 97 | 98 | When developing AI agents with cryptocurrency capabilities, two critical challenges emerge: 99 | 100 | **1. Security Considerations:** 101 | The traditional custodial approach creates significant security risks. If a launchpad platform holds custody of agent wallets, a single security breach could compromise all agents' funds. Non-custodial architecture eliminates this single point of failure, ensuring that each agent maintains independent control of its assets. 102 | 103 | **2. Regulatory Compliance:** 104 | In jurisdictions like the United States, platforms that have the ability to control or transmit user funds may fall under money transmitter regulations. This creates complex regulatory requirements and potential legal exposure. Non-custodial architecture helps platforms avoid classification as money transmitters by ensuring they never have direct access to or control over user funds. 105 | 106 | ## Get started 107 | 108 | ![Agent Launchpad Starter Kit](https://github.com/user-attachments/assets/9f55da35-b66b-4afc-a271-0fd7379d9237) 109 | 110 | ### Pre-requisites 111 | 112 | 1. [Install pnpm](https://pnpm.io/installation) as package manager 113 | 2. [Install OrbStack](https://orbstack.dev/) for local container management 114 | - Launch the Orbstack app on your computer and select "Docker" from the OrbStack setup menu 115 | 3. Create a developer project in Crossmint [staging console](https://staging.crossmint.com/console) and [production](https://www.crossmint.com/console) 116 | 117 | ### Local Setup 118 | 119 | 1. Obtain free API Keys from the [Staging environment of Crossmint Console](https://staging.crossmint.com). You'll need both a server-side and client-side API Key. Refer to these instructions to [Get a server-side API Key](https://docs.crossmint.com/introduction/platform/api-keys/server-side) and [a client-side one](https://docs.crossmint.com/introduction/platform/api-keys/client-side). 120 | 121 | - Ensure the Wallet Type is set to `Smart wallet` under Settings > General 122 | - Ensure API keys have the required scopes: 123 | - Server-side: All 'wallet API' scopes 124 | - Client-side: All 'wallet API' and 'users' scopes. Whitelist `http://localhost:3001` as an origin and check the "JWT Auth" box 125 | 126 | 2. Webapp setup 127 | 128 | ```bash 129 | cd launchpad-starter-next-app 130 | pnpm install 131 | cp .env.example .env 132 | ``` 133 | 134 | Enter your Crossmint API keys in the `.env` file. Leave the Docker URL and Phala API key as is for now. 135 | 136 | Then start the webapp: 137 | 138 | ```bash 139 | pnpm dev 140 | ``` 141 | 142 | The Next.js app will be available at `http://localhost:3001` 143 | 144 | 3. Agent setup 145 | 146 | Open a new terminal in the project root folder, and run: 147 | 148 | ```bash 149 | cd agent-tee-phala/image 150 | pnpm install 151 | ``` 152 | 153 | Then, build the image code: 154 | 155 | ```bash 156 | pnpm build 157 | ``` 158 | 159 | > **Note**: When running the nextjs app, the docker image will build and deploy in a simulated TEE environment. This simulated environment allows you to test your docker image code locally before deploying to production TEEs & Docker hub. 160 | 161 | 162 | ### Environment Variables 163 | 164 | Please refer to the [`.env.example`](launchpad-starter-next-app/.env.example) file for more information. 165 | 166 | #### Supported Chains 167 | 168 | For an exhaustive list of supported chains on Crossmint, please refer to the [Supported Chains](https://docs.crossmint.com/introduction/supported-chains) doc. 169 | 170 | #### EVM Smart Wallets 171 | 172 | > **Note on EVM Smart Wallets**: 173 | > The default implementation uses Crossmint's Smart Wallets for EVM chains, which requires setting the `ALCHEMY_API_KEY` environment variable. This API key is used to connect to the EVM chain and sign transactions on behalf of the EVM Smart Wallet. 174 | > To generate an API key for signing EVM Smart Wallet transactions: 175 | > 1. Create an account on [Alchemy](https://www.alchemy.com/) 176 | > 2. Create a new project and copy the API key 177 | > 3. Set the `ALCHEMY_API_KEY` environment variable in your `.env` file: 178 | > ``` 179 | > ALCHEMY_API_KEY=your_api_key 180 | 181 | #### Solana Smart Wallets 182 | 183 | > **Note on Solana Smart Wallets**: 184 | > The default implementation uses Crossmint's Smart Wallets for Solana, which requires generating a keypair and setting the `NEXT_PUBLIC_SOLANA_SIGNER_PUBLIC_KEY` and `SOLANA_SIGNER_PRIVATE_KEY` environment variables. This keypair will be used to sign transactions on behalf of the Solana Smart Wallet. 185 | > To generate a keypair for signing Solana Smart Wallet transactions: 186 | > 187 | > 1. Generate a new keypair using the [Solana Cookbook guide](https://solana.com/developers/cookbook/wallets/create-keypair) 188 | > 2. Set both environment variables in your `.env` file: 189 | > ``` 190 | > NEXT_PUBLIC_SOLANA_SIGNER_PUBLIC_KEY=your_public_key 191 | > SOLANA_SIGNER_PRIVATE_KEY=your_private_key 192 | > ``` 193 | > 3. Set the `NEXT_PUBLIC_PREFERRED_CHAIN` environment variable to `solana` in your `.env` file: 194 | > ``` 195 | > NEXT_PUBLIC_PREFERRED_CHAIN=solana 196 | > ``` 197 | > 198 | > If you prefer to use external wallets like Phantom instead, you can modify the wallet connection logic in the frontend code at `src/app/providers/wallet-provider.tsx`. See the [Solana Wallet Adapter](https://github.com/solana-labs/wallet-adapter) documentation for integrating external Solana wallets. The wallet provider currently uses a Solana keypair signer configuration, which you can replace with your preferred wallet connection method. 199 | 200 | ## Deploying to Production 201 | 202 | ### Building the Docker Image 203 | 204 | In order to run the docker image within a TEE, we need to first build the image. 205 | 206 | 1. From the root directory of this project, run the following command to build the Docker image: its important to use the `--platform linux/amd64` flag to ensure the image is built for the correct architecture. 207 | 208 | ```bash 209 | docker build --pull --rm -f 'agent-tee-phala/image/Dockerfile' --platform linux/amd64 -t '{your-image-name}:{version}' 'agent-tee-phala/image' 210 | ``` 211 | 212 | Example: 213 | 214 | ```bash 215 | docker build --pull --rm -f 'agent-tee-phala/image/Dockerfile' --platform linux/amd64 -t 'agentlaunchpadstarterkit:latest' 'agent-tee-phala/image' 216 | ``` 217 | 218 | 2. Publish the image to Docker Hub. 219 | In the [`launchpad-starter-next-app/src/server/services/container.ts`](launchpad-starter-next-app/src/server/services/container.ts), there's a inline comment that explains how to update the docker image name and version. Go to line 47 to find the instructions. 220 | 221 | ### Production Deployment Checklist 222 | 223 | 1. API Keys 224 | 225 | - Replace staging API keys with production keys from [Crossmint Console](https://www.crossmint.com/console) 226 | - Ensure API keys have the required scopes: 227 | - Server-side: All 'wallet API' scopes 228 | - Client-side: All 'wallet API' and 'users' scopes. Whitelist your webapp url as an origin and check the "JWT Auth" box. 229 | 230 | 2. Security Checklist 231 | 232 | - Verify and audit all code in your agent image folder to ensure it meets security standards 233 | - Publish reproduceable build code to an open source repository for transparency 234 | - Implement client-side checks to prevent agent deployments to TEEs that cannot remotely attest they are running audited code versions 235 | - Configure TEE to disallow code upgrades without explicit end user approval (feature coming soon to Phala and Marlin) 236 | 237 | 3. Deploy Agent to TEE (Phala Cloud) 238 | 239 | - Create an account on [Phala Cloud](https://cloud.phala.network) 240 | - Create a new project and copy the API key 241 | - Update the `PHALA_CLOUD_API_KEY` in your webapp's environment variables to add your Phala Cloud API key 242 | - NOTE: adding the API key to the environment variables will automatically use production environments in Phala Cloud. 243 | - To use local environments, you can just leave `PHALA_CLOUD_API_KEY` empty. 244 | 245 | 4. Deploy Webapp 246 | 247 | - Deploy your Next.js application to your preferred hosting platform (Vercel, AWS, etc.) 248 | - Set up environment variables in your hosting platform's dashboard 249 | 250 | 5. Testing 251 | 252 | - Verify wallet creation flow works end-to-end 253 | - Test agent deployment and communication 254 | - Confirm authentication and authorization are working as expected 255 | 256 | ## Disclaimer 257 | 258 | This software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software. 259 | 260 |

(back to top)

261 | -------------------------------------------------------------------------------- /agent-launchpad-starter.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "root", 5 | "path": "." 6 | }, 7 | { 8 | "name": "Agent Launchpad Starter Next.js App", 9 | "path": "./launchpad-starter-next-app" 10 | } 11 | ], 12 | "settings": { 13 | "editor.formatOnSave": true, 14 | "editor.defaultFormatter": "biomejs.biome", 15 | "editor.codeActionsOnSave": { 16 | "quickfix.biome": "explicit", 17 | "source.organizeImports.biome": "explicit" 18 | }, 19 | "typescript.tsdk": "./node_modules/typescript/lib", 20 | "[handlebars]": { 21 | "editor.formatOnSave": false, 22 | "editor.formatOnPaste": false 23 | }, 24 | "[yaml]": { 25 | "editor.defaultFormatter": "redhat.vscode-yaml" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /agent-tee-phala/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/agent-tee-phala/.DS_Store -------------------------------------------------------------------------------- /agent-tee-phala/.tee-cloud/compose-files/tee-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | build: 5 | context: "../../image" 6 | dockerfile: Dockerfile 7 | image: jonathanpaella/agentlaunchpadstarterkit:latest 8 | container_name: agent-tee-phala 9 | environment: 10 | - PORT=4000 11 | - DSTACK_SIMULATOR_ENDPOINT=${DSTACK_SIMULATOR_ENDPOINT:-} 12 | ports: 13 | - 4000:4000 14 | volumes: 15 | - /var/run/tappd.sock:/var/run/tappd.sock 16 | restart: always 17 | -------------------------------------------------------------------------------- /agent-tee-phala/image/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | 6 | # Production build 7 | dist/ 8 | build/ 9 | 10 | # Environment variables 11 | .env 12 | .env.local 13 | .env.*.local 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # IDE 23 | .idea/ 24 | .vscode/ 25 | *.swp 26 | *.swo 27 | 28 | # OS 29 | .DS_Store 30 | Thumbs.db -------------------------------------------------------------------------------- /agent-tee-phala/image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:23-slim 2 | 3 | # Install wget and other build dependencies 4 | RUN apt-get update && apt-get install -y wget 5 | 6 | # Install pnpm directly instead of using corepack 7 | RUN wget -qO /bin/pnpm "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64" && \ 8 | chmod +x /bin/pnpm 9 | 10 | WORKDIR /app 11 | 12 | # Install dependencies 13 | COPY package.json pnpm-lock.yaml ./ 14 | # Copy patched dependencies 15 | COPY patches/ ./patches 16 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 17 | 18 | # Copy source code 19 | COPY . . 20 | 21 | EXPOSE 4000 22 | 23 | ENV PORT=4000 24 | 25 | # Build TypeScript 26 | RUN pnpm build 27 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/elizaos/actions.ts: -------------------------------------------------------------------------------- 1 | import { getOnChainTools } from "@goat-sdk/adapter-vercel-ai"; 2 | import type { PluginBase, WalletClientBase } from "@goat-sdk/core"; 3 | 4 | import { 5 | generateText, 6 | type HandlerCallback, 7 | type IAgentRuntime, 8 | type Memory, 9 | ModelClass, 10 | type State, 11 | composeContext, 12 | } from "@elizaos/core"; 13 | 14 | export async function getOnChainActions(wallet: WalletClientBase, plugins: PluginBase[]) { 15 | const actionsWithoutHandler = [ 16 | { 17 | name: "SWAP_TOKENS", 18 | description: "Swap two different tokens using KIM protocol", 19 | similes: [], 20 | validate: async () => true, 21 | examples: [], 22 | }, 23 | // 1. Add your actions here 24 | ]; 25 | 26 | const tools = await getOnChainTools({ 27 | wallet: wallet, 28 | // 2. Configure the plugins you need to perform those actions 29 | plugins, 30 | }); 31 | 32 | // 3. Let GOAT handle all the actions 33 | return actionsWithoutHandler.map((action) => ({ 34 | ...action, 35 | handler: getActionHandler(action.name, action.description, tools), 36 | })); 37 | } 38 | 39 | function getActionHandler(actionName: string, actionDescription: string, tools: any) { 40 | return async ( 41 | runtime: IAgentRuntime, 42 | message: Memory, 43 | state: State | undefined, 44 | _options?: Record, 45 | callback?: HandlerCallback 46 | ): Promise => { 47 | let currentState = state ?? (await runtime.composeState(message)); 48 | currentState = await runtime.updateRecentMessageState(currentState); 49 | 50 | try { 51 | // 1. Call the tools needed 52 | const context = composeActionContext(actionName, actionDescription, currentState); 53 | const result = await generateText({ 54 | runtime, 55 | context, 56 | tools, 57 | maxSteps: 10, 58 | // Uncomment to see the log each tool call when debugging 59 | // onStepFinish: (step) => { 60 | // console.log(step.toolResults); 61 | // }, 62 | modelClass: ModelClass.LARGE, 63 | }); 64 | 65 | // 2. Compose the response 66 | const response = composeResponseContext(result, currentState); 67 | const responseText = await generateResponse(runtime, response); 68 | 69 | callback?.({ 70 | text: responseText, 71 | content: {}, 72 | }); 73 | return true; 74 | } catch (error) { 75 | const errorMessage = error instanceof Error ? error.message : String(error); 76 | 77 | // 3. Compose the error response 78 | const errorResponse = composeErrorResponseContext(errorMessage, currentState); 79 | const errorResponseText = await generateResponse(runtime, errorResponse); 80 | 81 | callback?.({ 82 | text: errorResponseText, 83 | content: { error: errorMessage }, 84 | }); 85 | return false; 86 | } 87 | }; 88 | } 89 | 90 | function composeActionContext(actionName: string, actionDescription: string, state: State): string { 91 | const actionTemplate = ` 92 | # Knowledge 93 | {{knowledge}} 94 | 95 | About {{agentName}}: 96 | {{bio}} 97 | {{lore}} 98 | 99 | {{providers}} 100 | 101 | {{attachments}} 102 | 103 | 104 | # Action: ${actionName} 105 | ${actionDescription} 106 | 107 | {{recentMessages}} 108 | 109 | Based on the action chosen and the previous messages, execute the action and respond to the user using the tools you were given. 110 | `; 111 | return composeContext({ state, template: actionTemplate }); 112 | } 113 | 114 | function composeResponseContext(result: unknown, state: State): string { 115 | const responseTemplate = ` 116 | # Action Examples 117 | {{actionExamples}} 118 | (Action examples are for reference only. Do not use the information from them in your response.) 119 | 120 | # Knowledge 121 | {{knowledge}} 122 | 123 | # Task: Generate dialog and actions for the character {{agentName}}. 124 | About {{agentName}}: 125 | {{bio}} 126 | {{lore}} 127 | 128 | {{providers}} 129 | 130 | {{attachments}} 131 | 132 | # Capabilities 133 | Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. 134 | 135 | Here is the result: 136 | ${JSON.stringify(result)} 137 | 138 | {{actions}} 139 | 140 | Respond to the message knowing that the action was successful and these were the previous messages: 141 | {{recentMessages}} 142 | `; 143 | return composeContext({ state, template: responseTemplate }); 144 | } 145 | 146 | function composeErrorResponseContext(errorMessage: string, state: State): string { 147 | const errorResponseTemplate = ` 148 | # Knowledge 149 | {{knowledge}} 150 | 151 | # Task: Generate dialog and actions for the character {{agentName}}. 152 | About {{agentName}}: 153 | {{bio}} 154 | {{lore}} 155 | 156 | {{providers}} 157 | 158 | {{attachments}} 159 | 160 | # Capabilities 161 | Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. 162 | 163 | {{actions}} 164 | 165 | Respond to the message knowing that the action failed. 166 | The error was: 167 | ${errorMessage} 168 | 169 | These were the previous messages: 170 | {{recentMessages}} 171 | `; 172 | return composeContext({ state, template: errorResponseTemplate }); 173 | } 174 | 175 | function generateResponse(runtime: IAgentRuntime, context: string): Promise { 176 | return generateText({ 177 | runtime, 178 | context, 179 | modelClass: ModelClass.SMALL, 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/elizaos/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "@elizaos/core"; 2 | import { getWalletClient, getWalletProvider } from "./wallet"; 3 | 4 | export default async function createElizaGoatPlugin(): Promise { 5 | const { walletClient, actions } = await getWalletClient(); 6 | return { 7 | name: "[GOAT] Onchain Actions", 8 | description: "Mode integration plugin", 9 | providers: [getWalletProvider(walletClient)], 10 | evaluators: [], 11 | services: [], 12 | actions, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/elizaos/wallet.ts: -------------------------------------------------------------------------------- 1 | import { crossmint } from "@goat-sdk/crossmint"; 2 | import { USDC, erc20 } from "@goat-sdk/plugin-erc20"; 3 | 4 | import { sendETH } from "@goat-sdk/wallet-evm"; 5 | import { getOnChainActions } from "./actions"; 6 | import type { WalletClientBase } from "@goat-sdk/core"; 7 | 8 | const apiKey = process.env.CROSSMINT_SERVER_API_KEY; 9 | const walletSignerSecretKey = process.env.SIGNER_WALLET_SECRET_KEY; 10 | const alchemyApiKey = process.env.ALCHEMY_API_KEY_BASE_SEPOLIA; 11 | const smartWalletAddress = process.env.SMART_WALLET_ADDRESS; 12 | 13 | if (!apiKey || !walletSignerSecretKey || !alchemyApiKey || !smartWalletAddress) { 14 | throw new Error("Missing environment variables"); 15 | } 16 | 17 | const { evmSmartWallet, faucet } = crossmint(apiKey); 18 | 19 | export async function getWalletClient() { 20 | const walletClient = await evmSmartWallet({ 21 | address: smartWalletAddress, 22 | signer: { 23 | secretKey: walletSignerSecretKey as `0x${string}`, 24 | }, 25 | chain: "base-sepolia", 26 | provider: alchemyApiKey as string, 27 | }); 28 | const plugins = [sendETH(), erc20({ tokens: [USDC] }), faucet()]; 29 | const actions = await getOnChainActions(walletClient, plugins); 30 | 31 | return { 32 | walletClient, 33 | actions, 34 | }; 35 | } 36 | 37 | export function getWalletProvider(walletClient: WalletClientBase) { 38 | return { 39 | async get(): Promise { 40 | try { 41 | const address = walletClient.getAddress(); 42 | const balance = await walletClient.balanceOf(address); 43 | return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH`; 44 | } catch (error) { 45 | console.error("Error in EVM wallet provider:", error); 46 | return null; 47 | } 48 | }, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/index.ts: -------------------------------------------------------------------------------- 1 | import startVercelAiAgent from "./vercel-ai"; 2 | 3 | export default async function startAgent() { 4 | console.log("Starting agent..."); 5 | await startVercelAiAgent(); 6 | console.log("Agent started"); 7 | } 8 | 9 | startAgent(); 10 | -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/vercel-ai/index.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@ai-sdk/openai"; 2 | import { generateText } from "ai"; 3 | import type { SupportedSmartWalletChains } from "@goat-sdk/crossmint/dist/chains"; 4 | import { getOnChainTools } from "@goat-sdk/adapter-vercel-ai"; 5 | // This is just an example plugin (coingecko) - see all available GOAT plugins at: 6 | // https://github.com/goat-sdk/goat/tree/main/typescript/packages/plugins 7 | import { coingecko } from "@goat-sdk/plugin-coingecko"; 8 | import { getWalletClient } from "./wallet"; 9 | 10 | const coingeckoApiKey = process.env.COINGECKO_API_KEY; 11 | const openaiApiKey = process.env.OPENAI_API_KEY; 12 | const chain = process.env.CHAIN as SupportedSmartWalletChains; 13 | 14 | if (!coingeckoApiKey || !openaiApiKey || !chain) { 15 | throw new Error("COINGECKO_API_KEY or OPENAI_API_KEY or CHAIN is not set"); 16 | } 17 | 18 | export default async function startVercelAiAgent() { 19 | const { walletClient } = await getWalletClient(chain); 20 | const tools = await getOnChainTools({ 21 | wallet: walletClient, 22 | plugins: [coingecko({ apiKey: coingeckoApiKey as string })], 23 | }); 24 | 25 | const result = await generateText({ 26 | model: openai("gpt-4o-mini"), 27 | tools, 28 | maxSteps: 5, 29 | prompt: "What are the trending cryptocurrencies right now and what's the price of Bonk? Also print the address of the wallet and if it's solana or evm.", 30 | }); 31 | 32 | console.log(result.text); 33 | } 34 | -------------------------------------------------------------------------------- /agent-tee-phala/image/agent/vercel-ai/wallet.ts: -------------------------------------------------------------------------------- 1 | import { crossmint } from "@goat-sdk/crossmint"; 2 | import type { SupportedSmartWalletChains } from "@goat-sdk/crossmint/dist/chains"; 3 | import { Connection } from "@solana/web3.js"; 4 | 5 | const apiKey = process.env.CROSSMINT_SERVER_API_KEY; 6 | const walletSignerSecretKey = process.env.SIGNER_WALLET_SECRET_KEY; 7 | const alchemyApiKey = process.env.ALCHEMY_API_KEY_BASE_SEPOLIA; 8 | const smartWalletAddress = process.env.SMART_WALLET_ADDRESS; 9 | const solanaRpcUrl = process.env.SOLANA_RPC_URL; 10 | 11 | if (!apiKey || !walletSignerSecretKey || !smartWalletAddress) { 12 | throw new Error("Missing environment variables"); 13 | } 14 | 15 | const { evmSmartWallet, solanaSmartWallet } = crossmint(apiKey); 16 | 17 | export async function getWalletClient(chain: SupportedSmartWalletChains | "solana") { 18 | if (chain === "solana") { 19 | if (!solanaRpcUrl) { 20 | throw new Error("Missing SOLANA_RPC_URL environment variable"); 21 | } 22 | return { 23 | walletClient: await solanaSmartWallet({ 24 | config: { 25 | adminSigner: { 26 | type: "solana-keypair", 27 | secretKey: walletSignerSecretKey as string, 28 | }, 29 | }, 30 | connection: new Connection(solanaRpcUrl as string), 31 | address: smartWalletAddress as string, 32 | }), 33 | }; 34 | } 35 | 36 | if (!alchemyApiKey) { 37 | throw new Error("Missing ALCHEMY_API_KEY_BASE_SEPOLIA environment variable"); 38 | } 39 | return { 40 | walletClient: await evmSmartWallet({ 41 | address: smartWalletAddress, 42 | signer: { 43 | secretKey: walletSignerSecretKey as `0x${string}`, 44 | }, 45 | chain, 46 | provider: alchemyApiKey as string, 47 | }), 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /agent-tee-phala/image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-tee-phala", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "start": "node dist/server/src/index.js", 7 | "dev": "ts-node-dev --respawn server/src/index.ts", 8 | "build:docker": "tsc && docker build -t agent-tee-phala .", 9 | "pull:docker-tee-simulator": "docker pull phalanetwork/tappd-simulator:latest && docker run --rm -d -p 8090:8090 phalanetwork/tappd-simulator:latest", 10 | "start:docker": "pnpm pull:docker-tee-simulator && docker-compose -f ../.tee-cloud/compose-files/tee-compose.yaml up -d", 11 | "start:agent": "node dist/agent/index.js" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/openai": "^1.1.9", 15 | "@elizaos/adapter-sqljs": "0.25.6-alpha.1", 16 | "@elizaos/core": "0.25.6-alpha.1", 17 | "@goat-sdk/adapter-vercel-ai": "^0.2.8", 18 | "@goat-sdk/core": "0.4.6", 19 | "@goat-sdk/crossmint": "^0.4.1", 20 | "@goat-sdk/plugin-coingecko": "^0.2.8", 21 | "@goat-sdk/plugin-erc20": "^0.2.10", 22 | "@goat-sdk/plugin-kim": "^0.1.10", 23 | "@goat-sdk/wallet-evm": "^0.2.8", 24 | "@goat-sdk/wallet-viem": "0.2.0", 25 | "@phala/dstack-sdk": "^0.1.10", 26 | "@solana/web3.js": "^1.98.0", 27 | "@types/express": "^5.0.0", 28 | "ai": "^4.1.24", 29 | "dotenv": "^16.4.5", 30 | "express": "^4.21.2", 31 | "sharp": "^0.33.5", 32 | "sql.js": "^1.12.0", 33 | "viem": "2.22.11" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20.0.0", 37 | "ts-node": "^10.9.2", 38 | "ts-node-dev": "^2.0.0", 39 | "typescript": "^5.0.0" 40 | }, 41 | "pnpm": { 42 | "patchedDependencies": { 43 | "@elizaos/core@0.25.6-alpha.1": "patches/@elizaos__core@0.25.6-alpha.1.patch" 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /agent-tee-phala/image/patches/@elizaos__core@0.25.6-alpha.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 184985c0d6dd82c9c1c0140438a0957bfe6e8023..de3df6f22ae8f9401fb60b4f6ee5deb2e31b3062 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -9,12 +9,11 @@ 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": { 9 | - "import": { 10 | - "@elizaos/source": "./src/index.ts", 11 | - "types": "./dist/index.d.ts", 12 | - "default": "./dist/index.js" 13 | - } 14 | - } 15 | + "types": "./dist/index.d.ts", 16 | + "default": "./dist/index.js", 17 | + "import": "./dist/index.js" 18 | + }, 19 | + "./source": "./src/index.ts" 20 | }, 21 | "files": [ 22 | "dist" 23 | -------------------------------------------------------------------------------- /agent-tee-phala/image/server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TappdClient } from "@phala/dstack-sdk"; 2 | // @ts-expect-error issue with the types from source code 3 | import { toKeypair } from "@phala/dstack-sdk/solana"; 4 | // @ts-expect-error issue with the types from source code 5 | import { toViemAccount } from "@phala/dstack-sdk/viem"; 6 | import { isHex, keccak256 } from "viem"; 7 | import express from "express"; 8 | import type { Request, Response } from "express"; 9 | 10 | import { exec } from "child_process"; 11 | import { promisify } from "util"; 12 | import dotenv from "dotenv"; 13 | dotenv.config(); 14 | 15 | const execAsync = promisify(exec); 16 | 17 | const app = express(); 18 | const port = process.env.PORT || 4000; 19 | 20 | let privateKey: string; 21 | let publicKey: string; 22 | let smartWalletAddress: string; 23 | 24 | app.get("/api/getPublicKey", (req, res) => { 25 | res.json({ publicKey }); 26 | }); 27 | 28 | app.post("/api/initialize", async (req: Request, res: Response) => { 29 | const smartWalletAddressHeader = req.header("x-wallet-address"); 30 | const crossmintServerApiKey = req.header("x-api-key"); 31 | const alchemyApiKey = req.header("x-alchemy-api-key"); 32 | const coingeckoApiKey = req.header("x-coingecko-api-key"); 33 | const openaiApiKey = req.header("x-openai-api-key"); 34 | const solanaRpcUrl = req.header("x-solana-rpc-url"); 35 | const chain = req.header("x-chain"); 36 | 37 | if (!smartWalletAddressHeader || !crossmintServerApiKey) { 38 | res.status(400).json({ 39 | error: "missing 'x-wallet-address' or 'x-api-key' header in request for initialization", 40 | }); 41 | return; 42 | } 43 | 44 | try { 45 | const isEVMWallet = isHex(smartWalletAddressHeader); 46 | const client = new TappdClient(process.env.DSTACK_SIMULATOR_ENDPOINT || undefined); 47 | const randomDeriveKey = await client.deriveKey(smartWalletAddressHeader, ""); 48 | 49 | if (isEVMWallet) { 50 | const keccakPrivateKey = keccak256(randomDeriveKey.asUint8Array()); 51 | const account = toViemAccount(randomDeriveKey); 52 | 53 | console.log("Generated agent keys from TEE"); 54 | console.log("EVM Account address:", account.address); 55 | 56 | privateKey = keccakPrivateKey; 57 | publicKey = account.address; 58 | } else { 59 | const keypair = toKeypair(randomDeriveKey); 60 | publicKey = keypair.publicKey.toString(); 61 | privateKey = keypair.secretKey.toString(); 62 | 63 | console.log("Generated agent keys from TEE"); 64 | console.log("Solana Account address:", publicKey); 65 | } 66 | 67 | smartWalletAddress = smartWalletAddressHeader; 68 | 69 | await initializeAgent( 70 | privateKey, 71 | crossmintServerApiKey, 72 | alchemyApiKey, 73 | coingeckoApiKey, 74 | openaiApiKey, 75 | solanaRpcUrl, 76 | chain 77 | ); 78 | 79 | res.json({ status: "success", publicKey }); 80 | } catch (error) { 81 | console.error("Error generating agent public key:", error); 82 | res.status(500).json({ error: "Failed to generate public key" }); 83 | } 84 | }); 85 | 86 | app.get("/api/health", (req, res) => { 87 | res.json({ status: "ok" }); 88 | }); 89 | 90 | app.listen(port, () => { 91 | console.log(`Server running on port ${port}`); 92 | }); 93 | 94 | async function initializeAgent( 95 | privateKey: string, 96 | crossmintServerApiKey: string, 97 | alchemyApiKey?: string, 98 | coingeckoApiKey?: string, 99 | openaiApiKey?: string, 100 | solanaRpcUrl?: string, 101 | chain?: string 102 | ) { 103 | try { 104 | console.log("Initializing agent..."); 105 | console.log( 106 | "NOTE: Running the agent locally in the simulated container is not yet supported.\n You will recieve an error about the TEE container not being able to initialize itself from within the tee" 107 | ); 108 | const environmentVariables = `SIGNER_WALLET_SECRET_KEY='${privateKey}' CROSSMINT_SERVER_API_KEY='${crossmintServerApiKey}' SMART_WALLET_ADDRESS='${smartWalletAddress}' ALCHEMY_API_KEY_BASE_SEPOLIA='${alchemyApiKey}' COINGECKO_API_KEY='${coingeckoApiKey}' OPENAI_API_KEY='${openaiApiKey}' SOLANA_RPC_URL='${solanaRpcUrl}' CHAIN='${chain}'`; 109 | const { stdout } = await execAsync(`${environmentVariables} pnpm run start:agent`); 110 | console.log("stdout:", stdout); 111 | } catch (error) { 112 | console.error("Error executing agent:", error); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /agent-tee-phala/image/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | }, 12 | "include": ["server/src/**/*", "agent/**/*"], 13 | "exclude": ["node_modules", "dist"] 14 | } -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 4, 8 | "lineEnding": "lf", 9 | "lineWidth": 120, 10 | "attributePosition": "auto", 11 | "ignore": [ 12 | "node_modules", 13 | "public", 14 | ".next", 15 | ".vercel", 16 | "playwright-report", 17 | "pnpm-lock.yaml", 18 | ".github", 19 | "**/dist/**", 20 | "**/build/**" 21 | ] 22 | }, 23 | "organizeImports": { 24 | "enabled": false 25 | }, 26 | "linter": { 27 | "enabled": true, 28 | "rules": { 29 | "recommended": false, 30 | "a11y": { 31 | "noAriaUnsupportedElements": "warn", 32 | "noBlankTarget": "warn", 33 | "useAltText": "warn", 34 | "useAriaPropsForRole": "warn", 35 | "useValidAriaProps": "warn", 36 | "useValidAriaValues": "warn" 37 | }, 38 | "complexity": { 39 | "noBannedTypes": "warn", 40 | "noUselessTypeConstraint": "error" 41 | }, 42 | "correctness": { 43 | "noPrecisionLoss": "error", 44 | "noUnusedImports": "error", 45 | "noChildrenProp": "error", 46 | "useHookAtTopLevel": "warn", 47 | "useJsxKeyInIterable": "warn", 48 | "noUnusedVariables": "warn", 49 | "useArrayLiterals": "warn" 50 | }, 51 | "security": { 52 | "noDangerouslySetInnerHtml": "error" 53 | }, 54 | "style": { 55 | "noInferrableTypes": "error", 56 | "noNamespace": "warn", 57 | "useAsConstAssertion": "error", 58 | "useBlockStatements": "warn", 59 | "noNonNullAssertion": "warn", 60 | "noParameterAssign": "warn", 61 | "useImportType": "warn" 62 | }, 63 | "suspicious": { 64 | "noExtraNonNullAssertion": "error", 65 | "noFallthroughSwitchClause": "error", 66 | "noMisleadingInstantiator": "error", 67 | "noDuplicateAtImportRules": "error", 68 | "useNamespaceKeyword": "error", 69 | "noCommentText": "error", 70 | "noDuplicateJsxProps": "error", 71 | "noEmptyBlockStatements": "warn", 72 | "noExplicitAny": "warn", 73 | "useAwait": "warn" 74 | } 75 | }, 76 | "ignore": [ 77 | "coverage", 78 | ".next", 79 | ".vercel", 80 | "playwright-report", 81 | "services/snowcrash", 82 | "pnpm-lock.yaml", 83 | "**/build/**", 84 | "**/dist/**" 85 | ] 86 | }, 87 | "javascript": { 88 | "formatter": { 89 | "jsxQuoteStyle": "double", 90 | "quoteProperties": "asNeeded", 91 | "trailingCommas": "es5", 92 | "semicolons": "always", 93 | "arrowParentheses": "always", 94 | "bracketSpacing": true, 95 | "bracketSameLine": false, 96 | "quoteStyle": "double", 97 | "attributePosition": "auto" 98 | }, 99 | "parser": { 100 | "unsafeParameterDecoratorsEnabled": true 101 | } 102 | }, 103 | "overrides": [ 104 | { 105 | "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], 106 | "linter": { 107 | "rules": { 108 | "correctness": { 109 | "noConstAssign": "warn", 110 | "noGlobalObjectCalls": "warn", 111 | "noInvalidConstructorSuper": "warn", 112 | "noNewSymbol": "warn", 113 | "noSetterReturn": "warn", 114 | "noUndeclaredVariables": "warn", 115 | "noUnreachable": "warn", 116 | "noUnreachableSuper": "warn" 117 | }, 118 | "style": { 119 | "noArguments": "error", 120 | "noVar": "warn", 121 | "useConst": "warn" 122 | }, 123 | "suspicious": { 124 | "noDuplicateClassMembers": "warn", 125 | "noDuplicateObjectKeys": "warn", 126 | "noDuplicateParameters": "warn", 127 | "noFunctionAssign": "warn", 128 | "noImportAssign": "warn", 129 | "noRedeclare": "warn", 130 | "noUnsafeNegation": "warn", 131 | "useGetterReturn": "warn", 132 | "useValidTypeof": "warn" 133 | } 134 | } 135 | } 136 | } 137 | ] 138 | } 139 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/.env.example: -------------------------------------------------------------------------------- 1 | # Required API Keys 2 | NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY= 3 | CROSSMINT_SERVER_API_KEY= 4 | COINGECKO_API_KEY= 5 | OPENAI_API_KEY= 6 | 7 | # Optional Settings 8 | NEXT_PUBLIC_PREFERRED_CHAIN= # Optional: Defaults to base-sepolia 9 | PHALA_CLOUD_API_KEY= # Optional for local simulated deployments, required for production TEE deployments 10 | 11 | # EVM Smart Wallet Settings 12 | # These settings are only needed if you're using EVM Smart Wallets 13 | ALCHEMY_API_KEY= 14 | 15 | # Solana Smart Wallet Settings 16 | # These settings are only needed if you're using Solana Smart Wallets 17 | NEXT_PUBLIC_SOLANA_SIGNER_PUBLIC_KEY= 18 | SOLANA_SIGNER_PRIVATE_KEY= 19 | SOLANA_RPC_URL= -------------------------------------------------------------------------------- /launchpad-starter-next-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | // Needed for Wallet Connect with next.js apps which we use via dynamic-labs in react-ui: https://docs.reown.com/appkit/next/core/installation#extra-configuration 4 | webpack: (config) => { 5 | config.externals.push("pino-pretty", "lokijs", "encoding"); 6 | return config; 7 | }, 8 | reactStrictMode: false, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crossmint/agent-launchpad-starter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "docker pull phalanetwork/tappd-simulator:latest && next dev -p 3001 && echo '\n🚀 Please make sure OrbStack is running before continuing...\n'", 8 | "lint": "next lint", 9 | "lint:fix": "biome check --write", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "@crossmint/client-sdk-react-ui": "^1.14.9", 14 | "@crossmint/common-sdk-base": "^0.3.1", 15 | "@noble/curves": "^1.8.1", 16 | "@radix-ui/react-avatar": "1.1.0", 17 | "@radix-ui/react-dropdown-menu": "2.1.1", 18 | "@radix-ui/react-slot": "1.1.0", 19 | "@radix-ui/react-tabs": "1.1.0", 20 | "@radix-ui/react-toast": "1.2.1", 21 | "@tanstack/react-query": "5.51.18", 22 | "bs58": "^6.0.0", 23 | "class-variance-authority": "0.7.0", 24 | "clsx": "2.1.1", 25 | "dotenv": "^16.4.7", 26 | "lucide-react": "0.473.0", 27 | "next": "15.1.6", 28 | "ox": "^0.6.7", 29 | "react": "^19", 30 | "react-dom": "^19", 31 | "tailwind-merge": "2.6.0", 32 | "tweetnacl": "^1.0.3", 33 | "viem": "2.22.11" 34 | }, 35 | "devDependencies": { 36 | "@biomejs/biome": "^1.9.4", 37 | "@types/node": "^20", 38 | "@types/react": "^19", 39 | "@types/react-dom": "^19", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.17", 42 | "typescript": "^5" 43 | } 44 | } -------------------------------------------------------------------------------- /launchpad-starter-next-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/public/agents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/public/agents.png -------------------------------------------------------------------------------- /launchpad-starter-next-app/public/ai-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/public/ai-agent.png -------------------------------------------------------------------------------- /launchpad-starter-next-app/public/fireworks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/public/fireworks.gif -------------------------------------------------------------------------------- /launchpad-starter-next-app/public/icons/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/public/icons/eth.png -------------------------------------------------------------------------------- /launchpad-starter-next-app/public/icons/sol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/public/icons/sol.png -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/_actions/get-agents.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { PhalaCloud } from "@/server/services/phala/phala-cloud"; 4 | 5 | export async function getMyDeployedAgents() { 6 | const phalaCloud = new PhalaCloud(); 7 | return await phalaCloud.queryCvmsByUserId(); 8 | } 9 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/_actions/solana/sign-solana-message.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import bs58 from "bs58"; 4 | import nacl from "tweetnacl"; 5 | 6 | const secretKey = process.env.SOLANA_SIGNER_PRIVATE_KEY as string; 7 | 8 | // biome-ignore lint/suspicious/useAwait: server action must be marked as async 9 | export async function signSolanaMessage(message: string): Promise { 10 | if (!secretKey) { 11 | throw new Error("SOLANA_SIGNER_PRIVATE_KEY is not set"); 12 | } 13 | const messageBytes = bs58.decode(message); 14 | const secretKeyBytes = bs58.decode(secretKey); 15 | 16 | const signature = bs58.encode(nacl.sign.detached(messageBytes, secretKeyBytes)); 17 | console.log("Signature:", signature); 18 | 19 | return signature; 20 | } 21 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/_actions/submit-signature-approval.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getBaseUrlFromApiKey } from "@/lib/utils"; 4 | 5 | const API_KEY = process.env.CROSSMINT_SERVER_API_KEY as string; 6 | const CROSSMINT_BASE_URL = getBaseUrlFromApiKey(API_KEY); 7 | 8 | export async function submitSignatureApproval( 9 | signature: any, 10 | signerLocator: string, 11 | walletAddress: string, 12 | signatureId: string, 13 | metadata?: any 14 | ) { 15 | try { 16 | const response = await fetch( 17 | `${CROSSMINT_BASE_URL}/wallets/${walletAddress}/signatures/${signatureId}/approvals`, 18 | { 19 | method: "POST", 20 | headers: { 21 | "X-API-KEY": API_KEY, 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify({ 25 | approvals: [ 26 | { 27 | signer: signerLocator, 28 | metadata, 29 | signature, 30 | }, 31 | ], 32 | }), 33 | } 34 | ); 35 | 36 | if (!response.ok) { 37 | throw new Error("Failed to submit signature approval"); 38 | } 39 | 40 | return { success: true }; 41 | } catch (error) { 42 | console.error("Error in submit-signature-approval:", error); 43 | throw new Error("Failed to submit signature approval"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/_actions/submit-txn-approval.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getBaseUrlFromApiKey } from "@/lib/utils"; 4 | 5 | const API_KEY = process.env.CROSSMINT_SERVER_API_KEY as string; 6 | const CROSSMINT_BASE_URL = getBaseUrlFromApiKey(API_KEY); 7 | 8 | export async function submitTransactionApproval( 9 | signature: any, 10 | signerLocator: string, 11 | walletAddress: string, 12 | transactionId: string 13 | ) { 14 | try { 15 | const response = await fetch( 16 | `${CROSSMINT_BASE_URL}/wallets/${walletAddress}/transactions/${transactionId}/approvals`, 17 | { 18 | method: "POST", 19 | headers: { 20 | "X-API-KEY": API_KEY, 21 | "Content-Type": "application/json", 22 | }, 23 | body: JSON.stringify({ 24 | approvals: [ 25 | { 26 | signer: signerLocator, 27 | signature, 28 | }, 29 | ], 30 | }), 31 | } 32 | ); 33 | 34 | if (!response.ok) { 35 | throw new Error("Failed to submit transaction approval"); 36 | } 37 | 38 | return { success: true }; 39 | } catch (error) { 40 | console.error("Error in submit-transaction-approval:", error); 41 | throw new Error("Failed to submit transaction approval"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/agents/agent-card.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@/components/typography"; 2 | import { Button } from "@/components/button"; 3 | import type { Cvm } from "../types/phala"; 4 | 5 | export default function AgentCard({ agent }: { agent: Cvm }) { 6 | return ( 7 |
8 |
9 |
10 | 11 | {agent.hosted.name} 12 | 13 | ID: {agent.hosted.id} 14 | Instance ID: {agent.hosted.instance_id} 15 |
16 |
17 |
22 | {agent.status} 23 |
24 |
25 | 26 |
27 |
28 | Uptime 29 | {agent.hosted.uptime} 30 |
31 |
32 | Node 33 | {agent.node.name} 34 |
35 |
36 | Memory 37 | {agent.hosted.configuration.memory}MB 38 |
39 |
40 | vCPU 41 | {agent.hosted.configuration.vcpu} 42 |
43 |
44 | 45 |
46 | 53 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/agents/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { PoweredByCrossmint } from "@/components/powered-by-crossmint"; 4 | import { Skeleton } from "@/components/skeleton"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | 7 | import { useWallet } from "../providers/wallet-provider"; 8 | import { getMyDeployedAgents } from "../_actions/get-agents"; 9 | import AgentCard from "./agent-card"; 10 | import { Typography } from "@/components/typography"; 11 | 12 | export default function Index() { 13 | const { isLoading } = useWallet(); 14 | 15 | const { data, isLoading: isLoadingAgents } = useQuery({ 16 | queryKey: ["agents"], 17 | queryFn: getMyDeployedAgents, 18 | refetchOnWindowFocus: false, 19 | refetchOnMount: false, 20 | staleTime: 1000 * 60 * 5, // 5 minutes 21 | }); 22 | 23 | if (isLoading || isLoadingAgents) { 24 | return ( 25 |
26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | ); 34 | } 35 | 36 | return ( 37 |
38 |
39 | Deployed Agents 40 |
41 | {(data || []).map((agent) => ( 42 | 43 | ))} 44 |
45 |
46 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/api/deploy/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { ContainerManager } from "@/server/services/container"; 3 | import { getOrCreateDelegatedSigner } from "@/server/services/delegated-signer"; 4 | 5 | const CHAIN = process.env.NEXT_PUBLIC_PREFERRED_CHAIN || "base-sepolia"; 6 | 7 | const containerManager = new ContainerManager(); 8 | 9 | // set to true to debug the container 10 | const DEBUG_CONTAINER = true; 11 | 12 | export async function POST(request: Request) { 13 | try { 14 | const { smartWalletAddress, walletSignerType } = await request.json(); 15 | 16 | if (smartWalletAddress == null || walletSignerType == null) { 17 | return NextResponse.json( 18 | { success: false, error: "body must contain smartWalletAddress and walletSignerType" }, 19 | { status: 400 } 20 | ); 21 | } 22 | 23 | // 1. Start TEE container if not already running 24 | if (!containerManager.isRunning()) { 25 | console.log("Starting TEE container..."); 26 | await containerManager.startContainer(); 27 | } 28 | 29 | // 2. Get agent key from deployed TEE instance 30 | const { publicKey } = await fetch(`${containerManager.deploymentUrl}/api/initialize`, { 31 | method: "POST", 32 | headers: { 33 | "x-api-key": process.env.CROSSMINT_SERVER_API_KEY as string, 34 | "x-wallet-address": smartWalletAddress, 35 | "x-alchemy-api-key": process.env.ALCHEMY_API_KEY as string, 36 | "x-coingecko-api-key": process.env.COINGECKO_API_KEY as string, 37 | "x-openai-api-key": process.env.OPENAI_API_KEY as string, 38 | "x-solana-rpc-url": process.env.SOLANA_RPC_URL as string, 39 | "x-chain": CHAIN, 40 | }, 41 | }).then((res) => res.json()); 42 | console.log(`Agent public key: ${publicKey}`); 43 | 44 | // 3. Get existing or create a new delegated signer request 45 | const delegatedSigner = await getOrCreateDelegatedSigner( 46 | smartWalletAddress, 47 | publicKey, 48 | CHAIN, 49 | walletSignerType 50 | ); 51 | 52 | return NextResponse.json({ 53 | success: true, 54 | containerId: containerManager.containerId, 55 | targetSignerLocator: delegatedSigner?.targetSignerLocator, 56 | delegatedSignerMessage: delegatedSigner?.message, 57 | delegatedSignerId: delegatedSigner?.id, 58 | delegatedSignerAlreadyActive: delegatedSigner?.delegatedSignerAlreadyActive ?? false, 59 | }); 60 | } catch (error) { 61 | console.error("Deployment error:", error); 62 | // Ensure container is stopped on error 63 | if (!DEBUG_CONTAINER) { 64 | await containerManager.stopContainer(); 65 | } 66 | 67 | return NextResponse.json( 68 | { 69 | success: false, 70 | error: error instanceof Error ? error.message : "Unknown error", 71 | }, 72 | { status: 500 } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/api/deploy/stop/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { ContainerManager } from "@/server/services/container"; 3 | 4 | const containerManager = new ContainerManager(); 5 | 6 | export async function POST() { 7 | try { 8 | await containerManager.stopContainer(); 9 | return NextResponse.json({ 10 | success: true, 11 | message: "Agent stopped successfully", 12 | }); 13 | } catch (error) { 14 | return NextResponse.json( 15 | { 16 | success: false, 17 | error: error instanceof Error ? error.message : "Unknown error", 18 | }, 19 | { status: 500 } 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/api/health/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export function GET() { 4 | return NextResponse.json({ status: "ok" }); 5 | } 6 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossmint/agent-launchpad-starter-kit/b8fd81fe969153bb7dcb337886b41ebc36b0bc88/launchpad-starter-next-app/src/app/favicon.ico -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | /* Define color variables */ 8 | --color-background: 104 52% 95%; /* #eff9ec */ 9 | --color-foreground: 210 14.3% 4.1%; /* #2d2d2d */ 10 | --color-card: 0 0% 100%; /* #ffffff */ 11 | --color-card-foreground: 210 14.3% 4.1%; /* #2d2d2d */ 12 | --color-popover: 0 0% 100%; /* #ffffff */ 13 | --color-popover-foreground: 210 14.3% 4.1%; /* #2d2d2d */ 14 | --color-primary: 210 95% 53.1%; /* #0066ff */ 15 | --color-primary-foreground: 210 30% 62%; /* #82A6BC */ 16 | --color-secondary: 210 4.8% 95.9%; /* #f2f2f2 */ 17 | --color-secondary-foreground: 210 56% 24%; /* #1B3960 */ 18 | --color-muted: 210 18% 74%; /* #B1BCC9 */ 19 | --color-muted-foreground: 210 77% 29%; /* #114983 */ 20 | --color-accent: 210 4.8% 95.9%; /* #f2f2f2 */ 21 | --color-accent-foreground: 210 9.8% 10%; /* #3d3d3d */ 22 | --color-destructive: 0 84.2% 60.2%; /* #ff3333 */ 23 | --color-destructive-foreground: 210 9.1% 97.8%; /* #f5f5f5 */ 24 | --color-border: 210 26% 80%; /* #C0CBD9 */ 25 | --color-input: 210 5.9% 90%; /* #e6e6e6 */ 26 | --color-ring: 210 95% 53.1%; /* #0066ff */ 27 | --color-chart-1: 210 76% 61%; /* #47A2FF */ 28 | --color-skeleton: 210 33% 88%; /* #D7E2EB */ 29 | 30 | /* Use color variables */ 31 | --background: var(--color-background); 32 | --foreground: var(--color-foreground); 33 | --card: var(--color-card); 34 | --card-foreground: var(--color-card-foreground); 35 | --popover: var(--color-popover); 36 | --popover-foreground: var(--color-popover-foreground); 37 | --primary: var(--color-primary); 38 | --primary-foreground: var(--color-primary-foreground); 39 | --secondary: var(--color-secondary); 40 | --secondary-foreground: var(--color-secondary-foreground); 41 | --muted: var(--color-muted); 42 | --muted-foreground: var(--color-muted-foreground); 43 | --accent: var(--color-accent); 44 | --accent-foreground: var(--color-accent-foreground); 45 | --destructive: var(--color-destructive); 46 | --destructive-foreground: var(--color-destructive-foreground); 47 | --border: var(--color-border); 48 | --input: var(--color-input); 49 | --ring: var(--color-ring); 50 | --shadow-light: 0 1px 2px 0 rgba(193, 208, 234, 0.6); 51 | --shadow-heavy: 0 4px 8px 0 rgba(193, 208, 234, 0.6); 52 | --shadow-dropdown: 0 2px 12px 0 rgba(193, 208, 234, 0.6); 53 | --chart-1: var(--color-chart-1); 54 | --radius: 0.5rem; 55 | } 56 | } 57 | @layer base { 58 | body { 59 | @apply bg-background text-foreground; 60 | font-feature-settings: "rlig" 1, "calt" 1; 61 | } 62 | } 63 | 64 | @layer utilities { 65 | .text-balance { 66 | text-wrap: balance; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import type React from "react"; 3 | import { Inter, Raleway } from "next/font/google"; 4 | 5 | import { cn } from "../lib/utils"; 6 | import "./globals.css"; 7 | import { Header } from "@/components/header"; 8 | import { Toaster } from "@/components/toaster"; 9 | import { Providers } from "./providers/providers"; 10 | 11 | export const metadata: Metadata = { 12 | title: "Crossmint Agent Launchpad", 13 | description: "Crossmint Agent Launchpad", 14 | }; 15 | 16 | const inter = Inter({ 17 | subsets: ["latin"], 18 | display: "swap", 19 | variable: "--font-inter", 20 | }); 21 | 22 | const raleway = Raleway({ 23 | subsets: ["latin"], 24 | display: "swap", 25 | variable: "--font-raleway", 26 | }); 27 | 28 | export default function RootLayout({ 29 | children, 30 | }: { 31 | children: React.ReactNode; 32 | }) { 33 | return ( 34 | 38 | 39 | {metadata.title as string} 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | {children} 48 | 49 |
50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import Link from "next/link"; 5 | import { Fireworks } from "@/components/fireworks"; 6 | import { DeployAgentButton } from "@/components/deploy-agent-button"; 7 | import { PoweredByCrossmint } from "@/components/powered-by-crossmint"; 8 | import { Typography } from "@/components/typography"; 9 | import { useWallet } from "./providers/wallet-provider"; 10 | import { SignInAuthButton } from "@/components/signin-auth-button"; 11 | import type { Wallet } from "./types/wallet"; 12 | import WalletTypeSelector from "@/components/wallet-type-selector"; 13 | import Image from "next/image"; 14 | 15 | function HomePrimaryAction({ wallet }: { wallet: Wallet | null }) { 16 | const [agentSuccessfullyDeployed, setAgentSuccessfullyDeployed] = useState(false); 17 | 18 | if (wallet == null) { 19 | return ; 20 | } 21 | 22 | if (agentSuccessfullyDeployed) { 23 | return ( 24 | <> 25 | 26 |
27 | 31 | View Rufus 32 | 33 |
34 | 35 | ); 36 | } else { 37 | return ; 38 | } 39 | } 40 | 41 | export default function Home() { 42 | const { wallet, walletType, setWalletType } = useWallet(); 43 | 44 | return ( 45 |
46 |
47 |
48 | 56 | Agent Launchpad 57 | 58 | 59 | A secure, non-custodial Next.js application for deploying AI agents with integrated wallet 60 | functionality 61 | 62 |
63 | 64 |
65 |
66 | ai agent 67 |
68 |
69 |
70 |

Agent Rufus

71 |
by Crossmint
72 |
73 |
74 |
75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/providers/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, type ReactNode } from "react"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { CrossmintProvider, CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui"; 6 | import { WalletProvider } from "./wallet-provider"; 7 | import { WalletType } from "../types/wallet"; 8 | 9 | const DEFAULT_WALLET_TYPE = process.env.NEXT_PUBLIC_PREFERRED_CHAIN?.includes("solana") 10 | ? WalletType.Solana 11 | : WalletType.EVM; 12 | 13 | export function Providers({ children }: { children: ReactNode }) { 14 | const queryClient = new QueryClient(); 15 | /* State needed lifting to the top level due to a re-render issue happening from CrossmintAuthProvider */ 16 | const [walletType, setWalletType] = useState(DEFAULT_WALLET_TYPE); 17 | 18 | return ( 19 | 20 | 21 | 37 | 38 | {children} 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/providers/wallet-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext, useState, type ReactNode } from "react"; 4 | import { useAuth } from "@crossmint/client-sdk-react-ui"; 5 | import { WebAuthnP256 } from "ox"; 6 | import { getBaseUrlFromApiKey } from "@/lib/utils"; 7 | import type { WalletType, Wallet } from "../types/wallet"; 8 | 9 | const SOLANA_PUBLIC_KEY_SIGNER = process.env.NEXT_PUBLIC_SOLANA_SIGNER_PUBLIC_KEY as string; 10 | const CLIENT_API_KEY = process.env.NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY as string; 11 | if (!CLIENT_API_KEY) { 12 | throw new Error("NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY is not set"); 13 | } 14 | 15 | const CROSSMINT_BASE_URL = getBaseUrlFromApiKey(CLIENT_API_KEY); 16 | 17 | interface WalletContextType { 18 | wallet: Wallet | null; 19 | isLoading: boolean; 20 | getOrCreateWallet: () => Promise; 21 | walletType: WalletType; 22 | setWalletType: (walletType: WalletType) => void; 23 | } 24 | 25 | const WalletContext = createContext(undefined); 26 | 27 | export function WalletProvider({ 28 | children, 29 | walletType, 30 | setWalletType, 31 | }: { children: ReactNode; walletType: WalletType; setWalletType: (walletType: WalletType) => void }) { 32 | const { jwt, logout } = useAuth(); 33 | const [wallet, setWallet] = useState(null); 34 | const [isLoading, setIsLoading] = useState(false); 35 | 36 | const getOrCreateWallet = async () => { 37 | if (!jwt) { 38 | console.error("No JWT available"); 39 | return; 40 | } 41 | 42 | setIsLoading(true); 43 | try { 44 | // First, try to get existing wallet 45 | const getResponse = await fetch(`${CROSSMINT_BASE_URL}/wallets/me:${walletType}`, { 46 | method: "GET", 47 | headers: { 48 | "X-API-KEY": CLIENT_API_KEY, 49 | authorization: `Bearer ${jwt}`, 50 | "Content-Type": "application/json", 51 | }, 52 | }); 53 | 54 | const existingWallet = await getResponse.json(); 55 | 56 | if (existingWallet != null && existingWallet.address != null) { 57 | console.log("Existing wallet found:", existingWallet); 58 | // Use the first wallet found 59 | setWallet({ 60 | address: existingWallet?.address, 61 | credentialId: existingWallet?.config.adminSigner.id, 62 | type: existingWallet?.type, 63 | }); 64 | return; 65 | } 66 | 67 | // Assemble the config for the wallet 68 | let config; 69 | if (walletType.includes("solana")) { 70 | if (!SOLANA_PUBLIC_KEY_SIGNER) { 71 | throw new Error("NEXT_PUBLIC_SOLANA_SIGNER_PUBLIC_KEY is not set"); 72 | } 73 | // Optional: Can replace the adminSigner with EOA wallet from say Phantom. 74 | config = { 75 | adminSigner: { 76 | type: "solana-keypair", 77 | address: SOLANA_PUBLIC_KEY_SIGNER, 78 | }, 79 | }; 80 | } else { 81 | // EVM 82 | const name = `Agent launchpad starter ${new Date().toISOString()}`; 83 | const credential = await WebAuthnP256.createCredential({ name }); 84 | config = { 85 | adminSigner: { 86 | type: "evm-passkey", 87 | id: credential.id, 88 | publicKey: { 89 | x: credential.publicKey.x.toString(), 90 | y: credential.publicKey.y.toString(), 91 | }, 92 | name, 93 | }, 94 | creationSeed: "0", 95 | }; 96 | } 97 | 98 | // If no wallet exists, create a new one 99 | const createResponse = await fetch(`${CROSSMINT_BASE_URL}/wallets/me`, { 100 | method: "POST", 101 | body: JSON.stringify({ 102 | type: walletType, 103 | config, 104 | }), 105 | headers: { 106 | "X-API-KEY": CLIENT_API_KEY, 107 | authorization: `Bearer ${jwt}`, 108 | "Content-Type": "application/json", 109 | }, 110 | }); 111 | 112 | const data = await createResponse.json(); 113 | 114 | setWallet({ 115 | address: data.address, 116 | credentialId: data.config.adminSigner.id, 117 | type: data.type, 118 | }); 119 | } catch (error) { 120 | console.error("Error with wallet operation:", error); 121 | logout(); 122 | setWallet(null); 123 | } finally { 124 | setIsLoading(false); 125 | } 126 | }; 127 | 128 | return ( 129 | 138 | {children} 139 | 140 | ); 141 | } 142 | 143 | export function useWallet() { 144 | const context = useContext(WalletContext); 145 | if (context === undefined) { 146 | throw new Error("useWallet must be used within a WalletProvider"); 147 | } 148 | return context; 149 | } 150 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/types/phala.ts: -------------------------------------------------------------------------------- 1 | interface DeployOptions { 2 | debug?: boolean; 3 | type?: string; 4 | mode?: string; 5 | name: string; 6 | vcpu?: number; 7 | memory?: number; 8 | diskSize?: number; 9 | compose?: string; 10 | env?: string[]; 11 | envFile?: string; 12 | envs: Env[]; 13 | } 14 | 15 | interface Env { 16 | key: string; 17 | value: string; 18 | } 19 | 20 | interface CvmConfig { 21 | teepod_id: number; 22 | name: string; 23 | image: string; 24 | vcpu: number; 25 | memory: number; 26 | disk_size: number; 27 | compose_manifest: { 28 | docker_compose_file: string; 29 | docker_config: { 30 | url: string; 31 | username: string; 32 | password: string; 33 | }; 34 | features: string[]; 35 | kms_enabled: boolean; 36 | manifest_version: number; 37 | name: string; 38 | public_logs: boolean; 39 | public_sysinfo: boolean; 40 | tproxy_enabled: boolean; 41 | }; 42 | listed: boolean; 43 | encrypted_env?: string; 44 | app_env_encrypt_pubkey?: string; 45 | app_id_salt?: string; 46 | } 47 | 48 | interface Cvm { 49 | hosted: Hosted; 50 | name: string; 51 | managed_user: any; 52 | node: any; 53 | listed: boolean; 54 | status: string; 55 | in_progress: boolean; 56 | dapp_dashboard_url: string; 57 | syslog_endpoint: string; 58 | allow_upgrade: boolean; 59 | } 60 | 61 | interface CvmNetwork { 62 | is_online: boolean; 63 | is_public: boolean; 64 | error?: string; 65 | internal_ip: string; 66 | latest_handshake: string; 67 | public_urls: PublicUrl[]; 68 | } 69 | 70 | interface PublicUrl { 71 | app: string; 72 | instance: string; 73 | } 74 | 75 | interface Hosted { 76 | id: string; 77 | name: string; 78 | status: string; 79 | uptime: string; 80 | app_url: string; 81 | app_id: string; 82 | instance_id: string; 83 | exited_at: string; 84 | boot_progress: string; 85 | boot_error: string; 86 | shutdown_progress: string; 87 | image_version: string; 88 | configuration: Configuration; 89 | } 90 | 91 | interface Configuration { 92 | memory: number; 93 | disk_size: number; 94 | vcpu: number; 95 | } 96 | 97 | interface CreateCvmResponse { 98 | app_id: string; 99 | [key: string]: any; 100 | } 101 | 102 | interface GetPubkeyFromCvmResponse { 103 | app_env_encrypt_pubkey: string; 104 | app_id_salt: string; 105 | } 106 | 107 | export type { DeployOptions, Env, CvmConfig, Cvm, CreateCvmResponse, GetPubkeyFromCvmResponse, CvmNetwork }; 108 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/app/types/wallet.ts: -------------------------------------------------------------------------------- 1 | export interface Wallet { 2 | address: string; 3 | credentialId: string; 4 | type: string; 5 | } 6 | 7 | export enum WalletType { 8 | EVM = "evm-smart-wallet", 9 | Solana = "solana-smart-wallet", 10 | } 11 | 12 | export interface WalletSelectorProps { 13 | value: WalletType; 14 | onChange: (value: WalletType) => void; 15 | } 16 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | import * as React from "react"; 6 | 7 | const Avatar = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 16 | )); 17 | Avatar.displayName = AvatarPrimitive.Root.displayName; 18 | 19 | const AvatarImage = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 26 | 27 | const AvatarFallback = React.forwardRef< 28 | React.ElementRef, 29 | React.ComponentPropsWithoutRef 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 38 | 39 | export { Avatar, AvatarImage, AvatarFallback }; 40 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { type VariantProps, cva } from "class-variance-authority"; 4 | import * as React from "react"; 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-primary-foreground", 12 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 13 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 14 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | ghost: "hover:bg-accent hover:text-accent-foreground", 16 | link: "text-primary underline-offset-4 hover:underline", 17 | }, 18 | size: { 19 | default: "h-[52px] px-4 py-2", 20 | sm: "h-9 rounded-md px-3", 21 | lg: "h-11 rounded-md px-8", 22 | icon: "h-10 w-10", 23 | }, 24 | }, 25 | defaultVariants: { 26 | variant: "default", 27 | size: "default", 28 | }, 29 | } 30 | ); 31 | 32 | export interface ButtonProps 33 | extends React.ButtonHTMLAttributes, 34 | VariantProps { 35 | asChild?: boolean; 36 | } 37 | 38 | const Button = React.forwardRef( 39 | ({ className, variant, size, asChild = false, ...props }, ref) => { 40 | const Comp = asChild ? Slot : "button"; 41 | return ; 42 | } 43 | ); 44 | Button.displayName = "Button"; 45 | 46 | export { Button, buttonVariants }; 47 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/deploy-agent-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Passkey } from "@/icons/passkey"; 5 | import { Spinner } from "@/icons/spinner"; 6 | import { WebAuthnP256 } from "ox"; 7 | 8 | import { Button } from "./button"; 9 | import { Typography } from "./typography"; 10 | import { useToast } from "./use-toast"; 11 | import { useWallet } from "@/app/providers/wallet-provider"; 12 | import { submitSignatureApproval } from "@/app/_actions/submit-signature-approval"; 13 | import { signSolanaMessage } from "@/app/_actions/solana/sign-solana-message"; 14 | import { submitTransactionApproval } from "@/app/_actions/submit-txn-approval"; 15 | 16 | export const DeployAgentButton = ({ 17 | setAgentSuccessfullyDeployed, 18 | }: { setAgentSuccessfullyDeployed: (a: boolean) => void }) => { 19 | const { wallet } = useWallet(); 20 | const [isLoadingDeploy, setIsLoadingDeploy] = useState(false); 21 | const { toast } = useToast(); 22 | 23 | if (isLoadingDeploy) { 24 | return ( 25 |
26 | 27 | 28 | Deploying Rufus... 29 | 30 | 31 |
32 | ); 33 | } 34 | 35 | const handleDeployAgent = async () => { 36 | setIsLoadingDeploy(true); 37 | try { 38 | if (!wallet?.address) { 39 | toast({ title: "Error occurred during wallet creation" }); 40 | return; 41 | } 42 | const isEVMWallet = wallet.type.includes("evm"); 43 | const walletSignerType = !isEVMWallet ? "solana-keypair" : "evm-passkey"; 44 | 45 | // 1. Deploy the agent 46 | const response = await fetch(`/api/deploy`, { 47 | method: "POST", 48 | headers: { 49 | "Content-Type": "application/json", 50 | }, 51 | body: JSON.stringify({ smartWalletAddress: wallet.address, walletSignerType }), 52 | }); 53 | 54 | const data = (await response.json()) as { 55 | success: true; 56 | containerId: string; 57 | delegatedSignerMessage: string; 58 | delegatedSignerId: string; 59 | delegatedSignerAlreadyActive?: boolean; 60 | targetSignerLocator: string; 61 | }; 62 | 63 | // If the delegated signer is already active, we can skip the signature approval step 64 | if (data?.delegatedSignerAlreadyActive) { 65 | setAgentSuccessfullyDeployed(true); 66 | return; 67 | } 68 | 69 | if (isEVMWallet) { 70 | // 2 Sign the delegated signer message for EVM passkey wallet 71 | const { metadata, signature } = await WebAuthnP256.sign({ 72 | credentialId: wallet.credentialId, 73 | challenge: data.delegatedSignerMessage as `0x${string}`, 74 | }); 75 | // 3. call the server-side action to submit the signature approval 76 | await submitSignatureApproval( 77 | { r: signature.r.toString(), s: signature.s.toString() }, 78 | data.targetSignerLocator, 79 | wallet.address, 80 | data.delegatedSignerId, 81 | metadata 82 | ); 83 | } else { 84 | // 2 Sign the delegated signer message for Solana keypair wallet 85 | const signature = await signSolanaMessage(data.delegatedSignerMessage); 86 | // 3. call the server-side action to submit the signature approval 87 | await submitTransactionApproval( 88 | signature, 89 | data.targetSignerLocator, 90 | wallet.address, 91 | data.delegatedSignerId 92 | ); 93 | } 94 | 95 | setAgentSuccessfullyDeployed(true); 96 | } catch (error) { 97 | console.error("Error deploying Rufus:", error); 98 | toast({ title: "Error occurred during deployment" }); 99 | } finally { 100 | setIsLoadingDeploy(false); 101 | } 102 | }; 103 | 104 | return ( 105 | 123 | ); 124 | }; 125 | 126 | const Timer = () => { 127 | const [count, setCount] = useState(0); 128 | 129 | useEffect(() => { 130 | const interval = setInterval(() => { 131 | setCount((prevCount) => prevCount + 1); 132 | }, 1000); 133 | return () => clearInterval(interval); 134 | }, []); 135 | 136 | return count + "s"; 137 | }; 138 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | import * as React from "react"; 7 | 8 | const DropdownMenu = DropdownMenuPrimitive.Root; 9 | 10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 11 | 12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 13 | 14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 15 | 16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 17 | 18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 19 | 20 | const DropdownMenuSubTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef & { 23 | inset?: boolean; 24 | } 25 | >(({ className, inset, children, ...props }, ref) => ( 26 | 35 | {children} 36 | 37 | 38 | )); 39 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )); 54 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; 55 | 56 | const DropdownMenuContent = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, sideOffset = 4, ...props }, ref) => ( 60 | 61 | 70 | 71 | )); 72 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 73 | 74 | const DropdownMenuItem = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef & { 77 | inset?: boolean; 78 | } 79 | >(({ className, inset, ...props }, ref) => ( 80 | 89 | )); 90 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 91 | 92 | const DropdownMenuCheckboxItem = React.forwardRef< 93 | React.ElementRef, 94 | React.ComponentPropsWithoutRef 95 | >(({ className, children, checked, ...props }, ref) => ( 96 | 105 | 106 | 107 | 108 | 109 | 110 | {children} 111 | 112 | )); 113 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; 114 | 115 | const DropdownMenuRadioItem = React.forwardRef< 116 | React.ElementRef, 117 | React.ComponentPropsWithoutRef 118 | >(({ className, children, ...props }, ref) => ( 119 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )); 135 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 136 | 137 | const DropdownMenuLabel = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef & { 140 | inset?: boolean; 141 | } 142 | >(({ className, inset, ...props }, ref) => ( 143 | 148 | )); 149 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 150 | 151 | const DropdownMenuSeparator = React.forwardRef< 152 | React.ElementRef, 153 | React.ComponentPropsWithoutRef 154 | >(({ className, ...props }, ref) => ( 155 | 156 | )); 157 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 158 | 159 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 160 | return ; 161 | }; 162 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 163 | 164 | export { 165 | DropdownMenu, 166 | DropdownMenuTrigger, 167 | DropdownMenuContent, 168 | DropdownMenuItem, 169 | DropdownMenuCheckboxItem, 170 | DropdownMenuRadioItem, 171 | DropdownMenuLabel, 172 | DropdownMenuSeparator, 173 | DropdownMenuShortcut, 174 | DropdownMenuGroup, 175 | DropdownMenuPortal, 176 | DropdownMenuSub, 177 | DropdownMenuSubContent, 178 | DropdownMenuSubTrigger, 179 | DropdownMenuRadioGroup, 180 | }; 181 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/fireworks.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export const Fireworks = ({ className }: { className?: string }) => { 5 | const [isPlaying, setIsPlaying] = useState(true); 6 | 7 | useEffect(() => { 8 | setIsPlaying(true); 9 | const timer = setTimeout(() => { 10 | setIsPlaying(false); 11 | }, 5000); // 5 seconds is a complete cycle of the fireworks gif 12 | return () => clearTimeout(timer); 13 | }, []); 14 | 15 | if (!isPlaying) { 16 | return null; 17 | } 18 | return ( 19 |
20 |
21 | fireworks 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type React from "react"; 4 | import { LogoutIcon } from "@/icons/logout"; 5 | import { Copy, Image as ImageIcon, User, WalletMinimal } from "lucide-react"; 6 | import Link from "next/link"; 7 | 8 | import { Avatar, AvatarFallback, AvatarImage } from "./avatar"; 9 | import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "./dropdown-menu"; 10 | import { Typography } from "./typography"; 11 | import { useToast } from "./use-toast"; 12 | import { useWallet } from "@/app/providers/wallet-provider"; 13 | import { useAuth } from "@crossmint/client-sdk-react-ui"; 14 | 15 | function formatWalletAddress(address: string, startLength: number, endLength: number): string { 16 | return `${address.substring(0, startLength)}...${address.substring(address.length - endLength)}`; 17 | } 18 | 19 | export const Header: React.FC = () => { 20 | const { logout } = useAuth(); 21 | const { wallet, isLoading } = useWallet(); 22 | const { toast } = useToast(); 23 | 24 | const handleLogout = () => { 25 | window.location.reload(); 26 | logout(); 27 | }; 28 | 29 | const handleCopyAddress = async () => { 30 | if (wallet?.address) { 31 | await navigator.clipboard.writeText(wallet.address); 32 | toast({ title: "Address copied to clipboard", duration: 5000 }); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 39 | {wallet != null && wallet.address != null && wallet.address !== "" && ( 40 | 46 | )} 47 |
48 | ); 49 | }; 50 | 51 | const HeaderLogo: React.FC = () => ( 52 | 53 | Agent Launchpad 54 | 55 | ); 56 | 57 | const UserMenu: React.FC<{ 58 | wallet: string | undefined; 59 | walletStatus: string; 60 | onLogout: () => void; 61 | onCopyAddress: () => void; 62 | }> = ({ wallet, walletStatus, onLogout, onCopyAddress }) => ( 63 | 64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 |
77 |
78 | {wallet ? formatWalletAddress(wallet, 14, 6) : ""} 79 | 80 |
81 | 82 | 83 | Agents 84 | 85 |
86 | 87 | Logout 88 |
89 |
90 |
91 |
92 | ); 93 | 94 | const WalletDisplay: React.FC<{ 95 | address: string | undefined; 96 | isLoading: boolean; 97 | }> = ({ address, isLoading }) => ( 98 |
99 | 100 | {isLoading ? "Loading..." : address ? formatWalletAddress(address, 6, 3) : ""} 101 |
102 | ); 103 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/powered-by-crossmint.tsx: -------------------------------------------------------------------------------- 1 | import { CrossmintLeaf } from "@/icons/crossmint-leaf"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | import { Typography } from "./typography"; 5 | 6 | export const PoweredByCrossmint = ({ className }: { className?: string }) => { 7 | return ( 8 | 15 | 16 | 17 | 18 | Secured by Crossmint 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/signin-auth-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | import { useAuth } from "@crossmint/client-sdk-react-ui"; 4 | import { Button } from "./button"; 5 | import { Typography } from "./typography"; 6 | import { useWallet } from "@/app/providers/wallet-provider"; 7 | 8 | export const SignInAuthButton = () => { 9 | const { login, jwt, status } = useAuth(); 10 | const { wallet, getOrCreateWallet, isLoading: isLoadingWallet } = useWallet(); 11 | 12 | useEffect(() => { 13 | if (wallet == null && jwt != null) { 14 | getOrCreateWallet(); 15 | } 16 | }, [wallet, jwt]); 17 | 18 | if (status === "in-progress" || isLoadingWallet) { 19 | return ( 20 |
21 | 37 | 38 | Waiting for your wallet... 39 | 40 |
41 | ); 42 | } 43 | 44 | return ( 45 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 5 | return
; 6 | } 7 | 8 | export { Skeleton }; 9 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import * as ToastPrimitives from "@radix-ui/react-toast"; 5 | import { type VariantProps, cva } from "class-variance-authority"; 6 | import { X } from "lucide-react"; 7 | import * as React from "react"; 8 | 9 | const ToastProvider = ToastPrimitives.Provider; 10 | 11 | const ToastViewport = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 25 | 26 | const toastVariants = cva( 27 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 28 | { 29 | variants: { 30 | variant: { 31 | default: "border bg-background text-foreground", 32 | destructive: "destructive group border-destructive bg-destructive text-destructive-foreground", 33 | }, 34 | }, 35 | defaultVariants: { 36 | variant: "default", 37 | }, 38 | } 39 | ); 40 | 41 | const Toast = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef & VariantProps 44 | >(({ className, variant, ...props }, ref) => { 45 | return ; 46 | }); 47 | Toast.displayName = ToastPrimitives.Root.displayName; 48 | 49 | const ToastAction = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, ...props }, ref) => ( 53 | 61 | )); 62 | ToastAction.displayName = ToastPrimitives.Action.displayName; 63 | 64 | const ToastClose = React.forwardRef< 65 | React.ElementRef, 66 | React.ComponentPropsWithoutRef 67 | >(({ className, ...props }, ref) => ( 68 | 77 | 78 | 79 | )); 80 | ToastClose.displayName = ToastPrimitives.Close.displayName; 81 | 82 | const ToastTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 87 | )); 88 | ToastTitle.displayName = ToastPrimitives.Title.displayName; 89 | 90 | const ToastDescription = React.forwardRef< 91 | React.ElementRef, 92 | React.ComponentPropsWithoutRef 93 | >(({ className, ...props }, ref) => ( 94 | 95 | )); 96 | ToastDescription.displayName = ToastPrimitives.Description.displayName; 97 | 98 | type ToastProps = React.ComponentPropsWithoutRef; 99 | 100 | type ToastActionElement = React.ReactElement; 101 | 102 | export { 103 | type ToastProps, 104 | type ToastActionElement, 105 | ToastProvider, 106 | ToastViewport, 107 | Toast, 108 | ToastTitle, 109 | ToastDescription, 110 | ToastClose, 111 | ToastAction, 112 | }; 113 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "./toast"; 4 | import { useToast } from "./use-toast"; 5 | 6 | export function Toaster() { 7 | const { toasts } = useToast(); 8 | 9 | return ( 10 | 11 | {toasts.map(function ({ id, title, description, action, ...props }) { 12 | return ( 13 | 14 |
15 | {title && {title}} 16 | {description && {description}} 17 |
18 | {action} 19 | 20 |
21 | ); 22 | })} 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/typography.tsx: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from "class-variance-authority"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "../lib/utils"; 5 | 6 | export const typographyVariants = cva("font-body transition-colors", { 7 | variants: { 8 | variant: { 9 | h1: "font-display text-4xl font-extrabold", 10 | h2: "font-display text-2xl font-extrabold leading-7", 11 | h3: "font-display text-xl font-extrabold leading-6", 12 | h4: "font-body text-lg font-semibold leading-6", 13 | h5: "font-body text-sm font-medium leading-6", 14 | h6: "font-body text-xs font-medium", 15 | tag: "font-body text-xs", 16 | body: "font-body text-base", 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: "body", 21 | }, 22 | }); 23 | 24 | export type TypographyProps = React.HTMLAttributes & 25 | VariantProps & { 26 | children: React.ReactNode; 27 | tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "span" | "p"; 28 | }; 29 | 30 | export function Typography({ variant, tag, className, children, ...props }: TypographyProps) { 31 | let Component: React.ElementType; 32 | 33 | if (tag) { 34 | Component = tag; 35 | } else { 36 | switch (variant) { 37 | case "h1": 38 | case "h2": 39 | case "h3": 40 | case "h4": 41 | case "h5": 42 | case "h6": 43 | Component = variant; 44 | break; 45 | case "tag": 46 | Component = "span"; 47 | break; 48 | case "body": 49 | default: 50 | Component = "p"; 51 | } 52 | } 53 | 54 | return ( 55 | 56 | {children} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/use-toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // Inspired by react-hot-toast library 4 | import type React from "react"; 5 | import { useEffect, useState } from "react"; 6 | 7 | import type { ToastActionElement, ToastProps } from "./toast"; 8 | 9 | const TOAST_LIMIT = 1; 10 | const TOAST_REMOVE_DELAY = 1000000; 11 | 12 | type ToasterToast = ToastProps & { 13 | id: string; 14 | title?: React.ReactNode; 15 | description?: React.ReactNode; 16 | action?: ToastActionElement; 17 | }; 18 | 19 | const actionTypes = { 20 | ADD_TOAST: "ADD_TOAST", 21 | UPDATE_TOAST: "UPDATE_TOAST", 22 | DISMISS_TOAST: "DISMISS_TOAST", 23 | REMOVE_TOAST: "REMOVE_TOAST", 24 | } as const; 25 | 26 | let count = 0; 27 | 28 | function genId() { 29 | count = (count + 1) % Number.MAX_SAFE_INTEGER; 30 | return count.toString(); 31 | } 32 | 33 | type ActionType = typeof actionTypes; 34 | 35 | type Action = 36 | | { 37 | type: ActionType["ADD_TOAST"]; 38 | toast: ToasterToast; 39 | } 40 | | { 41 | type: ActionType["UPDATE_TOAST"]; 42 | toast: Partial; 43 | } 44 | | { 45 | type: ActionType["DISMISS_TOAST"]; 46 | toastId?: ToasterToast["id"]; 47 | } 48 | | { 49 | type: ActionType["REMOVE_TOAST"]; 50 | toastId?: ToasterToast["id"]; 51 | }; 52 | 53 | interface State { 54 | toasts: ToasterToast[]; 55 | } 56 | 57 | const toastTimeouts = new Map>(); 58 | 59 | const addToRemoveQueue = (toastId: string) => { 60 | if (toastTimeouts.has(toastId)) { 61 | return; 62 | } 63 | 64 | const timeout = setTimeout(() => { 65 | toastTimeouts.delete(toastId); 66 | dispatch({ 67 | type: "REMOVE_TOAST", 68 | toastId: toastId, 69 | }); 70 | }, TOAST_REMOVE_DELAY); 71 | 72 | toastTimeouts.set(toastId, timeout); 73 | }; 74 | 75 | export const reducer = (state: State, action: Action): State => { 76 | switch (action.type) { 77 | case "ADD_TOAST": 78 | return { 79 | ...state, 80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 81 | }; 82 | 83 | case "UPDATE_TOAST": 84 | return { 85 | ...state, 86 | toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), 87 | }; 88 | 89 | case "DISMISS_TOAST": { 90 | const { toastId } = action; 91 | 92 | // ! Side effects ! - This could be extracted into a dismissToast() action, 93 | // but I'll keep it here for simplicity 94 | if (toastId) { 95 | addToRemoveQueue(toastId); 96 | } else { 97 | state.toasts.forEach((toast) => { 98 | addToRemoveQueue(toast.id); 99 | }); 100 | } 101 | 102 | return { 103 | ...state, 104 | toasts: state.toasts.map((t) => 105 | t.id === toastId || toastId === undefined 106 | ? { 107 | ...t, 108 | open: false, 109 | } 110 | : t 111 | ), 112 | }; 113 | } 114 | case "REMOVE_TOAST": 115 | if (action.toastId === undefined) { 116 | return { 117 | ...state, 118 | toasts: [], 119 | }; 120 | } 121 | return { 122 | ...state, 123 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 124 | }; 125 | } 126 | }; 127 | 128 | const listeners: Array<(state: State) => void> = []; 129 | 130 | let memoryState: State = { toasts: [] }; 131 | 132 | function dispatch(action: Action) { 133 | memoryState = reducer(memoryState, action); 134 | listeners.forEach((listener) => { 135 | listener(memoryState); 136 | }); 137 | } 138 | 139 | type Toast = Omit; 140 | 141 | function toast({ ...props }: Toast) { 142 | const id = genId(); 143 | 144 | const update = (props: ToasterToast) => 145 | dispatch({ 146 | type: "UPDATE_TOAST", 147 | toast: { ...props, id }, 148 | }); 149 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); 150 | 151 | dispatch({ 152 | type: "ADD_TOAST", 153 | toast: { 154 | ...props, 155 | id, 156 | open: true, 157 | onOpenChange: (open) => { 158 | if (!open) { 159 | dismiss(); 160 | } 161 | }, 162 | }, 163 | }); 164 | 165 | return { 166 | id: id, 167 | dismiss, 168 | update, 169 | }; 170 | } 171 | 172 | function useToast() { 173 | const [state, setState] = useState(memoryState); 174 | 175 | useEffect(() => { 176 | listeners.push(setState); 177 | return () => { 178 | const index = listeners.indexOf(setState); 179 | if (index > -1) { 180 | listeners.splice(index, 1); 181 | } 182 | }; 183 | }, [state]); 184 | 185 | return { 186 | ...state, 187 | toast, 188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 189 | }; 190 | } 191 | 192 | export { useToast, toast }; 193 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/components/wallet-type-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { type WalletSelectorProps, WalletType } from "@/app/types/wallet"; 5 | import { useWallet } from "@/app/providers/wallet-provider"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | export default function WalletTypeSelector({ value, onChange }: WalletSelectorProps) { 9 | const { wallet, isLoading } = useWallet(); 10 | 11 | const hintText = 12 | wallet != null ? `Wallet ${wallet.type} connected` : "Select your preferred wallet type to continue"; 13 | 14 | const disabled = wallet != null || isLoading; 15 | 16 | return ( 17 |
18 |
19 |
20 | 1 21 |
22 |

Select your preferred wallet type

23 |
24 | 25 |
26 | 40 | 41 | 55 |
56 | 57 | {hintText != null ?
{hintText}
: null} 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/icons/crossmint-leaf.tsx: -------------------------------------------------------------------------------- 1 | export const CrossmintLeaf = () => { 2 | return ( 3 | 4 | 13 | 14 | 19 | 20 | 26 | 32 | 38 | 43 | 44 | 45 | 51 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/icons/logo.tsx: -------------------------------------------------------------------------------- 1 | export const Logo = () => ( 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 26 | 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const MobileLogo = () => ( 51 | 52 | 59 | 66 | 75 | 89 | 90 | ); 91 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/icons/logout.tsx: -------------------------------------------------------------------------------- 1 | export const LogoutIcon = ({ className }: { className?: string }) => ( 2 | 10 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/icons/passkey.tsx: -------------------------------------------------------------------------------- 1 | export const Passkey = () => ( 2 | 3 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/icons/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export const Spinner = ({ className }: { className?: string }) => ( 4 | 20 | ); 21 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { APIKeyEnvironmentPrefix, getEnvironmentForKey } from "@crossmint/common-sdk-base"; 2 | import { type ClassValue, clsx } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | /** 6 | * Combines multiple class names into a single string. 7 | * 8 | * @param inputs - The class names to combine. 9 | * @returns The combined class names as a string. 10 | */ 11 | export function cn(...inputs: ClassValue[]) { 12 | return twMerge(clsx(inputs)); 13 | } 14 | 15 | export function getBaseUrlFromApiKey(apiKey: string) { 16 | switch (getEnvironmentForKey(apiKey)) { 17 | case APIKeyEnvironmentPrefix.STAGING: 18 | return "https://staging.crossmint.com/api/2022-06-09"; 19 | case APIKeyEnvironmentPrefix.PRODUCTION: 20 | return "https://www.crossmint.com/api/2022-06-09"; 21 | default: 22 | return "http://localhost:3000/api/2022-06-09"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/server/services/container.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { promisify } from "util"; 3 | import path from "path"; 4 | import { PhalaCloud } from "./phala/phala-cloud"; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | const IS_DEV = !process.env.PHALA_CLOUD_API_KEY; 9 | const LOCAL_COMPOSE_FILE_PATH = path.join( 10 | process.cwd(), 11 | "..", 12 | "agent-tee-phala", 13 | ".tee-cloud/compose-files/tee-compose.yaml" 14 | ); 15 | 16 | export class ContainerManager { 17 | public containerId: string | null = null; 18 | public deploymentUrl: string | null = null; 19 | public simulatorId?: string | null = null; 20 | 21 | async startContainer(): Promise { 22 | try { 23 | if (IS_DEV) { 24 | /* 25 | * DEVELOPMENT: Use docker-compose 26 | * Start simulator on port 8090 27 | */ 28 | console.log("Starting simulator..."); 29 | const { stdout: simulatorId } = await execAsync( 30 | `docker run -d --rm -p 8090:8090 phalanetwork/tappd-simulator:latest` 31 | ); 32 | console.log("Simulator started!"); 33 | this.simulatorId = simulatorId.trim(); 34 | 35 | // Build and start TEE container that will start the express server on port 4000 36 | console.log("Building TEE container..."); 37 | await execAsync(`docker-compose -f "${LOCAL_COMPOSE_FILE_PATH}" build`); 38 | console.log("Build complete!"); 39 | 40 | // Start TEE container that will start the express server on port 4000 41 | const DSTACK_SIMULATOR_ENDPOINT = "http://host.docker.internal:8090"; 42 | const { stdout: teeCompose } = await execAsync( 43 | `DSTACK_SIMULATOR_ENDPOINT=${DSTACK_SIMULATOR_ENDPOINT} docker-compose -f "${LOCAL_COMPOSE_FILE_PATH}" up -d` 44 | ); 45 | console.log("TEE container started!"); 46 | this.containerId = teeCompose.trim(); 47 | this.deploymentUrl = "http://app.compose-files.orb.local:4000"; 48 | } else { 49 | /* 50 | * PRODUCTION: Use Phala Production hardware (Phala Cloud) 51 | * ASSUME WE HAVE ALREADY DEPLOYED TO DOCKER HUB 52 | * AND WE JUST NEED TO DEPLOY TO PHALA CLOUD 53 | * 54 | * To add your own docker image: 55 | * 1. Build your docker image and push it to docker hub 56 | * 2. Update the name in the DockerImageObject below 57 | * 3. Update the compose file path in the .tee-cloud folder 58 | * 3a. Or replace the contents of the tee-compose.yaml file directly 59 | */ 60 | const phalaCloud = new PhalaCloud(); 61 | const DockerImageObject = { 62 | name: "agentlaunchpadstarterkit", 63 | compose: path.join(process.cwd(), "../agent-tee-phala/.tee-cloud/compose-files/tee-compose.yaml"), 64 | envs: [], 65 | }; 66 | const { appId } = await phalaCloud.deploy(DockerImageObject); 67 | 68 | // Wait for deployment using the helper function 69 | this.deploymentUrl = await phalaCloud.waitForDeployment(appId); 70 | this.containerId = appId; 71 | console.log("Deployment URL:", this.deploymentUrl); 72 | } 73 | 74 | await this.waitForContainerReadiness(); 75 | 76 | console.log(`TEE container started: ${this.containerId}`); 77 | console.log(`Deployment URL: ${this.deploymentUrl}`); 78 | 79 | if (IS_DEV) { 80 | // Show container logs 81 | const { stdout: logs } = await execAsync(`docker-compose -f "${LOCAL_COMPOSE_FILE_PATH}" logs app`); 82 | console.log("Container logs:", logs); 83 | } 84 | } catch (error) { 85 | console.error("Failed to start container:", error); 86 | throw error; 87 | } 88 | } 89 | 90 | async stopContainer(): Promise { 91 | if (this.isRunning()) { 92 | try { 93 | await execAsync(`docker-compose -f "${LOCAL_COMPOSE_FILE_PATH}" down`); 94 | if (this.simulatorId) { 95 | await execAsync(`docker stop ${this.simulatorId}`); 96 | console.log("Simulator stopped!"); 97 | } 98 | console.log("Container stopped:", this.containerId); 99 | this.containerId = null; 100 | this.deploymentUrl = null; 101 | } catch (error) { 102 | console.error("Failed to stop container:", error); 103 | throw error; 104 | } 105 | } 106 | } 107 | 108 | isRunning(): boolean { 109 | return this.containerId !== null; 110 | } 111 | 112 | // TODO: make re-usable function for both deploying and waiting for container readiness 113 | async waitForContainerReadiness(): Promise { 114 | const options = { 115 | initialDelay: 1000, 116 | maxDelay: 30000, 117 | maxRetries: 30, // Increased from 12 to 30 to maintain ~60s total wait time 118 | backoffFactor: 1.5, 119 | }; 120 | let retryCount = 0; 121 | 122 | while (retryCount < options.maxRetries) { 123 | // Calculate wait time with exponential backoff, capped at maxDelay 124 | const waitTime = Math.min( 125 | options.initialDelay * Math.pow(options.backoffFactor, retryCount), 126 | options.maxDelay 127 | ); 128 | 129 | try { 130 | const controller = new AbortController(); 131 | const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 132 | 133 | const response = await fetch(`${this.deploymentUrl}/api/health`, { 134 | signal: controller.signal, 135 | }).then((res) => res.json()); 136 | 137 | clearTimeout(timeoutId); 138 | 139 | if (response.status === "ok") { 140 | return; // Container is ready 141 | } 142 | } catch (_error) { 143 | console.log(`Waiting for container readiness... (attempt ${retryCount + 1}/${options.maxRetries})`); 144 | } 145 | 146 | await new Promise((resolve) => setTimeout(resolve, waitTime)); 147 | retryCount++; 148 | } 149 | 150 | throw new Error("Container failed to become ready within timeout period"); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/server/services/delegated-signer.ts: -------------------------------------------------------------------------------- 1 | import { getBaseUrlFromApiKey } from "@/lib/utils"; 2 | 3 | const API_KEY = process.env.CROSSMINT_SERVER_API_KEY as string; 4 | 5 | const CROSSMINT_BASE_URL = getBaseUrlFromApiKey(API_KEY); 6 | 7 | const headers = { 8 | "X-API-KEY": API_KEY, 9 | "Content-Type": "application/json", 10 | }; 11 | 12 | export async function getOrCreateDelegatedSigner( 13 | smartWalletAddress: string, 14 | agentKeyAddress: string, 15 | chain: string, 16 | walletSignerType: string 17 | ): Promise<{ message: string; id: string; targetSignerLocator: string; delegatedSignerAlreadyActive?: boolean }> { 18 | try { 19 | const isEVMWallet = walletSignerType.includes("evm"); 20 | const signerLocator = `${walletSignerType}:${agentKeyAddress}`; 21 | 22 | // 1. Check if the delegated signer already exists 23 | const getResponse = await fetch( 24 | `${CROSSMINT_BASE_URL}/wallets/${smartWalletAddress}/signers/${signerLocator}`, 25 | { 26 | method: "GET", 27 | headers, 28 | } 29 | ); 30 | 31 | // Only try to parse the response if it was successful 32 | if (getResponse.ok) { 33 | try { 34 | const existingDelegatedSigner = await getResponse.json(); 35 | 36 | const { 37 | message: existingMessage, 38 | id: existingId, 39 | status: existingStatus, 40 | targetSignerLocator: existingSignerLocator, 41 | } = parseDelegatedSignerMessageAndId(existingDelegatedSigner, isEVMWallet); 42 | 43 | // If the delegated signer exists and is awaiting approval, return it 44 | if (existingStatus === "awaiting-approval") { 45 | return { message: existingMessage, id: existingId, targetSignerLocator: existingSignerLocator }; 46 | } 47 | 48 | // If the delegated signer exists and is already approved, return it 49 | if (existingStatus === "active" || existingStatus === "success") { 50 | return { 51 | message: "", 52 | id: "", 53 | targetSignerLocator: existingSignerLocator, 54 | delegatedSignerAlreadyActive: true, 55 | }; 56 | } 57 | } catch (_error) { 58 | // If parsing fails, continue to step 2 59 | console.log(`No existing delegated signer found for ${signerLocator}, creating new one`); 60 | } 61 | } 62 | 63 | // 2. Create a new delegated signer 64 | const requestBody: { signer: string; chain?: string } = { 65 | signer: signerLocator, 66 | chain, 67 | }; 68 | if (!isEVMWallet) { 69 | // Solana doesn't need chain specified 70 | delete requestBody.chain; 71 | } 72 | 73 | const response = await fetch(`${CROSSMINT_BASE_URL}/wallets/${smartWalletAddress}/signers`, { 74 | method: "POST", 75 | headers, 76 | body: JSON.stringify(requestBody), 77 | }); 78 | 79 | const { message, id, targetSignerLocator } = parseDelegatedSignerMessageAndId( 80 | await response.json(), 81 | isEVMWallet 82 | ); 83 | return { message, id, targetSignerLocator }; 84 | } catch (error) { 85 | console.error("Error in getOrCreateDelegatedSigner:", error); 86 | throw error; 87 | } 88 | } 89 | 90 | function parseDelegatedSignerMessageAndId(delegatedSignerResponse: any, isEVMWallet: boolean) { 91 | const target = isEVMWallet 92 | ? Object.values(delegatedSignerResponse?.chains || {})[0] 93 | : delegatedSignerResponse?.transaction; 94 | 95 | if (!target) { 96 | throw new Error("Delegated signer not found"); 97 | } 98 | 99 | return { 100 | message: target?.approvals?.pending[0]?.message, 101 | id: target?.id, 102 | status: target?.status, 103 | targetSignerLocator: target?.approvals?.pending[0]?.signer, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/src/server/services/phala/phala-cloud.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { x25519 } from "@noble/curves/ed25519"; 3 | import * as crypto from "crypto"; 4 | import type { 5 | DeployOptions, 6 | CvmConfig, 7 | GetPubkeyFromCvmResponse, 8 | CreateCvmResponse, 9 | Cvm, 10 | Env, 11 | CvmNetwork, 12 | } from "@/app/types/phala"; 13 | 14 | export const CLOUD_API_URL = "https://cloud-api.phala.network"; 15 | export const CLOUD_URL = "https://cloud.phala.network"; 16 | export const CLI_VERSION = "0.1.0"; 17 | const headers = { 18 | "User-Agent": `tee-cli/${CLI_VERSION}`, 19 | "Content-Type": "application/json", 20 | "X-API-Key": process.env.PHALA_CLOUD_API_KEY as string, 21 | }; 22 | 23 | export class PhalaCloud { 24 | private readonly CLOUD_API_URL: string; 25 | private readonly CLOUD_URL: string; 26 | private apiKey: string; 27 | 28 | constructor() { 29 | this.CLOUD_API_URL = CLOUD_API_URL; 30 | this.CLOUD_URL = CLOUD_URL; 31 | this.apiKey = process.env.PHALA_CLOUD_API_KEY as string; 32 | if (!this.apiKey) { 33 | throw new Error("PHALA_CLOUD_API_KEY is not set"); 34 | } 35 | } 36 | 37 | async deploy(options: DeployOptions): Promise<{ appId: string }> { 38 | console.log("Deploying CVM ..."); 39 | 40 | const vmConfig = this.createVmConfig(options); 41 | 42 | const pubkey = await this.getPubkeyFromCvm(vmConfig); 43 | if (!pubkey) { 44 | throw new Error("Failed to get pubkey from CVM."); 45 | } 46 | 47 | const encrypted_env = await this.encryptSecrets(options.envs || {}, pubkey.app_env_encrypt_pubkey); 48 | 49 | if (options.debug) { 50 | console.log("Pubkey:", pubkey.app_env_encrypt_pubkey); 51 | console.log("Encrypted Env:", encrypted_env); 52 | console.log("Env:", options.envs); 53 | } 54 | 55 | const response = await this.createCvm({ 56 | ...vmConfig, 57 | encrypted_env, 58 | app_env_encrypt_pubkey: pubkey.app_env_encrypt_pubkey, 59 | app_id_salt: pubkey.app_id_salt, 60 | }); 61 | 62 | if (!response) { 63 | throw new Error("Error during deployment"); 64 | } 65 | 66 | console.log("Deployment successful"); 67 | console.log("App Id:", response.app_id); 68 | console.log("Phala Dashboard URL:", `${this.CLOUD_URL}/dashboard/cvms/app_${response.app_id}`); 69 | return { appId: response.app_id }; 70 | } 71 | 72 | private createVmConfig(options: DeployOptions): CvmConfig { 73 | const composeString = options.compose ? fs.readFileSync(options.compose, "utf8") : ""; 74 | 75 | return { 76 | teepod_id: 2, // TODO: get from /api/teepods 77 | name: options.name, 78 | image: "dstack-dev-0.3.4", 79 | vcpu: options.vcpu || 1, 80 | memory: options.memory || 2048, 81 | disk_size: options.diskSize || 20, 82 | compose_manifest: { 83 | docker_compose_file: composeString, 84 | docker_config: { 85 | url: "", 86 | username: "", 87 | password: "", 88 | }, 89 | features: ["kms", "tproxy-net"], 90 | kms_enabled: true, 91 | manifest_version: 2, 92 | name: options.name, 93 | public_logs: true, 94 | public_sysinfo: true, 95 | tproxy_enabled: true, 96 | }, 97 | listed: false, 98 | }; 99 | } 100 | 101 | private async getPubkeyFromCvm(vmConfig: CvmConfig): Promise { 102 | try { 103 | const response = await fetch(`${this.CLOUD_API_URL}/api/v1/cvms/pubkey/from_cvm_configuration`, { 104 | method: "POST", 105 | headers, 106 | body: JSON.stringify(vmConfig), 107 | }); 108 | if (!response.ok) { 109 | throw new Error(`HTTP error! status: ${response.status}`); 110 | } 111 | return (await response.json()) as GetPubkeyFromCvmResponse; 112 | } catch (error: any) { 113 | console.error("Error during get pubkey from cvm:", error.message); 114 | return null; 115 | } 116 | } 117 | 118 | public async waitForDeployment( 119 | appId: string, 120 | options = { 121 | initialDelay: 1000, 122 | maxDelay: 30000, 123 | maxRetries: 12, 124 | backoffFactor: 1.5, 125 | } 126 | ): Promise { 127 | let retryCount = 0; 128 | let deploymentUrl: string | null = null; 129 | 130 | while (retryCount < options.maxRetries && !deploymentUrl) { 131 | // Calculate wait time with exponential backoff, capped at maxDelay 132 | const waitTime = Math.min( 133 | options.initialDelay * Math.pow(options.backoffFactor, retryCount), 134 | options.maxDelay 135 | ); 136 | 137 | await new Promise((resolve) => setTimeout(resolve, waitTime)); 138 | 139 | const cvmResponse = await this.queryCvmByIdentifier(appId); 140 | deploymentUrl = cvmResponse?.public_urls?.[0]?.app || null; 141 | 142 | if (!deploymentUrl) { 143 | console.log(`Waiting for deployment... (attempt ${retryCount + 1}/${options.maxRetries})`); 144 | retryCount++; 145 | } 146 | } 147 | 148 | if (!deploymentUrl) { 149 | throw new Error("Deployment timeout: Failed to get deployment URL after maximum retries"); 150 | } 151 | 152 | return deploymentUrl; 153 | } 154 | 155 | public async queryCvmsByUserId(): Promise { 156 | try { 157 | const userInfo = await this.getUserInfo(); 158 | return await fetch(`${this.CLOUD_API_URL}/api/v1/cvms?user_id=${userInfo?.id}`, { 159 | headers, 160 | }).then((res) => res.json()); 161 | } catch (error: any) { 162 | console.error("Error during get cvms by user id:", error.response?.data || error.message); 163 | return null; 164 | } 165 | } 166 | 167 | public async queryCvmByIdentifier(identifier: string): Promise { 168 | try { 169 | return await fetch(`${this.CLOUD_API_URL}/api/v1/cvms/app_${identifier}/network`, { 170 | headers, 171 | }).then((res) => res.json()); 172 | } catch (error: any) { 173 | console.error("Error during get cvm by identifier:", error.response?.data || error.message); 174 | return null; 175 | } 176 | } 177 | 178 | private async createCvm(vmConfig: CvmConfig): Promise { 179 | try { 180 | const response = await fetch(`${this.CLOUD_API_URL}/api/v1/cvms/from_cvm_configuration`, { 181 | method: "POST", 182 | headers, 183 | body: JSON.stringify(vmConfig), 184 | }); 185 | if (!response.ok) { 186 | throw new Error(`HTTP error! status: ${response.status}`); 187 | } 188 | return (await response.json()) as CreateCvmResponse; 189 | } catch (error: any) { 190 | console.error("Error during create cvm:", error.message); 191 | return null; 192 | } 193 | } 194 | 195 | private async encryptSecrets(secrets: Env[], pubkey: string): Promise { 196 | const envsJson = JSON.stringify({ env: secrets }); 197 | 198 | // Generate private key and derive public key 199 | const privateKey = x25519.utils.randomPrivateKey(); 200 | const publicKey = x25519.getPublicKey(privateKey); 201 | 202 | // Generate shared key 203 | const remotePubkey = this.hexToUint8Array(pubkey); 204 | const shared = x25519.getSharedSecret(privateKey, remotePubkey); 205 | 206 | // Import shared key for AES-GCM 207 | const importedShared = await crypto.subtle.importKey("raw", shared, { name: "AES-GCM", length: 256 }, true, [ 208 | "encrypt", 209 | ]); 210 | 211 | // Encrypt the data 212 | const iv = crypto.getRandomValues(new Uint8Array(12)); 213 | const encrypted = await crypto.subtle.encrypt( 214 | { name: "AES-GCM", iv }, 215 | importedShared, 216 | new TextEncoder().encode(envsJson) 217 | ); 218 | 219 | // Combine all components 220 | const result = new Uint8Array(publicKey.length + iv.length + encrypted.byteLength); 221 | 222 | result.set(publicKey); 223 | result.set(iv, publicKey.length); 224 | result.set(new Uint8Array(encrypted), publicKey.length + iv.length); 225 | 226 | return this.uint8ArrayToHex(result); 227 | } 228 | 229 | private async getUserInfo(): Promise<{ id: string; username: string } | null> { 230 | try { 231 | const getUserAuth = await fetch(`${this.CLOUD_API_URL}/api/v1/auth/me`, { 232 | headers, 233 | }).then((res) => res.json()); 234 | const username = getUserAuth.username; 235 | const getUserId = await fetch(`${this.CLOUD_API_URL}/api/v1/users/search?q=${username}`, { 236 | headers, 237 | }).then((res) => res.json()); 238 | const userId = getUserId.users[0].id; 239 | return { id: userId, username: username }; 240 | } catch (error: any) { 241 | console.error("Error during get user info:", error.response?.data || error.message); 242 | return null; 243 | } 244 | } 245 | 246 | private hexToUint8Array(hex: string): Uint8Array { 247 | hex = hex.startsWith("0x") ? hex.slice(2) : hex; 248 | return new Uint8Array(hex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? []); 249 | } 250 | 251 | private uint8ArrayToHex(buffer: Uint8Array): string { 252 | return Array.from(buffer) 253 | .map((byte) => byte.toString(16).padStart(2, "0")) 254 | .join(""); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | colors: { 12 | background: "hsl(var(--color-background) / )", 13 | foreground: "hsl(var(--color-foreground) / )", 14 | card: "hsl(var(--color-card) / )", 15 | "card-foreground": "hsl(var(--color-card-foreground) / )", 16 | popover: "hsl(var(--color-popover) / )", 17 | "popover-foreground": "hsl(var(--color-popover-foreground) / )", 18 | primary: "hsl(var(--color-primary) / )", 19 | "primary-foreground": "hsl(var(--color-primary-foreground) / )", 20 | secondary: "hsl(var(--color-secondary) / )", 21 | "secondary-foreground": "hsl(var(--color-secondary-foreground) / )", 22 | muted: "hsl(var(--color-muted) / )", 23 | "muted-foreground": "hsl(var(--color-muted-foreground) / )", 24 | accent: "hsl(var(--color-accent) / )", 25 | "accent-foreground": "hsl(var(--color-accent-foreground) / )", 26 | destructive: "hsl(var(--color-destructive) / )", 27 | "destructive-foreground": "hsl(var(--color-destructive-foreground) / )", 28 | border: "hsl(var(--color-border) / )", 29 | input: "hsl(var(--color-input) / )", 30 | ring: "hsl(var(--color-ring) / )", 31 | "chart-1": "hsl(var(--color-chart-1) / )", 32 | skeleton: "hsl(var(--color-skeleton) / )", 33 | }, 34 | fontFamily: { 35 | body: ["var(--font-inter)", ...fontFamily.sans], 36 | }, 37 | boxShadow: { 38 | light: "var(--shadow-light)", 39 | heavy: "var(--shadow-heavy)", 40 | dropdown: "var(--shadow-dropdown)", 41 | primary: "0 1px 2px 0 #1B2C60", 42 | }, 43 | extend: { 44 | backgroundImage: { 45 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 46 | "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 47 | }, 48 | }, 49 | }, 50 | plugins: [], 51 | }; 52 | export default config; 53 | -------------------------------------------------------------------------------- /launchpad-starter-next-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "target": "ES2020", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------