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 +37 -0
- package/package.json +1 -1
- package/templates/base/.env.example.ejs +5 -0
- package/templates/mobile/components/History.tsx.ejs +1 -1
- package/templates/mobile/components/Recovery.tsx.ejs +8 -2
- package/templates/mobile/components/Swap.tsx.ejs +58 -152
- package/templates/state/redux/index.ts +2 -7
- package/templates/state/zustand/index.ts +0 -5
- package/templates/vite/src/components/History.tsx.ejs +1 -1
- package/templates/vite/src/components/Recovery.tsx.ejs +8 -2
- package/templates/vite/src/components/Swap.tsx.ejs +52 -91
- package/templates/web/components/History.tsx.ejs +1 -1
- package/templates/web/components/Recovery.tsx.ejs +10 -2
- package/templates/web/components/Swap.tsx.ejs +58 -134
package/README.md
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
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
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
278
|
-
|
|
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: {
|
|
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 {
|
|
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
|
}));
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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];
|
|
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
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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'
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
);
|