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.
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +10 -0
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/templates/next-ssr/README.md +4 -4
- package/templates/next-ssr/_package.json +1 -1
- package/templates/react-dvp/README.md +134 -0
- package/templates/react-dvp/_gitignore +8 -0
- package/templates/react-dvp/_package.json +26 -0
- package/templates/react-dvp/index.html +12 -0
- package/templates/react-dvp/src/App.css +385 -0
- package/templates/react-dvp/src/App.tsx +110 -0
- package/templates/react-dvp/src/context/DemoContext.tsx +34 -0
- package/templates/react-dvp/src/index.css +23 -0
- package/templates/react-dvp/src/lib/backend.ts +144 -0
- package/templates/react-dvp/src/lib/fixtures.ts +113 -0
- package/templates/react-dvp/src/lib/format.ts +59 -0
- package/templates/react-dvp/src/lib/invalidate.ts +18 -0
- package/templates/react-dvp/src/lib/match.ts +28 -0
- package/templates/react-dvp/src/lib/mutation.ts +17 -0
- package/templates/react-dvp/src/lib/store.ts +332 -0
- package/templates/react-dvp/src/lib/types.ts +35 -0
- package/templates/react-dvp/src/main.tsx +18 -0
- package/templates/react-dvp/src/sections/Holdings.tsx +51 -0
- package/templates/react-dvp/src/sections/MyAllocations.tsx +79 -0
- package/templates/react-dvp/src/sections/Trades.tsx +298 -0
- package/templates/react-dvp/src/ui/primitives.tsx +91 -0
- package/templates/react-dvp/tsconfig.json +20 -0
- package/templates/react-dvp/vite.config.ts +7 -0
- package/templates/react-tokenization/README.md +139 -0
- package/templates/react-tokenization/_gitignore +8 -0
- package/templates/react-tokenization/_package.json +26 -0
- package/templates/react-tokenization/index.html +12 -0
- package/templates/react-tokenization/src/App.css +361 -0
- package/templates/react-tokenization/src/App.tsx +111 -0
- package/templates/react-tokenization/src/context/DemoContext.tsx +34 -0
- package/templates/react-tokenization/src/index.css +23 -0
- package/templates/react-tokenization/src/lib/backend.ts +171 -0
- package/templates/react-tokenization/src/lib/fixtures.ts +134 -0
- package/templates/react-tokenization/src/lib/format.ts +59 -0
- package/templates/react-tokenization/src/lib/invalidate.ts +23 -0
- package/templates/react-tokenization/src/lib/mutation.ts +17 -0
- package/templates/react-tokenization/src/lib/store.ts +219 -0
- package/templates/react-tokenization/src/lib/types.ts +27 -0
- package/templates/react-tokenization/src/main.tsx +18 -0
- package/templates/react-tokenization/src/sections/Allocations.tsx +150 -0
- package/templates/react-tokenization/src/sections/Holdings.tsx +51 -0
- package/templates/react-tokenization/src/sections/Incoming.tsx +107 -0
- package/templates/react-tokenization/src/sections/Issuer.tsx +177 -0
- package/templates/react-tokenization/src/sections/Transfer.tsx +112 -0
- package/templates/react-tokenization/src/ui/primitives.tsx +91 -0
- package/templates/react-tokenization/tsconfig.json +20 -0
- package/templates/react-tokenization/vite.config.ts +7 -0
- package/templates/react-vite/README.md +2 -2
- package/templates/react-vite/_package.json +1 -1
- package/templates/vanilla/README.md +2 -2
- package/templates/vanilla/_package.json +1 -1
- package/templates/vanilla/src/main.ts +2 -2
- package/templates/vue-nuxt-pinia/README.md +3 -3
- 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,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,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>
|