create-partylayer-app 0.4.1 → 0.5.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/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/react-dvp/README.md +128 -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 +133 -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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed seed data for the demo backend, built from the REAL exported CIP-0056
|
|
3
|
+
* types. Three demo parties, one instrument (`DEMO`), holdings per party (one
|
|
4
|
+
* carrying a lock), a pending incoming transfer instruction for alice, a static
|
|
5
|
+
* allocation for the allocations card, and a fee estimate for CostPreview.
|
|
6
|
+
*
|
|
7
|
+
* The cost constructor and its types now come from `@partylayer/react` (re-exported
|
|
8
|
+
* from core), so the app no longer imports `@partylayer/core` directly.
|
|
9
|
+
*/
|
|
10
|
+
import { toTrafficCost, type CostEstimation } from '@partylayer/react';
|
|
11
|
+
import type {
|
|
12
|
+
TokenHolding,
|
|
13
|
+
TokenHoldingRef,
|
|
14
|
+
TokenTransferInstructionRef,
|
|
15
|
+
TokenAllocationRef,
|
|
16
|
+
} from '@partylayer/react/query';
|
|
17
|
+
import type { DemoParty, DemoPartyKey, InstrumentConfig } from './types';
|
|
18
|
+
|
|
19
|
+
export const PARTIES: Record<DemoPartyKey, DemoParty> = {
|
|
20
|
+
issuer: { key: 'issuer', label: 'Issuer', partyId: 'issuer::12208a3f9b' },
|
|
21
|
+
alice: { key: 'alice', label: 'Alice', partyId: 'alice::1220b7c142' },
|
|
22
|
+
bob: { key: 'bob', label: 'Bob', partyId: 'bob::1220e4d9a0' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PARTY_ORDER: DemoPartyKey[] = ['issuer', 'alice', 'bob'];
|
|
26
|
+
|
|
27
|
+
export const INSTRUMENT: InstrumentConfig = {
|
|
28
|
+
admin: PARTIES.issuer.partyId,
|
|
29
|
+
id: 'DEMO',
|
|
30
|
+
name: 'Demo Token',
|
|
31
|
+
description: 'A demo instrument administered by the issuer party for this example.',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A far-future ISO timestamp for the lock and allocation deadlines (stable, not time-based). */
|
|
35
|
+
const FUTURE = '2027-01-01T00:00:00Z';
|
|
36
|
+
|
|
37
|
+
function holding(owner: string, amount: string, lock?: TokenHolding['lock']): TokenHolding {
|
|
38
|
+
return {
|
|
39
|
+
owner,
|
|
40
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
41
|
+
amount,
|
|
42
|
+
lock,
|
|
43
|
+
meta: {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The seed holdings, as `{ cid, holding }` refs (the cid is the ACS contract id). */
|
|
48
|
+
export function seedHoldings(): Record<DemoPartyKey, TokenHoldingRef[]> {
|
|
49
|
+
return {
|
|
50
|
+
issuer: [{ cid: 'h-issuer-treasury', holding: holding(PARTIES.issuer.partyId, '1000000.00') }],
|
|
51
|
+
alice: [
|
|
52
|
+
{ cid: 'h-alice-1', holding: holding(PARTIES.alice.partyId, '150.00') },
|
|
53
|
+
{
|
|
54
|
+
cid: 'h-alice-2',
|
|
55
|
+
holding: holding(PARTIES.alice.partyId, '50.00', {
|
|
56
|
+
holders: [PARTIES.issuer.partyId],
|
|
57
|
+
expiresAt: FUTURE,
|
|
58
|
+
context: 'frozen by issuer',
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
bob: [{ cid: 'h-bob-1', holding: holding(PARTIES.bob.partyId, '75.00') }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The seed incoming transfer instructions, keyed by receiver party. Alice has one
|
|
68
|
+
* pending, in the `pendingReceiverAcceptance` state (so Accept and Reject apply).
|
|
69
|
+
*/
|
|
70
|
+
export function seedIncoming(): Record<DemoPartyKey, TokenTransferInstructionRef[]> {
|
|
71
|
+
return {
|
|
72
|
+
issuer: [],
|
|
73
|
+
alice: [
|
|
74
|
+
{
|
|
75
|
+
cid: 'ti-bob-alice-1',
|
|
76
|
+
instruction: {
|
|
77
|
+
transfer: {
|
|
78
|
+
sender: PARTIES.bob.partyId,
|
|
79
|
+
receiver: PARTIES.alice.partyId,
|
|
80
|
+
amount: '25.00',
|
|
81
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
82
|
+
requestedAt: '2026-07-22T09:00:00Z',
|
|
83
|
+
executeBefore: FUTURE,
|
|
84
|
+
inputHoldingCids: ['h-bob-1'],
|
|
85
|
+
meta: { memo: 'lunch split' },
|
|
86
|
+
},
|
|
87
|
+
status: { kind: 'pendingReceiverAcceptance' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
bob: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A static allocation for the read-only allocations card (alice funds a settlement leg). */
|
|
96
|
+
export function seedAllocations(): TokenAllocationRef[] {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
cid: 'alloc-cid-1',
|
|
100
|
+
allocation: {
|
|
101
|
+
allocation: {
|
|
102
|
+
settlement: {
|
|
103
|
+
executor: PARTIES.issuer.partyId,
|
|
104
|
+
settlementRef: { id: 'settlement-demo-1', cid: 'settle-cid-1' },
|
|
105
|
+
requestedAt: '2026-07-22T09:00:00Z',
|
|
106
|
+
allocateBefore: FUTURE,
|
|
107
|
+
settleBefore: FUTURE,
|
|
108
|
+
},
|
|
109
|
+
transferLegId: 'leg-1',
|
|
110
|
+
transferLeg: {
|
|
111
|
+
sender: PARTIES.alice.partyId,
|
|
112
|
+
receiver: PARTIES.bob.partyId,
|
|
113
|
+
amount: '10.00',
|
|
114
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
holdingCids: ['h-alice-1'],
|
|
118
|
+
meta: { settlement: 'demo' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A fixed fee estimate for CostPreview. The three fields are int64-as-string
|
|
126
|
+
* (`TrafficCost`); built with the `toTrafficCost` constructor now re-exported from
|
|
127
|
+
* `@partylayer/react`, so no direct core dependency is needed.
|
|
128
|
+
*/
|
|
129
|
+
export const FEE_ESTIMATE: CostEstimation = {
|
|
130
|
+
estimationTimestamp: '2026-07-22T09:00:00Z',
|
|
131
|
+
confirmationRequestTrafficCostEstimation: toTrafficCost('1200'),
|
|
132
|
+
confirmationResponseTrafficCostEstimation: toTrafficCost('800'),
|
|
133
|
+
totalTrafficCostEstimation: toTrafficCost('2000'),
|
|
134
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fixed-point decimal helpers for the demo.
|
|
3
|
+
*
|
|
4
|
+
* Amounts are decimal strings END TO END (never JS numbers), matching the
|
|
5
|
+
* CIP-0056 `Decimal` convention where precision must survive. This demo SIMPLIFIES
|
|
6
|
+
* to two decimal places using bigint cents, which is enough for the two-decimal
|
|
7
|
+
* fixtures; a real dApp handling arbitrary-precision decimals should use a decimal
|
|
8
|
+
* library rather than this two-place shortcut. Documented in the README.
|
|
9
|
+
*/
|
|
10
|
+
const SCALE = 100n;
|
|
11
|
+
|
|
12
|
+
/** Parse a two-decimal amount string into integer cents (bigint). */
|
|
13
|
+
export function toCents(amount: string): bigint {
|
|
14
|
+
const trimmed = amount.trim();
|
|
15
|
+
const negative = trimmed.startsWith('-');
|
|
16
|
+
const body = negative ? trimmed.slice(1) : trimmed;
|
|
17
|
+
const [whole, frac = ''] = body.split('.');
|
|
18
|
+
const cents = BigInt(whole || '0') * SCALE + BigInt(((frac + '00').slice(0, 2)) || '0');
|
|
19
|
+
return negative ? -cents : cents;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Render integer cents back to a two-decimal amount string. */
|
|
23
|
+
export function fromCents(cents: bigint): string {
|
|
24
|
+
const negative = cents < 0n;
|
|
25
|
+
const abs = negative ? -cents : cents;
|
|
26
|
+
const whole = abs / SCALE;
|
|
27
|
+
const frac = abs % SCALE;
|
|
28
|
+
const s = whole.toString() + '.' + frac.toString().padStart(2, '0');
|
|
29
|
+
return negative ? '-' + s : s;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function addAmount(a: string, b: string): string {
|
|
33
|
+
return fromCents(toCents(a) + toCents(b));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function subAmount(a: string, b: string): string {
|
|
37
|
+
return fromCents(toCents(a) - toCents(b));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Returns 1 / 0 / -1 for a > b / a === b / a < b. */
|
|
41
|
+
export function cmpAmount(a: string, b: string): number {
|
|
42
|
+
const d = toCents(a) - toCents(b);
|
|
43
|
+
return d > 0n ? 1 : d < 0n ? -1 : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Group the integer part with thousands separators; keep two decimals. */
|
|
47
|
+
export function formatAmount(amount: string): string {
|
|
48
|
+
const [whole, frac = '00'] = amount.split('.');
|
|
49
|
+
const negative = whole.startsWith('-');
|
|
50
|
+
const digits = negative ? whole.slice(1) : whole;
|
|
51
|
+
const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
52
|
+
return (negative ? '-' : '') + grouped + '.' + frac.padEnd(2, '0').slice(0, 2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether an amount is a valid non-negative two-decimal number greater than zero. */
|
|
56
|
+
export function isPositiveAmount(amount: string): boolean {
|
|
57
|
+
if (!/^\d+(\.\d{1,2})?$/.test(amount.trim())) return false;
|
|
58
|
+
return toCents(amount) > 0n;
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache invalidation helpers.
|
|
3
|
+
*
|
|
4
|
+
* The CIP-0056 read hooks namespace the consumer's `key` under their own key
|
|
5
|
+
* factory, so the real TanStack queryKey is `partyLayerKeys.tokenHoldings({ key })`,
|
|
6
|
+
* NOT the raw `key`. Invalidation therefore goes through the exported
|
|
7
|
+
* `partyLayerKeys` factories (an empty-args call prefix-matches every party's
|
|
8
|
+
* entry), as the hooks' `key` JSDoc and the `partyLayerKeys` doc now spell out.
|
|
9
|
+
*/
|
|
10
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { partyLayerKeys } from '@partylayer/react/query';
|
|
12
|
+
|
|
13
|
+
/** Refresh every party's holdings, instructions, and the generic reads (supply, refs). */
|
|
14
|
+
export function invalidateHoldingsAndReads(queryClient: QueryClient): void {
|
|
15
|
+
queryClient.invalidateQueries({ queryKey: partyLayerKeys.tokenHoldings() });
|
|
16
|
+
queryClient.invalidateQueries({ queryKey: partyLayerKeys.transferInstructions() });
|
|
17
|
+
queryClient.invalidateQueries({ queryKey: partyLayerKeys.damlContract() });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Refresh the allocations list. */
|
|
21
|
+
export function invalidateAllocations(queryClient: QueryClient): void {
|
|
22
|
+
queryClient.invalidateQueries({ queryKey: partyLayerKeys.tokenAllocations() });
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TransactionToastStatus } from '@partylayer/react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map a TanStack mutation's flags to the `TransactionToast` status prop. This is
|
|
5
|
+
* the documented mapping the toast's JSDoc describes: pending, success, error, else
|
|
6
|
+
* idle. Presentational: the toast renders whatever status the dApp derives here.
|
|
7
|
+
*/
|
|
8
|
+
export function toastStatus(m: {
|
|
9
|
+
isPending: boolean;
|
|
10
|
+
isSuccess: boolean;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
}): TransactionToastStatus {
|
|
13
|
+
if (m.isPending) return 'pending';
|
|
14
|
+
if (m.isSuccess) return 'success';
|
|
15
|
+
if (m.isError) return 'error';
|
|
16
|
+
return 'idle';
|
|
17
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny in-memory mutable store initialized from the fixtures. It stands in for a
|
|
3
|
+
* validator's active-contract set so every hook exercises real loading, success,
|
|
4
|
+
* error, and invalidation without a live ledger. Small artificial latency makes
|
|
5
|
+
* loading states visible.
|
|
6
|
+
*
|
|
7
|
+
* Decimal amounts stay strings throughout; arithmetic goes through the two-decimal
|
|
8
|
+
* helpers in `./format` (documented simplification).
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
TokenHoldingRef,
|
|
12
|
+
TokenTransferInstructionRef,
|
|
13
|
+
TokenAllocationRef,
|
|
14
|
+
} from '@partylayer/react/query';
|
|
15
|
+
import type { DemoPartyKey } from './types';
|
|
16
|
+
import { PARTIES, seedHoldings, seedIncoming, seedAllocations, INSTRUMENT } from './fixtures';
|
|
17
|
+
import { addAmount, subAmount, cmpAmount } from './format';
|
|
18
|
+
|
|
19
|
+
interface StoreState {
|
|
20
|
+
holdings: Record<DemoPartyKey, TokenHoldingRef[]>;
|
|
21
|
+
incoming: Record<DemoPartyKey, TokenTransferInstructionRef[]>;
|
|
22
|
+
allocations: TokenAllocationRef[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let state: StoreState = freshState();
|
|
26
|
+
let cidCounter = 0;
|
|
27
|
+
|
|
28
|
+
function freshState(): StoreState {
|
|
29
|
+
return {
|
|
30
|
+
holdings: seedHoldings(),
|
|
31
|
+
incoming: seedIncoming(),
|
|
32
|
+
allocations: seedAllocations(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nextCid(prefix: string): string {
|
|
37
|
+
cidCounter += 1;
|
|
38
|
+
// The 'gen' namespace keeps generated cids from colliding with the fixed seed
|
|
39
|
+
// cids (e.g. 'h-alice-1'), so a change output is never filtered out with a
|
|
40
|
+
// spent input that happens to share a numeric suffix.
|
|
41
|
+
return prefix + '-gen' + cidCounter.toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Map a party id string back to its demo key (fixtures use fixed ids). */
|
|
45
|
+
function keyOf(partyId: string): DemoPartyKey | null {
|
|
46
|
+
for (const key of Object.keys(PARTIES) as DemoPartyKey[]) {
|
|
47
|
+
if (PARTIES[key].partyId === partyId) return key;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Artificial latency so loading skeletons are visible (200 to 400 ms). */
|
|
53
|
+
export function latency(): Promise<void> {
|
|
54
|
+
const ms = 200 + ((cidCounter * 37) % 200);
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const demoStore = {
|
|
59
|
+
reset(): void {
|
|
60
|
+
state = freshState();
|
|
61
|
+
cidCounter = 0;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
holdingsOf(party: DemoPartyKey): TokenHoldingRef[] {
|
|
65
|
+
return state.holdings[party].map((ref) => ({ cid: ref.cid, holding: { ...ref.holding } }));
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
incomingOf(party: DemoPartyKey): TokenTransferInstructionRef[] {
|
|
69
|
+
return state.incoming[party].map((it) => ({ ...it }));
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
allocations(): TokenAllocationRef[] {
|
|
73
|
+
return state.allocations.map((a) => ({ cid: a.cid, allocation: a.allocation }));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/** Contract ids of a party's UNLOCKED holdings, for building `inputHoldingCids`. */
|
|
77
|
+
unlockedCids(party: DemoPartyKey): string[] {
|
|
78
|
+
return state.holdings[party].filter((ref) => !ref.holding.lock).map((ref) => ref.cid);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** Total held per party, summed across holdings (string arithmetic). */
|
|
82
|
+
balanceOf(party: DemoPartyKey): string {
|
|
83
|
+
return state.holdings[party].reduce((sum, ref) => addAmount(sum, ref.holding.amount), '0.00');
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/** Total supply across all parties for the instrument. */
|
|
87
|
+
totalSupply(): string {
|
|
88
|
+
return (Object.keys(state.holdings) as DemoPartyKey[]).reduce(
|
|
89
|
+
(sum, party) => addAmount(sum, this.balanceOf(party)),
|
|
90
|
+
'0.00',
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** Consume `amount` from a party's unlocked holdings (archives inputs, writes change). */
|
|
95
|
+
debit(party: DemoPartyKey, amount: string): void {
|
|
96
|
+
const unlocked = state.holdings[party].filter((ref) => !ref.holding.lock);
|
|
97
|
+
const available = unlocked.reduce((sum, ref) => addAmount(sum, ref.holding.amount), '0.00');
|
|
98
|
+
if (cmpAmount(available, amount) < 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'Insufficient unlocked balance: need ' + amount + ' but only ' + available + ' is available.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let remaining = amount;
|
|
104
|
+
const spentCids = new Set<string>();
|
|
105
|
+
for (const ref of unlocked) {
|
|
106
|
+
if (cmpAmount(remaining, '0.00') <= 0) break;
|
|
107
|
+
spentCids.add(ref.cid);
|
|
108
|
+
if (cmpAmount(ref.holding.amount, remaining) >= 0) {
|
|
109
|
+
const change = subAmount(ref.holding.amount, remaining);
|
|
110
|
+
remaining = '0.00';
|
|
111
|
+
if (cmpAmount(change, '0.00') > 0) {
|
|
112
|
+
state.holdings[party].push({
|
|
113
|
+
cid: nextCid('h-' + party),
|
|
114
|
+
holding: { ...ref.holding, amount: change, lock: undefined },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
remaining = subAmount(remaining, ref.holding.amount);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
state.holdings[party] = state.holdings[party].filter((ref) => !spentCids.has(ref.cid));
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/** Credit a party with a new unlocked holding. */
|
|
125
|
+
credit(partyId: string, amount: string, meta?: Record<string, string>): void {
|
|
126
|
+
const party = keyOf(partyId);
|
|
127
|
+
if (!party) throw new Error('Unknown party to credit.');
|
|
128
|
+
state.holdings[party].push({
|
|
129
|
+
cid: nextCid('h-' + party),
|
|
130
|
+
holding: {
|
|
131
|
+
owner: partyId,
|
|
132
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
133
|
+
amount,
|
|
134
|
+
lock: undefined,
|
|
135
|
+
meta: meta ?? {},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Initiate a transfer (two-step model): debit the sender now and create a pending
|
|
142
|
+
* incoming instruction for the receiver to accept. Throws on insufficient unlocked
|
|
143
|
+
* balance (drives the error path). Returns the new instruction cid.
|
|
144
|
+
*/
|
|
145
|
+
initiateTransfer(senderId: string, receiverId: string, amount: string, memo?: string): string {
|
|
146
|
+
const sender = keyOf(senderId);
|
|
147
|
+
const receiver = keyOf(receiverId);
|
|
148
|
+
if (!sender || !receiver) throw new Error('Unknown party in transfer.');
|
|
149
|
+
|
|
150
|
+
const inputCids = this.unlockedCids(sender);
|
|
151
|
+
this.debit(sender, amount);
|
|
152
|
+
|
|
153
|
+
const instructionCid = nextCid('ti-' + sender + '-' + receiver);
|
|
154
|
+
state.incoming[receiver].push({
|
|
155
|
+
cid: instructionCid,
|
|
156
|
+
instruction: {
|
|
157
|
+
transfer: {
|
|
158
|
+
sender: senderId,
|
|
159
|
+
receiver: receiverId,
|
|
160
|
+
amount,
|
|
161
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
162
|
+
requestedAt: '2026-07-22T09:00:00Z',
|
|
163
|
+
executeBefore: '2027-01-01T00:00:00Z',
|
|
164
|
+
inputHoldingCids: inputCids,
|
|
165
|
+
meta: memo ? { memo } : {},
|
|
166
|
+
},
|
|
167
|
+
status: { kind: 'pendingReceiverAcceptance' },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return instructionCid;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a pending incoming instruction. Accept credits the receiver; reject
|
|
175
|
+
* refunds the amount back to the sender (the debit is reversed).
|
|
176
|
+
*/
|
|
177
|
+
resolveIncoming(receiverId: string, instructionCid: string, accept: boolean): void {
|
|
178
|
+
const receiver = keyOf(receiverId);
|
|
179
|
+
if (!receiver) throw new Error('Unknown receiver party.');
|
|
180
|
+
const item = state.incoming[receiver].find((it) => it.cid === instructionCid);
|
|
181
|
+
if (!item) throw new Error('Instruction not found: ' + instructionCid);
|
|
182
|
+
|
|
183
|
+
state.incoming[receiver] = state.incoming[receiver].filter((it) => it.cid !== instructionCid);
|
|
184
|
+
const transfer = item.instruction.transfer;
|
|
185
|
+
if (accept) {
|
|
186
|
+
this.credit(receiverId, transfer.amount, transfer.meta);
|
|
187
|
+
} else {
|
|
188
|
+
this.credit(transfer.sender, transfer.amount, { refunded: 'true' });
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/** Issuer mint: create a new holding for a party. */
|
|
193
|
+
mint(toPartyId: string, amount: string): void {
|
|
194
|
+
const to = keyOf(toPartyId);
|
|
195
|
+
if (!to) throw new Error('Unknown mint target party.');
|
|
196
|
+
state.holdings[to].push({
|
|
197
|
+
cid: nextCid('h-' + to),
|
|
198
|
+
holding: {
|
|
199
|
+
owner: toPartyId,
|
|
200
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
201
|
+
amount,
|
|
202
|
+
lock: undefined,
|
|
203
|
+
meta: { minted: 'true' },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/** Issuer freeze toggle: add or remove the lock on a holding by cid. */
|
|
209
|
+
setFrozen(party: DemoPartyKey, cid: string, frozen: boolean): void {
|
|
210
|
+
const ref = state.holdings[party].find((h) => h.cid === cid);
|
|
211
|
+
if (!ref) throw new Error('Holding not found: ' + cid);
|
|
212
|
+
ref.holding = {
|
|
213
|
+
...ref.holding,
|
|
214
|
+
lock: frozen
|
|
215
|
+
? { holders: [INSTRUMENT.admin], expiresAt: '2027-01-01T00:00:00Z', context: 'frozen by issuer' }
|
|
216
|
+
: undefined,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level view models for the Tokenization example.
|
|
3
|
+
*
|
|
4
|
+
* The holding, allocation, and pending-instruction shapes now come straight from
|
|
5
|
+
* `@partylayer/react/query` as the corrected contract-ref types
|
|
6
|
+
* (`TokenHoldingRef`, `TokenAllocationRef`, `TokenTransferInstructionRef`), so the
|
|
7
|
+
* app no longer defines its own ref or instruction models. Only the demo-specific
|
|
8
|
+
* types remain here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The three demo parties this example switches between. */
|
|
12
|
+
export type DemoPartyKey = 'issuer' | 'alice' | 'bob';
|
|
13
|
+
|
|
14
|
+
export interface DemoParty {
|
|
15
|
+
key: DemoPartyKey;
|
|
16
|
+
label: string;
|
|
17
|
+
/** The party id string (Daml `Party`). */
|
|
18
|
+
partyId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A small instrument-config view read through the generic `useDamlContract`. */
|
|
22
|
+
export interface InstrumentConfig {
|
|
23
|
+
admin: string;
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import './index.css';
|
|
6
|
+
|
|
7
|
+
// The CIP-0056 hooks (the `@partylayer/react/query` entrypoint) are built on
|
|
8
|
+
// TanStack Query, so they need a QueryClient in context. Create one here and wrap
|
|
9
|
+
// the app, exactly as the react-vite scaffold template does.
|
|
10
|
+
const queryClient = new QueryClient();
|
|
11
|
+
|
|
12
|
+
createRoot(document.getElementById('root')!).render(
|
|
13
|
+
<StrictMode>
|
|
14
|
+
<QueryClientProvider client={queryClient}>
|
|
15
|
+
<App />
|
|
16
|
+
</QueryClientProvider>
|
|
17
|
+
</StrictMode>,
|
|
18
|
+
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocations section. Exercises the remaining three CIP-0056 hooks:
|
|
3
|
+
* - useTokenAllocations reads the funded-allocation list (typed view),
|
|
4
|
+
* - useAllocationInstruction creates an allocation via the factory,
|
|
5
|
+
* - useAllocationAction acts on a funded allocation (executeTransfer/cancel/withdraw).
|
|
6
|
+
* The demo backend returns fixture results for the writes; a real dApp performs the
|
|
7
|
+
* registry-specific factory/context flow inside these fetchers (see the README).
|
|
8
|
+
*/
|
|
9
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
+
import {
|
|
11
|
+
useTokenAllocations,
|
|
12
|
+
useAllocationInstruction,
|
|
13
|
+
useAllocationAction,
|
|
14
|
+
type AllocationActionKind,
|
|
15
|
+
} from '@partylayer/react/query';
|
|
16
|
+
import { TransactionToast } from '@partylayer/react';
|
|
17
|
+
import { useDemo, partyKey } from '../context/DemoContext';
|
|
18
|
+
import { Card, AsyncView, Badge } from '../ui/primitives';
|
|
19
|
+
import { toastStatus } from '../lib/mutation';
|
|
20
|
+
import { invalidateAllocations } from '../lib/invalidate';
|
|
21
|
+
import { formatAmount } from '../lib/format';
|
|
22
|
+
import { PARTIES } from '../lib/fixtures';
|
|
23
|
+
|
|
24
|
+
const ACTIONS: { kind: AllocationActionKind; label: string }[] = [
|
|
25
|
+
{ kind: 'executeTransfer', label: 'Execute' },
|
|
26
|
+
{ kind: 'cancel', label: 'Cancel' },
|
|
27
|
+
{ kind: 'withdraw', label: 'Withdraw' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function Allocations() {
|
|
31
|
+
const { party, backend } = useDemo();
|
|
32
|
+
const queryClient = useQueryClient();
|
|
33
|
+
|
|
34
|
+
const q = useTokenAllocations({
|
|
35
|
+
read: (signal) => backend.readAllocations(signal),
|
|
36
|
+
key: partyKey('allocations', party),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const create = useAllocationInstruction<{ ok: true }>({
|
|
40
|
+
submit: (request, signal) => backend.submitAllocation(request, signal),
|
|
41
|
+
mutation: {
|
|
42
|
+
onSuccess: () => invalidateAllocations(queryClient),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const act = useAllocationAction<{ ok: true }>({
|
|
47
|
+
submit: (request, signal) => backend.submitAllocationAction(request, signal),
|
|
48
|
+
mutation: {
|
|
49
|
+
onSuccess: () => invalidateAllocations(queryClient),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const createDemoAllocation = () => {
|
|
54
|
+
// Build the standard AllocationFactory_Allocate request from the issuer as admin.
|
|
55
|
+
create.submitAllocation({
|
|
56
|
+
expectedAdmin: PARTIES.issuer.partyId,
|
|
57
|
+
allocation: {
|
|
58
|
+
settlement: {
|
|
59
|
+
executor: PARTIES.issuer.partyId,
|
|
60
|
+
settlementRef: { id: 'settlement-demo-new' },
|
|
61
|
+
requestedAt: new Date().toISOString(),
|
|
62
|
+
allocateBefore: '2027-01-01T00:00:00Z',
|
|
63
|
+
settleBefore: '2027-01-01T00:00:00Z',
|
|
64
|
+
},
|
|
65
|
+
transferLegId: 'leg-new',
|
|
66
|
+
transferLeg: {
|
|
67
|
+
sender: PARTIES[party].partyId,
|
|
68
|
+
receiver: PARTIES.bob.partyId,
|
|
69
|
+
amount: '5.00',
|
|
70
|
+
instrumentId: { admin: PARTIES.issuer.partyId, id: 'DEMO' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
requestedAt: new Date().toISOString(),
|
|
74
|
+
inputHoldingCids: [],
|
|
75
|
+
meta: { demo: 'true' },
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const busy = create.isPending || act.isPending;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Card title="Allocations" hint="useTokenAllocations + useAllocationInstruction + useAllocationAction">
|
|
83
|
+
<div className="alloc-head">
|
|
84
|
+
<span className="muted">Funded allocations for a settlement leg.</span>
|
|
85
|
+
<button className="btn btn-ghost" onClick={createDemoAllocation} disabled={busy}>
|
|
86
|
+
Create demo allocation
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<AsyncView
|
|
91
|
+
isPending={q.isPending}
|
|
92
|
+
error={q.error}
|
|
93
|
+
data={q.allocations}
|
|
94
|
+
isEmpty={(a) => a.length === 0}
|
|
95
|
+
empty="No allocations."
|
|
96
|
+
>
|
|
97
|
+
{(refs) => (
|
|
98
|
+
<ul className="list">
|
|
99
|
+
{refs.map(({ cid, allocation: a }) => {
|
|
100
|
+
const leg = a.allocation.transferLeg;
|
|
101
|
+
const settle = a.allocation.settlement;
|
|
102
|
+
return (
|
|
103
|
+
<li key={cid} className="row row-block">
|
|
104
|
+
<div className="row-main">
|
|
105
|
+
<div className="row-title">
|
|
106
|
+
{formatAmount(leg.amount)} <span className="muted">{leg.instrumentId.id}</span>{' '}
|
|
107
|
+
<Badge tone="neutral">leg {a.allocation.transferLegId}</Badge>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="row-sub muted">
|
|
110
|
+
{leg.sender} to {leg.receiver}
|
|
111
|
+
</div>
|
|
112
|
+
<div className="row-sub muted">
|
|
113
|
+
executor {settle.executor} · settle before {settle.settleBefore.slice(0, 10)} ·{' '}
|
|
114
|
+
{a.holdingCids.length} holding{a.holdingCids.length === 1 ? '' : 's'}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="row-actions">
|
|
118
|
+
{ACTIONS.map((action) => (
|
|
119
|
+
<button
|
|
120
|
+
key={action.kind}
|
|
121
|
+
className="btn btn-ghost btn-small"
|
|
122
|
+
disabled={busy}
|
|
123
|
+
// The ref carries the ACS contract id, which feeds allocationCid.
|
|
124
|
+
onClick={() => act.submitAction({ allocationCid: cid, action: action.kind })}
|
|
125
|
+
>
|
|
126
|
+
{action.label}
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</li>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</ul>
|
|
134
|
+
)}
|
|
135
|
+
</AsyncView>
|
|
136
|
+
|
|
137
|
+
<TransactionToast
|
|
138
|
+
status={toastStatus(create.isPending || create.isSuccess || create.isError ? create : act)}
|
|
139
|
+
error={create.error ?? act.error}
|
|
140
|
+
message={
|
|
141
|
+
create.isSuccess
|
|
142
|
+
? 'Allocation created.'
|
|
143
|
+
: act.isSuccess
|
|
144
|
+
? 'Allocation action submitted.'
|
|
145
|
+
: undefined
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</Card>
|
|
149
|
+
);
|
|
150
|
+
}
|