create-lightning-scaffold 1.0.3 β†’ 1.0.5

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 CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  ![Demo](demo.gif)
4
4
 
5
+ > ⚑ **5 minutes from `npx` to first gasless transaction**
6
+
5
7
  CLI to scaffold Solana apps with LazorKit SDK. Generate React Native (Expo) or Next.js projects with passkey authentication, gasless transactions, and a ready-to-use swap interface.
6
8
 
9
+ ## πŸš€ Live Demo
10
+
11
+ **[Try it live on Devnet β†’](https://lighting-demo.vercel.app)**
12
+
13
+ Experience passkey authentication and gasless transactions without installing anything.
14
+
7
15
  ## Quick Start
8
16
 
9
17
  ```bash
@@ -143,6 +151,35 @@ const { swapTransaction } = await fetch('https://api.jup.ag/swap/v1/swap', {
143
151
  }).then(r => r.json());
144
152
  ```
145
153
 
154
+ ## Architecture
155
+
156
+ ```
157
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
158
+ β”‚ User │────▢│ LazorKit Portal │────▢│ Smart Wallet β”‚
159
+ β”‚ (Passkey) β”‚ β”‚ (Auth + Keys) β”‚ β”‚ (PDA on-chain) β”‚
160
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
161
+ β”‚
162
+ β–Ό
163
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
164
+ β”‚ Paymaster β”‚
165
+ β”‚ (Pays gas fees) β”‚
166
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
167
+ ```
168
+
169
+ ## Troubleshooting
170
+
171
+ | Issue | Solution |
172
+ |-------|----------|
173
+ | Passkey not working | Ensure HTTPS (localhost OK for dev), check browser supports WebAuthn |
174
+ | Transaction failing | Verify wallet has balance, check RPC endpoint, confirm paymaster config |
175
+ | Mobile redirect issues | Ensure deep link scheme matches `app.json` config |
176
+
177
+ ## More Resources
178
+
179
+ - **[SNIPPETS.md](./SNIPPETS.md)** - Copy-paste code examples
180
+ - **[TUTORIALS.md](./TUTORIALS.md)** - Step-by-step integration guides
181
+ - **[Twitter Thread](https://x.com/Tobi_Builder/status/2011043242251293087)** - Passkey wallet tutorial
182
+
146
183
  ## License
147
184
 
148
185
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lightning-scaffold",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "CLI to scaffold projects with LazorKit SDK integration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,11 @@ EXPO_PUBLIC_LAZORKIT_PAYMASTER_URL=https://kora.devnet.lazorkit.com
8
8
  NEXT_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com
9
9
  EXPO_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com
10
10
  VITE_SOLANA_RPC=https://api.devnet.solana.com
11
+
12
+ # Jupiter API (get free key at https://portal.jup.ag)
13
+ NEXT_PUBLIC_JUPITER_API_KEY=
14
+ EXPO_PUBLIC_JUPITER_API_KEY=
15
+ VITE_JUPITER_API_KEY=
11
16
  <% if (backend === 'supabase') { %>
12
17
  # Supabase
13
18
  NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
@@ -28,7 +28,7 @@ interface Props {
28
28
  interface TxInfo {
29
29
  signature: string;
30
30
  slot: number;
31
- blockTime: number | null;
31
+ blockTime: number | null | undefined;
32
32
  err: any;
33
33
  }
34
34
 
@@ -33,11 +33,17 @@ export function Recovery({ onBack }: Props) {
33
33
  const portalUrl = process.env.EXPO_PUBLIC_LAZORKIT_PORTAL_URL || 'https://portal.lazor.sh';
34
34
 
35
35
  const handleAddDevice = () => {
36
- Linking.openURL(`${portalUrl}/recovery/add-device`);
36
+ const url = wallet?.smartWallet
37
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=add-device`
38
+ : portalUrl;
39
+ Linking.openURL(url);
37
40
  };
38
41
 
39
42
  const handleManageDevices = () => {
40
- Linking.openURL(`${portalUrl}/recovery/devices`);
43
+ const url = wallet?.smartWallet
44
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=manage-devices`
45
+ : portalUrl;
46
+ Linking.openURL(url);
41
47
  };
42
48
 
43
49
  const copyAddress = async () => {
@@ -15,6 +15,7 @@ const TOKENS = [
15
15
  const SLIPPAGE_OPTIONS = [50, 100, 300];
16
16
 
17
17
  const JUPITER_API = 'https://api.jup.ag/swap/v1';
18
+ const JUP_API_KEY = process.env.EXPO_PUBLIC_JUPITER_API_KEY || '';
18
19
 
19
20
  function useBalances(walletAddress: string | undefined) {
20
21
  const [balances, setBalances] = useState<Record<string, number>>({});
@@ -23,9 +24,7 @@ function useBalances(walletAddress: string | undefined) {
23
24
  const rpc = process.env.EXPO_PUBLIC_SOLANA_RPC || 'https://api.devnet.solana.com';
24
25
  const conn = new Connection(rpc);
25
26
  const pk = new PublicKey(walletAddress);
26
-
27
27
  conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {});
28
-
29
28
  conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') })
30
29
  .then((res) => {
31
30
  const tokenBals: Record<string, number> = {};
@@ -39,13 +38,6 @@ function useBalances(walletAddress: string | undefined) {
39
38
  }, [walletAddress]);
40
39
  return balances;
41
40
  }
42
-
43
- const TOKENS = [
44
- { symbol: 'SOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9 },
45
- { symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 },
46
- ];
47
-
48
- const JUPITER_API = 'https://api.jup.ag/swap/v1';
49
41
  <% if (styling !== 'nativewind') { %>
50
42
  const styles = StyleSheet.create({
51
43
  container: { flex: 1, backgroundColor: '#fff', padding: 24, paddingTop: 60 },
@@ -101,22 +93,20 @@ export function Swap() {
101
93
  const [showPicker, setShowPicker] = useState<'from' | 'to' | null>(null);
102
94
  const redirectUrl = Linking.createURL('/');
103
95
 
104
- const switchTokens = () => {
105
- setFromToken(toToken);
106
- setToToken(fromToken);
107
- setAmount('');
108
- setQuote(null);
109
- };
96
+ const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(''); setQuote(null); };
110
97
 
111
98
  useEffect(() => {
112
99
  if (!amount || parseFloat(amount) <= 0) { setQuote(null); return; }
113
100
  const timeout = setTimeout(async () => {
114
101
  try {
115
102
  const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals);
116
- const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}`);
103
+ const headers: Record<string, string> = {};
104
+ if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY;
105
+ const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers });
117
106
  const data = await res.json();
118
107
  if (data.outAmount) setQuote(data);
119
- } catch (e) { console.error('Quote error:', e); }
108
+ else setQuote(null);
109
+ } catch (e) { console.error('Quote error:', e); setQuote(null); }
120
110
  }, 500);
121
111
  return () => clearTimeout(timeout);
122
112
  }, [amount, fromToken, toToken, slippage]);
@@ -125,34 +115,48 @@ export function Swap() {
125
115
 
126
116
  const handleSwap = async () => {
127
117
  if (!wallet?.smartWallet || !quote) return;
128
- setLoading(true);
129
- setResult(null);
118
+ setLoading(true); setResult(null);
130
119
  try {
131
- const swapRes = await fetch(`${JUPITER_API}/swap`, {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.smartWallet, dynamicComputeUnitLimit: true, dynamicSlippage: true }),
120
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
121
+ if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY;
122
+ const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
123
+ method: 'POST', headers,
124
+ body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.smartWallet, wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
125
+ });
126
+ if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`);
127
+ const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json();
128
+ if (!swapInstruction) throw new Error('No swap instruction returned');
129
+
130
+ const toInstruction = (ix: any) => ({
131
+ programId: new PublicKey(ix.programId),
132
+ keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
133
+ data: Buffer.from(ix.data, 'base64'),
135
134
  });
136
- const { swapTransaction } = await swapRes.json();
137
- const sig = await signAndSendTransaction({ instructions: [], transactionOptions: { feeToken: 'USDC' } }, { redirectUrl });
135
+ const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])];
136
+
137
+ let addressLookupTableAccounts: any[] = [];
138
+ if (addressLookupTableAddresses.length > 0) {
139
+ const rpc = process.env.EXPO_PUBLIC_SOLANA_RPC || 'https://api.devnet.solana.com';
140
+ const conn = new Connection(rpc);
141
+ const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value));
142
+ addressLookupTableAccounts = alts.filter(Boolean);
143
+ }
144
+
145
+ const sig = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: 'USDC', addressLookupTableAccounts } }, { redirectUrl });
138
146
  setResult({ success: true, message: `Swapped! ${sig.slice(0, 8)}...` });
139
- setAmount('');
140
- setQuote(null);
141
- } catch (e) {
142
- setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' });
143
- } finally { setLoading(false); }
147
+ setAmount(''); setQuote(null);
148
+ } catch (e) { console.error('Swap error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' }); }
149
+ finally { setLoading(false); }
144
150
  };
145
151
 
146
152
  const handleSignMessage = async () => {
147
- setSigning(true);
148
- setResult(null);
153
+ setSigning(true); setResult(null);
149
154
  try {
150
155
  const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`;
151
156
  const signature = await signMessage(message, { redirectUrl });
152
157
  setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` });
153
- } catch (e) {
154
- setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' });
155
- } finally { setSigning(false); }
158
+ } catch (e) { console.error('Sign error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' }); }
159
+ finally { setSigning(false); }
156
160
  };
157
161
 
158
162
  const addr = wallet?.smartWallet || '';
@@ -161,152 +165,54 @@ export function Swap() {
161
165
  <% if (styling === 'nativewind') { %>
162
166
  <View className="flex-1 bg-white p-6 pt-16">
163
167
  <View className="bg-neutral-900 rounded-2xl p-5">
164
- <View className="flex-row items-center justify-between mb-6">
165
- <Text className="text-lg font-semibold text-white">Swap</Text>
166
- <TouchableOpacity onPress={() => disconnect()}>
167
- <Text className="text-xs text-neutral-500">{addr.slice(0, 6)}...{addr.slice(-4)}</Text>
168
- </TouchableOpacity>
169
- </View>
170
-
168
+ <View className="flex-row items-center justify-between mb-6"><Text className="text-lg font-semibold text-white">Swap</Text><TouchableOpacity onPress={() => disconnect()}><Text className="text-xs text-neutral-500">{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity></View>
171
169
  <View className="bg-neutral-800 border border-neutral-700 rounded-xl p-4">
172
- <View className="flex-row justify-between mb-2">
173
- <Text className="text-xs text-neutral-500">From</Text>
174
- <Text className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text>
175
- </View>
176
- <View className="flex-row items-center">
177
- <TextInput className="flex-1 text-2xl font-medium text-white" placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" />
178
- <TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('from')}>
179
- <Text className="text-sm font-medium text-white mr-1">{fromToken.symbol}</Text><Text className="text-xs text-neutral-400">β–Ό</Text>
180
- </TouchableOpacity>
181
- </View>
170
+ <View className="flex-row justify-between mb-2"><Text className="text-xs text-neutral-500">From</Text><Text className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text></View>
171
+ <View className="flex-row items-center"><TextInput className="flex-1 text-2xl font-medium text-white" placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" /><TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('from')}><Text className="text-sm font-medium text-white mr-1">{fromToken.symbol}</Text><Text className="text-xs text-neutral-400">β–Ό</Text></TouchableOpacity></View>
182
172
  <TouchableOpacity onPress={() => setAmount(String(balances[fromToken.symbol] ?? 0))}><Text className="text-xs text-neutral-500 mt-1">Max</Text></TouchableOpacity>
183
173
  </View>
184
-
185
- <View className="items-center my-2">
186
- <TouchableOpacity onPress={switchTokens} className="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-lg items-center justify-center"><Text className="text-neutral-500">↓</Text></TouchableOpacity>
187
- </View>
188
-
174
+ <View className="items-center my-2"><TouchableOpacity onPress={switchTokens} className="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-lg items-center justify-center"><Text className="text-neutral-500">↓</Text></TouchableOpacity></View>
189
175
  <View className="bg-neutral-800 border border-neutral-700 rounded-xl p-4">
190
- <View className="flex-row justify-between mb-2">
191
- <Text className="text-xs text-neutral-500">To</Text>
192
- <Text className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text>
193
- </View>
194
- <View className="flex-row items-center">
195
- <Text className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</Text>
196
- <TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('to')}>
197
- <Text className="text-sm font-medium text-white mr-1">{toToken.symbol}</Text><Text className="text-xs text-neutral-400">β–Ό</Text>
198
- </TouchableOpacity>
199
- </View>
176
+ <View className="flex-row justify-between mb-2"><Text className="text-xs text-neutral-500">To</Text><Text className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text></View>
177
+ <View className="flex-row items-center"><Text className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</Text><TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('to')}><Text className="text-sm font-medium text-white mr-1">{toToken.symbol}</Text><Text className="text-xs text-neutral-400">β–Ό</Text></TouchableOpacity></View>
200
178
  {quote && <Text className="text-xs text-neutral-500 mt-2">via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</Text>}
201
179
  </View>
202
-
203
- <View className="flex-row items-center justify-between mt-3">
204
- <Text className="text-xs text-neutral-500">Slippage</Text>
205
- <View className="flex-row gap-1">
206
- {SLIPPAGE_OPTIONS.map((s) => (
207
- <TouchableOpacity key={s} onPress={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white' : 'bg-neutral-700'}`}>
208
- <Text className={`text-xs ${slippage === s ? 'text-black' : 'text-neutral-400'}`}>{s / 100}%</Text>
209
- </TouchableOpacity>
210
- ))}
211
- </View>
212
- </View>
213
-
214
- <TouchableOpacity className={`mt-4 bg-white py-3.5 rounded-xl items-center ${loading || !quote ? 'opacity-50' : ''}`} onPress={handleSwap} disabled={loading || !quote}>
215
- {loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text className="text-black text-sm font-medium">Swap</Text>}
216
- </TouchableOpacity>
217
-
218
- <TouchableOpacity className={`mt-2 border border-neutral-700 py-3 rounded-xl items-center ${signing ? 'opacity-50' : ''}`} onPress={handleSignMessage} disabled={signing}>
219
- {signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text className="text-white text-sm font-medium">Sign Message</Text>}
220
- </TouchableOpacity>
221
-
180
+ <View className="flex-row items-center justify-between mt-3"><Text className="text-xs text-neutral-500">Slippage</Text><View className="flex-row gap-1">{SLIPPAGE_OPTIONS.map((s) => (<TouchableOpacity key={s} onPress={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white' : 'bg-neutral-700'}`}><Text className={`text-xs ${slippage === s ? 'text-black' : 'text-neutral-400'}`}>{s / 100}%</Text></TouchableOpacity>))}</View></View>
181
+ <TouchableOpacity className={`mt-4 bg-white py-3.5 rounded-xl items-center ${loading || !quote ? 'opacity-50' : ''}`} onPress={handleSwap} disabled={loading || !quote}>{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text className="text-black text-sm font-medium">Swap</Text>}</TouchableOpacity>
182
+ <TouchableOpacity className={`mt-2 border border-neutral-700 py-3 rounded-xl items-center ${signing ? 'opacity-50' : ''}`} onPress={handleSignMessage} disabled={signing}>{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text className="text-white text-sm font-medium">Sign Message</Text>}</TouchableOpacity>
222
183
  {result && <Text className={`mt-3 text-sm text-center ${result.success ? 'text-neutral-400' : 'text-red-400'}`}>{result.message}</Text>}
223
184
  <Text className="mt-4 text-xs text-center text-neutral-500">Gas sponsored Β· Powered by LazorKit + Jupiter</Text>
224
185
  </View>
225
-
226
186
  <Modal visible={!!showPicker} transparent animationType="slide" onRequestClose={() => setShowPicker(null)}>
227
187
  <TouchableOpacity className="flex-1 justify-end bg-black/50" activeOpacity={1} onPress={() => setShowPicker(null)}>
228
- <View className="bg-neutral-800 rounded-t-2xl p-4 pb-8">
229
- <Text className="text-base font-semibold text-center mb-4 text-white">Select Token</Text>
230
- {TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (
231
- <TouchableOpacity key={t.symbol} className="py-3.5 border-b border-neutral-700" onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}>
232
- <Text className="text-base text-white">{t.symbol}</Text>
233
- </TouchableOpacity>
234
- ))}
235
- </View>
188
+ <View className="bg-neutral-800 rounded-t-2xl p-4 pb-8"><Text className="text-base font-semibold text-center mb-4 text-white">Select Token</Text>{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (<TouchableOpacity key={t.symbol} className="py-3.5 border-b border-neutral-700" onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}><Text className="text-base text-white">{t.symbol}</Text></TouchableOpacity>))}</View>
236
189
  </TouchableOpacity>
237
190
  </Modal>
238
191
  </View>
239
192
  <% } else { %>
240
193
  <View style={styles.container}>
241
194
  <View style={styles.card}>
242
- <View style={styles.header}>
243
- <Text style={styles.title}>Swap</Text>
244
- <TouchableOpacity onPress={() => disconnect()}><Text style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity>
245
- </View>
246
-
195
+ <View style={styles.header}><Text style={styles.title}>Swap</Text><TouchableOpacity onPress={() => disconnect()}><Text style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity></View>
247
196
  <View style={styles.inputCard}>
248
- <View style={styles.labelRow}>
249
- <Text style={styles.label}>From</Text>
250
- <Text style={styles.label}>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text>
251
- </View>
252
- <View style={styles.inputRow}>
253
- <TextInput style={styles.input} placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" />
254
- <TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('from')}>
255
- <Text style={styles.tokenText}>{fromToken.symbol}</Text><Text style={styles.tokenArrow}>β–Ό</Text>
256
- </TouchableOpacity>
257
- </View>
197
+ <View style={styles.labelRow}><Text style={styles.label}>From</Text><Text style={styles.label}>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text></View>
198
+ <View style={styles.inputRow}><TextInput style={styles.input} placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" /><TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('from')}><Text style={styles.tokenText}>{fromToken.symbol}</Text><Text style={styles.tokenArrow}>β–Ό</Text></TouchableOpacity></View>
258
199
  <TouchableOpacity onPress={() => setAmount(String(balances[fromToken.symbol] ?? 0))}><Text style={styles.switchText}>Max</Text></TouchableOpacity>
259
200
  </View>
260
-
261
201
  <View style={styles.arrow}><TouchableOpacity onPress={switchTokens} style={styles.arrowBox}><Text style={styles.arrowText}>↓</Text></TouchableOpacity></View>
262
-
263
202
  <View style={styles.inputCard}>
264
- <View style={styles.labelRow}>
265
- <Text style={styles.label}>To</Text>
266
- <Text style={styles.label}>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text>
267
- </View>
268
- <View style={styles.inputRow}>
269
- <Text style={styles.outputText}>{outputAmount}</Text>
270
- <TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('to')}>
271
- <Text style={styles.tokenText}>{toToken.symbol}</Text><Text style={styles.tokenArrow}>β–Ό</Text>
272
- </TouchableOpacity>
273
- </View>
203
+ <View style={styles.labelRow}><Text style={styles.label}>To</Text><Text style={styles.label}>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text></View>
204
+ <View style={styles.inputRow}><Text style={styles.outputText}>{outputAmount}</Text><TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('to')}><Text style={styles.tokenText}>{toToken.symbol}</Text><Text style={styles.tokenArrow}>β–Ό</Text></TouchableOpacity></View>
274
205
  {quote && <Text style={styles.quote}>via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</Text>}
275
206
  </View>
276
-
277
- <View style={styles.slippageRow}>
278
- <Text style={styles.slippageLabel}>Slippage</Text>
279
- <View style={{ flexDirection: 'row', gap: 4 }}>
280
- {SLIPPAGE_OPTIONS.map((s) => (
281
- <TouchableOpacity key={s} onPress={() => setSlippage(s)} style={[styles.slippageBtn, slippage === s && styles.slippageBtnActive]}>
282
- <Text style={[styles.slippageBtnText, slippage === s && styles.slippageBtnTextActive]}>{s / 100}%</Text>
283
- </TouchableOpacity>
284
- ))}
285
- </View>
286
- </View>
287
-
288
- <TouchableOpacity style={[styles.button, (loading || !quote) && styles.buttonDisabled]} onPress={handleSwap} disabled={loading || !quote}>
289
- {loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text style={styles.buttonText}>Swap</Text>}
290
- </TouchableOpacity>
291
-
292
- <TouchableOpacity style={[styles.buttonSecondary, signing && styles.buttonDisabled]} onPress={handleSignMessage} disabled={signing}>
293
- {signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text style={styles.buttonTextSecondary}>Sign Message</Text>}
294
- </TouchableOpacity>
295
-
207
+ <View style={styles.slippageRow}><Text style={styles.slippageLabel}>Slippage</Text><View style={{ flexDirection: 'row', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<TouchableOpacity key={s} onPress={() => setSlippage(s)} style={[styles.slippageBtn, slippage === s && styles.slippageBtnActive]}><Text style={[styles.slippageBtnText, slippage === s && styles.slippageBtnTextActive]}>{s / 100}%</Text></TouchableOpacity>))}</View></View>
208
+ <TouchableOpacity style={[styles.button, (loading || !quote) && styles.buttonDisabled]} onPress={handleSwap} disabled={loading || !quote}>{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text style={styles.buttonText}>Swap</Text>}</TouchableOpacity>
209
+ <TouchableOpacity style={[styles.buttonSecondary, signing && styles.buttonDisabled]} onPress={handleSignMessage} disabled={signing}>{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text style={styles.buttonTextSecondary}>Sign Message</Text>}</TouchableOpacity>
296
210
  {result && <Text style={[styles.result, { color: result.success ? '#737373' : '#ef4444' }]}>{result.message}</Text>}
297
211
  <Text style={styles.footer}>Gas sponsored Β· Powered by LazorKit + Jupiter</Text>
298
212
  </View>
299
-
300
213
  <Modal visible={!!showPicker} transparent animationType="slide" onRequestClose={() => setShowPicker(null)}>
301
214
  <TouchableOpacity style={styles.modal} activeOpacity={1} onPress={() => setShowPicker(null)}>
302
- <View style={styles.modalContent}>
303
- <Text style={styles.modalTitle}>Select Token</Text>
304
- {TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (
305
- <TouchableOpacity key={t.symbol} style={styles.modalItem} onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}>
306
- <Text style={styles.modalItemText}>{t.symbol}</Text>
307
- </TouchableOpacity>
308
- ))}
309
- </View>
215
+ <View style={styles.modalContent}><Text style={styles.modalTitle}>Select Token</Text>{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (<TouchableOpacity key={t.symbol} style={styles.modalItem} onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}><Text style={styles.modalItemText}>{t.symbol}</Text></TouchableOpacity>))}</View>
310
216
  </TouchableOpacity>
311
217
  </Modal>
312
218
  </View>
@@ -1,26 +1,21 @@
1
1
  import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
2
2
  import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux";
3
- import type { LazorWallet } from "../lazorkit";
4
3
 
5
4
  interface AppState {
6
- wallet: LazorWallet | null;
7
5
  isLoading: boolean;
8
6
  }
9
7
 
10
8
  const appSlice = createSlice({
11
9
  name: "app",
12
- initialState: { wallet: null, isLoading: false } as AppState,
10
+ initialState: { isLoading: false } as AppState,
13
11
  reducers: {
14
- setWallet: (state, action: PayloadAction<LazorWallet | null>) => {
15
- state.wallet = action.payload;
16
- },
17
12
  setLoading: (state, action: PayloadAction<boolean>) => {
18
13
  state.isLoading = action.payload;
19
14
  },
20
15
  },
21
16
  });
22
17
 
23
- export const { setWallet, setLoading } = appSlice.actions;
18
+ export const { setLoading } = appSlice.actions;
24
19
 
25
20
  export const store = configureStore({ reducer: { app: appSlice.reducer } });
26
21
 
@@ -1,16 +1,11 @@
1
1
  import { create } from "zustand";
2
- import type { LazorWallet } from "../lazorkit";
3
2
 
4
3
  interface AppState {
5
- wallet: LazorWallet | null;
6
- setWallet: (wallet: LazorWallet | null) => void;
7
4
  isLoading: boolean;
8
5
  setLoading: (loading: boolean) => void;
9
6
  }
10
7
 
11
8
  export const useStore = create<AppState>((set) => ({
12
- wallet: null,
13
- setWallet: (wallet) => set({ wallet }),
14
9
  isLoading: false,
15
10
  setLoading: (isLoading) => set({ isLoading }),
16
11
  }));
@@ -25,7 +25,7 @@ interface Props {
25
25
  interface TxInfo {
26
26
  signature: string;
27
27
  slot: number;
28
- blockTime: number | null;
28
+ blockTime: number | null | undefined;
29
29
  err: any;
30
30
  }
31
31
 
@@ -24,11 +24,17 @@ export function Recovery({ onBack }: Props) {
24
24
  const portalUrl = import.meta.env.VITE_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
25
25
 
26
26
  const handleAddDevice = () => {
27
- window.open(`${portalUrl}/recovery/add-device`, "_blank");
27
+ const url = wallet?.smartWallet
28
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=add-device`
29
+ : portalUrl;
30
+ window.open(url, "_blank");
28
31
  };
29
32
 
30
33
  const handleManageDevices = () => {
31
- window.open(`${portalUrl}/recovery/devices`, "_blank");
34
+ const url = wallet?.smartWallet
35
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=manage-devices`
36
+ : portalUrl;
37
+ window.open(url, "_blank");
32
38
  };
33
39
 
34
40
  const copyAddress = () => {
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { useWallet } from '@lazorkit/wallet'
3
- import { Connection, PublicKey, VersionedTransaction } from '@solana/web3.js'
3
+ import { Connection, PublicKey } from '@solana/web3.js'
4
4
 
5
5
  const TOKENS = [
6
6
  { symbol: 'SOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9 },
@@ -13,6 +13,7 @@ const TOKENS = [
13
13
  const SLIPPAGE_OPTIONS = [50, 100, 300]
14
14
 
15
15
  const JUPITER_API = 'https://api.jup.ag/swap/v1'
16
+ const JUP_API_KEY = import.meta.env.VITE_JUPITER_API_KEY || ''
16
17
 
17
18
  function useBalances(walletAddress: string | undefined) {
18
19
  const [balances, setBalances] = useState<Record<string, number>>({})
@@ -21,9 +22,7 @@ function useBalances(walletAddress: string | undefined) {
21
22
  const rpc = import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com'
22
23
  const conn = new Connection(rpc)
23
24
  const pk = new PublicKey(walletAddress)
24
-
25
25
  conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {})
26
-
27
26
  conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') })
28
27
  .then((res) => {
29
28
  const tokenBals: Record<string, number> = {}
@@ -72,22 +71,20 @@ export function Swap() {
72
71
  const [signing, setSigning] = useState(false)
73
72
  const [result, setResult] = useState<{ success: boolean; message: string } | null>(null)
74
73
 
75
- const switchTokens = () => {
76
- setFromToken(toToken)
77
- setToToken(fromToken)
78
- setAmount('')
79
- setQuote(null)
80
- }
74
+ const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(''); setQuote(null) }
81
75
 
82
76
  useEffect(() => {
83
77
  if (!amount || parseFloat(amount) <= 0) { setQuote(null); return }
84
78
  const timeout = setTimeout(async () => {
85
79
  try {
86
80
  const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals)
87
- const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}`)
81
+ const headers: Record<string, string> = {}
82
+ if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY
83
+ const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers })
88
84
  const data = await res.json()
89
85
  if (data.outAmount) setQuote(data)
90
- } catch (e) { console.error('Quote error:', e) }
86
+ else setQuote(null)
87
+ } catch (e) { console.error('Quote error:', e); setQuote(null) }
91
88
  }, 500)
92
89
  return () => clearTimeout(timeout)
93
90
  }, [amount, fromToken, toToken, slippage])
@@ -96,34 +93,48 @@ export function Swap() {
96
93
 
97
94
  const handleSwap = async () => {
98
95
  if (!smartWalletPubkey || !quote) return
99
- setLoading(true)
100
- setResult(null)
96
+ setLoading(true); setResult(null)
101
97
  try {
102
- const swapRes = await fetch(`${JUPITER_API}/swap`, {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), dynamicComputeUnitLimit: true, dynamicSlippage: true }),
98
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
99
+ if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY
100
+ const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
101
+ method: 'POST', headers,
102
+ body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
103
+ })
104
+ if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`)
105
+ const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json()
106
+ if (!swapInstruction) throw new Error('No swap instruction returned')
107
+
108
+ const toInstruction = (ix: any) => ({
109
+ programId: new PublicKey(ix.programId),
110
+ keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
111
+ data: Buffer.from(ix.data, 'base64'),
106
112
  })
107
- const { swapTransaction } = await swapRes.json()
108
- const signature = await signAndSendTransaction({ instructions: [], transactionOptions: { feeToken: 'USDC' } })
113
+ const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])]
114
+
115
+ let addressLookupTableAccounts: any[] = []
116
+ if (addressLookupTableAddresses.length > 0) {
117
+ const rpc = import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com'
118
+ const conn = new Connection(rpc)
119
+ const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value))
120
+ addressLookupTableAccounts = alts.filter(Boolean)
121
+ }
122
+
123
+ const signature = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: 'USDC', addressLookupTableAccounts } })
109
124
  setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` })
110
- setAmount('')
111
- setQuote(null)
112
- } catch (e) {
113
- setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' })
114
- } finally { setLoading(false) }
125
+ setAmount(''); setQuote(null)
126
+ } catch (e) { console.error('Swap error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' }) }
127
+ finally { setLoading(false) }
115
128
  }
116
129
 
117
130
  const handleSignMessage = async () => {
118
- setSigning(true)
119
- setResult(null)
131
+ setSigning(true); setResult(null)
120
132
  try {
121
133
  const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`
122
134
  const { signature } = await signMessage(message)
123
135
  setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` })
124
- } catch (e) {
125
- setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' })
126
- } finally { setSigning(false) }
136
+ } catch (e) { console.error('Sign error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' }) }
137
+ finally { setSigning(false) }
127
138
  }
128
139
 
129
140
  const addr = wallet?.smartWallet || ''
@@ -131,43 +142,21 @@ export function Swap() {
131
142
  return (
132
143
  <% if (styling === 'tailwind') { %>
133
144
  <div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
134
- <div className="flex items-center justify-between mb-6">
135
- <h1 className="text-lg font-semibold">Swap</h1>
136
- <button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button>
137
- </div>
145
+ <div className="flex items-center justify-between mb-6"><h1 className="text-lg font-semibold">Swap</h1><button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
138
146
  <div className="space-y-2">
139
147
  <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
140
- <div className="flex items-center justify-between mb-2">
141
- <span className="text-xs text-neutral-500">From</span>
142
- <span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
143
- </div>
144
- <div className="flex items-center gap-3">
145
- <input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" />
146
- <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
147
- </div>
148
+ <div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">From</span><span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
149
+ <div className="flex items-center gap-3"><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
148
150
  <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-500 hover:text-white mt-1">Max</button>
149
151
  </div>
150
152
  <div className="flex justify-center"><button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button></div>
151
153
  <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
152
- <div className="flex items-center justify-between mb-2">
153
- <span className="text-xs text-neutral-500">To</span>
154
- <span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
155
- </div>
156
- <div className="flex items-center gap-3">
157
- <span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span>
158
- <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
159
- </div>
154
+ <div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">To</span><span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
155
+ <div className="flex items-center gap-3"><span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
160
156
  {quote && <p className="text-xs text-neutral-500 mt-2">via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
161
157
  </div>
162
158
  </div>
163
- <div className="flex items-center justify-between mt-3 text-xs">
164
- <span className="text-neutral-500">Slippage</span>
165
- <div className="flex gap-1">
166
- {SLIPPAGE_OPTIONS.map((s) => (
167
- <button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white text-black' : 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'}`}>{s / 100}%</button>
168
- ))}
169
- </div>
170
- </div>
159
+ <div className="flex items-center justify-between mt-3 text-xs"><span className="text-neutral-500">Slippage</span><div className="flex gap-1">{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white text-black' : 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'}`}>{s / 100}%</button>))}</div></div>
171
160
  <button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">{loading ? 'Swapping...' : 'Swap'}</button>
172
161
  <button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">{signing ? 'Signing...' : 'Sign Message'}</button>
173
162
  {result && <p className={`mt-3 text-sm text-center ${result.success ? 'text-neutral-400' : 'text-red-400'}`}>{result.message}</p>}
@@ -175,35 +164,19 @@ export function Swap() {
175
164
  </div>
176
165
  <% } else { %>
177
166
  <div style={styles.container}>
178
- <div style={styles.header}>
179
- <h1 style={styles.title}>Swap</h1>
180
- <button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
181
- </div>
167
+ <div style={styles.header}><h1 style={styles.title}>Swap</h1><button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
182
168
  <div style={styles.card}>
183
169
  <div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
184
- <div style={styles.inputRow}>
185
- <input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
186
- <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
187
- </div>
170
+ <div style={styles.inputRow}><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
188
171
  <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
189
172
  </div>
190
173
  <div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer' }}>↓</button></div>
191
174
  <div style={styles.card}>
192
175
  <div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
193
- <div style={styles.inputRow}>
194
- <span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span>
195
- <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
196
- </div>
176
+ <div style={styles.inputRow}><span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
197
177
  {quote && <p style={styles.quote}>via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
198
178
  </div>
199
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
200
- <span style={{ color: '#737373' }}>Slippage</span>
201
- <div style={{ display: 'flex', gap: 4 }}>
202
- {SLIPPAGE_OPTIONS.map((s) => (
203
- <button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>
204
- ))}
205
- </div>
206
- </div>
179
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}><span style={{ color: '#737373' }}>Slippage</span><div style={{ display: 'flex', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>))}</div></div>
207
180
  <button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>{loading ? 'Swapping...' : 'Swap'}</button>
208
181
  <button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>{signing ? 'Signing...' : 'Sign Message'}</button>
209
182
  {result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
@@ -219,22 +192,10 @@ function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKEN
219
192
  <div style={{ position: 'relative' }}>
220
193
  <% if (styling === 'tailwind') { %>
221
194
  <button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">{token.symbol}<span className="text-neutral-400">β–Ό</span></button>
222
- {open && (
223
- <div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">
224
- {tokens.filter((t) => t.symbol !== exclude).map((t) => (
225
- <button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>
226
- ))}
227
- </div>
228
- )}
195
+ {open && (<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>))}</div>)}
229
196
  <% } else { %>
230
197
  <button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>β–Ό</span></button>
231
- {open && (
232
- <div style={styles.dropdown as React.CSSProperties}>
233
- {tokens.filter((t) => t.symbol !== exclude).map((t) => (
234
- <button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} style={styles.dropdownItem}>{t.symbol}</button>
235
- ))}
236
- </div>
237
- )}
198
+ {open && (<div style={styles.dropdown as React.CSSProperties}>{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} style={styles.dropdownItem}>{t.symbol}</button>))}</div>)}
238
199
  <% } %>
239
200
  </div>
240
201
  )
@@ -27,7 +27,7 @@ interface Props {
27
27
  interface TxInfo {
28
28
  signature: string;
29
29
  slot: number;
30
- blockTime: number | null;
30
+ blockTime: number | null | undefined;
31
31
  err: any;
32
32
  }
33
33
 
@@ -25,11 +25,19 @@ export function Recovery({ onBack }: Props) {
25
25
  const portalUrl = process.env.NEXT_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
26
26
 
27
27
  const handleAddDevice = () => {
28
- window.open(`${portalUrl}/recovery/add-device`, "_blank");
28
+ // Open portal with wallet context
29
+ const url = wallet?.smartWallet
30
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=add-device`
31
+ : portalUrl;
32
+ window.open(url, "_blank");
29
33
  };
30
34
 
31
35
  const handleManageDevices = () => {
32
- window.open(`${portalUrl}/recovery/devices`, "_blank");
36
+ // Open portal with wallet context
37
+ const url = wallet?.smartWallet
38
+ ? `${portalUrl}?wallet=${wallet.smartWallet}&action=manage-devices`
39
+ : portalUrl;
40
+ window.open(url, "_blank");
33
41
  };
34
42
 
35
43
  const copyAddress = () => {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { useState, useEffect } from "react";
3
3
  import { useWallet } from "@lazorkit/wallet";
4
- import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js";
4
+ import { Connection, PublicKey } from "@solana/web3.js";
5
5
 
6
6
  const TOKENS = [
7
7
  { symbol: "SOL", mint: "So11111111111111111111111111111111111111112", decimals: 9 },
@@ -11,9 +11,10 @@ const TOKENS = [
11
11
  { symbol: "JUP", mint: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", decimals: 6 },
12
12
  ];
13
13
 
14
- const SLIPPAGE_OPTIONS = [50, 100, 300]; // 0.5%, 1%, 3% in bps
14
+ const SLIPPAGE_OPTIONS = [50, 100, 300];
15
15
 
16
16
  const JUPITER_API = "https://api.jup.ag/swap/v1";
17
+ const JUP_API_KEY = process.env.NEXT_PUBLIC_JUPITER_API_KEY || "";
17
18
 
18
19
  function useBalances(walletAddress: string | undefined) {
19
20
  const [balances, setBalances] = useState<Record<string, number>>({});
@@ -22,11 +23,7 @@ function useBalances(walletAddress: string | undefined) {
22
23
  const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
23
24
  const conn = new Connection(rpc);
24
25
  const pk = new PublicKey(walletAddress);
25
-
26
- // Fetch SOL balance
27
26
  conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {});
28
-
29
- // Fetch token balances
30
27
  conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") })
31
28
  .then((res) => {
32
29
  const tokenBals: Record<string, number> = {};
@@ -75,85 +72,71 @@ export function Swap() {
75
72
  const [signing, setSigning] = useState(false);
76
73
  const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
77
74
 
78
- const switchTokens = () => {
79
- setFromToken(toToken);
80
- setToToken(fromToken);
81
- setAmount("");
82
- setQuote(null);
83
- };
75
+ const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(""); setQuote(null); };
84
76
 
85
- // Fetch quote from Jupiter
86
77
  useEffect(() => {
87
- if (!amount || parseFloat(amount) <= 0) {
88
- setQuote(null);
89
- return;
90
- }
78
+ if (!amount || parseFloat(amount) <= 0) { setQuote(null); return; }
91
79
  const timeout = setTimeout(async () => {
92
80
  try {
93
81
  const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals);
94
- const res = await fetch(
95
- `${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}`
96
- );
82
+ const headers: Record<string, string> = {};
83
+ if (JUP_API_KEY) headers["x-api-key"] = JUP_API_KEY;
84
+ const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers });
97
85
  const data = await res.json();
98
86
  if (data.outAmount) setQuote(data);
99
- } catch (e) {
100
- console.error("Quote error:", e);
101
- }
87
+ else setQuote(null);
88
+ } catch (e) { console.error("Quote error:", e); setQuote(null); }
102
89
  }, 500);
103
90
  return () => clearTimeout(timeout);
104
91
  }, [amount, fromToken, toToken, slippage]);
105
92
 
106
- const outputAmount = quote
107
- ? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4)
108
- : "0.00";
93
+ const outputAmount = quote ? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4) : "0.00";
109
94
 
110
95
  const handleSwap = async () => {
111
96
  if (!smartWalletPubkey || !quote) return;
112
97
  setLoading(true);
113
98
  setResult(null);
114
99
  try {
115
- // Get swap transaction from Jupiter
116
- const swapRes = await fetch(`${JUPITER_API}/swap`, {
117
- method: "POST",
118
- headers: { "Content-Type": "application/json" },
119
- body: JSON.stringify({
120
- quoteResponse: quote,
121
- userPublicKey: smartWalletPubkey.toBase58(),
122
- dynamicComputeUnitLimit: true,
123
- dynamicSlippage: true,
124
- }),
100
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
101
+ if (JUP_API_KEY) headers["x-api-key"] = JUP_API_KEY;
102
+ const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
103
+ method: "POST", headers,
104
+ body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
125
105
  });
126
- const { swapTransaction } = await swapRes.json();
127
-
128
- // Decode and sign via LazorKit
129
- const tx = VersionedTransaction.deserialize(Buffer.from(swapTransaction, "base64"));
130
- const signature = await signAndSendTransaction({
131
- instructions: [], // Jupiter provides full tx, we pass empty and let SDK handle
132
- transactionOptions: { feeToken: "USDC" },
106
+ if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`);
107
+ const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json();
108
+ if (!swapInstruction) throw new Error("No swap instruction returned");
109
+
110
+ const toInstruction = (ix: any) => ({
111
+ programId: new PublicKey(ix.programId),
112
+ keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
113
+ data: Buffer.from(ix.data, "base64"),
133
114
  });
134
-
115
+ const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])];
116
+
117
+ let addressLookupTableAccounts: any[] = [];
118
+ if (addressLookupTableAddresses.length > 0) {
119
+ const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
120
+ const conn = new Connection(rpc);
121
+ const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value));
122
+ addressLookupTableAccounts = alts.filter(Boolean);
123
+ }
124
+
125
+ const signature = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: "USDC", addressLookupTableAccounts } });
135
126
  setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` });
136
- setAmount("");
137
- setQuote(null);
138
- } catch (e) {
139
- setResult({ success: false, message: e instanceof Error ? e.message : "Swap failed" });
140
- } finally {
141
- setLoading(false);
142
- }
127
+ setAmount(""); setQuote(null);
128
+ } catch (e) { console.error("Swap error:", e); setResult({ success: false, message: e instanceof Error ? e.message : "Swap failed" }); }
129
+ finally { setLoading(false); }
143
130
  };
144
131
 
145
132
  const handleSignMessage = async () => {
146
- setSigning(true);
147
- setResult(null);
133
+ setSigning(true); setResult(null);
148
134
  try {
149
135
  const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`;
150
136
  const { signature } = await signMessage(message);
151
137
  setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` });
152
- } catch (e) {
153
- setResult({ success: false, message: e instanceof Error ? e.message : "Sign failed" });
154
- } finally {
155
- setSigning(false);
156
- }
138
+ } catch (e) { console.error("Sign error:", e); setResult({ success: false, message: e instanceof Error ? e.message : "Sign failed" }); }
139
+ finally { setSigning(false); }
157
140
  };
158
141
 
159
142
  const addr = wallet?.smartWallet || "";
@@ -163,30 +146,20 @@ export function Swap() {
163
146
  <div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
164
147
  <div className="flex items-center justify-between mb-6">
165
148
  <h1 className="text-lg font-semibold">Swap</h1>
166
- <button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">
167
- {addr.slice(0, 6)}...{addr.slice(-4)}
168
- </button>
149
+ <button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button>
169
150
  </div>
170
151
  <div className="space-y-2">
171
152
  <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
172
- <div className="flex items-center justify-between mb-2">
173
- <span className="text-xs text-neutral-500">From</span>
174
- <span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
175
- </div>
153
+ <div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">From</span><span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
176
154
  <div className="flex items-center gap-3">
177
155
  <input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" />
178
156
  <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
179
157
  </div>
180
158
  <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-500 hover:text-white mt-1">Max</button>
181
159
  </div>
182
- <div className="flex justify-center">
183
- <button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button>
184
- </div>
160
+ <div className="flex justify-center"><button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button></div>
185
161
  <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
186
- <div className="flex items-center justify-between mb-2">
187
- <span className="text-xs text-neutral-500">To</span>
188
- <span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
189
- </div>
162
+ <div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">To</span><span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
190
163
  <div className="flex items-center gap-3">
191
164
  <span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span>
192
165
  <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
@@ -194,64 +167,29 @@ export function Swap() {
194
167
  {quote && <p className="text-xs text-neutral-500 mt-2">via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
195
168
  </div>
196
169
  </div>
197
- <div className="flex items-center justify-between mt-3 text-xs">
198
- <span className="text-neutral-500">Slippage</span>
199
- <div className="flex gap-1">
200
- {SLIPPAGE_OPTIONS.map((s) => (
201
- <button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? "bg-white text-black" : "bg-neutral-700 text-neutral-300 hover:bg-neutral-600"}`}>
202
- {s / 100}%
203
- </button>
204
- ))}
205
- </div>
206
- </div>
207
- <button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">
208
- {loading ? "Swapping..." : "Swap"}
209
- </button>
210
- <button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">
211
- {signing ? "Signing..." : "Sign Message"}
212
- </button>
170
+ <div className="flex items-center justify-between mt-3 text-xs"><span className="text-neutral-500">Slippage</span><div className="flex gap-1">{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? "bg-white text-black" : "bg-neutral-700 text-neutral-300 hover:bg-neutral-600"}`}>{s / 100}%</button>))}</div></div>
171
+ <button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">{loading ? "Swapping..." : "Swap"}</button>
172
+ <button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">{signing ? "Signing..." : "Sign Message"}</button>
213
173
  {result && <p className={`mt-3 text-sm text-center ${result.success ? "text-neutral-400" : "text-red-400"}`}>{result.message}</p>}
214
174
  <p className="mt-4 text-xs text-center text-neutral-500">Gas sponsored Β· Powered by LazorKit + Jupiter</p>
215
175
  </div>
216
176
  <% } else { %>
217
177
  <div style={styles.container}>
218
- <div style={styles.header}>
219
- <h1 style={styles.title}>Swap</h1>
220
- <button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
221
- </div>
178
+ <div style={styles.header}><h1 style={styles.title}>Swap</h1><button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
222
179
  <div style={styles.card}>
223
180
  <div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
224
- <div style={styles.inputRow}>
225
- <input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
226
- <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
227
- </div>
181
+ <div style={styles.inputRow}><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
228
182
  <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
229
183
  </div>
230
- <div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer', border: '1px solid #e5e5e5' }}>↓</button></div>
184
+ <div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer' }}>↓</button></div>
231
185
  <div style={styles.card}>
232
186
  <div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
233
- <div style={styles.inputRow}>
234
- <span style={{ ...styles.input, color: '#d4d4d4' }}>{outputAmount}</span>
235
- <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
236
- </div>
187
+ <div style={styles.inputRow}><span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
237
188
  {quote && <p style={styles.quote}>via Jupiter Β· {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
238
189
  </div>
239
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
240
- <span style={{ color: '#737373' }}>Slippage</span>
241
- <div style={{ display: 'flex', gap: 4 }}>
242
- {SLIPPAGE_OPTIONS.map((s) => (
243
- <button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>
244
- {s / 100}%
245
- </button>
246
- ))}
247
- </div>
248
- </div>
249
- <button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>
250
- {loading ? "Swapping..." : "Swap"}
251
- </button>
252
- <button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>
253
- {signing ? "Signing..." : "Sign Message"}
254
- </button>
190
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}><span style={{ color: '#737373' }}>Slippage</span><div style={{ display: 'flex', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>))}</div></div>
191
+ <button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>{loading ? "Swapping..." : "Swap"}</button>
192
+ <button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>{signing ? "Signing..." : "Sign Message"}</button>
255
193
  {result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
256
194
  <p style={styles.footer}>Gas sponsored Β· Powered by LazorKit + Jupiter</p>
257
195
  </div>
@@ -264,25 +202,11 @@ function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKEN
264
202
  return (
265
203
  <div style={{ position: 'relative' }}>
266
204
  <% if (styling === 'tailwind') { %>
267
- <button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">
268
- {token.symbol}<span className="text-neutral-400">β–Ό</span>
269
- </button>
270
- {open && (
271
- <div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">
272
- {tokens.filter((t) => t.symbol !== exclude).map((t) => (
273
- <button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>
274
- ))}
275
- </div>
276
- )}
205
+ <button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">{token.symbol}<span className="text-neutral-400">β–Ό</span></button>
206
+ {open && (<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>))}</div>)}
277
207
  <% } else { %>
278
208
  <button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>β–Ό</span></button>
279
- {open && (
280
- <div style={styles.dropdown as React.CSSProperties}>
281
- {tokens.filter((t) => t.symbol !== exclude).map((t) => (
282
- <button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} style={styles.dropdownItem}>{t.symbol}</button>
283
- ))}
284
- </div>
285
- )}
209
+ {open && (<div style={styles.dropdown as React.CSSProperties}>{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} style={styles.dropdownItem}>{t.symbol}</button>))}</div>)}
286
210
  <% } %>
287
211
  </div>
288
212
  );