@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.
- package/README.md +144 -45
- package/dist/auth.d.ts +8 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +78 -59
- package/dist/auth.js.map +1 -1
- package/dist/catalog.d.ts +5 -5
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +33 -21
- package/dist/catalog.js.map +1 -1
- package/dist/client.d.ts +17 -30
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +53 -75
- package/dist/client.js.map +1 -1
- package/dist/cover.d.ts +4 -3
- package/dist/cover.d.ts.map +1 -1
- package/dist/cover.js +28 -17
- package/dist/cover.js.map +1 -1
- package/dist/credits.d.ts +12 -10
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +60 -33
- package/dist/credits.js.map +1 -1
- package/dist/deployments.d.ts +21 -13
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +84 -37
- package/dist/deployments.js.map +1 -1
- package/dist/errors.d.ts +91 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts +3 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +9 -4
- package/dist/execute.js.map +1 -1
- package/dist/genre.d.ts +25 -41
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +71 -20
- package/dist/genre.js.map +1 -1
- package/dist/party/client.d.ts +16 -13
- package/dist/party/client.d.ts.map +1 -1
- package/dist/party/client.js +33 -22
- package/dist/party/client.js.map +1 -1
- package/dist/party/extensions/links.d.ts +2 -1
- package/dist/party/extensions/links.d.ts.map +1 -1
- package/dist/party/extensions/links.js +2 -1
- package/dist/party/extensions/links.js.map +1 -1
- package/dist/party/queries.d.ts +12 -11
- package/dist/party/queries.d.ts.map +1 -1
- package/dist/party/queries.js +63 -57
- package/dist/party/queries.js.map +1 -1
- package/dist/party/types.d.ts +31 -20
- package/dist/party/types.d.ts.map +1 -1
- package/dist/party/types.js +82 -1
- package/dist/party/types.js.map +1 -1
- package/dist/pressing.d.ts +55 -35
- package/dist/pressing.d.ts.map +1 -1
- package/dist/pressing.js +99 -76
- package/dist/pressing.js.map +1 -1
- package/dist/queries.d.ts +1 -1
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +5 -4
- package/dist/queries.js.map +1 -1
- package/dist/read/artist.d.ts +6 -4
- package/dist/read/artist.d.ts.map +1 -1
- package/dist/read/artist.js +29 -27
- package/dist/read/artist.js.map +1 -1
- package/dist/read/catalog.d.ts +20 -17
- package/dist/read/catalog.d.ts.map +1 -1
- package/dist/read/catalog.js +120 -141
- package/dist/read/catalog.js.map +1 -1
- package/dist/read/client.d.ts +10 -9
- package/dist/read/client.d.ts.map +1 -1
- package/dist/read/client.js.map +1 -1
- package/dist/read/genres.d.ts +5 -3
- package/dist/read/genres.d.ts.map +1 -1
- package/dist/read/genres.js +18 -15
- package/dist/read/genres.js.map +1 -1
- package/dist/read/index.d.ts.map +1 -1
- package/dist/read/index.js +7 -1
- package/dist/read/index.js.map +1 -1
- package/dist/read/receipts.d.ts +15 -6
- package/dist/read/receipts.d.ts.map +1 -1
- package/dist/read/receipts.js +73 -60
- package/dist/read/receipts.js.map +1 -1
- package/dist/read/wallet.d.ts +11 -9
- package/dist/read/wallet.d.ts.map +1 -1
- package/dist/read/wallet.js +117 -132
- package/dist/read/wallet.js.map +1 -1
- package/dist/read/works.d.ts +9 -16
- package/dist/read/works.d.ts.map +1 -1
- package/dist/read/works.js +48 -117
- package/dist/read/works.js.map +1 -1
- package/dist/recording-extensions.d.ts +8 -7
- package/dist/recording-extensions.d.ts.map +1 -1
- package/dist/recording-extensions.js +30 -63
- package/dist/recording-extensions.js.map +1 -1
- package/dist/release-extensions.d.ts +3 -2
- package/dist/release-extensions.d.ts.map +1 -1
- package/dist/release-extensions.js +9 -10
- package/dist/release-extensions.js.map +1 -1
- package/dist/royalty.d.ts +51 -51
- package/dist/royalty.d.ts.map +1 -1
- package/dist/royalty.js +93 -11
- package/dist/royalty.js.map +1 -1
- package/dist/share.d.ts +7 -6
- package/dist/share.d.ts.map +1 -1
- package/dist/share.js +37 -30
- package/dist/share.js.map +1 -1
- package/dist/vault.d.ts +20 -8
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +31 -15
- package/dist/vault.js.map +1 -1
- package/package.json +11 -4
- package/src/auth.ts +89 -70
- package/src/catalog.ts +33 -49
- package/src/client.ts +117 -97
- package/src/cover.ts +17 -26
- package/src/credits.ts +43 -75
- package/src/deployments.ts +103 -69
- package/src/errors.ts +156 -0
- package/src/execute.ts +10 -6
- package/src/genre.ts +38 -23
- package/src/party/client.ts +35 -23
- package/src/party/extensions/links.ts +3 -2
- package/src/party/queries.ts +77 -65
- package/src/party/types.ts +52 -48
- package/src/pressing.ts +158 -161
- package/src/queries.ts +5 -5
- package/src/read/artist.ts +47 -42
- package/src/read/catalog.ts +200 -223
- package/src/read/client.ts +12 -11
- package/src/read/genres.ts +24 -16
- package/src/read/index.ts +7 -1
- package/src/read/receipts.ts +128 -67
- package/src/read/wallet.ts +194 -162
- package/src/read/works.ts +75 -194
- package/src/recording-extensions.ts +38 -92
- package/src/release-extensions.ts +7 -12
- package/src/royalty.ts +117 -20
- package/src/share.ts +44 -29
- package/src/vault.ts +47 -24
package/src/read/wallet.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
224
|
-
|
|
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 } =
|
|
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 =
|
|
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
|
|
261
|
-
client: MisoClient,
|
|
295
|
+
export const getPendingMemberships = Effect.fn("getPendingMemberships")(function* (
|
|
262
296
|
owner: string,
|
|
263
|
-
|
|
264
|
-
|
|
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 =
|
|
269
|
-
individuals
|
|
270
|
-
|
|
271
|
-
groupIds
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
325
|
-
const misoPackageId =
|
|
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
|
|
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
|
-
|
|
369
|
-
client: MisoClient,
|
|
405
|
+
const resolveVaultedReleaseCaps = Effect.fn("resolveVaultedReleaseCaps")(function* (
|
|
370
406
|
caps: readonly VaultedReleaseCap[],
|
|
371
|
-
|
|
407
|
+
config: MisoConfig,
|
|
408
|
+
): Effect.fn.Return<{ id: string; releaseId: string }[], SuiRpcError, SuiClient> {
|
|
372
409
|
if (caps.length === 0) return [];
|
|
373
|
-
const releaseCapType = `${
|
|
374
|
-
const vaultType = normalizeStructTag(
|
|
375
|
-
|
|
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
|
|
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(
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
472
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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 =
|
|
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 =
|
|
508
|
-
const titles =
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
const r =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
566
|
-
|
|
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] =
|
|
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 =
|
|
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<
|
|
604
|
+
const decimalsByClient = new WeakMap<object, Map<string, Promise<number>>>();
|
|
599
605
|
|
|
600
|
-
/**
|
|
601
|
-
*
|
|
602
|
-
|
|
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.
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
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
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
+
});
|