@misofm/sdk 0.4.0 → 0.6.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 (53) hide show
  1. package/dist/catalog.d.ts +4 -17
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +46 -58
  4. package/dist/catalog.js.map +1 -1
  5. package/dist/cover.d.ts +10 -0
  6. package/dist/cover.d.ts.map +1 -1
  7. package/dist/cover.js +66 -19
  8. package/dist/cover.js.map +1 -1
  9. package/dist/credits.d.ts +10 -0
  10. package/dist/credits.d.ts.map +1 -1
  11. package/dist/credits.js +149 -39
  12. package/dist/credits.js.map +1 -1
  13. package/dist/drop.d.ts +7 -0
  14. package/dist/drop.d.ts.map +1 -1
  15. package/dist/drop.js +84 -34
  16. package/dist/drop.js.map +1 -1
  17. package/dist/pressing.d.ts.map +1 -1
  18. package/dist/pressing.js +59 -16
  19. package/dist/pressing.js.map +1 -1
  20. package/dist/read/artist.d.ts +1 -6
  21. package/dist/read/artist.d.ts.map +1 -1
  22. package/dist/read/artist.js +39 -22
  23. package/dist/read/artist.js.map +1 -1
  24. package/dist/read/catalog.d.ts +24 -3
  25. package/dist/read/catalog.d.ts.map +1 -1
  26. package/dist/read/catalog.js +190 -89
  27. package/dist/read/catalog.js.map +1 -1
  28. package/dist/read/index.d.ts +6 -4
  29. package/dist/read/index.d.ts.map +1 -1
  30. package/dist/read/index.js +3 -3
  31. package/dist/read/index.js.map +1 -1
  32. package/dist/read/types.d.ts +21 -1
  33. package/dist/read/types.d.ts.map +1 -1
  34. package/dist/read/wallet.d.ts +10 -1
  35. package/dist/read/wallet.d.ts.map +1 -1
  36. package/dist/read/wallet.js +28 -0
  37. package/dist/read/wallet.js.map +1 -1
  38. package/dist/read/works.d.ts +3 -2
  39. package/dist/read/works.d.ts.map +1 -1
  40. package/dist/read/works.js +158 -48
  41. package/dist/read/works.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/catalog.ts +71 -77
  44. package/src/cover.ts +107 -26
  45. package/src/credits.ts +353 -89
  46. package/src/drop.ts +150 -40
  47. package/src/pressing.ts +146 -36
  48. package/src/read/artist.ts +73 -34
  49. package/src/read/catalog.ts +324 -89
  50. package/src/read/index.ts +24 -2
  51. package/src/read/types.ts +26 -2
  52. package/src/read/wallet.ts +45 -1
  53. package/src/read/works.ts +224 -59
@@ -21,7 +21,15 @@ import {
21
21
  } from "@misonetwork/sdk";
22
22
  import type { MisoClient } from "./client.ts";
23
23
  import { int, u64 } from "./internal/scalars.ts";
24
- import type { Balance, OwnedParty, OwnedRecord, OwnedWork, Ownership, WorkDetail } from "./types.ts";
24
+ import type {
25
+ Balance,
26
+ OwnedParty,
27
+ OwnedRecord,
28
+ OwnedWork,
29
+ Ownership,
30
+ PendingMembership,
31
+ WorkDetail,
32
+ } from "./types.ts";
25
33
  import { getRecordingTitles, getWorkAddressesByShareTypes, getWorksByIds } from "./works.ts";
26
34
 
27
35
  /** The on-chain type suffix every record object shares, across both live packages. */
@@ -156,6 +164,42 @@ export async function getOwnedParties(client: MisoClient, owner: string): Promis
156
164
  });
157
165
  }
158
166
 
167
+ /**
168
+ * Pending group invitations for every individual party the wallet administers.
169
+ *
170
+ * The Party module maintains a member-side pending-membership index, so this
171
+ * reads only the wallet's controlled parties — never a global event scan. Group
172
+ * names are resolved in one batch so the API can render an inbox without extra
173
+ * browser reads.
174
+ */
175
+ export async function getPendingMemberships(
176
+ client: MisoClient,
177
+ owner: string,
178
+ ): Promise<PendingMembership[]> {
179
+ const controlled = await getOwnedParties(client, owner);
180
+ const individuals = controlled.filter((party) => party.kind === "individual");
181
+ if (individuals.length === 0) return [];
182
+
183
+ const invitations = await Promise.all(
184
+ individuals.map(async (member) => ({
185
+ member,
186
+ groupIds: await client.sui.party.getPendingMemberships(member.partyId),
187
+ })),
188
+ );
189
+ const groupIds = [...new Set(invitations.flatMap(({ groupIds }) => groupIds))];
190
+ if (groupIds.length === 0) return [];
191
+
192
+ const groups = await client.sui.party.getPartiesByIds(groupIds);
193
+ return invitations.flatMap(({ member, groupIds }) =>
194
+ groupIds.flatMap((groupId): PendingMembership[] => {
195
+ const group = groups[groupId];
196
+ return group?.kind === "group"
197
+ ? [{ memberPartyId: member.partyId, memberCapId: member.capId, groupId, groupName: group.name }]
198
+ : [];
199
+ }),
200
+ );
201
+ }
202
+
159
203
  // ── Works (studio catalog) ───────────────────────────────────────────────────
160
204
 
161
205
  /**
package/src/read/works.ts CHANGED
@@ -3,14 +3,15 @@
3
3
 
4
4
  import type { ClientWithCoreApi } from "@mysten/sui/client";
5
5
  import type { SuiGraphQLClient } from "@mysten/sui/graphql";
6
+ import { normalizeSuiAddress } from "@mysten/sui/utils";
6
7
  import {
8
+ contracts,
7
9
  extractTypeParams2,
8
10
  getCompositionsByIds,
9
- getRecordingsByIds,
10
- getReleasesByIds,
11
11
  type Composition,
12
12
  type Recording,
13
13
  type Release,
14
+ type TrackState,
14
15
  } from "@misonetwork/sdk";
15
16
 
16
17
  export interface WorkShareTypes {
@@ -23,70 +24,114 @@ export interface WorkAddressesByShareType {
23
24
  recordings: Partial<Record<string, string>>;
24
25
  }
25
26
 
26
- interface ObjectsByTypeResult {
27
- objects: {
28
- nodes: Array<{
29
- address: string;
30
- asMoveObject: { contents: { type: { repr: string } } | null } | null;
31
- }>;
32
- } | null;
27
+ interface WorkAddressConnection {
28
+ nodes: Array<{
29
+ address: string;
30
+ asMoveObject?: {
31
+ contents?: { type?: { repr?: string } | null } | null;
32
+ } | null;
33
+ }>;
34
+ pageInfo?: { hasNextPage: boolean; endCursor: string | null };
33
35
  }
34
36
 
35
- /** Resolve work share types without relying on unpublished protocol helpers. */
37
+ /** Resolve all work share types in one aliased GraphQL request. */
36
38
  export async function getWorkAddressesByShareTypes(
37
39
  client: SuiGraphQLClient,
38
40
  shareTypes: WorkShareTypes,
39
41
  misoPackageId: string,
40
42
  ): Promise<WorkAddressesByShareType> {
41
43
  const compositions = [...new Set(shareTypes.compositions)];
42
- const recordingSet = new Set(shareTypes.recordings);
44
+ const recordings = new Set(shareTypes.recordings);
43
45
  const out: WorkAddressesByShareType = { compositions: {}, recordings: {} };
46
+ if (compositions.length === 0 && recordings.size === 0) return out;
44
47
 
45
- const compositionEntries = await Promise.all(
46
- compositions.map(async (shareType) => {
47
- const type = `${misoPackageId}::composition::Composition<${shareType}>`;
48
- const result = await client.query<ObjectsByTypeResult, { type: string }>({
49
- query: `query WorkByType($type: String!) {
50
- objects(filter: { type: $type }) {
51
- nodes { address asMoveObject { contents { type { repr } } } }
52
- }
53
- }`,
54
- variables: { type },
55
- });
56
- if (result.errors?.length) throw new Error(result.errors[0]!.message);
57
- return [shareType, result.data?.objects?.nodes[0]?.address] as const;
58
- }),
59
- );
60
- for (const [shareType, address] of compositionEntries) {
61
- if (address) out.compositions[shareType] = address;
48
+ const declarations: string[] = [];
49
+ const selections: string[] = [];
50
+ const variables: Record<string, string> = {};
51
+ compositions.forEach((shareType, index) => {
52
+ const variable = `compositionType${index}`;
53
+ declarations.push(`$${variable}: String!`);
54
+ selections.push(
55
+ `composition${index}: objects(first: 1, filter: { type: $${variable} }) { nodes { address } }`,
56
+ );
57
+ variables[variable] =
58
+ `${misoPackageId}::composition::Composition<${shareType}>`;
59
+ });
60
+ if (recordings.size > 0) {
61
+ declarations.push("$recordingType: String!");
62
+ selections.push(`recordings: objects(first: 50, filter: { type: $recordingType }) {
63
+ pageInfo { hasNextPage endCursor }
64
+ nodes { address asMoveObject { contents { type { repr } } } }
65
+ }`);
66
+ variables.recordingType = `${misoPackageId}::recording::Recording`;
62
67
  }
63
68
 
64
- if (recordingSet.size > 0) {
65
- const type = `${misoPackageId}::recording::Recording`;
66
- const result = await client.query<ObjectsByTypeResult, { type: string }>({
67
- query: `query RecordingsByType($type: String!) {
68
- objects(filter: { type: $type }) {
69
- nodes { address asMoveObject { contents { type { repr } } } }
70
- }
71
- }`,
72
- variables: { type },
73
- });
74
- if (result.errors?.length) throw new Error(result.errors[0]!.message);
75
- for (const node of result.data?.objects?.nodes ?? []) {
76
- const repr = node.asMoveObject?.contents?.type.repr;
69
+ const result = await client.query<
70
+ Record<string, WorkAddressConnection | null>,
71
+ Record<string, string>
72
+ >({
73
+ query: `query WorkAddressesByShareTypes(${declarations.join(", ")}) {
74
+ ${selections.join("\n")}
75
+ }`,
76
+ variables,
77
+ });
78
+ if (result.errors?.length) {
79
+ throw new AggregateError(
80
+ result.errors.map((error) => new Error(error.message)),
81
+ "Work type discovery failed",
82
+ );
83
+ }
84
+ compositions.forEach((shareType, index) => {
85
+ const address = result.data?.[`composition${index}`]?.nodes[0]?.address;
86
+ if (address) out.compositions[shareType] = address;
87
+ });
88
+
89
+ const readRecordingPage = (
90
+ page: WorkAddressConnection | null | undefined,
91
+ ) => {
92
+ for (const node of page?.nodes ?? []) {
93
+ const repr = node.asMoveObject?.contents?.type?.repr;
77
94
  if (!repr) continue;
78
- let recordingShareType: string | undefined;
79
95
  try {
80
- [recordingShareType] = extractTypeParams2(repr);
96
+ const [shareType] = extractTypeParams2(repr);
97
+ if (recordings.has(shareType)) out.recordings[shareType] = node.address;
81
98
  } catch {
82
- recordingShareType = /<(.+)>$/.exec(repr)?.[1]?.trim();
83
- }
84
- if (recordingShareType && recordingSet.has(recordingShareType)) {
85
- out.recordings[recordingShareType] = node.address;
99
+ // Ignore objects whose deployed type does not match the Recording ABI.
86
100
  }
87
101
  }
88
- }
102
+ };
89
103
 
104
+ let page = result.data?.recordings;
105
+ readRecordingPage(page);
106
+ while (
107
+ page?.pageInfo?.hasNextPage &&
108
+ page.pageInfo.endCursor &&
109
+ Object.keys(out.recordings).length < recordings.size
110
+ ) {
111
+ const next = await client.query<
112
+ { recordings: WorkAddressConnection | null },
113
+ { recordingType: string; cursor: string }
114
+ >({
115
+ query: `query RecordingWorkAddresses($recordingType: String!, $cursor: String!) {
116
+ recordings: objects(first: 50, after: $cursor, filter: { type: $recordingType }) {
117
+ pageInfo { hasNextPage endCursor }
118
+ nodes { address asMoveObject { contents { type { repr } } } }
119
+ }
120
+ }`,
121
+ variables: {
122
+ recordingType: variables.recordingType!,
123
+ cursor: page.pageInfo.endCursor,
124
+ },
125
+ });
126
+ if (next.errors?.length) {
127
+ throw new AggregateError(
128
+ next.errors.map((error) => new Error(error.message)),
129
+ "Recording type discovery failed",
130
+ );
131
+ }
132
+ page = next.data?.recordings;
133
+ readRecordingPage(page);
134
+ }
90
135
  return out;
91
136
  }
92
137
 
@@ -102,14 +147,127 @@ export interface WorksById {
102
147
  releases: Partial<Record<string, Release>>;
103
148
  }
104
149
 
105
- /** Fetch each work kind through the authoritative protocol SDK. */
106
- export async function getWorksByIds(client: ClientWithCoreApi, ids: WorkIds): Promise<WorksById> {
107
- const [compositions, recordings, releases] = await Promise.all([
108
- getCompositionsByIds(client, [...ids.compositions]),
109
- getRecordingsByIds(client, [...ids.recordings]),
110
- getReleasesByIds(client, [...ids.releases]),
111
- ]);
112
- return { compositions, recordings, releases };
150
+ type Parsed = Record<string, any>;
151
+
152
+ function workState(value: Parsed): Composition["state"] {
153
+ return value?.$kind === "Published"
154
+ ? { type: "Published", timestampMs: Number(value.Published) }
155
+ : { type: "Initialized" };
156
+ }
157
+
158
+ function parseComposition(id: string, content: Uint8Array): Composition {
159
+ const value = contracts.composition.Composition.parse(content) as Parsed;
160
+ return {
161
+ id,
162
+ state: workState(value.state),
163
+ title: String(value.title),
164
+ royaltyRate: {
165
+ value: Number(
166
+ Array.isArray(value.royalty_rate)
167
+ ? value.royalty_rate[0]
168
+ : value.royalty_rate,
169
+ ),
170
+ },
171
+ };
172
+ }
173
+
174
+ function parseRecording(id: string, content: Uint8Array): Recording {
175
+ const value = contracts.recording.Recording.parse(content) as Parsed;
176
+ return { id, state: workState(value.state) };
177
+ }
178
+
179
+ export function parseReleaseObject(
180
+ id: string,
181
+ content: Uint8Array,
182
+ json: unknown,
183
+ ): Release {
184
+ const deployed = json as {
185
+ discs?: Parsed[];
186
+ state?: Parsed;
187
+ title?: unknown;
188
+ } | null;
189
+ if (deployed?.discs) {
190
+ const tracks = deployed.discs.flatMap((disc) => disc.tracks ?? []);
191
+ const publishedAt =
192
+ deployed.state?.["@variant"] === "Published" ? deployed.state.pos0 : null;
193
+ return {
194
+ id,
195
+ state:
196
+ publishedAt == null
197
+ ? { type: "Initialized" }
198
+ : { type: "Published", timestampMs: Number(publishedAt) },
199
+ title: String(deployed.title ?? ""),
200
+ tracks: tracks.map((track) => ({
201
+ state: (track.state?.["@variant"] ?? "Unassigned") as TrackState,
202
+ recordingId: String(track.recording_id),
203
+ splitBps: { value: Number(track.split_bps?.pos0 ?? track.split_bps) },
204
+ })),
205
+ };
206
+ }
207
+ const value = contracts.release.Release.parse(content) as Parsed;
208
+ return {
209
+ id,
210
+ state: workState(value.state),
211
+ title: String(value.title),
212
+ tracks: (value.tracks ?? []).map((track: Parsed) => ({
213
+ state: (track.state?.$kind ?? "Unassigned") as TrackState,
214
+ recordingId: String(track.recording_id),
215
+ splitBps: {
216
+ value: Number(
217
+ Array.isArray(track.split_bps) ? track.split_bps[0] : track.split_bps,
218
+ ),
219
+ },
220
+ })),
221
+ };
222
+ }
223
+
224
+ /** Fetch and parse heterogeneous works through one Core bulk request. */
225
+ export async function getWorksByIds(
226
+ client: ClientWithCoreApi,
227
+ ids: WorkIds,
228
+ ): Promise<WorksById> {
229
+ const kinds = new Map<string, keyof WorksById>();
230
+ for (const [kind, objectIds] of Object.entries(ids) as Array<
231
+ [keyof WorksById, readonly string[]]
232
+ >) {
233
+ for (const objectId of objectIds) {
234
+ const normalized = normalizeSuiAddress(objectId);
235
+ const previous = kinds.get(normalized);
236
+ if (previous && previous !== kind) {
237
+ throw new Error(
238
+ `Work ${normalized} was requested as both ${previous} and ${kind}`,
239
+ );
240
+ }
241
+ kinds.set(normalized, kind);
242
+ }
243
+ }
244
+ const out: WorksById = { compositions: {}, recordings: {}, releases: {} };
245
+ if (kinds.size === 0) return out;
246
+ const { objects } = await client.core.getObjects({
247
+ objectIds: [...kinds.keys()],
248
+ include: { content: true, json: true },
249
+ });
250
+ for (const object of objects) {
251
+ if (object instanceof Error || !object.content) continue;
252
+ const kind = kinds.get(normalizeSuiAddress(object.objectId));
253
+ if (kind === "compositions")
254
+ out.compositions[object.objectId] = parseComposition(
255
+ object.objectId,
256
+ object.content,
257
+ );
258
+ else if (kind === "recordings")
259
+ out.recordings[object.objectId] = parseRecording(
260
+ object.objectId,
261
+ object.content,
262
+ );
263
+ else if (kind === "releases")
264
+ out.releases[object.objectId] = parseReleaseObject(
265
+ object.objectId,
266
+ object.content,
267
+ object.json,
268
+ );
269
+ }
270
+ return out;
113
271
  }
114
272
 
115
273
  /**
@@ -125,7 +283,10 @@ export async function getRecordingTitles(
125
283
  const ids = [...new Set(recordingIds)];
126
284
  if (ids.length === 0) return {};
127
285
 
128
- const { objects } = await client.core.getObjects({ objectIds: ids, include: { json: true } });
286
+ const { objects } = await client.core.getObjects({
287
+ objectIds: ids,
288
+ include: { json: true },
289
+ });
129
290
  const titles: Record<string, string> = {};
130
291
  const compositionShareByRecording: Record<string, string> = {};
131
292
 
@@ -155,9 +316,13 @@ export async function getRecordingTitles(
155
316
  client,
156
317
  Object.values(addresses.compositions).filter((id): id is string => !!id),
157
318
  );
158
- for (const [recordingId, shareType] of Object.entries(compositionShareByRecording)) {
319
+ for (const [recordingId, shareType] of Object.entries(
320
+ compositionShareByRecording,
321
+ )) {
159
322
  const compositionId = addresses.compositions[shareType];
160
- const title = compositionId ? compositions[compositionId]?.title : undefined;
323
+ const title = compositionId
324
+ ? compositions[compositionId]?.title
325
+ : undefined;
161
326
  if (title) titles[recordingId] = title;
162
327
  }
163
328
  return titles;