@yieldfabric/wallet 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 +680 -0
- package/dist/index.cjs +26761 -0
- package/dist/index.d.cts +5011 -0
- package/dist/index.d.ts +5011 -0
- package/dist/index.js +26570 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
# @yieldfabric/wallet
|
|
2
|
+
|
|
3
|
+
Reusable YieldFabric wallet for service developers — login + onboarding,
|
|
4
|
+
signing (manual MQ + generic API), profile, and key management. Drop
|
|
5
|
+
`<WalletProvider>` into your React app and the rest of the surface
|
|
6
|
+
(hooks, UIs, services) becomes available.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Published to the **public npm registry** — no scope `.npmrc`, no token:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @yieldfabric/wallet
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer deps: `react ^18`, `react-dom ^18` (and optionally `@stripe/stripe-js` for
|
|
17
|
+
the Stripe-backed KYC provider). All other runtime deps (`@avatune/*`,
|
|
18
|
+
`@averer/averer-websdk`, `@heroicons/react`, `js-sha3`, `zod`) install
|
|
19
|
+
automatically from the public npm registry. The SDK bundles a Tailwind-styled
|
|
20
|
+
UI; consumers must add it to their Tailwind content glob (see below).
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// app/layout.tsx (Next.js App Router)
|
|
26
|
+
'use client';
|
|
27
|
+
import { WalletProvider, SignatureWorkflow } from '@yieldfabric/wallet';
|
|
28
|
+
import { useRouter } from 'next/navigation';
|
|
29
|
+
|
|
30
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
return (
|
|
33
|
+
<html>
|
|
34
|
+
<body>
|
|
35
|
+
<WalletProvider
|
|
36
|
+
config={{
|
|
37
|
+
// Imperative navigation hook so the SDK can route between
|
|
38
|
+
// login / onboarding screens. Wire to your router.
|
|
39
|
+
onNavigate: (href) => router.push(href),
|
|
40
|
+
// (Optional) Called after a session is established.
|
|
41
|
+
// onSessionBootstrapped: (userId) => { /* invalidate caches */ },
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
{/* Zero-config global mount — wires the wallet's signature
|
|
46
|
+
queue, drawer, toast notifier, and (optionally) the
|
|
47
|
+
Signature Center launcher. This is the component every
|
|
48
|
+
host wants at the root; <SignatureDrawer /> by itself is
|
|
49
|
+
a *controlled inner* that needs `message` / `onClose` /
|
|
50
|
+
`userId` props and is rendered by <SignatureWorkflow>
|
|
51
|
+
for each pending message. */}
|
|
52
|
+
<SignatureWorkflow />
|
|
53
|
+
</WalletProvider>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// app/login/page.tsx
|
|
62
|
+
'use client';
|
|
63
|
+
import { LoginForm, useAuth } from '@yieldfabric/wallet';
|
|
64
|
+
|
|
65
|
+
export default function LoginPage() {
|
|
66
|
+
const { isAuthenticated } = useAuth();
|
|
67
|
+
if (isAuthenticated) /* redirect */;
|
|
68
|
+
return <LoginForm />;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Configuring service URLs
|
|
73
|
+
|
|
74
|
+
The SDK reads service base URLs from `process.env.REACT_APP_*` at
|
|
75
|
+
build time (see [`src/core/config.ts`](./src/core/config.ts)):
|
|
76
|
+
|
|
77
|
+
| Var | Default | Use |
|
|
78
|
+
|--|--|--|
|
|
79
|
+
| `REACT_APP_AUTH_SERVICE_URL` | `http://localhost:3000` | auth REST |
|
|
80
|
+
| `REACT_APP_PAYMENTS_SERVICE_URL` | `http://localhost:3002` | payments + WebSocket |
|
|
81
|
+
| `REACT_APP_API_URL` | `http://localhost:3002` | shared API base (WS derivation) |
|
|
82
|
+
| `REACT_APP_CORE_SERVICE_URL` | `http://localhost:3002` | core service URL |
|
|
83
|
+
| `REACT_APP_CHAIN_ID` | `31337` | default chain id |
|
|
84
|
+
|
|
85
|
+
How to expose them to the browser depends on your bundler:
|
|
86
|
+
|
|
87
|
+
- **CRA / craco**: `REACT_APP_*` works natively. Put them in `.env`.
|
|
88
|
+
- **Vite**: define them with `define: { 'process.env.REACT_APP_*': ... }` in `vite.config.ts`, or fork `core/config.ts` to read `import.meta.env.VITE_*`.
|
|
89
|
+
- **Next.js**: prefix with `NEXT_PUBLIC_` in `.env.local` AND map them in `next.config.js`'s `env` block so `process.env.REACT_APP_*` resolves in client bundles.
|
|
90
|
+
|
|
91
|
+
Future work: a follow-up will move URL resolution onto the
|
|
92
|
+
`WalletRuntimeConfig` so consumers can pass URLs at mount time and
|
|
93
|
+
not rely on env-var plumbing. See the comment at the top of
|
|
94
|
+
`core/config.ts`.
|
|
95
|
+
|
|
96
|
+
The full public surface — providers, hooks, UI components, core REST/crypto
|
|
97
|
+
services, types — is enumerated in [`src/index.ts`](./src/index.ts).
|
|
98
|
+
|
|
99
|
+
## Login surface API
|
|
100
|
+
|
|
101
|
+
The login screen is built from three independently-mountable pieces:
|
|
102
|
+
|
|
103
|
+
| Component | Use when |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `<LoginAltMethods />` | You want the wallet / passkey chip row (Averer, MetaMask, Dynamic, Passkey). No props. Reads `auth.providers.enabled` from `/auth/providers/config`. |
|
|
106
|
+
| `<LoginForm />` | You want the bundled email/password form with sensible defaults (`services: ['vault', 'payments']`, `autoRedirect: false`). |
|
|
107
|
+
| `<LoginComponent>` | You want full control. Pass `options` for hook config; pass `render={...}` or `children={...}` to take over the JSX while keeping the form state machine. |
|
|
108
|
+
|
|
109
|
+
### Custom-JSX login via `render` prop
|
|
110
|
+
|
|
111
|
+
`<LoginComponent render={...}>` receives the *full* form state — both
|
|
112
|
+
the submission half (`login`, `clearError`, `isLoading`, `isSubmitting`,
|
|
113
|
+
`error`) and the `useForm` machine (`values`, `errors`, `touched`,
|
|
114
|
+
`handleChange`, `handleBlur`, `handleSubmit`). This means you can render
|
|
115
|
+
any layout you want without re-implementing form state:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<LoginComponent
|
|
119
|
+
options={{ services: ['vault', 'payments'], autoRedirect: false }}
|
|
120
|
+
render={({ values, errors, touched, handleChange, handleSubmit, isSubmitting, error }) => (
|
|
121
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
122
|
+
<input
|
|
123
|
+
type="email"
|
|
124
|
+
value={values.email}
|
|
125
|
+
onChange={(e) => handleChange('email', e.target.value)}
|
|
126
|
+
className="my-custom-field"
|
|
127
|
+
/>
|
|
128
|
+
{/* …password, error, submit… */}
|
|
129
|
+
</form>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Incoming payments + obligations inbox
|
|
135
|
+
|
|
136
|
+
Embeddable surface for letting an authenticated user view and accept
|
|
137
|
+
pending payments (instant sends, swap legs) and obligations (contracts
|
|
138
|
+
whose status is `ACTIVE` and whose counterpart is the active entity).
|
|
139
|
+
|
|
140
|
+
Three drop-in components, ordered most opinionated → least:
|
|
141
|
+
|
|
142
|
+
| Component | Use when |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `<IncomingPaymentsLauncher />` | You want a nav-chip with a badge count. Click opens an internal drawer with the full inbox. Mirrors `<SignatureCenterLauncher>` in shape. |
|
|
145
|
+
| `<IncomingPaymentsCenter />` | You want a dedicated `/payments/inbox`-style page. Adds a header, refresh button, and summary tiles on top of the inbox. |
|
|
146
|
+
| `<IncomingPaymentsInbox />` | You want just the card list — embed in your own chrome (drawer, dashboard card, sidebar). |
|
|
147
|
+
| `useIncomingPayments()` | You want full UI control. Returns `{ items, groups, accept, acceptState, loading, error, refetch, activeEntityId, … }`. |
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import {
|
|
151
|
+
IncomingPaymentsLauncher,
|
|
152
|
+
IncomingPaymentsCenter,
|
|
153
|
+
IncomingPaymentsInbox,
|
|
154
|
+
useIncomingPayments,
|
|
155
|
+
} from '@yieldfabric/wallet';
|
|
156
|
+
|
|
157
|
+
// Launcher in your nav.
|
|
158
|
+
<IncomingPaymentsLauncher
|
|
159
|
+
variant="icon-label"
|
|
160
|
+
onReview={(item) => router.push(`/contracts/${item.contract?.id ?? ''}`)}
|
|
161
|
+
getAccountLabel={(id) => entityNameCache.get(id) ?? id}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
// Or a full page.
|
|
165
|
+
<Route path="/payments/inbox" element={<IncomingPaymentsCenter />} />
|
|
166
|
+
|
|
167
|
+
// Or build your own layout, SDK does the data.
|
|
168
|
+
function MyInbox() {
|
|
169
|
+
const { items, accept, acceptState } = useIncomingPayments();
|
|
170
|
+
return items.map((item) => (
|
|
171
|
+
<MyRow
|
|
172
|
+
key={item.key}
|
|
173
|
+
item={item}
|
|
174
|
+
onAccept={() => accept(item)}
|
|
175
|
+
busy={acceptState.itemKey === item.key}
|
|
176
|
+
/>
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Acting-as / delegation is automatic
|
|
182
|
+
|
|
183
|
+
When a host mints a delegation token via
|
|
184
|
+
`useAuthContext().createDelegationToken(group)`, the inbox automatically
|
|
185
|
+
scopes every query and mutation to the delegated group. No prop
|
|
186
|
+
threading required. The resolution order for the active entity is:
|
|
187
|
+
|
|
188
|
+
1. Explicit `entityId` prop on the hook / component (if passed).
|
|
189
|
+
2. `localStorage.delegation_group_id` — set by `<AppAuthProvider>` when
|
|
190
|
+
delegation is active.
|
|
191
|
+
3. `useAuth().user.id` — the authenticated user's own entity.
|
|
192
|
+
|
|
193
|
+
Hosts that maintain their own actingAs model can pass `entityId`
|
|
194
|
+
explicitly to override the auto-detection.
|
|
195
|
+
|
|
196
|
+
#### Signature requirement
|
|
197
|
+
|
|
198
|
+
Accepting a payment or obligation enqueues an on-chain transaction
|
|
199
|
+
through the MQ. When the user is in **Manual** signature mode the
|
|
200
|
+
SDK's signature drawer prompts them to sign before the executor
|
|
201
|
+
fires. Consumers using this inbox **must mount
|
|
202
|
+
`<SignatureWorkflow />`** once at the app root — that's the zero-prop
|
|
203
|
+
component that wires the queue + drawer + toast + Signature Center
|
|
204
|
+
launcher together. `<SignatureDrawer />` by itself is a controlled
|
|
205
|
+
inner that takes `message` / `onClose` / `userId` props; you don't
|
|
206
|
+
mount it directly.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// Once at the app root.
|
|
210
|
+
import { WalletProvider, SignatureWorkflow } from '@yieldfabric/wallet';
|
|
211
|
+
|
|
212
|
+
<WalletProvider config={…}>
|
|
213
|
+
{children}
|
|
214
|
+
<SignatureWorkflow />
|
|
215
|
+
</WalletProvider>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Without this mount, manual-mode users will see the inbox's `Accept`
|
|
219
|
+
mutation succeed at the resolver but the on-chain settlement will
|
|
220
|
+
never run.
|
|
221
|
+
|
|
222
|
+
#### Live refresh
|
|
223
|
+
|
|
224
|
+
The hook subscribes to the same entity-events SSE stream the
|
|
225
|
+
signature center uses (`Notification`, `MessageStatusChange`,
|
|
226
|
+
`WorkflowUpdate`, `PositionChange`). When any of those fire for the
|
|
227
|
+
active entity, the inbox refetches automatically. The cost of a
|
|
228
|
+
refetch is small enough that a coarse "refresh on any relevant
|
|
229
|
+
event" strategy is correct — no manual cache invalidation is
|
|
230
|
+
required.
|
|
231
|
+
|
|
232
|
+
Pass `disableLiveRefresh: true` to opt out (e.g. for tests).
|
|
233
|
+
|
|
234
|
+
### Send payment (confidential YF "instant send")
|
|
235
|
+
|
|
236
|
+
Embeddable surface for the outbound side of the wallet. Submits a YF
|
|
237
|
+
confidential payment via the `instant` GraphQL mutation — amounts are
|
|
238
|
+
encrypted on chain, queued through the MQ, gated by
|
|
239
|
+
`<SignatureWorkflow />` when the user is in Manual mode.
|
|
240
|
+
|
|
241
|
+
Three drop-in surfaces:
|
|
242
|
+
|
|
243
|
+
| Component | Use when |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `<SendPaymentDrawer />` | You want a "Send" button somewhere that opens a drawer. Zero-config. |
|
|
246
|
+
| `<SendPaymentForm />` | You want the form embedded in your own chrome (page, modal, sidebar). |
|
|
247
|
+
| `useSendPayment()` | You want full UI control. Returns `{ send, sending, error, lastResponse, reset }`. |
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import {
|
|
251
|
+
SendPaymentDrawer,
|
|
252
|
+
SendPaymentForm,
|
|
253
|
+
useSendPayment,
|
|
254
|
+
convertAmountToRaw,
|
|
255
|
+
} from '@yieldfabric/wallet';
|
|
256
|
+
|
|
257
|
+
// 1. Drawer — open from a button in your nav / wallet panel.
|
|
258
|
+
const [open, setOpen] = useState(false);
|
|
259
|
+
|
|
260
|
+
<button onClick={() => setOpen(true)}>Send</button>
|
|
261
|
+
|
|
262
|
+
<SendPaymentDrawer
|
|
263
|
+
isOpen={open}
|
|
264
|
+
onClose={() => setOpen(false)}
|
|
265
|
+
assets={[
|
|
266
|
+
{ id: 'aud-token-asset', currency: 'AUD', decimals: '18' },
|
|
267
|
+
{ id: 'usdc-token-asset', currency: 'USDC', decimals: '6' },
|
|
268
|
+
]}
|
|
269
|
+
onSent={(response) => {
|
|
270
|
+
// Resolver accepted — the recipient's inbox will now show the
|
|
271
|
+
// payment as PENDING. `response.messageId` is the MQ id for
|
|
272
|
+
// optional `messagesService.pollMessageCompletion` polling.
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
275
|
+
|
|
276
|
+
// 2. Embed the form directly (no drawer).
|
|
277
|
+
<SendPaymentForm
|
|
278
|
+
assets={assets}
|
|
279
|
+
defaultAssetId="aud-token-asset"
|
|
280
|
+
onSuccess={(response) => navigate(`/payments/${response.paymentId}`)}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
// 3. Build your own form, SDK handles the submit + state.
|
|
284
|
+
function MySendForm() {
|
|
285
|
+
const { send, sending, error } = useSendPayment();
|
|
286
|
+
return (
|
|
287
|
+
<form onSubmit={async (e) => {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
await send({
|
|
290
|
+
assetId: 'aud-token-asset',
|
|
291
|
+
amount: convertAmountToRaw('100.50', 18),
|
|
292
|
+
destinationId: 'someone@example.com',
|
|
293
|
+
});
|
|
294
|
+
}}>
|
|
295
|
+
… your fields …
|
|
296
|
+
<button disabled={sending}>{sending ? 'Sending…' : 'Send'}</button>
|
|
297
|
+
{error && <p>{error}</p>}
|
|
298
|
+
</form>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Asset list is host-supplied
|
|
304
|
+
|
|
305
|
+
The SDK doesn't bundle an asset-lookup query — that's host-specific
|
|
306
|
+
(GraphQL `assets.all` for some hosts, a hardcoded enum for others).
|
|
307
|
+
Pass the asset list as a prop to `<SendPaymentForm>` /
|
|
308
|
+
`<SendPaymentDrawer>`. The shape is minimal:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
interface SendPaymentAsset {
|
|
312
|
+
id: string; // token asset id (e.g. "aud-token-asset")
|
|
313
|
+
currency: string; // display label ("AUD", "USDC")
|
|
314
|
+
decimals?: string | number; // defaults to 18 when omitted
|
|
315
|
+
name?: string; // optional long name for the picker
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Amount conversion
|
|
320
|
+
|
|
321
|
+
The resolver wants a raw on-chain integer string (`"100500000000000000000"`
|
|
322
|
+
for 100.50 AUD with 18 decimals). The form does this conversion
|
|
323
|
+
internally via `convertAmountToRaw(humanAmount, decimals)`. If you're
|
|
324
|
+
calling the service / hook directly, do the same:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { convertAmountToRaw } from '@yieldfabric/wallet';
|
|
328
|
+
const raw = convertAmountToRaw('100.50', 18); // → "100500000000000000000"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`convertAmountToRaw` rejects negatives, NaN, scientific notation,
|
|
332
|
+
and over-precision (more fractional digits than the token's
|
|
333
|
+
decimals), so you can use it as your validator too.
|
|
334
|
+
|
|
335
|
+
#### Recipient resolution
|
|
336
|
+
|
|
337
|
+
The form's "Recipient" field accepts an **entity name or id**
|
|
338
|
+
(`someone@example.com`, a UUID, …). Behind the scenes the resolver
|
|
339
|
+
looks up the entity's default wallet. For multi-wallet entities
|
|
340
|
+
where the sender wants to target a specific wallet, set
|
|
341
|
+
`enableWalletIdMode` to expose a small toggle that switches the field
|
|
342
|
+
to wallet-UUID input.
|
|
343
|
+
|
|
344
|
+
Direct calls have all three options:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
await send({ assetId, amount, destinationId: 'someone@example.com' });
|
|
348
|
+
await send({ assetId, amount, destinationWalletId: 'd1b3…wallet-uuid' });
|
|
349
|
+
await send({ assetId, amount, contractId: 'CONTRACT-OBLIGATION-…' });
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Pass exactly one. The resolver throws when zero or multiple are
|
|
353
|
+
provided.
|
|
354
|
+
|
|
355
|
+
#### Acting-as is automatic
|
|
356
|
+
|
|
357
|
+
Same story as the inbox: when a delegation token is installed
|
|
358
|
+
(`useAuthContext().createDelegationToken(group)`), the send
|
|
359
|
+
originates from the delegated group instead of the user. No prop
|
|
360
|
+
threading needed — `tokenManager` picks the right token, the
|
|
361
|
+
resolver reads the `acting_as` claim and signs against the group's
|
|
362
|
+
wallet.
|
|
363
|
+
|
|
364
|
+
#### Signature requirement
|
|
365
|
+
|
|
366
|
+
For Manual-mode users (external wallets — passkey, MetaMask,
|
|
367
|
+
Dynamic), the on-chain send is gated by `<SignatureWorkflow />`.
|
|
368
|
+
Mount it once at the app root. Without it, `send()` resolves
|
|
369
|
+
successfully but the executor never runs.
|
|
370
|
+
|
|
371
|
+
### Block-explorer links
|
|
372
|
+
|
|
373
|
+
Transaction hashes rendered by the SDK's signature drawer + signature
|
|
374
|
+
center row + EOA-send status panel are automatically wrapped in
|
|
375
|
+
explorer links when the SDK has a registered explorer for the
|
|
376
|
+
message's chain id. Click the hash → opens the chain's explorer
|
|
377
|
+
(Etherscan, Polygonscan, BscScan, SnowTrace, Redbelly Explorer, …)
|
|
378
|
+
in a new tab.
|
|
379
|
+
|
|
380
|
+
Built-in coverage:
|
|
381
|
+
|
|
382
|
+
| Chain | Explorer |
|
|
383
|
+
|---|---|
|
|
384
|
+
| Ethereum Mainnet (`1`) | etherscan.io |
|
|
385
|
+
| Goerli (`5`) / Sepolia (`11155111`) | (goerli\|sepolia).etherscan.io |
|
|
386
|
+
| Polygon (`137`) / Mumbai (`80001`) | polygonscan.com / mumbai.polygonscan.com |
|
|
387
|
+
| BSC (`56`) / Testnet (`97`) | bscscan.com / testnet.bscscan.com |
|
|
388
|
+
| Avalanche (`43114`) / Fuji (`43113`) | snowtrace.io / testnet.snowtrace.io |
|
|
389
|
+
| Redbelly Mainnet (`151`) | redbelly.routescan.io |
|
|
390
|
+
| Redbelly Testnet (`153`) | redbelly.testnet.routescan.io |
|
|
391
|
+
|
|
392
|
+
Local-dev chains (`31337`, `33117`, `1337`) intentionally have no
|
|
393
|
+
explorer registered — the hash renders as plain text.
|
|
394
|
+
|
|
395
|
+
#### Adding or overriding an explorer
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { registerBlockExplorer } from '@yieldfabric/wallet';
|
|
399
|
+
|
|
400
|
+
// Add a chain not in the defaults:
|
|
401
|
+
registerBlockExplorer(8453, {
|
|
402
|
+
name: 'Basescan',
|
|
403
|
+
txUrl: (hash) => `https://basescan.org/tx/${hash}`,
|
|
404
|
+
addressUrl: (addr) => `https://basescan.org/address/${addr}`,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Override one path while inheriting the rest (e.g. prefer
|
|
408
|
+
// Blockscout for addresses but keep Etherscan for txs on mainnet):
|
|
409
|
+
registerBlockExplorer(1, {
|
|
410
|
+
addressUrl: (addr) => `https://eth.blockscout.com/address/${addr}`,
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Direct helpers if you need to render your own link:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
import {
|
|
418
|
+
getBlockExplorerTxUrl,
|
|
419
|
+
getBlockExplorerAddressUrl,
|
|
420
|
+
} from '@yieldfabric/wallet';
|
|
421
|
+
|
|
422
|
+
const txUrl = getBlockExplorerTxUrl(chainId, txHash); // string | null
|
|
423
|
+
const addrUrl = getBlockExplorerAddressUrl(chainId, addr); // string | null
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Both helpers return `null` when the chain has no registered explorer
|
|
427
|
+
or the input fails validation (empty / not `0x…`-hex / wrong length
|
|
428
|
+
for addresses), so the calling component can fall back to plain text
|
|
429
|
+
without an `if`-cascade.
|
|
430
|
+
|
|
431
|
+
### Passkey sign-in: no chain id needed by default
|
|
432
|
+
|
|
433
|
+
`signInWithPasskey()` and `signUpWithPasskey()` both accept an *optional*
|
|
434
|
+
`chainId`. When omitted, the SDK falls back to
|
|
435
|
+
`Number(getDefaultChainId())` (which reads `REACT_APP_CHAIN_ID`, defaulting
|
|
436
|
+
to `31337`):
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// All three call the same default chain
|
|
440
|
+
const r1 = await signInWithPasskey();
|
|
441
|
+
const r2 = await signInWithPasskey({});
|
|
442
|
+
const r3 = await signInWithPasskey({ chainId: Number(getDefaultChainId()) });
|
|
443
|
+
|
|
444
|
+
// Multi-chain consumers can still pin the chain explicitly
|
|
445
|
+
const r4 = await signInWithPasskey({ chainId: 1 });
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Tailwind setup
|
|
449
|
+
|
|
450
|
+
The SDK ships UI components styled with Tailwind utility classes. Tailwind
|
|
451
|
+
needs to scan the SDK source so it picks up which utilities to keep.
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
// tailwind.config.js
|
|
455
|
+
module.exports = {
|
|
456
|
+
content: [
|
|
457
|
+
'./src/**/*.{ts,tsx}',
|
|
458
|
+
// Scan the published bundles — className strings are intact (tsup
|
|
459
|
+
// does not minify), so the utilities survive purge.
|
|
460
|
+
'./node_modules/@yieldfabric/wallet/dist/**/*.{js,cjs}',
|
|
461
|
+
],
|
|
462
|
+
// …
|
|
463
|
+
};
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The package ships only `dist/` (compiled, source closed), so scan the bundles —
|
|
467
|
+
the class strings are preserved there verbatim.
|
|
468
|
+
|
|
469
|
+
## Theming for consumer apps
|
|
470
|
+
|
|
471
|
+
The SDK is theme-portable: every surface (login, onboarding, signature
|
|
472
|
+
drawer / center, keys manager, profile editor, primitives, identity card)
|
|
473
|
+
renders against a fixed set of **semantic** Tailwind token names. Map
|
|
474
|
+
them in your `tailwind.config.js` to re-skin the SDK to any palette —
|
|
475
|
+
dark, light, brand-coloured, anything. No `dark:` prefix is required; no
|
|
476
|
+
runtime theme switch; no SDK code change.
|
|
477
|
+
|
|
478
|
+
### The token contract
|
|
479
|
+
|
|
480
|
+
The SDK uses these names. Define them under `theme.colors` (or
|
|
481
|
+
`theme.extend.colors`) in your tailwind config.
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
// tailwind.config.js
|
|
485
|
+
module.exports = {
|
|
486
|
+
// …content paths…
|
|
487
|
+
theme: {
|
|
488
|
+
extend: {
|
|
489
|
+
colors: {
|
|
490
|
+
// ── Surfaces ────────────────────────────────────────────
|
|
491
|
+
page: '#ffffff', // viewport background
|
|
492
|
+
surface: '#ffffff', // card / sheet background
|
|
493
|
+
'surface-alt': '#f7f8fa', // muted card / striped row
|
|
494
|
+
raised: '#ffffff', // floating elements (toasts, drawers)
|
|
495
|
+
'raised-hover': '#f7f8fa',
|
|
496
|
+
overlay: 'rgba(255,255,255,0.92)',
|
|
497
|
+
background: '#ffffff', // legacy alias, still used by some SDK chrome
|
|
498
|
+
|
|
499
|
+
// ── Borders ─────────────────────────────────────────────
|
|
500
|
+
'border-default': '#e3e8f0',
|
|
501
|
+
'border-light': '#eef1f6',
|
|
502
|
+
'border-strong': '#c7d0dc',
|
|
503
|
+
|
|
504
|
+
// ── Text ────────────────────────────────────────────────
|
|
505
|
+
'text-primary': '#0a1b30', // body text
|
|
506
|
+
'text-secondary': '#32373c', // secondary copy
|
|
507
|
+
'text-muted': '#5f6b7a', // labels, captions
|
|
508
|
+
'text-inverse': '#ffffff', // text on dark surfaces
|
|
509
|
+
|
|
510
|
+
// ── Brand primary (accent / glow / focus ring) ─────────
|
|
511
|
+
primary: '#054e92',
|
|
512
|
+
'primary-muted': 'rgba(5,78,146,0.10)',
|
|
513
|
+
accent: '#0b6acf',
|
|
514
|
+
|
|
515
|
+
// ── CTA (high-emphasis button) ─────────────────────────
|
|
516
|
+
// In a dark theme this is intentionally near-text-color so the
|
|
517
|
+
// button reads as inverted; in a light theme it's typically the
|
|
518
|
+
// brand color. The two tokens are kept distinct so each consumer
|
|
519
|
+
// can choose what "CTA" should look like vs. the brand accent.
|
|
520
|
+
cta: '#054e92',
|
|
521
|
+
'cta-hover':'#054075',
|
|
522
|
+
'on-cta': '#ffffff',
|
|
523
|
+
|
|
524
|
+
// ── Chip (elevated small surface: sign-in chips,
|
|
525
|
+
// segmented controls, selected pills) ──────────
|
|
526
|
+
chip: '#f7f8fa',
|
|
527
|
+
'chip-hover': '#eef2f8',
|
|
528
|
+
'chip-active': '#e3e8f0',
|
|
529
|
+
|
|
530
|
+
// ── Input (subtle filled input / inline card bg) ───────
|
|
531
|
+
'input-bg': '#ffffff',
|
|
532
|
+
|
|
533
|
+
// ── Status (used by signature drawer / center / toasts) ─
|
|
534
|
+
'status-success-bg': '#dcf0e6',
|
|
535
|
+
'status-success-text': '#1a6b42',
|
|
536
|
+
'status-warning-bg': '#fbf3d4',
|
|
537
|
+
'status-warning-text': '#7a6518',
|
|
538
|
+
'status-error-bg': '#fde2e7',
|
|
539
|
+
'status-error-text': '#9b2c3c',
|
|
540
|
+
'status-info-bg': '#dceaf8',
|
|
541
|
+
'status-info-text': '#1a5090',
|
|
542
|
+
|
|
543
|
+
// ── Pastels (soft badge fills) ──────────────────────────
|
|
544
|
+
'pastel-peach': '#fde6d5',
|
|
545
|
+
'pastel-lavender': '#e9e2f5',
|
|
546
|
+
'pastel-mint': '#d4f0e4',
|
|
547
|
+
'pastel-sky': '#dceaf8',
|
|
548
|
+
'pastel-rose': '#fbe2e6',
|
|
549
|
+
'pastel-lemon': '#fbf3d4',
|
|
550
|
+
'pastel-lilac': '#ecdef6',
|
|
551
|
+
'pastel-coral': '#fde4dd',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The example values above are a light theme. Drop the same token names
|
|
559
|
+
into your own palette and the SDK picks up your colours.
|
|
560
|
+
|
|
561
|
+
### Backing tokens with CSS variables (optional)
|
|
562
|
+
|
|
563
|
+
If you run a single app with both light and dark modes, back each token
|
|
564
|
+
with a CSS variable in `:root` and `.dark`, then point Tailwind at the
|
|
565
|
+
variable. See the YieldFabric host's `index.css` for the canonical
|
|
566
|
+
pattern; the values to define are the same as the table above, prefixed
|
|
567
|
+
with `--color-`.
|
|
568
|
+
|
|
569
|
+
### Autofill (Chrome / Safari)
|
|
570
|
+
|
|
571
|
+
The login + signup inputs use a `.autofill-fix` class to keep autofilled
|
|
572
|
+
text legible. Add this CSS once in your global stylesheet (light theme
|
|
573
|
+
shown; flip the box-shadow + fill-color for dark):
|
|
574
|
+
|
|
575
|
+
```css
|
|
576
|
+
.autofill-fix:-webkit-autofill,
|
|
577
|
+
.autofill-fix:-webkit-autofill:hover,
|
|
578
|
+
.autofill-fix:-webkit-autofill:focus {
|
|
579
|
+
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
|
|
580
|
+
-webkit-text-fill-color: #0a1b30 !important;
|
|
581
|
+
caret-color: #0a1b30;
|
|
582
|
+
transition: background-color 5000s ease-in-out 0s;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### What you don't have to do
|
|
587
|
+
|
|
588
|
+
- **No `theme="light" | "dark"` prop** — the SDK has no theme switch;
|
|
589
|
+
the same JSX renders in either palette.
|
|
590
|
+
- **No CSS-in-JS** — Tailwind classes only, so your build pipeline is
|
|
591
|
+
unchanged.
|
|
592
|
+
- **No code forks per consumer** — every SDK surface is portable today;
|
|
593
|
+
a future consumer needs only the token map above.
|
|
594
|
+
|
|
595
|
+
## Package layout
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
src/
|
|
599
|
+
├── core/ no React, no DOM — REST clients, WebAuthn, crypto, token store
|
|
600
|
+
├── providers/ auth provider impls (email, passkey, MetaMask, Averer) + config
|
|
601
|
+
├── react/ hooks + headless controllers
|
|
602
|
+
└── ui/ styled components (LoginWidget, SignatureDrawer, KeysManager, …)
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Import graph: `ui → react → providers → core`. Forbid backward edges.
|
|
606
|
+
|
|
607
|
+
## Build
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
npm install
|
|
611
|
+
npm run build # one-shot — emits dist/index.{js,cjs,d.ts}
|
|
612
|
+
npm run dev # watch mode — recompiles on save
|
|
613
|
+
npm run typecheck # tsc --noEmit
|
|
614
|
+
npm run clean # rm -rf dist
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`tsup` (esbuild + dts) produces:
|
|
618
|
+
|
|
619
|
+
- `dist/index.js` — ESM bundle (consumed by modern bundlers / Next.js)
|
|
620
|
+
- `dist/index.cjs` — CommonJS bundle (Node fallback)
|
|
621
|
+
- `dist/index.d.ts` / `.d.cts` — type definitions
|
|
622
|
+
|
|
623
|
+
Both bundles are prepended with `"use client";` so Next.js App Router
|
|
624
|
+
treats the SDK as client-side by default (the surface is hook + DOM-heavy).
|
|
625
|
+
|
|
626
|
+
Externals: `react`, `react-dom`, and the runtime deps (`@averer/averer-websdk`,
|
|
627
|
+
`@heroicons/react`, `js-sha3`, `zod`) are NOT bundled — consumers install
|
|
628
|
+
them via the `dependencies` / `peerDependencies` declarations.
|
|
629
|
+
|
|
630
|
+
## Consuming from another monorepo package
|
|
631
|
+
|
|
632
|
+
If your app lives in the same repo and uses `file:` reference, the consumer
|
|
633
|
+
reads `dist/` via the `main` / `module` / `exports` fields:
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"dependencies": {
|
|
638
|
+
"@yieldfabric/wallet": "file:../yieldfabric-wallet-sdk"
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Workflow:
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
# In the SDK
|
|
647
|
+
cd yieldfabric-wallet-sdk
|
|
648
|
+
npm install
|
|
649
|
+
npm run build # one-shot, or `npm run dev` for watch mode
|
|
650
|
+
|
|
651
|
+
# In the consumer
|
|
652
|
+
cd ../my-app
|
|
653
|
+
npm install # picks up the freshly-built dist
|
|
654
|
+
npm run dev
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
If you're iterating on the SDK while running the consumer, leave
|
|
658
|
+
`npm run dev` running in the SDK directory; the consumer's bundler will
|
|
659
|
+
pick up changes on the next reload.
|
|
660
|
+
|
|
661
|
+
## Publishing
|
|
662
|
+
|
|
663
|
+
Ships to the **public npm registry** as a public scoped package
|
|
664
|
+
(`publishConfig.access: public`). Publishing `@yieldfabric/*` requires the
|
|
665
|
+
`yieldfabric` scope on npm — i.e. an npm org (or user) named `yieldfabric`.
|
|
666
|
+
Authenticate once with `npm login`, then:
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
# bump "version" first — re-publishing an existing version is rejected
|
|
670
|
+
npm publish # prepublishOnly runs clean && build automatically
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
`prepublishOnly` runs `clean && build`, so a stale `dist/` can't leak into the
|
|
674
|
+
tarball. The published tarball ships **`dist/` + `README.md` only** — no `src/`
|
|
675
|
+
and no source maps, so the TypeScript source stays closed (only compiled
|
|
676
|
+
output ships). Verify with `npm pack --dry-run`.
|
|
677
|
+
|
|
678
|
+
## License
|
|
679
|
+
|
|
680
|
+
Proprietary. See repo root for terms.
|