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
package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { CheckCircleIcon } from '@heroicons/react/24/outline'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AccountTypeEnum,
|
|
4
|
+
RecoveryMethod,
|
|
5
|
+
type ConnectedEmbeddedEthereumWallet,
|
|
6
|
+
} from '@openfort/react'
|
|
7
|
+
import type { EmbeddedAccount } from '@openfort/react'
|
|
8
|
+
import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
|
|
9
|
+
import { useState } from 'react'
|
|
3
10
|
|
|
4
11
|
import { Sheet } from '../../../components/ui/Sheet'
|
|
5
12
|
|
|
@@ -7,21 +14,32 @@ type CreateWalletPasswordSheetProps = {
|
|
|
7
14
|
open: boolean
|
|
8
15
|
onClose: () => void
|
|
9
16
|
onCreateWallet?: () => void
|
|
17
|
+
create: (options: {
|
|
18
|
+
recoveryMethod: RecoveryMethod
|
|
19
|
+
accountType?: AccountTypeEnum
|
|
20
|
+
password?: string
|
|
21
|
+
}) => Promise<EmbeddedAccount>
|
|
22
|
+
status: string
|
|
23
|
+
accountType: AccountTypeEnum
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
export function CreateWalletPasswordSheet({
|
|
13
27
|
open,
|
|
14
28
|
onClose,
|
|
15
29
|
onCreateWallet,
|
|
30
|
+
create,
|
|
31
|
+
status,
|
|
32
|
+
accountType,
|
|
16
33
|
}: CreateWalletPasswordSheetProps) {
|
|
17
|
-
const
|
|
34
|
+
const [error, setError] = useState<string | null>(null)
|
|
35
|
+
const isCreating = status === 'creating'
|
|
18
36
|
|
|
19
37
|
return (
|
|
20
38
|
<Sheet
|
|
21
39
|
open={open}
|
|
22
40
|
onClose={() => {
|
|
23
41
|
onClose()
|
|
24
|
-
|
|
42
|
+
setError(null)
|
|
25
43
|
}}
|
|
26
44
|
title="Enter Password"
|
|
27
45
|
description="Please enter the password of your wallet."
|
|
@@ -33,16 +51,16 @@ export function CreateWalletPasswordSheet({
|
|
|
33
51
|
const formData = new FormData(event.target as HTMLFormElement)
|
|
34
52
|
const password = formData.get('password') as string
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
try {
|
|
55
|
+
await create({
|
|
38
56
|
recoveryMethod: RecoveryMethod.PASSWORD,
|
|
57
|
+
accountType,
|
|
39
58
|
password,
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
if (!walletError) {
|
|
59
|
+
})
|
|
44
60
|
onCreateWallet?.()
|
|
45
61
|
onClose()
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setError(err instanceof Error ? err.message : 'Failed to create wallet')
|
|
46
64
|
}
|
|
47
65
|
}}
|
|
48
66
|
>
|
|
@@ -62,11 +80,12 @@ export function CreateWalletPasswordSheet({
|
|
|
62
80
|
<input
|
|
63
81
|
type="password"
|
|
64
82
|
name="password"
|
|
83
|
+
autoComplete="new-password"
|
|
65
84
|
placeholder="Enter your wallet's password"
|
|
66
85
|
className="w-full mt-2 p-2 border border-gray-300 rounded"
|
|
67
86
|
/>
|
|
68
87
|
{error && (
|
|
69
|
-
<span className="text-red-500 text-sm mt-2">{error
|
|
88
|
+
<span className="text-red-500 text-sm mt-2">{error}</span>
|
|
70
89
|
)}
|
|
71
90
|
<button
|
|
72
91
|
className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
|
|
@@ -83,7 +102,7 @@ export function CreateWalletPasswordSheet({
|
|
|
83
102
|
type WalletRecoverPasswordSheetProps = {
|
|
84
103
|
open: boolean
|
|
85
104
|
onClose: () => void
|
|
86
|
-
wallet:
|
|
105
|
+
wallet: ConnectedEmbeddedEthereumWallet | null
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
export function WalletRecoverPasswordSheet({
|
|
@@ -91,34 +110,38 @@ export function WalletRecoverPasswordSheet({
|
|
|
91
110
|
onClose,
|
|
92
111
|
wallet,
|
|
93
112
|
}: WalletRecoverPasswordSheetProps) {
|
|
94
|
-
const {
|
|
113
|
+
const { setActive, status } = useEthereumEmbeddedWallet()
|
|
114
|
+
const [error, setError] = useState<string | null>(null)
|
|
115
|
+
const isConnecting = status === 'connecting'
|
|
95
116
|
|
|
96
117
|
return (
|
|
97
118
|
<Sheet
|
|
98
119
|
open={open}
|
|
99
120
|
onClose={() => {
|
|
100
121
|
onClose()
|
|
101
|
-
|
|
122
|
+
setError(null)
|
|
102
123
|
}}
|
|
103
124
|
title="Enter Password"
|
|
104
125
|
description="Please enter the password of your wallet."
|
|
105
126
|
>
|
|
106
127
|
<form
|
|
107
128
|
className="w-full flex-1 flex flex-col justify-center"
|
|
108
|
-
onSubmit={(event) => {
|
|
129
|
+
onSubmit={async (event) => {
|
|
109
130
|
event.preventDefault()
|
|
110
131
|
const formData = new FormData(event.target as HTMLFormElement)
|
|
111
132
|
const password = formData.get('password') as string
|
|
112
133
|
if (!wallet) throw new Error('No wallet to recover')
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
try {
|
|
136
|
+
await setActive({
|
|
137
|
+
address: wallet.address,
|
|
117
138
|
recoveryMethod: RecoveryMethod.PASSWORD,
|
|
118
139
|
password,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
})
|
|
140
|
+
})
|
|
141
|
+
onClose()
|
|
142
|
+
} catch (err) {
|
|
143
|
+
setError(err instanceof Error ? err.message : 'Failed to recover wallet')
|
|
144
|
+
}
|
|
122
145
|
}}
|
|
123
146
|
>
|
|
124
147
|
{wallet && (
|
|
@@ -130,11 +153,12 @@ export function WalletRecoverPasswordSheet({
|
|
|
130
153
|
<input
|
|
131
154
|
type="password"
|
|
132
155
|
name="password"
|
|
156
|
+
autoComplete="current-password"
|
|
133
157
|
placeholder="Enter your wallet's password"
|
|
134
158
|
className="w-full mt-2 p-2 border border-gray-300 rounded"
|
|
135
159
|
/>
|
|
136
160
|
{error && (
|
|
137
|
-
<span className="text-red-500 text-sm mt-2">{error
|
|
161
|
+
<span className="text-red-500 text-sm mt-2">{error}</span>
|
|
138
162
|
)}
|
|
139
163
|
<button
|
|
140
164
|
className="mt-4 w-full bg-zinc-700 text-white p-2 rounded cursor-pointer"
|
|
@@ -84,7 +84,7 @@ Required environment variables for headless operation:
|
|
|
84
84
|
```env
|
|
85
85
|
VITE_OPENFORT_PUBLISHABLE_KEY=pk_test_...
|
|
86
86
|
VITE_SHIELD_PUBLISHABLE_KEY=sk_test_...
|
|
87
|
-
|
|
87
|
+
VITE_FEE_SPONSORSHIP_ID=pol_...
|
|
88
88
|
VITE_CREATE_ENCRYPTED_SESSION_ENDPOINT=/api/shield-session
|
|
89
89
|
VITE_WALLET_CONNECT_PROJECT_ID=your-wallet-connect-project-id
|
|
90
90
|
```
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@heroicons/react": "^2.2.0",
|
|
15
|
-
"@openfort/react": "
|
|
15
|
+
"@openfort/react": "workspace:^",
|
|
16
16
|
"@tanstack/react-query": ">=5.45.1",
|
|
17
17
|
"react": "^18.2.0",
|
|
18
18
|
"react-dom": "^18.2.0",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@biomejs/biome": "^2.3.8",
|
|
24
24
|
"@tailwindcss/vite": "^4.1.15",
|
|
25
|
-
"tailwindcss": "^4.1.15",
|
|
26
25
|
"@types/react": "^19.1.10",
|
|
27
26
|
"@types/react-dom": "^19.1.7",
|
|
28
27
|
"@vitejs/plugin-react": "^4.3.4",
|
|
28
|
+
"tailwindcss": "^4.1.15",
|
|
29
29
|
"typescript": "~5.8.3",
|
|
30
30
|
"vite": "^6.4.1"
|
|
31
31
|
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { getAddress, parseAbi } from 'viem'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useChains,
|
|
6
|
-
useReadContract,
|
|
7
|
-
useWriteContract
|
|
8
|
-
} from 'wagmi'
|
|
3
|
+
import { useAccount, useChainId, useChains, useReadContract, useWriteContract } from 'wagmi'
|
|
4
|
+
import { getMintContractConfig } from '../../lib/contracts'
|
|
9
5
|
import { TruncateData } from '../ui/TruncateData'
|
|
10
6
|
|
|
11
7
|
const MintContract = () => {
|
|
12
8
|
const { address } = useAccount()
|
|
9
|
+
const chainId = useChainId()
|
|
10
|
+
const config = getMintContractConfig(chainId)
|
|
13
11
|
|
|
14
12
|
const {
|
|
15
13
|
data: balance,
|
|
16
14
|
refetch,
|
|
17
15
|
error: balanceError,
|
|
18
16
|
} = useReadContract({
|
|
19
|
-
address:
|
|
17
|
+
address: (config?.address ?? undefined) as `0x${string}` | undefined,
|
|
20
18
|
abi: [
|
|
21
19
|
{
|
|
22
20
|
type: 'function',
|
|
@@ -27,11 +25,11 @@ const MintContract = () => {
|
|
|
27
25
|
},
|
|
28
26
|
],
|
|
29
27
|
functionName: 'balanceOf',
|
|
30
|
-
args: [address
|
|
28
|
+
args: config && address ? [address] : undefined,
|
|
31
29
|
})
|
|
32
30
|
|
|
33
31
|
const { data: tokenSymbol } = useReadContract({
|
|
34
|
-
address:
|
|
32
|
+
address: (config?.address ?? undefined) as `0x${string}` | undefined,
|
|
35
33
|
abi: [
|
|
36
34
|
{
|
|
37
35
|
type: 'function',
|
|
@@ -62,12 +60,23 @@ const MintContract = () => {
|
|
|
62
60
|
})
|
|
63
61
|
|
|
64
62
|
async function submit({ amount }: { amount: string }) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
if (!config?.address) return
|
|
64
|
+
const amountWei = BigInt(amount) * BigInt(10 ** 18)
|
|
65
|
+
if (config.type === 'claim') {
|
|
66
|
+
writeContract({
|
|
67
|
+
address: getAddress(config.address),
|
|
68
|
+
abi: parseAbi(['function claim(uint256 amount)']),
|
|
69
|
+
functionName: 'claim',
|
|
70
|
+
args: [amountWei],
|
|
71
|
+
})
|
|
72
|
+
} else {
|
|
73
|
+
writeContract({
|
|
74
|
+
address: getAddress(config.address),
|
|
75
|
+
abi: parseAbi(['function mint(address to, uint256 amount)']),
|
|
76
|
+
functionName: 'mint',
|
|
77
|
+
args: [address!, amountWei],
|
|
78
|
+
})
|
|
79
|
+
}
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
return (
|
|
@@ -90,7 +99,7 @@ const MintContract = () => {
|
|
|
90
99
|
className="grow peer"
|
|
91
100
|
name="amount"
|
|
92
101
|
/>
|
|
93
|
-
<button className="btn" disabled={isPending || !address}>
|
|
102
|
+
<button className="btn" disabled={isPending || !address || !config}>
|
|
94
103
|
{isPending ? 'Minting...' : 'Mint Tokens'}
|
|
95
104
|
</button>
|
|
96
105
|
</form>
|
|
@@ -102,7 +111,7 @@ const MintContract = () => {
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
export const Actions = () => {
|
|
105
|
-
const
|
|
114
|
+
const hasFeeSponsorship = useMemo(() => !!import.meta.env.VITE_FEE_SPONSORSHIP_ID, [])
|
|
106
115
|
const chains = useChains()
|
|
107
116
|
return (
|
|
108
117
|
<div className="flex flex-col w-full">
|
|
@@ -110,7 +119,7 @@ export const Actions = () => {
|
|
|
110
119
|
<span className="mb-4 text-zinc-400 text-sm">
|
|
111
120
|
Interact with smart contracts on the blockchain.
|
|
112
121
|
</span>
|
|
113
|
-
{!
|
|
122
|
+
{!hasFeeSponsorship && (
|
|
114
123
|
<div className="mb-3 p-3 bg-red-800 text-white rounded text-sm">
|
|
115
124
|
<strong>Warning: Transactions are not sponsored.</strong> Minting may
|
|
116
125
|
fail because transactions are not being sponsored. To sponsor
|
|
@@ -123,9 +132,9 @@ export const Actions = () => {
|
|
|
123
132
|
>
|
|
124
133
|
Openfort Dashboard
|
|
125
134
|
</a>{' '}
|
|
126
|
-
and <b>create a
|
|
127
|
-
<b>{chains[0].name}</b>. Set the <code>
|
|
128
|
-
environment variable with the
|
|
135
|
+
and <b>create a fee sponsorship</b> for transactions in{' '}
|
|
136
|
+
<b>{chains[0].name}</b>. Set the <code>VITE_FEE_SPONSORSHIP_ID</code>{' '}
|
|
137
|
+
environment variable with the fee sponsorship ID.
|
|
129
138
|
</div>
|
|
130
139
|
)}
|
|
131
140
|
<MintContract />
|
|
@@ -28,7 +28,6 @@ export const Profile = ({
|
|
|
28
28
|
<br />
|
|
29
29
|
You can sign messages and interact with smart contracts.
|
|
30
30
|
</p>
|
|
31
|
-
<p>
|
|
32
31
|
<div className="border border-zinc-700 rounded p-4">
|
|
33
32
|
<h2 className="mb-2">Get started</h2>
|
|
34
33
|
<p className="mb-2 text-zinc-400 text-sm">
|
|
@@ -69,7 +68,6 @@ export const Profile = ({
|
|
|
69
68
|
</a>
|
|
70
69
|
</div>
|
|
71
70
|
</div>
|
|
72
|
-
</p>
|
|
73
71
|
|
|
74
72
|
<button
|
|
75
73
|
onClick={() => {
|
|
@@ -1,42 +1,175 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CheckIcon,
|
|
3
|
+
ChevronDownIcon,
|
|
4
|
+
ChevronUpIcon,
|
|
5
|
+
ClipboardDocumentIcon,
|
|
2
6
|
FingerPrintIcon,
|
|
3
7
|
KeyIcon,
|
|
4
8
|
LockClosedIcon,
|
|
5
9
|
} from '@heroicons/react/24/outline'
|
|
6
10
|
import {
|
|
11
|
+
AccountTypeEnum,
|
|
7
12
|
RecoveryMethod,
|
|
8
|
-
type
|
|
13
|
+
type ConnectedEmbeddedEthereumWallet,
|
|
9
14
|
useSignOut,
|
|
10
15
|
useUser,
|
|
11
|
-
useWallets,
|
|
12
16
|
} from '@openfort/react'
|
|
13
|
-
import {
|
|
17
|
+
import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
|
|
18
|
+
import { useMemo, useState } from 'react'
|
|
14
19
|
import { useAccount } from 'wagmi'
|
|
15
20
|
import { CreateWallet, CreateWalletSheet } from '../createWallet'
|
|
16
21
|
import { WalletRecoverPasswordSheet } from '../passwordRecovery'
|
|
17
22
|
|
|
23
|
+
const ACCOUNT_TYPE_LABELS: Record<AccountTypeEnum, string> = {
|
|
24
|
+
[AccountTypeEnum.EOA]: 'EOA',
|
|
25
|
+
[AccountTypeEnum.SMART_ACCOUNT]: 'SM',
|
|
26
|
+
[AccountTypeEnum.DELEGATED_ACCOUNT]: 'DE',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VISIBLE_WALLET_COUNT = 4
|
|
30
|
+
|
|
31
|
+
const WalletRecoveryBadge = ({ wallet }: { wallet: ConnectedEmbeddedEthereumWallet }) => {
|
|
32
|
+
let Icon = LockClosedIcon
|
|
33
|
+
let text = 'Unknown'
|
|
34
|
+
|
|
35
|
+
switch (wallet.recoveryMethod) {
|
|
36
|
+
case RecoveryMethod.PASSWORD:
|
|
37
|
+
Icon = KeyIcon
|
|
38
|
+
text = 'Password'
|
|
39
|
+
break
|
|
40
|
+
case RecoveryMethod.AUTOMATIC:
|
|
41
|
+
Icon = LockClosedIcon
|
|
42
|
+
text = 'Automatic'
|
|
43
|
+
break
|
|
44
|
+
case RecoveryMethod.PASSKEY:
|
|
45
|
+
Icon = FingerPrintIcon
|
|
46
|
+
text = 'Passkey'
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex items-center text-xs">
|
|
52
|
+
<span>{text}</span>
|
|
53
|
+
<Icon className="h-5 w-5 ml-2" />
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const WalletItem = ({
|
|
59
|
+
wallet,
|
|
60
|
+
isActive,
|
|
61
|
+
isConnecting,
|
|
62
|
+
nested,
|
|
63
|
+
onClick,
|
|
64
|
+
}: {
|
|
65
|
+
wallet: ConnectedEmbeddedEthereumWallet
|
|
66
|
+
isActive: boolean
|
|
67
|
+
isConnecting: boolean
|
|
68
|
+
nested?: boolean
|
|
69
|
+
onClick: () => void
|
|
70
|
+
}) => (
|
|
71
|
+
<div className={nested ? 'ml-5 flex items-center gap-1' : undefined}>
|
|
72
|
+
{nested && <span className="text-zinc-500 text-sm">↳</span>}
|
|
73
|
+
<button
|
|
74
|
+
key={wallet.id + wallet.address}
|
|
75
|
+
className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm w-full"
|
|
76
|
+
onClick={onClick}
|
|
77
|
+
data-active={isActive}
|
|
78
|
+
disabled={isActive || isConnecting}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
{isConnecting && isActive ? (
|
|
82
|
+
<p>Connecting...</p>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="flex justify-between items-center">
|
|
85
|
+
<p className="font-medium mr-2">
|
|
86
|
+
{`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
|
|
87
|
+
</p>
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
{wallet.accountType && (
|
|
90
|
+
<span className="text-[10px] font-semibold leading-none border border-zinc-600 rounded px-1 py-0.5 opacity-70">
|
|
91
|
+
{ACCOUNT_TYPE_LABELS[wallet.accountType]}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
<WalletRecoveryBadge wallet={wallet} />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
|
|
18
102
|
export const Wallets = () => {
|
|
19
103
|
const {
|
|
20
104
|
wallets,
|
|
21
|
-
|
|
105
|
+
status,
|
|
22
106
|
activeWallet,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
107
|
+
setActive,
|
|
108
|
+
exportPrivateKey,
|
|
109
|
+
} = useEthereumEmbeddedWallet()
|
|
110
|
+
const isLoadingWallets = status === 'fetching-wallets'
|
|
111
|
+
const isConnecting = status === 'connecting'
|
|
27
112
|
const { user, isAuthenticated } = useUser()
|
|
28
113
|
const { isConnected } = useAccount()
|
|
29
114
|
const [createWalletSheetOpen, setCreateWalletSheetOpen] = useState(false)
|
|
30
|
-
const [walletToRecover, setWalletToRecover] =
|
|
31
|
-
null
|
|
32
|
-
)
|
|
115
|
+
const [walletToRecover, setWalletToRecover] =
|
|
116
|
+
useState<ConnectedEmbeddedEthereumWallet | null>(null)
|
|
117
|
+
const [showAllWallets, setShowAllWallets] = useState(false)
|
|
118
|
+
const [exportedKey, setExportedKey] = useState<string | null>(null)
|
|
119
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
120
|
+
const [exportError, setExportError] = useState<string | null>(null)
|
|
121
|
+
const [copied, setCopied] = useState(false)
|
|
33
122
|
const { signOut } = useSignOut()
|
|
34
123
|
|
|
124
|
+
// Group wallets: EOAs at top level, Smart/Delegated accounts nested under their owner
|
|
125
|
+
const { topLevel, childrenByOwner } = useMemo(() => {
|
|
126
|
+
const ownerAddresses = new Set(wallets.map((w) => w.address.toLowerCase()))
|
|
127
|
+
const childrenByOwner = new Map<string, ConnectedEmbeddedEthereumWallet[]>()
|
|
128
|
+
const topLevel: ConnectedEmbeddedEthereumWallet[] = []
|
|
129
|
+
|
|
130
|
+
for (const wallet of wallets) {
|
|
131
|
+
const owner = wallet.ownerAddress?.toLowerCase()
|
|
132
|
+
if (owner && ownerAddresses.has(owner) && owner !== wallet.address.toLowerCase()) {
|
|
133
|
+
const existing = childrenByOwner.get(owner) ?? []
|
|
134
|
+
existing.push(wallet)
|
|
135
|
+
childrenByOwner.set(owner, existing)
|
|
136
|
+
} else {
|
|
137
|
+
topLevel.push(wallet)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { topLevel, childrenByOwner }
|
|
142
|
+
}, [wallets])
|
|
143
|
+
|
|
144
|
+
const handleExportKey = async () => {
|
|
145
|
+
setIsExporting(true)
|
|
146
|
+
setExportError(null)
|
|
147
|
+
setExportedKey(null)
|
|
148
|
+
try {
|
|
149
|
+
const key = await exportPrivateKey()
|
|
150
|
+
setExportedKey(key)
|
|
151
|
+
} catch {
|
|
152
|
+
setExportError('Cannot export private key for this wallet.')
|
|
153
|
+
} finally {
|
|
154
|
+
setIsExporting(false)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const handleWalletClick = (wallet: ConnectedEmbeddedEthereumWallet) => {
|
|
159
|
+
const isActive = activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
|
|
160
|
+
if (isActive || isConnecting) return
|
|
161
|
+
if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
|
|
162
|
+
setWalletToRecover(wallet)
|
|
163
|
+
} else {
|
|
164
|
+
setActive({ address: wallet.address })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
35
168
|
if (!activeWallet && isConnecting) return <div>recovering ...</div>
|
|
36
169
|
if (isLoadingWallets || (!user && isAuthenticated)) {
|
|
37
170
|
return <div>Loading wallets...</div>
|
|
38
171
|
}
|
|
39
|
-
if (
|
|
172
|
+
if (wallets.length === 0) {
|
|
40
173
|
return (
|
|
41
174
|
<div className="flex gap-2 flex-col w-full">
|
|
42
175
|
<h1>Create a wallet</h1>
|
|
@@ -46,46 +179,9 @@ export const Wallets = () => {
|
|
|
46
179
|
)
|
|
47
180
|
}
|
|
48
181
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const method = wallet.recoveryMethod
|
|
53
|
-
|
|
54
|
-
switch (method) {
|
|
55
|
-
case RecoveryMethod.PASSWORD:
|
|
56
|
-
Icon = KeyIcon
|
|
57
|
-
text = 'Password'
|
|
58
|
-
break
|
|
59
|
-
case RecoveryMethod.AUTOMATIC:
|
|
60
|
-
Icon = LockClosedIcon
|
|
61
|
-
text = 'Automatic'
|
|
62
|
-
break
|
|
63
|
-
case RecoveryMethod.PASSKEY:
|
|
64
|
-
Icon = FingerPrintIcon
|
|
65
|
-
text = 'Passkey'
|
|
66
|
-
break
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className="flex items-center text-xs">
|
|
71
|
-
<span>{text}</span>
|
|
72
|
-
<Icon className="h-5 w-5 ml-2" />
|
|
73
|
-
</div>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const handleWalletClick = (wallet: UserWallet) => {
|
|
78
|
-
if (wallet.isActive || isConnecting) return
|
|
79
|
-
const method = wallet.recoveryMethod
|
|
80
|
-
if (method === RecoveryMethod.PASSWORD) {
|
|
81
|
-
setWalletToRecover(wallet)
|
|
82
|
-
} else {
|
|
83
|
-
setActiveWallet({
|
|
84
|
-
walletId: 'xyz.openfort',
|
|
85
|
-
address: wallet.address,
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
}
|
|
182
|
+
const visibleTopLevel = showAllWallets
|
|
183
|
+
? topLevel
|
|
184
|
+
: topLevel.slice(0, VISIBLE_WALLET_COUNT)
|
|
89
185
|
|
|
90
186
|
return (
|
|
91
187
|
<div className="flex flex-col w-full">
|
|
@@ -96,34 +192,101 @@ export const Wallets = () => {
|
|
|
96
192
|
<div className="space-y-4 pb-4">
|
|
97
193
|
<h2>Your Wallets</h2>
|
|
98
194
|
<div className="flex flex-col space-y-2">
|
|
99
|
-
{
|
|
195
|
+
{visibleTopLevel.map((wallet) => {
|
|
196
|
+
const isActive =
|
|
197
|
+
activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
|
|
198
|
+
const children = childrenByOwner.get(wallet.address.toLowerCase())
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div key={wallet.id} className="space-y-1">
|
|
202
|
+
<WalletItem
|
|
203
|
+
wallet={wallet}
|
|
204
|
+
isActive={isActive}
|
|
205
|
+
isConnecting={isConnecting}
|
|
206
|
+
onClick={() => handleWalletClick(wallet)}
|
|
207
|
+
/>
|
|
208
|
+
{children?.map((child) => {
|
|
209
|
+
const isChildActive =
|
|
210
|
+
activeWallet?.address.toLowerCase() === child.address.toLowerCase()
|
|
211
|
+
return (
|
|
212
|
+
<WalletItem
|
|
213
|
+
key={child.id}
|
|
214
|
+
wallet={child}
|
|
215
|
+
isActive={isChildActive}
|
|
216
|
+
isConnecting={isConnecting}
|
|
217
|
+
nested
|
|
218
|
+
onClick={() => handleWalletClick(child)}
|
|
219
|
+
/>
|
|
220
|
+
)
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
})}
|
|
225
|
+
|
|
226
|
+
{topLevel.length > VISIBLE_WALLET_COUNT && (
|
|
100
227
|
<button
|
|
101
|
-
|
|
102
|
-
className="
|
|
103
|
-
onClick={() =>
|
|
104
|
-
data-active={wallet.isActive}
|
|
105
|
-
disabled={wallet.isActive || isConnecting}
|
|
228
|
+
type="button"
|
|
229
|
+
className="flex items-center justify-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 cursor-pointer transition-colors py-1"
|
|
230
|
+
onClick={() => setShowAllWallets((prev) => !prev)}
|
|
106
231
|
>
|
|
107
|
-
{
|
|
108
|
-
|
|
232
|
+
{showAllWallets ? (
|
|
233
|
+
<>
|
|
234
|
+
Show less <ChevronUpIcon className="h-4 w-4" />
|
|
235
|
+
</>
|
|
109
236
|
) : (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{renderWalletRecovery(wallet)}
|
|
115
|
-
</div>
|
|
237
|
+
<>
|
|
238
|
+
Show {topLevel.length - VISIBLE_WALLET_COUNT} more{' '}
|
|
239
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
240
|
+
</>
|
|
116
241
|
)}
|
|
117
242
|
</button>
|
|
118
|
-
)
|
|
243
|
+
)}
|
|
119
244
|
|
|
120
245
|
<button
|
|
121
246
|
className="p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1"
|
|
122
247
|
onClick={() => setCreateWalletSheetOpen(true)}
|
|
248
|
+
type="button"
|
|
123
249
|
>
|
|
124
250
|
+ Create Wallet
|
|
125
251
|
</button>
|
|
126
252
|
</div>
|
|
253
|
+
|
|
254
|
+
{activeWallet && (
|
|
255
|
+
<div className="mt-4 space-y-2">
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className="w-full p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors text-sm flex items-center justify-center gap-2"
|
|
259
|
+
onClick={handleExportKey}
|
|
260
|
+
disabled={isExporting}
|
|
261
|
+
>
|
|
262
|
+
<KeyIcon className="h-4 w-4" />
|
|
263
|
+
{isExporting ? 'Exporting...' : 'Export Private Key'}
|
|
264
|
+
</button>
|
|
265
|
+
{exportError && (
|
|
266
|
+
<p className="text-red-400 text-xs">{exportError}</p>
|
|
267
|
+
)}
|
|
268
|
+
{exportedKey && (
|
|
269
|
+
<div className="flex items-center gap-2 p-3 border border-zinc-700 rounded bg-zinc-800 text-xs break-all font-mono">
|
|
270
|
+
<span className="flex-1">{exportedKey}</span>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className="shrink-0 p-1 rounded hover:bg-zinc-700 transition-colors cursor-pointer"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
navigator.clipboard.writeText(exportedKey)
|
|
276
|
+
setCopied(true)
|
|
277
|
+
setTimeout(() => setCopied(false), 2000)
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{copied ? (
|
|
281
|
+
<CheckIcon className="h-4 w-4 text-green-400" />
|
|
282
|
+
) : (
|
|
283
|
+
<ClipboardDocumentIcon className="h-4 w-4 text-zinc-400" />
|
|
284
|
+
)}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
127
290
|
</div>
|
|
128
291
|
<WalletRecoverPasswordSheet
|
|
129
292
|
wallet={walletToRecover}
|