@jerydam/lumina-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUTTON_FIXES.md +59 -0
- package/DOCS_INDEX.md +332 -0
- package/DOCUMENTATION.md +252 -0
- package/DOCUMENTATION_BUILD_SUMMARY.md +376 -0
- package/DOCUMENTATION_COMPLETE.md +311 -0
- package/FEATURES.md +333 -0
- package/Lumina-sdk/src/components/lumina-provider.tsx +46 -0
- package/Lumina-sdk/src/components/transaction-confirm.tsx +242 -0
- package/Lumina-sdk/src/components/wallet-display.tsx +157 -0
- package/Lumina-sdk/src/components/wallet-login.tsx +163 -0
- package/Lumina-sdk/src/hooks/use-mobile.ts +19 -0
- package/Lumina-sdk/src/hooks/use-toast.ts +191 -0
- package/Lumina-sdk/src/index.ts +0 -0
- package/Lumina-sdk/src/lib/api.ts +66 -0
- package/Lumina-sdk/src/lib/utils.ts +6 -0
- package/Lumina-sdk/src/package.json +42 -0
- package/Lumina-sdk/src/tsconfig.json +19 -0
- package/NEW_FILES_MANIFEST.txt +146 -0
- package/README.md +298 -0
- package/app/dashboard/analytics/page.tsx +218 -0
- package/app/dashboard/api-keys/page.tsx +260 -0
- package/app/dashboard/billing/page.tsx +412 -0
- package/app/dashboard/integration/page.tsx +185 -0
- package/app/dashboard/layout.tsx +18 -0
- package/app/dashboard/page.tsx +244 -0
- package/app/dashboard/settings/page.tsx +285 -0
- package/app/dashboard/users/page.tsx +148 -0
- package/app/docs/api/authentication/page.tsx +246 -0
- package/app/docs/api/endpoints/page.tsx +397 -0
- package/app/docs/api/errors/page.tsx +305 -0
- package/app/docs/api/overview/page.tsx +306 -0
- package/app/docs/examples/basic-setup/page.tsx +256 -0
- package/app/docs/examples/multi-chain/page.tsx +331 -0
- package/app/docs/examples/nextjs-full-stack/page.tsx +332 -0
- package/app/docs/getting-started/environment-setup/page.tsx +243 -0
- package/app/docs/getting-started/installation/page.tsx +187 -0
- package/app/docs/getting-started/introduction/page.tsx +178 -0
- package/app/docs/getting-started/quick-start/page.tsx +199 -0
- package/app/docs/guides/nextjs/page.tsx +358 -0
- package/app/docs/guides/react/page.tsx +230 -0
- package/app/docs/guides/security/page.tsx +284 -0
- package/app/docs/layout.tsx +32 -0
- package/app/docs/page.tsx +180 -0
- package/app/docs/sdk/lumina-provider/page.tsx +186 -0
- package/app/docs/sdk/transaction-confirm/page.tsx +331 -0
- package/app/docs/sdk/wallet-display/page.tsx +224 -0
- package/app/docs/sdk/wallet-login/page.tsx +207 -0
- package/app/docs/troubleshooting/common-issues/page.tsx +301 -0
- package/app/docs/troubleshooting/faq/page.tsx +105 -0
- package/app/globals.css +125 -0
- package/app/invite/[token]/page.tsx +78 -0
- package/app/layout.tsx +36 -0
- package/app/login/page.tsx +175 -0
- package/app/page.tsx +336 -0
- package/app/sdk-demo/page.tsx +239 -0
- package/components/dashboard-sidebar.tsx +113 -0
- package/components/docs/breadcrumb.tsx +51 -0
- package/components/docs/callout.tsx +53 -0
- package/components/docs/code-block.tsx +77 -0
- package/components/docs/docs-sidebar.tsx +214 -0
- package/components/docs/table-of-contents.tsx +83 -0
- package/components/sdk/lumina-provider.tsx +46 -0
- package/components/sdk/transaction-confirm.tsx +242 -0
- package/components/sdk/wallet-display.tsx +157 -0
- package/components/sdk/wallet-login.tsx +163 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/calendar.tsx +213 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +351 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +135 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +104 -0
- package/components/ui/field.tsx +244 -0
- package/components/ui/form.tsx +167 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +169 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/item.tsx +193 -0
- package/components/ui/kbd.tsx +28 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +166 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +56 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +185 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +59 -0
- package/components/ui/sonner.tsx +25 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/toast.tsx +129 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.tsx +19 -0
- package/components/ui/use-toast.ts +191 -0
- package/components.json +21 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +191 -0
- package/lib/api.ts +66 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +11 -0
- package/package.json +73 -0
- package/pnpm-workspace.yaml +5 -0
- package/postcss.config.mjs +8 -0
- package/public/apple-icon.png +0 -0
- package/public/fav.jpeg +0 -0
- package/public/fav.png +0 -0
- package/public/icon-dark-32x32.png +0 -0
- package/public/icon-light-32x32.png +0 -0
- package/public/icon.png +0 -0
- package/public/icon.svg +26 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/public/logo2.jpeg +0 -0
- package/public/logo2.png +0 -0
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/styles/globals.css +209 -0
- package/tailwind.config.ts +15 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Loader2, CheckCircle, AlertCircle, ExternalLink, Shield } from 'lucide-react'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Card } from '@/components/ui/card'
|
|
7
|
+
import { useLumina } from './lumina-provider'
|
|
8
|
+
import { encodeFunctionData, parseEther } from 'viem'
|
|
9
|
+
|
|
10
|
+
interface TransactionDetails {
|
|
11
|
+
from: string // The smart account address
|
|
12
|
+
signerAddress: string // The underlying Turnkey embedded wallet EOA
|
|
13
|
+
to: string
|
|
14
|
+
amount: string
|
|
15
|
+
token?: string
|
|
16
|
+
network?: string
|
|
17
|
+
chainId?: string
|
|
18
|
+
gasFee?: string
|
|
19
|
+
estimatedTime?: string
|
|
20
|
+
callData?: string // Raw hex data for contract interactions
|
|
21
|
+
isDeployed?: boolean // Tells us if we need to append initCode
|
|
22
|
+
factoryAddress?: string // Required if !isDeployed
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TransactionConfirmProps {
|
|
26
|
+
transaction: TransactionDetails
|
|
27
|
+
onConfirm?: () => Promise<void>
|
|
28
|
+
onCancel?: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type TransactionStatus = 'pending' | 'sponsoring' | 'signing' | 'submitting' | 'confirmed' | 'failed'
|
|
32
|
+
|
|
33
|
+
export function TransactionConfirm({
|
|
34
|
+
transaction,
|
|
35
|
+
onConfirm,
|
|
36
|
+
onCancel,
|
|
37
|
+
}: TransactionConfirmProps) {
|
|
38
|
+
const { fetchApi } = useLumina()
|
|
39
|
+
const [status, setStatus] = useState<TransactionStatus>('pending')
|
|
40
|
+
const [txHash, setTxHash] = useState<string>('')
|
|
41
|
+
const [errorMessage, setErrorMessage] = useState<string>('')
|
|
42
|
+
|
|
43
|
+
const handleConfirm = async () => {
|
|
44
|
+
setStatus('sponsoring')
|
|
45
|
+
setErrorMessage('')
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (onConfirm) await onConfirm()
|
|
49
|
+
|
|
50
|
+
const chainId = transaction.chainId || '1'
|
|
51
|
+
|
|
52
|
+
// ── 1. Handle Counterfactual Deployment (initCode) ──────────────────────
|
|
53
|
+
let finalInitCode = '0x'
|
|
54
|
+
|
|
55
|
+
if (!transaction.isDeployed && transaction.factoryAddress && transaction.signerAddress) {
|
|
56
|
+
const factoryABI = [{
|
|
57
|
+
name: 'createAccount',
|
|
58
|
+
type: 'function',
|
|
59
|
+
inputs: [
|
|
60
|
+
{ name: 'owner', type: 'address' },
|
|
61
|
+
{ name: 'salt', type: 'uint256' }
|
|
62
|
+
]
|
|
63
|
+
}]
|
|
64
|
+
|
|
65
|
+
const createAccountData = encodeFunctionData({
|
|
66
|
+
abi: factoryABI,
|
|
67
|
+
functionName: 'createAccount',
|
|
68
|
+
args: [transaction.signerAddress as `0x${string}`, 0n] // salt = 0 matches auth.py
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// initCode is Factory Address + calldata
|
|
72
|
+
finalInitCode = `${transaction.factoryAddress}${createAccountData.replace('0x', '')}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── 2. Encode SmartAccount execution (callData) ─────────────────────────
|
|
76
|
+
const smartAccountABI = [{
|
|
77
|
+
name: 'execute',
|
|
78
|
+
type: 'function',
|
|
79
|
+
inputs: [
|
|
80
|
+
{ name: 'target', type: 'address' },
|
|
81
|
+
{ name: 'value', type: 'uint256' },
|
|
82
|
+
{ name: 'data', type: 'bytes' }
|
|
83
|
+
]
|
|
84
|
+
}]
|
|
85
|
+
|
|
86
|
+
const finalCallData = encodeFunctionData({
|
|
87
|
+
abi: smartAccountABI,
|
|
88
|
+
functionName: 'execute',
|
|
89
|
+
args: [
|
|
90
|
+
transaction.to as `0x${string}`,
|
|
91
|
+
transaction.amount ? parseEther(transaction.amount) : 0n,
|
|
92
|
+
(transaction.callData as `0x${string}`) || '0x'
|
|
93
|
+
]
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ── 3. Build the UserOperation ──────────────────────────────────────────
|
|
97
|
+
const partialUserOp = {
|
|
98
|
+
sender: transaction.from,
|
|
99
|
+
nonce: 0, // In production, fetch this via eth_call to the EntryPoint
|
|
100
|
+
init_code: finalInitCode,
|
|
101
|
+
call_data: finalCallData,
|
|
102
|
+
call_gas_limit: 50000,
|
|
103
|
+
verification_gas_limit: 150000,
|
|
104
|
+
pre_verification_gas: 21000,
|
|
105
|
+
max_fee_per_gas: 2000000000,
|
|
106
|
+
max_priority_fee_per_gas: 1000000000,
|
|
107
|
+
chain_id: chainId
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 4. Get Paymaster Sponsorship ────────────────────────────────────────
|
|
111
|
+
const signRes = await fetchApi('/v1/paymaster/sign', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify(partialUserOp)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
setStatus('signing')
|
|
117
|
+
|
|
118
|
+
// ── 5. Local User Signature (Turnkey integration goes here) ─────────────
|
|
119
|
+
// Simulating a Passkey/Biometric signing delay
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
121
|
+
const mockUserSignature = "0xabcd1234..." // Replace with Turnkey sign output
|
|
122
|
+
|
|
123
|
+
const fullySignedOp = {
|
|
124
|
+
...partialUserOp,
|
|
125
|
+
paymasterAndData: signRes.paymaster_and_data,
|
|
126
|
+
signature: mockUserSignature
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setStatus('submitting')
|
|
130
|
+
|
|
131
|
+
// ── 6. Submit to the Bundler ────────────────────────────────────────────
|
|
132
|
+
const submitRes = await fetchApi('/v1/paymaster/submit', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
user_op_id: signRes.user_op_id,
|
|
136
|
+
signed_user_op: fullySignedOp,
|
|
137
|
+
chain_id: chainId
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
setTxHash(submitRes.user_op_hash)
|
|
142
|
+
setStatus('confirmed')
|
|
143
|
+
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
console.error("Transaction failed:", error)
|
|
146
|
+
setStatus('failed')
|
|
147
|
+
setErrorMessage(error.message || "Failed to process transaction")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Card className="glassmorphism-dark p-8 border-border w-full max-w-lg mx-auto">
|
|
153
|
+
{status === 'pending' && (
|
|
154
|
+
<>
|
|
155
|
+
<div className="mb-6">
|
|
156
|
+
<h2 className="text-2xl font-bold mb-2">Confirm Transaction</h2>
|
|
157
|
+
<p className="text-foreground/60 text-sm">Review the details before confirming</p>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="space-y-4 mb-6 p-4 bg-white/5 rounded-lg border border-border/50">
|
|
161
|
+
<div className="flex justify-between items-start">
|
|
162
|
+
<span className="text-foreground/60">To Address</span>
|
|
163
|
+
<span className="font-mono text-sm">{transaction.to.slice(0, 6)}...{transaction.to.slice(-4)}</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="flex justify-between items-start border-t border-border/30 pt-4">
|
|
166
|
+
<span className="text-foreground/60">Amount</span>
|
|
167
|
+
<span className="font-bold text-lg">{transaction.amount} {transaction.token || 'ETH'}</span>
|
|
168
|
+
</div>
|
|
169
|
+
{transaction.gasFee && (
|
|
170
|
+
<div className="flex justify-between items-start border-t border-border/30 pt-4">
|
|
171
|
+
<span className="text-foreground/60">Gas Fee</span>
|
|
172
|
+
<span className="font-medium text-emerald-400">Sponsored by Paymaster</span>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="mb-6 p-4 bg-emerald-600/10 rounded-lg border border-emerald-600/30 flex gap-3">
|
|
178
|
+
<Shield className="text-emerald-500 shrink-0 mt-0.5" size={20} />
|
|
179
|
+
<div className="text-sm">
|
|
180
|
+
<p className="font-medium mb-1">Gasless Transaction</p>
|
|
181
|
+
<p className="text-foreground/70">The network fees for this action are being covered by the application.</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="flex gap-3">
|
|
186
|
+
<Button onClick={onCancel} variant="outline" className="flex-1">
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
<Button onClick={handleConfirm} className="flex-1 bg-emerald-600 hover:bg-emerald-700">
|
|
190
|
+
Confirm Transaction
|
|
191
|
+
</Button>
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{(status === 'sponsoring' || status === 'signing' || status === 'submitting') && (
|
|
197
|
+
<div className="text-center py-8">
|
|
198
|
+
<Loader2 className="animate-spin mx-auto mb-4 text-emerald-500" size={48} />
|
|
199
|
+
<h3 className="text-xl font-bold mb-2">
|
|
200
|
+
{status === 'sponsoring' && "Sponsoring Gas..."}
|
|
201
|
+
{status === 'signing' && "Awaiting Signature..."}
|
|
202
|
+
{status === 'submitting' && "Submitting to Network..."}
|
|
203
|
+
</h3>
|
|
204
|
+
<p className="text-foreground/60 text-sm">Please do not close this window.</p>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{status === 'confirmed' && (
|
|
209
|
+
<div className="text-center py-8">
|
|
210
|
+
<CheckCircle className="mx-auto mb-4 text-emerald-500" size={48} />
|
|
211
|
+
<h3 className="text-xl font-bold mb-2">Transaction Submitted</h3>
|
|
212
|
+
<p className="text-foreground/60 mb-4 text-sm">Your operation has been sent to the bundler network.</p>
|
|
213
|
+
|
|
214
|
+
<div className="p-3 bg-white/5 rounded-lg border border-border/50 mb-6 text-left">
|
|
215
|
+
<p className="text-xs text-foreground/60 mb-1">UserOp Hash</p>
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<code className="text-xs font-mono flex-1 truncate">{txHash}</code>
|
|
218
|
+
<button className="p-1 hover:bg-white/10 rounded">
|
|
219
|
+
<ExternalLink size={16} className="text-foreground/60" />
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<Button onClick={onCancel} className="w-full bg-emerald-600 hover:bg-emerald-700">
|
|
224
|
+
Return to Wallet
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{status === 'failed' && (
|
|
230
|
+
<div className="text-center py-8">
|
|
231
|
+
<AlertCircle className="mx-auto mb-4 text-red-500" size={48} />
|
|
232
|
+
<h3 className="text-xl font-bold mb-2">Transaction Failed</h3>
|
|
233
|
+
<p className="text-foreground/60 mb-6 text-sm">{errorMessage}</p>
|
|
234
|
+
|
|
235
|
+
<Button onClick={() => setStatus('pending')} className="w-full bg-emerald-600 hover:bg-emerald-700">
|
|
236
|
+
Try Again
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</Card>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Copy, ExternalLink, Send, Download, Loader2 } from 'lucide-react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Card } from '@/components/ui/card'
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
7
|
+
import { useLumina } from './lumina-provider'
|
|
8
|
+
|
|
9
|
+
interface WalletDisplayProps {
|
|
10
|
+
signerAddress: string // We need the signer to fetch details from your backend
|
|
11
|
+
smartAccountAddress: string
|
|
12
|
+
balance?: string
|
|
13
|
+
token?: string
|
|
14
|
+
network?: string
|
|
15
|
+
chainId?: string
|
|
16
|
+
onSend?: () => void
|
|
17
|
+
onReceive?: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function WalletDisplay({
|
|
21
|
+
signerAddress,
|
|
22
|
+
smartAccountAddress,
|
|
23
|
+
balance = '0.00',
|
|
24
|
+
token = 'ETH',
|
|
25
|
+
network = 'Ethereum',
|
|
26
|
+
chainId = '1',
|
|
27
|
+
onSend,
|
|
28
|
+
onReceive,
|
|
29
|
+
}: WalletDisplayProps) {
|
|
30
|
+
const { fetchApi } = useLumina()
|
|
31
|
+
const [copied, setCopied] = useState(false)
|
|
32
|
+
const [isDeployed, setIsDeployed] = useState<boolean | null>(null)
|
|
33
|
+
const [isLoadingState, setIsLoadingState] = useState(true)
|
|
34
|
+
|
|
35
|
+
// Fetch wallet details on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let isMounted = true
|
|
38
|
+
|
|
39
|
+
const syncWalletState = async () => {
|
|
40
|
+
try {
|
|
41
|
+
// 1. Fetch current DB state
|
|
42
|
+
const walletData = await fetchApi(`/v1/wallets/${signerAddress}/${chainId}`)
|
|
43
|
+
|
|
44
|
+
if (!isMounted) return
|
|
45
|
+
|
|
46
|
+
setIsDeployed(walletData.is_deployed)
|
|
47
|
+
|
|
48
|
+
// 2. If the DB thinks it's not deployed, double-check on-chain
|
|
49
|
+
// This is crucial because users might fund their counterfactual address
|
|
50
|
+
if (!walletData.is_deployed) {
|
|
51
|
+
const deployCheck = await fetchApi(`/v1/wallets/${signerAddress}/${chainId}/check-deployment`, {
|
|
52
|
+
method: 'POST'
|
|
53
|
+
})
|
|
54
|
+
if (isMounted) setIsDeployed(deployCheck.is_deployed)
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Failed to sync wallet state:", error)
|
|
58
|
+
} finally {
|
|
59
|
+
if (isMounted) setIsLoadingState(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
syncWalletState()
|
|
64
|
+
return () => { isMounted = false }
|
|
65
|
+
}, [signerAddress, chainId, fetchApi])
|
|
66
|
+
|
|
67
|
+
const handleCopy = () => {
|
|
68
|
+
navigator.clipboard.writeText(smartAccountAddress)
|
|
69
|
+
setCopied(true)
|
|
70
|
+
setTimeout(() => setCopied(false), 2000)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Card className="glassmorphism-dark p-8 border-border w-full max-w-md mx-auto">
|
|
75
|
+
<div className="text-center mb-8">
|
|
76
|
+
<h2 className="text-2xl font-bold mb-2">Your Wallet</h2>
|
|
77
|
+
<div className="flex items-center justify-center gap-2">
|
|
78
|
+
<p className="text-foreground/60 text-sm">{network} Network</p>
|
|
79
|
+
{isLoadingState ? (
|
|
80
|
+
<Loader2 className="w-3 h-3 animate-spin text-foreground/60" />
|
|
81
|
+
) : (
|
|
82
|
+
<span className={`text-[10px] px-2 py-0.5 rounded-full ${isDeployed ? 'bg-emerald-500/20 text-emerald-400' : 'bg-amber-500/20 text-amber-400'}`}>
|
|
83
|
+
{isDeployed ? 'Deployed' : 'Counterfactual'}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Balance Display */}
|
|
90
|
+
<div className="text-center mb-8 p-6 bg-linear-to-b from-emerald-600/20 to-emerald-600/5 rounded-lg border border-emerald-600/30">
|
|
91
|
+
<p className="text-foreground/60 text-sm mb-2">Available Balance</p>
|
|
92
|
+
<p className="text-4xl font-bold text-emerald-400 mb-2">{balance}</p>
|
|
93
|
+
<p className="text-foreground/60">{token}</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Address Display */}
|
|
97
|
+
<div className="mb-6 p-4 bg-white/5 rounded-lg border border-border/50">
|
|
98
|
+
<p className="text-xs text-foreground/60 mb-2">Smart Account Address</p>
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<code className="text-sm font-mono flex-1 truncate">{smartAccountAddress}</code>
|
|
101
|
+
<button
|
|
102
|
+
onClick={handleCopy}
|
|
103
|
+
className="p-2 hover:bg-white/10 rounded-lg transition"
|
|
104
|
+
title="Copy address"
|
|
105
|
+
>
|
|
106
|
+
<Copy size={16} className="text-foreground/60" />
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
{copied && <p className="text-xs text-emerald-500 mt-2">Copied!</p>}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Action Buttons */}
|
|
113
|
+
<div className="grid grid-cols-2 gap-3 mb-6">
|
|
114
|
+
<Button
|
|
115
|
+
onClick={onReceive}
|
|
116
|
+
variant="outline"
|
|
117
|
+
className="gap-2"
|
|
118
|
+
>
|
|
119
|
+
<Download size={18} />
|
|
120
|
+
Receive
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
onClick={onSend}
|
|
124
|
+
className="bg-emerald-600 hover:bg-emerald-700 gap-2"
|
|
125
|
+
>
|
|
126
|
+
<Send size={18} />
|
|
127
|
+
Send
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Network Info */}
|
|
132
|
+
<div className="p-4 bg-white/5 rounded-lg border border-border/50 space-y-2">
|
|
133
|
+
<div className="flex justify-between text-sm">
|
|
134
|
+
<span className="text-foreground/60">Network</span>
|
|
135
|
+
<span className="font-medium">{network}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex justify-between text-sm border-t border-border/30 pt-2">
|
|
138
|
+
<span className="text-foreground/60">Network Status</span>
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
|
141
|
+
<span className="font-medium">Operational</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* View on Explorer */}
|
|
147
|
+
<Button
|
|
148
|
+
variant="outline"
|
|
149
|
+
className="w-full mt-6 gap-2"
|
|
150
|
+
onClick={() => window.open(`https://etherscan.io/address/${smartAccountAddress}`, '_blank')}
|
|
151
|
+
>
|
|
152
|
+
<ExternalLink size={18} />
|
|
153
|
+
View on Explorer
|
|
154
|
+
</Button>
|
|
155
|
+
</Card>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { Loader2, Lock, Mail } from 'lucide-react'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
import { Card } from '@/components/ui/card'
|
|
9
|
+
import { apiClient } from '@/lib/api' // Our centralized API client
|
|
10
|
+
export interface WalletLoginProps {
|
|
11
|
+
authMethods?: string[];
|
|
12
|
+
onSuccess?: (address: string) => void;
|
|
13
|
+
onError?: (err: Error) => void;
|
|
14
|
+
}
|
|
15
|
+
export default function WalletLogin({ authMethods, onSuccess, onError }: WalletLoginProps) { const router = useRouter()
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
17
|
+
const [isRegistering, setIsRegistering] = useState(false)
|
|
18
|
+
const [error, setError] = useState('')
|
|
19
|
+
|
|
20
|
+
const [formData, setFormData] = useState({
|
|
21
|
+
name: '',
|
|
22
|
+
email: '',
|
|
23
|
+
password: ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
setIsLoading(true)
|
|
29
|
+
setError('')
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (isRegistering) {
|
|
33
|
+
// 1. Register a new developer
|
|
34
|
+
await apiClient.dashboard('/v1/developers/register', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
name: formData.name,
|
|
38
|
+
email: formData.email,
|
|
39
|
+
password: formData.password
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
// Auto-login after registration
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Log in to get the JWT access token
|
|
46
|
+
const data = await apiClient.dashboard('/v1/developers/login', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
email: formData.email,
|
|
50
|
+
password: formData.password
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 3. Securely store the JWT for future dashboard API calls
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
localStorage.setItem('lumina_dev_token', data.access_token)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Redirect to the main dashboard
|
|
60
|
+
router.push('/dashboard')
|
|
61
|
+
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
setError(err.message || 'Authentication failed. Please check your credentials.')
|
|
64
|
+
} finally {
|
|
65
|
+
setIsLoading(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
|
|
71
|
+
<Card className="glassmorphism-dark p-8 border-border w-full max-w-md">
|
|
72
|
+
<div className="mb-8 text-center">
|
|
73
|
+
<h1 className="text-3xl font-bold mb-2">Lumina Portal</h1>
|
|
74
|
+
<p className="text-foreground/60 text-sm">
|
|
75
|
+
{isRegistering ? 'Create your developer account' : 'Sign in to manage your workspace'}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{error && (
|
|
80
|
+
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/50 rounded-lg text-red-400 text-sm text-center">
|
|
81
|
+
{error}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
86
|
+
{isRegistering && (
|
|
87
|
+
<div>
|
|
88
|
+
<label className="block text-sm font-medium mb-2">Full Name</label>
|
|
89
|
+
<Input
|
|
90
|
+
type="text"
|
|
91
|
+
placeholder="Jane Doe"
|
|
92
|
+
value={formData.name}
|
|
93
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
94
|
+
required={isRegistering}
|
|
95
|
+
className="bg-white/5 border-border"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div>
|
|
101
|
+
<label className="block text-sm font-medium mb-2">Email Address</label>
|
|
102
|
+
<div className="relative">
|
|
103
|
+
<Mail className="absolute left-3 top-2.5 text-foreground/40" size={18} />
|
|
104
|
+
<Input
|
|
105
|
+
type="email"
|
|
106
|
+
placeholder="developer@company.com"
|
|
107
|
+
value={formData.email}
|
|
108
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
109
|
+
required
|
|
110
|
+
className="pl-10 bg-white/5 border-border"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<label className="block text-sm font-medium mb-2">Password</label>
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<Lock className="absolute left-3 top-2.5 text-foreground/40" size={18} />
|
|
119
|
+
<Input
|
|
120
|
+
type="password"
|
|
121
|
+
placeholder="••••••••••••"
|
|
122
|
+
value={formData.password}
|
|
123
|
+
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
124
|
+
required
|
|
125
|
+
minLength={12}
|
|
126
|
+
className="pl-10 bg-white/5 border-border"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
{isRegistering && (
|
|
130
|
+
<p className="text-xs text-foreground/50 mt-2">Must be at least 12 characters long.</p>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<Button
|
|
135
|
+
type="submit"
|
|
136
|
+
disabled={isLoading}
|
|
137
|
+
className="w-full bg-emerald-600 hover:bg-emerald-700 mt-6"
|
|
138
|
+
>
|
|
139
|
+
{isLoading ? (
|
|
140
|
+
<Loader2 className="animate-spin" size={18} />
|
|
141
|
+
) : (
|
|
142
|
+
isRegistering ? 'Create Account' : 'Sign In'
|
|
143
|
+
)}
|
|
144
|
+
</Button>
|
|
145
|
+
</form>
|
|
146
|
+
|
|
147
|
+
<div className="mt-6 text-center text-sm text-foreground/60">
|
|
148
|
+
{isRegistering ? 'Already have an account?' : "Don't have an account?"}{' '}
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => {
|
|
152
|
+
setIsRegistering(!isRegistering)
|
|
153
|
+
setError('')
|
|
154
|
+
}}
|
|
155
|
+
className="text-emerald-400 hover:text-emerald-300 font-medium"
|
|
156
|
+
>
|
|
157
|
+
{isRegistering ? 'Sign In' : 'Register'}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</Card>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
type ThemeProviderProps,
|
|
7
|
+
} from 'next-themes'
|
|
8
|
+
|
|
9
|
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
10
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
|
5
|
+
import { ChevronDownIcon } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
|
|
9
|
+
function Accordion({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
12
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function AccordionItem({
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
19
|
+
return (
|
|
20
|
+
<AccordionPrimitive.Item
|
|
21
|
+
data-slot="accordion-item"
|
|
22
|
+
className={cn('border-b last:border-b-0', className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AccordionTrigger({
|
|
29
|
+
className,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
33
|
+
return (
|
|
34
|
+
<AccordionPrimitive.Header className="flex">
|
|
35
|
+
<AccordionPrimitive.Trigger
|
|
36
|
+
data-slot="accordion-trigger"
|
|
37
|
+
className={cn(
|
|
38
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
45
|
+
</AccordionPrimitive.Trigger>
|
|
46
|
+
</AccordionPrimitive.Header>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AccordionContent({
|
|
51
|
+
className,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
55
|
+
return (
|
|
56
|
+
<AccordionPrimitive.Content
|
|
57
|
+
data-slot="accordion-content"
|
|
58
|
+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
|
62
|
+
</AccordionPrimitive.Content>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|