create-partylayer-app 0.4.1 → 0.5.1

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.
Files changed (60) hide show
  1. package/dist/templates.d.ts.map +1 -1
  2. package/dist/templates.js +10 -0
  3. package/dist/templates.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/next-ssr/README.md +4 -4
  6. package/templates/next-ssr/_package.json +1 -1
  7. package/templates/react-dvp/README.md +134 -0
  8. package/templates/react-dvp/_gitignore +8 -0
  9. package/templates/react-dvp/_package.json +26 -0
  10. package/templates/react-dvp/index.html +12 -0
  11. package/templates/react-dvp/src/App.css +385 -0
  12. package/templates/react-dvp/src/App.tsx +110 -0
  13. package/templates/react-dvp/src/context/DemoContext.tsx +34 -0
  14. package/templates/react-dvp/src/index.css +23 -0
  15. package/templates/react-dvp/src/lib/backend.ts +144 -0
  16. package/templates/react-dvp/src/lib/fixtures.ts +113 -0
  17. package/templates/react-dvp/src/lib/format.ts +59 -0
  18. package/templates/react-dvp/src/lib/invalidate.ts +18 -0
  19. package/templates/react-dvp/src/lib/match.ts +28 -0
  20. package/templates/react-dvp/src/lib/mutation.ts +17 -0
  21. package/templates/react-dvp/src/lib/store.ts +332 -0
  22. package/templates/react-dvp/src/lib/types.ts +35 -0
  23. package/templates/react-dvp/src/main.tsx +18 -0
  24. package/templates/react-dvp/src/sections/Holdings.tsx +51 -0
  25. package/templates/react-dvp/src/sections/MyAllocations.tsx +79 -0
  26. package/templates/react-dvp/src/sections/Trades.tsx +298 -0
  27. package/templates/react-dvp/src/ui/primitives.tsx +91 -0
  28. package/templates/react-dvp/tsconfig.json +20 -0
  29. package/templates/react-dvp/vite.config.ts +7 -0
  30. package/templates/react-tokenization/README.md +139 -0
  31. package/templates/react-tokenization/_gitignore +8 -0
  32. package/templates/react-tokenization/_package.json +26 -0
  33. package/templates/react-tokenization/index.html +12 -0
  34. package/templates/react-tokenization/src/App.css +361 -0
  35. package/templates/react-tokenization/src/App.tsx +111 -0
  36. package/templates/react-tokenization/src/context/DemoContext.tsx +34 -0
  37. package/templates/react-tokenization/src/index.css +23 -0
  38. package/templates/react-tokenization/src/lib/backend.ts +171 -0
  39. package/templates/react-tokenization/src/lib/fixtures.ts +134 -0
  40. package/templates/react-tokenization/src/lib/format.ts +59 -0
  41. package/templates/react-tokenization/src/lib/invalidate.ts +23 -0
  42. package/templates/react-tokenization/src/lib/mutation.ts +17 -0
  43. package/templates/react-tokenization/src/lib/store.ts +219 -0
  44. package/templates/react-tokenization/src/lib/types.ts +27 -0
  45. package/templates/react-tokenization/src/main.tsx +18 -0
  46. package/templates/react-tokenization/src/sections/Allocations.tsx +150 -0
  47. package/templates/react-tokenization/src/sections/Holdings.tsx +51 -0
  48. package/templates/react-tokenization/src/sections/Incoming.tsx +107 -0
  49. package/templates/react-tokenization/src/sections/Issuer.tsx +177 -0
  50. package/templates/react-tokenization/src/sections/Transfer.tsx +112 -0
  51. package/templates/react-tokenization/src/ui/primitives.tsx +91 -0
  52. package/templates/react-tokenization/tsconfig.json +20 -0
  53. package/templates/react-tokenization/vite.config.ts +7 -0
  54. package/templates/react-vite/README.md +2 -2
  55. package/templates/react-vite/_package.json +1 -1
  56. package/templates/vanilla/README.md +2 -2
  57. package/templates/vanilla/_package.json +1 -1
  58. package/templates/vanilla/src/main.ts +2 -2
  59. package/templates/vue-nuxt-pinia/README.md +3 -3
  60. package/templates/vue-nuxt-pinia/_package.json +1 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Trades section (the DvP core). Role-aware over the same allocation requests.
3
+ *
4
+ * Counterparties (Alice, Bob) see the legs they send: each has an "Allocate my leg"
5
+ * action (CostPreview then useAllocationInstruction) and a "Reject" action
6
+ * (useAllocationRequestAction, action 'reject', actor = the current party).
7
+ *
8
+ * The venue sees a "New trade" form, per-leg allocation status (matched or missing
9
+ * via the expected-spec comparator over the counterparties' allocations), an atomic
10
+ * "Settle" (the generic useChoice; enabled only when every leg is matched), a
11
+ * "Withdraw trade" (useAllocationRequestAction, action 'withdraw'), and a per-matched
12
+ * -leg "Cancel allocation" (useAllocationAction, action 'cancel') for the abort path.
13
+ */
14
+ import { useState } from 'react';
15
+ import { useQueryClient } from '@tanstack/react-query';
16
+ import {
17
+ useAllocationRequests,
18
+ useTokenAllocations,
19
+ useAllocationInstruction,
20
+ useAllocationAction,
21
+ useAllocationRequestAction,
22
+ useChoice,
23
+ type AllocationInstructionRequest,
24
+ } from '@partylayer/react/query';
25
+ import { CostPreview, TransactionToast } from '@partylayer/react';
26
+ import { useDemo, partyKey } from '../context/DemoContext';
27
+ import { Card, AsyncView, Badge, Field } from '../ui/primitives';
28
+ import { toastStatus } from '../lib/mutation';
29
+ import { invalidateAll } from '../lib/invalidate';
30
+ import { allocationForLeg, matchedLegIds } from '../lib/match';
31
+ import { formatAmount, isPositiveAmount } from '../lib/format';
32
+ import { PARTIES, REGISTRY, FEE_ESTIMATE } from '../lib/fixtures';
33
+ import { demoStore } from '../lib/store';
34
+ import type { SettleTrade, CreateTrade } from '../lib/types';
35
+
36
+ export function Trades() {
37
+ const { party } = useDemo();
38
+ return party === 'venue' ? <VenueTrades /> : <CounterpartyTrades />;
39
+ }
40
+
41
+ function CounterpartyTrades() {
42
+ const { party, backend } = useDemo();
43
+ const queryClient = useQueryClient();
44
+ const me = PARTIES[party].partyId;
45
+ const [allocating, setAllocating] = useState<string | null>(null);
46
+
47
+ const trades = useAllocationRequests({
48
+ read: (signal) => backend.readTrades(signal),
49
+ key: partyKey('trades', party),
50
+ });
51
+
52
+ const allocate = useAllocationInstruction<{ ok: true }>({
53
+ submit: (request, signal) => backend.submitAllocation(request, signal),
54
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
55
+ });
56
+
57
+ const reject = useAllocationRequestAction<{ ok: true }>({
58
+ submit: (request, signal) => backend.submitRequestAction(request, signal),
59
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
60
+ });
61
+
62
+ const doAllocate = (requestCid: string, legId: string) => {
63
+ const trade = trades.requests?.find((t) => t.cid === requestCid);
64
+ if (!trade) return;
65
+ const leg = trade.request.transferLegs[legId];
66
+ const request: AllocationInstructionRequest = {
67
+ // SECURITY: expectedAdmin comes from a trusted source (here the registry
68
+ // constant); the choice checks the factory's admin against it.
69
+ expectedAdmin: REGISTRY,
70
+ allocation: {
71
+ settlement: trade.request.settlement,
72
+ transferLegId: legId,
73
+ transferLeg: leg,
74
+ },
75
+ requestedAt: new Date().toISOString(),
76
+ inputHoldingCids: demoStore.unlockedCids(party, leg.instrumentId.admin, leg.instrumentId.id),
77
+ meta: {},
78
+ };
79
+ setAllocating(requestCid + ':' + legId);
80
+ allocate.submitAllocation(request);
81
+ };
82
+
83
+ return (
84
+ <Card title="Trades" hint="useAllocationRequests + useAllocationInstruction + useAllocationRequestAction">
85
+ <AsyncView
86
+ isPending={trades.isPending}
87
+ error={trades.error}
88
+ data={trades.requests}
89
+ isEmpty={(items) => items.length === 0}
90
+ empty="No open trades."
91
+ >
92
+ {(items) => (
93
+ <ul className="list">
94
+ {items.map((trade) => {
95
+ const myLegs = Object.entries(trade.request.transferLegs).filter(
96
+ ([, leg]) => leg.sender === me,
97
+ );
98
+ return (
99
+ <li key={trade.cid} className="row row-block">
100
+ <div className="row-main">
101
+ <div className="row-title">
102
+ Trade {trade.request.settlement.settlementRef.id}{' '}
103
+ <span className="muted">executor {trade.request.settlement.executor}</span>
104
+ </div>
105
+ {Object.entries(trade.request.transferLegs).map(([legId, leg]) => (
106
+ <div key={legId} className="row-sub muted">
107
+ {legId}: {formatAmount(leg.amount)} {leg.instrumentId.id} from {leg.sender} to {leg.receiver}
108
+ </div>
109
+ ))}
110
+ </div>
111
+ {myLegs.length > 0 ? (
112
+ <div className="leg-actions">
113
+ {myLegs.map(([legId, leg]) => (
114
+ <div key={legId} className="leg-action">
115
+ <div className="row-sub">
116
+ Your leg <Badge tone="neutral">{legId}</Badge>{' '}
117
+ {formatAmount(leg.amount)} {leg.instrumentId.id}
118
+ </div>
119
+ <CostPreview estimate={FEE_ESTIMATE} />
120
+ <div className="row-actions">
121
+ <button
122
+ className="btn btn-primary btn-small"
123
+ disabled={allocate.isPending}
124
+ onClick={() => doAllocate(trade.cid, legId)}
125
+ >
126
+ Allocate my leg
127
+ </button>
128
+ <button
129
+ className="btn btn-ghost btn-small"
130
+ disabled={reject.isPending}
131
+ onClick={() => reject.submitAction({ requestCid: trade.cid, action: 'reject', actor: me })}
132
+ >
133
+ Reject
134
+ </button>
135
+ </div>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ ) : (
140
+ <div className="row-sub muted">No leg for you to allocate on this trade.</div>
141
+ )}
142
+ </li>
143
+ );
144
+ })}
145
+ </ul>
146
+ )}
147
+ </AsyncView>
148
+
149
+ <TransactionToast
150
+ status={toastStatus(allocate)}
151
+ error={allocate.error}
152
+ message={allocate.isSuccess ? 'Allocated ' + (allocating ?? '') + '.' : undefined}
153
+ />
154
+ <TransactionToast
155
+ status={toastStatus(reject)}
156
+ error={reject.error}
157
+ message={reject.isSuccess ? 'Trade rejected.' : undefined}
158
+ />
159
+ </Card>
160
+ );
161
+ }
162
+
163
+ function VenueTrades() {
164
+ const { party, backend } = useDemo();
165
+ const queryClient = useQueryClient();
166
+
167
+ const trades = useAllocationRequests({
168
+ read: (signal) => backend.readTrades(signal),
169
+ key: partyKey('trades', party),
170
+ });
171
+ // The venue matches allocations against trade legs from the counterparties'
172
+ // allocation reads (party-scoped keys), then compares with the app comparator.
173
+ const aliceAllocs = useTokenAllocations({
174
+ read: (signal) => backend.readAllocations('alice', signal),
175
+ key: partyKey('allocations', 'alice'),
176
+ });
177
+ const bobAllocs = useTokenAllocations({
178
+ read: (signal) => backend.readAllocations('bob', signal),
179
+ key: partyKey('allocations', 'bob'),
180
+ });
181
+ const combined = [...(aliceAllocs.allocations ?? []), ...(bobAllocs.allocations ?? [])];
182
+
183
+ const createTrade = useChoice<{ ok: true }, CreateTrade>({
184
+ exercise: (vars) => backend.submitCreateTrade(vars),
185
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
186
+ });
187
+ const settle = useChoice<{ ok: true }, SettleTrade>({
188
+ exercise: (vars) => backend.submitSettle(vars),
189
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
190
+ });
191
+ const requestAction = useAllocationRequestAction<{ ok: true }>({
192
+ submit: (request, signal) => backend.submitRequestAction(request, signal),
193
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
194
+ });
195
+ const cancel = useAllocationAction<{ ok: true }>({
196
+ submit: (request, signal) => backend.submitAllocationAction(request, signal),
197
+ mutation: { onSuccess: () => invalidateAll(queryClient) },
198
+ });
199
+
200
+ const [usdAmount, setUsdAmount] = useState('100.00');
201
+ const [bondAmount, setBondAmount] = useState('5.00');
202
+ const busy = createTrade.isPending || settle.isPending || requestAction.isPending || cancel.isPending;
203
+
204
+ return (
205
+ <Card title="Trades" hint="useAllocationRequests + useTokenAllocations + useChoice + request/allocation actions">
206
+ <div className="form-grid">
207
+ <Field label="USD amount (Alice pays)">
208
+ <input inputMode="decimal" value={usdAmount} onChange={(e) => setUsdAmount(e.target.value)} />
209
+ </Field>
210
+ <Field label="BOND amount (Bob delivers)">
211
+ <input inputMode="decimal" value={bondAmount} onChange={(e) => setBondAmount(e.target.value)} />
212
+ </Field>
213
+ <div className="field field-action">
214
+ <button
215
+ className="btn btn-primary"
216
+ disabled={busy || !isPositiveAmount(usdAmount) || !isPositiveAmount(bondAmount)}
217
+ onClick={() => createTrade.exerciseChoice({ usdAmount, bondAmount })}
218
+ >
219
+ New trade
220
+ </button>
221
+ </div>
222
+ </div>
223
+
224
+ <AsyncView
225
+ isPending={trades.isPending}
226
+ error={trades.error}
227
+ data={trades.requests}
228
+ isEmpty={(items) => items.length === 0}
229
+ empty="No open trades."
230
+ >
231
+ {(items) => (
232
+ <ul className="list">
233
+ {items.map((trade) => {
234
+ const legIds = Object.keys(trade.request.transferLegs);
235
+ const matched = matchedLegIds(trade.request, combined);
236
+ const allMatched = matched.length === legIds.length;
237
+ return (
238
+ <li key={trade.cid} className="row row-block">
239
+ <div className="row-main">
240
+ <div className="row-title">
241
+ Trade {trade.request.settlement.settlementRef.id}{' '}
242
+ <Badge tone={allMatched ? 'ok' : 'neutral'}>
243
+ {matched.length}/{legIds.length} allocated
244
+ </Badge>
245
+ </div>
246
+ {legIds.map((legId) => {
247
+ const leg = trade.request.transferLegs[legId];
248
+ const alloc = allocationForLeg(trade.request, legId, combined);
249
+ return (
250
+ <div key={legId} className="row-sub muted leg-status">
251
+ <span>
252
+ {legId}: {formatAmount(leg.amount)} {leg.instrumentId.id}{' '}
253
+ {alloc ? <Badge tone="ok">matched</Badge> : <Badge tone="lock">missing</Badge>}
254
+ </span>
255
+ {alloc ? (
256
+ <button
257
+ className="btn btn-ghost btn-small"
258
+ disabled={busy}
259
+ onClick={() => cancel.submitAction({ allocationCid: alloc.cid, action: 'cancel' })}
260
+ >
261
+ Cancel allocation
262
+ </button>
263
+ ) : null}
264
+ </div>
265
+ );
266
+ })}
267
+ </div>
268
+ <div className="row-actions">
269
+ <button
270
+ className="btn btn-primary btn-small"
271
+ disabled={busy || !allMatched}
272
+ title={allMatched ? 'Settle atomically' : 'All legs must be allocated first'}
273
+ onClick={() => settle.exerciseChoice({ requestCid: trade.cid })}
274
+ >
275
+ Settle
276
+ </button>
277
+ <button
278
+ className="btn btn-ghost btn-small"
279
+ disabled={busy}
280
+ onClick={() => requestAction.submitAction({ requestCid: trade.cid, action: 'withdraw' })}
281
+ >
282
+ Withdraw trade
283
+ </button>
284
+ </div>
285
+ </li>
286
+ );
287
+ })}
288
+ </ul>
289
+ )}
290
+ </AsyncView>
291
+
292
+ <TransactionToast status={toastStatus(createTrade)} error={createTrade.error} message={createTrade.isSuccess ? 'Trade created.' : undefined} />
293
+ <TransactionToast status={toastStatus(settle)} error={settle.error} message={settle.isSuccess ? 'Settled atomically. Balances swapped.' : undefined} />
294
+ <TransactionToast status={toastStatus(requestAction)} error={requestAction.error} message={requestAction.isSuccess ? 'Trade withdrawn.' : undefined} />
295
+ <TransactionToast status={toastStatus(cancel)} error={cancel.error} message={cancel.isSuccess ? 'Allocation cancelled.' : undefined} />
296
+ </Card>
297
+ );
298
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Small presentational helpers for the demo chrome. These are the APP's own UI
3
+ * (cards, skeletons, badges, states), distinct from the themed PartyLayer
4
+ * primitives (ConnectButton, PartyAvatar, CostPreview, TransactionToast) which the
5
+ * sections import from `@partylayer/react`.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+
9
+ export function Card({ title, hint, children }: { title: string; hint?: string; children: ReactNode }) {
10
+ return (
11
+ <section className="card">
12
+ <header className="card-head">
13
+ <h2>{title}</h2>
14
+ {hint ? <span className="card-hint">{hint}</span> : null}
15
+ </header>
16
+ <div className="card-body">{children}</div>
17
+ </section>
18
+ );
19
+ }
20
+
21
+ export function Skeleton({ rows = 3 }: { rows?: number }) {
22
+ return (
23
+ <div className="skeleton" aria-hidden="true">
24
+ {Array.from({ length: rows }).map((_, i) => (
25
+ <div key={i} className="skeleton-row" />
26
+ ))}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export function ErrorState({ error }: { error: Error }) {
32
+ return (
33
+ <div className="state state-error" role="alert">
34
+ <strong>Something went wrong.</strong>
35
+ <span>{error.message}</span>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function EmptyState({ children }: { children: ReactNode }) {
41
+ return <div className="state state-empty">{children}</div>;
42
+ }
43
+
44
+ export function Badge({
45
+ tone = 'neutral',
46
+ title,
47
+ children,
48
+ }: {
49
+ tone?: 'neutral' | 'lock' | 'ok';
50
+ title?: string;
51
+ children: ReactNode;
52
+ }) {
53
+ return (
54
+ <span className={'badge badge-' + tone} title={title}>
55
+ {children}
56
+ </span>
57
+ );
58
+ }
59
+
60
+ export function Field({ label, children }: { label: string; children: ReactNode }) {
61
+ return (
62
+ <label className="field">
63
+ <span className="field-label">{label}</span>
64
+ {children}
65
+ </label>
66
+ );
67
+ }
68
+
69
+ /** A view of the standard read-hook state: pick loading, error, empty, or content. */
70
+ export function AsyncView<T>({
71
+ isPending,
72
+ error,
73
+ data,
74
+ isEmpty,
75
+ empty,
76
+ children,
77
+ rows,
78
+ }: {
79
+ isPending: boolean;
80
+ error: Error | null;
81
+ data: T | null | undefined;
82
+ isEmpty: (data: T) => boolean;
83
+ empty: ReactNode;
84
+ children: (data: T) => ReactNode;
85
+ rows?: number;
86
+ }) {
87
+ if (isPending) return <Skeleton rows={rows} />;
88
+ if (error) return <ErrorState error={error} />;
89
+ if (data == null || isEmpty(data)) return <EmptyState>{empty}</EmptyState>;
90
+ return <>{children(data)}</>;
91
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src", "vite.config.ts"]
20
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ });
@@ -0,0 +1,139 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A Tokenization vertical for Canton, scaffolded with create-partylayer-app. A
4
+ single-page dApp that composes the full CIP-0056 (Canton Token Standard) helper
5
+ surface from `@partylayer/react`, the UI primitives, and the theme families against
6
+ a typed demo backend.
7
+
8
+ Everything runs on an in-memory demo backend (typed fixtures behind a small
9
+ `TokenizationBackend` interface), so every hook is exercised for real (loading,
10
+ success, error, invalidation) without a live ledger. Model 2 throughout: PartyLayer
11
+ owns none of the transport; the dApp supplies every read and submit fetcher, and the
12
+ hooks type the request and wrap the query or mutation.
13
+
14
+ > Reference quality, NOT production audited. This template demonstrates the API in a
15
+ > realistic composition. Review, test, and harden it before using it in production.
16
+
17
+ ## Run it
18
+
19
+ ```bash
20
+ npm create partylayer-app@latest my-app -- --template react-tokenization
21
+ cd my-app
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ ## What is on screen
27
+
28
+ A header with the connect UI, a demo-party switcher (issuer / alice / bob), a
29
+ synchronizer picker, and a light/dark toggle; then five section cards:
30
+
31
+ | Section | Hook(s) exercised |
32
+ | :-- | :-- |
33
+ | Holdings | `useTokenHoldings` |
34
+ | Transfer | `useTransferInstruction` (plus `CostPreview` and `TransactionToast`) |
35
+ | Incoming | `useTransferInstructions` and `useTransferInstructionAction` |
36
+ | Issuer | `useDamlContract` (instrument, supply, refs) and `useChoice` (mint, freeze) |
37
+ | Allocations | `useTokenAllocations`, `useAllocationInstruction`, `useAllocationAction` |
38
+
39
+ All seven CIP-0056 hooks are mounted and exercised, plus the two generic hooks
40
+ (`useDamlContract`, `useChoice`) for the instrument reads and registry-specific
41
+ writes the typed hooks do not cover.
42
+
43
+ ## Demo flows
44
+
45
+ 1. Connect. Click Connect Wallet in the header. The console adapter is registered, so
46
+ the wallet modal renders themed by the teal family. The sections below are driven
47
+ by the demo-party switcher, not the connection.
48
+ 2. View holdings with a frozen lock. As Alice, the Holdings card shows two holdings;
49
+ one carries a lock and renders a "Locked: frozen by issuer" badge.
50
+ 3. Transfer with a cost preview and a toast. In Transfer, pick a receiver and an
51
+ amount. A `CostPreview` shows the fee before you confirm; confirming drives a
52
+ `TransactionToast` from pending to success. The transfer initiates a pending
53
+ instruction and debits the sender; both sides refresh after invalidation.
54
+ 4. Accept the incoming transfer. Switch the demo party to the receiver (Bob). The
55
+ Incoming card shows the pending transfer. Accept and Reject render only while the
56
+ instruction is in the `pendingReceiverAcceptance` state (spec-accurate: those
57
+ choices are unavailable in the internal-workflow state, which instead shows the
58
+ `pendingActions` signal). Accept completes it and credits the receiver, and the
59
+ pending item disappears.
60
+ 5. Issuer mints. Switch to Issuer, enter a party and an amount under Mint. The total
61
+ supply and the target party's holdings update.
62
+ 6. Issuer freezes. As Issuer, pick a target party and toggle Freeze or Unfreeze on a
63
+ holding; the lock badge appears and the holding becomes unspendable in transfers.
64
+ 7. Error path. As any party, try to transfer more than the unlocked balance. The
65
+ submit rejects and the toast renders the error message.
66
+
67
+ ## Party-scoped caching
68
+
69
+ Every hook call folds the current demo party into its `key` (for example
70
+ `key: ['tokenization', 'holdings', party]`), so each party's data is cached
71
+ independently. Switching parties never shows another party's cached data: a party you
72
+ have not loaded shows a loading skeleton, and a party you have loaded shows its own
73
+ data.
74
+
75
+ Note on invalidation: the hooks nest your `key` inside their own key factory, so the
76
+ real TanStack query key is `partyLayerKeys.tokenHoldings({ key })`, not the raw `key`
77
+ you passed. To invalidate after a write, import `partyLayerKeys` from
78
+ `@partylayer/react/query` and match on the factory (see `src/lib/invalidate.ts`).
79
+
80
+ ## Decimal handling
81
+
82
+ Amounts are decimal strings end to end (never JS numbers), matching the CIP-0056
83
+ `Decimal` convention. The demo store simplifies arithmetic to two decimal places
84
+ using bigint cents (`src/lib/format.ts`); a real dApp handling arbitrary precision
85
+ should use a decimal library instead of this two-place shortcut.
86
+
87
+ ## Real mode
88
+
89
+ To wire this against a live validator and registry, replace the demo backend's
90
+ fetchers. The hooks and the UI stay exactly as they are; only the fetchers change.
91
+
92
+ Reads (holdings, instructions, allocations). Query your validator's active-contracts
93
+ (ACS) endpoint with an interface filter and map each contract into the typed
94
+ `{ cid, view }` ref the read hook returns (the `cid` is the ACS contract id, needed
95
+ for the write requests):
96
+
97
+ - holdings: interface filter `Splice.Api.Token.HoldingV1:Holding`, map into
98
+ `TokenHoldingRef` (`{ cid, holding }`); the `cid` feeds `inputHoldingCids`;
99
+ - instructions: interface filter
100
+ `Splice.Api.Token.TransferInstructionV1:TransferInstruction`, map into
101
+ `TokenTransferInstructionRef` (`{ cid, instruction }`); the `cid` feeds
102
+ `TransferInstructionActionRequest.instructionCid`. Discriminate
103
+ `instruction.status.kind`: only `pendingReceiverAcceptance` allows Accept and
104
+ Reject; `pendingInternalWorkflow` carries `pendingActions` (which party could act
105
+ to advance the transfer);
106
+ - allocations: interface filter `Splice.Api.Token.AllocationV1:Allocation`, map into
107
+ `TokenAllocationRef` (`{ cid, allocation }`); the `cid` feeds
108
+ `AllocationActionRequest.allocationCid`.
109
+
110
+ Transfer initiation (`useTransferInstruction`). The registry flow is off-ledger and
111
+ not standardized, so it lives in your `submit` fetcher: gather the sender's
112
+ `inputHoldingCids`, POST the registry's transfer-instruction transfer-factory
113
+ endpoint to get the `factoryId`, `choiceContext`, and `disclosedContracts`, fill
114
+ `extraArgs.context`, then exercise `TransferFactory_Transfer` on the factory with the
115
+ `disclosedContracts`.
116
+
117
+ Transfer completion (`useTransferInstructionAction`). For accept, reject, or
118
+ withdraw, fetch the per-action choice context from the registry, fill `extraArgs`,
119
+ and exercise the choice on the instruction contract id with the `disclosedContracts`.
120
+
121
+ Allocation create and act (`useAllocationInstruction`, `useAllocationAction`). Mirror
122
+ the transfer flow against the registry's allocation-instruction factory endpoint and
123
+ the per-action choice contexts for `Allocation_ExecuteTransfer`, `Allocation_Cancel`,
124
+ and `Allocation_Withdraw`.
125
+
126
+ For the shapes these fetchers pass around, `@partylayer/react/query` exports the
127
+ disclosed contract and choice context types (`TokenDisclosedContract`,
128
+ `TokenChoiceContext`, `TokenTransferFactory`, `TokenAllocationFactory`) plus
129
+ `mergeDisclosedContracts` for combining the disclosures of several registry contexts
130
+ into one submission.
131
+
132
+ Issuer writes (mint, freeze). These are registry-specific admin operations, shown
133
+ here through the generic `useChoice` escape hatch. Real registries typically expose
134
+ issuance through the standard's `BurnMintV1` (`BurnMintFactory_BurnMint`, which
135
+ returns `outputCids`) or a custom admin choice; the exact choice fields are
136
+ registry-defined.
137
+
138
+ The official Splice token-standard CLI is the canonical reference for these registry
139
+ flows.
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ dist-ssr
4
+ *.local
5
+ .DS_Store
6
+ .env
7
+ .env.*
8
+ !.env.example
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@partylayer/react": "^2.2.1",
13
+ "@partylayer/sdk": "^0.16.0",
14
+ "@partylayer/adapter-console": "^0.3.15",
15
+ "@tanstack/react-query": "^5.0.0",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^18.3.3",
21
+ "@types/react-dom": "^18.3.0",
22
+ "@vitejs/plugin-react": "^4.3.1",
23
+ "typescript": "^5.6.2",
24
+ "vite": "^5.4.2"
25
+ }
26
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{PROJECT_NAME}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>