@usearete/react 0.46.0 → 0.48.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 +162 -1
- package/dist/index.cjs +909 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +423 -5
- package/dist/index.esm.js +900 -31
- package/dist/index.esm.js.map +1 -1
- package/dist/testing.d.ts +9 -1
- package/dist/wallet-standard.cjs +129 -0
- package/dist/wallet-standard.cjs.map +1 -0
- package/dist/wallet-standard.d.ts +73 -0
- package/dist/wallet-standard.esm.js +120 -0
- package/dist/wallet-standard.esm.js.map +1 -0
- package/package.json +30 -3
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Supported props:
|
|
|
123
123
|
- `autoConnect` — defaults to `true`; controls only whether the provider starts the initial connection
|
|
124
124
|
- `autoReconnect` — defaults to `true`; controls only recovery after an established connection is lost
|
|
125
125
|
- `wallet`
|
|
126
|
-
- `auth`
|
|
126
|
+
- `auth` (see [Signing users in with their Arete login](#signing-users-in-with-their-arete-login))
|
|
127
127
|
- `fetch`
|
|
128
128
|
- `validateFrames` — set `false` to suppress rejected-frame warnings; generated schemas still normalize and validate entities
|
|
129
129
|
- `onFrameValidationError` — structured callback for frames that were not stored: schema rejections (`reason: 'schema'`) and patches for keys the client holds no entity for (`reason: 'unknown-key'`)
|
|
@@ -136,6 +136,90 @@ These are provider-wide defaults. Per-call overrides stay on the hook call and a
|
|
|
136
136
|
|
|
137
137
|
The lifecycle flags are intentionally independent. `autoConnect={false}` leaves initial connection ownership to the application. It does not disable automatic recovery for a connection the application establishes, but `canRetry` remains false because provider-owned replacement attempts would also stay disconnected; use the client lifecycle directly. `autoReconnect={false}` does not suppress the initial connection; it disables only automatic recovery after that connection is lost.
|
|
138
138
|
|
|
139
|
+
### Signing users in with their Arete login
|
|
140
|
+
|
|
141
|
+
An app whose users sign in to Arete (a dashboard built on the SDKs) passes their login as an API credential. The SDK sends it only to the Arete API and its session endpoint; stack, gateway and program read connections mint their own sessions with it:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// Module level, so its identity is stable.
|
|
145
|
+
const auth = { tokenKind: 'api', tokenEndpoint: '/api/arete-token', tokenEndpointCredentials: 'same-origin' } as const;
|
|
146
|
+
|
|
147
|
+
<AreteProvider auth={auth}>
|
|
148
|
+
<App />
|
|
149
|
+
</AreteProvider>;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`getToken` works the same (`{ tokenKind: 'api', getToken: async () => session.accessToken }`). `usePlatformHttpClient({ apiUrl })` returns a client for the Arete API's account routes with that same auth, to hand to SDK extensions that take an `http` client.
|
|
153
|
+
|
|
154
|
+
## Sessions: `AreteSessionProvider` and `useAreteSession`
|
|
155
|
+
|
|
156
|
+
`AreteProvider` manages stack clients. For a [`createSession`](../core/README.md) session (programs, stacks and protocols behind one auth and one wallet), put an `AreteSessionProvider` above the components that use it:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { AreteSessionProvider, useAreteSession, useOperationMutation } from '@usearete/react';
|
|
160
|
+
import { SQUADS_V4_PROGRAM } from './sdk/squads-v4';
|
|
161
|
+
|
|
162
|
+
// Module level, so their identity is stable.
|
|
163
|
+
const SQUADS = { programs: { squads: SQUADS_V4_PROGRAM } };
|
|
164
|
+
const auth = { tokenKind: 'api', tokenEndpoint: '/api/arete-token', tokenEndpointCredentials: 'same-origin' } as const;
|
|
165
|
+
|
|
166
|
+
<AreteSessionProvider definition={SQUADS} options={{ auth }} wallet={wallet}>
|
|
167
|
+
<Squads />
|
|
168
|
+
</AreteSessionProvider>;
|
|
169
|
+
|
|
170
|
+
function Squads() {
|
|
171
|
+
const { session, status, error } = useAreteSession<typeof SQUADS>();
|
|
172
|
+
const squads = session?.programs.squads; // typed from the definition
|
|
173
|
+
…
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`useAreteSession()` returns `{ session, status, error, ready, retry }`. `status` is `opening`, `open` or `error`; `session` is `null` until it is `open`. `ready()` resolves with the session once it is open, waiting through a reopen. If the provider unmounts before then, it rejects with an `AreteSessionUnavailableError` (`code: 'provider_unmounted'`; StrictMode's extra mount doesn't count). It is stable, so it can be a `useOperationMutation` executor. `retry()` opens it again after a failure. Type it with the definition (`useAreteSession<typeof SQUADS>()`) or with a session type (`useAreteSession<MySession>()`). It throws outside an `AreteSessionProvider`.
|
|
178
|
+
|
|
179
|
+
How the provider manages the session:
|
|
180
|
+
|
|
181
|
+
- **Opened once, closed on unmount.** Given `definition` (and `options`, as for `createSession(definition, options)`), the provider opens the session when it mounts and closes it when it unmounts.
|
|
182
|
+
- **Auth from `AreteProvider`.** `options.auth` and `options.fetch` default to the enclosing `AreteProvider`'s, so one `auth` drives both.
|
|
183
|
+
- **A change reopens it.** When the definition or the options change, the old session is closed at once and a new one is opened; `session` is `null` until the new one is ready, so nothing runs against a session opened with credentials that no longer apply. A run waiting on `ready()` gets the new session. Options are compared by shape two levels deep: an inline `options={{ auth: { tokenKind: 'api', … } }}` with the same fields is the same options. Functions (`getToken`, `fetch`) compare by identity, so keep them stable (module level or `useCallback`).
|
|
184
|
+
- **The wallet does not reopen it.** `wallet` is a prop, not an option: changing it calls `session.setWallet`.
|
|
185
|
+
- **An existing session.** Pass `session={mySession}` instead of `definition` to share a session the app opened. The provider never closes it, and sets `wallet` on it only when the `wallet` prop is passed. `session={null}` while the app is still opening it leaves the provider `opening`.
|
|
186
|
+
|
|
187
|
+
Under React StrictMode in development the provider opens, closes and reopens the session once on mount, like any effect.
|
|
188
|
+
|
|
189
|
+
[Compose a session in React](https://docs.arete.run/sdks/react-sessions/) walks through a whole app on one session: providers, live views from a session stack, program reads with `useAsyncRead`, `useOperationMutation` with a Wallet Standard wallet, typed wallet errors and protocol hooks.
|
|
190
|
+
|
|
191
|
+
### Stacks in a session
|
|
192
|
+
|
|
193
|
+
A stack member of the session (`session.stacks.<key>`) is a connected client of its own. Pass it to `useArete` for the same views, programs, reads and mutation hooks `useArete(stack)` gives, typed for that stack, without a second connection:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { AreteSessionProvider, useArete, useAreteSession } from '@usearete/react';
|
|
197
|
+
import { ORE_STREAM_STACK } from './generated/ore-stack';
|
|
198
|
+
|
|
199
|
+
const APP = { stacks: { ore: ORE_STREAM_STACK } };
|
|
200
|
+
|
|
201
|
+
<AreteSessionProvider definition={APP} options={{ auth }} wallet={wallet}>
|
|
202
|
+
<Round />
|
|
203
|
+
</AreteSessionProvider>;
|
|
204
|
+
|
|
205
|
+
function Round() {
|
|
206
|
+
const { session } = useAreteSession<typeof APP>();
|
|
207
|
+
// `null` while the session opens: the stack definition gives the placeholders.
|
|
208
|
+
const ore = useArete(session?.stacks.ore, { stack: APP.stacks.ore });
|
|
209
|
+
const round = ore.views.OreRound.latest.use();
|
|
210
|
+
const deploy = ore.programs.ore.instructions.deploy.useMutation();
|
|
211
|
+
…
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- **One connection.** The hooks read the member's own connection and store; nothing connects again, and an enclosing `AreteProvider` opens nothing for it.
|
|
216
|
+
- **The session owns it.** Unmounting never disconnects the member. The provider closes it with the session.
|
|
217
|
+
- **While the session opens** the member is `null`, and the result is the one `useArete(stack)` gives before its client exists: `status: 'connecting'`, `client: null`, placeholder views and programs (addresses, PDAs and constants from the definition). Pass the stack definition as `stack` for this; TypeScript asks for it whenever the member can be `null` or `undefined`. Programs the session attached to the member (`createSession(APP, { stacks: { ore: { programs: { memo } } } })`) are not in that definition: pass them as `programs` (`{ stack: APP.stacks.ore, programs: { memo } }`) so their addresses, PDAs and constants work while it opens. TypeScript asks for every attached program `stack` lacks. If the session fails to open, `status` is `'error'` and `retry()` reopens it.
|
|
218
|
+
- **A reopen** (new auth or options) gives a new member: the hooks drop the old one and subscribe on the new one, with nothing carried over.
|
|
219
|
+
- **The wallet is the session's.** Mutations sign with the member's wallet, which the provider's `wallet` prop (`session.setWallet`) sets. `AreteProvider`'s `wallet` does not reach session members.
|
|
220
|
+
- With a member that is never `null` (`useArete(session.stacks.ore)`, for a session the app opened), `stack` can be left out: the hooks read the member's `stack`.
|
|
221
|
+
- **The transport is the member's.** Mutations wait for the streamed slot they landed in only when the session connected the member over the WebSocket (`member.transport === 'ws'`). A member the session connected over HTTP (`transport: 'http'` in the session options) confirms without waiting.
|
|
222
|
+
|
|
139
223
|
## `useArete(stack?, options?)`
|
|
140
224
|
|
|
141
225
|
`useArete` returns the connected React surface for a stack. With no arguments it resolves the provider's `stack`/`stackOptions`. Passing the same stack explicitly keeps the provider's `stackOptions`; per-call options override them, while another explicit stack is isolated from the provider stack's options. It throws a descriptive error when no stack is passed and no default is configured.
|
|
@@ -315,6 +399,83 @@ await or compose the result.
|
|
|
315
399
|
|
|
316
400
|
Program and read hooks are safe to call while the client is still connecting: mutations throw "Arete client is not connected" if submitted early, and reads stay disabled.
|
|
317
401
|
|
|
402
|
+
### Running an operation or flow: `useOperationMutation`
|
|
403
|
+
|
|
404
|
+
`useOperationMutation(prepare, { wallet, executor?, signers?, allowExecutorWallet? })` runs any prepared instruction, transaction or flow through anything with `execute(prepared, options)`: a connected client or a session, so it works without a stack. Inside an [`AreteSessionProvider`](#sessions-aretesessionprovider-and-usearetesession) `executor` can be left out: the hook runs on the provider's session, waiting for it to open. `prepare` returns the operation, or `{ operation, signers }` when it creates signers (a create key):
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { generateKeyPairSigner } from '@solana/kit';
|
|
408
|
+
import { useOperationMutation } from '@usearete/react';
|
|
409
|
+
|
|
410
|
+
const create = useOperationMutation(
|
|
411
|
+
async (input: { members: string[]; threshold: number }) => {
|
|
412
|
+
const createKey = await generateKeyPairSigner();
|
|
413
|
+
const operation = await squads.flows.arete.createMultisig.prepare({ ...input, createKey: createKey.address });
|
|
414
|
+
return { operation, signers: [createKey] };
|
|
415
|
+
},
|
|
416
|
+
{ executor: session, wallet },
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
<button disabled={create.isBusy} onClick={() => create.mutate(input)}>Create</button>;
|
|
420
|
+
{create.isAwaitingWallet && <p>Approve the request in your wallet</p>}
|
|
421
|
+
{create.error?.kind === 'wallet-rejected' && <p>You declined the request.</p>}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`status` walks `idle` → `preparing` → `signing` → `sending` → `confirming` → `done` (or `error`), once per transaction of a flow (`transactionIndex` of `transactionCount`). `sending` and `confirming` come from wallets that report their activity, as the `@usearete/adapter-kit` adapters do; any other wallet stays `signing` until it confirms. `prepared`, `artifacts`, `result` (the receipt) and `signatures` hold the latest run. `executor` may be a function that resolves it (a session opened on first use). A second run while one is in progress is refused with `busy`, so a double click never opens two wallet prompts. "In progress" is exactly `isBusy`: a run ends when `status` becomes `done` or `error`, before `onSuccess`/`onError` are awaited, so while an async callback runs the button above is enabled and a click starts a new run (the earlier `run()` promise still settles after its callback). Each hook follows only its own transactions, so several hooks can share one wallet without moving each other's `status`. `run()` rejects; `mutate()` keeps the error in state.
|
|
425
|
+
|
|
426
|
+
**No wallet, no run.** The run signs with the `wallet` given to `run(input, { wallet })` or to the hook. Without one (`wallet` is `undefined` while no wallet is connected) it fails with `not-ready` before `prepare` is called, so nothing is prepared or signed. The executor's own wallet (`session.wallet`, the provider's) is used only with `allowExecutorWallet: true`.
|
|
427
|
+
|
|
428
|
+
`error` is an `OperationMutationError`; switch on its `kind`:
|
|
429
|
+
|
|
430
|
+
| `kind` | Meaning |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| `busy` | A run of this hook is already in progress; nothing was started. |
|
|
433
|
+
| `not-ready` | No executor, or no wallet; nothing was prepared or signed. |
|
|
434
|
+
| `prepare-failed` | `prepare` threw; nothing was signed. |
|
|
435
|
+
| `wallet-rejected` | The user declined or closed the wallet prompt. |
|
|
436
|
+
| `wallet-feature-unsupported` | The wallet cannot sign without sending (no `solana:signTransaction`). |
|
|
437
|
+
| `wallet-chain-unsupported` | The wallet does not sign for the chain it was asked to. |
|
|
438
|
+
| `wallet-version-unsupported` | The wallet signs, but not this transaction version. |
|
|
439
|
+
| `wallet-modified` | The wallet changed the transaction, so it was not sent. |
|
|
440
|
+
| `not-submitted` | Failed before anything reached the chain. |
|
|
441
|
+
| `submitted-unknown` | Sent, but the confirmation did not arrive; check the chain before trying again. |
|
|
442
|
+
| `chain-failed` | Failed on chain; see `programError`. |
|
|
443
|
+
|
|
444
|
+
It also carries the transaction `failure`, `programError` and the signatures that confirmed before it. `toOperationMutationError(error)` classifies a failure from an imperative `session.execute` the same way.
|
|
445
|
+
|
|
446
|
+
### Wallet Standard wallets: `@usearete/react/wallet-standard`
|
|
447
|
+
|
|
448
|
+
`useWalletStandardWallet(account, { chain })` turns the account a user connected with `@wallet-standard/react` into the wallet Arete signs with. Sign only: the wallet signs and Arete relays the bytes through the client's or session's transaction transport. It needs `@usearete/adapter-kit` (with `@solana/kit`) and `@wallet-standard/ui-registry`, optional peers of this package.
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
import { useWalletStandardWallet } from '@usearete/react/wallet-standard';
|
|
452
|
+
|
|
453
|
+
const account = connectedWallet?.accounts[0] ?? null; // a UiWalletAccount
|
|
454
|
+
const { wallet, isPromptOpen, error } = useWalletStandardWallet(account, { chain: 'solana:mainnet' });
|
|
455
|
+
|
|
456
|
+
<AreteProvider wallet={wallet}>…</AreteProvider>;
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`wallet` is `undefined` until an account is connected, and when it cannot sign for Arete. `error` then says why: a `WalletStandardSignerError` (exported from this entry) whose `code` is
|
|
460
|
+
|
|
461
|
+
- `sign_transaction_unsupported`: the wallet lacks `solana:signTransaction` (it only signs and sends in one step);
|
|
462
|
+
- `chain_unsupported`: the account does not list `chain` (a devnet account asked for `solana:mainnet`);
|
|
463
|
+
- `account_unavailable`: the account could not be read, or its wallet is no longer registered.
|
|
464
|
+
|
|
465
|
+
A declined prompt is not an `error` here: it fails the run (`wallet-rejected`, with a `user_rejected` signer error in its `cause` chain) and the wallet stays usable. `isPromptOpen` is true while the wallet's approval prompt is open, and only for the current account. A raw `{ wallet, account }` pair from the Wallet Standard registry is accepted too.
|
|
466
|
+
|
|
467
|
+
With a session provider:
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
const { wallet, error } = useWalletStandardWallet(account, { chain: 'solana:mainnet' });
|
|
471
|
+
|
|
472
|
+
<AreteSessionProvider definition={SQUADS} options={{ auth }} wallet={wallet}>…</AreteSessionProvider>;
|
|
473
|
+
|
|
474
|
+
// below it
|
|
475
|
+
const propose = useOperationMutation(prepareProposal, { wallet });
|
|
476
|
+
if (error?.code === 'chain_unsupported') return <p>Switch your wallet to mainnet.</p>;
|
|
477
|
+
```
|
|
478
|
+
|
|
318
479
|
## Protocols
|
|
319
480
|
|
|
320
481
|
Protocol clients come from `@usearete/sdk` (`createProtocolClient(DEFINITION, { auth })` or `session.protocols.<key>`; see its README). Create one per key outside render (or memoize it) and pass it to these hooks:
|