@misofm/musicos 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 (50) hide show
  1. package/README.md +90 -13
  2. package/dist/client.d.ts +27 -26
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +58 -51
  5. package/dist/client.js.map +1 -1
  6. package/dist/deployments.d.ts +9 -0
  7. package/dist/deployments.d.ts.map +1 -1
  8. package/dist/deployments.js +14 -0
  9. package/dist/deployments.js.map +1 -1
  10. package/dist/errors.d.ts +10 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +17 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/events.d.ts +6 -1
  15. package/dist/events.d.ts.map +1 -1
  16. package/dist/events.js +0 -5
  17. package/dist/events.js.map +1 -1
  18. package/dist/execute.d.ts +1 -48
  19. package/dist/execute.d.ts.map +1 -1
  20. package/dist/execute.js +5 -106
  21. package/dist/execute.js.map +1 -1
  22. package/dist/internal.d.ts +45 -6
  23. package/dist/internal.d.ts.map +1 -1
  24. package/dist/internal.js.map +1 -1
  25. package/dist/queries.d.ts +38 -62
  26. package/dist/queries.d.ts.map +1 -1
  27. package/dist/queries.js +240 -266
  28. package/dist/queries.js.map +1 -1
  29. package/dist/transactions.d.ts +2 -1
  30. package/dist/transactions.d.ts.map +1 -1
  31. package/dist/transactions.js.map +1 -1
  32. package/dist/types.d.ts +129 -79
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/types.js +169 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/view.d.ts +3 -2
  37. package/dist/view.d.ts.map +1 -1
  38. package/dist/view.js +19 -6
  39. package/dist/view.js.map +1 -1
  40. package/package.json +8 -1
  41. package/src/client.ts +78 -98
  42. package/src/deployments.ts +15 -0
  43. package/src/errors.ts +30 -0
  44. package/src/events.ts +1 -1
  45. package/src/execute.ts +5 -133
  46. package/src/internal.ts +17 -23
  47. package/src/queries.ts +386 -424
  48. package/src/transactions.ts +2 -1
  49. package/src/types.ts +59 -48
  50. package/src/view.ts +23 -9
package/src/queries.ts CHANGED
@@ -6,22 +6,31 @@
6
6
  // tracks the on-chain ABI). Generic-type discovery (by share type / by owner)
7
7
  // uses GraphQL to find object addresses, then reads them through the Core path.
8
8
  //
9
- // Missing-object convention (null vs throw):
10
- // - Core-object getters in this module (`getCompositionById`,
11
- // `get*AdminCapById`, …) THROW when the object is missing. Callers pass ids
12
- // they obtained from the chain, so a miss means a broken reference — an
13
- // exceptional state, not a normal one.
14
- // - Extension dynamic-field readers return `null` — extension data is optional
15
- // by design, and "not attached" is a normal, expected state. Use
16
- // `getExtensionField` with the generated BCS codec for the extension.
17
- // All null-returning readers use {@link isNotFound} to distinguish a missing
18
- // object from a transport failure (which still throws).
19
-
20
- import type { ClientWithCoreApi } from "@mysten/sui/client";
21
- import type { SuiGraphQLClient } from "@mysten/sui/graphql";
9
+ // Every read requires the `SuiClient` service (and `SuiGraphQL` for the
10
+ // type-discovery reads) instead of taking a client parameter — see
11
+ // `@misofm/effect`. Not-found is a typed `ObjectNotFoundError`; a BCS decode
12
+ // failure (including a wrong on-chain type, which fails to parse against the
13
+ // expected ABI) is a typed `BcsDecodeError`. Extension dynamic-field readers
14
+ // return `Option.none()` for "not attached" — extension data is optional by
15
+ // design, and absence is a normal, expected state, not a failure.
16
+
17
+ import { ConflictingWorkKindError } from "./errors.ts";
18
+ import { Effect, Option, Schema } from "effect";
22
19
  import { graphql } from "@mysten/sui/graphql/schema";
23
20
  import { deriveObjectID, normalizeSuiAddress } from "@mysten/sui/utils";
24
21
 
22
+ import {
23
+ type BcsParser,
24
+ BcsDecodeError,
25
+ decodeBcs,
26
+ getObjectContent,
27
+ getObjectsContent,
28
+ ObjectNotFoundError,
29
+ SuiClient,
30
+ SuiGraphQL,
31
+ SuiRpcError,
32
+ } from "@misofm/effect";
33
+
25
34
  import { Composition as CompositionBcs } from "./contracts/musicos/composition.ts";
26
35
  import { Recording as RecordingBcs } from "./contracts/musicos/recording.ts";
27
36
  import {
@@ -29,20 +38,22 @@ import {
29
38
  ReleaseRegistry as ReleaseRegistryBcs,
30
39
  } from "./contracts/musicos/release.ts";
31
40
  import {
32
- mapBps,
33
41
  mapComposition,
34
42
  mapRecording,
35
43
  mapRelease,
36
44
  } from "./internal.ts";
37
- import type {
45
+ import {
38
46
  Composition,
39
47
  CompositionAdminCap,
40
48
  Recording,
41
49
  RecordingAdminCap,
42
50
  Release,
43
51
  ReleaseAdminCap,
52
+ ReleaseRegistry,
44
53
  } from "./types.ts";
45
54
 
55
+ export type { BcsParser };
56
+
46
57
  // ============================================================================
47
58
  // Helpers
48
59
  // ============================================================================
@@ -73,70 +84,86 @@ export function extractTypeParams2(objectType: string): [string, string] {
73
84
  throw new Error(`Expected two type parameters in: ${objectType}`);
74
85
  }
75
86
 
87
+ /** Key bytes for Move unit structs (single `0x00` for `dummy_field: bool = false`). */
88
+ const UNIT_STRUCT_KEY_BYTES = new Uint8Array([0x00]);
89
+
76
90
  /**
77
- * True when `e` is a "this object does not exist" error from any of the Sui
78
- * client transports, so null-returning readers can distinguish absence from
79
- * transport failure. Matches, in order of preference:
91
+ * True when `e` is a "this object/dynamic field does not exist" error from any
92
+ * of the Sui client transports. This mirrors `@misofm/effect`'s internal
93
+ * classifier (not exported, since `getObjectContent` already covers the common
94
+ * case) — needed here for the reads that don't go through it: type-only object
95
+ * reads (no `content`) and dynamic-field reads. Matches, in order of
96
+ * preference:
80
97
  *
81
98
  * 1. Structured `ObjectError.code` values thrown by the JSON-RPC core client
82
99
  * (`notExists`, `deleted`, `dynamicFieldNotFound`) and the GraphQL core
83
- * client (`notFound`). The class itself is not exported by `@mysten/sui`,
84
- * so we duck-type on `code`.
100
+ * client (`notFound`).
85
101
  * 2. The message shapes those clients (and the gRPC core client, which wraps
86
- * the server's per-object status message in a plain `Error`) produce:
87
- * "Object 0x… does not exist" / "Object 0x… not found" / "Object 0x… has
88
- * been deleted" / "Dynamic field not found for object 0x…" / "No object
89
- * found for id 0x…".
90
- *
91
- * Transport/protocol errors must NOT match: every message pattern requires
92
- * object-ish context ("object" / "dynamic field"), so e.g. a JSON-RPC
93
- * "Method not found" or a gRPC "peer not found" is never treated as a missing
94
- * object and propagates to the caller.
102
+ * the server's per-object status message in a plain `Error`) produce.
95
103
  */
96
- export function isNotFound(e: unknown): boolean {
104
+ function isMissingObjectError(e: unknown): boolean {
97
105
  if (typeof e === "object" && e !== null && "code" in e) {
98
106
  const code = (e as { code: unknown }).code;
99
- if (
100
- code === "notExists" ||
101
- code === "deleted" ||
102
- code === "dynamicFieldNotFound" ||
103
- code === "notFound"
104
- ) {
107
+ if (code === "notExists" || code === "deleted" || code === "dynamicFieldNotFound" || code === "notFound") {
105
108
  return true;
106
109
  }
107
110
  }
108
111
  const msg = e instanceof Error ? e.message : String(e);
109
112
  return (
110
- /\bobject\b[\s\S]*\b(?:not\s?found|does not exist|has been deleted)\b/i.test(
111
- msg,
112
- ) ||
113
+ /\bobject\b[\s\S]*\b(?:not\s?found|does not exist|has been deleted)\b/i.test(msg) ||
113
114
  /\bdynamic field\b[\s\S]*\bnot\s?found\b/i.test(msg) ||
114
115
  /\bno object\b/i.test(msg)
115
116
  );
116
117
  }
117
118
 
118
- /** Key bytes for Move unit structs (single `0x00` for `dummy_field: bool = false`). */
119
- const UNIT_STRUCT_KEY_BYTES = new Uint8Array([0x00]);
120
-
121
- /** Any generated BCS codec with a `parse` method. */
122
- export interface BcsParser<T> {
123
- parse(bytes: Uint8Array): T;
119
+ /** Classifies a Core API rejection against `objectId`: missing -> `ObjectNotFoundError`, else `SuiRpcError`. */
120
+ function classifyMissing(objectId: string, operation: string, cause: unknown): ObjectNotFoundError | SuiRpcError {
121
+ return isMissingObjectError(cause) ? new ObjectNotFoundError({ objectId }) : new SuiRpcError({ operation, cause });
124
122
  }
125
123
 
124
+ /** Fetches just an object's on-chain `type` (no content) through the Core API. */
125
+ const getObjectType = Effect.fn("getObjectType")(function* (
126
+ objectId: string,
127
+ ): Effect.fn.Return<string, ObjectNotFoundError | SuiRpcError, SuiClient> {
128
+ const client = yield* SuiClient;
129
+ const { object } = yield* Effect.tryPromise({
130
+ try: (signal) => client.core.getObject({ objectId, signal }),
131
+ catch: (cause) => classifyMissing(objectId, "getObject", cause),
132
+ });
133
+ return object.type;
134
+ });
135
+
136
+ /** Fetches one dynamic field's raw BCS value bytes; not-found becomes `ObjectNotFoundError`. */
137
+ const getDynamicFieldValue = Effect.fn("getDynamicFieldValue")(function* (
138
+ parentId: string,
139
+ name: { type: string; bcs: Uint8Array },
140
+ ): Effect.fn.Return<Uint8Array, ObjectNotFoundError | SuiRpcError, SuiClient> {
141
+ const client = yield* SuiClient;
142
+ const { dynamicField } = yield* Effect.tryPromise({
143
+ try: (signal) => client.core.getDynamicField({ parentId, name, signal }),
144
+ catch: (cause) => classifyMissing(parentId, "getDynamicField", cause),
145
+ });
146
+ return dynamicField.value.bcs;
147
+ });
148
+
126
149
  /** Exhaust every Core owned-object page; cap discovery must not truncate. */
127
- async function listAllOwnedObjects(
128
- client: ClientWithCoreApi,
150
+ const listAllOwnedObjects = Effect.fn("listAllOwnedObjects")(function* (
129
151
  input: Record<string, unknown>,
130
- ): Promise<Array<{ objectId: string; type?: string; json?: unknown }>> {
152
+ ): Effect.fn.Return<Array<{ objectId: string; type?: string; json?: unknown }>, SuiRpcError, SuiClient> {
153
+ const client = yield* SuiClient;
131
154
  const objects: Array<{ objectId: string; type?: string; json?: unknown }> = [];
132
155
  let cursor: string | null | undefined;
133
156
  do {
134
- const page = await (client.core.listOwnedObjects as (args: unknown) => Promise<{
135
- objects: Array<{ objectId: string; type?: string; json?: unknown }>;
136
- pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
137
- hasNextPage?: boolean;
138
- cursor?: string | null;
139
- }>)(cursor ? { ...input, cursor } : input);
157
+ const page = yield* Effect.tryPromise({
158
+ try: (signal) =>
159
+ (client.core.listOwnedObjects as (args: unknown) => Promise<{
160
+ objects: Array<{ objectId: string; type?: string; json?: unknown }>;
161
+ pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
162
+ hasNextPage?: boolean;
163
+ cursor?: string | null;
164
+ }>)(cursor ? { ...input, cursor, signal } : { ...input, signal }),
165
+ catch: (cause) => new SuiRpcError({ operation: "listOwnedObjects", cause }),
166
+ });
140
167
  objects.push(...page.objects);
141
168
  cursor = page.pageInfo?.hasNextPage
142
169
  ? page.pageInfo.endCursor
@@ -145,33 +172,22 @@ async function listAllOwnedObjects(
145
172
  : null;
146
173
  } while (cursor);
147
174
  return objects;
148
- }
149
-
150
- /** Fetches one object's BCS content bytes (or null if absent). */
151
- async function getContent(
152
- client: ClientWithCoreApi,
153
- objectId: string,
154
- ): Promise<Uint8Array | null> {
155
- const { object } = await client.core.getObject({
156
- objectId,
157
- include: { content: true },
158
- });
159
- return object.content ?? null;
160
- }
175
+ });
161
176
 
162
177
  /**
163
- * Fetch and parse an object through the transport-neutral Core API. Object
164
- * content is BCS; never pass the full `objectBcs` envelope to a Move codec.
178
+ * Fetch and parse an object through the transport-neutral Core API, validating
179
+ * the parsed result against a domain `Schema`. Object content is BCS; never
180
+ * pass the full `objectBcs` envelope to a Move codec.
165
181
  */
166
- export async function getObjectByBcs<T>(
167
- client: ClientWithCoreApi,
182
+ export const getObjectByBcs = Effect.fn("getObjectByBcs")(function* <P, A>(
168
183
  objectId: string,
169
- codec: BcsParser<T>,
170
- ): Promise<T> {
171
- const content = await getContent(client, objectId);
172
- if (!content) throw new Error(`Object not found: ${objectId}`);
173
- return codec.parse(content);
174
- }
184
+ codec: BcsParser<P>,
185
+ schema: Schema.Codec<A, any, never, never>,
186
+ typeName: string,
187
+ ): Effect.fn.Return<A, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient> {
188
+ const { content } = yield* getObjectContent(objectId);
189
+ return yield* decodeBcs(codec, schema, content, { type: typeName, objectId });
190
+ });
175
191
 
176
192
  export interface ExtensionFieldParams<T> {
177
193
  /** Freshly published package address for the extension. */
@@ -185,27 +201,20 @@ export interface ExtensionFieldParams<T> {
185
201
  /**
186
202
  * Read an optional first-party extension field from a core object's UID. Every
187
203
  * current extension uses a fieldless `ExtensionKey`, whose BCS is one false
188
- * boolean byte. Absence returns `null`; transport errors still propagate.
204
+ * boolean byte. Absence resolves to `Option.none()`; transport errors still fail.
189
205
  */
190
- export async function getExtensionField<T>(
191
- client: ClientWithCoreApi,
206
+ export const getExtensionField = Effect.fn("getExtensionField")(function* <T>(
192
207
  parentId: string,
193
208
  params: ExtensionFieldParams<T>,
194
- ): Promise<T | null> {
195
- try {
196
- const { dynamicField } = await client.core.getDynamicField({
197
- parentId,
198
- name: {
199
- type: `${params.packageId}::${params.module}::ExtensionKey`,
200
- bcs: UNIT_STRUCT_KEY_BYTES,
201
- },
202
- });
203
- return params.codec.parse(dynamicField.value.bcs);
204
- } catch (error) {
205
- if (isNotFound(error)) return null;
206
- throw error;
207
- }
208
- }
209
+ ): Effect.fn.Return<Option.Option<T>, SuiRpcError, SuiClient> {
210
+ return yield* getDynamicFieldValue(parentId, {
211
+ type: `${params.packageId}::${params.module}::ExtensionKey`,
212
+ bcs: UNIT_STRUCT_KEY_BYTES,
213
+ }).pipe(
214
+ Effect.map((bytes) => Option.some(params.codec.parse(bytes))),
215
+ Effect.catchTag("ObjectNotFoundError", () => Effect.succeed(Option.none<T>())),
216
+ );
217
+ });
209
218
 
210
219
  export interface ReleaseDspFieldParams<T> {
211
220
  /** Freshly published `release_dsp_link` package address. */
@@ -216,46 +225,37 @@ export interface ReleaseDspFieldParams<T> {
216
225
  codec: BcsParser<T>;
217
226
  }
218
227
 
219
- async function getReleaseDspField<T>(
220
- client: ClientWithCoreApi,
228
+ const getReleaseDspField = Effect.fn("getReleaseDspField")(function* <T>(
221
229
  releaseId: string,
222
230
  key: "ReleaseLinkKey" | "TrackLinksKey",
223
231
  params: ReleaseDspFieldParams<T>,
224
- ): Promise<T | null> {
232
+ ): Effect.fn.Return<Option.Option<T>, SuiRpcError, SuiClient> {
225
233
  if (!Number.isInteger(params.platform) || params.platform < 0 || params.platform > 255) {
226
234
  throw new Error("DSP platform must be a u8 discriminator");
227
235
  }
228
- try {
229
- const { dynamicField } = await client.core.getDynamicField({
230
- parentId: releaseId,
231
- name: {
232
- type: `${params.packageId}::release_dsp_link::${key}`,
233
- bcs: Uint8Array.of(params.platform),
234
- },
235
- });
236
- return params.codec.parse(dynamicField.value.bcs);
237
- } catch (error) {
238
- if (isNotFound(error)) return null;
239
- throw error;
240
- }
241
- }
236
+ return yield* getDynamicFieldValue(releaseId, {
237
+ type: `${params.packageId}::release_dsp_link::${key}`,
238
+ bcs: Uint8Array.of(params.platform),
239
+ }).pipe(
240
+ Effect.map((bytes) => Option.some(params.codec.parse(bytes))),
241
+ Effect.catchTag("ObjectNotFoundError", () => Effect.succeed(Option.none<T>())),
242
+ );
243
+ });
242
244
 
243
245
  /** Read a release-level DSP link stored under `ReleaseLinkKey(platform)`. */
244
246
  export function getReleaseDspLink<T>(
245
- client: ClientWithCoreApi,
246
247
  releaseId: string,
247
248
  params: ReleaseDspFieldParams<T>,
248
- ): Promise<T | null> {
249
- return getReleaseDspField(client, releaseId, "ReleaseLinkKey", params);
249
+ ): Effect.Effect<Option.Option<T>, SuiRpcError, SuiClient> {
250
+ return getReleaseDspField(releaseId, "ReleaseLinkKey", params);
250
251
  }
251
252
 
252
253
  /** Read the per-track DSP-link array stored under `TrackLinksKey(platform)`. */
253
254
  export function getTrackDspLinks<T>(
254
- client: ClientWithCoreApi,
255
255
  releaseId: string,
256
256
  params: ReleaseDspFieldParams<T>,
257
- ): Promise<T | null> {
258
- return getReleaseDspField(client, releaseId, "TrackLinksKey", params);
257
+ ): Effect.Effect<Option.Option<T>, SuiRpcError, SuiClient> {
258
+ return getReleaseDspField(releaseId, "TrackLinksKey", params);
259
259
  }
260
260
 
261
261
  // ============================================================================
@@ -263,11 +263,10 @@ export function getTrackDspLinks<T>(
263
263
  // ============================================================================
264
264
 
265
265
  /** Parse the shared canonical core `miso::release::ReleaseRegistry` by ID. */
266
- export async function getReleaseRegistryById(
267
- client: ClientWithCoreApi,
266
+ export function getReleaseRegistryById(
268
267
  registryId: string,
269
- ) {
270
- return getObjectByBcs(client, registryId, ReleaseRegistryBcs);
268
+ ): Effect.Effect<ReleaseRegistry, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient> {
269
+ return getObjectByBcs(registryId, ReleaseRegistryBcs, ReleaseRegistry, "ReleaseRegistry");
271
270
  }
272
271
 
273
272
  // ============================================================================
@@ -339,16 +338,17 @@ interface WorkAddressConnection {
339
338
  * exposes the first, so one bare Recording scan is shared by every requested
340
339
  * recording type and filtered client-side.
341
340
  */
342
- export async function getWorkAddressesByShareTypes(
343
- client: SuiGraphQLClient,
341
+ export const getWorkAddressesByShareTypes = Effect.fn("getWorkAddressesByShareTypes")(function* (
344
342
  shareTypes: WorkShareTypes,
345
343
  misoPackageId: string,
346
- ): Promise<WorkAddressesByShareType> {
344
+ ): Effect.fn.Return<WorkAddressesByShareType, SuiRpcError, SuiGraphQL> {
347
345
  const compositions = [...new Set(shareTypes.compositions)];
348
346
  const recordings = new Set(shareTypes.recordings);
349
347
  const out: WorkAddressesByShareType = { compositions: {}, recordings: {} };
350
348
  if (compositions.length === 0 && recordings.size === 0) return out;
351
349
 
350
+ const client = yield* SuiGraphQL;
351
+
352
352
  const declarations: string[] = [];
353
353
  const selections: string[] = [];
354
354
  const variables: Record<string, string> = {};
@@ -359,8 +359,7 @@ export async function getWorkAddressesByShareTypes(
359
359
  selections.push(
360
360
  `composition${index}: objects(first: 1, filter: { type: $${variable} }) { nodes { address } }`,
361
361
  );
362
- variables[variable] =
363
- `${misoPackageId}::composition::Composition<${shareType}>`;
362
+ variables[variable] = `${misoPackageId}::composition::Composition<${shareType}>`;
364
363
  });
365
364
 
366
365
  if (recordings.size > 0) {
@@ -372,20 +371,24 @@ export async function getWorkAddressesByShareTypes(
372
371
  variables.recordingType = `${misoPackageId}::recording::Recording`;
373
372
  }
374
373
 
375
- const result = await client.query<
376
- Record<string, WorkAddressConnection | null>,
377
- Record<string, string>
378
- >({
379
- query: `query WorkAddressesByShareTypes(${declarations.join(", ")}) {
380
- ${selections.join("\n")}
381
- }`,
382
- variables,
374
+ const result = yield* Effect.tryPromise({
375
+ try: () =>
376
+ client.query<Record<string, WorkAddressConnection | null>, Record<string, string>>({
377
+ query: `query WorkAddressesByShareTypes(${declarations.join(", ")}) {
378
+ ${selections.join("\n")}
379
+ }`,
380
+ variables,
381
+ }),
382
+ catch: (cause) => new SuiRpcError({ operation: "workAddressesByShareTypes", cause }),
383
383
  });
384
384
  if (result.errors?.length) {
385
- throw new AggregateError(
386
- result.errors.map((error) => new Error(error.message)),
387
- "Work type discovery failed",
388
- );
385
+ return yield* new SuiRpcError({
386
+ operation: "workAddressesByShareTypes",
387
+ cause: new AggregateError(
388
+ result.errors.map((error) => new Error(error.message)),
389
+ "Work type discovery failed",
390
+ ),
391
+ });
389
392
  }
390
393
 
391
394
  compositions.forEach((shareType, index) => {
@@ -393,9 +396,7 @@ export async function getWorkAddressesByShareTypes(
393
396
  if (address) out.compositions[shareType] = address;
394
397
  });
395
398
 
396
- const readRecordingPage = (
397
- page: WorkAddressConnection | null | undefined,
398
- ) => {
399
+ const readRecordingPage = (page: WorkAddressConnection | null | undefined) => {
399
400
  for (const node of page?.nodes ?? []) {
400
401
  const repr = node.asMoveObject?.contents?.type?.repr;
401
402
  if (!repr) continue;
@@ -417,33 +418,35 @@ export async function getWorkAddressesByShareTypes(
417
418
  recordingPage.pageInfo.endCursor &&
418
419
  Object.keys(out.recordings).length < recordings.size
419
420
  ) {
420
- const next = await client.query<
421
- { recordings: WorkAddressConnection | null },
422
- { recordingType: string; cursor: string }
423
- >({
424
- query: `query RecordingWorkAddresses($recordingType: String!, $cursor: String!) {
425
- recordings: objects(first: 50, after: $cursor, filter: { type: $recordingType }) {
426
- pageInfo { hasNextPage endCursor }
427
- nodes { address asMoveObject { contents { type { repr } } } }
428
- }
429
- }`,
430
- variables: {
431
- recordingType: variables.recordingType!,
432
- cursor: recordingPage.pageInfo.endCursor,
433
- },
421
+ const cursor: string = recordingPage.pageInfo.endCursor;
422
+ const next = yield* Effect.tryPromise({
423
+ try: () =>
424
+ client.query<{ recordings: WorkAddressConnection | null }, { recordingType: string; cursor: string }>({
425
+ query: `query RecordingWorkAddresses($recordingType: String!, $cursor: String!) {
426
+ recordings: objects(first: 50, after: $cursor, filter: { type: $recordingType }) {
427
+ pageInfo { hasNextPage endCursor }
428
+ nodes { address asMoveObject { contents { type { repr } } } }
429
+ }
430
+ }`,
431
+ variables: { recordingType: variables.recordingType!, cursor },
432
+ }),
433
+ catch: (cause) => new SuiRpcError({ operation: "recordingWorkAddresses", cause }),
434
434
  });
435
435
  if (next.errors?.length) {
436
- throw new AggregateError(
437
- next.errors.map((error) => new Error(error.message)),
438
- "Recording type discovery failed",
439
- );
436
+ return yield* new SuiRpcError({
437
+ operation: "recordingWorkAddresses",
438
+ cause: new AggregateError(
439
+ next.errors.map((error) => new Error(error.message)),
440
+ "Recording type discovery failed",
441
+ ),
442
+ });
440
443
  }
441
444
  recordingPage = next.data?.recordings;
442
445
  readRecordingPage(recordingPage);
443
446
  }
444
447
 
445
448
  return out;
446
- }
449
+ });
447
450
 
448
451
  export interface WorkIds {
449
452
  compositions: readonly string[];
@@ -458,21 +461,16 @@ export interface WorksById {
458
461
  }
459
462
 
460
463
  /** Fetch and parse heterogeneous work objects through one Core bulk request. */
461
- export async function getWorksByIds(
462
- client: ClientWithCoreApi,
464
+ export const getWorksByIds = Effect.fn("getWorksByIds")(function* (
463
465
  ids: WorkIds,
464
- ): Promise<WorksById> {
466
+ ): Effect.fn.Return<WorksById, ConflictingWorkKindError | SuiRpcError | BcsDecodeError, SuiClient> {
465
467
  const kinds = new Map<string, keyof WorksById>();
466
- for (const [kind, objectIds] of Object.entries(ids) as Array<
467
- [keyof WorksById, readonly string[]]
468
- >) {
468
+ for (const [kind, objectIds] of Object.entries(ids) as Array<[keyof WorksById, readonly string[]]>) {
469
469
  for (const objectId of objectIds) {
470
470
  const normalized = normalizeSuiAddress(objectId);
471
471
  const previous = kinds.get(normalized);
472
472
  if (previous && previous !== kind) {
473
- throw new Error(
474
- `Work ${normalized} was requested as both ${previous} and ${kind}`,
475
- );
473
+ return yield* new ConflictingWorkKindError({ objectId: normalized, kinds: [previous, kind] });
476
474
  }
477
475
  kinds.set(normalized, kind);
478
476
  }
@@ -481,93 +479,95 @@ export async function getWorksByIds(
481
479
  const out: WorksById = { compositions: {}, recordings: {}, releases: {} };
482
480
  if (kinds.size === 0) return out;
483
481
 
484
- const { objects } = await client.core.getObjects({
485
- objectIds: [...kinds.keys()],
486
- include: { content: true },
487
- });
488
- for (const obj of objects) {
489
- if (obj instanceof Error || !obj.content) continue;
490
- const kind = kinds.get(normalizeSuiAddress(obj.objectId));
491
- if (kind === "compositions") {
492
- out.compositions[obj.objectId] = mapComposition(
493
- obj.objectId,
494
- CompositionBcs.parse(obj.content),
495
- );
496
- } else if (kind === "recordings") {
497
- out.recordings[obj.objectId] = mapRecording(
498
- obj.objectId,
499
- RecordingBcs.parse(obj.content),
500
- );
501
- } else if (kind === "releases") {
502
- out.releases[obj.objectId] = mapRelease(
503
- obj.objectId,
504
- ReleaseBcs.parse(obj.content),
505
- );
506
- }
507
- }
482
+ const contents = yield* getObjectsContent([...kinds.keys()]);
483
+ yield* Effect.forEach(
484
+ [...contents.entries()],
485
+ ([objectId, { content }]) =>
486
+ Effect.gen(function* () {
487
+ const kind = kinds.get(normalizeSuiAddress(objectId));
488
+ if (kind === "compositions") {
489
+ out.compositions[objectId] = yield* decodeBcs(
490
+ { parse: (bytes) => mapComposition(objectId, CompositionBcs.parse(bytes)) },
491
+ Composition,
492
+ content,
493
+ { type: "Composition", objectId },
494
+ );
495
+ } else if (kind === "recordings") {
496
+ out.recordings[objectId] = yield* decodeBcs(
497
+ { parse: (bytes) => mapRecording(objectId, RecordingBcs.parse(bytes)) },
498
+ Recording,
499
+ content,
500
+ { type: "Recording", objectId },
501
+ );
502
+ } else if (kind === "releases") {
503
+ out.releases[objectId] = yield* decodeBcs(
504
+ { parse: (bytes) => mapRelease(objectId, ReleaseBcs.parse(bytes)) },
505
+ Release,
506
+ content,
507
+ { type: "Release", objectId },
508
+ );
509
+ }
510
+ }),
511
+ { concurrency: "unbounded" },
512
+ );
508
513
  return out;
509
- }
514
+ });
510
515
 
511
516
  // ============================================================================
512
517
  // Composition
513
518
  // ============================================================================
514
519
 
515
520
  /** Fetches multiple compositions by ID in one Core request. */
516
- export async function getCompositionsByIds(
517
- client: ClientWithCoreApi,
521
+ export const getCompositionsByIds = Effect.fn("getCompositionsByIds")(function* (
518
522
  compositionIds: string[],
519
- ): Promise<Record<string, Composition>> {
523
+ ): Effect.fn.Return<Record<string, Composition>, SuiRpcError | BcsDecodeError, SuiClient> {
520
524
  if (compositionIds.length === 0) return {};
521
- const { objects } = await client.core.getObjects({
522
- objectIds: compositionIds,
523
- include: { content: true },
524
- });
525
- const out: Record<string, Composition> = {};
526
- for (const obj of objects) {
527
- if (obj instanceof Error || !obj.content) continue;
528
- out[obj.objectId] = mapComposition(
529
- obj.objectId,
530
- CompositionBcs.parse(obj.content),
531
- );
532
- }
533
- return out;
534
- }
525
+ const contents = yield* getObjectsContent(compositionIds);
526
+ const decoded = yield* Effect.forEach(
527
+ [...contents.entries()],
528
+ ([objectId, { content }]) =>
529
+ decodeBcs(
530
+ { parse: (bytes) => mapComposition(objectId, CompositionBcs.parse(bytes)) },
531
+ Composition,
532
+ content,
533
+ { type: "Composition", objectId },
534
+ ).pipe(Effect.map((composition) => [objectId, composition] as const)),
535
+ { concurrency: "unbounded" },
536
+ );
537
+ return Object.fromEntries(decoded);
538
+ });
535
539
 
536
540
  /** Fetches a composition by its object ID. */
537
- export async function getCompositionById(
538
- client: ClientWithCoreApi,
541
+ export function getCompositionById(
539
542
  compositionId: string,
540
- ): Promise<Composition> {
541
- const content = await getContent(client, compositionId);
542
- if (!content) throw new Error(`Composition not found: ${compositionId}`);
543
- return mapComposition(compositionId, CompositionBcs.parse(content));
543
+ ): Effect.Effect<Composition, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient> {
544
+ return getObjectByBcs(
545
+ compositionId,
546
+ { parse: (bytes) => mapComposition(compositionId, CompositionBcs.parse(bytes)) },
547
+ Composition,
548
+ "Composition",
549
+ );
544
550
  }
545
551
 
546
552
  /** Extracts the share type `T` from a `Composition<T>` object. */
547
- export async function getCompositionShareType(
548
- client: ClientWithCoreApi,
553
+ export const getCompositionShareType = Effect.fn("getCompositionShareType")(function* (
549
554
  compositionId: string,
550
- ): Promise<string> {
551
- const { object } = await client.core.getObject({ objectId: compositionId });
552
- return extractTypeParam(object.type);
553
- }
555
+ ): Effect.fn.Return<string, ObjectNotFoundError | SuiRpcError, SuiClient> {
556
+ const type = yield* getObjectType(compositionId);
557
+ return extractTypeParam(type);
558
+ });
554
559
 
555
560
  /** Fetches a composition by its share type (GraphQL discovery + Core read). */
556
- export async function getCompositionByShareType(
557
- client: ClientWithCoreApi,
558
- graphqlClient: SuiGraphQLClient,
561
+ export const getCompositionByShareType = Effect.fn("getCompositionByShareType")(function* (
559
562
  shareType: string,
560
563
  misoPackageId: string,
561
- ): Promise<Composition> {
562
- const address = await getCompositionAddressByShareType(
563
- graphqlClient,
564
- shareType,
565
- misoPackageId,
566
- );
567
- if (!address)
568
- throw new Error(`Composition not found for share type: ${shareType}`);
569
- return getCompositionById(client, address);
570
- }
564
+ ): Effect.fn.Return<Composition, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
565
+ const address = yield* getCompositionAddressByShareType(shareType, misoPackageId);
566
+ if (Option.isNone(address)) {
567
+ return yield* new ObjectNotFoundError({ objectId: `composition::Composition<${shareType}>` });
568
+ }
569
+ return yield* getCompositionById(address.value);
570
+ });
571
571
 
572
572
  /**
573
573
  * Resolves a composition share type to its object address.
@@ -576,22 +576,20 @@ export async function getCompositionByShareType(
576
576
  * composition's identity but will read extension fields rather than the core
577
577
  * Composition contents.
578
578
  */
579
- export async function getCompositionAddressByShareType(
580
- graphqlClient: SuiGraphQLClient,
579
+ export function getCompositionAddressByShareType(
581
580
  shareType: string,
582
581
  misoPackageId: string,
583
- ): Promise<string | null> {
582
+ ): Effect.Effect<Option.Option<string>, SuiRpcError, SuiGraphQL> {
584
583
  const type = `${misoPackageId}::composition::Composition<${shareType}>`;
585
- return firstAddressOfType(graphqlClient, type);
584
+ return firstAddressOfType(type);
586
585
  }
587
586
 
588
- export async function getCompositionAdminCapById(
589
- client: ClientWithCoreApi,
587
+ export const getCompositionAdminCapById = Effect.fn("getCompositionAdminCapById")(function* (
590
588
  adminCapId: string,
591
- ): Promise<CompositionAdminCap> {
592
- const { object } = await client.core.getObject({ objectId: adminCapId });
593
- return { id: adminCapId, shareType: extractTypeParam(object.type) };
594
- }
589
+ ): Effect.fn.Return<CompositionAdminCap, ObjectNotFoundError | SuiRpcError, SuiClient> {
590
+ const type = yield* getObjectType(adminCapId);
591
+ return new CompositionAdminCap({ id: adminCapId, shareType: extractTypeParam(type) });
592
+ });
595
593
 
596
594
  /**
597
595
  * Composition admin caps owned by `owner`.
@@ -600,25 +598,21 @@ export async function getCompositionAdminCapById(
600
598
  * object's instantiated `type`, so the share type is read straight off
601
599
  * `CompositionAdminCap<CompositionShare>` with no second round-trip.
602
600
  */
603
- export async function getOwnedCompositionAdminCaps(
604
- client: ClientWithCoreApi,
601
+ export const getOwnedCompositionAdminCaps = Effect.fn("getOwnedCompositionAdminCaps")(function* (
605
602
  owner: string,
606
603
  misoPackageId: string,
607
- ): Promise<CompositionAdminCap[]> {
604
+ ): Effect.fn.Return<CompositionAdminCap[], SuiRpcError, SuiClient> {
608
605
  const capType = `${misoPackageId}::composition::CompositionAdminCap`;
609
- const objects = await listAllOwnedObjects(client, { owner, type: capType });
606
+ const objects = yield* listAllOwnedObjects({ owner, type: capType });
610
607
  const caps: CompositionAdminCap[] = [];
611
608
  for (const obj of objects) {
612
609
  const match = obj.type?.match(/<(.+)>$/);
613
- if (match?.[1]) caps.push({ id: obj.objectId, shareType: match[1] });
610
+ if (match?.[1]) caps.push(new CompositionAdminCap({ id: obj.objectId, shareType: match[1] }));
614
611
  }
615
612
  return caps;
616
- }
613
+ });
617
614
 
618
- export function deriveCompositionAdminCapId(
619
- compositionId: string,
620
- misoPackageId: string,
621
- ): string {
615
+ export function deriveCompositionAdminCapId(compositionId: string, misoPackageId: string): string {
622
616
  return deriveObjectID(
623
617
  compositionId,
624
618
  `${misoPackageId}::composition::CompositionAdminCapKey`,
@@ -630,33 +624,34 @@ export function deriveCompositionAdminCapId(
630
624
  // Recording
631
625
  // ============================================================================
632
626
 
633
- export async function getRecordingsByIds(
634
- client: ClientWithCoreApi,
627
+ export const getRecordingsByIds = Effect.fn("getRecordingsByIds")(function* (
635
628
  recordingIds: string[],
636
- ): Promise<Record<string, Recording>> {
629
+ ): Effect.fn.Return<Record<string, Recording>, SuiRpcError | BcsDecodeError, SuiClient> {
637
630
  if (recordingIds.length === 0) return {};
638
- const { objects } = await client.core.getObjects({
639
- objectIds: recordingIds,
640
- include: { content: true },
641
- });
642
- const out: Record<string, Recording> = {};
643
- for (const obj of objects) {
644
- if (obj instanceof Error || !obj.content) continue;
645
- out[obj.objectId] = mapRecording(
646
- obj.objectId,
647
- RecordingBcs.parse(obj.content),
648
- );
649
- }
650
- return out;
651
- }
631
+ const contents = yield* getObjectsContent(recordingIds);
632
+ const decoded = yield* Effect.forEach(
633
+ [...contents.entries()],
634
+ ([objectId, { content }]) =>
635
+ decodeBcs(
636
+ { parse: (bytes) => mapRecording(objectId, RecordingBcs.parse(bytes)) },
637
+ Recording,
638
+ content,
639
+ { type: "Recording", objectId },
640
+ ).pipe(Effect.map((recording) => [objectId, recording] as const)),
641
+ { concurrency: "unbounded" },
642
+ );
643
+ return Object.fromEntries(decoded);
644
+ });
652
645
 
653
- export async function getRecordingById(
654
- client: ClientWithCoreApi,
646
+ export function getRecordingById(
655
647
  recordingId: string,
656
- ): Promise<Recording> {
657
- const content = await getContent(client, recordingId);
658
- if (!content) throw new Error(`Recording not found: ${recordingId}`);
659
- return mapRecording(recordingId, RecordingBcs.parse(content));
648
+ ): Effect.Effect<Recording, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient> {
649
+ return getObjectByBcs(
650
+ recordingId,
651
+ { parse: (bytes) => mapRecording(recordingId, RecordingBcs.parse(bytes)) },
652
+ Recording,
653
+ "Recording",
654
+ );
660
655
  }
661
656
 
662
657
  /**
@@ -665,16 +660,12 @@ export async function getRecordingById(
665
660
  * them and returns the first; use {@link getRecordingShareTypes} when the
666
661
  * parent composition's share type is needed too.
667
662
  */
668
- export async function getRecordingShareType(
669
- client: ClientWithCoreApi,
663
+ export const getRecordingShareType = Effect.fn("getRecordingShareType")(function* (
670
664
  recordingId: string,
671
- ): Promise<string> {
672
- const [recordingShareType] = await getRecordingShareTypes(
673
- client,
674
- recordingId,
675
- );
665
+ ): Effect.fn.Return<string, ObjectNotFoundError | SuiRpcError, SuiClient> {
666
+ const [recordingShareType] = yield* getRecordingShareTypes(recordingId);
676
667
  return recordingShareType;
677
- }
668
+ });
678
669
 
679
670
  /**
680
671
  * Both of a recording's share types, as `[RecordingShare, CompositionShare]`.
@@ -682,37 +673,30 @@ export async function getRecordingShareType(
682
673
  * and the recording credit/pool extensions are all generic over both, in this
683
674
  * order.
684
675
  */
685
- export async function getRecordingShareTypes(
686
- client: ClientWithCoreApi,
676
+ export const getRecordingShareTypes = Effect.fn("getRecordingShareTypes")(function* (
687
677
  recordingId: string,
688
- ): Promise<[string, string]> {
689
- const { object } = await client.core.getObject({ objectId: recordingId });
690
- return extractTypeParams2(object.type);
691
- }
678
+ ): Effect.fn.Return<[string, string], ObjectNotFoundError | SuiRpcError, SuiClient> {
679
+ const type = yield* getObjectType(recordingId);
680
+ return extractTypeParams2(type);
681
+ });
692
682
 
693
- export async function getRecordingByShareType(
694
- client: ClientWithCoreApi,
695
- graphqlClient: SuiGraphQLClient,
683
+ export const getRecordingByShareType = Effect.fn("getRecordingByShareType")(function* (
696
684
  shareType: string,
697
685
  misoPackageId: string,
698
- ): Promise<Recording> {
699
- const address = await addressOfRecordingWithShareType(
700
- graphqlClient,
701
- misoPackageId,
702
- shareType,
703
- );
704
- if (!address)
705
- throw new Error(`Recording not found for share type: ${shareType}`);
706
- return getRecordingById(client, address);
707
- }
686
+ ): Effect.fn.Return<Recording, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
687
+ const address = yield* addressOfRecordingWithShareType(misoPackageId, shareType);
688
+ if (Option.isNone(address)) {
689
+ return yield* new ObjectNotFoundError({ objectId: `recording::Recording<${shareType}, ...>` });
690
+ }
691
+ return yield* getRecordingById(address.value);
692
+ });
708
693
 
709
- export async function getRecordingAdminCapById(
710
- client: ClientWithCoreApi,
694
+ export const getRecordingAdminCapById = Effect.fn("getRecordingAdminCapById")(function* (
711
695
  adminCapId: string,
712
- ): Promise<RecordingAdminCap> {
713
- const { object } = await client.core.getObject({ objectId: adminCapId });
714
- return { id: adminCapId, shareType: extractTypeParam(object.type) };
715
- }
696
+ ): Effect.fn.Return<RecordingAdminCap, ObjectNotFoundError | SuiRpcError, SuiClient> {
697
+ const type = yield* getObjectType(adminCapId);
698
+ return new RecordingAdminCap({ id: adminCapId, shareType: extractTypeParam(type) });
699
+ });
716
700
 
717
701
  /**
718
702
  * Recording admin caps owned by `owner`.
@@ -722,108 +706,89 @@ export async function getRecordingAdminCapById(
722
706
  * so this yields only the recording's own share type — its parent composition's
723
707
  * share type is not recoverable from the cap alone.
724
708
  */
725
- export async function getOwnedRecordingAdminCaps(
726
- client: ClientWithCoreApi,
709
+ export const getOwnedRecordingAdminCaps = Effect.fn("getOwnedRecordingAdminCaps")(function* (
727
710
  owner: string,
728
711
  misoPackageId: string,
729
- ): Promise<RecordingAdminCap[]> {
712
+ ): Effect.fn.Return<RecordingAdminCap[], SuiRpcError, SuiClient> {
730
713
  const capType = `${misoPackageId}::recording::RecordingAdminCap`;
731
- const objects = await listAllOwnedObjects(client, { owner, type: capType });
714
+ const objects = yield* listAllOwnedObjects({ owner, type: capType });
732
715
  const caps: RecordingAdminCap[] = [];
733
716
  for (const obj of objects) {
734
717
  const match = obj.type?.match(/<(.+)>$/);
735
- if (match?.[1]) caps.push({ id: obj.objectId, shareType: match[1] });
718
+ if (match?.[1]) caps.push(new RecordingAdminCap({ id: obj.objectId, shareType: match[1] }));
736
719
  }
737
720
  return caps;
738
- }
721
+ });
739
722
 
740
- export function deriveRecordingAdminCapId(
741
- recordingId: string,
742
- misoPackageId: string,
743
- ): string {
744
- return deriveObjectID(
745
- recordingId,
746
- `${misoPackageId}::recording::RecordingAdminCapKey`,
747
- UNIT_STRUCT_KEY_BYTES,
748
- );
723
+ export function deriveRecordingAdminCapId(recordingId: string, misoPackageId: string): string {
724
+ return deriveObjectID(recordingId, `${misoPackageId}::recording::RecordingAdminCapKey`, UNIT_STRUCT_KEY_BYTES);
749
725
  }
750
726
 
751
727
  // ============================================================================
752
728
  // Release
753
729
  // ============================================================================
754
730
 
755
- export async function getReleasesByIds(
756
- client: ClientWithCoreApi,
731
+ export const getReleasesByIds = Effect.fn("getReleasesByIds")(function* (
757
732
  releaseIds: string[],
758
- ): Promise<Record<string, Release>> {
733
+ ): Effect.fn.Return<Record<string, Release>, SuiRpcError | BcsDecodeError, SuiClient> {
759
734
  if (releaseIds.length === 0) return {};
760
- const { objects } = await client.core.getObjects({
761
- objectIds: releaseIds,
762
- include: { content: true },
763
- });
764
- const out: Record<string, Release> = {};
765
- for (const obj of objects) {
766
- if (obj instanceof Error || !obj.content) continue;
767
- out[obj.objectId] = mapRelease(obj.objectId, ReleaseBcs.parse(obj.content));
768
- }
769
- return out;
770
- }
735
+ const contents = yield* getObjectsContent(releaseIds);
736
+ const decoded = yield* Effect.forEach(
737
+ [...contents.entries()],
738
+ ([objectId, { content }]) =>
739
+ decodeBcs(
740
+ { parse: (bytes) => mapRelease(objectId, ReleaseBcs.parse(bytes)) },
741
+ Release,
742
+ content,
743
+ { type: "Release", objectId },
744
+ ).pipe(Effect.map((release) => [objectId, release] as const)),
745
+ { concurrency: "unbounded" },
746
+ );
747
+ return Object.fromEntries(decoded);
748
+ });
771
749
 
772
- export async function getReleaseById(
773
- client: ClientWithCoreApi,
750
+ export function getReleaseById(
774
751
  releaseId: string,
775
- ): Promise<Release> {
776
- const { object } = await client.core.getObject({
777
- objectId: releaseId,
778
- include: { content: true },
779
- });
780
- if (!object.content) throw new Error(`Release not found: ${releaseId}`);
781
- return mapRelease(releaseId, ReleaseBcs.parse(object.content));
752
+ ): Effect.Effect<Release, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient> {
753
+ return getObjectByBcs(
754
+ releaseId,
755
+ { parse: (bytes) => mapRelease(releaseId, ReleaseBcs.parse(bytes)) },
756
+ Release,
757
+ "Release",
758
+ );
782
759
  }
783
760
 
784
- export async function getReleaseAdminCapById(
785
- client: ClientWithCoreApi,
761
+ export const getReleaseAdminCapById = Effect.fn("getReleaseAdminCapById")(function* (
786
762
  adminCapId: string,
787
- ): Promise<ReleaseAdminCap> {
788
- const { object } = await client.core.getObject({
789
- objectId: adminCapId,
790
- include: { json: true },
763
+ ): Effect.fn.Return<ReleaseAdminCap, ObjectNotFoundError | SuiRpcError, SuiClient> {
764
+ const client = yield* SuiClient;
765
+ const { object } = yield* Effect.tryPromise({
766
+ try: (signal) => client.core.getObject({ objectId: adminCapId, include: { json: true }, signal }),
767
+ catch: (cause) => classifyMissing(adminCapId, "getObject", cause),
791
768
  });
792
769
  const json = object.json as { release_id: string } | null;
793
- if (!json?.release_id)
794
- throw new Error(`ReleaseAdminCap not found: ${adminCapId}`);
795
- return { id: adminCapId, releaseId: json.release_id };
796
- }
770
+ if (!json?.release_id) {
771
+ return yield* new ObjectNotFoundError({ objectId: adminCapId });
772
+ }
773
+ return new ReleaseAdminCap({ id: adminCapId, releaseId: json.release_id });
774
+ });
797
775
 
798
- export async function getOwnedReleaseAdminCaps(
799
- client: ClientWithCoreApi,
776
+ export const getOwnedReleaseAdminCaps = Effect.fn("getOwnedReleaseAdminCaps")(function* (
800
777
  owner: string,
801
778
  misoPackageId: string,
802
- ): Promise<ReleaseAdminCap[]> {
779
+ ): Effect.fn.Return<ReleaseAdminCap[], SuiRpcError, SuiClient> {
803
780
  const capType = `${misoPackageId}::release::ReleaseAdminCap`;
804
- const objects = await listAllOwnedObjects(client, {
805
- owner,
806
- type: capType,
807
- include: { json: true },
808
- });
781
+ const objects = yield* listAllOwnedObjects({ owner, type: capType, include: { json: true } });
809
782
  const caps: ReleaseAdminCap[] = [];
810
783
  for (const obj of objects) {
811
784
  const json = obj.json as { release_id: string } | null;
812
- if (json?.release_id)
813
- caps.push({ id: obj.objectId, releaseId: json.release_id });
785
+ if (json?.release_id) caps.push(new ReleaseAdminCap({ id: obj.objectId, releaseId: json.release_id }));
814
786
  }
815
787
  return caps;
816
- }
788
+ });
817
789
 
818
- export function deriveReleaseAdminCapId(
819
- releaseId: string,
820
- misoPackageId: string,
821
- ): string {
822
- return deriveObjectID(
823
- releaseId,
824
- `${misoPackageId}::release::ReleaseAdminCapKey`,
825
- UNIT_STRUCT_KEY_BYTES,
826
- );
790
+ export function deriveReleaseAdminCapId(releaseId: string, misoPackageId: string): string {
791
+ return deriveObjectID(releaseId, `${misoPackageId}::release::ReleaseAdminCapKey`, UNIT_STRUCT_KEY_BYTES);
827
792
  }
828
793
 
829
794
  // ============================================================================
@@ -831,13 +796,12 @@ export function deriveReleaseAdminCapId(
831
796
  // ============================================================================
832
797
 
833
798
  /** Extracts the share type `T` from a `Currency<T>` object. */
834
- export async function getShareCurrencyType(
835
- client: ClientWithCoreApi,
799
+ export const getShareCurrencyType = Effect.fn("getShareCurrencyType")(function* (
836
800
  shareCurrencyId: string,
837
- ): Promise<string> {
838
- const { object } = await client.core.getObject({ objectId: shareCurrencyId });
839
- return extractTypeParam(object.type);
840
- }
801
+ ): Effect.fn.Return<string, ObjectNotFoundError | SuiRpcError, SuiClient> {
802
+ const type = yield* getObjectType(shareCurrencyId);
803
+ return extractTypeParam(type);
804
+ });
841
805
 
842
806
  /**
843
807
  * Finds the `TreasuryCap<ShareType>` owned by `owner`. One Core API call.
@@ -850,43 +814,40 @@ export async function getShareCurrencyType(
850
814
  * compose the two:
851
815
  *
852
816
  * ```ts
853
- * const shareType = await getShareCurrencyType(client, shareCurrencyId);
854
- * const capId = await getShareCurrencyTreasuryCap(client, shareType, owner);
817
+ * const shareType = yield* getShareCurrencyType(shareCurrencyId);
818
+ * const capId = yield* getShareCurrencyTreasuryCap(shareType, owner);
855
819
  * ```
856
820
  */
857
- export async function getShareCurrencyTreasuryCap(
858
- client: ClientWithCoreApi,
821
+ export const getShareCurrencyTreasuryCap = Effect.fn("getShareCurrencyTreasuryCap")(function* (
859
822
  shareType: string,
860
823
  owner: string,
861
- ): Promise<string> {
862
- const objects = await listAllOwnedObjects(client, {
863
- owner,
864
- type: `0x2::coin::TreasuryCap<${shareType}>`,
865
- });
824
+ ): Effect.fn.Return<string, SuiRpcError, SuiClient> {
825
+ const objects = yield* listAllOwnedObjects({ owner, type: `0x2::coin::TreasuryCap<${shareType}>` });
866
826
  if (objects.length === 0) {
867
827
  throw new Error(`No TreasuryCap found for ${shareType} owned by ${owner}`);
868
828
  }
869
829
  return objects[0]!.objectId;
870
- }
830
+ });
871
831
 
872
832
  // ============================================================================
873
833
  // Private
874
834
  // ============================================================================
875
835
 
876
- /** Returns the first object address of a fully-qualified type, or null. */
877
- async function firstAddressOfType(
878
- client: SuiGraphQLClient,
836
+ /** Returns the first object address of a fully-qualified type, or `Option.none()`. */
837
+ const firstAddressOfType = Effect.fn("firstAddressOfType")(function* (
879
838
  type: string,
880
- ): Promise<string | null> {
881
- const result = await client.query({
882
- query: AddressesByTypeQuery,
883
- variables: { type },
839
+ ): Effect.fn.Return<Option.Option<string>, SuiRpcError, SuiGraphQL> {
840
+ const client = yield* SuiGraphQL;
841
+ const result = yield* Effect.tryPromise({
842
+ try: () => client.query({ query: AddressesByTypeQuery, variables: { type } }),
843
+ catch: (cause) => new SuiRpcError({ operation: "addressesByType", cause }),
884
844
  });
885
- return result.data?.objects?.nodes?.[0]?.address ?? null;
886
- }
845
+ return Option.fromNullishOr(result.data?.objects?.nodes?.[0]?.address);
846
+ });
887
847
 
888
848
  /**
889
- * Address of the `Recording` whose FIRST type parameter is `shareType`, or null.
849
+ * Address of the `Recording` whose FIRST type parameter is `shareType`, or
850
+ * `Option.none()`.
890
851
  *
891
852
  * `Recording<RecordingShare, CompositionShare>` takes two parameters and a type
892
853
  * filter must supply all of them or none, so filtering by
@@ -896,33 +857,34 @@ async function firstAddressOfType(
896
857
  * first parameter client-side. A recording's share currency is unique to it, so
897
858
  * the match is unambiguous.
898
859
  */
899
- async function addressOfRecordingWithShareType(
900
- client: SuiGraphQLClient,
860
+ const addressOfRecordingWithShareType = Effect.fn("addressOfRecordingWithShareType")(function* (
901
861
  misoPackageId: string,
902
862
  shareType: string,
903
- ): Promise<string | null> {
863
+ ): Effect.fn.Return<Option.Option<string>, SuiRpcError, SuiGraphQL> {
864
+ const client = yield* SuiGraphQL;
904
865
  let cursor: string | null | undefined;
905
866
  do {
906
- const result = await client.query<
907
- { objects?: WorkAddressConnection | null },
908
- { type: string; cursor?: string | null }
909
- >({
910
- query: `query RecordingAddress($type: String!, $cursor: String) {
911
- objects(first: 50, after: $cursor, filter: { type: $type }) {
912
- pageInfo { hasNextPage endCursor }
913
- nodes { address asMoveObject { contents { type { repr } } } }
914
- }
915
- }`,
916
- variables: { type: `${misoPackageId}::recording::Recording`, cursor },
867
+ const result = yield* Effect.tryPromise({
868
+ try: () =>
869
+ client.query<{ objects?: WorkAddressConnection | null }, { type: string; cursor?: string | null }>({
870
+ query: `query RecordingAddress($type: String!, $cursor: String) {
871
+ objects(first: 50, after: $cursor, filter: { type: $type }) {
872
+ pageInfo { hasNextPage endCursor }
873
+ nodes { address asMoveObject { contents { type { repr } } } }
874
+ }
875
+ }`,
876
+ variables: { type: `${misoPackageId}::recording::Recording`, cursor },
877
+ }),
878
+ catch: (cause) => new SuiRpcError({ operation: "recordingAddress", cause }),
917
879
  });
918
880
  const page = result.data?.objects;
919
881
  for (const node of page?.nodes ?? []) {
920
882
  const repr = node?.asMoveObject?.contents?.type?.repr;
921
883
  if (!repr || !node.address) continue;
922
884
  const [recordingShareType] = extractTypeParams2(repr);
923
- if (recordingShareType === shareType) return node.address;
885
+ if (recordingShareType === shareType) return Option.some(node.address);
924
886
  }
925
887
  cursor = page?.pageInfo?.hasNextPage ? page.pageInfo.endCursor : null;
926
888
  } while (cursor);
927
- return null;
928
- }
889
+ return Option.none();
890
+ });