@horus-wallet/sdk-react 0.1.0-beta.2
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 +398 -0
- package/dist/index.cjs +682 -0
- package/dist/index.d.cts +314 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +648 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# @horus-wallet/sdk-react
|
|
2
|
+
|
|
3
|
+
React provider + hooks + drop-in components for the Horus embedded-wallet platform. Sign-in, wallets, signing, and on-chain transfers in **15 minutes**.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { HorusProvider, useHorusAuth, useWallets, HorusLoginButton } from '@horus-wallet/sdk-react';
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
return (
|
|
10
|
+
<HorusProvider appId="app_acme123">
|
|
11
|
+
<Page />
|
|
12
|
+
</HorusProvider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Page() {
|
|
17
|
+
const { authenticated, user, logout } = useHorusAuth();
|
|
18
|
+
const { wallets } = useWallets();
|
|
19
|
+
|
|
20
|
+
if (!authenticated) {
|
|
21
|
+
return <HorusLoginButton flow="google">Continue with Google</HorusLoginButton>;
|
|
22
|
+
}
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<p>Hi {user?.email}</p>
|
|
26
|
+
<p>EVM wallet: {wallets.find(w => w.network === 'EVM')?.publicKey}</p>
|
|
27
|
+
<button onClick={logout}>Sign out</button>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. Sign-in, token persistence, auto-refresh, and wallet fetching all happen inside the provider.
|
|
34
|
+
|
|
35
|
+
## What you get
|
|
36
|
+
|
|
37
|
+
- **Drop-in React DX** — one provider, a handful of hooks, no wallet-management code in your app
|
|
38
|
+
- **Built-in auth** — email + password, magic-link email, Google / Apple / phone via popup
|
|
39
|
+
- **Multi-chain wallets** — EVM, Bitcoin, ICP, Casper, Aeternity — same hook, pick `network`
|
|
40
|
+
- **Signing + transfers** — `signMessage`, native + ERC-20 transfers, all behind a hook
|
|
41
|
+
- **Token persistence** — sessions survive page reloads; cross-tab sync; auto-refresh near expiry
|
|
42
|
+
- **Drop-in components** — `<HorusLoginButton>` for one-click signin if you don't want to build the UI yourself
|
|
43
|
+
- **White-label friendly** — partner branding (logo, colors, terms links) flows through to the sign-in popup
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @horus-wallet/sdk-react
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Peer dependency: React 17+. Works in React 18, Next.js (App Router and Pages), Vite, Remix.
|
|
52
|
+
|
|
53
|
+
## Quick setup (3 steps)
|
|
54
|
+
|
|
55
|
+
### 1. Wrap your app in `<HorusProvider>`
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { HorusProvider } from '@horus-wallet/sdk-react';
|
|
59
|
+
|
|
60
|
+
export function App({ children }) {
|
|
61
|
+
return (
|
|
62
|
+
<HorusProvider appId="app_acme123">
|
|
63
|
+
{children}
|
|
64
|
+
</HorusProvider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You get `appId` from Horus during onboarding.
|
|
70
|
+
|
|
71
|
+
### 2. Mount a sign-in button
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { HorusLoginButton } from '@horus-wallet/sdk-react';
|
|
75
|
+
|
|
76
|
+
<HorusLoginButton flow="google">Continue with Google</HorusLoginButton>
|
|
77
|
+
<HorusLoginButton flow="email_link">Email me a link</HorusLoginButton>
|
|
78
|
+
<HorusLoginButton flow="phone">Continue with phone</HorusLoginButton>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or build your own form and call `useHorusAuth()` methods directly (see hooks below).
|
|
82
|
+
|
|
83
|
+
### 3. Add a server-side proxy
|
|
84
|
+
|
|
85
|
+
The React SDK calls **your backend** so your secret API key never reaches the browser:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
React app ──► Your backend (~50 lines) ──► api.horuswallet.com
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The proxy is a thin pass-through. Reference implementations for Express and Next.js are below.
|
|
92
|
+
|
|
93
|
+
## Provider config
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<HorusProvider
|
|
97
|
+
appId="app_acme123" // required — server-assigned during onboarding
|
|
98
|
+
apiBase="/api/horus" // where YOUR backend exposes the proxy. Default: /api/horus
|
|
99
|
+
|
|
100
|
+
branding={{ // optional — passed to the sign-in popup
|
|
101
|
+
logoUrl: 'https://acme.com/logo.png',
|
|
102
|
+
brandName: 'Acme',
|
|
103
|
+
primaryColor: '#FF5733',
|
|
104
|
+
showPoweredByHorus: false, // requires the white-label tier
|
|
105
|
+
}}
|
|
106
|
+
|
|
107
|
+
tokenStorage="localStorage" // 'localStorage' | 'sessionStorage' | 'memory'
|
|
108
|
+
autoRefresh={true} // refresh tokens before expiry; default true
|
|
109
|
+
>
|
|
110
|
+
<App />
|
|
111
|
+
</HorusProvider>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Hooks
|
|
115
|
+
|
|
116
|
+
### `useHorusAuth()` — sign-in, sign-up, sign-out
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
const {
|
|
120
|
+
ready, // true once the provider has hydrated from storage
|
|
121
|
+
authenticated, // true if user is signed in
|
|
122
|
+
user, // { uid, email, displayName, ... } or undefined
|
|
123
|
+
|
|
124
|
+
// Headless flows — your form, your UI
|
|
125
|
+
loginWithEmail, // ({ email, password }) => Promise<User>
|
|
126
|
+
signUpWithEmail, // ({ email, password }) => Promise<User>
|
|
127
|
+
sendEmailLink, // ({ email, continueUrl }) => Promise<void>
|
|
128
|
+
verifyEmailLink, // ({ email, oobCode }) => Promise<User>
|
|
129
|
+
|
|
130
|
+
// Popup flows — Horus-hosted UI for OAuth providers
|
|
131
|
+
loginWithGoogle, // () => Promise<User>
|
|
132
|
+
loginWithEmailLink, // popup variant of email-link
|
|
133
|
+
loginWithPhone, // ({ phone? }) => Promise<User>
|
|
134
|
+
|
|
135
|
+
logout, // clears tokens
|
|
136
|
+
refresh, // manual token refresh
|
|
137
|
+
} = useHorusAuth();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `useUser()` — read the current user
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const user = useUser();
|
|
144
|
+
// User | undefined — pure read, no side-effects
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `useWallets()` — the user's wallets
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
const { wallets, ready, refresh, error } = useWallets();
|
|
151
|
+
// wallets: [{ network: 'EVM', publicKey: '0x…' }, { network: 'BITCOIN', publicKey: 'bc1…' }, …]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `useSignMessage()` — sign arbitrary messages
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const { signMessage, pending, error } = useSignMessage();
|
|
158
|
+
|
|
159
|
+
const sig = await signMessage({
|
|
160
|
+
network: 'BASE',
|
|
161
|
+
networkType: 'MAINNET',
|
|
162
|
+
message: 'Sign in to Acme — nonce abc123',
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `useTransfer()` — native + token transfers
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
const { native, token, pending, error } = useTransfer();
|
|
170
|
+
|
|
171
|
+
// Native (ETH / MATIC / BNB / …)
|
|
172
|
+
const { txHash } = await native({
|
|
173
|
+
network: 'BASE',
|
|
174
|
+
networkType: 'MAINNET',
|
|
175
|
+
recipients: ['0x742d35Cc…'],
|
|
176
|
+
amount: '1000000000000000', // wei (string for values > 2^53)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ERC-20 / chain-equivalent token
|
|
180
|
+
const { txHash } = await token({
|
|
181
|
+
network: 'BASE',
|
|
182
|
+
networkType: 'MAINNET',
|
|
183
|
+
tokenAddress: '0xA0b86…', // USDC
|
|
184
|
+
recipient: '0x742d35Cc…',
|
|
185
|
+
amount: '5000000', // smallest unit (USDC has 6 decimals)
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Drop-in components
|
|
190
|
+
|
|
191
|
+
### `<HorusLoginButton>`
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<HorusLoginButton flow="google">Continue with Google</HorusLoginButton>
|
|
195
|
+
<HorusLoginButton flow="email_link">Email me a link</HorusLoginButton>
|
|
196
|
+
<HorusLoginButton flow="phone">Continue with phone</HorusLoginButton>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For total UI control, skip the component and call `useHorusAuth()` methods from your own form.
|
|
200
|
+
|
|
201
|
+
## Backend setup (the proxy)
|
|
202
|
+
|
|
203
|
+
A thin pass-through that forwards SDK calls to Horus, holding your secret key server-side. Roughly **50 lines**.
|
|
204
|
+
|
|
205
|
+
### Express / Connect
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// server/horus-proxy.ts
|
|
209
|
+
import express from 'express';
|
|
210
|
+
import { HorusClient } from '@horus-wallet/sdk';
|
|
211
|
+
|
|
212
|
+
export function horusProxy() {
|
|
213
|
+
const router = express.Router();
|
|
214
|
+
|
|
215
|
+
// Per-request client — picks up the user's auth token from the SDK header.
|
|
216
|
+
const clientFor = (req: express.Request) =>
|
|
217
|
+
new HorusClient({
|
|
218
|
+
baseUrl: process.env.HORUS_BASE_URL!,
|
|
219
|
+
apiKey: process.env.HORUS_API_KEY!, // hk_sk_*
|
|
220
|
+
getIdToken: async () => req.header('x-horus-id-token') ?? '',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Auth — unauthenticated; mints tokens on success
|
|
224
|
+
router.post('/auth/email/signup', async (req, res, next) => {
|
|
225
|
+
try { res.json(await clientFor(req).auth.signUpWithEmail(req.body)); }
|
|
226
|
+
catch (err) { next(err); }
|
|
227
|
+
});
|
|
228
|
+
router.post('/auth/email/signin', async (req, res, next) => {
|
|
229
|
+
try { res.json(await clientFor(req).auth.signInWithEmail(req.body)); }
|
|
230
|
+
catch (err) { next(err); }
|
|
231
|
+
});
|
|
232
|
+
router.post('/auth/email-link/send', async (req, res, next) => {
|
|
233
|
+
try { res.json(await clientFor(req).auth.sendEmailLink(req.body)); }
|
|
234
|
+
catch (err) { next(err); }
|
|
235
|
+
});
|
|
236
|
+
router.post('/auth/email-link/verify', async (req, res, next) => {
|
|
237
|
+
try { res.json(await clientFor(req).auth.verifyEmailLink(req.body)); }
|
|
238
|
+
catch (err) { next(err); }
|
|
239
|
+
});
|
|
240
|
+
router.post('/auth/oauth', async (req, res, next) => {
|
|
241
|
+
try { res.json(await clientFor(req).auth.signInWithOAuth(req.body)); }
|
|
242
|
+
catch (err) { next(err); }
|
|
243
|
+
});
|
|
244
|
+
router.post('/auth/refresh', async (req, res, next) => {
|
|
245
|
+
try { res.json(await clientFor(req).auth.refresh(req.body.refreshToken)); }
|
|
246
|
+
catch (err) { next(err); }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Wallet ops — authenticated; SDK forwards the user's token
|
|
250
|
+
router.get('/wallets', async (req, res, next) => {
|
|
251
|
+
try { res.json(await clientFor(req).wallets.get()); }
|
|
252
|
+
catch (err) { next(err); }
|
|
253
|
+
});
|
|
254
|
+
router.post('/sign-message', async (req, res, next) => {
|
|
255
|
+
try { res.json(await clientFor(req).wallets.signMessage(req.body)); }
|
|
256
|
+
catch (err) { next(err); }
|
|
257
|
+
});
|
|
258
|
+
router.post('/transfer/native', async (req, res, next) => {
|
|
259
|
+
try { res.json(await clientFor(req).transfers.native(req.body)); }
|
|
260
|
+
catch (err) { next(err); }
|
|
261
|
+
});
|
|
262
|
+
router.post('/transfer/token', async (req, res, next) => {
|
|
263
|
+
try { res.json(await clientFor(req).transfers.token(req.body)); }
|
|
264
|
+
catch (err) { next(err); }
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return router;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Mount it:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import express from 'express';
|
|
275
|
+
import { horusProxy } from './horus-proxy';
|
|
276
|
+
|
|
277
|
+
const app = express();
|
|
278
|
+
app.use(express.json());
|
|
279
|
+
app.use('/api/horus', horusProxy());
|
|
280
|
+
app.listen(3001);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
That's the whole backend integration.
|
|
284
|
+
|
|
285
|
+
### Next.js (App Router)
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// app/providers.tsx
|
|
289
|
+
'use client';
|
|
290
|
+
import { HorusProvider } from '@horus-wallet/sdk-react';
|
|
291
|
+
|
|
292
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
293
|
+
return (
|
|
294
|
+
<HorusProvider appId={process.env.NEXT_PUBLIC_HORUS_APP_ID!}>
|
|
295
|
+
{children}
|
|
296
|
+
</HorusProvider>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// app/layout.tsx
|
|
301
|
+
import { Providers } from './providers';
|
|
302
|
+
|
|
303
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
304
|
+
return <html><body><Providers>{children}</Providers></body></html>;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
// app/api/horus/[...path]/route.ts
|
|
310
|
+
import { HorusClient } from '@horus-wallet/sdk';
|
|
311
|
+
|
|
312
|
+
const client = (req: Request) =>
|
|
313
|
+
new HorusClient({
|
|
314
|
+
baseUrl: process.env.HORUS_BASE_URL!,
|
|
315
|
+
apiKey: process.env.HORUS_API_KEY!,
|
|
316
|
+
getIdToken: async () => req.headers.get('x-horus-id-token') ?? '',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const handlers: Record<string, (req: Request, body: any) => Promise<unknown>> = {
|
|
320
|
+
'auth/email/signin': (req, body) => client(req).auth.signInWithEmail(body),
|
|
321
|
+
'auth/email/signup': (req, body) => client(req).auth.signUpWithEmail(body),
|
|
322
|
+
'auth/email-link/send': (req, body) => client(req).auth.sendEmailLink(body),
|
|
323
|
+
'auth/email-link/verify': (req, body) => client(req).auth.verifyEmailLink(body),
|
|
324
|
+
'auth/oauth': (req, body) => client(req).auth.signInWithOAuth(body),
|
|
325
|
+
'auth/refresh': (req, body) => client(req).auth.refresh(body.refreshToken),
|
|
326
|
+
'wallets': (req) => client(req).wallets.get(),
|
|
327
|
+
'sign-message': (req, body) => client(req).wallets.signMessage(body),
|
|
328
|
+
'transfer/native': (req, body) => client(req).transfers.native(body),
|
|
329
|
+
'transfer/token': (req, body) => client(req).transfers.token(body),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
async function dispatch(req: Request, ctx: { params: { path: string[] } }) {
|
|
333
|
+
const fn = handlers[ctx.params.path.join('/')];
|
|
334
|
+
if (!fn) return Response.json({ message: 'Not found' }, { status: 404 });
|
|
335
|
+
try {
|
|
336
|
+
const body = req.headers.get('content-type')?.includes('json') ? await req.json() : undefined;
|
|
337
|
+
return Response.json(await fn(req, body));
|
|
338
|
+
} catch (err: any) {
|
|
339
|
+
return Response.json({ message: err.message }, { status: err.status ?? 500 });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export const GET = dispatch;
|
|
344
|
+
export const POST = dispatch;
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Errors
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
import { HorusHttpError } from '@horus-wallet/sdk-react';
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await loginWithEmail({ email, password });
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (err instanceof HorusHttpError) {
|
|
356
|
+
if (err.status === 401) {
|
|
357
|
+
// Wrong creds, expired token, or token rejected by server
|
|
358
|
+
}
|
|
359
|
+
if (err.status === 429) {
|
|
360
|
+
// Rate limited — back off
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The hooks (`useWallets`, `useSignMessage`, `useTransfer`) also expose `error` as part of their return shape so you can render error states declaratively.
|
|
367
|
+
|
|
368
|
+
## Security
|
|
369
|
+
|
|
370
|
+
- The `hk_sk_*` secret stays on your backend — it never reaches the browser
|
|
371
|
+
- Tokens persist in `localStorage` by default. For stricter privacy, use `sessionStorage` (cleared on tab close) or `memory` (RAM only) via the `tokenStorage` prop
|
|
372
|
+
- The popup flows pin their `postMessage` target origin to the partner's window — tokens cannot be exfiltrated to other origins
|
|
373
|
+
- White-label settings (hiding the Horus footer in the popup) are server-enforced — partners on the standard tier always see the footer regardless of client config
|
|
374
|
+
|
|
375
|
+
## Coming from Privy / Magic / Web3Auth?
|
|
376
|
+
|
|
377
|
+
| Privy | Horus |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `<PrivyProvider>` | `<HorusProvider>` |
|
|
380
|
+
| `usePrivy()` | `useHorusAuth()` |
|
|
381
|
+
| `usePrivy().login()` | `useHorusAuth().loginWithGoogle()` (or any flow) |
|
|
382
|
+
| `useWallets()` | `useWallets()` |
|
|
383
|
+
| `usePrivy().user` | `useHorusAuth().user` or `useUser()` |
|
|
384
|
+
| Hosted modal | `<HorusLoginButton>` (popup) |
|
|
385
|
+
| Backend SDK | `@horus-wallet/sdk` |
|
|
386
|
+
|
|
387
|
+
Conceptual differences:
|
|
388
|
+
- Horus uses a **partner-backend proxy** so your secret never leaves your server. Privy talks to its API directly from the browser.
|
|
389
|
+
- **More chains supported** — EVM + Bitcoin + ICP + Casper + Aeternity all in one SDK.
|
|
390
|
+
|
|
391
|
+
## What's not yet here
|
|
392
|
+
|
|
393
|
+
- External wallet connect (MetaMask / WalletConnect) — coming
|
|
394
|
+
- Mobile SDKs (iOS / Android / React Native) — coming
|
|
395
|
+
- Drop-in `<HorusAuthModal>` (inline UI without popup) — coming
|
|
396
|
+
- Pre-generated wallets for unsigned-in users — coming
|
|
397
|
+
|
|
398
|
+
For partner integration questions: `info@horuswallet.com`.
|