@misofm/platform 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -16,6 +16,7 @@
16
16
  // Party now shares the same network SDK registration and deployment as protocol
17
17
  // core, so every read hangs off one `sui.miso` namespace.
18
18
 
19
+ import type { ClientWithCoreApi } from "@mysten/sui/client";
19
20
  import { SuiGrpcClient } from "@mysten/sui/grpc";
20
21
  import { SuiGraphQLClient } from "@mysten/sui/graphql";
21
22
  import { miso, type MisoProtocolClient } from "@misofm/musicos/client";
@@ -28,25 +29,25 @@ function definedOverrides<T extends object>(value: T): Partial<T> {
28
29
  }
29
30
 
30
31
  /**
31
- * The structural slice of a Sui client the protocol SDK's read helpers want. They
32
- * declare `ClientWithCoreApi` against their own @mysten/sui; this alias names the
33
- * same shape from our side so the one cast in `createMisoClient` is explicit
34
- * rather than an `any` smeared across the codebase.
32
+ * The transport slice every `@misofm/platform/read` function needs to run its
33
+ * `Effect`: `Effect.provide(SuiClient.layer(client.protocol))`. `sui` (below)
34
+ * satisfies this structurally, since it is a real `ClientWithCoreApi`.
35
35
  */
36
- export type ProtocolClient = Parameters<typeof import("@misofm/musicos").getReleaseById>[0];
36
+ export type ProtocolClient = ClientWithCoreApi;
37
37
 
38
- /** A GraphQL client in the shape the protocol SDK's type-discovery queries want. */
38
+ /** A GraphQL client in the shape `@misofm/effect`'s `SuiGraphQL` service wants. */
39
39
  export type ProtocolGraphQLClient = SuiGraphQLClient;
40
40
 
41
41
  export interface MisoClient {
42
42
  config: MisoConfig;
43
43
  /** gRPC data plane (object-model core only; Party is `client.party`). */
44
44
  sui: SuiGrpcClient & { miso: MisoProtocolClient };
45
- /** The same client, typed for the protocol SDK's read helpers. */
45
+ /** The same client, for `Effect.provide(SuiClient.layer(client.protocol))`. */
46
46
  protocol: ProtocolClient;
47
- /** GraphQL RPC, typed for the protocol SDK's type-discovery queries. */
47
+ /** GraphQL RPC, for `Effect.provide(SuiGraphQL.layer(client.graphql))`. */
48
48
  graphql: ProtocolGraphQLClient;
49
- /** The raw GraphQL client, for this SDK's own queries (the pruned-transaction read). */
49
+ /** The raw GraphQL client identical to `graphql`; both names are kept for
50
+ * source compatibility with callers migrating from the pre-Effect client. */
50
51
  graphqlRaw: SuiGraphQLClient;
51
52
  /** Party identity and profile reads/builders, bound to `config.partyos`/`config.party`. */
52
53
  party: PartyPlatformClient;
@@ -85,8 +86,8 @@ export function createMisoClient(options: CreateMisoClientOptions = {}): MisoCli
85
86
  return {
86
87
  config,
87
88
  sui,
88
- protocol: sui as unknown as ProtocolClient,
89
- graphql: graphqlRaw as unknown as ProtocolGraphQLClient,
89
+ protocol: sui,
90
+ graphql: graphqlRaw,
90
91
  graphqlRaw,
91
92
  party,
92
93
  };
@@ -5,7 +5,9 @@
5
5
  // object carries a screaming-snake name ("HIP_HOP"). There is no id→name table
6
6
  // anywhere off-chain, so the names are read from the objects and humanized.
7
7
 
8
- import type { MisoClient } from "./client.ts";
8
+ import { Effect } from "effect";
9
+ import { getObjectsContent, type SuiClient } from "@misofm/effect";
10
+ import { Genre as GenreBcs } from "../contracts/genre/genre.ts";
9
11
 
10
12
  /** "HIP_HOP" → "Hip Hop". */
11
13
  function humanize(name: string): string {
@@ -18,20 +20,26 @@ function humanize(name: string): string {
18
20
 
19
21
  /**
20
22
  * Resolve `Genre` object ids to display names. Best-effort: an id that fails to
21
- * read is dropped rather than failing the artist page around it.
23
+ * read, or whose content does not parse as a `Genre`, is dropped rather than
24
+ * failing the artist page around it.
22
25
  */
23
- export async function resolveGenreNames(client: MisoClient, ids: string[]): Promise<string[]> {
26
+ export const resolveGenreNames = Effect.fn("resolveGenreNames")(function* (
27
+ ids: string[],
28
+ ): Effect.fn.Return<string[], never, SuiClient> {
24
29
  if (ids.length === 0) return [];
25
- try {
26
- const { objects } = await client.protocol.core.getObjects({ objectIds: ids, include: { json: true } });
27
- const out: string[] = [];
28
- for (const o of objects) {
29
- if (o instanceof Error) continue;
30
- const name = (o.json as { name?: unknown } | null)?.name;
31
- if (typeof name === "string" && name) out.push(humanize(name));
32
- }
33
- return out;
34
- } catch {
35
- return [];
36
- }
37
- }
30
+ return yield* getObjectsContent(ids).pipe(
31
+ Effect.map((contentById) => {
32
+ const out: string[] = [];
33
+ for (const { content } of contentById.values()) {
34
+ try {
35
+ const name = GenreBcs.parse(content).name;
36
+ if (name) out.push(humanize(name));
37
+ } catch {
38
+ // Malformed content — drop this genre rather than fail the page.
39
+ }
40
+ }
41
+ return out;
42
+ }),
43
+ Effect.catch(() => Effect.succeed([])),
44
+ );
45
+ });
package/src/read/index.ts CHANGED
@@ -10,9 +10,15 @@
10
10
  //
11
11
  // Browser- and server-compatible. The HTTP API is a thin cached transport over
12
12
  // this same surface; direct clients can use it without going through the API.
13
+ // Every read returns `Effect<A, E, SuiClient | SuiGraphQL>`; `createMisoClient`
14
+ // bundles the transport, config, and Party client — provide its `sui`/`graphqlRaw`
15
+ // as the Effect services once, at your program's boundary:
13
16
  //
14
17
  // const miso = createMisoClient({ network: "testnet" })
15
- // const pressing = await getPressingDetail(miso, pressingId)
18
+ // const layer = Layer.mergeAll(SuiClient.layer(miso.sui), SuiGraphQL.layer(miso.graphqlRaw))
19
+ // const pressing = await Effect.runPromise(
20
+ // getPressingDetail(pressingId, miso.config).pipe(Effect.provide(layer)),
21
+ // )
16
22
 
17
23
  export { createMisoClient } from "./client.ts";
18
24
  export type { MisoClient, CreateMisoClientOptions } from "./client.ts";
@@ -26,10 +26,13 @@ import {
26
26
  getCompositionsByIds,
27
27
  } from "@misofm/musicos";
28
28
  import { normalizeStructTag, normalizeSuiAddress } from "@mysten/sui/utils";
29
+ import { Effect } from "effect";
30
+ import { SuiClient, SuiGraphQL, SuiRpcError, type BcsDecodeError, type ObjectTypeMismatchError } from "@misofm/effect";
29
31
  import { getPressingDetail } from "./catalog.ts";
30
- import type { MisoClient } from "./client.ts";
31
32
  import { int } from "./internal/scalars.ts";
32
33
  import { requireRecordSalesDeployment } from "../deployments.ts";
34
+ import { MalformedRecordSoldEventError, ReceiptNotFoundError, ReleaseNotFoundError, RecordPurchaseNotFoundError } from "../errors.ts";
35
+ import type { MisoConfig } from "./config.ts";
33
36
  import type { Price, PressingDetail, PurchaseReceipt, RecordSale, TrackRoyalty } from "./types.ts";
34
37
  import { getWorkAddressesByShareTypes } from "./works.ts";
35
38
 
@@ -46,10 +49,6 @@ const RECORD_SOLD_EVENT_NAME = "listing::RecordSoldEvent";
46
49
  */
47
50
  const FULLNODE_WAIT_MS = 5_000;
48
51
 
49
- export class MalformedRecordSoldEventError extends Error {
50
- override readonly name = "MalformedRecordSoldEventError";
51
- }
52
-
53
52
  /** Composition royalty rate (bps) per recording id. Sparse — an unresolved parent has no entry. */
54
53
  type CompositionRates = Record<string, number | undefined>;
55
54
 
@@ -225,7 +224,7 @@ export function findRecordSales(
225
224
  !pricing || embeddedCurrency !== currencyType ||
226
225
  !validPricingRelationship(purchasePrice, pricing) ||
227
226
  s.edition <= 0 || s.number <= 0
228
- ) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent");
227
+ ) throw new MalformedRecordSoldEventError({ reason: "malformed canonical RecordSoldEvent" });
229
228
  sales.push({
230
229
  listingId: s.listing_id,
231
230
  pressingId: s.pressing_id,
@@ -245,13 +244,13 @@ export function findRecordSales(
245
244
  // Never reinterpret malformed canonical BCS through a looser JSON view.
246
245
  if (e.bcs.length > 0) {
247
246
  if (error instanceof MalformedRecordSoldEventError) throw error;
248
- throw new MalformedRecordSoldEventError(
249
- `malformed canonical RecordSoldEvent BCS: ${error instanceof Error ? error.message : String(error)}`,
250
- );
247
+ throw new MalformedRecordSoldEventError({
248
+ reason: `malformed canonical RecordSoldEvent BCS: ${error instanceof Error ? error.message : String(error)}`,
249
+ });
251
250
  }
252
251
  }
253
252
  const fromJson = e.json && saleFromJson(e.json, currencyType);
254
- if (!fromJson) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent JSON");
253
+ if (!fromJson) throw new MalformedRecordSoldEventError({ reason: "malformed canonical RecordSoldEvent JSON" });
255
254
  sales.push(fromJson);
256
255
  }
257
256
  return sales;
@@ -270,23 +269,37 @@ export function findRecordSale(
270
269
  }
271
270
 
272
271
  /** Every canonical sale, read from the fullnode in event order. */
273
- async function salesFromFullnode(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
274
- const result = await client.sui.waitForTransaction({
275
- digest: txDigest,
276
- include: { events: true },
277
- timeout: FULLNODE_WAIT_MS,
272
+ const salesFromFullnode = Effect.fn("salesFromFullnode")(function* (
273
+ txDigest: string,
274
+ config: MisoConfig,
275
+ ): Effect.fn.Return<
276
+ RecordSale[],
277
+ ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
278
+ SuiClient
279
+ > {
280
+ const client = yield* SuiClient;
281
+ const result = yield* Effect.tryPromise({
282
+ try: (signal) =>
283
+ client.core.waitForTransaction({
284
+ digest: txDigest,
285
+ include: { events: true },
286
+ timeout: FULLNODE_WAIT_MS,
287
+ signal,
288
+ }),
289
+ catch: (cause) => new SuiRpcError({ operation: "waitForTransaction", cause }),
278
290
  });
279
291
  if (result.$kind !== "Transaction" || !result.Transaction.status.success) {
280
- throw new Error(`Transaction did not succeed: ${txDigest}`);
292
+ return yield* new ReceiptNotFoundError({ digest: txDigest });
281
293
  }
282
- const sales = requireRecordSalesDeployment(client.config.recordSales);
283
- const found = findRecordSales(
284
- result.Transaction.events,
285
- sales.recordShopPackageId,
286
- );
287
- if (found.length === 0) throw new Error(`No record purchase in transaction ${txDigest}`);
294
+ const sales = requireRecordSalesDeployment(config.recordSales);
295
+ // `findRecordSales` only ever throws `MalformedRecordSoldEventError`.
296
+ const found = yield* Effect.try({
297
+ try: () => findRecordSales(result.Transaction.events, sales.recordShopPackageId),
298
+ catch: (cause) => cause as MalformedRecordSoldEventError,
299
+ });
300
+ if (found.length === 0) return yield* new RecordPurchaseNotFoundError({ digest: txDigest });
288
301
  return found;
289
- }
302
+ });
290
303
 
291
304
  const TX_EVENTS_QUERY = `query TransactionEvents($digest: String!) {
292
305
  transaction(digest: $digest) {
@@ -307,18 +320,35 @@ interface TxEventsResult {
307
320
  }
308
321
 
309
322
  /** Every canonical sale, read from the GraphQL indexer in event order. */
310
- async function salesFromIndexer(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
311
- const { data, errors } = await client.graphqlRaw.query<TxEventsResult, { digest: string }>({
312
- query: TX_EVENTS_QUERY,
313
- variables: { digest: txDigest },
323
+ const salesFromIndexer = Effect.fn("salesFromIndexer")(function* (
324
+ txDigest: string,
325
+ config: MisoConfig,
326
+ ): Effect.fn.Return<
327
+ RecordSale[],
328
+ ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
329
+ SuiGraphQL
330
+ > {
331
+ const client = yield* SuiGraphQL;
332
+ const { data, errors } = yield* Effect.tryPromise({
333
+ try: () =>
334
+ client.query<TxEventsResult, { digest: string }>({
335
+ query: TX_EVENTS_QUERY,
336
+ variables: { digest: txDigest },
337
+ }),
338
+ catch: (cause) => new SuiRpcError({ operation: "transactionEvents", cause }),
314
339
  });
315
- if (errors?.length) throw new Error(errors[0]!.message);
340
+ if (errors?.length) {
341
+ return yield* new SuiRpcError({
342
+ operation: "transactionEvents",
343
+ cause: new AggregateError(errors.map((e) => new Error(e.message)), "Transaction events query failed"),
344
+ });
345
+ }
316
346
 
317
347
  const effects = data?.transaction?.effects;
318
- if (!effects) throw new Error(`Transaction not found: ${txDigest}`);
319
- if (effects.status !== "SUCCESS") throw new Error(`Transaction did not succeed: ${txDigest}`);
348
+ if (!effects) return yield* new ReceiptNotFoundError({ digest: txDigest });
349
+ if (effects.status !== "SUCCESS") return yield* new ReceiptNotFoundError({ digest: txDigest });
320
350
 
321
- const salesDeployment = requireRecordSalesDeployment(client.config.recordSales);
351
+ const salesDeployment = requireRecordSalesDeployment(config.recordSales);
322
352
  const sales: RecordSale[] = [];
323
353
  for (const node of effects.events?.nodes ?? []) {
324
354
  const contents = node.contents;
@@ -332,19 +362,29 @@ async function salesFromIndexer(client: MisoClient, txDigest: string): Promise<R
332
362
  typeof contents.json === "object" && contents.json !== null
333
363
  ? saleFromJson(contents.json as Record<string, unknown>, currencyType)
334
364
  : null;
335
- if (!sale) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent JSON");
365
+ if (!sale) return yield* new MalformedRecordSoldEventError({ digest: txDigest, reason: "malformed canonical RecordSoldEvent JSON" });
336
366
  sales.push(sale);
337
367
  }
338
368
  if (sales.length > 0) return sales;
339
- throw new Error(`No record purchase in transaction ${txDigest}`);
340
- }
369
+ return yield* new RecordPurchaseNotFoundError({ digest: txDigest });
370
+ });
341
371
 
342
- async function salesFromChain(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
343
- return salesFromFullnode(client, txDigest).catch((error) => {
344
- if (error instanceof MalformedRecordSoldEventError) throw error;
345
- return salesFromIndexer(client, txDigest);
346
- });
347
- }
372
+ const salesFromChain = Effect.fn("salesFromChain")(function* (
373
+ txDigest: string,
374
+ config: MisoConfig,
375
+ ): Effect.fn.Return<
376
+ RecordSale[],
377
+ ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
378
+ SuiClient | SuiGraphQL
379
+ > {
380
+ return yield* salesFromFullnode(txDigest, config).pipe(
381
+ Effect.catchIf(
382
+ (error): error is ReceiptNotFoundError | RecordPurchaseNotFoundError | SuiRpcError =>
383
+ error._tag !== "MalformedRecordSoldEventError",
384
+ () => salesFromIndexer(txDigest, config),
385
+ ),
386
+ );
387
+ });
348
388
 
349
389
  /**
350
390
  * Each recording's parent composition royalty rate, in basis points, keyed by
@@ -357,11 +397,18 @@ async function salesFromChain(client: MisoClient, txDigest: string): Promise<Rec
357
397
  * Best-effort by design: any failure returns the rates resolved so far, and the
358
398
  * receipt drops to a track-level breakdown rather than failing the whole page.
359
399
  */
360
- async function compositionRatesByRecording(client: MisoClient, recordingIds: string[]): Promise<CompositionRates> {
400
+ const compositionRatesByRecording = Effect.fn("compositionRatesByRecording")(function* (
401
+ recordingIds: string[],
402
+ config: MisoConfig,
403
+ ): Effect.fn.Return<CompositionRates, SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
361
404
  if (recordingIds.length === 0) return {};
362
405
 
406
+ const client = yield* SuiClient;
363
407
  const shareTypeByRecording: Record<string, string> = {};
364
- const { objects } = await client.protocol.core.getObjects({ objectIds: [...new Set(recordingIds)] });
408
+ const { objects } = yield* Effect.tryPromise({
409
+ try: (signal) => client.core.getObjects({ objectIds: [...new Set(recordingIds)], signal }),
410
+ catch: (cause) => new SuiRpcError({ operation: "getObjects", cause }),
411
+ });
365
412
  for (const obj of objects) {
366
413
  if (obj instanceof Error || !obj.type) continue;
367
414
  try {
@@ -375,13 +422,12 @@ async function compositionRatesByRecording(client: MisoClient, recordingIds: str
375
422
  const shareTypes = Object.values(shareTypeByRecording);
376
423
  if (shareTypes.length === 0) return {};
377
424
 
378
- const addresses = await getWorkAddressesByShareTypes(
379
- client.graphql,
425
+ const addresses = yield* getWorkAddressesByShareTypes(
380
426
  { compositions: shareTypes, recordings: [] },
381
- client.config.deployment.musicos,
427
+ config.deployment.musicos,
382
428
  );
383
429
  const compositionIds = Object.values(addresses.compositions).filter((id): id is string => !!id);
384
- const compositions = await getCompositionsByIds(client.protocol, compositionIds);
430
+ const compositions = yield* getCompositionsByIds(compositionIds);
385
431
 
386
432
  const rates: CompositionRates = {};
387
433
  for (const [recordingId, shareType] of Object.entries(shareTypeByRecording)) {
@@ -390,7 +436,7 @@ async function compositionRatesByRecording(client: MisoClient, recordingIds: str
390
436
  if (composition) rates[recordingId] = int(composition.royaltyRate.value);
391
437
  }
392
438
  return rates;
393
- }
439
+ });
394
440
 
395
441
  /**
396
442
  * Split `paid` across the release's tracks by their `splitBps`, then split each
@@ -428,18 +474,31 @@ export function breakdown(
428
474
  * The sale identifies the Pressing before its detail is fetched. `null` only when
429
475
  * that immutable Pressing cannot be read; it is never superseded or destroyed.
430
476
  */
431
- async function hydratePurchaseReceipt(
432
- client: MisoClient,
477
+ type ReceiptError =
478
+ | ReceiptNotFoundError
479
+ | RecordPurchaseNotFoundError
480
+ | MalformedRecordSoldEventError
481
+ | ObjectTypeMismatchError
482
+ | ReleaseNotFoundError
483
+ | BcsDecodeError
484
+ | SuiRpcError;
485
+
486
+ const hydratePurchaseReceipt = Effect.fn("hydratePurchaseReceipt")(function* (
433
487
  sale: RecordSale,
434
- ): Promise<PurchaseReceipt | null> {
435
- const detail = await getPressingDetail(client, sale.pressingId);
488
+ config: MisoConfig,
489
+ ): Effect.fn.Return<
490
+ PurchaseReceipt | null,
491
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
492
+ SuiClient | SuiGraphQL
493
+ > {
494
+ const detail = yield* getPressingDetail(sale.pressingId, config);
436
495
  if (!detail) return null;
437
496
 
438
497
  // Best-effort: a failed composition lookup costs the sub-rows, not the page.
439
- const rates = await compositionRatesByRecording(
440
- client,
498
+ const rates = yield* compositionRatesByRecording(
441
499
  detail.release.tracks.map((t) => t.recordingId),
442
- ).catch(() => ({}));
500
+ config,
501
+ ).pipe(Effect.catch(() => Effect.succeed({} as CompositionRates)));
443
502
 
444
503
  return {
445
504
  sale,
@@ -447,30 +506,32 @@ async function hydratePurchaseReceipt(
447
506
  price: sale.pricing.amount,
448
507
  tracks: breakdown(detail.release.tracks, sale.purchasePrice, rates),
449
508
  };
450
- }
509
+ });
451
510
 
452
511
  /** Read and hydrate every canonical Record sale in transaction event order. */
453
- export async function getPurchaseReceipts(
454
- client: MisoClient,
512
+ export const getPurchaseReceipts = Effect.fn("getPurchaseReceipts")(function* (
455
513
  txDigest: string,
456
- ): Promise<PurchaseReceipt[]> {
457
- const sales = await salesFromChain(client, txDigest);
458
- const hydrated = await Promise.all(sales.map((sale) => hydratePurchaseReceipt(client, sale)));
514
+ config: MisoConfig,
515
+ ): Effect.fn.Return<PurchaseReceipt[], ReceiptError, SuiClient | SuiGraphQL> {
516
+ const sales = yield* salesFromChain(txDigest, config);
517
+ const hydrated = yield* Effect.forEach(sales, (sale) => hydratePurchaseReceipt(sale, config), {
518
+ concurrency: "unbounded",
519
+ });
459
520
  if (hydrated.some((receipt) => receipt === null)) {
460
521
  throw new Error(`A canonical Record sale in transaction ${txDigest} references an unreadable Pressing`);
461
522
  }
462
523
  return hydrated as PurchaseReceipt[];
463
- }
524
+ });
464
525
 
465
526
  /** Read one canonical receipt selected by its Record ID. Selection is mandatory:
466
527
  * one transaction may purchase multiple Records from the same Pressing. */
467
- export async function getPurchaseReceipt(
468
- client: MisoClient,
528
+ export const getPurchaseReceipt = Effect.fn("getPurchaseReceipt")(function* (
469
529
  txDigest: string,
470
530
  recordId: string,
471
- ): Promise<PurchaseReceipt | null> {
472
- const sales = await salesFromChain(client, txDigest);
531
+ config: MisoConfig,
532
+ ): Effect.fn.Return<PurchaseReceipt | null, ReceiptError, SuiClient | SuiGraphQL> {
533
+ const sales = yield* salesFromChain(txDigest, config);
473
534
  const wanted = normalizeSuiAddress(recordId);
474
535
  const sale = sales.find((candidate) => normalizeSuiAddress(candidate.recordId) === wanted);
475
- return sale ? hydratePurchaseReceipt(client, sale) : null;
476
- }
536
+ return sale ? yield* hydratePurchaseReceipt(sale, config) : null;
537
+ });