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,110 @@
1
+ /**
2
+ * The DvP vertical example shell.
3
+ *
4
+ * PartyLayerKit provides the session + theme context (mirroring the react-vite
5
+ * template's setup), themed with the `gold` family and a light/dark toggle. The
6
+ * header carries the connect UI and a DEMO-PARTY switcher: switching the demo party
7
+ * changes whose data every section reads and who acts (venue vs counterparty). It
8
+ * is app state, 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 { Trades } from './sections/Trades';
24
+ import { MyAllocations } from './sections/MyAllocations';
25
+ import { Holdings } from './sections/Holdings';
26
+ import './App.css';
27
+
28
+ // The dev wallet adapter apps/demo uses, so the connect surface works in a demo
29
+ // context. The demo-party switcher, not this connection, drives the section data.
30
+ const ADAPTERS = [new ConsoleAdapter()];
31
+
32
+ const SYNCHRONIZERS: SynchronizerOption[] = [
33
+ { networkId: 'canton:da-devnet', label: 'DevNet' },
34
+ { networkId: 'canton:da-testnet', label: 'TestNet' },
35
+ ];
36
+
37
+ export default function App() {
38
+ const [mode, setMode] = useState<'light' | 'dark'>('light');
39
+ const [party, setParty] = useState<DemoPartyKey>('venue');
40
+ const [synchronizer, setSynchronizer] = useState('canton:da-devnet');
41
+
42
+ const theme = mode === 'dark' ? themes.gold.dark : themes.gold.light;
43
+
44
+ return (
45
+ <PartyLayerKit network="devnet" appName="PartyLayer DvP" theme={theme} adapters={ADAPTERS}>
46
+ <DemoProvider value={{ party, setParty, backend: demoBackend, mode }}>
47
+ <div className={'app app-' + mode}>
48
+ <header className="topbar">
49
+ <div className="brand">
50
+ <span className="brand-mark" aria-hidden="true" />
51
+ <div>
52
+ <div className="brand-title">DvP</div>
53
+ <div className="brand-sub">delivery versus payment, CIP-0056 allocations</div>
54
+ </div>
55
+ </div>
56
+
57
+ <div className="topbar-controls">
58
+ <div className="party-switch" role="group" aria-label="demo party">
59
+ <span className="party-switch-label">demo party</span>
60
+ {PARTY_ORDER.map((p) => (
61
+ <button
62
+ key={p}
63
+ className={'party-chip' + (party === p ? ' party-chip-on' : '')}
64
+ onClick={() => setParty(p)}
65
+ >
66
+ {PARTIES[p].label}
67
+ </button>
68
+ ))}
69
+ </div>
70
+
71
+ <SynchronizerSwitcher
72
+ networkId={synchronizer}
73
+ options={SYNCHRONIZERS}
74
+ onSwitch={setSynchronizer}
75
+ />
76
+
77
+ <button
78
+ className="btn btn-ghost"
79
+ onClick={() => setMode((m) => (m === 'dark' ? 'light' : 'dark'))}
80
+ >
81
+ {mode === 'dark' ? 'Light' : 'Dark'}
82
+ </button>
83
+
84
+ <ConnectButton />
85
+ </div>
86
+ </header>
87
+
88
+ <p className="acting-line">
89
+ Acting as <strong>{PARTIES[party].label}</strong>{' '}
90
+ <code>{PARTIES[party].partyId}</code>.{' '}
91
+ {party === 'venue'
92
+ ? 'The venue creates trades and settles atomically once both legs are allocated.'
93
+ : 'Allocate your leg of a trade, or reject it.'}
94
+ </p>
95
+
96
+ <main className="grid">
97
+ <Trades />
98
+ <MyAllocations />
99
+ <Holdings />
100
+ </main>
101
+
102
+ <footer className="footer">
103
+ Demo backend, in-memory fixtures. Model 2: the dApp supplies every read and submit.
104
+ See the README for the atomic settle semantics and real-mode wiring.
105
+ </footer>
106
+ </div>
107
+ </DemoProvider>
108
+ </PartyLayerKit>
109
+ );
110
+ }
@@ -0,0 +1,34 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { DemoPartyKey } from '../lib/types';
3
+ import type { DvpBackend } 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: DvpBackend;
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 (trades, allocations, holdings).
31
+ */
32
+ export function partyKey(scope: string, party: DemoPartyKey): [string, string, DemoPartyKey] {
33
+ return ['dvp', 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,144 @@
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 / trade-app command submission (see the README "Real mode").
6
+ */
7
+ import type {
8
+ TokenHoldingRef,
9
+ TokenAllocationRef,
10
+ TokenAllocationRequestRef,
11
+ AllocationInstructionRequest,
12
+ AllocationActionRequest,
13
+ AllocationRequestActionRequest,
14
+ } from '@partylayer/react/query';
15
+ import type { DemoPartyKey, SettleTrade, CreateTrade } from './types';
16
+ import { demoStore, latency } from './store';
17
+ import { PARTIES } from './fixtures';
18
+
19
+ export interface DvpBackend {
20
+ /** ACS read: a party's holdings as `{ cid, holding }` refs. `null` means none yet. */
21
+ readHoldings(party: DemoPartyKey, signal?: AbortSignal): Promise<TokenHoldingRef[] | null>;
22
+ /** ACS read: the open trades (allocation requests). `null` means none yet. */
23
+ readTrades(signal?: AbortSignal): Promise<TokenAllocationRequestRef[] | null>;
24
+ /** ACS read: a party's allocations as `{ cid, allocation }` refs. `null` means none yet. */
25
+ readAllocations(party: DemoPartyKey, signal?: AbortSignal): Promise<TokenAllocationRef[] | null>;
26
+ /** Which leg ids of a trade currently have a matching allocation (venue view). */
27
+ readMatchedLegs(requestCid: string, signal?: AbortSignal): Promise<string[]>;
28
+
29
+ /** Submit: allocate a leg (create an allocation via the factory). */
30
+ submitAllocation(request: AllocationInstructionRequest, signal?: AbortSignal): Promise<{ ok: true }>;
31
+ /** Submit: act on a funded allocation (withdraw / cancel). */
32
+ submitAllocationAction(request: AllocationActionRequest, signal?: AbortSignal): Promise<{ ok: true }>;
33
+ /** Submit: respond to a trade request (reject / withdraw). */
34
+ submitRequestAction(request: AllocationRequestActionRequest, signal?: AbortSignal): Promise<{ ok: true }>;
35
+ /** Submit: the venue's atomic settle (a trade-app choice, via the generic useChoice). */
36
+ submitSettle(vars: SettleTrade, signal?: AbortSignal): Promise<{ ok: true }>;
37
+ /** Submit: the venue creates a new trade (its own settlement-app choice). */
38
+ submitCreateTrade(vars: CreateTrade, signal?: AbortSignal): Promise<{ ok: true }>;
39
+ }
40
+
41
+ /** Reject if the caller aborted while we were "in flight". */
42
+ function throwIfAborted(signal?: AbortSignal): void {
43
+ if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
44
+ }
45
+
46
+ function partyIdToKey(partyId: string): DemoPartyKey {
47
+ for (const key of Object.keys(PARTIES) as DemoPartyKey[]) {
48
+ if (PARTIES[key].partyId === partyId) return key;
49
+ }
50
+ throw new Error('Unknown party id: ' + partyId);
51
+ }
52
+
53
+ /** The allocation cid a party owns, matched by a request/leg an action targets. */
54
+ function findAllocationParty(allocationCid: string): DemoPartyKey {
55
+ for (const key of Object.keys(PARTIES) as DemoPartyKey[]) {
56
+ if (demoStore.allocationsOf(key).some((a) => a.cid === allocationCid)) return key;
57
+ }
58
+ throw new Error('Allocation not found: ' + allocationCid);
59
+ }
60
+
61
+ export const demoBackend: DvpBackend = {
62
+ async readHoldings(party, signal) {
63
+ await latency();
64
+ throwIfAborted(signal);
65
+ return demoStore.holdingsOf(party);
66
+ },
67
+
68
+ async readTrades(signal) {
69
+ await latency();
70
+ throwIfAborted(signal);
71
+ return demoStore.requestsPending();
72
+ },
73
+
74
+ async readAllocations(party, signal) {
75
+ await latency();
76
+ throwIfAborted(signal);
77
+ return demoStore.allocationsOf(party);
78
+ },
79
+
80
+ async readMatchedLegs(requestCid, signal) {
81
+ await latency();
82
+ throwIfAborted(signal);
83
+ return demoStore.matchedLegIds(requestCid);
84
+ },
85
+
86
+ async submitAllocation(request, signal) {
87
+ await latency();
88
+ throwIfAborted(signal);
89
+ // The allocation spec was composed from the request; map it back to the store's
90
+ // requestCid + legId + sender. The sender selects backing holdings itself
91
+ // (inputHoldingCids), which the store consumes.
92
+ const requestCid = request.allocation.settlement.settlementRef.cid;
93
+ if (!requestCid) throw new Error('Allocation is missing its settlement request cid.');
94
+ const legId = request.allocation.transferLegId;
95
+ const sender = partyIdToKey(request.allocation.transferLeg.sender);
96
+ demoStore.allocate(sender, requestCid, legId, request.inputHoldingCids);
97
+ return { ok: true };
98
+ },
99
+
100
+ async submitAllocationAction(request, signal) {
101
+ await latency();
102
+ throwIfAborted(signal);
103
+ if (request.action === 'withdraw') {
104
+ const party = findAllocationParty(request.allocationCid);
105
+ demoStore.withdrawAllocation(party, request.allocationCid);
106
+ } else if (request.action === 'cancel') {
107
+ demoStore.cancelAllocation(request.allocationCid);
108
+ } else {
109
+ // executeTransfer is a per-allocation move; in this vertical the venue settles
110
+ // all legs atomically instead, so it is not wired here.
111
+ throw new Error('executeTransfer is settled atomically by the venue in this example.');
112
+ }
113
+ return { ok: true };
114
+ },
115
+
116
+ async submitRequestAction(request, signal) {
117
+ await latency();
118
+ throwIfAborted(signal);
119
+ if (request.action === 'reject') {
120
+ demoStore.rejectRequest(request.requestCid, request.actor);
121
+ } else {
122
+ demoStore.withdrawRequest(request.requestCid);
123
+ }
124
+ return { ok: true };
125
+ },
126
+
127
+ // The venue's settle is a dApp-specific trade choice, wired through the generic
128
+ // useChoice. A real venue settles atomically via its trade contract's choice whose
129
+ // body exercises Allocation_ExecuteTransfer per leg in a single transaction, which
130
+ // is exactly why this is useChoice and not the per-allocation typed hook.
131
+ async submitSettle(vars, signal) {
132
+ await latency();
133
+ throwIfAborted(signal);
134
+ demoStore.settle(vars.requestCid);
135
+ return { ok: true };
136
+ },
137
+
138
+ async submitCreateTrade(vars, signal) {
139
+ await latency();
140
+ throwIfAborted(signal);
141
+ demoStore.createTrade(vars.usdAmount, vars.bondAmount);
142
+ return { ok: true };
143
+ },
144
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Typed seed data for the DvP demo backend, built from the REAL exported CIP-0056
3
+ * types. A settlement venue plus two counterparties, two registry-administered
4
+ * instruments (DEMO-USD, DEMO-BOND), seed holdings, and one open trade (an
5
+ * allocation request with two opposite legs).
6
+ */
7
+ import { toTrafficCost, type CostEstimation } from '@partylayer/react';
8
+ import type {
9
+ TokenHolding,
10
+ TokenHoldingRef,
11
+ TokenAllocationRequestRef,
12
+ TokenTransferLeg,
13
+ } from '@partylayer/react/query';
14
+ import type { DemoParty, DemoPartyKey, InstrumentConfig } from './types';
15
+
16
+ /**
17
+ * The registry party that administers both instruments. NOT selectable as a demo
18
+ * party: the executor (venue) and the registry admin are distinct roles.
19
+ */
20
+ export const REGISTRY = 'registry::1220c0ffee';
21
+
22
+ export const PARTIES: Record<DemoPartyKey, DemoParty> = {
23
+ venue: { key: 'venue', label: 'Venue', partyId: 'venue::12208a3f9b' },
24
+ alice: { key: 'alice', label: 'Alice', partyId: 'alice::1220b7c142' },
25
+ bob: { key: 'bob', label: 'Bob', partyId: 'bob::1220e4d9a0' },
26
+ };
27
+
28
+ export const PARTY_ORDER: DemoPartyKey[] = ['venue', 'alice', 'bob'];
29
+
30
+ export const USD: InstrumentConfig = { admin: REGISTRY, id: 'DEMO-USD', name: 'Demo USD' };
31
+ export const BOND: InstrumentConfig = { admin: REGISTRY, id: 'DEMO-BOND', name: 'Demo Bond' };
32
+
33
+ /** The two transfer-leg identifiers a trade settles across. */
34
+ export const LEG_USD = 'leg-usd';
35
+ export const LEG_BOND = 'leg-bond';
36
+
37
+ /** Far-future ISO deadlines for the seed trade (stable, not time-based). */
38
+ export const DEFAULT_ALLOCATE_BEFORE = '2027-01-01T00:00:00Z';
39
+ export const DEFAULT_SETTLE_BEFORE = '2027-01-02T00:00:00Z';
40
+
41
+ function holding(owner: string, admin: string, id: string, amount: string): TokenHolding {
42
+ return { owner, instrumentId: { admin, id }, amount, lock: undefined, meta: {} };
43
+ }
44
+
45
+ /** The seed holdings: Alice holds USD, Bob holds BOND, the venue holds nothing. */
46
+ export function seedHoldings(): Record<DemoPartyKey, TokenHoldingRef[]> {
47
+ return {
48
+ venue: [],
49
+ alice: [
50
+ { cid: 'h-alice-usd-1', holding: holding(PARTIES.alice.partyId, USD.admin, USD.id, '300.00') },
51
+ { cid: 'h-alice-usd-2', holding: holding(PARTIES.alice.partyId, USD.admin, USD.id, '50.00') },
52
+ ],
53
+ bob: [{ cid: 'h-bob-bond-1', holding: holding(PARTIES.bob.partyId, BOND.admin, BOND.id, '10.00') }],
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Build a trade as an allocation request: two opposite legs (Alice pays USD to Bob,
59
+ * Bob delivers BOND to Alice), an executor of the venue, and the given deadlines.
60
+ */
61
+ export function buildTrade(
62
+ tradeId: string,
63
+ requestCid: string,
64
+ usdAmount: string,
65
+ bondAmount: string,
66
+ allocateBefore: string,
67
+ settleBefore: string,
68
+ ): TokenAllocationRequestRef {
69
+ const legUsd: TokenTransferLeg = {
70
+ sender: PARTIES.alice.partyId,
71
+ receiver: PARTIES.bob.partyId,
72
+ amount: usdAmount,
73
+ instrumentId: { admin: USD.admin, id: USD.id },
74
+ };
75
+ const legBond: TokenTransferLeg = {
76
+ sender: PARTIES.bob.partyId,
77
+ receiver: PARTIES.alice.partyId,
78
+ amount: bondAmount,
79
+ instrumentId: { admin: BOND.admin, id: BOND.id },
80
+ };
81
+ return {
82
+ cid: requestCid,
83
+ request: {
84
+ settlement: {
85
+ executor: PARTIES.venue.partyId,
86
+ settlementRef: { id: tradeId, cid: requestCid },
87
+ requestedAt: '2026-07-22T09:00:00Z',
88
+ allocateBefore,
89
+ settleBefore,
90
+ meta: { venue: 'demo' },
91
+ },
92
+ transferLegs: { [LEG_USD]: legUsd, [LEG_BOND]: legBond },
93
+ meta: { trade: tradeId },
94
+ },
95
+ };
96
+ }
97
+
98
+ /** The seed open trade: 100 USD against 5 BOND. */
99
+ export function seedRequests(): TokenAllocationRequestRef[] {
100
+ return [buildTrade('trade-1', 'ar-cid-1', '100.00', '5.00', DEFAULT_ALLOCATE_BEFORE, DEFAULT_SETTLE_BEFORE)];
101
+ }
102
+
103
+ /**
104
+ * A fixed fee estimate for CostPreview before a leg allocation. The three fields are
105
+ * int64-as-string (`TrafficCost`); built with `toTrafficCost` re-exported from
106
+ * `@partylayer/react`, so no direct core dependency is needed.
107
+ */
108
+ export const FEE_ESTIMATE: CostEstimation = {
109
+ estimationTimestamp: '2026-07-22T09:00:00Z',
110
+ confirmationRequestTrafficCostEstimation: toTrafficCost('1500'),
111
+ confirmationResponseTrafficCostEstimation: toTrafficCost('900'),
112
+ totalTrafficCostEstimation: toTrafficCost('2400'),
113
+ };
@@ -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,18 @@
1
+ /**
2
+ * Cache invalidation helper.
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 spell out.
9
+ */
10
+ import type { QueryClient } from '@tanstack/react-query';
11
+ import { partyLayerKeys } from '@partylayer/react/query';
12
+
13
+ /** Refresh every party's trades, allocations, and holdings after a write. */
14
+ export function invalidateAll(queryClient: QueryClient): void {
15
+ queryClient.invalidateQueries({ queryKey: partyLayerKeys.allocationRequests() });
16
+ queryClient.invalidateQueries({ queryKey: partyLayerKeys.tokenAllocations() });
17
+ queryClient.invalidateQueries({ queryKey: partyLayerKeys.tokenHoldings() });
18
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Thin app helpers over the canonical matching comparator now owned by
3
+ * `@partylayer/react`. The DvP finding that motivated the package helper is
4
+ * resolved; this file only adapts `allocationMatchesRequestLeg` to the array-search
5
+ * conveniences the venue view uses (find the allocation for a leg, list matched leg
6
+ * ids). No app-level amount comparator lives here anymore.
7
+ */
8
+ import { allocationMatchesRequestLeg } from '@partylayer/react/query';
9
+ import type { TokenAllocationRef, TokenAllocationRequest } from '@partylayer/react/query';
10
+
11
+ /** The allocation ref that satisfies a request's leg, if any. */
12
+ export function allocationForLeg(
13
+ request: TokenAllocationRequest,
14
+ legId: string,
15
+ allocations: TokenAllocationRef[],
16
+ ): TokenAllocationRef | undefined {
17
+ return allocations.find((a) => allocationMatchesRequestLeg(a.allocation, request, legId));
18
+ }
19
+
20
+ /** The leg ids of a request that are satisfied by the given allocations. */
21
+ export function matchedLegIds(
22
+ request: TokenAllocationRequest,
23
+ allocations: TokenAllocationRef[],
24
+ ): string[] {
25
+ return Object.keys(request.transferLegs).filter(
26
+ (legId) => !!allocationForLeg(request, legId, allocations),
27
+ );
28
+ }
@@ -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
+ }