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,361 @@
1
+ /* App chrome. Two token sets follow the light/dark toggle; the PartyLayer
2
+ primitives retint from the teal theme independently. */
3
+ .app {
4
+ --bg: #ffffff;
5
+ --surface: #f0fdfa;
6
+ --card: #ffffff;
7
+ --text: #0f1e1c;
8
+ --muted: #5f6b6a;
9
+ --border: rgba(19, 78, 74, 0.14);
10
+ --accent: #0d9488;
11
+ --accent-contrast: #ffffff;
12
+ --error-bg: #fef2f2;
13
+ --error-text: #b00020;
14
+ --lock-bg: #fff7ed;
15
+ --lock-text: #9a3412;
16
+ min-height: 100vh;
17
+ background: var(--bg);
18
+ color: var(--text);
19
+ transition: background 200ms, color 200ms;
20
+ }
21
+
22
+ .app-dark {
23
+ --bg: #0a1414;
24
+ --surface: #0f1d1d;
25
+ --card: #122423;
26
+ --text: #e6fbf6;
27
+ --muted: #8fa8a6;
28
+ --border: rgba(255, 255, 255, 0.1);
29
+ --accent: #2dd4bf;
30
+ --accent-contrast: #0a1414;
31
+ --error-bg: #2a0f10;
32
+ --error-text: #fca5a5;
33
+ --lock-bg: #2a1c0f;
34
+ --lock-text: #fbbf24;
35
+ }
36
+
37
+ .topbar {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ gap: 16px;
42
+ flex-wrap: wrap;
43
+ padding: 16px 24px;
44
+ border-bottom: 1px solid var(--border);
45
+ }
46
+
47
+ .brand {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 10px;
51
+ }
52
+ .brand-mark {
53
+ width: 28px;
54
+ height: 28px;
55
+ border-radius: 8px;
56
+ background: var(--accent);
57
+ display: inline-block;
58
+ }
59
+ .brand-title {
60
+ font-weight: 700;
61
+ letter-spacing: -0.01em;
62
+ }
63
+ .brand-sub {
64
+ font-size: 12px;
65
+ color: var(--muted);
66
+ }
67
+
68
+ .topbar-controls {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 12px;
72
+ flex-wrap: wrap;
73
+ }
74
+
75
+ .party-switch {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ padding: 4px 8px;
80
+ border: 1px solid var(--border);
81
+ border-radius: 10px;
82
+ }
83
+ .party-switch-label {
84
+ font-size: 11px;
85
+ text-transform: uppercase;
86
+ letter-spacing: 0.06em;
87
+ color: var(--muted);
88
+ margin-right: 2px;
89
+ }
90
+ .party-chip {
91
+ border: none;
92
+ background: transparent;
93
+ color: var(--muted);
94
+ padding: 4px 10px;
95
+ border-radius: 8px;
96
+ cursor: pointer;
97
+ font-size: 13px;
98
+ font-weight: 600;
99
+ }
100
+ .party-chip-on {
101
+ background: var(--accent);
102
+ color: var(--accent-contrast);
103
+ }
104
+
105
+ .acting-line {
106
+ max-width: 1100px;
107
+ margin: 16px auto 0;
108
+ padding: 0 24px;
109
+ color: var(--muted);
110
+ font-size: 14px;
111
+ }
112
+ .acting-line code {
113
+ font-size: 12px;
114
+ }
115
+
116
+ .grid {
117
+ max-width: 1100px;
118
+ margin: 16px auto 0;
119
+ padding: 0 24px 40px;
120
+ display: grid;
121
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
122
+ gap: 20px;
123
+ }
124
+
125
+ .card {
126
+ background: var(--card);
127
+ border: 1px solid var(--border);
128
+ border-radius: 14px;
129
+ overflow: hidden;
130
+ }
131
+ .card-head {
132
+ display: flex;
133
+ align-items: baseline;
134
+ justify-content: space-between;
135
+ gap: 10px;
136
+ padding: 14px 16px;
137
+ border-bottom: 1px solid var(--border);
138
+ }
139
+ .card-head h2 {
140
+ margin: 0;
141
+ font-size: 16px;
142
+ }
143
+ .card-hint {
144
+ font-size: 11px;
145
+ color: var(--muted);
146
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
147
+ }
148
+ .card-body {
149
+ padding: 16px;
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 14px;
153
+ }
154
+
155
+ .list {
156
+ list-style: none;
157
+ margin: 0;
158
+ padding: 0;
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 10px;
162
+ }
163
+ .row {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 12px;
167
+ padding: 10px 12px;
168
+ border: 1px solid var(--border);
169
+ border-radius: 10px;
170
+ }
171
+ .row-block {
172
+ flex-direction: column;
173
+ align-items: stretch;
174
+ gap: 10px;
175
+ }
176
+ .row-block .row-actions {
177
+ justify-content: flex-start;
178
+ }
179
+ .row-main {
180
+ flex: 1;
181
+ min-width: 0;
182
+ }
183
+ .row-title {
184
+ font-weight: 600;
185
+ }
186
+ .row-sub {
187
+ font-size: 12px;
188
+ word-break: break-all;
189
+ }
190
+ .row-actions {
191
+ display: flex;
192
+ gap: 8px;
193
+ flex-wrap: wrap;
194
+ }
195
+
196
+ .muted {
197
+ color: var(--muted);
198
+ }
199
+
200
+ .badge {
201
+ font-size: 11px;
202
+ font-weight: 600;
203
+ padding: 3px 8px;
204
+ border-radius: 999px;
205
+ white-space: nowrap;
206
+ }
207
+ .badge-neutral {
208
+ background: var(--surface);
209
+ color: var(--muted);
210
+ }
211
+ .badge-lock {
212
+ background: var(--lock-bg);
213
+ color: var(--lock-text);
214
+ }
215
+ .badge-ok {
216
+ background: var(--surface);
217
+ color: var(--accent);
218
+ }
219
+
220
+ .form-grid {
221
+ display: grid;
222
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
223
+ gap: 12px;
224
+ align-items: end;
225
+ }
226
+ .field-action .btn {
227
+ width: 100%;
228
+ }
229
+ .field {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 4px;
233
+ }
234
+ .field-label {
235
+ font-size: 12px;
236
+ color: var(--muted);
237
+ }
238
+ .field-action {
239
+ justify-content: flex-end;
240
+ }
241
+ .field input,
242
+ .field select,
243
+ .topbar-controls select {
244
+ padding: 8px 10px;
245
+ border: 1px solid var(--border);
246
+ border-radius: 8px;
247
+ background: var(--bg);
248
+ color: var(--text);
249
+ font-size: 14px;
250
+ }
251
+
252
+ .btn {
253
+ border: 1px solid transparent;
254
+ border-radius: 8px;
255
+ padding: 8px 14px;
256
+ font-size: 14px;
257
+ font-weight: 600;
258
+ cursor: pointer;
259
+ }
260
+ .btn:disabled {
261
+ opacity: 0.55;
262
+ cursor: not-allowed;
263
+ }
264
+ .btn-primary {
265
+ background: var(--accent);
266
+ color: var(--accent-contrast);
267
+ }
268
+ .btn-ghost {
269
+ background: transparent;
270
+ color: var(--text);
271
+ border-color: var(--border);
272
+ }
273
+ .btn-small {
274
+ padding: 5px 10px;
275
+ font-size: 13px;
276
+ }
277
+
278
+ .review {
279
+ display: flex;
280
+ flex-direction: column;
281
+ gap: 12px;
282
+ padding-top: 4px;
283
+ }
284
+ .review-line,
285
+ .balance-line,
286
+ .hint-line {
287
+ font-size: 14px;
288
+ }
289
+
290
+ .state {
291
+ padding: 16px;
292
+ border-radius: 10px;
293
+ font-size: 14px;
294
+ display: flex;
295
+ flex-direction: column;
296
+ gap: 4px;
297
+ }
298
+ .state-empty {
299
+ background: var(--surface);
300
+ color: var(--muted);
301
+ }
302
+ .state-error {
303
+ background: var(--error-bg);
304
+ color: var(--error-text);
305
+ }
306
+
307
+ .skeleton {
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 10px;
311
+ }
312
+ .skeleton-row {
313
+ height: 40px;
314
+ border-radius: 10px;
315
+ background: linear-gradient(90deg, var(--surface), var(--border), var(--surface));
316
+ background-size: 200% 100%;
317
+ animation: shimmer 1.2s ease-in-out infinite;
318
+ }
319
+ @keyframes shimmer {
320
+ 0% {
321
+ background-position: 200% 0;
322
+ }
323
+ 100% {
324
+ background-position: -200% 0;
325
+ }
326
+ }
327
+
328
+ .issuer-summary {
329
+ display: flex;
330
+ align-items: flex-start;
331
+ justify-content: space-between;
332
+ gap: 12px;
333
+ flex-wrap: wrap;
334
+ }
335
+ .supply {
336
+ display: flex;
337
+ flex-direction: column;
338
+ align-items: flex-end;
339
+ gap: 2px;
340
+ }
341
+ .freeze {
342
+ display: flex;
343
+ flex-direction: column;
344
+ gap: 10px;
345
+ }
346
+ .alloc-head {
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: space-between;
350
+ gap: 10px;
351
+ flex-wrap: wrap;
352
+ }
353
+
354
+ .footer {
355
+ max-width: 1100px;
356
+ margin: 0 auto;
357
+ padding: 24px;
358
+ color: var(--muted);
359
+ font-size: 13px;
360
+ border-top: 1px solid var(--border);
361
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The Tokenization vertical example shell.
3
+ *
4
+ * PartyLayerKit provides the session + theme context (mirroring the react-vite
5
+ * template's setup), themed with the `teal` trading family and a light/dark toggle.
6
+ * The header carries the connect UI and a DEMO-PARTY switcher: switching the demo
7
+ * party changes whose data every section reads and who acts. It is app state,
8
+ * separate from the wallet session.
9
+ */
10
+ import { useState } from 'react';
11
+ import {
12
+ PartyLayerKit,
13
+ ConnectButton,
14
+ SynchronizerSwitcher,
15
+ themes,
16
+ type SynchronizerOption,
17
+ } from '@partylayer/react';
18
+ import { ConsoleAdapter } from '@partylayer/adapter-console';
19
+ import { DemoProvider } from './context/DemoContext';
20
+ import { demoBackend } from './lib/backend';
21
+ import { PARTIES, PARTY_ORDER } from './lib/fixtures';
22
+ import type { DemoPartyKey } from './lib/types';
23
+ import { Holdings } from './sections/Holdings';
24
+ import { Transfer } from './sections/Transfer';
25
+ import { Incoming } from './sections/Incoming';
26
+ import { Issuer } from './sections/Issuer';
27
+ import { Allocations } from './sections/Allocations';
28
+ import './App.css';
29
+
30
+ // The dev wallet adapter apps/demo uses, so the connect surface works in a demo
31
+ // context. The demo-party switcher, not this connection, drives the section data.
32
+ const ADAPTERS = [new ConsoleAdapter()];
33
+
34
+ const SYNCHRONIZERS: SynchronizerOption[] = [
35
+ { networkId: 'canton:da-devnet', label: 'DevNet' },
36
+ { networkId: 'canton:da-testnet', label: 'TestNet' },
37
+ ];
38
+
39
+ export default function App() {
40
+ const [mode, setMode] = useState<'light' | 'dark'>('light');
41
+ const [party, setParty] = useState<DemoPartyKey>('alice');
42
+ const [synchronizer, setSynchronizer] = useState('canton:da-devnet');
43
+
44
+ const theme = mode === 'dark' ? themes.teal.dark : themes.teal.light;
45
+
46
+ return (
47
+ <PartyLayerKit network="devnet" appName="PartyLayer Tokenization" theme={theme} adapters={ADAPTERS}>
48
+ <DemoProvider value={{ party, setParty, backend: demoBackend, mode }}>
49
+ <div className={'app app-' + mode}>
50
+ <header className="topbar">
51
+ <div className="brand">
52
+ <span className="brand-mark" aria-hidden="true" />
53
+ <div>
54
+ <div className="brand-title">Tokenization</div>
55
+ <div className="brand-sub">CIP-0056 vertical example</div>
56
+ </div>
57
+ </div>
58
+
59
+ <div className="topbar-controls">
60
+ <div className="party-switch" role="group" aria-label="demo party">
61
+ <span className="party-switch-label">demo party</span>
62
+ {PARTY_ORDER.map((p) => (
63
+ <button
64
+ key={p}
65
+ className={'party-chip' + (party === p ? ' party-chip-on' : '')}
66
+ onClick={() => setParty(p)}
67
+ >
68
+ {PARTIES[p].label}
69
+ </button>
70
+ ))}
71
+ </div>
72
+
73
+ <SynchronizerSwitcher
74
+ networkId={synchronizer}
75
+ options={SYNCHRONIZERS}
76
+ onSwitch={setSynchronizer}
77
+ />
78
+
79
+ <button
80
+ className="btn btn-ghost"
81
+ onClick={() => setMode((m) => (m === 'dark' ? 'light' : 'dark'))}
82
+ >
83
+ {mode === 'dark' ? 'Light' : 'Dark'}
84
+ </button>
85
+
86
+ <ConnectButton />
87
+ </div>
88
+ </header>
89
+
90
+ <p className="acting-line">
91
+ Acting as <strong>{PARTIES[party].label}</strong>{' '}
92
+ <code>{PARTIES[party].partyId}</code>. Every section below reads and acts as this party.
93
+ </p>
94
+
95
+ <main className="grid">
96
+ <Holdings />
97
+ <Transfer />
98
+ <Incoming />
99
+ <Issuer />
100
+ <Allocations />
101
+ </main>
102
+
103
+ <footer className="footer">
104
+ Demo backend, in-memory fixtures. Model 2: the dApp supplies every read and submit.
105
+ See the README for real-mode wiring against a live validator and registry.
106
+ </footer>
107
+ </div>
108
+ </DemoProvider>
109
+ </PartyLayerKit>
110
+ );
111
+ }
@@ -0,0 +1,34 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { DemoPartyKey } from '../lib/types';
3
+ import type { TokenizationBackend } from '../lib/backend';
4
+
5
+ /**
6
+ * The demo context threads the current demo party, the backend, and the theme mode
7
+ * to every section. The current party is app state (a demo-party switcher), SEPARATE
8
+ * from the wallet session: it selects whose data the sections read and who acts.
9
+ */
10
+ export interface DemoContextValue {
11
+ party: DemoPartyKey;
12
+ setParty: (party: DemoPartyKey) => void;
13
+ backend: TokenizationBackend;
14
+ mode: 'light' | 'dark';
15
+ }
16
+
17
+ const DemoContext = createContext<DemoContextValue | null>(null);
18
+
19
+ export const DemoProvider = DemoContext.Provider;
20
+
21
+ export function useDemo(): DemoContextValue {
22
+ const ctx = useContext(DemoContext);
23
+ if (!ctx) throw new Error('useDemo must be used within a DemoProvider.');
24
+ return ctx;
25
+ }
26
+
27
+ /**
28
+ * Party-scoped query key. Every hook folds the current party into its cache key so
29
+ * switching parties never shows another party's cached data. The `scope` names the
30
+ * section (holdings, incoming, ...).
31
+ */
32
+ export function partyKey(scope: string, party: DemoPartyKey): [string, string, DemoPartyKey] {
33
+ return ['tokenization', scope, party];
34
+ }
@@ -0,0 +1,23 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body,
9
+ #root {
10
+ margin: 0;
11
+ padding: 0;
12
+ min-height: 100%;
13
+ }
14
+
15
+ body {
16
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto,
17
+ 'Helvetica Neue', Arial, sans-serif;
18
+ line-height: 1.5;
19
+ }
20
+
21
+ button {
22
+ font-family: inherit;
23
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The backend interface the hooks read/submit through, plus a demo implementation
3
+ * against the in-memory store. Model 2: PartyLayer owns none of this; the dApp
4
+ * supplies these fetchers. In real mode each method becomes an ACS query or a
5
+ * registry-mediated command submission (see the README "Real mode" section).
6
+ */
7
+ import type {
8
+ TokenHoldingRef,
9
+ TokenTransfer,
10
+ TokenAllocationRef,
11
+ TokenTransferInstructionRef,
12
+ TransferInstructionActionRequest,
13
+ AllocationInstructionRequest,
14
+ AllocationActionRequest,
15
+ } from '@partylayer/react/query';
16
+ import type { DemoPartyKey, InstrumentConfig } from './types';
17
+ import { demoStore, latency } from './store';
18
+ import { INSTRUMENT, PARTIES } from './fixtures';
19
+
20
+ /** Issuer-only admin operations, exercised through the generic `useChoice`. */
21
+ export type IssuerChoice =
22
+ | { kind: 'mint'; toParty: DemoPartyKey; amount: string }
23
+ | { kind: 'setFrozen'; party: DemoPartyKey; cid: string; frozen: boolean };
24
+
25
+ export interface TokenizationBackend {
26
+ /** ACS read: a party's holdings as `{ cid, holding }` refs. `null` means none yet. */
27
+ readHoldings(party: DemoPartyKey, signal?: AbortSignal): Promise<TokenHoldingRef[] | null>;
28
+ /** ACS read: a party's pending incoming transfer instructions. `null` means none yet. */
29
+ readIncoming(party: DemoPartyKey, signal?: AbortSignal): Promise<TokenTransferInstructionRef[] | null>;
30
+ /**
31
+ * ACS read: a party's holdings refs for the issuer freeze panel, read through the
32
+ * generic `useDamlContract` (so that hook stays exercised alongside the typed ones).
33
+ */
34
+ readHoldingRefs(party: DemoPartyKey, signal?: AbortSignal): Promise<TokenHoldingRef[] | null>;
35
+ /** Read: the instrument configuration (generic contract read). */
36
+ readInstrument(signal?: AbortSignal): Promise<InstrumentConfig | null>;
37
+ /** Read: the total supply across parties (issuer summary). */
38
+ readSupply(signal?: AbortSignal): Promise<string | null>;
39
+ /** ACS read: the static allocations list as `{ cid, allocation }` refs. */
40
+ readAllocations(signal?: AbortSignal): Promise<TokenAllocationRef[] | null>;
41
+
42
+ /** Submit: initiate a transfer (creates a pending instruction for the receiver). */
43
+ submitTransfer(transfer: TokenTransfer, signal?: AbortSignal): Promise<{ ok: true }>;
44
+ /** Submit: a completion choice (accept/reject/withdraw) on a transfer instruction. */
45
+ submitTransferAction(
46
+ request: TransferInstructionActionRequest,
47
+ signal?: AbortSignal,
48
+ ): Promise<{ ok: true }>;
49
+ /** Submit: an issuer admin choice (mint/freeze), the registry-specific escape hatch. */
50
+ submitIssuerChoice(choice: IssuerChoice, signal?: AbortSignal): Promise<{ ok: true }>;
51
+ /** Submit: create an allocation via the factory (wired, fixture result). */
52
+ submitAllocation(request: AllocationInstructionRequest, signal?: AbortSignal): Promise<{ ok: true }>;
53
+ /** Submit: act on a funded allocation (executeTransfer/cancel/withdraw; fixture result). */
54
+ submitAllocationAction(request: AllocationActionRequest, signal?: AbortSignal): Promise<{ ok: true }>;
55
+ }
56
+
57
+ /** Reject if the caller aborted while we were "in flight". */
58
+ function throwIfAborted(signal?: AbortSignal): void {
59
+ if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
60
+ }
61
+
62
+ function partyIdToKey(partyId: string): DemoPartyKey {
63
+ for (const key of Object.keys(PARTIES) as DemoPartyKey[]) {
64
+ if (PARTIES[key].partyId === partyId) return key;
65
+ }
66
+ throw new Error('Unknown party id: ' + partyId);
67
+ }
68
+
69
+ export const demoBackend: TokenizationBackend = {
70
+ async readHoldings(party, signal) {
71
+ await latency();
72
+ throwIfAborted(signal);
73
+ return demoStore.holdingsOf(party);
74
+ },
75
+
76
+ async readIncoming(party, signal) {
77
+ await latency();
78
+ throwIfAborted(signal);
79
+ return demoStore.incomingOf(party);
80
+ },
81
+
82
+ async readHoldingRefs(party, signal) {
83
+ await latency();
84
+ throwIfAborted(signal);
85
+ return demoStore.holdingsOf(party);
86
+ },
87
+
88
+ async readInstrument(signal) {
89
+ await latency();
90
+ throwIfAborted(signal);
91
+ return INSTRUMENT;
92
+ },
93
+
94
+ async readSupply(signal) {
95
+ await latency();
96
+ throwIfAborted(signal);
97
+ return demoStore.totalSupply();
98
+ },
99
+
100
+ async readAllocations(signal) {
101
+ await latency();
102
+ throwIfAborted(signal);
103
+ return demoStore.allocations();
104
+ },
105
+
106
+ async submitTransfer(transfer, signal) {
107
+ await latency();
108
+ throwIfAborted(signal);
109
+ const memo = transfer.meta?.memo;
110
+ demoStore.initiateTransfer(transfer.sender, transfer.receiver, transfer.amount, memo);
111
+ return { ok: true };
112
+ },
113
+
114
+ async submitTransferAction(request, signal) {
115
+ await latency();
116
+ throwIfAborted(signal);
117
+ // The demo tracks the instruction on the receiver's list; find who holds it.
118
+ const receiverKey = findIncomingReceiver(request.instructionCid);
119
+ if (!receiverKey) throw new Error('Instruction not found: ' + request.instructionCid);
120
+ if (request.action === 'accept') {
121
+ demoStore.resolveIncoming(PARTIES[receiverKey].partyId, request.instructionCid, true);
122
+ } else {
123
+ // reject and withdraw both release the instruction; the demo refunds the sender.
124
+ demoStore.resolveIncoming(PARTIES[receiverKey].partyId, request.instructionCid, false);
125
+ }
126
+ return { ok: true };
127
+ },
128
+
129
+ // Mint and freeze are registry-specific admin operations. Real registries
130
+ // typically expose issuance through the standard's BurnMintV1
131
+ // (BurnMintFactory_BurnMint, which returns outputCids) or a custom admin choice;
132
+ // the exact choice fields are registry-defined and not asserted here. This demo
133
+ // shows the generic `useChoice` escape hatch for registry-specific writes
134
+ // alongside the typed CIP-0056 hooks.
135
+ async submitIssuerChoice(choice, signal) {
136
+ await latency();
137
+ throwIfAborted(signal);
138
+ if (choice.kind === 'mint') {
139
+ demoStore.mint(PARTIES[choice.toParty].partyId, choice.amount);
140
+ } else {
141
+ demoStore.setFrozen(choice.party, choice.cid, choice.frozen);
142
+ }
143
+ return { ok: true };
144
+ },
145
+
146
+ async submitAllocation(request, signal) {
147
+ await latency();
148
+ throwIfAborted(signal);
149
+ // Registry-specific in real mode; the demo just validates the sender is known.
150
+ partyIdToKey(request.allocation.transferLeg.sender);
151
+ return { ok: true };
152
+ },
153
+
154
+ async submitAllocationAction(request, signal) {
155
+ await latency();
156
+ throwIfAborted(signal);
157
+ // Registry-specific in real mode; the demo accepts any of the three actions.
158
+ if (!['executeTransfer', 'cancel', 'withdraw'].includes(request.action)) {
159
+ throw new Error('Unknown allocation action: ' + request.action);
160
+ }
161
+ return { ok: true };
162
+ },
163
+ };
164
+
165
+ /** Which demo party currently holds a pending instruction, by cid. */
166
+ function findIncomingReceiver(instructionCid: string): DemoPartyKey | null {
167
+ for (const key of Object.keys(PARTIES) as DemoPartyKey[]) {
168
+ if (demoStore.incomingOf(key).some((it) => it.cid === instructionCid)) return key;
169
+ }
170
+ return null;
171
+ }