create-lightning-scaffold 1.0.2 → 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.
@@ -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
  )
@@ -4,20 +4,20 @@ import { useWallet } from "@lazorkit/wallet";
4
4
  import { Connection, PublicKey } from "@solana/web3.js";
5
5
  <% if (styling !== 'tailwind') { %>
6
6
  const styles: Record<string, React.CSSProperties> = {
7
- container: { width: '100%', maxWidth: 360 },
7
+ container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
8
8
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
9
- title: { fontSize: 18, fontWeight: 600, margin: 0 },
9
+ title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
10
10
  list: { display: 'flex', flexDirection: 'column', gap: 8 },
11
- item: { border: '1px solid #e5e5e5', borderRadius: 12, padding: 12 },
11
+ item: { border: '1px solid #262626', borderRadius: 12, padding: 12, background: '#171717' },
12
12
  row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
13
- sig: { fontFamily: 'monospace', fontSize: 13, color: '#0a0a0a' },
14
- time: { fontSize: 12, color: '#a3a3a3' },
13
+ sig: { fontFamily: 'monospace', fontSize: 13, color: '#fafafa' },
14
+ time: { fontSize: 12, color: '#737373', marginTop: 4 },
15
15
  status: { fontSize: 11, padding: '2px 6px', borderRadius: 4 },
16
- success: { backgroundColor: '#dcfce7', color: '#166534' },
17
- failed: { backgroundColor: '#fee2e2', color: '#991b1b' },
18
- empty: { textAlign: 'center', padding: 32, color: '#a3a3a3', fontSize: 14 },
16
+ success: { backgroundColor: '#14532d', color: '#4ade80' },
17
+ failed: { backgroundColor: '#7f1d1d', color: '#f87171' },
18
+ empty: { textAlign: 'center', padding: 32, color: '#737373', fontSize: 14 },
19
19
  back: { fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
20
- link: { color: '#0a0a0a', textDecoration: 'none' },
20
+ link: { color: '#fafafa', textDecoration: 'none' },
21
21
  };
22
22
  <% } %>
23
23
  interface Props {
@@ -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
 
@@ -58,26 +58,26 @@ export function History({ onBack }: Props) {
58
58
 
59
59
  return (
60
60
  <% if (styling === 'tailwind') { %>
61
- <div className="w-full max-w-sm">
61
+ <div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
62
62
  <div className="flex items-center justify-between mb-4">
63
63
  <h1 className="text-lg font-semibold">History</h1>
64
- <button onClick={onBack} className="text-sm text-neutral-500 hover:text-black">← Back</button>
64
+ <button onClick={onBack} className="text-sm text-neutral-500 hover:text-white">← Back</button>
65
65
  </div>
66
66
  {loading ? (
67
- <p className="text-center py-8 text-neutral-400">Loading...</p>
67
+ <p className="text-center py-8 text-neutral-500">Loading...</p>
68
68
  ) : txs.length === 0 ? (
69
- <p className="text-center py-8 text-neutral-400">No transactions yet</p>
69
+ <p className="text-center py-8 text-neutral-500">No transactions yet</p>
70
70
  ) : (
71
71
  <div className="space-y-2">
72
72
  {txs.map((tx) => (
73
- <a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-200 rounded-xl p-3 hover:bg-neutral-50">
73
+ <a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-700 rounded-xl p-3 bg-neutral-800 hover:bg-neutral-700">
74
74
  <div className="flex justify-between items-center">
75
75
  <span className="font-mono text-sm">{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
76
- <span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700"}`}>
76
+ <span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-900 text-red-400" : "bg-green-900 text-green-400"}`}>
77
77
  {tx.err ? "Failed" : "Success"}
78
78
  </span>
79
79
  </div>
80
- <p className="text-xs text-neutral-400 mt-1">{formatTime(tx.blockTime)}</p>
80
+ <p className="text-xs text-neutral-500 mt-1">{formatTime(tx.blockTime)}</p>
81
81
  </a>
82
82
  ))}
83
83
  </div>
@@ -3,15 +3,15 @@ import { useState } from "react";
3
3
  import { useWallet } from "@lazorkit/wallet";
4
4
  <% if (styling !== 'tailwind') { %>
5
5
  const styles: Record<string, React.CSSProperties> = {
6
- container: { width: '100%', maxWidth: 360 },
7
- title: { fontSize: 18, fontWeight: 600, margin: 0 },
6
+ container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
7
+ title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
8
8
  subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 1.5 },
9
- card: { marginTop: 16, border: '1px solid #e5e5e5', borderRadius: 12, padding: 16 },
10
- cardTitle: { fontSize: 14, fontWeight: 500, margin: 0 },
9
+ card: { marginTop: 16, border: '1px solid #262626', borderRadius: 12, padding: 16, background: '#171717' },
10
+ cardTitle: { fontSize: 14, fontWeight: 500, margin: 0, color: '#fafafa' },
11
11
  cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
12
- button: { marginTop: 12, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
13
- buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: '1px solid #e5e5e5', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
14
- info: { marginTop: 16, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8, fontSize: 12, color: '#525252' },
12
+ button: { marginTop: 12, width: '100%', backgroundColor: '#fafafa', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
13
+ buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#fafafa', padding: '12px 16px', borderRadius: 10, border: '1px solid #262626', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
14
+ info: { marginTop: 16, padding: 12, backgroundColor: '#171717', borderRadius: 8, fontSize: 12, color: '#a3a3a3' },
15
15
  back: { marginTop: 16, fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
16
16
  };
17
17
  <% } %>
@@ -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 = () => {
@@ -42,39 +50,39 @@ export function Recovery({ onBack }: Props) {
42
50
 
43
51
  return (
44
52
  <% if (styling === 'tailwind') { %>
45
- <div className="w-full max-w-sm">
53
+ <div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
46
54
  <h1 className="text-lg font-semibold">Recovery & Backup</h1>
47
55
  <p className="mt-2 text-sm text-neutral-500">
48
56
  Add backup passkeys from other devices to ensure you never lose access.
49
57
  </p>
50
- <div className="mt-4 border border-neutral-200 rounded-xl p-4">
58
+ <div className="mt-4 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
51
59
  <h3 className="text-sm font-medium">Add Backup Device</h3>
52
60
  <p className="mt-1 text-xs text-neutral-500">
53
61
  Register a passkey from another phone, tablet, or computer.
54
62
  </p>
55
- <button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-black text-white text-sm font-medium rounded-lg hover:opacity-90">
63
+ <button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-white text-black text-sm font-medium rounded-lg hover:opacity-90">
56
64
  Add Device
57
65
  </button>
58
66
  </div>
59
- <div className="mt-3 border border-neutral-200 rounded-xl p-4">
67
+ <div className="mt-3 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
60
68
  <h3 className="text-sm font-medium">Manage Devices</h3>
61
69
  <p className="mt-1 text-xs text-neutral-500">
62
70
  View and remove registered passkeys.
63
71
  </p>
64
- <button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-black text-sm font-medium rounded-lg border border-neutral-200 hover:bg-neutral-50">
72
+ <button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-white text-sm font-medium rounded-lg border border-neutral-700 hover:bg-neutral-700">
65
73
  View Devices
66
74
  </button>
67
75
  </div>
68
- <div className="mt-4 p-3 bg-neutral-100 rounded-lg">
69
- <p className="text-xs text-neutral-600">
76
+ <div className="mt-4 p-3 bg-neutral-800 rounded-lg">
77
+ <p className="text-xs text-neutral-400">
70
78
  <strong>Wallet:</strong>{" "}
71
- <button onClick={copyAddress} className="font-mono hover:text-black">
79
+ <button onClick={copyAddress} className="font-mono hover:text-white">
72
80
  {wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
73
81
  </button>
74
- {copied && <span className="ml-2 text-green-600">Copied!</span>}
82
+ {copied && <span className="ml-2 text-green-400">Copied!</span>}
75
83
  </p>
76
84
  </div>
77
- <button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-black">
85
+ <button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-white">
78
86
  ← Back
79
87
  </button>
80
88
  </div>