Skip to content

Commit 9f6d892

Browse files
committed
feat: initial implementation of solana auth
1 parent 135db06 commit 9f6d892

29 files changed

+1374
-0
lines changed

.react-router/types/+register.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type Params = {
4444
"/onboarding/done": {};
4545
"/dashboard": {};
4646
"/profile": {};
47+
"/solana": {};
4748
"/login": {};
4849
"/logout": {};
4950
"/about": {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// React Router generated types for route:
2+
// ./features/solana/user-solana-layout.tsx
3+
4+
import type * as T from "react-router/route-module"
5+
6+
import type { Info as Parent0 } from "../../../+types/root.js"
7+
import type { Info as Parent1 } from "../../app/+types/layout-app.js"
8+
9+
type Module = typeof import("../user-solana-layout.js")
10+
11+
export type Info = {
12+
parents: [Parent0, Parent1],
13+
id: "features/solana/user-solana-layout"
14+
file: "./features/solana/user-solana-layout.tsx"
15+
path: "undefined"
16+
params: {} & { [key: string]: string | undefined }
17+
module: Module
18+
loaderData: T.CreateLoaderData<Module>
19+
actionData: T.CreateActionData<Module>
20+
}
21+
22+
export namespace Route {
23+
export type LinkDescriptors = T.LinkDescriptors
24+
export type LinksFunction = () => LinkDescriptors
25+
26+
export type MetaArgs = T.CreateMetaArgs<Info>
27+
export type MetaDescriptors = T.MetaDescriptors
28+
export type MetaFunction = (args: MetaArgs) => MetaDescriptors
29+
30+
export type HeadersArgs = T.HeadersArgs
31+
export type HeadersFunction = (args: HeadersArgs) => Headers | HeadersInit
32+
33+
export type unstable_MiddlewareFunction = T.CreateServerMiddlewareFunction<Info>
34+
export type unstable_ClientMiddlewareFunction = T.CreateClientMiddlewareFunction<Info>
35+
export type LoaderArgs = T.CreateServerLoaderArgs<Info>
36+
export type ClientLoaderArgs = T.CreateClientLoaderArgs<Info>
37+
export type ActionArgs = T.CreateServerActionArgs<Info>
38+
export type ClientActionArgs = T.CreateClientActionArgs<Info>
39+
40+
export type HydrateFallbackProps = T.CreateHydrateFallbackProps<Info>
41+
export type ComponentProps = T.CreateComponentProps<Info>
42+
export type ErrorBoundaryProps = T.CreateErrorBoundaryProps<Info>
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// React Router generated types for route:
2+
// ./features/solana/user-solana-wallet.tsx
3+
4+
import type * as T from "react-router/route-module"
5+
6+
import type { Info as Parent0 } from "../../../+types/root.js"
7+
import type { Info as Parent1 } from "../../app/+types/layout-app.js"
8+
import type { Info as Parent2 } from "./user-solana-layout.js"
9+
10+
type Module = typeof import("../user-solana-wallet.js")
11+
12+
export type Info = {
13+
parents: [Parent0, Parent1, Parent2],
14+
id: "features/solana/user-solana-wallet"
15+
file: "./features/solana/user-solana-wallet.tsx"
16+
path: "solana"
17+
params: {} & { [key: string]: string | undefined }
18+
module: Module
19+
loaderData: T.CreateLoaderData<Module>
20+
actionData: T.CreateActionData<Module>
21+
}
22+
23+
export namespace Route {
24+
export type LinkDescriptors = T.LinkDescriptors
25+
export type LinksFunction = () => LinkDescriptors
26+
27+
export type MetaArgs = T.CreateMetaArgs<Info>
28+
export type MetaDescriptors = T.MetaDescriptors
29+
export type MetaFunction = (args: MetaArgs) => MetaDescriptors
30+
31+
export type HeadersArgs = T.HeadersArgs
32+
export type HeadersFunction = (args: HeadersArgs) => Headers | HeadersInit
33+
34+
export type unstable_MiddlewareFunction = T.CreateServerMiddlewareFunction<Info>
35+
export type unstable_ClientMiddlewareFunction = T.CreateClientMiddlewareFunction<Info>
36+
export type LoaderArgs = T.CreateServerLoaderArgs<Info>
37+
export type ClientLoaderArgs = T.CreateClientLoaderArgs<Info>
38+
export type ActionArgs = T.CreateServerActionArgs<Info>
39+
export type ClientActionArgs = T.CreateClientActionArgs<Info>
40+
41+
export type HydrateFallbackProps = T.CreateHydrateFallbackProps<Info>
42+
export type ComponentProps = T.CreateComponentProps<Info>
43+
export type ErrorBoundaryProps = T.CreateErrorBoundaryProps<Info>
44+
}

app/features/app/layout-app.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function LayoutApp({ loaderData: { user } }: Route.ComponentProps
2222
const links: UiHeaderLink[] = [
2323
{ label: 'Dashboard', to: '/dashboard' },
2424
{ label: 'Profile', to: '/profile' },
25+
{ label: 'Solana', to: '/solana' },
2526
]
2627
if (user.admin) {
2728
links.push({ label: 'Admin', to: '/admin' })

app/features/solana/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { index, layout, prefix } from '@react-router/dev/routes'
2+
3+
export const userSolanaRoutes = layout('./features/solana/user-solana-layout.tsx',
4+
prefix('solana', [
5+
index('./features/solana/user-solana-wallet.tsx'),
6+
])
7+
)
8+
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React, { Suspense } from 'react'
2+
import { Loader } from '@mantine/core'
3+
import { Outlet } from 'react-router'
4+
5+
export default function LayoutApp() {
6+
return (
7+
<Suspense fallback={<Loader />}>
8+
<Outlet />
9+
</Suspense>
10+
)
11+
}
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { Button, Flex, Group, Stack, Text } from '@mantine/core'
2+
import { useFetcher } from 'react-router'
3+
import { getBase58Decoder, getBase58Encoder, type ReadonlyUint8Array } from 'gill'
4+
import type { Route } from './+types/user-solana-wallet'
5+
import { useWallet } from '@solana/wallet-adapter-react'
6+
import { solanaAuth } from '~/lib/solana-auth/solana-auth'
7+
import type { SolanaAuthMessage, SolanaAuthMessageSigned } from '~/lib/solana-auth/solana-auth-message'
8+
9+
function parsePayload(payload: string = ''): SolanaAuthMessageSigned {
10+
try {
11+
return JSON.parse(payload)
12+
} catch {
13+
throw new Error(`Invalid payload`)
14+
}
15+
}
16+
17+
export async function action({ request }: Route.LoaderArgs) {
18+
const formData = await request.formData()
19+
const action = formData.get('action')?.toString()
20+
const payload = formData.get('payload')?.toString()
21+
const publicKey = formData.get('publicKey')?.toString()
22+
23+
if (!publicKey) {
24+
return { success: false, message: `No public key` }
25+
}
26+
27+
console.log(`user-solana-wallet -> action`, action, 'publicKey', publicKey)
28+
29+
switch (formData.get('action')) {
30+
case 'sign-message-create':
31+
return {
32+
success: true,
33+
message: await solanaAuth.createMessage({ method: 'solana:signMessage', publicKey }),
34+
type: 'solana-auth-message',
35+
}
36+
case 'sign-message-verify':
37+
const { message, signature, blockhash, nonce } = parsePayload(payload)
38+
console.log(`sign message -> verify`, 'message', message, 'signature', signature, 'blockhash', blockhash)
39+
const result = await solanaAuth.verifyMessage({
40+
method: 'solana:signMessage',
41+
publicKey,
42+
message,
43+
signature,
44+
blockhash,
45+
nonce,
46+
})
47+
return {
48+
success: true,
49+
message: result,
50+
type: 'solana-auth-result',
51+
}
52+
case 'sign-transaction-create':
53+
return {
54+
success: true,
55+
message: await solanaAuth.createMessage({ method: 'solana:signTransaction', publicKey }),
56+
type: 'solana-auth-message',
57+
}
58+
default:
59+
return {
60+
success: false,
61+
message: `Unknown action ${action}`,
62+
}
63+
}
64+
}
65+
66+
export default function UserSolanaWallet() {
67+
const wallet = useWallet()
68+
const publicKey = wallet.publicKey?.toString() ?? ''
69+
const fetcher = useFetcher()
70+
71+
async function handleSubmit(data: Record<string, string>) {
72+
return await fetcher.submit(data, { method: 'post' })
73+
}
74+
75+
async function handleCreate(action: 'sign-message-create' | 'sign-transaction-create') {
76+
if (!publicKey.length) {
77+
console.warn(`No public key, please connect your wallet`)
78+
return
79+
}
80+
await handleSubmit({ action, publicKey })
81+
}
82+
83+
async function handleVerify(
84+
action: 'sign-message-verify' | 'sign-transaction-verify',
85+
payload: SolanaAuthMessageSigned,
86+
) {
87+
if (!publicKey.length) {
88+
console.warn(`No public key, please connect your wallet`)
89+
return
90+
}
91+
await handleSubmit({ action, publicKey, payload: JSON.stringify(payload) })
92+
}
93+
94+
return (
95+
<Flex direction="column" align="center" justify="center" h="100%" style={{ border: '2px dotted hotpink' }}>
96+
<Stack align="center" gap="xl">
97+
{publicKey.length ? (
98+
<Stack align="center">
99+
<Text size="xl">Connected to</Text>
100+
<Text size="xs" ff="monospace">
101+
{publicKey}
102+
</Text>
103+
</Stack>
104+
) : null}
105+
<Group>
106+
<Button
107+
disabled={!publicKey}
108+
loading={fetcher.state === 'submitting'}
109+
onClick={() => handleCreate('sign-message-create')}
110+
>
111+
Verify by signing a message
112+
</Button>
113+
<Button
114+
disabled={!publicKey}
115+
loading={fetcher.state === 'submitting'}
116+
onClick={() => handleCreate('sign-transaction-create')}
117+
>
118+
Verify by signing a transaction
119+
</Button>
120+
</Group>
121+
{fetcher.data?.type === 'solana-auth-message' ? (
122+
<div>
123+
<SignComponent
124+
message={fetcher.data.message}
125+
sign={(payload) => handleVerify('sign-message-verify', payload)}
126+
/>
127+
<pre>{JSON.stringify(fetcher.data.message, null, 2)}</pre>
128+
</div>
129+
) : null}
130+
</Stack>
131+
</Flex>
132+
)
133+
}
134+
135+
function SignComponent({
136+
message,
137+
sign,
138+
}: {
139+
message: SolanaAuthMessage
140+
sign: (payload: SolanaAuthMessageSigned) => Promise<void>
141+
}) {
142+
const { signMessage } = useWallet()
143+
144+
return (
145+
<div>
146+
{signMessage ? (
147+
<Button
148+
onClick={async () => {
149+
const result = await createSignatureWallet({
150+
message,
151+
signMessage,
152+
})
153+
return await sign(result)
154+
}}
155+
>
156+
Sign
157+
</Button>
158+
) : (
159+
<div>No wallet</div>
160+
)}
161+
</div>
162+
)
163+
}
164+
165+
export interface CreateSignatureWallet {
166+
message: SolanaAuthMessage
167+
signMessage: (message: Uint8Array) => Promise<Uint8Array>
168+
}
169+
170+
export function bs58Encode(data: Uint8Array) {
171+
return getBase58Decoder().decode(data)
172+
}
173+
174+
export function bs58Decode(data: string): ReadonlyUint8Array {
175+
return getBase58Encoder().encode(data)
176+
}
177+
178+
export async function createSignatureWallet({
179+
message,
180+
signMessage,
181+
}: CreateSignatureWallet): Promise<SolanaAuthMessageSigned> {
182+
const encoded = encodeMessage(message.message.text)
183+
const signature = await signMessage(encoded)
184+
185+
return { ...message, signature: bs58Encode(signature) }
186+
}
187+
188+
export function encodeMessage(message: string): Uint8Array {
189+
return new TextEncoder().encode(message)
190+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { SolanaAuthConfig } from './solana-auth-config'
2+
import type { SolanaAuthInstance } from './solana-auth-instance'
3+
import type { SolanaAuthMessageCreateOptions, SolanaAuthMessageSigned } from './solana-auth-message'
4+
import { solanaAuthMethodCreate } from './solana-auth-method-create'
5+
import { solanaAuthMethodVerify } from './solana-auth-method-verify'
6+
7+
export function createSolanaAuth(config: SolanaAuthConfig): SolanaAuthInstance {
8+
return {
9+
createMessage: async (options: SolanaAuthMessageCreateOptions) => {
10+
return await solanaAuthMethodCreate(config.client, options)
11+
},
12+
verifyMessage: async (options: SolanaAuthMessageSigned) => {
13+
return await solanaAuthMethodVerify(config.client, options)
14+
},
15+
}
16+
}

0 commit comments

Comments
 (0)