@misofm/sdk 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.
- package/README.md +37 -5
- package/dist/catalog.d.ts +30 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +60 -1
- package/dist/catalog.js.map +1 -1
- package/dist/contracts/miso_drop/drop.d.ts +550 -0
- package/dist/contracts/miso_drop/drop.d.ts.map +1 -0
- package/dist/contracts/miso_drop/drop.js +523 -0
- package/dist/contracts/miso_drop/drop.js.map +1 -0
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +4 -2
- package/dist/credits.js.map +1 -1
- package/dist/drop.d.ts +120 -0
- package/dist/drop.d.ts.map +1 -0
- package/dist/drop.js +152 -0
- package/dist/drop.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/read/artist.d.ts +24 -0
- package/dist/read/artist.d.ts.map +1 -0
- package/dist/read/artist.js +114 -0
- package/dist/read/artist.js.map +1 -0
- package/dist/read/catalog.d.ts +63 -0
- package/dist/read/catalog.d.ts.map +1 -0
- package/dist/read/catalog.js +252 -0
- package/dist/read/catalog.js.map +1 -0
- package/dist/read/client.d.ts +36 -0
- package/dist/read/client.d.ts.map +1 -0
- package/dist/read/client.js +46 -0
- package/dist/read/client.js.map +1 -0
- package/dist/read/config.d.ts +83 -0
- package/dist/read/config.d.ts.map +1 -0
- package/dist/read/config.js +76 -0
- package/dist/read/config.js.map +1 -0
- package/dist/read/genres.d.ts +7 -0
- package/dist/read/genres.d.ts.map +1 -0
- package/dist/read/genres.js +38 -0
- package/dist/read/genres.js.map +1 -0
- package/dist/read/index.d.ts +12 -0
- package/dist/read/index.d.ts.map +1 -0
- package/dist/read/index.js +23 -0
- package/dist/read/index.js.map +1 -0
- package/dist/read/internal/scalars.d.ts +23 -0
- package/dist/read/internal/scalars.d.ts.map +1 -0
- package/dist/read/internal/scalars.js +56 -0
- package/dist/read/internal/scalars.js.map +1 -0
- package/dist/read/internal/walrus.d.ts +4 -0
- package/dist/read/internal/walrus.d.ts.map +1 -0
- package/dist/read/internal/walrus.js +23 -0
- package/dist/read/internal/walrus.js.map +1 -0
- package/dist/read/receipts.d.ts +22 -0
- package/dist/read/receipts.d.ts.map +1 -0
- package/dist/read/receipts.js +233 -0
- package/dist/read/receipts.js.map +1 -0
- package/dist/read/types.d.ts +279 -0
- package/dist/read/types.d.ts.map +1 -0
- package/dist/read/types.js +21 -0
- package/dist/read/types.js.map +1 -0
- package/dist/read/wallet.d.ts +58 -0
- package/dist/read/wallet.d.ts.map +1 -0
- package/dist/read/wallet.js +325 -0
- package/dist/read/wallet.js.map +1 -0
- package/dist/read/works.d.ts +31 -0
- package/dist/read/works.d.ts.map +1 -0
- package/dist/read/works.js +106 -0
- package/dist/read/works.js.map +1 -0
- package/package.json +16 -3
- package/src/catalog.ts +118 -0
- package/src/contracts/miso_drop/drop.ts +798 -0
- package/src/contracts.ts +4 -0
- package/src/credits.ts +26 -12
- package/src/drop.ts +266 -0
- package/src/index.ts +5 -0
- package/src/read/artist.ts +136 -0
- package/src/read/catalog.ts +293 -0
- package/src/read/client.ts +79 -0
- package/src/read/config.ts +164 -0
- package/src/read/genres.ts +37 -0
- package/src/read/index.ts +53 -0
- package/src/read/internal/scalars.ts +57 -0
- package/src/read/internal/walrus.ts +32 -0
- package/src/read/receipts.ts +266 -0
- package/src/read/types.ts +361 -0
- package/src/read/wallet.ts +360 -0
- package/src/read/works.ts +164 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Address-scoped reads: what a wallet owns, administers, and can spend.
|
|
5
|
+
//
|
|
6
|
+
// None of this is cacheable at a shared edge — every answer is different per
|
|
7
|
+
// address — but it belongs here anyway, because the VALUE of a single read layer
|
|
8
|
+
// is the response shape, not just the cache. The studio catalog's three-transport
|
|
9
|
+
// dance (owned caps over gRPC → share types over GraphQL → work contents over
|
|
10
|
+
// gRPC) is exactly the sort of thing that should exist once in the SDK rather
|
|
11
|
+
// than be reimplemented by every browser, Worker, server, or native client.
|
|
12
|
+
|
|
13
|
+
import { normalizeSuiAddress } from "@mysten/sui/utils";
|
|
14
|
+
import { contracts as partyContracts } from "@misonetwork/miso-party";
|
|
15
|
+
import {
|
|
16
|
+
getCompositionByShareType,
|
|
17
|
+
getOwnedReleaseAdminCaps,
|
|
18
|
+
getRecordingByShareType,
|
|
19
|
+
getReleaseById,
|
|
20
|
+
isNotFound,
|
|
21
|
+
} from "@misonetwork/sdk";
|
|
22
|
+
import type { MisoClient } from "./client.ts";
|
|
23
|
+
import { int, u64 } from "./internal/scalars.ts";
|
|
24
|
+
import type { Balance, OwnedParty, OwnedRecord, OwnedWork, Ownership, WorkDetail } from "./types.ts";
|
|
25
|
+
import { getRecordingTitles, getWorkAddressesByShareTypes, getWorksByIds } from "./works.ts";
|
|
26
|
+
|
|
27
|
+
/** The on-chain type suffix every record object shares, across both live packages. */
|
|
28
|
+
export const RECORD_TYPE_SUFFIX = "::record::Record";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Page cap on the owned-objects scan, so a wallet holding a huge unrelated object
|
|
32
|
+
* set can't spin forever. 50/page × 20 = 1000 objects — far beyond any realistic
|
|
33
|
+
* library.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_PAGES = 20;
|
|
36
|
+
|
|
37
|
+
// ── Records ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function coerceNumber(v: unknown): number | null {
|
|
40
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
41
|
+
if (typeof v === "bigint") return Number(v);
|
|
42
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This copy's number in its run, across both record struct layouts: card
|
|
48
|
+
* checkout's `{ release_id, number }` and the SDK view's
|
|
49
|
+
* `{ release_id, edition, variant: { Prototype|Production: { number } } }`.
|
|
50
|
+
* The gRPC JSON enum shape varies by transport, so this probes defensively.
|
|
51
|
+
*
|
|
52
|
+
* Precedence is MOST SPECIFIC FIRST — a top-level `number`, then the variant's
|
|
53
|
+
* `number`, and only then `edition`. miso-app's original
|
|
54
|
+
* (`lib/records.ts:readRecordNumber`) checked `edition` before descending into
|
|
55
|
+
* the variant, so an SDK-view record reported which EDITION it came from (2)
|
|
56
|
+
* where the field means which COPY it is (12). `edition` survives as the last
|
|
57
|
+
* resort for a layout that carries nothing better.
|
|
58
|
+
*/
|
|
59
|
+
function readRecordNumber(json: Record<string, unknown>): number | null {
|
|
60
|
+
const direct = coerceNumber(json.number);
|
|
61
|
+
if (direct != null) return direct;
|
|
62
|
+
|
|
63
|
+
const variant = json.variant;
|
|
64
|
+
if (variant && typeof variant === "object") {
|
|
65
|
+
const v = variant as Record<string, unknown>;
|
|
66
|
+
const top = coerceNumber(v.number);
|
|
67
|
+
if (top != null) return top;
|
|
68
|
+
for (const inner of Object.values(v)) {
|
|
69
|
+
if (inner && typeof inner === "object") {
|
|
70
|
+
const n = coerceNumber((inner as Record<string, unknown>).number);
|
|
71
|
+
if (n != null) return n;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return coerceNumber(json.edition);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readReleaseId(json: Record<string, unknown>): string | null {
|
|
79
|
+
const r = json.release_id ?? json.releaseId;
|
|
80
|
+
return typeof r === "string" && r ? r : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The records `owner` holds. Ownership is DIRECT — a record is an address-owned
|
|
85
|
+
* `<pkg>::record::Record` with no pressing/license/receipt intermediary.
|
|
86
|
+
*
|
|
87
|
+
* There is deliberately no `type` filter on the listing: two record packages are
|
|
88
|
+
* live, so we match the `::record::Record` SUFFIX client-side and catch both with
|
|
89
|
+
* no package-id config. Objects that fail to parse are skipped, never thrown.
|
|
90
|
+
*/
|
|
91
|
+
export async function getOwnedRecords(client: MisoClient, owner: string): Promise<OwnedRecord[]> {
|
|
92
|
+
const out: OwnedRecord[] = [];
|
|
93
|
+
let cursor: string | null = null;
|
|
94
|
+
|
|
95
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
96
|
+
// Annotated: `cursor` is both an input and assigned from the result, which
|
|
97
|
+
// TypeScript reads as a circular initializer without an explicit type.
|
|
98
|
+
const res: Awaited<ReturnType<typeof client.protocol.core.listOwnedObjects>> =
|
|
99
|
+
await client.protocol.core.listOwnedObjects({
|
|
100
|
+
owner,
|
|
101
|
+
cursor,
|
|
102
|
+
limit: 50,
|
|
103
|
+
include: { json: true },
|
|
104
|
+
});
|
|
105
|
+
for (const obj of res.objects) {
|
|
106
|
+
if (!obj.type || !obj.type.endsWith(RECORD_TYPE_SUFFIX)) continue;
|
|
107
|
+
const json = (obj.json ?? {}) as Record<string, unknown>;
|
|
108
|
+
out.push({
|
|
109
|
+
id: obj.objectId,
|
|
110
|
+
type: obj.type,
|
|
111
|
+
releaseId: readReleaseId(json),
|
|
112
|
+
number: readRecordNumber(json),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (!res.hasNextPage) break;
|
|
116
|
+
cursor = res.cursor;
|
|
117
|
+
if (!cursor) break;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Parties ──────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The parties `owner` administers. A wallet "owns" a party iff it holds the
|
|
126
|
+
* party's `PartyAdminCap`, so this is one owned-objects listing filtered to the
|
|
127
|
+
* cap type plus a name resolve. Caps are transferable, which is why this is
|
|
128
|
+
* authoritative in a way that remembering created parties client-side is not.
|
|
129
|
+
*/
|
|
130
|
+
export async function getOwnedParties(client: MisoClient, owner: string): Promise<OwnedParty[]> {
|
|
131
|
+
const capType = `${client.config.party.partyPackageId}::party::PartyAdminCap`;
|
|
132
|
+
|
|
133
|
+
// One page of 50 caps is plenty for launch-scale artists; paginate if labels
|
|
134
|
+
// ever start hitting the cap.
|
|
135
|
+
const { objects } = await client.protocol.core.listOwnedObjects({
|
|
136
|
+
owner,
|
|
137
|
+
type: capType,
|
|
138
|
+
limit: 50,
|
|
139
|
+
include: { content: true },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const caps = objects.flatMap((obj) => {
|
|
143
|
+
try {
|
|
144
|
+
const cap = partyContracts.party.PartyAdminCap.parse(obj.content);
|
|
145
|
+
return [{ capId: obj.objectId, partyId: cap.party_id }];
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (caps.length === 0) return [];
|
|
151
|
+
|
|
152
|
+
const parties = await client.sui.party.getPartiesByIds(caps.map((c) => c.partyId));
|
|
153
|
+
return caps.flatMap(({ capId, partyId }) => {
|
|
154
|
+
const p = parties[partyId];
|
|
155
|
+
return p ? [{ partyId, capId, name: p.name, kind: p.kind }] : [];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Works (studio catalog) ───────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Owned generic admin caps over gRPC. The bare type filter matches every
|
|
163
|
+
* instantiation of `CompositionAdminCap<T>` / `RecordingAdminCap<T>`; the share
|
|
164
|
+
* type `T` is parsed back out of each instance's type tag, because the cap does
|
|
165
|
+
* not store the work's id — only its share type.
|
|
166
|
+
*/
|
|
167
|
+
async function ownedGenericCaps(client: MisoClient, owner: string, capType: string) {
|
|
168
|
+
const { objects } = await client.protocol.core.listOwnedObjects({ owner, type: capType, limit: 50 });
|
|
169
|
+
const caps: { id: string; shareType: string }[] = [];
|
|
170
|
+
for (const obj of objects) {
|
|
171
|
+
const match = /<(.+)>$/.exec(obj.type);
|
|
172
|
+
if (match?.[1]) caps.push({ id: obj.objectId, shareType: match[1] });
|
|
173
|
+
}
|
|
174
|
+
return caps;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Every work the wallet administers, keyed by its ADMIN CAP id (the studio
|
|
179
|
+
* catalog's routing unit — caps are what control means here).
|
|
180
|
+
*
|
|
181
|
+
* Three transports, one answer: cap discovery is gRPC; one aliased GraphQL
|
|
182
|
+
* request maps every share type to its work address (gRPC cannot ask "which
|
|
183
|
+
* object has type X"); one gRPC batch loads all the work contents. Releases skip
|
|
184
|
+
* the GraphQL hop entirely — `ReleaseAdminCap` is not generic and carries
|
|
185
|
+
* `release_id` directly.
|
|
186
|
+
*/
|
|
187
|
+
export async function getOwnedWorks(client: MisoClient, owner: string): Promise<OwnedWork[]> {
|
|
188
|
+
const miso = client.config.protocol.miso;
|
|
189
|
+
|
|
190
|
+
const [compCaps, recCaps, relCaps] = await Promise.all([
|
|
191
|
+
ownedGenericCaps(client, owner, `${miso}::composition::CompositionAdminCap`),
|
|
192
|
+
ownedGenericCaps(client, owner, `${miso}::recording::RecordingAdminCap`),
|
|
193
|
+
getOwnedReleaseAdminCaps(client.protocol, owner, miso),
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const addresses = await getWorkAddressesByShareTypes(
|
|
197
|
+
client.graphql,
|
|
198
|
+
{ compositions: compCaps.map((c) => c.shareType), recordings: recCaps.map((c) => c.shareType) },
|
|
199
|
+
miso,
|
|
200
|
+
);
|
|
201
|
+
const works = await getWorksByIds(client.protocol, {
|
|
202
|
+
compositions: Object.values(addresses.compositions).filter((id): id is string => id !== undefined),
|
|
203
|
+
recordings: Object.values(addresses.recordings).filter((id): id is string => id !== undefined),
|
|
204
|
+
releases: relCaps.map((c) => c.releaseId),
|
|
205
|
+
});
|
|
206
|
+
const recordingTitles = await getRecordingTitles(
|
|
207
|
+
client.protocol,
|
|
208
|
+
client.graphql,
|
|
209
|
+
Object.values(addresses.recordings).filter((id): id is string => id !== undefined),
|
|
210
|
+
miso,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const comps = compCaps.flatMap((cap): OwnedWork[] => {
|
|
214
|
+
const workId = addresses.compositions[cap.shareType];
|
|
215
|
+
const composition = workId ? works.compositions[workId] : undefined;
|
|
216
|
+
if (!workId || !composition) return [];
|
|
217
|
+
return [{ capId: cap.id, kind: "composition", workId, title: composition.title, state: composition.state.type }];
|
|
218
|
+
});
|
|
219
|
+
const recs = recCaps.flatMap((cap): OwnedWork[] => {
|
|
220
|
+
const workId = addresses.recordings[cap.shareType];
|
|
221
|
+
const recording = workId ? works.recordings[workId] : undefined;
|
|
222
|
+
if (!workId || !recording) return [];
|
|
223
|
+
return [{
|
|
224
|
+
capId: cap.id,
|
|
225
|
+
kind: "recording",
|
|
226
|
+
workId,
|
|
227
|
+
title: recordingTitles[workId] ?? "Untitled",
|
|
228
|
+
state: recording.state.type,
|
|
229
|
+
}];
|
|
230
|
+
});
|
|
231
|
+
const rels = relCaps.flatMap((cap): OwnedWork[] => {
|
|
232
|
+
const release = works.releases[cap.releaseId];
|
|
233
|
+
if (!release) return [];
|
|
234
|
+
return [{ capId: cap.id, kind: "release", workId: release.id, title: release.title, state: release.state.type }];
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return [...comps, ...recs, ...rels];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Classify a cap by its on-chain type, then resolve the work behind it. */
|
|
241
|
+
export async function getWorkByCap(client: MisoClient, capId: string): Promise<WorkDetail | null> {
|
|
242
|
+
const miso = client.config.protocol.miso;
|
|
243
|
+
|
|
244
|
+
let type: string;
|
|
245
|
+
let json: Record<string, unknown> | null;
|
|
246
|
+
try {
|
|
247
|
+
const { object } = await client.protocol.core.getObject({ objectId: capId, include: { json: true } });
|
|
248
|
+
type = object.type;
|
|
249
|
+
json = (object.json ?? null) as Record<string, unknown> | null;
|
|
250
|
+
} catch (e) {
|
|
251
|
+
if (isNotFound(e)) return null;
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (type.startsWith(`${miso}::composition::CompositionAdminCap<`)) {
|
|
256
|
+
const shareType = type.slice(type.indexOf("<") + 1, -1);
|
|
257
|
+
const c = await getCompositionByShareType(client.protocol, client.graphql, shareType, miso);
|
|
258
|
+
return {
|
|
259
|
+
capId,
|
|
260
|
+
kind: "composition",
|
|
261
|
+
workId: c.id,
|
|
262
|
+
title: c.title,
|
|
263
|
+
state: c.state.type,
|
|
264
|
+
royaltyRateBps: int(c.royaltyRate.value),
|
|
265
|
+
shareType,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (type.startsWith(`${miso}::recording::RecordingAdminCap<`)) {
|
|
270
|
+
const shareType = type.slice(type.indexOf("<") + 1, -1);
|
|
271
|
+
const r = await getRecordingByShareType(client.protocol, client.graphql, shareType, miso);
|
|
272
|
+
const titles = await getRecordingTitles(client.protocol, client.graphql, [r.id], miso);
|
|
273
|
+
return {
|
|
274
|
+
capId,
|
|
275
|
+
kind: "recording",
|
|
276
|
+
workId: r.id,
|
|
277
|
+
title: titles[r.id] ?? "Untitled",
|
|
278
|
+
state: r.state.type,
|
|
279
|
+
shareType,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (type.startsWith(`${miso}::release::ReleaseAdminCap`)) {
|
|
284
|
+
const releaseId = typeof json?.release_id === "string" ? json.release_id : null;
|
|
285
|
+
if (!releaseId) throw new Error(`Release cap ${capId} carries no release id`);
|
|
286
|
+
const r = await getReleaseById(client.protocol, releaseId);
|
|
287
|
+
return {
|
|
288
|
+
capId,
|
|
289
|
+
kind: "release",
|
|
290
|
+
workId: r.id,
|
|
291
|
+
title: r.title,
|
|
292
|
+
state: r.state.type,
|
|
293
|
+
discCount: r.tracks.length > 0 ? 1 : 0,
|
|
294
|
+
trackCount: r.tracks.length,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// A real object, but not a work admin cap — "no such work", not an error.
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Balance ──────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A wallet's spendable balance in one currency. `core.getBalance` totals coin
|
|
306
|
+
* OBJECTS and the address balance, so funds that arrived either way are counted —
|
|
307
|
+
* which matters because `balance::send_funds` (how the app transfers dollars)
|
|
308
|
+
* credits the address balance, not a coin.
|
|
309
|
+
*/
|
|
310
|
+
export async function getBalance(client: MisoClient, address: string, coinType?: string): Promise<Balance> {
|
|
311
|
+
const type = coinType ?? client.config.money.usdCoinType;
|
|
312
|
+
const res = await client.protocol.core.getBalance({ owner: address, coinType: type });
|
|
313
|
+
return {
|
|
314
|
+
address: normalizeSuiAddress(address),
|
|
315
|
+
coinType: type,
|
|
316
|
+
balance: u64(res.balance.balance),
|
|
317
|
+
decimals: type === client.config.money.usdCoinType ? client.config.money.usdDecimals : 9,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── Ownership ────────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
function isAddressOwner(owner: unknown, address: string): boolean {
|
|
324
|
+
const o = owner as { $kind?: string; AddressOwner?: string } | undefined;
|
|
325
|
+
return (
|
|
326
|
+
o?.$kind === "AddressOwner" && !!o.AddressOwner && normalizeSuiAddress(o.AddressOwner) === normalizeSuiAddress(address)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Whether `address` controls a party — i.e. holds its `PartyAdminCap`. The cap is
|
|
332
|
+
* a DERIVED object of the party, so this is one `getObject` on a computed id, no
|
|
333
|
+
* search. The chain enforces writes regardless; this drives the UI and hands back
|
|
334
|
+
* the cap id that owner-gated writes need.
|
|
335
|
+
*
|
|
336
|
+
* A failed read means "can't confirm", which resolves to `false` — the editor
|
|
337
|
+
* stays hidden rather than being offered and then rejected on submit.
|
|
338
|
+
*/
|
|
339
|
+
export async function ownsParty(client: MisoClient, address: string, partyId: string): Promise<Ownership> {
|
|
340
|
+
const capId = client.sui.party.derivePartyAdminCapId(partyId);
|
|
341
|
+
const isOwner = await client.protocol.core
|
|
342
|
+
.getObject({ objectId: capId })
|
|
343
|
+
.then(({ object }) => isAddressOwner(object?.owner, address))
|
|
344
|
+
.catch(() => false);
|
|
345
|
+
return { address: normalizeSuiAddress(address), objectId: partyId, isOwner, capId };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Whether `address` owns a record. Records are address-owned objects, so this is
|
|
350
|
+
* one `getObject` and an owner compare. Presentation gating only — it decides
|
|
351
|
+
* whether the Snapshots tab appears, and a non-owner has no snapshot content to
|
|
352
|
+
* fetch regardless.
|
|
353
|
+
*/
|
|
354
|
+
export async function ownsRecord(client: MisoClient, address: string, recordId: string): Promise<Ownership> {
|
|
355
|
+
const isOwner = await client.protocol.core
|
|
356
|
+
.getObject({ objectId: recordId })
|
|
357
|
+
.then(({ object }) => isAddressOwner(object?.owner, address))
|
|
358
|
+
.catch(() => false);
|
|
359
|
+
return { address: normalizeSuiAddress(address), objectId: recordId, isOwner };
|
|
360
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client";
|
|
5
|
+
import type { SuiGraphQLClient } from "@mysten/sui/graphql";
|
|
6
|
+
import {
|
|
7
|
+
extractTypeParams2,
|
|
8
|
+
getCompositionsByIds,
|
|
9
|
+
getRecordingsByIds,
|
|
10
|
+
getReleasesByIds,
|
|
11
|
+
type Composition,
|
|
12
|
+
type Recording,
|
|
13
|
+
type Release,
|
|
14
|
+
} from "@misonetwork/sdk";
|
|
15
|
+
|
|
16
|
+
export interface WorkShareTypes {
|
|
17
|
+
compositions: readonly string[];
|
|
18
|
+
recordings: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkAddressesByShareType {
|
|
22
|
+
compositions: Partial<Record<string, string>>;
|
|
23
|
+
recordings: Partial<Record<string, string>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ObjectsByTypeResult {
|
|
27
|
+
objects: {
|
|
28
|
+
nodes: Array<{
|
|
29
|
+
address: string;
|
|
30
|
+
asMoveObject: { contents: { type: { repr: string } } | null } | null;
|
|
31
|
+
}>;
|
|
32
|
+
} | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve work share types without relying on unpublished protocol helpers. */
|
|
36
|
+
export async function getWorkAddressesByShareTypes(
|
|
37
|
+
client: SuiGraphQLClient,
|
|
38
|
+
shareTypes: WorkShareTypes,
|
|
39
|
+
misoPackageId: string,
|
|
40
|
+
): Promise<WorkAddressesByShareType> {
|
|
41
|
+
const compositions = [...new Set(shareTypes.compositions)];
|
|
42
|
+
const recordingSet = new Set(shareTypes.recordings);
|
|
43
|
+
const out: WorkAddressesByShareType = { compositions: {}, recordings: {} };
|
|
44
|
+
|
|
45
|
+
const compositionEntries = await Promise.all(
|
|
46
|
+
compositions.map(async (shareType) => {
|
|
47
|
+
const type = `${misoPackageId}::composition::Composition<${shareType}>`;
|
|
48
|
+
const result = await client.query<ObjectsByTypeResult, { type: string }>({
|
|
49
|
+
query: `query WorkByType($type: String!) {
|
|
50
|
+
objects(filter: { type: $type }) {
|
|
51
|
+
nodes { address asMoveObject { contents { type { repr } } } }
|
|
52
|
+
}
|
|
53
|
+
}`,
|
|
54
|
+
variables: { type },
|
|
55
|
+
});
|
|
56
|
+
if (result.errors?.length) throw new Error(result.errors[0]!.message);
|
|
57
|
+
return [shareType, result.data?.objects?.nodes[0]?.address] as const;
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
for (const [shareType, address] of compositionEntries) {
|
|
61
|
+
if (address) out.compositions[shareType] = address;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (recordingSet.size > 0) {
|
|
65
|
+
const type = `${misoPackageId}::recording::Recording`;
|
|
66
|
+
const result = await client.query<ObjectsByTypeResult, { type: string }>({
|
|
67
|
+
query: `query RecordingsByType($type: String!) {
|
|
68
|
+
objects(filter: { type: $type }) {
|
|
69
|
+
nodes { address asMoveObject { contents { type { repr } } } }
|
|
70
|
+
}
|
|
71
|
+
}`,
|
|
72
|
+
variables: { type },
|
|
73
|
+
});
|
|
74
|
+
if (result.errors?.length) throw new Error(result.errors[0]!.message);
|
|
75
|
+
for (const node of result.data?.objects?.nodes ?? []) {
|
|
76
|
+
const repr = node.asMoveObject?.contents?.type.repr;
|
|
77
|
+
if (!repr) continue;
|
|
78
|
+
let recordingShareType: string | undefined;
|
|
79
|
+
try {
|
|
80
|
+
[recordingShareType] = extractTypeParams2(repr);
|
|
81
|
+
} catch {
|
|
82
|
+
recordingShareType = /<(.+)>$/.exec(repr)?.[1]?.trim();
|
|
83
|
+
}
|
|
84
|
+
if (recordingShareType && recordingSet.has(recordingShareType)) {
|
|
85
|
+
out.recordings[recordingShareType] = node.address;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface WorkIds {
|
|
94
|
+
compositions: readonly string[];
|
|
95
|
+
recordings: readonly string[];
|
|
96
|
+
releases: readonly string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface WorksById {
|
|
100
|
+
compositions: Partial<Record<string, Composition>>;
|
|
101
|
+
recordings: Partial<Record<string, Recording>>;
|
|
102
|
+
releases: Partial<Record<string, Release>>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Fetch each work kind through the authoritative protocol SDK. */
|
|
106
|
+
export async function getWorksByIds(client: ClientWithCoreApi, ids: WorkIds): Promise<WorksById> {
|
|
107
|
+
const [compositions, recordings, releases] = await Promise.all([
|
|
108
|
+
getCompositionsByIds(client, [...ids.compositions]),
|
|
109
|
+
getRecordingsByIds(client, [...ids.recordings]),
|
|
110
|
+
getReleasesByIds(client, [...ids.releases]),
|
|
111
|
+
]);
|
|
112
|
+
return { compositions, recordings, releases };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve display titles for both deployed recording layouts. Older recordings
|
|
117
|
+
* carry `title` in JSON; current recordings inherit it from their composition.
|
|
118
|
+
*/
|
|
119
|
+
export async function getRecordingTitles(
|
|
120
|
+
client: ClientWithCoreApi,
|
|
121
|
+
graphql: SuiGraphQLClient,
|
|
122
|
+
recordingIds: readonly string[],
|
|
123
|
+
misoPackageId: string,
|
|
124
|
+
): Promise<Record<string, string>> {
|
|
125
|
+
const ids = [...new Set(recordingIds)];
|
|
126
|
+
if (ids.length === 0) return {};
|
|
127
|
+
|
|
128
|
+
const { objects } = await client.core.getObjects({ objectIds: ids, include: { json: true } });
|
|
129
|
+
const titles: Record<string, string> = {};
|
|
130
|
+
const compositionShareByRecording: Record<string, string> = {};
|
|
131
|
+
|
|
132
|
+
for (const object of objects) {
|
|
133
|
+
if (object instanceof Error) continue;
|
|
134
|
+
const json = object.json as { title?: unknown } | null;
|
|
135
|
+
if (typeof json?.title === "string" && json.title) {
|
|
136
|
+
titles[object.objectId] = json.title;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const [, compositionShareType] = extractTypeParams2(object.type);
|
|
141
|
+
compositionShareByRecording[object.objectId] = compositionShareType;
|
|
142
|
+
} catch {
|
|
143
|
+
// A deployed layout with no composition type and no JSON title remains Untitled.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const compositionShareTypes = Object.values(compositionShareByRecording);
|
|
148
|
+
if (compositionShareTypes.length === 0) return titles;
|
|
149
|
+
const addresses = await getWorkAddressesByShareTypes(
|
|
150
|
+
graphql,
|
|
151
|
+
{ compositions: compositionShareTypes, recordings: [] },
|
|
152
|
+
misoPackageId,
|
|
153
|
+
);
|
|
154
|
+
const compositions = await getCompositionsByIds(
|
|
155
|
+
client,
|
|
156
|
+
Object.values(addresses.compositions).filter((id): id is string => !!id),
|
|
157
|
+
);
|
|
158
|
+
for (const [recordingId, shareType] of Object.entries(compositionShareByRecording)) {
|
|
159
|
+
const compositionId = addresses.compositions[shareType];
|
|
160
|
+
const title = compositionId ? compositions[compositionId]?.title : undefined;
|
|
161
|
+
if (title) titles[recordingId] = title;
|
|
162
|
+
}
|
|
163
|
+
return titles;
|
|
164
|
+
}
|