create-partylayer-app 0.4.0 → 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/next-ssr/_package.json +4 -3
- package/templates/next-ssr/app/page.tsx +4 -4
- package/templates/next-ssr/app/providers.tsx +16 -6
- 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
- package/templates/react-vite/_package.json +3 -2
- package/templates/react-vite/src/App.tsx +1 -1
- package/templates/react-vite/src/main.tsx +11 -1
- package/templates/vanilla/_package.json +1 -1
- package/templates/vue-nuxt-pinia/_package.json +4 -3
- package/templates/vue-nuxt-pinia/app.vue +2 -2
- package/templates/vue-nuxt-pinia/plugins/partylayer.client.ts +9 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holdings section. Exercises useTokenHoldings (CIP-0056 typed read). Renders each
|
|
3
|
+
* holding with the owner avatar, the amount (string precision preserved), the
|
|
4
|
+
* instrument, and a lock badge when the holding carries a TokenLock.
|
|
5
|
+
*/
|
|
6
|
+
import { useTokenHoldings } from '@partylayer/react/query';
|
|
7
|
+
import { PartyAvatar } from '@partylayer/react';
|
|
8
|
+
import { useDemo, partyKey } from '../context/DemoContext';
|
|
9
|
+
import { Card, AsyncView, Badge } from '../ui/primitives';
|
|
10
|
+
import { formatAmount } from '../lib/format';
|
|
11
|
+
|
|
12
|
+
export function Holdings() {
|
|
13
|
+
const { party, backend } = useDemo();
|
|
14
|
+
const q = useTokenHoldings({
|
|
15
|
+
read: (signal) => backend.readHoldings(party, signal),
|
|
16
|
+
key: partyKey('holdings', party),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Card title="Holdings" hint="useTokenHoldings">
|
|
21
|
+
<AsyncView
|
|
22
|
+
isPending={q.isPending}
|
|
23
|
+
error={q.error}
|
|
24
|
+
data={q.holdings}
|
|
25
|
+
isEmpty={(refs) => refs.length === 0}
|
|
26
|
+
empty="No holdings yet for this party."
|
|
27
|
+
>
|
|
28
|
+
{(refs) => (
|
|
29
|
+
<ul className="list">
|
|
30
|
+
{refs.map(({ cid, holding: h }) => (
|
|
31
|
+
<li key={cid} className="row">
|
|
32
|
+
<PartyAvatar id={h.owner} size={28} />
|
|
33
|
+
<div className="row-main">
|
|
34
|
+
<div className="row-title">
|
|
35
|
+
{formatAmount(h.amount)} <span className="muted">{h.instrumentId.id}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="row-sub muted">admin {h.instrumentId.admin}</div>
|
|
38
|
+
</div>
|
|
39
|
+
{h.lock ? (
|
|
40
|
+
<Badge tone="lock" title={'expires ' + (h.lock.expiresAt ?? 'never')}>
|
|
41
|
+
Locked: {h.lock.context ?? 'held'}
|
|
42
|
+
</Badge>
|
|
43
|
+
) : null}
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
)}
|
|
48
|
+
</AsyncView>
|
|
49
|
+
</Card>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incoming section. Reads the current party's pending transfer instructions with
|
|
3
|
+
* the typed useTransferInstructions hook, and completes them with
|
|
4
|
+
* useTransferInstructionAction (accept / reject). Accept and Reject render ONLY
|
|
5
|
+
* when the instruction is in the pendingReceiverAcceptance state, matching the
|
|
6
|
+
* spec (those choices are unavailable in the internal-workflow state). On success
|
|
7
|
+
* it invalidates holdings and instructions so both sides refresh.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { useTransferInstructions, useTransferInstructionAction } from '@partylayer/react/query';
|
|
12
|
+
import { PartyAvatar, TransactionToast } from '@partylayer/react';
|
|
13
|
+
import { useDemo, partyKey } from '../context/DemoContext';
|
|
14
|
+
import { Card, AsyncView } from '../ui/primitives';
|
|
15
|
+
import { toastStatus } from '../lib/mutation';
|
|
16
|
+
import { invalidateHoldingsAndReads } from '../lib/invalidate';
|
|
17
|
+
import { formatAmount } from '../lib/format';
|
|
18
|
+
|
|
19
|
+
export function Incoming() {
|
|
20
|
+
const { party, backend } = useDemo();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const [actingCid, setActingCid] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const q = useTransferInstructions({
|
|
25
|
+
read: (signal) => backend.readIncoming(party, signal),
|
|
26
|
+
key: partyKey('incoming', party),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const action = useTransferInstructionAction<{ ok: true }>({
|
|
30
|
+
submit: (request, signal) => backend.submitTransferAction(request, signal),
|
|
31
|
+
mutation: {
|
|
32
|
+
onSuccess: () => invalidateHoldingsAndReads(queryClient),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const act = (instructionCid: string, kind: 'accept' | 'reject') => {
|
|
37
|
+
setActingCid(instructionCid);
|
|
38
|
+
action.submitAction({ instructionCid, action: kind });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Card title="Incoming" hint="useTransferInstructions + useTransferInstructionAction">
|
|
43
|
+
<AsyncView
|
|
44
|
+
isPending={q.isPending}
|
|
45
|
+
error={q.error}
|
|
46
|
+
data={q.instructions}
|
|
47
|
+
isEmpty={(items) => items.length === 0}
|
|
48
|
+
empty="No pending incoming transfers."
|
|
49
|
+
>
|
|
50
|
+
{(items) => (
|
|
51
|
+
<ul className="list">
|
|
52
|
+
{items.map(({ cid, instruction }) => {
|
|
53
|
+
const t = instruction.transfer;
|
|
54
|
+
const acceptable = instruction.status.kind === 'pendingReceiverAcceptance';
|
|
55
|
+
return (
|
|
56
|
+
<li key={cid} className="row">
|
|
57
|
+
<PartyAvatar id={t.sender} size={28} />
|
|
58
|
+
<div className="row-main">
|
|
59
|
+
<div className="row-title">
|
|
60
|
+
{formatAmount(t.amount)} <span className="muted">{t.instrumentId.id}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="row-sub muted">
|
|
63
|
+
from {t.sender} · execute before {t.executeBefore.slice(0, 10)}
|
|
64
|
+
{t.meta?.memo ? ' · ' + t.meta.memo : ''}
|
|
65
|
+
</div>
|
|
66
|
+
{!acceptable ? (
|
|
67
|
+
<div className="row-sub muted">
|
|
68
|
+
pending internal workflow
|
|
69
|
+
{instruction.status.kind === 'pendingInternalWorkflow'
|
|
70
|
+
? ' · waiting on ' + Object.keys(instruction.status.pendingActions).join(', ')
|
|
71
|
+
: ''}
|
|
72
|
+
</div>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
{acceptable ? (
|
|
76
|
+
<div className="row-actions">
|
|
77
|
+
<button
|
|
78
|
+
className="btn btn-primary"
|
|
79
|
+
disabled={action.isPending}
|
|
80
|
+
onClick={() => act(cid, 'accept')}
|
|
81
|
+
>
|
|
82
|
+
Accept
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
className="btn btn-ghost"
|
|
86
|
+
disabled={action.isPending}
|
|
87
|
+
onClick={() => act(cid, 'reject')}
|
|
88
|
+
>
|
|
89
|
+
Reject
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
</li>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
)}
|
|
98
|
+
</AsyncView>
|
|
99
|
+
|
|
100
|
+
<TransactionToast
|
|
101
|
+
status={toastStatus(action)}
|
|
102
|
+
error={action.error}
|
|
103
|
+
message={action.isSuccess ? 'Instruction ' + (actingCid ?? '') + ' completed.' : undefined}
|
|
104
|
+
/>
|
|
105
|
+
</Card>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issuer section. Visible for every party; actionable only as the issuer (a hint
|
|
3
|
+
* shows otherwise). Reads the instrument config and total supply through the
|
|
4
|
+
* generic useDamlContract, and performs registry-specific admin writes (mint,
|
|
5
|
+
* freeze/unfreeze) through the generic useChoice: the escape hatch for writes the
|
|
6
|
+
* typed CIP-0056 hooks do not cover.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
+
import { useDamlContract, useChoice, type TokenHoldingRef } from '@partylayer/react/query';
|
|
11
|
+
import { TransactionToast } from '@partylayer/react';
|
|
12
|
+
import { useDemo, partyKey } from '../context/DemoContext';
|
|
13
|
+
import { Card, AsyncView, Badge, Field } from '../ui/primitives';
|
|
14
|
+
import { toastStatus } from '../lib/mutation';
|
|
15
|
+
import { invalidateHoldingsAndReads } from '../lib/invalidate';
|
|
16
|
+
import { formatAmount, isPositiveAmount } from '../lib/format';
|
|
17
|
+
import { PARTIES, PARTY_ORDER } from '../lib/fixtures';
|
|
18
|
+
import type { IssuerChoice } from '../lib/backend';
|
|
19
|
+
import type { DemoPartyKey, InstrumentConfig } from '../lib/types';
|
|
20
|
+
|
|
21
|
+
export function Issuer() {
|
|
22
|
+
const { party, backend } = useDemo();
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
const isIssuer = party === 'issuer';
|
|
25
|
+
|
|
26
|
+
const instrument = useDamlContract<InstrumentConfig>({
|
|
27
|
+
read: (signal) => backend.readInstrument(signal),
|
|
28
|
+
key: ['tokenization', 'instrument'],
|
|
29
|
+
});
|
|
30
|
+
const supply = useDamlContract<string>({
|
|
31
|
+
read: (signal) => backend.readSupply(signal),
|
|
32
|
+
key: ['tokenization', 'supply'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const [mintTarget, setMintTarget] = useState<DemoPartyKey>('alice');
|
|
36
|
+
const [mintAmount, setMintAmount] = useState('');
|
|
37
|
+
const [freezeTarget, setFreezeTarget] = useState<DemoPartyKey>('alice');
|
|
38
|
+
|
|
39
|
+
const refreshAll = () => invalidateHoldingsAndReads(queryClient);
|
|
40
|
+
|
|
41
|
+
const issuerChoice = useChoice<{ ok: true }, IssuerChoice>({
|
|
42
|
+
exercise: (choice) => backend.submitIssuerChoice(choice),
|
|
43
|
+
mutation: { onSuccess: refreshAll },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const freezeRefs = useDamlContract<TokenHoldingRef[]>({
|
|
47
|
+
read: (signal) => backend.readHoldingRefs(freezeTarget, signal),
|
|
48
|
+
key: partyKey('holdingRefs', freezeTarget),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const doMint = () => {
|
|
52
|
+
if (!isPositiveAmount(mintAmount)) return;
|
|
53
|
+
issuerChoice.exerciseChoice({ kind: 'mint', toParty: mintTarget, amount: mintAmount });
|
|
54
|
+
setMintAmount('');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const toggleFreeze = (ref: TokenHoldingRef) => {
|
|
58
|
+
issuerChoice.exerciseChoice({
|
|
59
|
+
kind: 'setFrozen',
|
|
60
|
+
party: freezeTarget,
|
|
61
|
+
cid: ref.cid,
|
|
62
|
+
frozen: !ref.holding.lock,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Card title="Issuer" hint="useDamlContract + useChoice">
|
|
68
|
+
<div className="issuer-summary">
|
|
69
|
+
<AsyncView
|
|
70
|
+
isPending={instrument.isPending}
|
|
71
|
+
error={instrument.error}
|
|
72
|
+
data={instrument.contract}
|
|
73
|
+
isEmpty={() => false}
|
|
74
|
+
empty=""
|
|
75
|
+
rows={2}
|
|
76
|
+
>
|
|
77
|
+
{(cfg) => (
|
|
78
|
+
<div>
|
|
79
|
+
<div className="row-title">
|
|
80
|
+
{cfg.name} <span className="muted">{cfg.id}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="row-sub muted">{cfg.description}</div>
|
|
83
|
+
<div className="row-sub muted">admin {cfg.admin}</div>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</AsyncView>
|
|
87
|
+
<div className="supply">
|
|
88
|
+
<span className="muted">Total supply</span>
|
|
89
|
+
<strong>{supply.isPending ? '...' : formatAmount(supply.contract ?? '0.00')}</strong>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{!isIssuer ? (
|
|
94
|
+
<div className="state state-empty">
|
|
95
|
+
Switch the demo party to <strong>Issuer</strong> to mint and freeze.
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
<div className="form-grid">
|
|
100
|
+
<Field label="Mint to">
|
|
101
|
+
<select value={mintTarget} onChange={(e) => setMintTarget(e.target.value as DemoPartyKey)}>
|
|
102
|
+
{PARTY_ORDER.map((p) => (
|
|
103
|
+
<option key={p} value={p}>
|
|
104
|
+
{PARTIES[p].label}
|
|
105
|
+
</option>
|
|
106
|
+
))}
|
|
107
|
+
</select>
|
|
108
|
+
</Field>
|
|
109
|
+
<Field label="Amount">
|
|
110
|
+
<input
|
|
111
|
+
inputMode="decimal"
|
|
112
|
+
placeholder="0.00"
|
|
113
|
+
value={mintAmount}
|
|
114
|
+
onChange={(e) => setMintAmount(e.target.value)}
|
|
115
|
+
/>
|
|
116
|
+
</Field>
|
|
117
|
+
<div className="field field-action">
|
|
118
|
+
<button className="btn btn-primary" onClick={doMint} disabled={issuerChoice.isPending}>
|
|
119
|
+
Mint
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="freeze">
|
|
125
|
+
<Field label="Freeze holdings of">
|
|
126
|
+
<select value={freezeTarget} onChange={(e) => setFreezeTarget(e.target.value as DemoPartyKey)}>
|
|
127
|
+
{PARTY_ORDER.map((p) => (
|
|
128
|
+
<option key={p} value={p}>
|
|
129
|
+
{PARTIES[p].label}
|
|
130
|
+
</option>
|
|
131
|
+
))}
|
|
132
|
+
</select>
|
|
133
|
+
</Field>
|
|
134
|
+
<AsyncView
|
|
135
|
+
isPending={freezeRefs.isPending}
|
|
136
|
+
error={freezeRefs.error}
|
|
137
|
+
data={freezeRefs.contract}
|
|
138
|
+
isEmpty={(refs) => refs.length === 0}
|
|
139
|
+
empty="No holdings to freeze."
|
|
140
|
+
rows={2}
|
|
141
|
+
>
|
|
142
|
+
{(refs) => (
|
|
143
|
+
<ul className="list">
|
|
144
|
+
{refs.map((ref) => (
|
|
145
|
+
<li key={ref.cid} className="row">
|
|
146
|
+
<div className="row-main">
|
|
147
|
+
<div className="row-title">
|
|
148
|
+
{formatAmount(ref.holding.amount)}{' '}
|
|
149
|
+
<span className="muted">{ref.holding.instrumentId.id}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="row-sub muted">{ref.cid}</div>
|
|
152
|
+
</div>
|
|
153
|
+
{ref.holding.lock ? <Badge tone="lock">Frozen</Badge> : null}
|
|
154
|
+
<button
|
|
155
|
+
className="btn btn-ghost"
|
|
156
|
+
disabled={issuerChoice.isPending}
|
|
157
|
+
onClick={() => toggleFreeze(ref)}
|
|
158
|
+
>
|
|
159
|
+
{ref.holding.lock ? 'Unfreeze' : 'Freeze'}
|
|
160
|
+
</button>
|
|
161
|
+
</li>
|
|
162
|
+
))}
|
|
163
|
+
</ul>
|
|
164
|
+
)}
|
|
165
|
+
</AsyncView>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<TransactionToast
|
|
169
|
+
status={toastStatus(issuerChoice)}
|
|
170
|
+
error={issuerChoice.error}
|
|
171
|
+
message={issuerChoice.isSuccess ? 'Issuer action applied.' : undefined}
|
|
172
|
+
/>
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</Card>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transfer section. Builds a standard TokenTransfer and submits it through
|
|
3
|
+
* useTransferInstruction. Shows CostPreview before confirm and drives a
|
|
4
|
+
* TransactionToast from the mutation state. On success it invalidates the holdings
|
|
5
|
+
* and incoming query keys so both sides refresh.
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useTransferInstruction, type TokenTransfer } from '@partylayer/react/query';
|
|
10
|
+
import { CostPreview, TransactionToast } from '@partylayer/react';
|
|
11
|
+
import { useDemo } from '../context/DemoContext';
|
|
12
|
+
import { Card, Field } from '../ui/primitives';
|
|
13
|
+
import { toastStatus } from '../lib/mutation';
|
|
14
|
+
import { invalidateHoldingsAndReads } from '../lib/invalidate';
|
|
15
|
+
import { PARTIES, PARTY_ORDER, INSTRUMENT, FEE_ESTIMATE } from '../lib/fixtures';
|
|
16
|
+
import { demoStore } from '../lib/store';
|
|
17
|
+
import { formatAmount, isPositiveAmount } from '../lib/format';
|
|
18
|
+
import type { DemoPartyKey } from '../lib/types';
|
|
19
|
+
|
|
20
|
+
export function Transfer() {
|
|
21
|
+
const { party, backend } = useDemo();
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
|
|
24
|
+
const others = PARTY_ORDER.filter((p) => p !== party);
|
|
25
|
+
const [receiver, setReceiver] = useState<DemoPartyKey>(others[0]);
|
|
26
|
+
const [amount, setAmount] = useState('');
|
|
27
|
+
const [memo, setMemo] = useState('');
|
|
28
|
+
|
|
29
|
+
const mutation = useTransferInstruction<{ ok: true }>({
|
|
30
|
+
submit: (transfer, signal) => backend.submitTransfer(transfer, signal),
|
|
31
|
+
mutation: {
|
|
32
|
+
onSuccess: () => {
|
|
33
|
+
// Refresh every party's holdings and incoming so both sides update.
|
|
34
|
+
invalidateHoldingsAndReads(queryClient);
|
|
35
|
+
setAmount('');
|
|
36
|
+
setMemo('');
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Keep the receiver valid when the acting party changes.
|
|
42
|
+
const receiverKey = others.includes(receiver) ? receiver : others[0];
|
|
43
|
+
const balance = demoStore.balanceOf(party);
|
|
44
|
+
const valid = isPositiveAmount(amount);
|
|
45
|
+
|
|
46
|
+
const onConfirm = () => {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const transfer: TokenTransfer = {
|
|
49
|
+
sender: PARTIES[party].partyId,
|
|
50
|
+
receiver: PARTIES[receiverKey].partyId,
|
|
51
|
+
amount,
|
|
52
|
+
instrumentId: { admin: INSTRUMENT.admin, id: INSTRUMENT.id },
|
|
53
|
+
requestedAt: now.toISOString(),
|
|
54
|
+
executeBefore: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
55
|
+
inputHoldingCids: demoStore.unlockedCids(party),
|
|
56
|
+
meta: memo ? { memo } : {},
|
|
57
|
+
};
|
|
58
|
+
mutation.submitTransfer(transfer);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card title="Transfer" hint="useTransferInstruction">
|
|
63
|
+
<div className="muted balance-line">
|
|
64
|
+
Available to send: <strong>{formatAmount(balance)}</strong> {INSTRUMENT.id}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="form-grid">
|
|
68
|
+
<Field label="Receiver">
|
|
69
|
+
<select value={receiverKey} onChange={(e) => setReceiver(e.target.value as DemoPartyKey)}>
|
|
70
|
+
{others.map((p) => (
|
|
71
|
+
<option key={p} value={p}>
|
|
72
|
+
{PARTIES[p].label}
|
|
73
|
+
</option>
|
|
74
|
+
))}
|
|
75
|
+
</select>
|
|
76
|
+
</Field>
|
|
77
|
+
<Field label={'Amount (' + INSTRUMENT.id + ')'}>
|
|
78
|
+
<input
|
|
79
|
+
inputMode="decimal"
|
|
80
|
+
placeholder="0.00"
|
|
81
|
+
value={amount}
|
|
82
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
83
|
+
/>
|
|
84
|
+
</Field>
|
|
85
|
+
<Field label="Memo (optional)">
|
|
86
|
+
<input value={memo} placeholder="what is it for" onChange={(e) => setMemo(e.target.value)} />
|
|
87
|
+
</Field>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{valid ? (
|
|
91
|
+
<div className="review">
|
|
92
|
+
<div className="review-line">
|
|
93
|
+
Send <strong>{formatAmount(amount)}</strong> {INSTRUMENT.id} to{' '}
|
|
94
|
+
<strong>{PARTIES[receiverKey].label}</strong>
|
|
95
|
+
</div>
|
|
96
|
+
<CostPreview estimate={FEE_ESTIMATE} />
|
|
97
|
+
<button className="btn btn-primary" onClick={onConfirm} disabled={mutation.isPending}>
|
|
98
|
+
{mutation.isPending ? 'Submitting...' : 'Confirm transfer'}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="muted hint-line">Enter a positive amount to preview the fee and confirm.</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<TransactionToast
|
|
106
|
+
status={toastStatus(mutation)}
|
|
107
|
+
error={mutation.error}
|
|
108
|
+
message={mutation.isSuccess ? 'Transfer sent. The receiver can now accept it.' : undefined}
|
|
109
|
+
/>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@partylayer/react": "^
|
|
13
|
-
"@partylayer/sdk": "^0.
|
|
12
|
+
"@partylayer/react": "^2.1.0",
|
|
13
|
+
"@partylayer/sdk": "^0.15.0",
|
|
14
|
+
"@tanstack/react-query": "^5.0.0",
|
|
14
15
|
"react": "^18.3.1",
|
|
15
16
|
"react-dom": "^18.3.1"
|
|
16
17
|
},
|
|
@@ -12,7 +12,7 @@ export default function App() {
|
|
|
12
12
|
<PartyLayerKit network="devnet" appName="{{PROJECT_NAME}}">
|
|
13
13
|
<main className="app">
|
|
14
14
|
<h1>{{PROJECT_NAME}}</h1>
|
|
15
|
-
<p className="subtitle">A PartyLayer dApp on Canton
|
|
15
|
+
<p className="subtitle">A PartyLayer dApp on Canton: connect any verified wallet.</p>
|
|
16
16
|
<ConnectButton />
|
|
17
17
|
<SessionPanel />
|
|
18
18
|
</main>
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
4
|
import App from './App';
|
|
4
5
|
import './index.css';
|
|
5
6
|
|
|
7
|
+
// PartyLayer's data hooks (the `@partylayer/react/query` entrypoint:
|
|
8
|
+
// useDamlContract, useChoice, the cost hooks) are built on TanStack Query, so
|
|
9
|
+
// they need a QueryClient in context. Create one here and wrap the app. The base
|
|
10
|
+
// session surface (PartyLayerKit, ConnectButton, useAccount) works without it,
|
|
11
|
+
// but having it set up means the query hooks are ready when you reach for them.
|
|
12
|
+
const queryClient = new QueryClient();
|
|
13
|
+
|
|
6
14
|
createRoot(document.getElementById('root')!).render(
|
|
7
15
|
<StrictMode>
|
|
8
|
-
<
|
|
16
|
+
<QueryClientProvider client={queryClient}>
|
|
17
|
+
<App />
|
|
18
|
+
</QueryClientProvider>
|
|
9
19
|
</StrictMode>,
|
|
10
20
|
);
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"postinstall": "nuxt prepare"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@partylayer/sdk": "^0.
|
|
14
|
-
"@partylayer/session": "^1.1.
|
|
15
|
-
"@partylayer/vue": "^0.1
|
|
13
|
+
"@partylayer/sdk": "^0.15.0",
|
|
14
|
+
"@partylayer/session": "^1.1.4",
|
|
15
|
+
"@partylayer/vue": "^1.0.1",
|
|
16
16
|
"@pinia/nuxt": "^0.5.5",
|
|
17
|
+
"@tanstack/vue-query": "^5.0.0",
|
|
17
18
|
"pinia": "^2.2.0",
|
|
18
19
|
"vue": "^3.5.0"
|
|
19
20
|
},
|
|
@@ -15,14 +15,14 @@ const sessionStore = useSessionStore();
|
|
|
15
15
|
<template>
|
|
16
16
|
<main class="app">
|
|
17
17
|
<h1>{{PROJECT_NAME}}</h1>
|
|
18
|
-
<p class="subtitle">A PartyLayer dApp on Canton
|
|
18
|
+
<p class="subtitle">A PartyLayer dApp on Canton: Nuxt 3 + Pinia, SSR session.</p>
|
|
19
19
|
|
|
20
20
|
<!-- SERVER-RENDERED party from the cookie (initial HTML, no flash) -->
|
|
21
21
|
<p v-if="serverParty" class="session">
|
|
22
22
|
Connected as <code data-testid="server-party">{{ truncateParty(serverParty) }}</code>
|
|
23
23
|
</p>
|
|
24
24
|
<p v-else class="session" data-testid="server-cta">
|
|
25
|
-
Not connected
|
|
25
|
+
Not connected: connect a wallet to continue.
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
<!-- Pinia-backed interactive session (reads the Pinia store, not the composables) -->
|