create-openfort 0.1.10 → 1.0.1
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/CHANGELOG.md +16 -0
- package/dist/index.js +24 -23
- package/package.json +1 -1
- package/template/openfort-templates/firebase/AGENTS.md +1 -1
- package/template/openfort-templates/firebase/package.json +3 -3
- package/template/openfort-templates/firebase/src/App.tsx +2 -1
- package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
- package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
- package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
- package/template/openfort-templates/headless/AGENTS.md +1 -1
- package/template/openfort-templates/headless/package.json +2 -2
- package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
- package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
- package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
- package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
- package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
- package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
- package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
- package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
- package/template/openfort-templates/openfort-ui/package.json +3 -3
- package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
- package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
- package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
- package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
- package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
- package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
- package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
- package/template/openfort-templates/solana-headless/biome.json +70 -0
- package/template/openfort-templates/solana-headless/index.html +16 -0
- package/template/openfort-templates/solana-headless/package.json +34 -0
- package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
- package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
- package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
- package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
- package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
- package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
- package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
- package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
- package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
- package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
- package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
- package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
- package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
- package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
- package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
- package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
- package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
- package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
- package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
- package/template/openfort-templates/solana-headless/src/index.css +180 -0
- package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
- package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
- package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
- package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
- package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
- package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
- package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
type RoutePoint = {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
delay: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Route = {
|
|
10
|
+
start: RoutePoint
|
|
11
|
+
end: RoutePoint
|
|
12
|
+
color: string
|
|
13
|
+
outDelay?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function withOpacity(color: string, opacity: number) {
|
|
17
|
+
if (color.startsWith('rgb')) {
|
|
18
|
+
const nums = color.match(/[\d.]+/g)?.map(Number)
|
|
19
|
+
if (!nums || nums.length < 3) throw new Error('Invalid rgb format')
|
|
20
|
+
const [r, g, b] = nums
|
|
21
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error(`Unsupported color format: ${color}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GlowCanvas = ({
|
|
28
|
+
accentColor,
|
|
29
|
+
backgroundColor,
|
|
30
|
+
}: {
|
|
31
|
+
accentColor: string
|
|
32
|
+
backgroundColor: string
|
|
33
|
+
}) => {
|
|
34
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
35
|
+
const mousePosRef = useRef<{ x: number; y: number } | null>(null)
|
|
36
|
+
const animationRef = useRef<number | null>(null)
|
|
37
|
+
|
|
38
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
|
39
|
+
|
|
40
|
+
const color = useMemo(() => withOpacity(accentColor, 0.7), [accentColor])
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const canvas = canvasRef.current
|
|
44
|
+
if (!canvas) return
|
|
45
|
+
|
|
46
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
47
|
+
const { width, height } = entries[0].contentRect
|
|
48
|
+
setDimensions({ width, height })
|
|
49
|
+
canvas.width = width
|
|
50
|
+
canvas.height = height
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
resizeObserver.observe(canvas.parentElement as Element)
|
|
54
|
+
return () => resizeObserver.disconnect()
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!dimensions.width || !dimensions.height) return
|
|
59
|
+
|
|
60
|
+
let startTime = Date.now()
|
|
61
|
+
|
|
62
|
+
const canvas = canvasRef.current
|
|
63
|
+
if (!canvas) return
|
|
64
|
+
const ctx = canvas.getContext('2d')
|
|
65
|
+
if (!ctx) return
|
|
66
|
+
|
|
67
|
+
const gap = 14
|
|
68
|
+
const dotRadius = 1.5
|
|
69
|
+
|
|
70
|
+
const generateDots = (width: number, height: number) => {
|
|
71
|
+
const dots: { x: number; y: number; radius: number; opacity: number }[] =
|
|
72
|
+
[]
|
|
73
|
+
|
|
74
|
+
for (let x = 0; x < width; x += gap) {
|
|
75
|
+
for (let y = 0; y < height; y += gap) {
|
|
76
|
+
if (Math.random() > 0.4) {
|
|
77
|
+
dots.push({
|
|
78
|
+
x,
|
|
79
|
+
y,
|
|
80
|
+
radius: dotRadius,
|
|
81
|
+
opacity: Math.random() * 0.4 + 0.1,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return dots
|
|
87
|
+
}
|
|
88
|
+
const dots = generateDots(dimensions.width, dimensions.height)
|
|
89
|
+
|
|
90
|
+
const draw = () => {
|
|
91
|
+
ctx.clearRect(0, 0, dimensions.width, dimensions.height)
|
|
92
|
+
|
|
93
|
+
if (mousePosRef.current) {
|
|
94
|
+
const { x, y } = mousePosRef.current
|
|
95
|
+
|
|
96
|
+
ctx.shadowBlur = 25
|
|
97
|
+
ctx.shadowColor = backgroundColor
|
|
98
|
+
|
|
99
|
+
dots.forEach((dot) => {
|
|
100
|
+
const dx = dot.x - x
|
|
101
|
+
const dy = dot.y - y
|
|
102
|
+
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
103
|
+
|
|
104
|
+
ctx.beginPath()
|
|
105
|
+
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2)
|
|
106
|
+
|
|
107
|
+
const opacityFactor = 1 - distance / 100
|
|
108
|
+
|
|
109
|
+
ctx.fillStyle = withOpacity(accentColor, opacityFactor)
|
|
110
|
+
ctx.fill()
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Draw the dots
|
|
115
|
+
dots.forEach((dot) => {
|
|
116
|
+
ctx.beginPath()
|
|
117
|
+
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2)
|
|
118
|
+
ctx.fillStyle = withOpacity(backgroundColor, dot.opacity)
|
|
119
|
+
ctx.fill()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
drawRoutes()
|
|
123
|
+
// If all routes are complete, restart the animation
|
|
124
|
+
const currentTime = (Date.now() - startTime) / 1000
|
|
125
|
+
if (currentTime > 25) {
|
|
126
|
+
// Reset after 15 seconds
|
|
127
|
+
startTime = Date.now()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
animationRef.current = requestAnimationFrame(draw)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Set up routes that will animate across the map
|
|
134
|
+
const yOffset = 50
|
|
135
|
+
const scale = 12
|
|
136
|
+
const h = 11 * scale
|
|
137
|
+
const w = 18 * scale
|
|
138
|
+
const xOffset = (dimensions.width - w) / 2
|
|
139
|
+
|
|
140
|
+
const routes: Route[] = [
|
|
141
|
+
{
|
|
142
|
+
start: { x: xOffset, y: yOffset + h, delay: 0 },
|
|
143
|
+
end: { x: xOffset, y: yOffset, delay: 2 },
|
|
144
|
+
color,
|
|
145
|
+
outDelay: 10,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
start: { x: xOffset, y: yOffset, delay: 2 },
|
|
149
|
+
end: { x: xOffset + w, y: yOffset, delay: 4 },
|
|
150
|
+
color,
|
|
151
|
+
outDelay: 10,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
start: { x: xOffset + w, y: yOffset, delay: 4 },
|
|
155
|
+
end: { x: xOffset + w, y: yOffset + h, delay: 6 },
|
|
156
|
+
color,
|
|
157
|
+
outDelay: 10,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
start: { x: xOffset + w / 4, y: yOffset + h / 3, delay: 4 },
|
|
162
|
+
end: { x: xOffset + w / 4, y: yOffset + h, delay: 6 },
|
|
163
|
+
color,
|
|
164
|
+
outDelay: 10,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
start: { x: xOffset + (3 * w) / 4, y: yOffset + h / 3, delay: 2 },
|
|
168
|
+
end: { x: xOffset + w / 4, y: yOffset + h / 3, delay: 4 },
|
|
169
|
+
color,
|
|
170
|
+
outDelay: 10,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
start: { x: xOffset + (3 * w) / 4, y: yOffset + h, delay: 0 },
|
|
174
|
+
end: { x: xOffset + (3 * w) / 4, y: yOffset + h / 3, delay: 2 },
|
|
175
|
+
color,
|
|
176
|
+
outDelay: 10,
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
start: { x: xOffset + (2 * w) / 4, y: yOffset + h, delay: 6 },
|
|
181
|
+
end: { x: xOffset + (2 * w) / 4, y: yOffset + (2 * h) / 3, delay: 7 },
|
|
182
|
+
color,
|
|
183
|
+
outDelay: 10,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
start: { x: 200, y: 280, delay: 2 },
|
|
188
|
+
end: { x: 260, y: 420, delay: 4 },
|
|
189
|
+
color,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
start: { x: 50, y: 550, delay: 2 },
|
|
193
|
+
end: { x: 150, y: 380, delay: 7 },
|
|
194
|
+
outDelay: 4,
|
|
195
|
+
color,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
start: { x: 280, y: 260, delay: 7.5 },
|
|
199
|
+
end: { x: 80, y: 280, delay: 12.5 },
|
|
200
|
+
color,
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
start: { x: 380, y: 460, delay: 15 },
|
|
205
|
+
end: { x: 180, y: 520, delay: 20 },
|
|
206
|
+
color,
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
function drawRoutes() {
|
|
211
|
+
if (!ctx) return
|
|
212
|
+
|
|
213
|
+
const debug = false
|
|
214
|
+
|
|
215
|
+
const currentTime = (Date.now() - startTime) / 1000 // Time in seconds
|
|
216
|
+
routes.forEach((route) => {
|
|
217
|
+
const elapsed = debug ? 100 : currentTime - route.start.delay
|
|
218
|
+
if (elapsed <= 0) return
|
|
219
|
+
|
|
220
|
+
const duration = route.end.delay - route.start.delay // animation duration
|
|
221
|
+
const progress = Math.min(elapsed / duration, 1) // normal progress
|
|
222
|
+
|
|
223
|
+
// Fade from end to start after the line is fully drawn
|
|
224
|
+
let startProgress = 0 // the starting point along the line
|
|
225
|
+
const outDelay = route.outDelay || 0
|
|
226
|
+
if (elapsed > duration + outDelay) {
|
|
227
|
+
const fadeProgress = Math.min(
|
|
228
|
+
(elapsed - (duration + outDelay)) / duration,
|
|
229
|
+
1,
|
|
230
|
+
)
|
|
231
|
+
startProgress = fadeProgress // move the start point forward
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Line vector
|
|
235
|
+
const dx = route.end.x - route.start.x
|
|
236
|
+
const dy = route.end.y - route.start.y
|
|
237
|
+
|
|
238
|
+
for (let x = 0; x < dimensions.width; x += gap) {
|
|
239
|
+
for (let y = 0; y < dimensions.height; y += gap) {
|
|
240
|
+
const t =
|
|
241
|
+
((x - route.start.x) * dx + (y - route.start.y) * dy) /
|
|
242
|
+
(dx * dx + dy * dy)
|
|
243
|
+
if (t < startProgress || t > progress) continue // only show dots in the visible segment
|
|
244
|
+
|
|
245
|
+
const closestX = route.start.x + dx * t
|
|
246
|
+
const closestY = route.start.y + dy * t
|
|
247
|
+
const distance = Math.hypot(x - closestX, y - closestY)
|
|
248
|
+
|
|
249
|
+
if (distance < gap) {
|
|
250
|
+
ctx.beginPath()
|
|
251
|
+
ctx.arc(x, y, dotRadius, 0, Math.PI * 2)
|
|
252
|
+
|
|
253
|
+
// Fade in: opacity goes from 0 → 1 as `progress` grows
|
|
254
|
+
const opacity = Math.min(
|
|
255
|
+
1,
|
|
256
|
+
Math.max(0, Math.min(t - startProgress, progress - t) * 5),
|
|
257
|
+
)
|
|
258
|
+
ctx.fillStyle = withOpacity(route.color, opacity)
|
|
259
|
+
|
|
260
|
+
ctx.fill()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (debug) {
|
|
266
|
+
const xStart = route.start.x
|
|
267
|
+
const yStart = route.start.y
|
|
268
|
+
const xEnd = route.start.x + dx * progress
|
|
269
|
+
const yEnd = route.start.y + dy * progress
|
|
270
|
+
|
|
271
|
+
ctx.beginPath()
|
|
272
|
+
ctx.moveTo(xStart, yStart)
|
|
273
|
+
ctx.lineTo(xEnd, yEnd)
|
|
274
|
+
ctx.strokeStyle = route.color
|
|
275
|
+
ctx.lineWidth = 1.5
|
|
276
|
+
ctx.stroke()
|
|
277
|
+
|
|
278
|
+
ctx.beginPath()
|
|
279
|
+
ctx.arc(xEnd, yEnd, 3, 0, Math.PI * 2)
|
|
280
|
+
ctx.fillStyle = route.color
|
|
281
|
+
ctx.fill()
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
287
|
+
const rect = canvas.getBoundingClientRect()
|
|
288
|
+
mousePosRef.current = {
|
|
289
|
+
x: e.x - rect.left,
|
|
290
|
+
y: e.y - rect.top,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
295
|
+
|
|
296
|
+
draw()
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
300
|
+
if (animationRef.current) cancelAnimationFrame(animationRef.current)
|
|
301
|
+
}
|
|
302
|
+
}, [dimensions, accentColor, backgroundColor, color])
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<canvas
|
|
306
|
+
ref={canvasRef}
|
|
307
|
+
className="absolute inset-0 w-full h-full pointer-events-none"
|
|
308
|
+
/>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const Head = ({
|
|
313
|
+
onStart,
|
|
314
|
+
sample,
|
|
315
|
+
color,
|
|
316
|
+
backgroundColor = color,
|
|
317
|
+
logo,
|
|
318
|
+
href,
|
|
319
|
+
subtitle,
|
|
320
|
+
}: {
|
|
321
|
+
onStart: () => void
|
|
322
|
+
sample: string
|
|
323
|
+
color: string
|
|
324
|
+
backgroundColor?: string
|
|
325
|
+
logo: string
|
|
326
|
+
href: string
|
|
327
|
+
subtitle?: string
|
|
328
|
+
}) => {
|
|
329
|
+
return (
|
|
330
|
+
<div
|
|
331
|
+
className="bg-zinc-900 w-(--card-width) flex flex-col items-center justify-center text-center relative"
|
|
332
|
+
style={{ '--color-sample': color } as React.CSSProperties}
|
|
333
|
+
>
|
|
334
|
+
<div className='z-10'>
|
|
335
|
+
<a href="https://openfort.io/" target="_blank" rel="noopener">
|
|
336
|
+
<img src="/openfort.svg" className="logo" alt="Openfort logo" />
|
|
337
|
+
</a>
|
|
338
|
+
<a href={href} target="_blank">
|
|
339
|
+
<img src={logo} className="logo sample-logo" alt={`${sample} logo`} />
|
|
340
|
+
</a>
|
|
341
|
+
</div>
|
|
342
|
+
<h1 className="relative z-10 text-4xl font-bold mb-4">
|
|
343
|
+
<span style={{ color: '#FC3627' }}>Openfort</span> +{' '}
|
|
344
|
+
<span style={{ color }}>{sample}</span>
|
|
345
|
+
</h1>
|
|
346
|
+
{subtitle && <p className="relative z-10 mb-6 text-sm max-w-2/3">{subtitle}</p>}
|
|
347
|
+
<button
|
|
348
|
+
className="relative z-10 lg:hidden mt-4 px-6 py-2 border border-zinc-500 rounded hover:bg-zinc-500/10 transition-colors cursor-pointer"
|
|
349
|
+
onClick={onStart}
|
|
350
|
+
>
|
|
351
|
+
Click here to Start
|
|
352
|
+
</button>
|
|
353
|
+
<p className="absolute z-10 text-zinc-400 mb-6 text-sm bottom-0">
|
|
354
|
+
Sign in to explore openfort capabilities.
|
|
355
|
+
</p>
|
|
356
|
+
<GlowCanvas accentColor={color} backgroundColor={backgroundColor} />
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
import { getTransactionHistory } from '../../lib/solana'
|
|
4
|
+
import { TruncateData } from '../ui/TruncateData'
|
|
5
|
+
|
|
6
|
+
interface TransactionHistoryItem {
|
|
7
|
+
signature: string
|
|
8
|
+
slot: number
|
|
9
|
+
blockTime: number | null
|
|
10
|
+
err: unknown | null
|
|
11
|
+
memo: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getSolanaExplorerUrl(signature: string, cluster: string): string {
|
|
15
|
+
const clusterParam = cluster === 'mainnet-beta' ? '' : `?cluster=${cluster}`
|
|
16
|
+
return `https://explorer.solana.com/tx/${signature}${clusterParam}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COLLAPSED_COUNT = 4
|
|
20
|
+
|
|
21
|
+
export const History = () => {
|
|
22
|
+
const { address, cluster, rpcUrl } = useSolanaEmbeddedWallet()
|
|
23
|
+
const [transactions, setTransactions] = useState<TransactionHistoryItem[]>([])
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
const [showAll, setShowAll] = useState(false)
|
|
27
|
+
|
|
28
|
+
const rpc = rpcUrl ?? 'https://api.devnet.solana.com'
|
|
29
|
+
|
|
30
|
+
const fetchHistory = () => {
|
|
31
|
+
if (!address) return
|
|
32
|
+
setIsLoading(true)
|
|
33
|
+
setError(null)
|
|
34
|
+
getTransactionHistory(address, 20, rpc)
|
|
35
|
+
.then(setTransactions)
|
|
36
|
+
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load history'))
|
|
37
|
+
.finally(() => setIsLoading(false))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetchHistory()
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [address, rpc])
|
|
44
|
+
|
|
45
|
+
const visible = showAll ? transactions : transactions.slice(0, COLLAPSED_COUNT)
|
|
46
|
+
const hasMore = transactions.length > COLLAPSED_COUNT
|
|
47
|
+
|
|
48
|
+
const formatTime = (blockTime: number | null) => {
|
|
49
|
+
if (!blockTime) return '--'
|
|
50
|
+
return new Date(blockTime * 1000).toLocaleString()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col w-full">
|
|
55
|
+
<div className="flex items-center justify-between mb-4">
|
|
56
|
+
<div>
|
|
57
|
+
<h1>Transaction History</h1>
|
|
58
|
+
<p className="text-sm text-zinc-400">Recent transactions for your Solana wallet.</p>
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={fetchHistory}
|
|
63
|
+
disabled={isLoading}
|
|
64
|
+
className="p-2 border border-zinc-700 rounded hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors cursor-pointer text-xs"
|
|
65
|
+
>
|
|
66
|
+
{isLoading ? '...' : 'Refresh'}
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{error && <TruncateData data={error} className="text-red-500" />}
|
|
71
|
+
|
|
72
|
+
{isLoading ? (
|
|
73
|
+
<p className="text-sm text-zinc-400">Loading...</p>
|
|
74
|
+
) : transactions.length === 0 ? (
|
|
75
|
+
<p className="text-sm text-zinc-400">No transactions found.</p>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="space-y-2">
|
|
78
|
+
{visible.map((tx) => {
|
|
79
|
+
const explorerUrl = cluster
|
|
80
|
+
? getSolanaExplorerUrl(tx.signature, cluster)
|
|
81
|
+
: null
|
|
82
|
+
const isError = tx.err != null
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
key={tx.signature}
|
|
87
|
+
className="flex items-center justify-between text-sm border-b border-zinc-700 pb-2 last:border-0"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex flex-col gap-0.5 min-w-0 flex-1 mr-2">
|
|
90
|
+
<span className="font-mono text-xs truncate">
|
|
91
|
+
{explorerUrl ? (
|
|
92
|
+
<a
|
|
93
|
+
href={explorerUrl}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noreferrer"
|
|
96
|
+
className="text-primary hover:underline"
|
|
97
|
+
>
|
|
98
|
+
{tx.signature.slice(0, 20)}...
|
|
99
|
+
</a>
|
|
100
|
+
) : (
|
|
101
|
+
`${tx.signature.slice(0, 20)}...`
|
|
102
|
+
)}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="text-xs text-zinc-400">{formatTime(tx.blockTime)}</span>
|
|
105
|
+
</div>
|
|
106
|
+
<span
|
|
107
|
+
className={`text-xs font-medium px-2 py-0.5 rounded-full shrink-0 ${
|
|
108
|
+
isError
|
|
109
|
+
? 'bg-red-900/30 text-red-400'
|
|
110
|
+
: 'bg-green-900/30 text-green-400'
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
{isError ? 'Failed' : 'Success'}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
118
|
+
|
|
119
|
+
{hasMore && (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => setShowAll((v) => !v)}
|
|
123
|
+
className="text-sm text-primary hover:underline w-full text-center pt-1 cursor-pointer"
|
|
124
|
+
>
|
|
125
|
+
{showAll
|
|
126
|
+
? 'Show less'
|
|
127
|
+
: `Show more (${transactions.length - COLLAPSED_COUNT})`}
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClockIcon,
|
|
3
|
+
HomeIcon,
|
|
4
|
+
PaperAirplaneIcon,
|
|
5
|
+
PencilSquareIcon,
|
|
6
|
+
WalletIcon,
|
|
7
|
+
} from '@heroicons/react/24/outline'
|
|
8
|
+
import { useUser } from '@openfort/react'
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
import { DesktopTabGroup, MobileTabGroup, type TabType } from '../ui/Tabs'
|
|
11
|
+
import { Auth } from './auth'
|
|
12
|
+
import { Head } from './head'
|
|
13
|
+
import { History } from './history'
|
|
14
|
+
import { Profile } from './profile'
|
|
15
|
+
import { Send } from './send'
|
|
16
|
+
import { Sign } from './sign'
|
|
17
|
+
import { Wallets } from './wallets'
|
|
18
|
+
|
|
19
|
+
interface LayoutProps {
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
step: number
|
|
22
|
+
tabs?: TabType[]
|
|
23
|
+
currentTab?: TabType
|
|
24
|
+
setCurrentTab?: (tab: TabType) => void
|
|
25
|
+
showTabs?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Layout = ({
|
|
29
|
+
children,
|
|
30
|
+
step,
|
|
31
|
+
tabs,
|
|
32
|
+
currentTab,
|
|
33
|
+
setCurrentTab,
|
|
34
|
+
showTabs,
|
|
35
|
+
}: LayoutProps) => {
|
|
36
|
+
return (
|
|
37
|
+
<div className="min-h-screen min-w-screen bg-zinc-900 flex flex-col items-center justify-center">
|
|
38
|
+
<div className="relative">
|
|
39
|
+
<DesktopTabGroup
|
|
40
|
+
tabs={tabs || []}
|
|
41
|
+
currentTab={currentTab}
|
|
42
|
+
setCurrentTab={setCurrentTab}
|
|
43
|
+
showTabs={showTabs}
|
|
44
|
+
/>
|
|
45
|
+
<div className="w-(--card-group-width) layout-card-group">
|
|
46
|
+
<div
|
|
47
|
+
className="h-(--card-group-height) grid grid-flow-col auto-cols-max transition-transform duration-500"
|
|
48
|
+
style={{
|
|
49
|
+
transform: `translateX(calc(-${step} * var(--card-width)))`,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Main = () => {
|
|
61
|
+
const { isAuthenticated } = useUser()
|
|
62
|
+
const [step, setStep] = useState(0)
|
|
63
|
+
|
|
64
|
+
const tabs: TabType[] = [
|
|
65
|
+
{
|
|
66
|
+
name: 'Home',
|
|
67
|
+
component: (
|
|
68
|
+
<Profile
|
|
69
|
+
sampleGithubUrl="https://github.com/openfort-xyz/openfort-react/tree/main/examples/quickstarts/solana-headless"
|
|
70
|
+
description="This is a demo app using Headless Wallet with Solana and Openfort."
|
|
71
|
+
/>
|
|
72
|
+
),
|
|
73
|
+
icon: HomeIcon,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Sign',
|
|
77
|
+
component: <Sign />,
|
|
78
|
+
icon: PencilSquareIcon,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Send',
|
|
82
|
+
component: <Send />,
|
|
83
|
+
icon: PaperAirplaneIcon,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'History',
|
|
87
|
+
component: <History />,
|
|
88
|
+
icon: ClockIcon,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'Wallets',
|
|
92
|
+
component: <Wallets />,
|
|
93
|
+
icon: WalletIcon,
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
const [currentTab, setCurrentTab] = useState<TabType>(tabs[0])
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Layout
|
|
100
|
+
step={step}
|
|
101
|
+
showTabs={!!isAuthenticated}
|
|
102
|
+
tabs={tabs}
|
|
103
|
+
currentTab={currentTab}
|
|
104
|
+
setCurrentTab={setCurrentTab}
|
|
105
|
+
>
|
|
106
|
+
<Head
|
|
107
|
+
onStart={() => setStep(1)}
|
|
108
|
+
sample="Solana"
|
|
109
|
+
color="rgba(153,69,255,1)"
|
|
110
|
+
logo="/solanaLogo.svg"
|
|
111
|
+
href="https://solana.com/"
|
|
112
|
+
subtitle="Get started with headless Solana wallet integration using Openfort Authentication"
|
|
113
|
+
/>
|
|
114
|
+
{!isAuthenticated ? (
|
|
115
|
+
<Auth />
|
|
116
|
+
) : (
|
|
117
|
+
<div className="block relative overflow-y-auto overflow-x-hidden">
|
|
118
|
+
<div className="card flex-col min-h-full">
|
|
119
|
+
<div className="w-full flex-1 relative">
|
|
120
|
+
{tabs.map((tab) => (
|
|
121
|
+
<div
|
|
122
|
+
key={tab.name}
|
|
123
|
+
className={`w-full flex-1 flex${currentTab.name === tab.name ? '' : ' hidden'}`}
|
|
124
|
+
>
|
|
125
|
+
{tab.component}
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
<MobileTabGroup
|
|
130
|
+
tabs={tabs}
|
|
131
|
+
currentTab={currentTab}
|
|
132
|
+
setCurrentTab={setCurrentTab}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
<div className="card relative" />
|
|
138
|
+
</Layout>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BookOpenIcon } from '@heroicons/react/24/outline'
|
|
2
|
+
import { useSignOut, useUser } from '@openfort/react'
|
|
3
|
+
import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
|
|
4
|
+
import { Wallets } from './wallets'
|
|
5
|
+
|
|
6
|
+
export const Profile = ({
|
|
7
|
+
sampleGithubUrl,
|
|
8
|
+
description,
|
|
9
|
+
}: {
|
|
10
|
+
sampleGithubUrl: string
|
|
11
|
+
description: string
|
|
12
|
+
}) => {
|
|
13
|
+
const { user } = useUser()
|
|
14
|
+
const isLocal = window.location.hostname === 'localhost'
|
|
15
|
+
const { signOut } = useSignOut()
|
|
16
|
+
const { status } = useSolanaEmbeddedWallet()
|
|
17
|
+
|
|
18
|
+
if (status !== 'connected' && status !== 'connecting') {
|
|
19
|
+
return <Wallets />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col flex-1 gap-4">
|
|
24
|
+
<h1 className="truncate">Welcome, {user?.name || user?.email}</h1>
|
|
25
|
+
<p className="text-zinc-400 text-sm">
|
|
26
|
+
{description}
|
|
27
|
+
<br />
|
|
28
|
+
You can sign messages and send SOL transactions.
|
|
29
|
+
</p>
|
|
30
|
+
<div className="border border-zinc-700 rounded p-4">
|
|
31
|
+
<h2 className="mb-2">Get started</h2>
|
|
32
|
+
<p className="mb-2 text-zinc-400 text-sm">
|
|
33
|
+
Start by creating a wallet, signing messages and sending SOL.
|
|
34
|
+
</p>
|
|
35
|
+
{isLocal ? (
|
|
36
|
+
<p className="mb-2 text-sm">
|
|
37
|
+
Edit <code>src/components/main.tsx</code> to customize the app.
|
|
38
|
+
</p>
|
|
39
|
+
) : (
|
|
40
|
+
<p className="mb-2 text-sm">
|
|
41
|
+
Clone this project and test it yourself, it is open source!
|
|
42
|
+
</p>
|
|
43
|
+
)}
|
|
44
|
+
<div className="flex gap-4 mt-4">
|
|
45
|
+
<a
|
|
46
|
+
href={sampleGithubUrl}
|
|
47
|
+
className="btn bg-inherit border border-zinc-600 hover:border-zinc-400 text-zinc-400 hover:text-white"
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noreferrer"
|
|
50
|
+
>
|
|
51
|
+
<img
|
|
52
|
+
src="/githubLogo.svg"
|
|
53
|
+
className="w-5 h-5 mr-2"
|
|
54
|
+
alt="GitHub logo"
|
|
55
|
+
/>
|
|
56
|
+
View on github
|
|
57
|
+
</a>
|
|
58
|
+
<a
|
|
59
|
+
href="https://www.openfort.io/docs/products/embedded-wallet/react"
|
|
60
|
+
target="_blank"
|
|
61
|
+
rel="noreferrer"
|
|
62
|
+
className="btn"
|
|
63
|
+
>
|
|
64
|
+
<BookOpenIcon className="h-5 w-5 mr-2" />
|
|
65
|
+
View docs
|
|
66
|
+
</a>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => {
|
|
72
|
+
signOut()
|
|
73
|
+
}}
|
|
74
|
+
className="btn mt-auto"
|
|
75
|
+
>
|
|
76
|
+
Sign Out
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|