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