create-sbc-app 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -5
- package/bin/cli.js +13 -5
- package/package.json +1 -1
- package/templates/README.md +53 -4
- package/templates/react/README.md.template +8 -9
- package/templates/react-dynamic/.env.template +1 -1
- package/templates/react-dynamic/README.md.template +131 -13
- package/templates/react-para/.env.template +1 -1
- package/templates/react-para/README.md.template +154 -13
- package/templates/react-turnkey/.env.template +26 -0
- package/templates/react-turnkey/README.md.template +206 -0
- package/templates/react-turnkey/eslint.config.js.template +32 -0
- package/templates/react-turnkey/index.html.template +14 -0
- package/templates/react-turnkey/package.json.template +45 -0
- package/templates/react-turnkey/public/sbc-logo.png +0 -0
- package/templates/react-turnkey/server/index.ts.template +271 -0
- package/templates/react-turnkey/src/App.tsx.template +1701 -0
- package/templates/react-turnkey/src/env.d.ts.template +17 -0
- package/templates/react-turnkey/src/index.css.template +16 -0
- package/templates/react-turnkey/src/main.tsx.template +11 -0
- package/templates/react-turnkey/tsconfig.json.template +25 -0
- package/templates/react-turnkey/tsconfig.node.json.template +10 -0
- package/templates/react-turnkey/vite.config.ts.template +10 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
SBC + Turnkey fullstack example with embedded wallets and gasless transactions.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Backend Architecture**: Express server handling Turnkey sub-org creation
|
|
8
|
+
- ✅ **Passkey Authentication**: Users create wallets with biometric auth (Face ID/Touch ID)
|
|
9
|
+
- ✅ **Wallet Authentication**: Connect with MetaMask/Coinbase Wallet as alternative to passkeys
|
|
10
|
+
- ✅ **Embedded Wallets**: Non-custodial wallets managed by Turnkey
|
|
11
|
+
- ✅ **Smart Accounts**: ERC-4337 account abstraction with SBC paymaster
|
|
12
|
+
- ✅ **Gasless Transactions**: All gas fees sponsored by SBC
|
|
13
|
+
- ✅ **Account History**: Never lose access to accounts - all accounts saved and switchable
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
Before you begin, you need:
|
|
18
|
+
|
|
19
|
+
1. **SBC API Key**: Get from https://dashboard.stablecoin.xyz
|
|
20
|
+
2. **Turnkey Organization + API Keys**: Get from https://app.turnkey.com
|
|
21
|
+
|
|
22
|
+
### Getting Turnkey API Keys
|
|
23
|
+
|
|
24
|
+
1. Go to https://app.turnkey.com and sign up
|
|
25
|
+
2. Create a new organization (or use existing)
|
|
26
|
+
3. Navigate to **Settings** → **API Keys**
|
|
27
|
+
4. Click **"Create API Key"**
|
|
28
|
+
5. **Save both keys securely**:
|
|
29
|
+
- Copy the **API Public Key**
|
|
30
|
+
- Copy the **API Private Key** (shown only once!)
|
|
31
|
+
6. Copy your **Organization ID** from Settings
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Install Dependencies
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install
|
|
39
|
+
# or
|
|
40
|
+
pnpm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Configure Environment Variables
|
|
44
|
+
|
|
45
|
+
Edit the `.env` file that was created for you:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# SBC Configuration
|
|
49
|
+
VITE_SBC_API_KEY=your_sbc_api_key_here
|
|
50
|
+
|
|
51
|
+
# Turnkey Frontend (Public - safe for browser)
|
|
52
|
+
VITE_TURNKEY_API_BASE_URL=https://api.turnkey.com
|
|
53
|
+
VITE_TURNKEY_RPID=localhost
|
|
54
|
+
|
|
55
|
+
# Turnkey Backend (Secret - never expose to frontend!)
|
|
56
|
+
TURNKEY_API_BASE_URL=https://api.turnkey.com
|
|
57
|
+
TURNKEY_ORGANIZATION_ID=your_turnkey_org_id_here
|
|
58
|
+
TURNKEY_API_PUBLIC_KEY=your_turnkey_public_key_here
|
|
59
|
+
TURNKEY_API_PRIVATE_KEY=your_turnkey_private_key_here
|
|
60
|
+
|
|
61
|
+
# Backend Server
|
|
62
|
+
PORT=3001
|
|
63
|
+
VITE_BACKEND_URL=http://localhost:3001
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Run the Application
|
|
67
|
+
|
|
68
|
+
**Option A: Run both frontend and backend together (recommended)**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run dev:fullstack
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This starts:
|
|
75
|
+
- Backend server on `http://localhost:3001`
|
|
76
|
+
- Frontend on `http://localhost:5173`
|
|
77
|
+
|
|
78
|
+
**Option B: Run separately**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Terminal 1 - Backend
|
|
82
|
+
npm run dev:backend
|
|
83
|
+
|
|
84
|
+
# Terminal 2 - Frontend
|
|
85
|
+
npm run dev
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
### Authentication Flows
|
|
91
|
+
|
|
92
|
+
#### Passkey Flow (Biometric)
|
|
93
|
+
1. User clicks "Continue with Passkey"
|
|
94
|
+
2. Browser creates passkey via WebAuthn (Face ID/Touch ID)
|
|
95
|
+
3. Frontend → Backend: `POST /api/create-sub-org` with attestation
|
|
96
|
+
4. Backend → Turnkey: Creates sub-org + Turnkey-managed wallet
|
|
97
|
+
5. User signs transactions with biometric auth
|
|
98
|
+
|
|
99
|
+
#### Wallet Flow (MetaMask/Coinbase)
|
|
100
|
+
1. User clicks "Connect Wallet"
|
|
101
|
+
2. MetaMask prompts for connection + signature
|
|
102
|
+
3. Frontend derives public key from signature
|
|
103
|
+
4. Frontend → Backend: `POST /api/create-sub-org-with-wallet` with public key
|
|
104
|
+
5. Backend → Turnkey: Creates sub-org (uses user's wallet as owner)
|
|
105
|
+
6. User signs transactions with their connected wallet
|
|
106
|
+
|
|
107
|
+
### Transaction Flow
|
|
108
|
+
|
|
109
|
+
1. User initiates transaction (e.g., "Send 1 SBC")
|
|
110
|
+
2. Frontend builds transaction via SBC App Kit
|
|
111
|
+
3. User signs transaction:
|
|
112
|
+
- **Passkey**: Biometric prompt (Face ID/Touch ID)
|
|
113
|
+
- **Wallet**: MetaMask/Coinbase popup
|
|
114
|
+
4. SBC paymaster sponsors all gas fees
|
|
115
|
+
5. Transaction executes on-chain via ERC-4337
|
|
116
|
+
|
|
117
|
+
## Project Structure
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
{{projectName}}/
|
|
121
|
+
├── server/ # Backend Express server
|
|
122
|
+
│ └── index.ts # Turnkey API endpoints
|
|
123
|
+
├── src/ # Frontend React app
|
|
124
|
+
│ ├── App.tsx # Main app component
|
|
125
|
+
│ ├── main.tsx # Entry point
|
|
126
|
+
│ └── index.css # Styles
|
|
127
|
+
├── public/ # Static assets
|
|
128
|
+
├── .env # Environment variables (do not commit!)
|
|
129
|
+
└── package.json # Dependencies and scripts
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Available Scripts
|
|
133
|
+
|
|
134
|
+
- `npm run dev` - Start frontend development server
|
|
135
|
+
- `npm run dev:backend` - Start backend server with hot reload
|
|
136
|
+
- `npm run dev:fullstack` - Run both frontend and backend concurrently
|
|
137
|
+
- `npm run build` - Build for production
|
|
138
|
+
- `npm run preview` - Preview production build
|
|
139
|
+
|
|
140
|
+
## Security Notes
|
|
141
|
+
|
|
142
|
+
⚠️ **IMPORTANT**: Never expose Turnkey API keys to the frontend!
|
|
143
|
+
|
|
144
|
+
- API keys stay on the backend only
|
|
145
|
+
- Frontend uses passkeys for user authentication
|
|
146
|
+
- Each user gets their own isolated sub-organization
|
|
147
|
+
|
|
148
|
+
## Production Deployment
|
|
149
|
+
|
|
150
|
+
### Backend Deployment
|
|
151
|
+
Deploy the Express server to:
|
|
152
|
+
- Railway, Render, Fly.io (Node.js)
|
|
153
|
+
- Vercel, Netlify (Serverless functions)
|
|
154
|
+
- AWS Lambda, Google Cloud Functions
|
|
155
|
+
|
|
156
|
+
### Frontend Deployment
|
|
157
|
+
Deploy the Vite app to:
|
|
158
|
+
- Vercel, Netlify, CloudFlare Pages
|
|
159
|
+
- Any static hosting service
|
|
160
|
+
|
|
161
|
+
### Environment Variables
|
|
162
|
+
|
|
163
|
+
**Frontend (.env):**
|
|
164
|
+
```bash
|
|
165
|
+
VITE_SBC_API_KEY=prod_key_here
|
|
166
|
+
VITE_TURNKEY_API_BASE_URL=https://api.turnkey.com
|
|
167
|
+
VITE_TURNKEY_RPID=yourdomain.com # Your production domain
|
|
168
|
+
VITE_BACKEND_URL=https://your-backend.com
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Backend (.env):**
|
|
172
|
+
```bash
|
|
173
|
+
TURNKEY_API_BASE_URL=https://api.turnkey.com
|
|
174
|
+
TURNKEY_ORGANIZATION_ID=prod_org_id
|
|
175
|
+
TURNKEY_API_PUBLIC_KEY=prod_public_key
|
|
176
|
+
TURNKEY_API_PRIVATE_KEY=prod_private_key
|
|
177
|
+
PORT=3001
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Troubleshooting
|
|
181
|
+
|
|
182
|
+
### "Failed to create sub-org"
|
|
183
|
+
- Check backend logs for detailed error
|
|
184
|
+
- Verify `TURNKEY_API_PUBLIC_KEY` and `TURNKEY_API_PRIVATE_KEY` are correct
|
|
185
|
+
- Ensure `TURNKEY_ORGANIZATION_ID` matches your Turnkey org
|
|
186
|
+
|
|
187
|
+
### "Network Error" when signing up
|
|
188
|
+
- Make sure backend is running (`npm run dev:backend`)
|
|
189
|
+
- Check `VITE_BACKEND_URL` points to correct backend URL
|
|
190
|
+
- Verify CORS is configured (backend allows frontend origin)
|
|
191
|
+
|
|
192
|
+
### Passkey creation fails
|
|
193
|
+
- Use HTTPS in production (passkeys require secure context)
|
|
194
|
+
- For localhost: use `http://localhost` (not `127.0.0.1`)
|
|
195
|
+
- Check `VITE_TURNKEY_RPID` matches your domain
|
|
196
|
+
|
|
197
|
+
## Resources
|
|
198
|
+
|
|
199
|
+
- [Turnkey Documentation](https://docs.turnkey.com)
|
|
200
|
+
- [Turnkey Dashboard](https://app.turnkey.com)
|
|
201
|
+
- [SBC App Kit Docs](https://docs.stablecoin.xyz)
|
|
202
|
+
- [Turnkey SDK GitHub](https://github.com/tkhq/sdk)
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from '@typescript-eslint/eslint-plugin'
|
|
6
|
+
import tsparser from '@typescript-eslint/parser'
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
{ ignores: ['dist'] },
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2020,
|
|
14
|
+
globals: globals.browser,
|
|
15
|
+
parser: tsparser,
|
|
16
|
+
},
|
|
17
|
+
plugins: {
|
|
18
|
+
'@typescript-eslint': tseslint,
|
|
19
|
+
'react-hooks': reactHooks,
|
|
20
|
+
'react-refresh': reactRefresh,
|
|
21
|
+
},
|
|
22
|
+
rules: {
|
|
23
|
+
...js.configs.recommended.rules,
|
|
24
|
+
...tseslint.configs.recommended.rules,
|
|
25
|
+
...reactHooks.configs.recommended.rules,
|
|
26
|
+
'react-refresh/only-export-components': [
|
|
27
|
+
'warn',
|
|
28
|
+
{ allowConstantExport: true },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/sbc-logo.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{projectName}}</title>
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:backend": "tsx watch server/index.ts",
|
|
9
|
+
"dev:fullstack": "concurrently \"npm run dev:backend\" \"npm run dev\"",
|
|
10
|
+
"build": "tsc && vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"start": "vite"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@stablecoin.xyz/core": "latest",
|
|
17
|
+
"@stablecoin.xyz/react": "latest",
|
|
18
|
+
"@turnkey/sdk-react": "^5.4.10",
|
|
19
|
+
"@turnkey/sdk-server": "^4.12.0",
|
|
20
|
+
"@turnkey/viem": "^0.14.12",
|
|
21
|
+
"cors": "^2.8.5",
|
|
22
|
+
"dotenv": "^17.2.3",
|
|
23
|
+
"express": "^5.1.0",
|
|
24
|
+
"react": "^18.2.0",
|
|
25
|
+
"react-dom": "^18.2.0",
|
|
26
|
+
"viem": "^2.33.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/js": "^9.9.0",
|
|
30
|
+
"@types/cors": "^2.8.19",
|
|
31
|
+
"@types/express": "^5.0.5",
|
|
32
|
+
"@types/react": "^18.2.66",
|
|
33
|
+
"@types/react-dom": "^18.2.22",
|
|
34
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
35
|
+
"concurrently": "^9.2.1",
|
|
36
|
+
"eslint": "^9.9.0",
|
|
37
|
+
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
|
38
|
+
"eslint-plugin-react-refresh": "^0.4.9",
|
|
39
|
+
"globals": "^15.9.0",
|
|
40
|
+
"tsx": "^4.20.6",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"typescript-eslint": "^8.0.1",
|
|
43
|
+
"vite": "^5.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import { Turnkey } from '@turnkey/sdk-server';
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const port = process.env.PORT || 3001;
|
|
8
|
+
|
|
9
|
+
app.use(cors());
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
|
|
12
|
+
// Log environment variables BEFORE initialization
|
|
13
|
+
console.log('\n🔍 Environment Variables:');
|
|
14
|
+
console.log(' TURNKEY_ORGANIZATION_ID:', process.env.TURNKEY_ORGANIZATION_ID);
|
|
15
|
+
console.log(' TURNKEY_API_BASE_URL:', process.env.TURNKEY_API_BASE_URL);
|
|
16
|
+
console.log(' TURNKEY_API_PUBLIC_KEY:', process.env.TURNKEY_API_PUBLIC_KEY?.substring(0, 20) + '...');
|
|
17
|
+
console.log(' TURNKEY_API_PRIVATE_KEY:', process.env.TURNKEY_API_PRIVATE_KEY ? '[SET]' : '[NOT SET]');
|
|
18
|
+
|
|
19
|
+
// Initialize Turnkey SDK
|
|
20
|
+
const turnkey = new Turnkey({
|
|
21
|
+
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
|
|
22
|
+
apiBaseUrl: process.env.TURNKEY_API_BASE_URL || 'https://api.turnkey.com',
|
|
23
|
+
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
|
|
24
|
+
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create API client for signing requests
|
|
28
|
+
const turnkeyClient = turnkey.apiClient();
|
|
29
|
+
|
|
30
|
+
console.log('\n✓ Turnkey SDK initialized');
|
|
31
|
+
console.log(' Using Organization ID:', process.env.TURNKEY_ORGANIZATION_ID);
|
|
32
|
+
console.log(' API Base URL:', process.env.TURNKEY_API_BASE_URL);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create sub-organization with passkey authentication
|
|
36
|
+
*
|
|
37
|
+
* Creates a new Turnkey sub-organization for a user using WebAuthn passkey authentication.
|
|
38
|
+
* This is the primary flow for biometric authentication (Face ID/Touch ID).
|
|
39
|
+
* Also creates a Turnkey-managed wallet for the user.
|
|
40
|
+
*
|
|
41
|
+
* @route POST /api/create-sub-org
|
|
42
|
+
* @param {string} req.body.userName - User's display name
|
|
43
|
+
* @param {string} req.body.userEmail - User's email address
|
|
44
|
+
* @param {string} req.body.attestation - WebAuthn attestation object from passkey creation
|
|
45
|
+
* @param {string} req.body.challenge - WebAuthn challenge used for passkey creation
|
|
46
|
+
* @returns {object} Response object
|
|
47
|
+
* @returns {string} response.subOrganizationId - ID of the created sub-organization
|
|
48
|
+
* @returns {string[]} response.addresses - Array of wallet addresses (Turnkey-managed wallet)
|
|
49
|
+
* @throws {500} If sub-organization creation fails
|
|
50
|
+
*/
|
|
51
|
+
app.post('/api/create-sub-org', async (req, res) => {
|
|
52
|
+
console.log('\n🚀 [BACKEND] POST /api/create-sub-org - Request received');
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const { userName, userEmail, attestation, challenge } = req.body;
|
|
56
|
+
console.log('📝 [BACKEND] Request data:', {
|
|
57
|
+
userName,
|
|
58
|
+
userEmail,
|
|
59
|
+
hasAttestation: !!attestation,
|
|
60
|
+
hasChallenge: !!challenge,
|
|
61
|
+
attestationLength: attestation?.length,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Step 1: Create sub-organization
|
|
65
|
+
console.log('🏢 [BACKEND] Step 1: Creating sub-organization...');
|
|
66
|
+
console.log('🔑 [BACKEND] Forcing organization ID:', process.env.TURNKEY_ORGANIZATION_ID);
|
|
67
|
+
|
|
68
|
+
const subOrgResponse = await turnkeyClient.createSubOrganization({
|
|
69
|
+
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
|
|
70
|
+
subOrganizationName: `${userName}'s Organization`,
|
|
71
|
+
rootUsers: [{
|
|
72
|
+
userName,
|
|
73
|
+
userEmail,
|
|
74
|
+
apiKeys: [],
|
|
75
|
+
authenticators: [{
|
|
76
|
+
authenticatorName: `${userName}'s Passkey`,
|
|
77
|
+
challenge,
|
|
78
|
+
attestation,
|
|
79
|
+
}],
|
|
80
|
+
oauthProviders: [],
|
|
81
|
+
}],
|
|
82
|
+
rootQuorumThreshold: 1,
|
|
83
|
+
wallet: {
|
|
84
|
+
walletName: `${userName}'s Wallet`,
|
|
85
|
+
accounts: [{
|
|
86
|
+
curve: 'CURVE_SECP256K1',
|
|
87
|
+
pathFormat: 'PATH_FORMAT_BIP32',
|
|
88
|
+
path: "m/44'/60'/0'/0/0",
|
|
89
|
+
addressFormat: 'ADDRESS_FORMAT_ETHEREUM',
|
|
90
|
+
}],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
console.log('✅ [BACKEND] Sub-org created successfully!', {
|
|
94
|
+
subOrgId: subOrgResponse.subOrganizationId,
|
|
95
|
+
wallet: subOrgResponse.wallet,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const subOrgId = subOrgResponse.subOrganizationId;
|
|
99
|
+
const walletAddresses = subOrgResponse.wallet?.addresses || [];
|
|
100
|
+
|
|
101
|
+
const responseData = {
|
|
102
|
+
subOrganizationId: subOrgId,
|
|
103
|
+
addresses: walletAddresses,
|
|
104
|
+
};
|
|
105
|
+
console.log('📤 [BACKEND] Sending success response:', responseData);
|
|
106
|
+
res.json(responseData);
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
console.error('❌ [BACKEND] Error creating sub-org:', error);
|
|
109
|
+
console.error('❌ [BACKEND] Error message:', error.message);
|
|
110
|
+
console.error('❌ [BACKEND] Error stack:', error.stack);
|
|
111
|
+
res.status(500).json({ error: error.message });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create sub-organization with wallet authentication
|
|
117
|
+
*
|
|
118
|
+
* Creates a new Turnkey sub-organization for a user using external wallet authentication
|
|
119
|
+
* (MetaMask, Coinbase Wallet, etc.). The user's wallet becomes the smart account owner.
|
|
120
|
+
* Does NOT create a Turnkey-managed wallet - uses user's existing wallet instead.
|
|
121
|
+
*
|
|
122
|
+
* @route POST /api/create-sub-org-with-wallet
|
|
123
|
+
* @param {string} req.body.userName - User's display name
|
|
124
|
+
* @param {string} req.body.userEmail - User's email address
|
|
125
|
+
* @param {string} req.body.publicKey - Compressed secp256k1 public key derived from wallet signature
|
|
126
|
+
* @param {string} req.body.walletAddress - User's wallet address (0x...)
|
|
127
|
+
* @returns {object} Response object
|
|
128
|
+
* @returns {string} response.subOrganizationId - ID of the created sub-organization
|
|
129
|
+
* @returns {string[]} response.addresses - Array containing the user's wallet address
|
|
130
|
+
* @throws {400} If required parameters are missing
|
|
131
|
+
* @throws {500} If sub-organization creation fails
|
|
132
|
+
*/
|
|
133
|
+
app.post('/api/create-sub-org-with-wallet', async (req, res) => {
|
|
134
|
+
console.log('\n🚀 [BACKEND] POST /api/create-sub-org-with-wallet - Request received');
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { userName, userEmail, publicKey, walletAddress } = req.body;
|
|
138
|
+
console.log('📝 [BACKEND] Request data:', {
|
|
139
|
+
userName,
|
|
140
|
+
userEmail,
|
|
141
|
+
publicKey,
|
|
142
|
+
walletAddress,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!userName || !userEmail || !publicKey || !walletAddress) {
|
|
146
|
+
return res.status(400).json({ error: 'userName, userEmail, publicKey, and walletAddress are required' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compressed public keys from secp256k1 start with 0x02 or 0x03 (33 bytes / 66 hex chars)
|
|
150
|
+
console.log('📝 [BACKEND] Using public key:', publicKey);
|
|
151
|
+
const formattedPublicKey = publicKey;
|
|
152
|
+
|
|
153
|
+
// Create sub-organization with wallet-based API key (NO Turnkey wallet creation)
|
|
154
|
+
console.log('🏢 [BACKEND] Creating sub-organization with wallet authentication...');
|
|
155
|
+
console.log('🔑 [BACKEND] User will use their MetaMask wallet as owner:', walletAddress);
|
|
156
|
+
|
|
157
|
+
const subOrgResponse = await turnkeyClient.createSubOrganization({
|
|
158
|
+
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
|
|
159
|
+
subOrganizationName: `${userName}'s Organization`,
|
|
160
|
+
rootUsers: [{
|
|
161
|
+
userName,
|
|
162
|
+
userEmail,
|
|
163
|
+
apiKeys: [{
|
|
164
|
+
apiKeyName: `${userName}'s Wallet Key`,
|
|
165
|
+
publicKey: formattedPublicKey,
|
|
166
|
+
curveType: 'API_KEY_CURVE_SECP256K1',
|
|
167
|
+
}],
|
|
168
|
+
authenticators: [], // No passkey authenticators for wallet-based auth
|
|
169
|
+
oauthProviders: [],
|
|
170
|
+
}],
|
|
171
|
+
rootQuorumThreshold: 1,
|
|
172
|
+
// NO wallet creation - user's MetaMask wallet will be the owner
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log('✅ [BACKEND] Sub-org created with wallet auth!', {
|
|
176
|
+
subOrgId: subOrgResponse.subOrganizationId,
|
|
177
|
+
userWalletAddress: walletAddress,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const subOrgId = subOrgResponse.subOrganizationId;
|
|
181
|
+
|
|
182
|
+
// Return the user's MetaMask wallet address, not a Turnkey wallet
|
|
183
|
+
const responseData = {
|
|
184
|
+
subOrganizationId: subOrgId,
|
|
185
|
+
addresses: [walletAddress], // User's MetaMask wallet is the owner
|
|
186
|
+
};
|
|
187
|
+
console.log('📤 [BACKEND] Sending success response:', responseData);
|
|
188
|
+
res.json(responseData);
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
console.error('❌ [BACKEND] Error creating sub-org with wallet:', error);
|
|
191
|
+
console.error('❌ [BACKEND] Error message:', error.message);
|
|
192
|
+
console.error('❌ [BACKEND] Error stack:', error.stack);
|
|
193
|
+
res.status(500).json({ error: error.message });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get sub-organization by wallet public key
|
|
199
|
+
*
|
|
200
|
+
* Looks up a sub-organization ID using the wallet's public key.
|
|
201
|
+
* Used to check if a wallet user already has an existing sub-organization.
|
|
202
|
+
* Also fetches wallet addresses associated with the sub-organization.
|
|
203
|
+
*
|
|
204
|
+
* @route POST /api/get-sub-org-by-wallet
|
|
205
|
+
* @param {string} req.body.publicKey - Compressed secp256k1 public key to search for
|
|
206
|
+
* @returns {object} Response object
|
|
207
|
+
* @returns {string|null} response.subOrganizationId - ID of the found sub-organization, or null if not found
|
|
208
|
+
* @returns {string[]} response.addresses - Array of wallet addresses (empty if not found)
|
|
209
|
+
* @throws {400} If publicKey is missing or invalid
|
|
210
|
+
* @throws {500} If sub-organization lookup fails
|
|
211
|
+
*/
|
|
212
|
+
app.post('/api/get-sub-org-by-wallet', async (req, res) => {
|
|
213
|
+
console.log('\n🔍 [BACKEND] POST /api/get-sub-org-by-wallet - Request received');
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const { publicKey } = req.body;
|
|
217
|
+
console.log('📝 [BACKEND] Public key:', publicKey);
|
|
218
|
+
|
|
219
|
+
if (!publicKey || typeof publicKey !== 'string') {
|
|
220
|
+
return res.status(400).json({ error: 'publicKey is required' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log('🔎 [BACKEND] Searching for sub-org with public key...');
|
|
224
|
+
const result = await turnkeyClient.getSubOrgIds({
|
|
225
|
+
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
|
|
226
|
+
filterType: 'PUBLIC_KEY',
|
|
227
|
+
filterValue: publicKey,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log('✅ [BACKEND] Sub-org IDs found:', result.organizationIds);
|
|
231
|
+
|
|
232
|
+
const subOrgId = result.organizationIds?.[0] ?? null;
|
|
233
|
+
|
|
234
|
+
if (subOrgId) {
|
|
235
|
+
// Fetch wallet info for this sub-org
|
|
236
|
+
console.log('💼 [BACKEND] Fetching wallet for sub-org:', subOrgId);
|
|
237
|
+
const walletsResponse = await turnkeyClient.getWallets({
|
|
238
|
+
organizationId: subOrgId,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const addresses: string[] = [];
|
|
242
|
+
if (walletsResponse.wallets && walletsResponse.wallets.length > 0) {
|
|
243
|
+
const wallet = walletsResponse.wallets[0];
|
|
244
|
+
const accountsResponse = await turnkeyClient.getWalletAccounts({
|
|
245
|
+
organizationId: subOrgId,
|
|
246
|
+
walletId: wallet.walletId,
|
|
247
|
+
});
|
|
248
|
+
if (accountsResponse.accounts) {
|
|
249
|
+
addresses.push(...accountsResponse.accounts.map(acc => acc.address));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.json({
|
|
254
|
+
subOrganizationId: subOrgId,
|
|
255
|
+
addresses,
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
res.json({
|
|
259
|
+
subOrganizationId: null,
|
|
260
|
+
addresses: [],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch (error: any) {
|
|
264
|
+
console.error('❌ [BACKEND] Error fetching sub-org:', error);
|
|
265
|
+
res.status(500).json({ error: error.message });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.listen(port, () => {
|
|
270
|
+
console.log(`Turnkey backend server running on http://localhost:${port}`);
|
|
271
|
+
});
|