@misofm/platform 0.25.0 → 0.26.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 (140) hide show
  1. package/README.md +144 -45
  2. package/dist/auth.d.ts +8 -13
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +78 -59
  5. package/dist/auth.js.map +1 -1
  6. package/dist/catalog.d.ts +5 -5
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +33 -21
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/client.d.ts +17 -30
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +53 -75
  13. package/dist/client.js.map +1 -1
  14. package/dist/cover.d.ts +4 -3
  15. package/dist/cover.d.ts.map +1 -1
  16. package/dist/cover.js +28 -17
  17. package/dist/cover.js.map +1 -1
  18. package/dist/credits.d.ts +12 -10
  19. package/dist/credits.d.ts.map +1 -1
  20. package/dist/credits.js +60 -33
  21. package/dist/credits.js.map +1 -1
  22. package/dist/deployments.d.ts +21 -13
  23. package/dist/deployments.d.ts.map +1 -1
  24. package/dist/deployments.js +84 -37
  25. package/dist/deployments.js.map +1 -1
  26. package/dist/errors.d.ts +91 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +111 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/execute.d.ts +3 -1
  31. package/dist/execute.d.ts.map +1 -1
  32. package/dist/execute.js +9 -4
  33. package/dist/execute.js.map +1 -1
  34. package/dist/genre.d.ts +25 -41
  35. package/dist/genre.d.ts.map +1 -1
  36. package/dist/genre.js +71 -20
  37. package/dist/genre.js.map +1 -1
  38. package/dist/party/client.d.ts +16 -13
  39. package/dist/party/client.d.ts.map +1 -1
  40. package/dist/party/client.js +33 -22
  41. package/dist/party/client.js.map +1 -1
  42. package/dist/party/extensions/links.d.ts +2 -1
  43. package/dist/party/extensions/links.d.ts.map +1 -1
  44. package/dist/party/extensions/links.js +2 -1
  45. package/dist/party/extensions/links.js.map +1 -1
  46. package/dist/party/queries.d.ts +12 -11
  47. package/dist/party/queries.d.ts.map +1 -1
  48. package/dist/party/queries.js +63 -57
  49. package/dist/party/queries.js.map +1 -1
  50. package/dist/party/types.d.ts +31 -20
  51. package/dist/party/types.d.ts.map +1 -1
  52. package/dist/party/types.js +82 -1
  53. package/dist/party/types.js.map +1 -1
  54. package/dist/pressing.d.ts +55 -35
  55. package/dist/pressing.d.ts.map +1 -1
  56. package/dist/pressing.js +99 -76
  57. package/dist/pressing.js.map +1 -1
  58. package/dist/queries.d.ts +1 -1
  59. package/dist/queries.d.ts.map +1 -1
  60. package/dist/queries.js +5 -4
  61. package/dist/queries.js.map +1 -1
  62. package/dist/read/artist.d.ts +6 -4
  63. package/dist/read/artist.d.ts.map +1 -1
  64. package/dist/read/artist.js +29 -27
  65. package/dist/read/artist.js.map +1 -1
  66. package/dist/read/catalog.d.ts +20 -17
  67. package/dist/read/catalog.d.ts.map +1 -1
  68. package/dist/read/catalog.js +120 -141
  69. package/dist/read/catalog.js.map +1 -1
  70. package/dist/read/client.d.ts +10 -9
  71. package/dist/read/client.d.ts.map +1 -1
  72. package/dist/read/client.js.map +1 -1
  73. package/dist/read/genres.d.ts +5 -3
  74. package/dist/read/genres.d.ts.map +1 -1
  75. package/dist/read/genres.js +18 -15
  76. package/dist/read/genres.js.map +1 -1
  77. package/dist/read/index.d.ts.map +1 -1
  78. package/dist/read/index.js +7 -1
  79. package/dist/read/index.js.map +1 -1
  80. package/dist/read/receipts.d.ts +15 -6
  81. package/dist/read/receipts.d.ts.map +1 -1
  82. package/dist/read/receipts.js +73 -60
  83. package/dist/read/receipts.js.map +1 -1
  84. package/dist/read/wallet.d.ts +11 -9
  85. package/dist/read/wallet.d.ts.map +1 -1
  86. package/dist/read/wallet.js +117 -132
  87. package/dist/read/wallet.js.map +1 -1
  88. package/dist/read/works.d.ts +9 -16
  89. package/dist/read/works.d.ts.map +1 -1
  90. package/dist/read/works.js +48 -117
  91. package/dist/read/works.js.map +1 -1
  92. package/dist/recording-extensions.d.ts +8 -7
  93. package/dist/recording-extensions.d.ts.map +1 -1
  94. package/dist/recording-extensions.js +30 -63
  95. package/dist/recording-extensions.js.map +1 -1
  96. package/dist/release-extensions.d.ts +3 -2
  97. package/dist/release-extensions.d.ts.map +1 -1
  98. package/dist/release-extensions.js +9 -10
  99. package/dist/release-extensions.js.map +1 -1
  100. package/dist/royalty.d.ts +51 -51
  101. package/dist/royalty.d.ts.map +1 -1
  102. package/dist/royalty.js +93 -11
  103. package/dist/royalty.js.map +1 -1
  104. package/dist/share.d.ts +7 -6
  105. package/dist/share.d.ts.map +1 -1
  106. package/dist/share.js +37 -30
  107. package/dist/share.js.map +1 -1
  108. package/dist/vault.d.ts +20 -8
  109. package/dist/vault.d.ts.map +1 -1
  110. package/dist/vault.js +31 -15
  111. package/dist/vault.js.map +1 -1
  112. package/package.json +11 -4
  113. package/src/auth.ts +89 -70
  114. package/src/catalog.ts +33 -49
  115. package/src/client.ts +117 -97
  116. package/src/cover.ts +17 -26
  117. package/src/credits.ts +43 -75
  118. package/src/deployments.ts +103 -69
  119. package/src/errors.ts +156 -0
  120. package/src/execute.ts +10 -6
  121. package/src/genre.ts +38 -23
  122. package/src/party/client.ts +35 -23
  123. package/src/party/extensions/links.ts +3 -2
  124. package/src/party/queries.ts +77 -65
  125. package/src/party/types.ts +52 -48
  126. package/src/pressing.ts +158 -161
  127. package/src/queries.ts +5 -5
  128. package/src/read/artist.ts +47 -42
  129. package/src/read/catalog.ts +200 -223
  130. package/src/read/client.ts +12 -11
  131. package/src/read/genres.ts +24 -16
  132. package/src/read/index.ts +7 -1
  133. package/src/read/receipts.ts +128 -67
  134. package/src/read/wallet.ts +194 -162
  135. package/src/read/works.ts +75 -194
  136. package/src/recording-extensions.ts +38 -92
  137. package/src/release-extensions.ts +7 -12
  138. package/src/royalty.ts +117 -20
  139. package/src/share.ts +44 -29
  140. package/src/vault.ts +47 -24
@@ -11,16 +11,27 @@
11
11
  // than be reimplemented by every browser, Worker, server, or native client.
12
12
 
13
13
  import { normalizeStructTag, normalizeSuiAddress, parseStructTag } from "@mysten/sui/utils";
14
+ import { Effect, Option } from "effect";
14
15
  import {
15
16
  contracts as networkContracts,
16
17
  getCompositionByShareType,
17
18
  getOwnedReleaseAdminCaps,
18
19
  getRecordingByShareType,
19
20
  getReleaseById,
20
- isNotFound,
21
21
  } from "@misofm/musicos";
22
22
  import { party as partyContracts } from "@misofm/partyos/contracts";
23
- import type { MisoClient } from "./client.ts";
23
+ import { derivePartyAdminCapId, getPartiesByIds, getPendingMemberships as getPartyPendingMemberships } from "@misofm/partyos";
24
+ import {
25
+ getObjectsContent,
26
+ getOptionalObjectContent,
27
+ SuiClient,
28
+ SuiGraphQL,
29
+ SuiRpcError,
30
+ type BcsDecodeError,
31
+ type ObjectNotFoundError,
32
+ type ObjectTypeMismatchError,
33
+ } from "@misofm/effect";
34
+ import type { MisoConfig } from "./config.ts";
24
35
  import { int, u64 } from "./internal/scalars.ts";
25
36
  import * as vaultContract from "../contracts/vault/vault.ts";
26
37
  import * as recordContract from "../contracts/record/record.ts";
@@ -156,29 +167,52 @@ function isCanonicalRecordType(
156
167
  return true;
157
168
  }
158
169
 
170
+ /** Page shape this file needs from `client.core.listOwnedObjects` — content is optional per-call, not per-object. */
171
+ interface OwnedObjectsPage {
172
+ objects: Array<{ objectId: string; type: string; content?: Uint8Array }>;
173
+ hasNextPage: boolean;
174
+ cursor: string | null;
175
+ }
176
+
177
+ /** One page of `client.core.listOwnedObjects`, requesting `content` when `withContent` is set. */
178
+ const listOwnedObjectsPage = Effect.fn("listOwnedObjectsPage")(function* (
179
+ owner: string,
180
+ type: string,
181
+ cursor: string | null,
182
+ withContent: boolean,
183
+ ): Effect.fn.Return<OwnedObjectsPage, SuiRpcError, SuiClient> {
184
+ const client = yield* SuiClient;
185
+ return yield* Effect.tryPromise({
186
+ try: (signal) =>
187
+ (client.core.listOwnedObjects as (args: unknown) => Promise<OwnedObjectsPage>)({
188
+ owner,
189
+ type,
190
+ cursor,
191
+ limit: 50,
192
+ ...(withContent ? { include: { content: true } } : {}),
193
+ signal,
194
+ }),
195
+ catch: (cause) => new SuiRpcError({ operation: "listOwnedObjects", cause }),
196
+ });
197
+ });
198
+
159
199
  /**
160
200
  * The records `owner` holds. Ownership is DIRECT — a record is an address-owned
161
201
  * `<record>::record::Record` with no pressing/license/receipt intermediary.
162
202
  * The server-side type filter and exact local check deliberately exclude records
163
203
  * from any retired package namespace.
164
204
  */
165
- export async function getOwnedRecords(client: MisoClient, owner: string): Promise<OwnedRecord[]> {
205
+ export const getOwnedRecords = Effect.fn("getOwnedRecords")(function* (
206
+ owner: string,
207
+ config: MisoConfig,
208
+ ): Effect.fn.Return<OwnedRecord[], SuiRpcError, SuiClient> {
166
209
  const out: OwnedRecord[] = [];
167
210
  let cursor: string | null = null;
168
- const sales = requireRecordSalesDeployment(client.config.recordSales);
211
+ const sales = requireRecordSalesDeployment(config.recordSales);
169
212
  const recordType = `${sales.recordPackageId}::record::Record`;
170
213
 
171
214
  for (let page = 0; page < MAX_PAGES; page++) {
172
- // Annotated: `cursor` is both an input and assigned from the result, which
173
- // TypeScript reads as a circular initializer without an explicit type.
174
- const res: Awaited<ReturnType<typeof client.protocol.core.listOwnedObjects>> =
175
- await client.protocol.core.listOwnedObjects({
176
- owner,
177
- type: recordType,
178
- cursor,
179
- limit: 50,
180
- include: { content: true },
181
- });
215
+ const res = (yield* listOwnedObjectsPage(owner, recordType, cursor, true)) as OwnedObjectsPage;
182
216
  for (const obj of res.objects) {
183
217
  if (!isCanonicalRecordType(obj.type, sales.recordPackageId)) {
184
218
  continue;
@@ -210,7 +244,7 @@ export async function getOwnedRecords(client: MisoClient, owner: string): Promis
210
244
  if (!cursor) break;
211
245
  }
212
246
  return out;
213
- }
247
+ });
214
248
 
215
249
  // ── Parties ──────────────────────────────────────────────────────────────────
216
250
 
@@ -220,21 +254,19 @@ export async function getOwnedRecords(client: MisoClient, owner: string): Promis
220
254
  * cap type plus a name resolve. Caps are transferable, which is why this is
221
255
  * authoritative in a way that remembering created parties client-side is not.
222
256
  */
223
- export async function getOwnedParties(client: MisoClient, owner: string): Promise<OwnedParty[]> {
224
- const capType = `${client.config.partyos.partyos}::party::PartyAdminCap`;
257
+ export const getOwnedParties = Effect.fn("getOwnedParties")(function* (
258
+ owner: string,
259
+ config: MisoConfig,
260
+ ): Effect.fn.Return<OwnedParty[], SuiRpcError | ObjectTypeMismatchError | BcsDecodeError, SuiClient> {
261
+ const capType = `${config.partyos.partyos}::party::PartyAdminCap`;
225
262
 
226
263
  // One page of 50 caps is plenty for launch-scale artists; paginate if labels
227
264
  // ever start hitting the cap.
228
- const { objects } = await client.protocol.core.listOwnedObjects({
229
- owner,
230
- type: capType,
231
- limit: 50,
232
- include: { content: true },
233
- });
265
+ const { objects } = yield* listOwnedObjectsPage(owner, capType, null, true);
234
266
 
235
267
  const caps = objects.flatMap((obj) => {
236
268
  try {
237
- const cap = partyContracts.PartyAdminCap.parse(obj.content);
269
+ const cap = partyContracts.PartyAdminCap.parse(obj.content!);
238
270
  return [{ capId: obj.objectId, partyId: cap.party_id }];
239
271
  } catch {
240
272
  return [];
@@ -242,12 +274,15 @@ export async function getOwnedParties(client: MisoClient, owner: string): Promis
242
274
  });
243
275
  if (caps.length === 0) return [];
244
276
 
245
- const parties = await client.party.getPartiesByIds(caps.map((c) => c.partyId));
277
+ const parties = yield* getPartiesByIds(
278
+ caps.map((c) => c.partyId),
279
+ config.partyos.partyos,
280
+ );
246
281
  return caps.flatMap(({ capId, partyId }) => {
247
282
  const p = parties[partyId];
248
283
  return p ? [{ partyId, capId, name: p.name, kind: p.kind }] : [];
249
284
  });
250
- }
285
+ });
251
286
 
252
287
  /**
253
288
  * Pending group invitations for every individual party the wallet administers.
@@ -257,24 +292,24 @@ export async function getOwnedParties(client: MisoClient, owner: string): Promis
257
292
  * names are resolved in one batch so the API can render an inbox without extra
258
293
  * browser reads.
259
294
  */
260
- export async function getPendingMemberships(
261
- client: MisoClient,
295
+ export const getPendingMemberships = Effect.fn("getPendingMemberships")(function* (
262
296
  owner: string,
263
- ): Promise<PendingMembership[]> {
264
- const controlled = await getOwnedParties(client, owner);
297
+ config: MisoConfig,
298
+ ): Effect.fn.Return<PendingMembership[], SuiRpcError | ObjectTypeMismatchError | BcsDecodeError, SuiClient> {
299
+ const controlled = yield* getOwnedParties(owner, config);
265
300
  const individuals = controlled.filter((party) => party.kind === "individual");
266
301
  if (individuals.length === 0) return [];
267
302
 
268
- const invitations = await Promise.all(
269
- individuals.map(async (member) => ({
270
- member,
271
- groupIds: await client.party.getPendingMemberships(member.partyId),
272
- })),
303
+ const invitations = yield* Effect.forEach(
304
+ individuals,
305
+ (member) =>
306
+ getPartyPendingMemberships(member.partyId).pipe(Effect.map((groupIds) => ({ member, groupIds }))),
307
+ { concurrency: "unbounded" },
273
308
  );
274
309
  const groupIds = [...new Set(invitations.flatMap(({ groupIds }) => groupIds))];
275
310
  if (groupIds.length === 0) return [];
276
311
 
277
- const groups = await client.party.getPartiesByIds(groupIds);
312
+ const groups = yield* getPartiesByIds(groupIds, config.partyos.partyos);
278
313
  return invitations.flatMap(({ member, groupIds }) =>
279
314
  groupIds.flatMap((groupId): PendingMembership[] => {
280
315
  const group = groups[groupId];
@@ -283,7 +318,7 @@ export async function getPendingMemberships(
283
318
  : [];
284
319
  }),
285
320
  );
286
- }
321
+ });
287
322
 
288
323
  // ── Works (studio catalog) ───────────────────────────────────────────────────
289
324
 
@@ -293,12 +328,14 @@ export async function getPendingMemberships(
293
328
  * type `T` is parsed back out of each instance's type tag, because the cap does
294
329
  * not store the work's id — only its share type.
295
330
  */
296
- async function ownedGenericCaps(client: MisoClient, owner: string, capType: string) {
331
+ const ownedGenericCaps = Effect.fn("ownedGenericCaps")(function* (
332
+ owner: string,
333
+ capType: string,
334
+ ): Effect.fn.Return<{ id: string; shareType: string }[], SuiRpcError, SuiClient> {
297
335
  const caps: { id: string; shareType: string }[] = [];
298
336
  let cursor: string | null = null;
299
337
  for (let page = 0; page < MAX_PAGES; page++) {
300
- const res: Awaited<ReturnType<typeof client.protocol.core.listOwnedObjects>> =
301
- await client.protocol.core.listOwnedObjects({ owner, type: capType, cursor, limit: 50 });
338
+ const res = (yield* listOwnedObjectsPage(owner, capType, cursor, false)) as OwnedObjectsPage;
302
339
  for (const obj of res.objects) {
303
340
  const match = /<(.+)>$/.exec(obj.type);
304
341
  if (match?.[1]) caps.push({ id: obj.objectId, shareType: match[1] });
@@ -308,33 +345,33 @@ async function ownedGenericCaps(client: MisoClient, owner: string, capType: stri
308
345
  if (!cursor) break;
309
346
  }
310
347
  return caps;
311
- }
348
+ });
312
349
 
313
350
  /**
314
351
  * Discover owner-held VaultAdminCaps whose wrapped capability administers a
315
352
  * catalog work. PressingAdminCap is deliberately ignored: Catalog is a work
316
353
  * surface (composition / recording / release), not a sale-management surface.
317
354
  */
318
- async function ownedVaultedWorkCaps(client: MisoClient, owner: string) {
355
+ const ownedVaultedWorkCaps = Effect.fn("ownedVaultedWorkCaps")(function* (
356
+ owner: string,
357
+ config: MisoConfig,
358
+ ): Effect.fn.Return<
359
+ { compositions: VaultedGenericCap[]; recordings: VaultedGenericCap[]; releases: VaultedReleaseCap[] },
360
+ SuiRpcError,
361
+ SuiClient
362
+ > {
319
363
  const out: {
320
364
  compositions: VaultedGenericCap[];
321
365
  recordings: VaultedGenericCap[];
322
366
  releases: VaultedReleaseCap[];
323
367
  } = { compositions: [], recordings: [], releases: [] };
324
- const vaultPackageId = client.config.protocol.vault;
325
- const misoPackageId = client.config.deployment.musicos;
368
+ const vaultPackageId = config.protocol.vault;
369
+ const misoPackageId = config.deployment.musicos;
326
370
  const capType = `${vaultPackageId}::vault::VaultAdminCap`;
327
371
  let cursor: string | null = null;
328
372
 
329
373
  for (let page = 0; page < MAX_PAGES; page++) {
330
- const res: Awaited<ReturnType<typeof client.protocol.core.listOwnedObjects>> =
331
- await client.protocol.core.listOwnedObjects({
332
- owner,
333
- type: capType,
334
- cursor,
335
- limit: 50,
336
- include: { content: true },
337
- });
374
+ const res = (yield* listOwnedObjectsPage(owner, capType, cursor, true)) as OwnedObjectsPage;
338
375
  for (const object of res.objects) {
339
376
  if (!object.content) continue;
340
377
  const work = classifyVaultedWorkAdminCapType(
@@ -362,32 +399,24 @@ async function ownedVaultedWorkCaps(client: MisoClient, owner: string) {
362
399
  if (!cursor) break;
363
400
  }
364
401
  return out;
365
- }
402
+ });
366
403
 
367
404
  /** Read the ReleaseAdminCap wrapped by each shared release vault. */
368
- async function resolveVaultedReleaseCaps(
369
- client: MisoClient,
405
+ const resolveVaultedReleaseCaps = Effect.fn("resolveVaultedReleaseCaps")(function* (
370
406
  caps: readonly VaultedReleaseCap[],
371
- ): Promise<{ id: string; releaseId: string }[]> {
407
+ config: MisoConfig,
408
+ ): Effect.fn.Return<{ id: string; releaseId: string }[], SuiRpcError, SuiClient> {
372
409
  if (caps.length === 0) return [];
373
- const releaseCapType = `${client.config.deployment.musicos}::release::ReleaseAdminCap`;
374
- const vaultType = normalizeStructTag(
375
- `${client.config.protocol.vault}::vault::Vault<${releaseCapType}>`,
376
- );
377
- const { objects } = await client.protocol.core.getObjects({
378
- objectIds: caps.map((cap) => cap.vaultId),
379
- include: { content: true },
380
- });
410
+ const releaseCapType = `${config.deployment.musicos}::release::ReleaseAdminCap`;
411
+ const vaultType = normalizeStructTag(`${config.protocol.vault}::vault::Vault<${releaseCapType}>`);
412
+ const contentById = yield* getObjectsContent(caps.map((cap) => cap.vaultId));
381
413
  const releasesByVault = new Map<string, string>();
382
- for (const object of objects) {
383
- if (object instanceof Error || !object.content) continue;
414
+ for (const [objectId, object] of contentById) {
384
415
  try {
385
416
  if (normalizeStructTag(object.type) !== vaultType) continue;
386
- const vault = vaultContract.Vault(networkContracts.release.ReleaseAdminCap).parse(
387
- object.content,
388
- );
417
+ const vault = vaultContract.Vault(networkContracts.release.ReleaseAdminCap).parse(object.content);
389
418
  const releaseId = vault.cap?.value?.release_id;
390
- if (releaseId) releasesByVault.set(object.objectId, releaseId);
419
+ if (releaseId) releasesByVault.set(objectId, releaseId);
391
420
  } catch {
392
421
  // Batch catalog discovery is best-effort per authority, matching the
393
422
  // existing direct-cap path's treatment of unreadable work objects.
@@ -397,7 +426,7 @@ async function resolveVaultedReleaseCaps(
397
426
  const releaseId = releasesByVault.get(cap.vaultId);
398
427
  return releaseId ? [{ id: cap.id, releaseId }] : [];
399
428
  });
400
- }
429
+ });
401
430
 
402
431
  /**
403
432
  * Every work the wallet administers, keyed by its ADMIN CAP id (the studio
@@ -409,33 +438,33 @@ async function resolveVaultedReleaseCaps(
409
438
  * the GraphQL hop entirely — `ReleaseAdminCap` is not generic and carries
410
439
  * `release_id` directly.
411
440
  */
412
- export async function getOwnedWorks(client: MisoClient, owner: string): Promise<OwnedWork[]> {
413
- const miso = client.config.deployment.musicos;
414
-
415
- const [directCompCaps, directRecCaps, directRelCaps, vaulted] = await Promise.all([
416
- ownedGenericCaps(client, owner, `${miso}::composition::CompositionAdminCap`),
417
- ownedGenericCaps(client, owner, `${miso}::recording::RecordingAdminCap`),
418
- getOwnedReleaseAdminCaps(client.protocol, owner, miso),
419
- ownedVaultedWorkCaps(client, owner),
441
+ export const getOwnedWorks = Effect.fn("getOwnedWorks")(function* (
442
+ owner: string,
443
+ config: MisoConfig,
444
+ ): Effect.fn.Return<OwnedWork[], SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
445
+ const miso = config.deployment.musicos;
446
+
447
+ const [directCompCaps, directRecCaps, directRelCaps, vaulted] = yield* Effect.all([
448
+ ownedGenericCaps(owner, `${miso}::composition::CompositionAdminCap`),
449
+ ownedGenericCaps(owner, `${miso}::recording::RecordingAdminCap`),
450
+ getOwnedReleaseAdminCaps(owner, miso),
451
+ ownedVaultedWorkCaps(owner, config),
420
452
  ]);
421
- const vaultedRelCaps = await resolveVaultedReleaseCaps(client, vaulted.releases);
453
+ const vaultedRelCaps = yield* resolveVaultedReleaseCaps(vaulted.releases, config);
422
454
  const compCaps = [...directCompCaps, ...vaulted.compositions];
423
455
  const recCaps = [...directRecCaps, ...vaulted.recordings];
424
456
  const relCaps = [...directRelCaps, ...vaultedRelCaps];
425
457
 
426
- const addresses = await getWorkAddressesByShareTypes(
427
- client.graphql,
458
+ const addresses = yield* getWorkAddressesByShareTypes(
428
459
  { compositions: compCaps.map((c) => c.shareType), recordings: recCaps.map((c) => c.shareType) },
429
460
  miso,
430
461
  );
431
- const works = await getWorksByIds(client.protocol, {
462
+ const works = yield* getWorksByIds({
432
463
  compositions: Object.values(addresses.compositions).filter((id): id is string => id !== undefined),
433
464
  recordings: Object.values(addresses.recordings).filter((id): id is string => id !== undefined),
434
465
  releases: relCaps.map((c) => c.releaseId),
435
466
  });
436
- const recordingTitles = await getRecordingTitles(
437
- client.protocol,
438
- client.graphql,
467
+ const recordingTitles = yield* getRecordingTitles(
439
468
  Object.values(addresses.recordings).filter((id): id is string => id !== undefined),
440
469
  miso,
441
470
  );
@@ -465,32 +494,23 @@ export async function getOwnedWorks(client: MisoClient, owner: string): Promise<
465
494
  });
466
495
 
467
496
  return [...comps, ...recs, ...rels];
468
- }
497
+ });
469
498
 
470
499
  /** Classify a direct or vaulted cap by its on-chain type, then resolve its work. */
471
- export async function getWorkByCap(client: MisoClient, capId: string): Promise<WorkDetail | null> {
472
- const miso = client.config.deployment.musicos;
500
+ export const getWorkByCap = Effect.fn("getWorkByCap")(function* (
501
+ capId: string,
502
+ config: MisoConfig,
503
+ ): Effect.fn.Return<WorkDetail | null, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
504
+ const miso = config.deployment.musicos;
473
505
 
474
- let type: string;
475
- let json: Record<string, unknown> | null;
476
- let content: Uint8Array | undefined;
477
- try {
478
- const { object } = await client.protocol.core.getObject({
479
- objectId: capId,
480
- include: { json: true, content: true },
481
- });
482
- type = object.type;
483
- json = (object.json ?? null) as Record<string, unknown> | null;
484
- content = object.content;
485
- } catch (e) {
486
- if (isNotFound(e)) return null;
487
- throw e;
488
- }
506
+ const found = yield* getOptionalObjectContent(capId);
507
+ if (Option.isNone(found)) return null;
508
+ const { content, type } = found.value;
489
509
 
490
510
  const direct = classifyWorkAdminCapType(type, miso);
491
511
  if (direct?.kind === "composition") {
492
512
  const { shareType } = direct;
493
- const c = await getCompositionByShareType(client.protocol, client.graphql, shareType, miso);
513
+ const c = yield* getCompositionByShareType(shareType, miso);
494
514
  return {
495
515
  capId,
496
516
  kind: "composition",
@@ -504,8 +524,8 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
504
524
 
505
525
  if (direct?.kind === "recording") {
506
526
  const { shareType } = direct;
507
- const r = await getRecordingByShareType(client.protocol, client.graphql, shareType, miso);
508
- const titles = await getRecordingTitles(client.protocol, client.graphql, [r.id], miso);
527
+ const r = yield* getRecordingByShareType(shareType, miso);
528
+ const titles = yield* getRecordingTitles([r.id], miso);
509
529
  return {
510
530
  capId,
511
531
  kind: "recording",
@@ -517,9 +537,9 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
517
537
  }
518
538
 
519
539
  if (direct?.kind === "release") {
520
- const releaseId = typeof json?.release_id === "string" ? json.release_id : null;
521
- if (!releaseId) throw new Error(`Release cap ${capId} carries no release id`);
522
- const r = await getReleaseById(client.protocol, releaseId);
540
+ // `ReleaseAdminCap`'s BCS carries `release_id` directly no `json` include needed.
541
+ const { release_id: releaseId } = networkContracts.release.ReleaseAdminCap.parse(content);
542
+ const r = yield* getReleaseById(releaseId);
523
543
  return {
524
544
  capId,
525
545
  kind: "release",
@@ -531,12 +551,8 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
531
551
  };
532
552
  }
533
553
 
534
- const vaulted = classifyVaultedWorkAdminCapType(
535
- type,
536
- client.config.protocol.vault,
537
- miso,
538
- );
539
- if (!vaulted || !content) {
554
+ const vaulted = classifyVaultedWorkAdminCapType(type, config.protocol.vault, miso);
555
+ if (!vaulted) {
540
556
  // A real object, but not a supported work authority — "no such work",
541
557
  // not a transport failure. PressingAdminCap intentionally lands here.
542
558
  return null;
@@ -544,12 +560,7 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
544
560
  const { vault_id: vaultId } = vaultContract.VaultAdminCap.parse(content);
545
561
 
546
562
  if (vaulted.kind === "composition") {
547
- const c = await getCompositionByShareType(
548
- client.protocol,
549
- client.graphql,
550
- vaulted.shareType,
551
- miso,
552
- );
563
+ const c = yield* getCompositionByShareType(vaulted.shareType, miso);
553
564
  return {
554
565
  capId,
555
566
  kind: "composition",
@@ -562,13 +573,8 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
562
573
  }
563
574
 
564
575
  if (vaulted.kind === "recording") {
565
- const r = await getRecordingByShareType(
566
- client.protocol,
567
- client.graphql,
568
- vaulted.shareType,
569
- miso,
570
- );
571
- const titles = await getRecordingTitles(client.protocol, client.graphql, [r.id], miso);
576
+ const r = yield* getRecordingByShareType(vaulted.shareType, miso);
577
+ const titles = yield* getRecordingTitles([r.id], miso);
572
578
  return {
573
579
  capId,
574
580
  kind: "recording",
@@ -579,9 +585,9 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
579
585
  };
580
586
  }
581
587
 
582
- const [releaseCap] = await resolveVaultedReleaseCaps(client, [{ id: capId, vaultId }]);
588
+ const [releaseCap] = yield* resolveVaultedReleaseCaps([{ id: capId, vaultId }], config);
583
589
  if (!releaseCap) throw new Error(`Release vault ${vaultId} carries no release admin cap`);
584
- const r = await getReleaseById(client.protocol, releaseCap.releaseId);
590
+ const r = yield* getReleaseById(releaseCap.releaseId);
585
591
  return {
586
592
  capId,
587
593
  kind: "release",
@@ -591,15 +597,17 @@ export async function getWorkByCap(client: MisoClient, capId: string): Promise<W
591
597
  discCount: r.tracks.length > 0 ? 1 : 0,
592
598
  trackCount: r.tracks.length,
593
599
  };
594
- }
600
+ });
595
601
 
596
602
  // ── Balance ──────────────────────────────────────────────────────────────────
597
603
 
598
- const decimalsByClient = new WeakMap<MisoClient, Map<string, Promise<number>>>();
604
+ const decimalsByClient = new WeakMap<object, Map<string, Promise<number>>>();
599
605
 
600
- /** Resolve once per client/type. Failed lookups are evicted so a transient
601
- * fullnode error cannot poison a long-lived API worker. */
602
- async function coinDecimals(client: MisoClient, coinType: string): Promise<number> {
606
+ /**
607
+ * Resolve once per (transport client, coin type). Failed lookups are evicted so
608
+ * a transient fullnode error cannot poison a long-lived API worker.
609
+ */
610
+ function coinDecimalsPromise(client: object & { core: { getCoinMetadata: (args: { coinType: string }) => Promise<{ coinMetadata: { decimals?: number } | null }> } }, coinType: string): Promise<number> {
603
611
  let cache = decimalsByClient.get(client);
604
612
  if (!cache) {
605
613
  cache = new Map();
@@ -609,7 +617,7 @@ async function coinDecimals(client: MisoClient, coinType: string): Promise<numbe
609
617
  const cached = cache.get(key);
610
618
  if (cached) return cached;
611
619
 
612
- const pending = client.protocol.core.getCoinMetadata({ coinType: key }).then(({ coinMetadata }) => {
620
+ const pending = client.core.getCoinMetadata({ coinType: key }).then(({ coinMetadata }) => {
613
621
  const decimals = coinMetadata?.decimals;
614
622
  if (!Number.isSafeInteger(decimals) || decimals! < 0 || decimals! > 18) {
615
623
  throw new Error(`Coin metadata for ${key} has no supported decimal precision`);
@@ -617,12 +625,10 @@ async function coinDecimals(client: MisoClient, coinType: string): Promise<numbe
617
625
  return decimals!;
618
626
  });
619
627
  cache.set(key, pending);
620
- try {
621
- return await pending;
622
- } catch (error) {
623
- if (cache.get(key) === pending) cache.delete(key);
624
- throw error;
625
- }
628
+ pending.catch(() => {
629
+ if (cache!.get(key) === pending) cache!.delete(key);
630
+ });
631
+ return pending;
626
632
  }
627
633
 
628
634
  /**
@@ -630,11 +636,22 @@ async function coinDecimals(client: MisoClient, coinType: string): Promise<numbe
630
636
  * classes: callers using a `FundsWithdrawal` must never mistake coin-object
631
637
  * value for immediately withdrawable address balance.
632
638
  */
633
- export async function getBalance(client: MisoClient, address: string, coinType?: string): Promise<Balance> {
634
- const type = coinType ?? client.config.money.usdCoinType;
635
- const [res, decimals] = await Promise.all([
636
- client.protocol.core.getBalance({ owner: address, coinType: type }),
637
- coinDecimals(client, type),
639
+ export const getBalance = Effect.fn("getBalance")(function* (
640
+ address: string,
641
+ config: MisoConfig,
642
+ coinType?: string,
643
+ ): Effect.fn.Return<Balance, SuiRpcError, SuiClient> {
644
+ const client = yield* SuiClient;
645
+ const type = coinType ?? config.money.usdCoinType;
646
+ const [res, decimals] = yield* Effect.all([
647
+ Effect.tryPromise({
648
+ try: (signal) => client.core.getBalance({ owner: address, coinType: type, signal }),
649
+ catch: (cause) => new SuiRpcError({ operation: "getBalance", cause }),
650
+ }),
651
+ Effect.tryPromise({
652
+ try: () => coinDecimalsPromise(client, type),
653
+ catch: (cause) => new SuiRpcError({ operation: "getCoinMetadata", cause }),
654
+ }),
638
655
  ]);
639
656
  return {
640
657
  address: normalizeSuiAddress(address),
@@ -644,7 +661,7 @@ export async function getBalance(client: MisoClient, address: string, coinType?:
644
661
  addressBalance: u64(res.balance.addressBalance),
645
662
  decimals,
646
663
  };
647
- }
664
+ });
648
665
 
649
666
  // ── Ownership ────────────────────────────────────────────────────────────────
650
667
 
@@ -664,14 +681,22 @@ function isAddressOwner(owner: unknown, address: string): boolean {
664
681
  * A failed read means "can't confirm", which resolves to `false` — the editor
665
682
  * stays hidden rather than being offered and then rejected on submit.
666
683
  */
667
- export async function ownsParty(client: MisoClient, address: string, partyId: string): Promise<Ownership> {
668
- const capId = client.party.derivePartyAdminCapId(partyId);
669
- const isOwner = await client.protocol.core
670
- .getObject({ objectId: capId })
671
- .then(({ object }) => isAddressOwner(object?.owner, address))
672
- .catch(() => false);
684
+ export const ownsParty = Effect.fn("ownsParty")(function* (
685
+ address: string,
686
+ partyId: string,
687
+ config: MisoConfig,
688
+ ): Effect.fn.Return<Ownership, never, SuiClient> {
689
+ const capId = derivePartyAdminCapId(partyId, config.partyos.partyos);
690
+ const client = yield* SuiClient;
691
+ const isOwner = yield* Effect.tryPromise({
692
+ try: (signal) => client.core.getObject({ objectId: capId, signal }),
693
+ catch: (cause) => cause,
694
+ }).pipe(
695
+ Effect.map(({ object }) => isAddressOwner(object?.owner, address)),
696
+ Effect.catch(() => Effect.succeed(false)),
697
+ );
673
698
  return { address: normalizeSuiAddress(address), objectId: partyId, isOwner, capId };
674
- }
699
+ });
675
700
 
676
701
  /**
677
702
  * Whether `address` owns a record. Records are address-owned objects, so this is
@@ -679,10 +704,17 @@ export async function ownsParty(client: MisoClient, address: string, partyId: st
679
704
  * whether the Snapshots tab appears, and a non-owner has no snapshot content to
680
705
  * fetch regardless.
681
706
  */
682
- export async function ownsRecord(client: MisoClient, address: string, recordId: string): Promise<Ownership> {
683
- const isOwner = await client.protocol.core
684
- .getObject({ objectId: recordId })
685
- .then(({ object }) => isAddressOwner(object?.owner, address))
686
- .catch(() => false);
707
+ export const ownsRecord = Effect.fn("ownsRecord")(function* (
708
+ address: string,
709
+ recordId: string,
710
+ ): Effect.fn.Return<Ownership, never, SuiClient> {
711
+ const client = yield* SuiClient;
712
+ const isOwner = yield* Effect.tryPromise({
713
+ try: (signal) => client.core.getObject({ objectId: recordId, signal }),
714
+ catch: (cause) => cause,
715
+ }).pipe(
716
+ Effect.map(({ object }) => isAddressOwner(object?.owner, address)),
717
+ Effect.catch(() => Effect.succeed(false)),
718
+ );
687
719
  return { address: normalizeSuiAddress(address), objectId: recordId, isOwner };
688
- }
720
+ });