@moon-x/react-sdk 0.1.0

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/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @moon-x/react-sdk
2
+
3
+ React SDK for [MoonX](https://moonx.com) — embedded wallets with passkey-protected MPC signing for Ethereum and Solana, drop-in auth UI flows, and headless wallet methods.
4
+
5
+ ```sh
6
+ npm install @moon-x/react-sdk
7
+ # or
8
+ pnpm add @moon-x/react-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ Wrap your app with `MoonKeyProvider` and import the CSS bundle once:
14
+
15
+ ```tsx
16
+ import { MoonKeyProvider, mainnet, sepolia } from "@moon-x/react-sdk";
17
+ import "@moon-x/react-sdk/style.css";
18
+
19
+ export default function App() {
20
+ return (
21
+ <MoonKeyProvider
22
+ publishableKey={process.env.NEXT_PUBLIC_MOONKEY_PUBLISHABLE_KEY!}
23
+ config={{
24
+ appearance: {
25
+ accentColor: "#6366f1",
26
+ backgroundColor: "#ffffff",
27
+ },
28
+ defaultChain: mainnet,
29
+ supportedChains: [mainnet, sepolia],
30
+ }}
31
+ >
32
+ <YourApp />
33
+ </MoonKeyProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ Open the auth modal and read user state:
39
+
40
+ ```tsx
41
+ import { useMoonKey } from "@moon-x/react-sdk";
42
+
43
+ function LoginButton() {
44
+ const { ready, isAuthenticated, user, start, logout } = useMoonKey();
45
+
46
+ if (!ready) return null;
47
+ if (isAuthenticated) {
48
+ return <button onClick={() => logout()}>Sign out ({user?.id})</button>;
49
+ }
50
+ return <button onClick={() => start?.()}>Sign in</button>;
51
+ }
52
+ ```
53
+
54
+ Sign a message with one of the user's wallets:
55
+
56
+ ```tsx
57
+ import { useWallets } from "@moon-x/react-sdk";
58
+ import { useSignMessage } from "@moon-x/react-sdk/ethereum";
59
+
60
+ function SignDemo() {
61
+ const { wallets } = useWallets();
62
+ const { signMessage } = useSignMessage();
63
+
64
+ const onSign = async () => {
65
+ const wallet = wallets.find((w) => w.wallet_type === "ethereum");
66
+ if (!wallet) return;
67
+ const { signature } = await signMessage({
68
+ message: "Hello world",
69
+ wallet,
70
+ options: { uiOptions: { showWalletUI: true } }, // opens the modal
71
+ });
72
+ console.log(signature);
73
+ };
74
+
75
+ return <button onClick={onSign}>Sign</button>;
76
+ }
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ `MoonKeyProvider` accepts `publishableKey` (required) plus a `config` object:
82
+
83
+ | Field | Type | Purpose |
84
+ |---|---|---|
85
+ | `appearance` | `AuthAppearance` | High-level theming — `accentColor`, `backgroundColor`, `displayMode` (`"light" \| "dark" \| "auto"`), `logo`, `loginHeaderTitle`, `borderRadius`, `fontFamily`. |
86
+ | `loginMethods` | `("email" \| "google" \| "apple" \| "wallet")[]` | Which auth methods to show in the modal. |
87
+ | `walletChainType` | `"ethereum" \| "solana" \| "ethereum-or-solana"` | Which wallet type to create at signup. |
88
+ | `defaultChain` / `supportedChains` | `Chain` / `Chain[]` | EVM chain config — top-level, **not nested under `ethereum`**. `Chain` is viem's type; common chains are re-exported by this package. |
89
+ | `solana.rpcs` | `{ [cluster]: { rpc, rpcSubscriptions? } }` | Solana RPC endpoints per cluster. |
90
+ | `walletConnect.projectId` | `string` | WalletConnect v2 project ID for external-wallet flows. |
91
+ | `emailConfig`, `passkeyEnrollConfig`, `signMessageConfig`, `signTransactionConfig`, `sendTransactionConfig`, `exportKeyConfig` | various | Per-flow UI overrides — titles, button text, etc. |
92
+ | `security.assertionCacheTtlMs` | `number` | Parent-side passkey-assertion cache TTL in milliseconds. **Defaults to `0` (no cache — every sensitive op forces a fresh biometric).** Set to a positive value (e.g. `5 * 60 * 1000`) to opt into a warm window where routine ops skip the OS picker. See [Security: assertion cache + `requireFreshAssertion`](#security-assertion-cache--requirefreshassertion) below for the trade-off. |
93
+ | `theme` | `MoonKeyThemeConfig` | Low-level token overrides — fine-grained color palette, full borderRadius scale. Most apps don't need this; `appearance` is the recommended surface. |
94
+
95
+ ## Hooks
96
+
97
+ ### Authentication & user state
98
+
99
+ | Hook | What it does |
100
+ |---|---|
101
+ | `useMoonKey()` | The big one. `{ ready, isAuthenticated, user, start, logout, setAppearance, getSessionTokens, refreshUser, ... }` + every SDK method on the same instance. |
102
+ | `useUser()` | Just `{ user, refreshUser }`. Re-subscribes to user changes. |
103
+ | `useLoginWithEmail({ onComplete?, onError? })` | Headless email-OTP. Returns `{ state, sendCode, loginWithCode, reset }`. `state` is a discriminated union: `idle` / `sending` / `awaiting-code` / `verifying` / `complete` / `error`. |
104
+ | `useLoginWithOAuth()` | Google + Apple flows. Returns `{ state, loginWithOAuth, reset }`. |
105
+ | `useLogout()` | `{ logout }` — also clears local storage + iframe session. |
106
+
107
+ ### Passkeys
108
+
109
+ | Hook | What it does |
110
+ |---|---|
111
+ | `usePasskeyStatus()` | `{ status, refresh }`. `status.passkeys` lists the user's enrolled passkeys with provider labels (e.g. `"1Password on Chrome"`). |
112
+ | `useRegisterPasskey()` | First-time passkey enrollment for a user who signed in via OTP/OAuth without one. |
113
+ | `useAddPasskey()` | Add an additional passkey to an authenticated user. |
114
+ | `useRemovePasskey()` | Remove a passkey by its credential ID. |
115
+
116
+ ### Wallets
117
+
118
+ | Hook | What it does |
119
+ |---|---|
120
+ | `useWallets()` | `{ wallets, loading }` — both Ethereum and Solana, fetched once on mount. |
121
+ | `useCreateWallet()` | Mint a new MPC wallet. Pass `{ walletType: "ethereum" \| "solana" }`. |
122
+ | `useImportKey()` | Two-mode: headless if you pass a key, modal-driven if you don't. |
123
+ | `useConnectWallet()` | External-wallet flow (MetaMask, Phantom, WalletConnect) for both chains. |
124
+ | `useAttachOAuth()` / `useDetachOAuth()` | Link / unlink an OAuth provider on an existing user. |
125
+
126
+ ### Per-chain signing — `/ethereum` and `/solana` subpaths
127
+
128
+ Chain-aware hooks live under subpaths so they don't pull in the other chain's adapters if you only use one:
129
+
130
+ ```tsx
131
+ import { useSignMessage, useSignTransaction, useSendTransaction } from "@moon-x/react-sdk/ethereum";
132
+ import { useSignMessage as useSignSolanaMessage } from "@moon-x/react-sdk/solana";
133
+ ```
134
+
135
+ **Ethereum hooks:**
136
+ - `useSignMessage` — EIP-191 `personal_sign`. Returns `{ signature: "0x..." }`.
137
+ - `useSignTransaction` — Signs an EIP-1559 tx. Returns `{ signature, serializedSigned, hash }`.
138
+ - `useSignTypedData` — EIP-712. Returns `{ signature: "0x..." }`.
139
+ - `useSignHash` — Raw ECDSA digest sign (Privy parity — `secp256k1_sign`).
140
+ - `useSign7702Authorization` — EIP-7702 delegation auth.
141
+ - `useSendTransaction` — Sign + broadcast via the configured RPC.
142
+ - `useGetBalance` — Native token balance.
143
+
144
+ **Solana hooks:**
145
+ - `useSignMessage` — Ed25519 signature.
146
+ - `useSignTransaction` — Signs a serialized base58 tx. Returns `{ signedTransaction: Uint8Array }`.
147
+ - `useSendTransaction` — Sign + broadcast.
148
+ - `useGetBalance` — Lamport balance.
149
+
150
+ Every signing hook accepts `options.uiOptions.showWalletUI` to toggle modal vs headless mode. With UI, the user sees the message/transaction in a modal and the biometric prompt fires only on the Sign tap. Headless skips the modal entirely.
151
+
152
+ Every signing hook also accepts a top-level `requireFreshAssertion?: boolean` flag. When `true`, that single call bypasses any parent-side assertion cache and forces a fresh biometric — useful for high-value transactions in an otherwise-warm flow. See [Security](#security-assertion-cache--requirefreshassertion) below.
153
+
154
+ ### Security: assertion cache + `requireFreshAssertion`
155
+
156
+ MoonX's parent-side assertion cache holds the WebAuthn assertion result (`userHandleB64url` — the AES-GCM key that wraps the user's DEK) in JS memory for an integrator-configured TTL. Within that window, routine wallet ops skip the OS passkey picker.
157
+
158
+ **The default is `assertionCacheTtlMs: 0` — no cache.** Every sensitive op fires a fresh biometric and the raw userHandle never lingers in the parent JS heap past the moment of use. This is the safe posture: a parent-page XSS can't lift a cached key because there is no cache.
159
+
160
+ Integrators who want a warm-UX trade-off opt back in:
161
+
162
+ ```tsx
163
+ <MoonKeyProvider
164
+ publishableKey={pk}
165
+ config={{ security: { assertionCacheTtlMs: 5 * 60 * 1000 } }}
166
+ >
167
+ ...
168
+ </MoonKeyProvider>
169
+ ```
170
+
171
+ With the cache on, parent-coresident JS (including XSS) can drive cached ops without re-prompting for the duration of the window. The TTL bounds the exposure but doesn't eliminate it. Pick the trade-off deliberately.
172
+
173
+ Per-call escape hatch — works regardless of the global setting:
174
+
175
+ ```tsx
176
+ await signTransaction({
177
+ transaction: tx,
178
+ wallet,
179
+ requireFreshAssertion: true, // force a fresh biometric for THIS op
180
+ });
181
+ ```
182
+
183
+ Sensitive lifecycle ops (`exportKey`, `addPasskey`, `removePasskey`) ignore the cache unconditionally — they always re-prompt.
184
+
185
+ ### Theming
186
+
187
+ The simplest path is `appearance`:
188
+
189
+ ```tsx
190
+ <MoonKeyProvider
191
+ config={{
192
+ appearance: {
193
+ accentColor: "#6366f1",
194
+ backgroundColor: "#0f172a",
195
+ displayMode: "dark",
196
+ borderRadius: "md", // "none" | "sm" | "md" | "lg" — or { sm, md, lg }
197
+ fontFamily: "Inter, sans-serif",
198
+ logo: "https://example.com/logo.svg",
199
+ },
200
+ }}
201
+ >
202
+ ```
203
+
204
+ You can dynamically update appearance at runtime — useful for light/dark mode toggles:
205
+
206
+ ```tsx
207
+ const { setAppearance } = useMoonKey();
208
+ setAppearance({ displayMode: colorScheme });
209
+ ```
210
+
211
+ For finer control (overriding derived tokens like `accentDark`, `background2`, etc.), pass a `theme: MoonKeyThemeConfig`. The merge order is: base theme → `appearance` derivations → explicit `theme` overrides.
212
+
213
+ ## EVM chain helpers
214
+
215
+ The package re-exports gas / RPC utilities that work with any viem `Chain`:
216
+
217
+ ```ts
218
+ import {
219
+ getEvmGasPrice,
220
+ getEvmMaxPriorityFeePerGas,
221
+ getEvmNonce,
222
+ estimateEvmGas,
223
+ estimateEvmGasReserve,
224
+ getRpcUrl,
225
+ setChainRpcUrl,
226
+ getChainById,
227
+ getDefaultChain,
228
+ isChainSupported,
229
+ validateChainConfig,
230
+ } from "@moon-x/react-sdk";
231
+ ```
232
+
233
+ Useful when you need a pre-flight gas reserve estimate, want to override an RPC URL at runtime, or are building a custom chain-picker.
234
+
235
+ ## React Native
236
+
237
+ For React Native apps, use [`@moon-x/react-native-sdk`](https://www.npmjs.com/package/@moon-x/react-native-sdk) — same hook shape with a WebView-backed transport.
238
+
239
+ ## License
240
+
241
+ MIT.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,119 @@
1
+ import { PublicWallet } from '@moon-x/core/types';
2
+
3
+ interface BaseWallet {
4
+ id: string;
5
+ type: "solana" | "ethereum";
6
+ address: string;
7
+ isConnected: boolean;
8
+ }
9
+ interface BaseUIOptions {
10
+ title?: string;
11
+ description?: string;
12
+ buttonText?: string;
13
+ showWalletUI?: boolean;
14
+ }
15
+ interface BaseSignMessageParams {
16
+ message: string;
17
+ wallet: BaseWallet;
18
+ options?: {
19
+ uiOptions?: BaseUIOptions;
20
+ };
21
+ }
22
+ interface BaseSignMessageResult {
23
+ signature: Uint8Array;
24
+ signedMessage?: Uint8Array;
25
+ }
26
+ interface BaseSignTransactionParams {
27
+ transaction: Uint8Array | string;
28
+ wallet: BaseWallet;
29
+ options?: {
30
+ uiOptions?: BaseUIOptions;
31
+ };
32
+ }
33
+ interface BaseSignTransactionResult {
34
+ signature: Uint8Array;
35
+ signedTransaction: Uint8Array | string;
36
+ }
37
+ interface BaseSendTransactionParams {
38
+ transaction: Uint8Array | string;
39
+ wallet: BaseWallet;
40
+ rpcUrl: string;
41
+ options?: {
42
+ uiOptions?: BaseUIOptions;
43
+ commitment?: string;
44
+ encoding?: string;
45
+ replaceRecentBlockhash?: boolean;
46
+ sigVerify?: boolean;
47
+ minContextSlot?: number;
48
+ innerInstructions?: boolean;
49
+ accounts?: any[];
50
+ network?: string;
51
+ };
52
+ }
53
+ interface BaseSendTransactionResult {
54
+ signature: Uint8Array;
55
+ transactionHash: string;
56
+ walletAddress: string;
57
+ }
58
+ interface BaseGetBalanceParams {
59
+ wallet: BaseWallet;
60
+ }
61
+ interface BaseGetBalanceResult {
62
+ balance: string;
63
+ balanceInSOL?: string;
64
+ balanceInETH?: string;
65
+ walletAddress: string;
66
+ }
67
+ interface BaseGetTokenAccountsParams {
68
+ wallet: PublicWallet;
69
+ rpcUrl?: string;
70
+ options?: any;
71
+ }
72
+ interface BaseWalletHooks {
73
+ useCreateWallet: (callbacks?: {
74
+ onSuccess?: (result: {
75
+ wallet: PublicWallet;
76
+ }) => void;
77
+ onError?: (error: Error) => void;
78
+ }) => {
79
+ createWallet: (options?: {
80
+ createAdditional?: boolean;
81
+ walletIndex?: number;
82
+ }) => Promise<{
83
+ wallet: PublicWallet;
84
+ }>;
85
+ };
86
+ useWallets: () => {
87
+ wallets: BaseWallet[];
88
+ loading: boolean;
89
+ error?: Error;
90
+ refetch: () => void;
91
+ };
92
+ useSignMessage: () => {
93
+ signMessage: (params: BaseSignMessageParams) => Promise<BaseSignMessageResult>;
94
+ loading: boolean;
95
+ error?: Error;
96
+ };
97
+ useSignTransaction: () => {
98
+ signTransaction: (params: BaseSignTransactionParams) => Promise<BaseSignTransactionResult>;
99
+ loading: boolean;
100
+ error?: Error;
101
+ };
102
+ useSendTransaction: () => {
103
+ sendTransaction: (params: BaseSendTransactionParams) => Promise<BaseSendTransactionResult>;
104
+ loading: boolean;
105
+ error?: Error;
106
+ };
107
+ useGetBalance: () => {
108
+ getBalance: (params: BaseGetBalanceParams) => Promise<BaseGetBalanceResult>;
109
+ loading: boolean;
110
+ error?: Error;
111
+ };
112
+ useGetTokenAccounts: () => {
113
+ getTokenAccounts: (params: BaseGetTokenAccountsParams) => Promise<any>;
114
+ loading: boolean;
115
+ error?: Error;
116
+ };
117
+ }
118
+
119
+ export type { BaseWalletHooks as B };
@@ -0,0 +1,119 @@
1
+ import { PublicWallet } from '@moon-x/core/types';
2
+
3
+ interface BaseWallet {
4
+ id: string;
5
+ type: "solana" | "ethereum";
6
+ address: string;
7
+ isConnected: boolean;
8
+ }
9
+ interface BaseUIOptions {
10
+ title?: string;
11
+ description?: string;
12
+ buttonText?: string;
13
+ showWalletUI?: boolean;
14
+ }
15
+ interface BaseSignMessageParams {
16
+ message: string;
17
+ wallet: BaseWallet;
18
+ options?: {
19
+ uiOptions?: BaseUIOptions;
20
+ };
21
+ }
22
+ interface BaseSignMessageResult {
23
+ signature: Uint8Array;
24
+ signedMessage?: Uint8Array;
25
+ }
26
+ interface BaseSignTransactionParams {
27
+ transaction: Uint8Array | string;
28
+ wallet: BaseWallet;
29
+ options?: {
30
+ uiOptions?: BaseUIOptions;
31
+ };
32
+ }
33
+ interface BaseSignTransactionResult {
34
+ signature: Uint8Array;
35
+ signedTransaction: Uint8Array | string;
36
+ }
37
+ interface BaseSendTransactionParams {
38
+ transaction: Uint8Array | string;
39
+ wallet: BaseWallet;
40
+ rpcUrl: string;
41
+ options?: {
42
+ uiOptions?: BaseUIOptions;
43
+ commitment?: string;
44
+ encoding?: string;
45
+ replaceRecentBlockhash?: boolean;
46
+ sigVerify?: boolean;
47
+ minContextSlot?: number;
48
+ innerInstructions?: boolean;
49
+ accounts?: any[];
50
+ network?: string;
51
+ };
52
+ }
53
+ interface BaseSendTransactionResult {
54
+ signature: Uint8Array;
55
+ transactionHash: string;
56
+ walletAddress: string;
57
+ }
58
+ interface BaseGetBalanceParams {
59
+ wallet: BaseWallet;
60
+ }
61
+ interface BaseGetBalanceResult {
62
+ balance: string;
63
+ balanceInSOL?: string;
64
+ balanceInETH?: string;
65
+ walletAddress: string;
66
+ }
67
+ interface BaseGetTokenAccountsParams {
68
+ wallet: PublicWallet;
69
+ rpcUrl?: string;
70
+ options?: any;
71
+ }
72
+ interface BaseWalletHooks {
73
+ useCreateWallet: (callbacks?: {
74
+ onSuccess?: (result: {
75
+ wallet: PublicWallet;
76
+ }) => void;
77
+ onError?: (error: Error) => void;
78
+ }) => {
79
+ createWallet: (options?: {
80
+ createAdditional?: boolean;
81
+ walletIndex?: number;
82
+ }) => Promise<{
83
+ wallet: PublicWallet;
84
+ }>;
85
+ };
86
+ useWallets: () => {
87
+ wallets: BaseWallet[];
88
+ loading: boolean;
89
+ error?: Error;
90
+ refetch: () => void;
91
+ };
92
+ useSignMessage: () => {
93
+ signMessage: (params: BaseSignMessageParams) => Promise<BaseSignMessageResult>;
94
+ loading: boolean;
95
+ error?: Error;
96
+ };
97
+ useSignTransaction: () => {
98
+ signTransaction: (params: BaseSignTransactionParams) => Promise<BaseSignTransactionResult>;
99
+ loading: boolean;
100
+ error?: Error;
101
+ };
102
+ useSendTransaction: () => {
103
+ sendTransaction: (params: BaseSendTransactionParams) => Promise<BaseSendTransactionResult>;
104
+ loading: boolean;
105
+ error?: Error;
106
+ };
107
+ useGetBalance: () => {
108
+ getBalance: (params: BaseGetBalanceParams) => Promise<BaseGetBalanceResult>;
109
+ loading: boolean;
110
+ error?: Error;
111
+ };
112
+ useGetTokenAccounts: () => {
113
+ getTokenAccounts: (params: BaseGetTokenAccountsParams) => Promise<any>;
114
+ loading: boolean;
115
+ error?: Error;
116
+ };
117
+ }
118
+
119
+ export type { BaseWalletHooks as B };