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,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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
A [PartyLayer](https://partylayer.xyz) dApp
|
|
3
|
+
A [PartyLayer](https://partylayer.xyz) dApp: React + Vite, with Canton wallet integration.
|
|
4
4
|
|
|
5
5
|
## Develop
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ A [PartyLayer](https://partylayer.xyz) dApp — React + Vite, with Canton wallet
|
|
|
8
8
|
npm run dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Open the app and click **Connect Wallet
|
|
11
|
+
Open the app and click **Connect Wallet**. The modal lists every verified Canton wallet from the registry.
|
|
12
12
|
|
|
13
13
|
## How it's wired
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
A [PartyLayer](https://partylayer.xyz) dApp
|
|
3
|
+
A [PartyLayer](https://partylayer.xyz) dApp: vanilla TypeScript + Vite, no framework.
|
|
4
4
|
|
|
5
5
|
## Develop
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ client.on('session:disconnected', () => { /* … */ });
|
|
|
22
22
|
const active = await client.getActiveSession(); // restore on load
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`createPartyLayer` is the dApp connect API
|
|
25
|
+
`createPartyLayer` is the dApp connect API, no framework bindings needed. See `src/main.ts` for the full hand-rolled connect UI. Switch networks by changing `network` to `"testnet"` or `"mainnet"`.
|
|
26
26
|
|
|
27
27
|
## Build
|
|
28
28
|
|
|
@@ -9,7 +9,7 @@ const app = document.querySelector<HTMLDivElement>('#app')!;
|
|
|
9
9
|
app.innerHTML = `
|
|
10
10
|
<main class="app">
|
|
11
11
|
<h1>{{PROJECT_NAME}}</h1>
|
|
12
|
-
<p class="subtitle">A PartyLayer dApp on Canton
|
|
12
|
+
<p class="subtitle">A PartyLayer dApp on Canton: vanilla TypeScript, no framework.</p>
|
|
13
13
|
<div id="status" class="status"></div>
|
|
14
14
|
<button id="connect" class="btn">Connect Wallet</button>
|
|
15
15
|
<ul id="wallets" class="wallets" hidden></ul>
|
|
@@ -34,7 +34,7 @@ function renderConnected(session: Session) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function renderDisconnected() {
|
|
37
|
-
statusEl.textContent = 'Not connected
|
|
37
|
+
statusEl.textContent = 'Not connected. Connect a wallet to continue.';
|
|
38
38
|
connectBtn.hidden = false;
|
|
39
39
|
walletsEl.hidden = true;
|
|
40
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
A [PartyLayer](https://partylayer.xyz) dApp
|
|
3
|
+
A [PartyLayer](https://partylayer.xyz) dApp: Nuxt 3 + Vue 3 + Pinia, with **SSR session**.
|
|
4
4
|
|
|
5
5
|
## Develop
|
|
6
6
|
|
|
@@ -12,8 +12,8 @@ npm run dev
|
|
|
12
12
|
|
|
13
13
|
- **Server** ([lib/session.ts](lib/session.ts)) reads the session cookie via Nuxt's SSR-aware `useCookie()`, wrapped as a `CookieAdapter` and fed to `@partylayer/session`'s `createCookieStorage` → the connected party renders in the **initial HTML** (no flash). `app.vue` gates on `getServerSession()`.
|
|
14
14
|
- **Client** ([plugins/partylayer.client.ts](plugins/partylayer.client.ts)) creates the PartyLayer client and provides the session store (`createPartyLayerSession`), using `createCookieStorage()` (the same cookie, `document.cookie`).
|
|
15
|
-
- **Pinia** ([stores/session.ts](stores/session.ts)) surfaces the PartyLayer session (`isConnected` / `party` / `status` + `connect`/`disconnect`) as a store
|
|
16
|
-
- Nuxt's cookie API is used only in your app (`lib/session.ts`), never in `@partylayer/session
|
|
15
|
+
- **Pinia** ([stores/session.ts](stores/session.ts)) surfaces the PartyLayer session (`isConnected` / `party` / `status` + `connect`/`disconnect`) as a store. Components read the store. This is the idiomatic "PartyLayer + Pinia" pattern.
|
|
16
|
+
- Nuxt's cookie API is used only in your app (`lib/session.ts`), never in `@partylayer/session`. The library stays framework-agnostic (the same `CookieAdapter` the Next template feeds with `cookies()`).
|
|
17
17
|
|
|
18
18
|
## Build
|
|
19
19
|
|