Skip to content

Commit 4a1af79

Browse files
authored
fix(token): default role to app-provider so --instance alone resolves the endpoint (#307)
* fix(token): default role to app-provider so --instance alone resolves the endpoint Endpoint auto-resolution already worked, but every layer defaulted the role to app-user, so instance-only token commands resolved the app-user participant instead of the provider. Make app-provider the single canonical default across the token CLI flags, the shared resolvers (token.ResolveLedgerEndpoint, localnet.ResolveLedgerEndpoint), the roleOrDefault/opts.Role fallbacks, and the Web UI roleFromQuery so CLI and UI stay at parity. Add endpoint-optional tests: a token verb resolves its ledger endpoint from --instance alone (default role) and from --instance + --role picks that role's captured participant port, with --endpoint always optional. * fix(token): address PR #307 review — UI default, Explorer isolation, endpoint visibility - Align Tokens frontend DEFAULT_ROLE / tokenQuery omission with app-provider - Restore shared localnet.ResolveLedgerEndpoint empty-role default to app-user (Explorer) - Surface resolved host:port + role via resolveEndpoint stderr and activity JSON/UI - Return ErrUnresolvedLedgerEndpoint from RunMint/RunBurn before instrument checks - Document --role default change; expand mint/burn/accept/UI regression coverage * fix(explorer): align empty-role default with shared app-provider resolver Instead of isolating the token default, keep ResolveLedgerEndpoint at app-provider and update Explorer CLI flags, Web handlers, and the Explorer screen so omitting --role / ?role= cannot drift from the shared resolver. DAR remains app-user. * fix(dar): default role to app-provider across CLI, UI, and Wallet Bring DAR list/inspect/upload empty-role fallback and the DAR/Wallet screen pickers in line with the canonical app-provider default already used by Tokens and Explorer. Update docs and mocks accordingly. * fix(token): echo resolved endpoint/role in JSON+UI and pin every role default Addresses the two open review findings on the endpoint-optional PR. Finding "endpoint contract not universal": add a shared api/types.ResolvedEndpoint (endpoint + role) and embed it in the TokenListResponse, TokenHoldingsResponse, AllocationsResponse, and PendingTransfersResponse bodies, then populate it at every surface: - CLI: token ls / balance / allocations / transfers now set the resolved host:port + role on the JSON envelope (activity already carried them). - Web UI: handleTokensList / handleTokenHoldings / handleAllocationsList / handlePendingTransfersList echo the same endpoint + role. Mirror the new fields in frontend/src/api.ts and regenerate the schema-shape golden (add-only; no SchemaVersion bump). Finding "partial default coverage": expand TestCLIRoleFlags_DefaultAppProvider from six builders to every token CLI builder that carries a --role flag (allocate, allocations, allocations withdraw/cancel, balance, balances, create, demo, faucet, party new/ls, summary, transfers), so any verb regressing to app-user fails loudly. Add regression tests asserting the endpoint/role fields appear on the Web UI list/holdings JSON (offline fallback + live endpoint) and the CLI balance JSON, and update docs/changes-from-proposal.md to describe the fields precisely.
1 parent bd2297d commit 4a1af79

70 files changed

Lines changed: 1127 additions & 249 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/changes-from-proposal.md‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Every deviation listed here is **intentional**, not an oversight or implementati
4444
- [`burn` requires explicit confirmation](#burn-requires-explicit-confirmation)
4545
- [`--instance` required flag](#--instance-required-flag)
4646
- [`--name` collision in `token create`](#--name-collision-in-token-create)
47+
- [`token ls` — list instruments (new)](#token-ls--list-instruments-new)
48+
- [Ledger endpoint auto-resolves on every token verb](#ledger-endpoint-auto-resolves-on-every-token-verb)
49+
- [`--role` defaults to `app-provider`](#--role-defaults-to-app-provider)
50+
- [`token create` issuer defaults to the acting role's party](#token-create-issuer-defaults-to-the-acting-roles-party)
51+
- [phantom `token settle` reference removed](#phantom-token-settle-reference-removed)
4752
- [`telemetry` (root-level, new)](#telemetry-root-level-new)
4853

4954
---
@@ -289,7 +294,7 @@ The embedded skill docs are the same artifacts that back the Web UI's Agent Skil
289294

290295
**Proposal said:** DAR commands connect to participants via the named instance implicitly.
291296

292-
**Shipped:** Each `dar` subcommand carries its own connection flags: `--admin-host`, `--token`, `--insecure` (defaults to `true`), `--ca-cert`, `--instance` (alias `--name`), `--role` (default `app-user`). There is no standalone `dar connect` command.
297+
**Shipped:** Each `dar` subcommand carries its own connection flags: `--admin-host`, `--token`, `--insecure` (defaults to `true`), `--ca-cert`, `--instance` (alias `--name`), `--role` (default `app-provider`). There is no standalone `dar connect` command.
293298

294299
**Why:** Per-command connection flags make the DAR subcommands usable against any Ledger API endpoint, not just DevKit-managed instances. This gives operators more flexibility in CI and multi-environment workflows without requiring a running LocalNet registry.
295300

@@ -406,12 +411,22 @@ The embedded skill docs are the same artifacts that back the Web UI's Agent Skil
406411

407412
**Proposal said:** the token commands' connection details were unspecified beyond selecting an instance.
408413

409-
**Shipped:** every `token` verb resolves the participant gRPC endpoint from the instance's captured ports when `--endpoint` is omitted — matching the Web UI, which never asks for an endpoint. `--endpoint` stays an explicit override; when no port is captured the command prints a remediation and exits non-zero rather than failing opaquely. `party ls` and `token ls` resolve best-effort (falling back to the registry-only list when offline).
414+
**Shipped:** every `token` verb resolves the participant gRPC endpoint from the instance's captured ports when `--endpoint` is omitted — matching the Web UI, which never asks for an endpoint. `--endpoint` stays an explicit override; when no port is captured the command prints a remediation and exits non-zero rather than failing opaquely. `party ls` and `token ls` resolve best-effort (falling back to the registry-only list when offline). Successful resolution announces the host:port and role — on CLI stderr for every verb, and as `endpoint`/`role` fields on the shared JSON/UI response bodies (`token ls`, `token balance`, `token allocations`, `token transfers`, `token activity` and their Web UI counterparts, via `api/types.ResolvedEndpoint`) — so operators can see which participant a verb dialed regardless of surface.
410415

411416
**Why:** Requiring a hand-typed `--endpoint` on a command the Web UI runs endpoint-free breaks CLI ↔ Web UI parity and makes the common case (`token <verb> --instance foo`) unusable on a running LocalNet.
412417

413418
---
414419

420+
### `--role` defaults to `app-provider`
421+
422+
**Proposal said:** role selection was unspecified; early implementation defaulted `--role` / `?role=` to `app-user`.
423+
424+
**Shipped:** every participant-scoped surface falls back to **`app-provider`** when role is omitted: token CLI/UI (`token.DefaultRole`, `roleFromQuery`, `tokenQuery`), Explorer (`internal/localnet.ResolveLedgerEndpoint`, `contracts` / `tx`, Web ACS/transactions handlers, Explorer screen), DAR (`dar --role`, Web DAR list/inspect/upload fallback, DAR screen), and the Wallet screen's role picker. Explicit `--role` / `?role=` / upload `roles` still win.
425+
426+
**Why:** The provider participant is the mint/create operator on LocalNet. Defaulting to `app-user` made `--instance` alone resolve the wrong (often uncaptured) participant ledger port, so endpoint auto-resolution looked broken. Aligning every surface on `app-provider` restores instance-only usability and keeps CLI ↔ Web UI on the same participant.
427+
428+
---
429+
415430
### `token create` issuer defaults to the acting role's party
416431

417432
**Proposal said:** `token create` names an issuer but did not specify how a bare alias or role name resolves.

‎docs/explorer.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ The top-bar **Projecting through** widget controls two things:
3838
sticks to the URL's `?instance=` query parameter, so it
3939
survives navigating between tabs and reloading the page.
4040
2. **Role** — which Splice JWT to authenticate with. The choices
41-
are `app-user`, `app-provider`, and `sv`. Each role is a
41+
are `app-provider` (default), `app-user`, and `sv`. Each role is a
4242
different user in the participant's user management; the
4343
contracts you see are the ones that role's user has rights to
44-
read.
44+
read. The default matches the CLI (`contracts` / `tx --role`) and
45+
the shared ledger-endpoint resolver.
4546

4647
Switching either control re-runs the ACS snapshot. There is no
4748
client-side filtering across roles — you get a fresh view from the

‎docs/tokens.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ canton-devkit localnet up --name v2 --version 0.6.12
3333
canton-devkit localnet status --name v2
3434
```
3535

36-
All token subcommands take `--instance <name>` and, for on-ledger
37-
actions, `--endpoint <participant host:port>` (the participant ledger
38-
gRPC port — `localnet status --name v2` prints it). Where a `--token`
39-
flag exists it can stay empty — a per-role dev JWT is auto-issued
36+
All token subcommands take `--instance <name>`. For on-ledger actions,
37+
`--endpoint <participant host:port>` is optional: when omitted the
38+
command resolves the acting role's captured participant ledger gRPC
39+
port from the instance (the same port `localnet status --name v2`
40+
prints). An explicit `--endpoint` always wins. Where a `--token` flag
41+
exists it can stay empty — a per-role dev JWT is auto-issued
4042
(`mint`/`create`/`demo` always auto-issue); `--role` defaults to
41-
`app-user`.
43+
`app-provider` (the provider participant that mints/creates).
4244

4345
---
4446

‎frontend/mock/router.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,8 @@ function handleTokensRoute(
555555
current_role?: string;
556556
};
557557
identity.instance = instance;
558-
identity.available_roles = identity.available_roles ?? ["app-user", "app-provider", "sv"];
559-
identity.current_role = url.searchParams.get("role") ?? identity.current_role ?? "app-user";
558+
identity.available_roles = identity.available_roles ?? ["app-provider", "app-user", "sv"];
559+
identity.current_role = url.searchParams.get("role") ?? identity.current_role ?? "app-provider";
560560
jsonResponse(res, 200, identity);
561561
return true;
562562
}

‎frontend/mock/seed-utils.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export function validateSchema(body: unknown): boolean {
183183
export function parseSeedArgs(argv: string[]): SeedOptions | { error: string } {
184184
let baseUrl = "http://127.0.0.1:7777";
185185
let instance = "";
186-
let role = "app-user";
186+
let role = "app-provider";
187187
let asName: string | undefined;
188188
let dryRun = false;
189189

@@ -199,7 +199,7 @@ export function parseSeedArgs(argv: string[]): SeedOptions | { error: string } {
199199
error: [
200200
"Usage: npm run mock:seed -- --instance <name> [options]",
201201
" --base-url URL Backend base URL (default http://127.0.0.1:7777)",
202-
" --role ROLE Role for scoped endpoints (default app-user)",
202+
" --role ROLE Role for scoped endpoints (default app-provider)",
203203
" --as NAME Rewrite instance name in output (e.g. demo)",
204204
" --dry-run Print targets without writing files",
205205
].join("\n"),

‎frontend/src/api.ts‎

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -711,9 +711,9 @@ export interface MetricsSummary {
711711
// ── DAR Manager ─────────────────────────────────────────
712712
//
713713
// The Web UI lists DARs uploaded to a participant. Role defaults
714-
// to app-user (the common dev target). The backend reads the
715-
// per-role admin port from state.json so the browser
716-
// doesn't need to know about gRPC.
714+
// to app-provider (same canonical default as Tokens / Explorer).
715+
// The backend reads the per-role admin port from state.json so the
716+
// browser doesn't need to know about gRPC.
717717
export type Role = "app-user" | "app-provider" | "sv";
718718

719719
export interface DARRow {
@@ -734,7 +734,7 @@ export interface DARListResponse {
734734
dars: DARRow[];
735735
}
736736

737-
export const fetchDARList = (name: string, role: Role = "app-user") =>
737+
export const fetchDARList = (name: string, role: Role = "app-provider") =>
738738
apiFetch<DARListResponse>(
739739
`/api/instances/${encodeURIComponent(name)}/dar?role=${role}`,
740740
);
@@ -859,7 +859,7 @@ export interface DARInspectResponse {
859859
export const fetchDARInspect = (
860860
instance: string,
861861
mainID: string,
862-
role: Role = "app-user",
862+
role: Role = "app-provider",
863863
) =>
864864
apiFetch<DARInspectResponse>(
865865
`/api/instances/${encodeURIComponent(instance)}/dar/${encodeURIComponent(mainID)}/inspect?role=${role}`,
@@ -916,7 +916,7 @@ export const fetchDARDiff = (
916916
instance: string,
917917
a: string,
918918
b: string,
919-
role: Role = "app-user",
919+
role: Role = "app-provider",
920920
) =>
921921
apiFetch<DARDiffResponse>(
922922
`/api/instances/${encodeURIComponent(instance)}/dar/diff?a=${encodeURIComponent(a)}&b=${encodeURIComponent(b)}&role=${role}`,
@@ -1021,7 +1021,7 @@ export interface ContractsListResponse {
10211021

10221022
export const fetchContracts = (
10231023
name: string,
1024-
role: Role = "app-user",
1024+
role: Role = "app-provider",
10251025
limit = 100,
10261026
) =>
10271027
apiFetch<ContractsListResponse>(
@@ -1057,7 +1057,7 @@ export interface ContractDetailResponse {
10571057
export const fetchContractDetail = (
10581058
name: string,
10591059
contractId: string,
1060-
role: Role = "app-user",
1060+
role: Role = "app-provider",
10611061
) =>
10621062
apiFetch<ContractDetailResponse>(
10631063
`/api/instances/${encodeURIComponent(name)}/contracts/${encodeURIComponent(contractId)}?role=${role}`,
@@ -1094,7 +1094,7 @@ export interface ContractStreamEvent {
10941094
// (strict-tail semantics — direct curl, smoke tests).
10951095
export function openContractsStream(
10961096
name: string,
1097-
role: Role = "app-user",
1097+
role: Role = "app-provider",
10981098
since?: number,
10991099
): EventSource {
11001100
const sinceParam =
@@ -1161,7 +1161,7 @@ export interface TransactionFilters {
11611161

11621162
export const fetchTransactions = (
11631163
name: string,
1164-
role: Role = "app-user",
1164+
role: Role = "app-provider",
11651165
limit = 100,
11661166
filters: TransactionFilters = {},
11671167
) => {
@@ -1216,7 +1216,7 @@ export interface TxReplayResponse {
12161216
export const fetchTxReplay = (
12171217
name: string,
12181218
updateId: string,
1219-
role: Role = "app-user",
1219+
role: Role = "app-provider",
12201220
parties?: string[],
12211221
) => {
12221222
const params = new URLSearchParams({ role });
@@ -1601,11 +1601,21 @@ export interface TokenHolding {
16011601

16021602
export interface TokensListResponse {
16031603
schema_version: number;
1604+
// Endpoint contract: the participant host:port + role this list was
1605+
// resolved against ("" endpoint = recorded/offline fallback). Mirrors
1606+
// internal/api/types.ResolvedEndpoint.
1607+
endpoint: string;
1608+
role: string;
16041609
tokens: TokenRef[];
16051610
}
16061611

16071612
export interface TokenHoldingsResponse {
16081613
schema_version: number;
1614+
// Endpoint contract: the participant host:port + role this balance was
1615+
// read from ("" endpoint = registry pseudo-balance). Mirrors
1616+
// internal/api/types.ResolvedEndpoint.
1617+
endpoint: string;
1618+
role: string;
16091619
// Response-level provenance (matches every row's source); the UI
16101620
// renders one disclaimer banner when "registry".
16111621
source: HoldingSource;
@@ -1700,11 +1710,13 @@ export interface BatchResult {
17001710
ok: boolean;
17011711
}
17021712

1703-
// Matches the backend default in roleFromQuery — keep in sync.
1704-
const DEFAULT_ROLE = "app-user";
1713+
// Matches the backend token.DefaultRole / roleFromQuery — keep in sync.
1714+
// Explorer and DAR callers below keep their own app-user defaults.
1715+
const DEFAULT_ROLE = "app-provider";
17051716

17061717
// tokenQuery builds `?instance=...&role=...` (role omitted when it just
17071718
// repeats the default), so every token endpoint forwards role uniformly.
1719+
// Selecting app-user must send role=app-user (no longer the omitted default).
17081720
function tokenQuery(instance: string, role?: string): string {
17091721
const params = new URLSearchParams({ instance });
17101722
if (role && role !== DEFAULT_ROLE) params.set("role", role);
@@ -1807,13 +1819,13 @@ interface PartiesResponse {
18071819
}
18081820

18091821
// Lists the instance's registered party aliases.
1810-
export const fetchParties = (instance: string, role = "app-user") =>
1822+
export const fetchParties = (instance: string, role = "app-provider") =>
18111823
apiFetch<PartiesResponse>(
18121824
`/api/parties?instance=${encodeURIComponent(instance)}&role=${encodeURIComponent(role)}`,
18131825
).then((r) => r.parties);
18141826

18151827
// Allocates a party under an alias and grants the role's user act/read-as.
1816-
export const createParty = (instance: string, alias: string, role = "app-user") =>
1828+
export const createParty = (instance: string, alias: string, role = "app-provider") =>
18171829
apiFetch<PartyRef>(`/api/parties?instance=${encodeURIComponent(instance)}`, {
18181830
method: "POST",
18191831
headers: { "Content-Type": "application/json" },
@@ -1845,7 +1857,7 @@ interface HoldingContractsResponse {
18451857
// TokenRef list (mapped to InstrumentRef) when the ledger is unreachable.
18461858
export async function fetchInstruments(
18471859
instance: string,
1848-
role = "app-user",
1860+
role = "app-provider",
18491861
): Promise<InstrumentRef[]> {
18501862
const r = await apiFetch<InstrumentsResponse>(
18511863
`/api/tokens?instance=${encodeURIComponent(instance)}&role=${encodeURIComponent(role)}`,
@@ -1864,7 +1876,7 @@ export async function fetchInstruments(
18641876
}));
18651877
}
18661878

1867-
export const fetchMatrix = (instance: string, role = "app-user") =>
1879+
export const fetchMatrix = (instance: string, role = "app-provider") =>
18681880
apiFetch<MatrixResponse>(
18691881
`/api/tokens/matrix?instance=${encodeURIComponent(instance)}&role=${encodeURIComponent(role)}`,
18701882
).then((r) => r.matrix);
@@ -1899,7 +1911,7 @@ interface InstrumentSummaryResponse {
18991911
export const fetchInstrumentSummary = (
19001912
instance: string,
19011913
symbol: string,
1902-
role = "app-user",
1914+
role = "app-provider",
19031915
) =>
19041916
apiFetch<InstrumentSummaryResponse>(
19051917
`/api/tokens/${encodeURIComponent(symbol)}/summary?instance=${encodeURIComponent(
@@ -1948,7 +1960,7 @@ export interface ActivityPage {
19481960
export const fetchActivity = (
19491961
instance: string,
19501962
symbol: string,
1951-
role = "app-user",
1963+
role = "app-provider",
19521964
limit = 50,
19531965
): Promise<ActivityPage> =>
19541966
apiFetch<ActivityResponse>(
@@ -1961,7 +1973,7 @@ export const fetchHoldingContracts = (
19611973
instance: string,
19621974
symbol: string,
19631975
party: string,
1964-
role = "app-user",
1976+
role = "app-provider",
19651977
) => {
19661978
const params = new URLSearchParams({
19671979
instance,
@@ -2126,7 +2138,7 @@ export async function planTransfer(
21262138
symbol: string,
21272139
from: string,
21282140
amount: string,
2129-
role = "app-user",
2141+
role = "app-provider",
21302142
): Promise<TransferPlan> {
21312143
const params = new URLSearchParams({ instance, role, plan: "1" });
21322144
const resp = await fetch(
@@ -2177,6 +2189,10 @@ export const acceptTransfer = (
21772189

21782190
interface AllocationsListResponse {
21792191
schema_version: number;
2192+
// Endpoint contract: participant host:port + role this scan resolved
2193+
// against. Mirrors internal/api/types.ResolvedEndpoint.
2194+
endpoint: string;
2195+
role: string;
21802196
allocations: AllocationSummary[];
21812197
aliases: AliasMap;
21822198
}
@@ -2199,6 +2215,10 @@ export async function fetchAllocations(
21992215

22002216
interface PendingTransfersListResponse {
22012217
schema_version: number;
2218+
// Endpoint contract: participant host:port + role this scan resolved
2219+
// against. Mirrors internal/api/types.ResolvedEndpoint.
2220+
endpoint: string;
2221+
role: string;
22022222
pending_transfers: TransferSummary[];
22032223
truncated?: boolean;
22042224
aliases: AliasMap;

‎frontend/src/screens/DARScreen.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ function withProviders(ui: React.ReactElement) {
132132
const fakeDAR = {
133133
schema_version: 1,
134134
instance: "demo",
135-
role: "app-user",
135+
role: "app-provider",
136136
dars: [
137137
{ main: "a".repeat(64), name: "demo-pkg", version: "1.0.0" },
138138
{ main: "b".repeat(64), name: "other-pkg", version: "0.1.0" },

‎frontend/src/screens/DARScreen.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import { DARDiff } from "./DARDiff";
3636
// right detail rail that only exists once a package is selected.
3737
// Vetting is live per-participant end-to-end.
3838

39-
const ROLES: Role[] = ["app-user", "app-provider", "sv"];
39+
// Default (app-provider) first — matches CLI dar --role and handler
40+
// empty-role fallback.
41+
const ROLES: Role[] = ["app-provider", "app-user", "sv"];
4042

4143
// Package table column template — shared by the header band and rows.
4244
const COLS = "1.6fr 0.55fr 1.1fr 1.15fr";
@@ -60,7 +62,7 @@ type UploadState =
6062
export function DARScreen() {
6163
const sel = useInstanceSelection();
6264
const name = sel.selected;
63-
const [role, setRole] = useState<Role>("app-user");
65+
const [role, setRole] = useState<Role>("app-provider");
6466
// Participants an upload fans out to (parallel, backend-side).
6567
// Orthogonal to `role`, which drives only the package LIST.
6668
const [vetTargets, setVetTargets] = useState<Record<Role, boolean>>({

‎frontend/src/screens/ExplorerScreen.test.tsx‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function stubFetch(contracts: unknown[] = []) {
7878
return json({
7979
schema_version: 1,
8080
instance: "demo",
81-
role: "app-user",
81+
role: "app-provider",
8282
ledger_end: 30,
8383
count: TX_ROWS.length,
8484
scanned_from: 0,
@@ -90,7 +90,7 @@ function stubFetch(contracts: unknown[] = []) {
9090
return json({
9191
schema_version: 1,
9292
instance: "demo",
93-
role: "app-user",
93+
role: "app-provider",
9494
ledger_end: 30,
9595
contracts,
9696
});
@@ -122,6 +122,27 @@ describe("ExplorerScreen — transactions filters + replay", () => {
122122
vi.restoreAllMocks();
123123
});
124124

125+
it("defaults the Explorer role to app-provider", async () => {
126+
stubFetch();
127+
render(
128+
<Providers>
129+
<ExplorerScreen />
130+
</Providers>,
131+
);
132+
const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
133+
await waitFor(() => {
134+
const contractsCall = fetchMock.mock.calls
135+
.map(([u]) => String(u))
136+
.find((u) => u.includes("/contracts?") || u.includes("/contracts&"));
137+
// Snapshot fetch uses /contracts?role=...&limit=...
138+
const snap = fetchMock.mock.calls
139+
.map(([u]) => String(u))
140+
.find((u) => u.includes("/contracts") && u.includes("role="));
141+
expect(snap ?? contractsCall).toBeTruthy();
142+
expect(snap ?? contractsCall).toContain("role=app-provider");
143+
});
144+
});
145+
125146
it("forwards party / template / offset filters to the transactions endpoint (#24)", async () => {
126147
stubFetch();
127148
await gotoTransactions();

0 commit comments

Comments
 (0)