@misofm/sdk 0.2.0 → 0.3.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.
Files changed (91) hide show
  1. package/README.md +37 -5
  2. package/dist/catalog.d.ts +30 -0
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/catalog.js +60 -1
  5. package/dist/catalog.js.map +1 -1
  6. package/dist/contracts/miso_drop/drop.d.ts +550 -0
  7. package/dist/contracts/miso_drop/drop.d.ts.map +1 -0
  8. package/dist/contracts/miso_drop/drop.js +523 -0
  9. package/dist/contracts/miso_drop/drop.js.map +1 -0
  10. package/dist/contracts.d.ts +1 -0
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +3 -0
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/credits.d.ts.map +1 -1
  15. package/dist/credits.js +4 -2
  16. package/dist/credits.js.map +1 -1
  17. package/dist/drop.d.ts +120 -0
  18. package/dist/drop.d.ts.map +1 -0
  19. package/dist/drop.js +152 -0
  20. package/dist/drop.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/read/artist.d.ts +24 -0
  26. package/dist/read/artist.d.ts.map +1 -0
  27. package/dist/read/artist.js +114 -0
  28. package/dist/read/artist.js.map +1 -0
  29. package/dist/read/catalog.d.ts +63 -0
  30. package/dist/read/catalog.d.ts.map +1 -0
  31. package/dist/read/catalog.js +252 -0
  32. package/dist/read/catalog.js.map +1 -0
  33. package/dist/read/client.d.ts +36 -0
  34. package/dist/read/client.d.ts.map +1 -0
  35. package/dist/read/client.js +46 -0
  36. package/dist/read/client.js.map +1 -0
  37. package/dist/read/config.d.ts +83 -0
  38. package/dist/read/config.d.ts.map +1 -0
  39. package/dist/read/config.js +76 -0
  40. package/dist/read/config.js.map +1 -0
  41. package/dist/read/genres.d.ts +7 -0
  42. package/dist/read/genres.d.ts.map +1 -0
  43. package/dist/read/genres.js +38 -0
  44. package/dist/read/genres.js.map +1 -0
  45. package/dist/read/index.d.ts +12 -0
  46. package/dist/read/index.d.ts.map +1 -0
  47. package/dist/read/index.js +23 -0
  48. package/dist/read/index.js.map +1 -0
  49. package/dist/read/internal/scalars.d.ts +23 -0
  50. package/dist/read/internal/scalars.d.ts.map +1 -0
  51. package/dist/read/internal/scalars.js +56 -0
  52. package/dist/read/internal/scalars.js.map +1 -0
  53. package/dist/read/internal/walrus.d.ts +4 -0
  54. package/dist/read/internal/walrus.d.ts.map +1 -0
  55. package/dist/read/internal/walrus.js +23 -0
  56. package/dist/read/internal/walrus.js.map +1 -0
  57. package/dist/read/receipts.d.ts +22 -0
  58. package/dist/read/receipts.d.ts.map +1 -0
  59. package/dist/read/receipts.js +233 -0
  60. package/dist/read/receipts.js.map +1 -0
  61. package/dist/read/types.d.ts +279 -0
  62. package/dist/read/types.d.ts.map +1 -0
  63. package/dist/read/types.js +21 -0
  64. package/dist/read/types.js.map +1 -0
  65. package/dist/read/wallet.d.ts +58 -0
  66. package/dist/read/wallet.d.ts.map +1 -0
  67. package/dist/read/wallet.js +325 -0
  68. package/dist/read/wallet.js.map +1 -0
  69. package/dist/read/works.d.ts +31 -0
  70. package/dist/read/works.d.ts.map +1 -0
  71. package/dist/read/works.js +106 -0
  72. package/dist/read/works.js.map +1 -0
  73. package/package.json +16 -3
  74. package/src/catalog.ts +118 -0
  75. package/src/contracts/miso_drop/drop.ts +798 -0
  76. package/src/contracts.ts +4 -0
  77. package/src/credits.ts +26 -12
  78. package/src/drop.ts +266 -0
  79. package/src/index.ts +5 -0
  80. package/src/read/artist.ts +136 -0
  81. package/src/read/catalog.ts +293 -0
  82. package/src/read/client.ts +79 -0
  83. package/src/read/config.ts +164 -0
  84. package/src/read/genres.ts +37 -0
  85. package/src/read/index.ts +53 -0
  86. package/src/read/internal/scalars.ts +57 -0
  87. package/src/read/internal/walrus.ts +32 -0
  88. package/src/read/receipts.ts +266 -0
  89. package/src/read/types.ts +361 -0
  90. package/src/read/wallet.ts +360 -0
  91. package/src/read/works.ts +164 -0
package/src/contracts.ts CHANGED
@@ -14,6 +14,10 @@ export * as pressing from "./contracts/miso_pressing/pressing.ts";
14
14
  export * as listing from "./contracts/miso_pressing/listing.ts";
15
15
  export * as certificate from "./contracts/miso_pressing/certificate.ts";
16
16
 
17
+ // Existing launch drops remain readable and purchasable while releases move to
18
+ // the pressing/listing contracts.
19
+ export * as drop from "./contracts/miso_drop/drop.ts";
20
+
17
21
  // Royalty-pool extension (base lib + per-work extensions).
18
22
  export * as royaltyPool from "./contracts/royalty_pool/pool.ts";
19
23
  export * as royaltyPoolStake from "./contracts/royalty_pool/stake.ts";
package/src/credits.ts CHANGED
@@ -597,12 +597,19 @@ export async function getCompositionCredits(
597
597
  compositionId: string,
598
598
  compositionCreditsPackageId: string,
599
599
  ): Promise<CreditView[] | null> {
600
- const content = await fetchCreditsField(
601
- client,
602
- compositionId,
603
- `${compositionCreditsPackageId}::composition_credits::ExtensionKey`,
604
- COMPOSITION_CREDITS_KEY_BYTES,
605
- );
600
+ const content =
601
+ (await fetchCreditsField(
602
+ client,
603
+ compositionId,
604
+ `${compositionCreditsPackageId}::composition_credits::ExtensionKey`,
605
+ COMPOSITION_CREDITS_KEY_BYTES,
606
+ )) ??
607
+ (await fetchCreditsField(
608
+ client,
609
+ compositionId,
610
+ `${compositionCreditsPackageId}::composition_credits::CompositionCreditsKey`,
611
+ COMPOSITION_CREDITS_KEY_BYTES,
612
+ ));
606
613
  if (!content) return null;
607
614
  return creditViews(CompositionCreditsField.parse(content).value.credits, compositionRoleLabel);
608
615
  }
@@ -616,12 +623,19 @@ export async function getRecordingCredits(
616
623
  recordingId: string,
617
624
  recordingCreditsPackageId: string,
618
625
  ): Promise<RecordingCreditsView | null> {
619
- const content = await fetchCreditsField(
620
- client,
621
- recordingId,
622
- `${recordingCreditsPackageId}::recording_credits::ExtensionKey`,
623
- RECORDING_CREDITS_KEY_BYTES,
624
- );
626
+ const content =
627
+ (await fetchCreditsField(
628
+ client,
629
+ recordingId,
630
+ `${recordingCreditsPackageId}::recording_credits::ExtensionKey`,
631
+ RECORDING_CREDITS_KEY_BYTES,
632
+ )) ??
633
+ (await fetchCreditsField(
634
+ client,
635
+ recordingId,
636
+ `${recordingCreditsPackageId}::recording_credits::RecordingCreditsKey`,
637
+ RECORDING_CREDITS_KEY_BYTES,
638
+ ));
625
639
  if (!content) return null;
626
640
  const value = RecordingCreditsField.parse(content).value;
627
641
  return {
package/src/drop.ts ADDED
@@ -0,0 +1,266 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Drop — the record sale. A `Drop<Currency>` is a shared, immutable object that sells
5
+ // copies of a Release's records at a fixed or floor price, optionally capped
6
+ // (`maxSupply`) and/or time-boxed. A release has at most ONE live drop: `createDrop`
7
+ // opens edition 0, and `newEdition` opens edition n+1 by consuming the predecessor —
8
+ // the one way to change price, currency, cap, or window. The registry tracks each
9
+ // release's live drop (`getCurrentDrop`).
10
+
11
+ import type { ClientWithCoreApi } from "@mysten/sui/client";
12
+ import { deriveDynamicFieldID } from "@mysten/sui/utils";
13
+ import { bcs } from "@mysten/sui/bcs";
14
+ import type { TxThunk } from "./transactions.ts";
15
+ import { isNotFound } from "./queries.ts";
16
+ import * as drop from "./contracts/miso_drop/drop.ts";
17
+
18
+ /** Pricing policy: pay exactly `amount` (fixed) or at least `amount` (floor). */
19
+ export type DropPrice = { kind: "fixed" | "floor"; amount: bigint | number | string };
20
+
21
+ export interface CreateDropParams {
22
+ /** The `Release` object the drop sells copies of — also the drop's derivation
23
+ * parent and the home of the live-drop pointer. */
24
+ releaseId: string;
25
+ /** The release's `ReleaseAdminCap` — authorizes the drop. */
26
+ releaseAdminCapId: string;
27
+ /** Fixed or floor price, in the currency's smallest unit. */
28
+ price: DropPrice;
29
+ /** The `Currency` type argument, e.g. `0x2::sui::SUI`. */
30
+ currencyType: string;
31
+ /** Copies this edition may ever sell; `null`/omitted = uncapped (an open edition). */
32
+ maxSupply?: bigint | number | null;
33
+ /** Unix ms the sale opens. Default 0 (open immediately). */
34
+ startTimestampMs?: bigint | number;
35
+ /** Unix ms the sale closes; `null`/omitted = evergreen. */
36
+ endTimestampMs?: bigint | number | null;
37
+ misoDropPackageId: string;
38
+ }
39
+
40
+ /**
41
+ * Creates and shares a release's FIRST drop — edition 0. A release's edition
42
+ * sequence can only ever start once; later terms changes go through `newEdition`.
43
+ * The `Clock` is auto-supplied by the generated binding.
44
+ */
45
+ export function createDrop(p: CreateDropParams): TxThunk {
46
+ return (tx) => {
47
+ const terms = buildTerms(tx, p);
48
+ tx.add(
49
+ drop._new({
50
+ package: p.misoDropPackageId,
51
+ arguments: [p.releaseId, p.releaseAdminCapId, terms.price, terms.supply, terms.window],
52
+ typeArguments: [p.currencyType],
53
+ }),
54
+ );
55
+ };
56
+ }
57
+
58
+ export interface NewEditionParams {
59
+ /** The `Release` whose drop is being superseded. */
60
+ releaseId: string;
61
+ /** The release's CURRENT drop — consumed (destroyed) by this call. */
62
+ oldDropId: string;
63
+ /** The current drop's `Currency` type. */
64
+ oldCurrencyType: string;
65
+ /** The release's `ReleaseAdminCap`. */
66
+ releaseAdminCapId: string;
67
+ /** The successor's price. */
68
+ price: DropPrice;
69
+ /** The successor's `Currency` type (may differ from the old one). */
70
+ currencyType: string;
71
+ /** Successor's supply cap; `null`/omitted = uncapped. */
72
+ maxSupply?: bigint | number | null;
73
+ /** Unix ms the successor opens. Default 0 (open immediately). */
74
+ startTimestampMs?: bigint | number;
75
+ /** Unix ms the successor closes; `null`/omitted = evergreen. */
76
+ endTimestampMs?: bigint | number | null;
77
+ misoDropPackageId: string;
78
+ }
79
+
80
+ /**
81
+ * Opens edition n+1 of a release's drop, CONSUMING the current one — the one way to
82
+ * change price, currency, cap, or window (or start a fresh run after a sell-out).
83
+ * Serial numbers restart at 1 for the new edition. The `Clock` is auto-supplied.
84
+ */
85
+ export function newEdition(p: NewEditionParams): TxThunk {
86
+ return (tx) => {
87
+ const terms = buildTerms(tx, p);
88
+ tx.add(
89
+ drop.newEdition({
90
+ package: p.misoDropPackageId,
91
+ arguments: [p.releaseId, p.oldDropId, p.releaseAdminCapId, terms.price, terms.supply, terms.window],
92
+ typeArguments: [p.oldCurrencyType, p.currencyType],
93
+ }),
94
+ );
95
+ };
96
+ }
97
+
98
+ /** Builds the three on-chain term values (`Price`, `Supply`, `Window`) from the flat
99
+ * facade params — the flat shape stays the public API; the enums are a wire detail. */
100
+ function buildTerms(
101
+ tx: Parameters<TxThunk>[0],
102
+ p: Pick<CreateDropParams, "price" | "maxSupply" | "startTimestampMs" | "endTimestampMs" | "misoDropPackageId">,
103
+ ) {
104
+ const pkg = { package: p.misoDropPackageId };
105
+ const makePrice = p.price.kind === "fixed" ? drop.newFixedPrice : drop.newFloorPrice;
106
+ const price = tx.add(makePrice({ ...pkg, arguments: [BigInt(p.price.amount)] }));
107
+ const supply =
108
+ p.maxSupply == null
109
+ ? tx.add(drop.newUncappedSupply({ ...pkg, arguments: [] }))
110
+ : tx.add(drop.newCappedSupply({ ...pkg, arguments: [BigInt(p.maxSupply)] }));
111
+ const start = BigInt(p.startTimestampMs ?? 0);
112
+ const window =
113
+ p.endTimestampMs == null
114
+ ? tx.add(drop.newUnboundedWindow({ ...pkg, arguments: [start] }))
115
+ : tx.add(drop.newBoundedWindow({ ...pkg, arguments: [start, BigInt(p.endTimestampMs)] }));
116
+ return { price, supply, window };
117
+ }
118
+
119
+ export interface BuyRecordParams {
120
+ /** The shared `Drop<Currency>` to buy from. */
121
+ dropId: string;
122
+ /** Buyer's `Coin<Currency>` object ids to pay from — merged, then split to `amount`. */
123
+ paymentCoinIds: string[];
124
+ /** Amount to pay in the currency's smallest unit (exact for `fixed`, ≥ for `floor`). */
125
+ amount: bigint | number | string;
126
+ /** The drop's `Currency` type, e.g. `0x2::sui::SUI`. */
127
+ currencyType: string;
128
+ /** The `miso_record` `Settings` shared object (authorizes the mint). */
129
+ settingsId: string;
130
+ /** Where to send the bought `Record` (usually the buyer). */
131
+ recipient: string;
132
+ misoDropPackageId: string;
133
+ }
134
+
135
+ /**
136
+ * Buys one record from a drop: merges the buyer's payment coins, splits the exact
137
+ * price, calls `buy`, and sends the minted `Record` to `recipient`. The buyer selects
138
+ * which of their coins to spend (see the app's coin selection); this builder only
139
+ * assembles the PTB. The `Clock` is auto-supplied.
140
+ */
141
+ export function buyRecord(p: BuyRecordParams): TxThunk {
142
+ return (tx) => {
143
+ const [first, ...rest] = p.paymentCoinIds;
144
+ if (!first) throw new Error("buyRecord: no payment coins provided");
145
+ const primary = tx.object(first);
146
+ if (rest.length > 0) tx.mergeCoins(primary, rest.map((id) => tx.object(id)));
147
+ const [payment] = tx.splitCoins(primary, [tx.pure.u64(BigInt(p.amount))]);
148
+ const record = tx.add(
149
+ drop.buy({
150
+ package: p.misoDropPackageId,
151
+ arguments: [p.dropId, payment, p.settingsId],
152
+ typeArguments: [p.currencyType],
153
+ }),
154
+ );
155
+ tx.transferObjects([record], p.recipient);
156
+ };
157
+ }
158
+
159
+ /** A parsed drop, in the shape the featured-drop card renders. */
160
+ export interface DropView {
161
+ id: string;
162
+ releaseId: string;
163
+ edition: number;
164
+ price: { kind: "fixed" | "floor"; amount: bigint };
165
+ /** `null` = uncapped (an open edition). */
166
+ maxSupply: bigint | null;
167
+ quantitySold: bigint;
168
+ startTimestampMs: bigint;
169
+ /** `null` = evergreen. */
170
+ endTimestampMs: bigint | null;
171
+ /** The drop's `Currency` type, parsed from its object type tag (`null` if unavailable). */
172
+ currencyType: string | null;
173
+ }
174
+
175
+ /** Extracts `CURRENCY` from a `…::drop::Drop<CURRENCY>` type tag. */
176
+ function currencyFromType(type: string | null | undefined): string | null {
177
+ if (!type) return null;
178
+ const lt = type.indexOf("<");
179
+ return lt >= 0 && type.endsWith(">") ? type.slice(lt + 1, -1) : null;
180
+ }
181
+
182
+ /**
183
+ * Fetches and parses a shared `Drop` object, or `null` if it doesn't exist.
184
+ *
185
+ * NOTE: this deliberately deviates from the core-getter convention in
186
+ * `./queries.ts` (throw on missing object). Consumers probe pasted or stored
187
+ * drop ids — a party's featured drop, a superseded edition — where "no such
188
+ * drop" is a normal state, so absence is `null` and only transport failures throw.
189
+ */
190
+ export async function getDrop(client: ClientWithCoreApi, dropId: string): Promise<DropView | null> {
191
+ let content: Uint8Array | null | undefined;
192
+ let type: string | undefined;
193
+ try {
194
+ const { object } = await client.core.getObject({ objectId: dropId, include: { content: true } });
195
+ content = object?.content;
196
+ type = object?.type;
197
+ } catch (e) {
198
+ if (isNotFound(e)) return null;
199
+ throw e;
200
+ }
201
+ if (!content) return null;
202
+
203
+ const d = drop.Drop.parse(content);
204
+ const price = d.price as { $kind?: string; Fixed?: { amount: string }; Floor?: { amount: string } };
205
+ const isFixed = price.$kind === "Fixed" || price.Fixed != null;
206
+ const amount = (isFixed ? price.Fixed?.amount : price.Floor?.amount) ?? "0";
207
+ const supply = d.supply as { $kind?: string; Capped?: { max: string } };
208
+ const capped = supply.$kind === "Capped" || supply.Capped != null;
209
+ const window = d.window as {
210
+ $kind?: string;
211
+ Unbounded?: { start_timestamp_ms: string };
212
+ Bounded?: { start_timestamp_ms: string; end_timestamp_ms: string };
213
+ };
214
+ const bounded = window.$kind === "Bounded" || window.Bounded != null;
215
+
216
+ return {
217
+ id: dropId,
218
+ releaseId: d.release_id,
219
+ edition: d.edition,
220
+ price: { kind: isFixed ? "fixed" : "floor", amount: BigInt(amount) },
221
+ maxSupply: capped ? BigInt(supply.Capped?.max ?? "0") : null,
222
+ quantitySold: BigInt(d.quantity_sold),
223
+ startTimestampMs: BigInt(
224
+ (bounded ? window.Bounded?.start_timestamp_ms : window.Unbounded?.start_timestamp_ms) ?? "0",
225
+ ),
226
+ endTimestampMs: bounded ? BigInt(window.Bounded?.end_timestamp_ms ?? "0") : null,
227
+ currencyType: currencyFromType(type),
228
+ };
229
+ }
230
+
231
+ // The release's `CurrentDropKey` dynamic field holds a bare `ID`; the field object
232
+ // serializes as `Field { id, name: CurrentDropKey (unit), value: ID }`.
233
+ const CurrentDropField = bcs.struct("Field", { id: bcs.Address, name: bcs.bool(), value: bcs.Address });
234
+
235
+ // Unit-struct key bytes (single `0x00` for the dummy bool field).
236
+ const CURRENT_DROP_KEY_BYTES = drop.CurrentDropKey.serialize([false]).toBytes();
237
+
238
+ export interface GetCurrentDropParams {
239
+ /** The release whose live drop to resolve. */
240
+ releaseId: string;
241
+ misoDropPackageId: string;
242
+ }
243
+
244
+ /**
245
+ * Resolves a release's LIVE drop through the `CurrentDropKey` pointer on the
246
+ * release's UID, or `null` if the release has never dropped. Superseded drops are
247
+ * destroyed, so this — not derived-address probing — is how the current edition is
248
+ * found.
249
+ */
250
+ export async function getCurrentDrop(client: ClientWithCoreApi, p: GetCurrentDropParams): Promise<DropView | null> {
251
+ const fieldId = deriveDynamicFieldID(
252
+ p.releaseId,
253
+ `${p.misoDropPackageId}::drop::CurrentDropKey`,
254
+ CURRENT_DROP_KEY_BYTES,
255
+ );
256
+ let content: Uint8Array | null | undefined;
257
+ try {
258
+ const { object } = await client.core.getObject({ objectId: fieldId, include: { content: true } });
259
+ content = object?.content;
260
+ } catch (e) {
261
+ if (isNotFound(e)) return null;
262
+ throw e;
263
+ }
264
+ if (!content) return null;
265
+ return getDrop(client, CurrentDropField.parse(content).value);
266
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,11 @@ export * from "./share-template.ts";
35
35
  export * from "./release-graph.ts";
36
36
  export * from "./catalog.ts";
37
37
 
38
+ // High-level, JSON-safe platform projections used directly by clients and by
39
+ // the thin HTTP read service. Namespaced because these composed view types are
40
+ // intentionally distinct from the lower-level contract views above.
41
+ export * as read from "./read/index.ts";
42
+
38
43
  // First-party extensions — attached to a protocol work via its cap-gated
39
44
  // `uid_mut` hook.
40
45
  export * from "./credits.ts";
@@ -0,0 +1,136 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // Artist reads: a `Party` plus every extension attached to its UID.
5
+ //
6
+ // The artist page is the clearest case for this whole layer. Its data lives in
7
+ // EIGHT separate dynamic fields, each its own package, each its own read — party,
8
+ // profile, ctas, genres, links, roles, tags, featured. miso-app fanned all eight
9
+ // out from the browser (`lib/party-queries.ts`, plus a second identical fan-out in
10
+ // `PartyEditSheet`). Here they are one call, resolved once, cached once.
11
+ //
12
+ // `roles` and `tags` are opt-in via `include`: only the owner's edit sheet reads
13
+ // them, and leaving them out of the public read keeps the hot path to six.
14
+
15
+ import { resolveGenreNames } from "./genres.ts";
16
+ import { readReleaseCover } from "./catalog.ts";
17
+ import { getDrop } from "../drop.ts";
18
+ import { getReleaseById } from "@misonetwork/sdk";
19
+ import { getReleaseCredits } from "../credits.ts";
20
+ import type { MisoClient } from "./client.ts";
21
+ import type { ArtistProfile, FeaturedRelease, PartyMember, PartySummary } from "./types.ts";
22
+
23
+ /** Optional sub-resources — read only when asked for. */
24
+ export type ArtistInclude = "roles" | "tags";
25
+
26
+ export interface GetArtistOptions {
27
+ include?: readonly ArtistInclude[];
28
+ }
29
+
30
+ /** Public avatar URL for a party (served from the API's R2 lane; 404 when unset). */
31
+ export function partyAvatarUrl(apiBaseUrl: string, partyId: string): string {
32
+ return `${apiBaseUrl.replace(/\/$/, "")}/media/avatar/${partyId}`;
33
+ }
34
+
35
+ /**
36
+ * An artist page, fully resolved. Throws only if the PARTY itself can't be read —
37
+ * every extension is optional by design (a party with no profile set is a new
38
+ * party, not a broken one), so each decoration read falls back to its empty value.
39
+ */
40
+ export async function getArtistProfile(
41
+ client: MisoClient,
42
+ partyId: string,
43
+ options: GetArtistOptions = {},
44
+ ): Promise<ArtistProfile> {
45
+ const include = new Set(options.include ?? []);
46
+ const { party } = client.sui;
47
+
48
+ const [profile, ctas, genreIds, links, roles, tags] = await Promise.all([
49
+ party.getProfile(partyId).catch(() => null),
50
+ party.getCtas(partyId).catch(() => []),
51
+ party.getGenres(partyId).catch(() => [] as string[]),
52
+ party.getLinks(partyId).catch(() => []),
53
+ include.has("roles") ? party.getRoles(partyId).catch(() => [] as string[]) : Promise.resolve(undefined),
54
+ include.has("tags") ? party.getTags(partyId).catch(() => [] as string[]) : Promise.resolve(undefined),
55
+ ]);
56
+
57
+ // The party read is the one that may legitimately fail (no such party), so it
58
+ // is awaited on its own rather than being swallowed by the batch above.
59
+ const entity = await party.getPartyById(partyId);
60
+
61
+ const [genres, members] = await Promise.all([
62
+ resolveGenreNames(client, genreIds),
63
+ resolveMembers(client, entity.kind === "group" ? (entity.members ?? []) : []),
64
+ ]);
65
+
66
+ return {
67
+ id: entity.id,
68
+ kind: entity.kind,
69
+ name: entity.name,
70
+ createdAtMs: entity.createdAtMs,
71
+ bioShort: profile?.bioShort ?? null,
72
+ bioLong: profile?.bioLong ?? null,
73
+ country: profile?.country ?? null,
74
+ languages: profile?.languages ?? [],
75
+ genres,
76
+ links: links.map((l) => ({ platform: l.platform, value: l.value, url: l.url })),
77
+ ctas: ctas.map((c) => ({ label: c.label, url: c.url })),
78
+ members,
79
+ ...(roles !== undefined ? { roles } : {}),
80
+ ...(tags !== undefined ? { tags } : {}),
81
+ avatarUrl: partyAvatarUrl(client.config.apiBaseUrl, entity.id),
82
+ };
83
+ }
84
+
85
+ /** Group members with names resolved. A member that fails to read is dropped. */
86
+ async function resolveMembers(client: MisoClient, memberIds: readonly string[]): Promise<PartyMember[]> {
87
+ if (memberIds.length === 0) return [];
88
+ const parties = await client.sui.party.getPartiesByIds([...memberIds]).catch(() => ({}) as Record<string, undefined>);
89
+ return memberIds.flatMap((id) => {
90
+ const p = parties[id];
91
+ return p ? [{ id, name: p.name }] : [];
92
+ });
93
+ }
94
+
95
+ /** Name + kind for many parties at once. Ids that don't resolve are omitted. */
96
+ export async function getPartySummaries(client: MisoClient, ids: readonly string[]): Promise<PartySummary[]> {
97
+ if (ids.length === 0) return [];
98
+ const parties = await client.sui.party.getPartiesByIds([...ids]);
99
+ return ids.flatMap((id) => {
100
+ const p = parties[id];
101
+ return p ? [{ id, name: p.name, kind: p.kind }] : [];
102
+ });
103
+ }
104
+
105
+ /**
106
+ * The party's pinned pressing, resolved through to its release and cover.
107
+ * `null` — a successful empty — when nothing is pinned or the pin is dangling
108
+ * (the pressing it named has since been superseded and destroyed).
109
+ */
110
+ export async function getFeaturedRelease(client: MisoClient, partyId: string): Promise<FeaturedRelease | null> {
111
+ const pressingId = await client.sui.party.getFeaturedDrop(partyId);
112
+ if (!pressingId) return null;
113
+
114
+ const pressing = await getDrop(client.protocol, pressingId);
115
+ if (!pressing) return null;
116
+
117
+ const [release, cover, credits, entity] = await Promise.all([
118
+ getReleaseById(client.protocol, pressing.releaseId),
119
+ readReleaseCover(client, pressing.releaseId),
120
+ getReleaseCredits(client.protocol, pressing.releaseId, client.config.protocol.releaseCredits).catch(() => null),
121
+ client.sui.party.getPartyById(partyId),
122
+ ]);
123
+
124
+ // The artist line prefers the release's own PRIMARY credits and falls back to
125
+ // the party whose page this is — a release with no credits set still shows an
126
+ // artist, which is the behavior the featured card has always had.
127
+ const primary = (credits ?? []).filter((c) => c.roles.includes("Primary")).map((c) => c.displayName);
128
+
129
+ return {
130
+ pressingId,
131
+ releaseId: pressing.releaseId,
132
+ title: release.title,
133
+ artist: primary.length ? primary.join(", ") : entity.name,
134
+ coverUrl: cover?.still.url ?? null,
135
+ };
136
+ }