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