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,167 @@
|
|
|
1
|
+
import { CheckCircleIcon } from '@heroicons/react/24/outline'
|
|
2
|
+
import { RecoveryMethod } from '@openfort/react'
|
|
3
|
+
import type { EmbeddedAccount } from '@openfort/react'
|
|
4
|
+
import {
|
|
5
|
+
type ConnectedEmbeddedSolanaWallet,
|
|
6
|
+
useSolanaEmbeddedWallet,
|
|
7
|
+
} from '@openfort/react/solana'
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import { Sheet } from './ui/Sheet'
|
|
10
|
+
|
|
11
|
+
type CreateWalletPasswordSheetProps = {
|
|
12
|
+
open: boolean
|
|
13
|
+
onClose: () => void
|
|
14
|
+
onCreateWallet?: () => void
|
|
15
|
+
create: (options: {
|
|
16
|
+
recoveryMethod: RecoveryMethod
|
|
17
|
+
password?: string
|
|
18
|
+
}) => Promise<EmbeddedAccount>
|
|
19
|
+
status: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CreateWalletPasswordSheet = ({
|
|
23
|
+
open,
|
|
24
|
+
onClose,
|
|
25
|
+
onCreateWallet,
|
|
26
|
+
create,
|
|
27
|
+
status,
|
|
28
|
+
}: CreateWalletPasswordSheetProps) => {
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
const isCreating = status === 'creating'
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Sheet
|
|
34
|
+
open={open}
|
|
35
|
+
onClose={() => {
|
|
36
|
+
onClose()
|
|
37
|
+
setError(null)
|
|
38
|
+
}}
|
|
39
|
+
title="Enter Password"
|
|
40
|
+
description="Please enter the password of your wallet."
|
|
41
|
+
>
|
|
42
|
+
<form
|
|
43
|
+
className="flex-1 w-full flex flex-col justify-center max-w-md mx-auto"
|
|
44
|
+
onSubmit={async (e) => {
|
|
45
|
+
e.preventDefault()
|
|
46
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
47
|
+
const password = formData.get('password') as string
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await create({
|
|
51
|
+
recoveryMethod: RecoveryMethod.PASSWORD,
|
|
52
|
+
password,
|
|
53
|
+
})
|
|
54
|
+
onCreateWallet?.()
|
|
55
|
+
onClose()
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(err instanceof Error ? err.message : 'Failed to create wallet')
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<div className="flex flex-col gap-2 mr-4 mb-4">
|
|
62
|
+
<div className="flex items-center gap-2">
|
|
63
|
+
<CheckCircleIcon className="h-5 w-5 text-primary my-4 shrink-0" />
|
|
64
|
+
<span>This password will be used to secure your account.</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<CheckCircleIcon className="h-5 w-5 text-primary my-4 shrink-0" />
|
|
68
|
+
<span>
|
|
69
|
+
If you lose this password, you will not be able to access your
|
|
70
|
+
wallet.
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<input
|
|
75
|
+
type="password"
|
|
76
|
+
name="password"
|
|
77
|
+
autoComplete="new-password"
|
|
78
|
+
placeholder="Enter your wallet's password"
|
|
79
|
+
className="w-full mt-2 p-2 border border-gray-300 rounded"
|
|
80
|
+
/>
|
|
81
|
+
{error && (
|
|
82
|
+
<span className="text-red-500 text-sm mt-2">{error}</span>
|
|
83
|
+
)}
|
|
84
|
+
<button
|
|
85
|
+
className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
|
|
86
|
+
type="submit"
|
|
87
|
+
disabled={isCreating}
|
|
88
|
+
>
|
|
89
|
+
{isCreating ? 'Creating wallet...' : 'Create Wallet'}
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
</Sheet>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type WalletRecoverPasswordProps = {
|
|
97
|
+
open: boolean
|
|
98
|
+
onClose: () => void
|
|
99
|
+
wallet: ConnectedEmbeddedSolanaWallet | null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const WalletRecoverPasswordSheet = ({
|
|
103
|
+
open,
|
|
104
|
+
onClose,
|
|
105
|
+
wallet,
|
|
106
|
+
}: WalletRecoverPasswordProps) => {
|
|
107
|
+
const { setActive, status } = useSolanaEmbeddedWallet()
|
|
108
|
+
const [error, setError] = useState<string | null>(null)
|
|
109
|
+
const isConnecting = status === 'connecting'
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Sheet
|
|
113
|
+
open={open}
|
|
114
|
+
onClose={() => {
|
|
115
|
+
onClose()
|
|
116
|
+
setError(null)
|
|
117
|
+
}}
|
|
118
|
+
title="Enter Password"
|
|
119
|
+
description="Please enter the password of your wallet."
|
|
120
|
+
>
|
|
121
|
+
<form
|
|
122
|
+
className="w-full flex-1 flex flex-col justify-center"
|
|
123
|
+
onSubmit={async (e) => {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
126
|
+
const password = formData.get('password') as string
|
|
127
|
+
if (!wallet) throw new Error('No wallet to recover')
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await setActive({
|
|
131
|
+
address: wallet.address,
|
|
132
|
+
recoveryMethod: RecoveryMethod.PASSWORD,
|
|
133
|
+
password,
|
|
134
|
+
})
|
|
135
|
+
onClose()
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setError(err instanceof Error ? err.message : 'Failed to recover wallet')
|
|
138
|
+
}
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{wallet && (
|
|
142
|
+
<p>
|
|
143
|
+
Recover wallet {wallet.address.slice(0, 6)}...
|
|
144
|
+
{wallet.address.slice(-4)} with password
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
<input
|
|
148
|
+
type="password"
|
|
149
|
+
name="password"
|
|
150
|
+
autoComplete="current-password"
|
|
151
|
+
placeholder="Enter your wallet's password"
|
|
152
|
+
className="w-full mt-2 p-2 border border-gray-300 rounded"
|
|
153
|
+
/>
|
|
154
|
+
{error && (
|
|
155
|
+
<span className="text-red-500 text-sm mt-2">{error}</span>
|
|
156
|
+
)}
|
|
157
|
+
<button
|
|
158
|
+
className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
|
|
159
|
+
type="submit"
|
|
160
|
+
disabled={isConnecting}
|
|
161
|
+
>
|
|
162
|
+
{isConnecting ? 'Recovering...' : 'Recover Wallet'}
|
|
163
|
+
</button>
|
|
164
|
+
</form>
|
|
165
|
+
</Sheet>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ChainTypeEnum, OpenfortProvider } from '@openfort/react'
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
const queryClient = new QueryClient()
|
|
6
|
+
|
|
7
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<QueryClientProvider client={queryClient}>
|
|
10
|
+
<OpenfortProvider
|
|
11
|
+
publishableKey={import.meta.env.VITE_OPENFORT_PUBLISHABLE_KEY}
|
|
12
|
+
walletConfig={{
|
|
13
|
+
shieldPublishableKey: import.meta.env.VITE_SHIELD_PUBLISHABLE_KEY,
|
|
14
|
+
chainType: ChainTypeEnum.SVM,
|
|
15
|
+
createEncryptedSessionEndpoint: import.meta.env.VITE_CREATE_ENCRYPTED_SESSION_ENDPOINT,
|
|
16
|
+
connectOnLogin: true,
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</OpenfortProvider>
|
|
21
|
+
</QueryClientProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ChevronLeftIcon } from '@heroicons/react/24/outline'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
type SheetProps = {
|
|
5
|
+
open: boolean
|
|
6
|
+
onClose: () => void
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SheetInner = ({ onClose, title, description, children }: SheetProps) => {
|
|
13
|
+
const [isClosing, setIsClosing] = useState(false)
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isClosing) {
|
|
16
|
+
const timer = setTimeout(onClose, 300)
|
|
17
|
+
return () => clearTimeout(timer)
|
|
18
|
+
}
|
|
19
|
+
}, [isClosing, onClose])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="flex flex-col m-0 p-4 absolute inset-0 bg-zinc-800 data-[closing=false]:animate-sheet-in data-[closing=true]:animate-sheet-out outline-l outline-1 outline-zinc-700"
|
|
24
|
+
data-closing={isClosing}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex items-center gap-2 mb-4">
|
|
27
|
+
<button
|
|
28
|
+
className="rounded p-2 hover:text-white transition-colors cursor-pointer"
|
|
29
|
+
onClick={() => setIsClosing(true)}
|
|
30
|
+
>
|
|
31
|
+
<ChevronLeftIcon className="h-5 w-5" />
|
|
32
|
+
</button>
|
|
33
|
+
<div>
|
|
34
|
+
<h2 className="mb-0">{title}</h2>
|
|
35
|
+
<p className="text-sm text-zinc-400">{description}</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Sheet = (props: SheetProps) => {
|
|
44
|
+
if (!props.open) return null
|
|
45
|
+
|
|
46
|
+
return <SheetInner {...props} />
|
|
47
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export type TabType = {
|
|
2
|
+
name: string
|
|
3
|
+
component: React.ReactNode
|
|
4
|
+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type TabProps = {
|
|
8
|
+
onClick?: () => void
|
|
9
|
+
isActive?: boolean
|
|
10
|
+
} & TabType
|
|
11
|
+
|
|
12
|
+
const DesktopTab = ({ name, isActive, ...buttonProps }: TabProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
className="relative h-8 mx-2.5 transition-colors cursor-pointer"
|
|
16
|
+
style={
|
|
17
|
+
{
|
|
18
|
+
'--tab-bg-color': isActive
|
|
19
|
+
? 'var(--color-zinc-800)'
|
|
20
|
+
: 'var(--color-zinc-700)',
|
|
21
|
+
opacity: isActive ? 1 : 0.6,
|
|
22
|
+
} as React.CSSProperties
|
|
23
|
+
}
|
|
24
|
+
{...buttonProps}
|
|
25
|
+
>
|
|
26
|
+
<div className="absolute w-5 h-8 bg-(--tab-bg-color) rotate-20 transform origin-top-left top-1" />
|
|
27
|
+
<div className="absolute w-5 h-8 bg-(--tab-bg-color) -rotate-20 transform origin-top-right right-0 top-1" />
|
|
28
|
+
<div className="absolute inset-0 rounded-md bg-(--tab-bg-color)" />
|
|
29
|
+
|
|
30
|
+
<span
|
|
31
|
+
className={`${isActive ? 'text-white' : 'text-zinc-400'} whitespace-nowrap bg-(--tab-bg-color) z-10 relative mx-2 pb-4`}
|
|
32
|
+
>
|
|
33
|
+
{name}
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type TabGroupProps = {
|
|
40
|
+
tabs: TabType[]
|
|
41
|
+
currentTab?: TabType
|
|
42
|
+
setCurrentTab?: (tab: TabType) => void
|
|
43
|
+
showTabs?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DesktopTabGroup = ({
|
|
47
|
+
tabs,
|
|
48
|
+
currentTab,
|
|
49
|
+
setCurrentTab,
|
|
50
|
+
showTabs,
|
|
51
|
+
}: TabGroupProps) => {
|
|
52
|
+
return (
|
|
53
|
+
<div className="absolute left-[100%] top-2 rotate-90 transform origin-top-left hidden xs:block">
|
|
54
|
+
<div
|
|
55
|
+
className="flex gap-2 transition-transform duration-500"
|
|
56
|
+
style={{
|
|
57
|
+
transform: showTabs ? 'translateY(-100%)' : 'translateY(10px)',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{tabs?.map((tab) => (
|
|
61
|
+
<DesktopTab
|
|
62
|
+
key={tab.name}
|
|
63
|
+
onClick={() => setCurrentTab?.(tab)}
|
|
64
|
+
isActive={currentTab?.name === tab.name}
|
|
65
|
+
{...tab}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MobileTab = ({
|
|
74
|
+
name,
|
|
75
|
+
isActive,
|
|
76
|
+
icon: Icon,
|
|
77
|
+
...buttonProps
|
|
78
|
+
}: TabProps) => {
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
className="relative h-8 mx-2.5 transition-colors cursor-pointer"
|
|
82
|
+
{...buttonProps}
|
|
83
|
+
>
|
|
84
|
+
<Icon className="h-5 w-5 mx-auto mb-1" />
|
|
85
|
+
<span
|
|
86
|
+
className={`${isActive ? 'text-white' : 'text-zinc-400'} whitespace-nowrap`}
|
|
87
|
+
>
|
|
88
|
+
{name}
|
|
89
|
+
</span>
|
|
90
|
+
</button>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const MobileTabGroup = ({
|
|
95
|
+
tabs,
|
|
96
|
+
currentTab,
|
|
97
|
+
setCurrentTab,
|
|
98
|
+
}: TabGroupProps) => {
|
|
99
|
+
return (
|
|
100
|
+
<div className="mt-auto xs:hidden flex pt-6 pb-2 items-end justify-between text-zinc-400 text-sm">
|
|
101
|
+
{tabs?.map((tab) => (
|
|
102
|
+
<MobileTab
|
|
103
|
+
key={tab.name}
|
|
104
|
+
onClick={() => setCurrentTab?.(tab)}
|
|
105
|
+
isActive={currentTab?.name === tab.name}
|
|
106
|
+
{...tab}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export const TruncateData = ({
|
|
4
|
+
className,
|
|
5
|
+
data,
|
|
6
|
+
}: {
|
|
7
|
+
className?: string
|
|
8
|
+
data?: string
|
|
9
|
+
}) => {
|
|
10
|
+
const [viewMore, setViewMore] = useState(false)
|
|
11
|
+
if (!data) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={`mt-4 p-2 border border-zinc-700 rounded bg-zinc-900 ${className}`}
|
|
16
|
+
>
|
|
17
|
+
<pre className="break-words whitespace-normal text-sm">
|
|
18
|
+
{viewMore ? data : data.length > 90 ? `${data.slice(0, 90)}...` : data}
|
|
19
|
+
</pre>
|
|
20
|
+
{data.length > 90 && (
|
|
21
|
+
<button
|
|
22
|
+
className="text-primary hover:text-primary-hover transition-colors hover:underline text-sm cursor-pointer"
|
|
23
|
+
onClick={() => setViewMore(!viewMore)}
|
|
24
|
+
type="button"
|
|
25
|
+
>
|
|
26
|
+
{viewMore ? 'View less' : 'View more'}
|
|
27
|
+
</button>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign messages with Openfort Solana embedded wallet.
|
|
3
|
+
* Returns { data, signMessage, isPending, error }.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
|
|
7
|
+
import { useCallback, useState } from 'react'
|
|
8
|
+
import { toError } from '../lib/errors'
|
|
9
|
+
|
|
10
|
+
export function useSolanaMessageSigner() {
|
|
11
|
+
const solana = useSolanaEmbeddedWallet()
|
|
12
|
+
const [data, setData] = useState<string | null>(null)
|
|
13
|
+
const [error, setError] = useState<Error | null>(null)
|
|
14
|
+
const [isPending, setIsPending] = useState(false)
|
|
15
|
+
|
|
16
|
+
const signMessage = useCallback(
|
|
17
|
+
async (params: { message: string }) => {
|
|
18
|
+
if (solana.status !== 'connected') {
|
|
19
|
+
setError(new Error('Wallet not connected'))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
setError(null)
|
|
23
|
+
setIsPending(true)
|
|
24
|
+
try {
|
|
25
|
+
const signature = await solana.provider.signMessage(params.message)
|
|
26
|
+
setData(signature)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setError(toError(err))
|
|
29
|
+
} finally {
|
|
30
|
+
setIsPending(false)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
[solana.status, solana.provider]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return { data: data ?? undefined, signMessage, isPending, error: error ?? null }
|
|
37
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
@theme inline {
|
|
6
|
+
--color-primary: var(--color-primary);
|
|
7
|
+
--color-primary-hover: var(--color-primary-hover);
|
|
8
|
+
--breakpoint-xs: 34rem;
|
|
9
|
+
|
|
10
|
+
--animate-sheet-in: sheet-in 0.3s ease forwards;
|
|
11
|
+
--animate-sheet-out: sheet-out 0.3s ease forwards;
|
|
12
|
+
|
|
13
|
+
@keyframes sheet-in {
|
|
14
|
+
from {
|
|
15
|
+
transform: translateX(100%);
|
|
16
|
+
opacity: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
to {
|
|
20
|
+
transform: translateX(0);
|
|
21
|
+
opacity: 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes sheet-out {
|
|
26
|
+
to {
|
|
27
|
+
transform: translateX(100%);
|
|
28
|
+
opacity: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
from {
|
|
32
|
+
transform: translateX(0);
|
|
33
|
+
opacity: 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@layer base {
|
|
39
|
+
.layout-card-group {
|
|
40
|
+
@media (min-width: 34rem) {
|
|
41
|
+
@apply border border-zinc-700 rounded-lg shadow-lg shadow-white/10;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* @media (max-width: 64rem) { */
|
|
45
|
+
@apply overflow-hidden;
|
|
46
|
+
/* } */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
h1 {
|
|
50
|
+
@apply text-2xl font-bold
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
h2 {
|
|
54
|
+
@apply text-xl font-bold
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.card {
|
|
58
|
+
@apply p-6 w-(--card-width) bg-zinc-800 flex justify-center flex-col;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.btn {
|
|
62
|
+
@apply flex items-center justify-center w-full py-2 px-4 bg-primary text-white rounded cursor-pointer hover:bg-primary-hover transition-colors;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
input {
|
|
66
|
+
@apply px-4 py-2 rounded-md bg-zinc-700 border-none outline-none w-full;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:root {
|
|
71
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
font-weight: 400;
|
|
74
|
+
|
|
75
|
+
color-scheme: light dark;
|
|
76
|
+
color: rgba(255, 255, 255, 0.87);
|
|
77
|
+
background-color: #242424;
|
|
78
|
+
|
|
79
|
+
font-synthesis: none;
|
|
80
|
+
text-rendering: optimizeLegibility;
|
|
81
|
+
-webkit-font-smoothing: antialiased;
|
|
82
|
+
-moz-osx-font-smoothing: grayscale;
|
|
83
|
+
|
|
84
|
+
--color-primary: rgba(153, 69, 255, 1);
|
|
85
|
+
--color-primary-foreground: #fff;
|
|
86
|
+
--color-primary-hover: rgba(133, 49, 235, 1);
|
|
87
|
+
|
|
88
|
+
--card-group-width: 58rem;
|
|
89
|
+
--card-width: 29rem;
|
|
90
|
+
--card-group-height: 600px;
|
|
91
|
+
|
|
92
|
+
@media (max-width: calc(64rem - 1px)) {
|
|
93
|
+
--card-group-width: 29rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@media (max-width: calc(34rem - 1px)) {
|
|
97
|
+
--card-width: 100vw;
|
|
98
|
+
--card-group-width: 100vw;
|
|
99
|
+
--card-group-height: 100dvh;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.logo {
|
|
104
|
+
display: inline;
|
|
105
|
+
height: 7rem;
|
|
106
|
+
width: 7rem;
|
|
107
|
+
padding: 1em;
|
|
108
|
+
will-change: filter;
|
|
109
|
+
transition: filter 300ms;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.logo:hover {
|
|
113
|
+
filter: drop-shadow(0 0 3em rgba(153, 69, 255, 0.8));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
.logo.sample-logo:hover {
|
|
119
|
+
filter: drop-shadow(0 0 3em var(--color-sample));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.wallet-option-group:hover .wallet-option {
|
|
123
|
+
color: var(--color-zinc-700);
|
|
124
|
+
border-color: var(--color-zinc-700);
|
|
125
|
+
background-color: var(--color-zinc-900);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.wallet-option-group:hover .wallet-option:hover {
|
|
129
|
+
color: var(--color-zinc-100);
|
|
130
|
+
border-color: var(--color-zinc-100);
|
|
131
|
+
background-color: var(--color-zinc-800);
|
|
132
|
+
filter: drop-shadow(0 0 1em #ffffff33);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.wallet-option {
|
|
136
|
+
padding: 1em;
|
|
137
|
+
text-decoration: none;
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
flex: 1;
|
|
140
|
+
display: flex;
|
|
141
|
+
gap: 1rem;
|
|
142
|
+
align-items: center;
|
|
143
|
+
color: var(--color-zinc-100);
|
|
144
|
+
border: 1px solid var(--color-zinc-100);
|
|
145
|
+
background-color: var(--color-zinc-800);
|
|
146
|
+
transition:
|
|
147
|
+
border-color 300ms,
|
|
148
|
+
background-color 300ms,
|
|
149
|
+
color 300ms,
|
|
150
|
+
filter 300ms;
|
|
151
|
+
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
|
|
154
|
+
width: 100%;
|
|
155
|
+
min-height: 2rem;
|
|
156
|
+
|
|
157
|
+
.hover-description {
|
|
158
|
+
transition:
|
|
159
|
+
transform 300ms,
|
|
160
|
+
max-height 500ms,
|
|
161
|
+
opacity 300ms;
|
|
162
|
+
|
|
163
|
+
max-height: 2.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
h4 {
|
|
167
|
+
font-size: 1.25rem;
|
|
168
|
+
margin: 0;
|
|
169
|
+
margin-right: 1rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
svg {
|
|
173
|
+
height: 50px;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.wallet-option:not(:hover) .hover-description {
|
|
178
|
+
max-height: 0;
|
|
179
|
+
opacity: 0;
|
|
180
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const DEFAULT_RPC = 'https://api.devnet.solana.com'
|
|
2
|
+
|
|
3
|
+
export async function fetchSolanaBalance(rpcUrl: string | undefined, address: string): Promise<number> {
|
|
4
|
+
const rpc = rpcUrl ?? DEFAULT_RPC
|
|
5
|
+
const res = await fetch(rpc, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
jsonrpc: '2.0',
|
|
10
|
+
id: 1,
|
|
11
|
+
method: 'getBalance',
|
|
12
|
+
params: [address, { commitment: 'confirmed' }],
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
const data = await res.json()
|
|
16
|
+
return data.result?.value ?? 0
|
|
17
|
+
}
|