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.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +24 -23
  3. package/package.json +1 -1
  4. package/template/openfort-templates/firebase/AGENTS.md +1 -1
  5. package/template/openfort-templates/firebase/package.json +3 -3
  6. package/template/openfort-templates/firebase/src/App.tsx +2 -1
  7. package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
  8. package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
  9. package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
  10. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
  11. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
  12. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
  13. package/template/openfort-templates/headless/AGENTS.md +1 -1
  14. package/template/openfort-templates/headless/package.json +2 -2
  15. package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
  16. package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
  17. package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
  18. package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
  19. package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
  20. package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
  21. package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
  22. package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
  23. package/template/openfort-templates/openfort-ui/package.json +3 -3
  24. package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
  25. package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
  26. package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
  27. package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
  28. package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
  29. package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
  30. package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
  31. package/template/openfort-templates/solana-headless/biome.json +70 -0
  32. package/template/openfort-templates/solana-headless/index.html +16 -0
  33. package/template/openfort-templates/solana-headless/package.json +34 -0
  34. package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
  35. package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
  36. package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
  37. package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
  38. package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
  39. package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
  40. package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
  41. package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
  42. package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
  43. package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
  44. package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
  45. package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
  46. package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
  47. package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
  48. package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
  49. package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
  50. package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
  51. package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
  52. package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
  53. package/template/openfort-templates/solana-headless/src/index.css +180 -0
  54. package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
  55. package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
  56. package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
  57. package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
  58. package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
  59. package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
  60. package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
  61. package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
  62. package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
  63. package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
  64. package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
  65. 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,4 @@
1
+ export function toError(err: unknown): Error {
2
+ if (err instanceof Error) return err
3
+ return new Error(String(err))
4
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export { fetchSolanaBalance } from './balance'
2
+ export { sendGaslessSolTransaction } from './kora'
3
+ export { sendSolTransaction } from './transaction'
4
+ export { getTransactionHistory } from './transactionHistory'