@misofm/sdk 0.2.1 → 0.4.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 +68 -5
- 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/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/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 +19 -2
- package/src/auth.ts +275 -0
- package/src/contracts/miso_drop/drop.ts +798 -0
- package/src/contracts.ts +4 -0
- 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
package/src/contracts.ts
CHANGED
|
@@ -14,6 +14,10 @@ export * as pressing from "./contracts/miso_pressing/pressing.ts";
|
|
|
14
14
|
export * as listing from "./contracts/miso_pressing/listing.ts";
|
|
15
15
|
export * as certificate from "./contracts/miso_pressing/certificate.ts";
|
|
16
16
|
|
|
17
|
+
// Existing launch drops remain readable and purchasable while releases move to
|
|
18
|
+
// the pressing/listing contracts.
|
|
19
|
+
export * as drop from "./contracts/miso_drop/drop.ts";
|
|
20
|
+
|
|
17
21
|
// Royalty-pool extension (base lib + per-work extensions).
|
|
18
22
|
export * as royaltyPool from "./contracts/royalty_pool/pool.ts";
|
|
19
23
|
export * as royaltyPoolStake from "./contracts/royalty_pool/stake.ts";
|
package/src/drop.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Drop — the record sale. A `Drop<Currency>` is a shared, immutable object that sells
|
|
5
|
+
// copies of a Release's records at a fixed or floor price, optionally capped
|
|
6
|
+
// (`maxSupply`) and/or time-boxed. A release has at most ONE live drop: `createDrop`
|
|
7
|
+
// opens edition 0, and `newEdition` opens edition n+1 by consuming the predecessor —
|
|
8
|
+
// the one way to change price, currency, cap, or window. The registry tracks each
|
|
9
|
+
// release's live drop (`getCurrentDrop`).
|
|
10
|
+
|
|
11
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client";
|
|
12
|
+
import { deriveDynamicFieldID } from "@mysten/sui/utils";
|
|
13
|
+
import { bcs } from "@mysten/sui/bcs";
|
|
14
|
+
import type { TxThunk } from "./transactions.ts";
|
|
15
|
+
import { isNotFound } from "./queries.ts";
|
|
16
|
+
import * as drop from "./contracts/miso_drop/drop.ts";
|
|
17
|
+
|
|
18
|
+
/** Pricing policy: pay exactly `amount` (fixed) or at least `amount` (floor). */
|
|
19
|
+
export type DropPrice = { kind: "fixed" | "floor"; amount: bigint | number | string };
|
|
20
|
+
|
|
21
|
+
export interface CreateDropParams {
|
|
22
|
+
/** The `Release` object the drop sells copies of — also the drop's derivation
|
|
23
|
+
* parent and the home of the live-drop pointer. */
|
|
24
|
+
releaseId: string;
|
|
25
|
+
/** The release's `ReleaseAdminCap` — authorizes the drop. */
|
|
26
|
+
releaseAdminCapId: string;
|
|
27
|
+
/** Fixed or floor price, in the currency's smallest unit. */
|
|
28
|
+
price: DropPrice;
|
|
29
|
+
/** The `Currency` type argument, e.g. `0x2::sui::SUI`. */
|
|
30
|
+
currencyType: string;
|
|
31
|
+
/** Copies this edition may ever sell; `null`/omitted = uncapped (an open edition). */
|
|
32
|
+
maxSupply?: bigint | number | null;
|
|
33
|
+
/** Unix ms the sale opens. Default 0 (open immediately). */
|
|
34
|
+
startTimestampMs?: bigint | number;
|
|
35
|
+
/** Unix ms the sale closes; `null`/omitted = evergreen. */
|
|
36
|
+
endTimestampMs?: bigint | number | null;
|
|
37
|
+
misoDropPackageId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates and shares a release's FIRST drop — edition 0. A release's edition
|
|
42
|
+
* sequence can only ever start once; later terms changes go through `newEdition`.
|
|
43
|
+
* The `Clock` is auto-supplied by the generated binding.
|
|
44
|
+
*/
|
|
45
|
+
export function createDrop(p: CreateDropParams): TxThunk {
|
|
46
|
+
return (tx) => {
|
|
47
|
+
const terms = buildTerms(tx, p);
|
|
48
|
+
tx.add(
|
|
49
|
+
drop._new({
|
|
50
|
+
package: p.misoDropPackageId,
|
|
51
|
+
arguments: [p.releaseId, p.releaseAdminCapId, terms.price, terms.supply, terms.window],
|
|
52
|
+
typeArguments: [p.currencyType],
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NewEditionParams {
|
|
59
|
+
/** The `Release` whose drop is being superseded. */
|
|
60
|
+
releaseId: string;
|
|
61
|
+
/** The release's CURRENT drop — consumed (destroyed) by this call. */
|
|
62
|
+
oldDropId: string;
|
|
63
|
+
/** The current drop's `Currency` type. */
|
|
64
|
+
oldCurrencyType: string;
|
|
65
|
+
/** The release's `ReleaseAdminCap`. */
|
|
66
|
+
releaseAdminCapId: string;
|
|
67
|
+
/** The successor's price. */
|
|
68
|
+
price: DropPrice;
|
|
69
|
+
/** The successor's `Currency` type (may differ from the old one). */
|
|
70
|
+
currencyType: string;
|
|
71
|
+
/** Successor's supply cap; `null`/omitted = uncapped. */
|
|
72
|
+
maxSupply?: bigint | number | null;
|
|
73
|
+
/** Unix ms the successor opens. Default 0 (open immediately). */
|
|
74
|
+
startTimestampMs?: bigint | number;
|
|
75
|
+
/** Unix ms the successor closes; `null`/omitted = evergreen. */
|
|
76
|
+
endTimestampMs?: bigint | number | null;
|
|
77
|
+
misoDropPackageId: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Opens edition n+1 of a release's drop, CONSUMING the current one — the one way to
|
|
82
|
+
* change price, currency, cap, or window (or start a fresh run after a sell-out).
|
|
83
|
+
* Serial numbers restart at 1 for the new edition. The `Clock` is auto-supplied.
|
|
84
|
+
*/
|
|
85
|
+
export function newEdition(p: NewEditionParams): TxThunk {
|
|
86
|
+
return (tx) => {
|
|
87
|
+
const terms = buildTerms(tx, p);
|
|
88
|
+
tx.add(
|
|
89
|
+
drop.newEdition({
|
|
90
|
+
package: p.misoDropPackageId,
|
|
91
|
+
arguments: [p.releaseId, p.oldDropId, p.releaseAdminCapId, terms.price, terms.supply, terms.window],
|
|
92
|
+
typeArguments: [p.oldCurrencyType, p.currencyType],
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Builds the three on-chain term values (`Price`, `Supply`, `Window`) from the flat
|
|
99
|
+
* facade params — the flat shape stays the public API; the enums are a wire detail. */
|
|
100
|
+
function buildTerms(
|
|
101
|
+
tx: Parameters<TxThunk>[0],
|
|
102
|
+
p: Pick<CreateDropParams, "price" | "maxSupply" | "startTimestampMs" | "endTimestampMs" | "misoDropPackageId">,
|
|
103
|
+
) {
|
|
104
|
+
const pkg = { package: p.misoDropPackageId };
|
|
105
|
+
const makePrice = p.price.kind === "fixed" ? drop.newFixedPrice : drop.newFloorPrice;
|
|
106
|
+
const price = tx.add(makePrice({ ...pkg, arguments: [BigInt(p.price.amount)] }));
|
|
107
|
+
const supply =
|
|
108
|
+
p.maxSupply == null
|
|
109
|
+
? tx.add(drop.newUncappedSupply({ ...pkg, arguments: [] }))
|
|
110
|
+
: tx.add(drop.newCappedSupply({ ...pkg, arguments: [BigInt(p.maxSupply)] }));
|
|
111
|
+
const start = BigInt(p.startTimestampMs ?? 0);
|
|
112
|
+
const window =
|
|
113
|
+
p.endTimestampMs == null
|
|
114
|
+
? tx.add(drop.newUnboundedWindow({ ...pkg, arguments: [start] }))
|
|
115
|
+
: tx.add(drop.newBoundedWindow({ ...pkg, arguments: [start, BigInt(p.endTimestampMs)] }));
|
|
116
|
+
return { price, supply, window };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface BuyRecordParams {
|
|
120
|
+
/** The shared `Drop<Currency>` to buy from. */
|
|
121
|
+
dropId: string;
|
|
122
|
+
/** Buyer's `Coin<Currency>` object ids to pay from — merged, then split to `amount`. */
|
|
123
|
+
paymentCoinIds: string[];
|
|
124
|
+
/** Amount to pay in the currency's smallest unit (exact for `fixed`, ≥ for `floor`). */
|
|
125
|
+
amount: bigint | number | string;
|
|
126
|
+
/** The drop's `Currency` type, e.g. `0x2::sui::SUI`. */
|
|
127
|
+
currencyType: string;
|
|
128
|
+
/** The `miso_record` `Settings` shared object (authorizes the mint). */
|
|
129
|
+
settingsId: string;
|
|
130
|
+
/** Where to send the bought `Record` (usually the buyer). */
|
|
131
|
+
recipient: string;
|
|
132
|
+
misoDropPackageId: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Buys one record from a drop: merges the buyer's payment coins, splits the exact
|
|
137
|
+
* price, calls `buy`, and sends the minted `Record` to `recipient`. The buyer selects
|
|
138
|
+
* which of their coins to spend (see the app's coin selection); this builder only
|
|
139
|
+
* assembles the PTB. The `Clock` is auto-supplied.
|
|
140
|
+
*/
|
|
141
|
+
export function buyRecord(p: BuyRecordParams): TxThunk {
|
|
142
|
+
return (tx) => {
|
|
143
|
+
const [first, ...rest] = p.paymentCoinIds;
|
|
144
|
+
if (!first) throw new Error("buyRecord: no payment coins provided");
|
|
145
|
+
const primary = tx.object(first);
|
|
146
|
+
if (rest.length > 0) tx.mergeCoins(primary, rest.map((id) => tx.object(id)));
|
|
147
|
+
const [payment] = tx.splitCoins(primary, [tx.pure.u64(BigInt(p.amount))]);
|
|
148
|
+
const record = tx.add(
|
|
149
|
+
drop.buy({
|
|
150
|
+
package: p.misoDropPackageId,
|
|
151
|
+
arguments: [p.dropId, payment, p.settingsId],
|
|
152
|
+
typeArguments: [p.currencyType],
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
tx.transferObjects([record], p.recipient);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A parsed drop, in the shape the featured-drop card renders. */
|
|
160
|
+
export interface DropView {
|
|
161
|
+
id: string;
|
|
162
|
+
releaseId: string;
|
|
163
|
+
edition: number;
|
|
164
|
+
price: { kind: "fixed" | "floor"; amount: bigint };
|
|
165
|
+
/** `null` = uncapped (an open edition). */
|
|
166
|
+
maxSupply: bigint | null;
|
|
167
|
+
quantitySold: bigint;
|
|
168
|
+
startTimestampMs: bigint;
|
|
169
|
+
/** `null` = evergreen. */
|
|
170
|
+
endTimestampMs: bigint | null;
|
|
171
|
+
/** The drop's `Currency` type, parsed from its object type tag (`null` if unavailable). */
|
|
172
|
+
currencyType: string | null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Extracts `CURRENCY` from a `…::drop::Drop<CURRENCY>` type tag. */
|
|
176
|
+
function currencyFromType(type: string | null | undefined): string | null {
|
|
177
|
+
if (!type) return null;
|
|
178
|
+
const lt = type.indexOf("<");
|
|
179
|
+
return lt >= 0 && type.endsWith(">") ? type.slice(lt + 1, -1) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetches and parses a shared `Drop` object, or `null` if it doesn't exist.
|
|
184
|
+
*
|
|
185
|
+
* NOTE: this deliberately deviates from the core-getter convention in
|
|
186
|
+
* `./queries.ts` (throw on missing object). Consumers probe pasted or stored
|
|
187
|
+
* drop ids — a party's featured drop, a superseded edition — where "no such
|
|
188
|
+
* drop" is a normal state, so absence is `null` and only transport failures throw.
|
|
189
|
+
*/
|
|
190
|
+
export async function getDrop(client: ClientWithCoreApi, dropId: string): Promise<DropView | null> {
|
|
191
|
+
let content: Uint8Array | null | undefined;
|
|
192
|
+
let type: string | undefined;
|
|
193
|
+
try {
|
|
194
|
+
const { object } = await client.core.getObject({ objectId: dropId, include: { content: true } });
|
|
195
|
+
content = object?.content;
|
|
196
|
+
type = object?.type;
|
|
197
|
+
} catch (e) {
|
|
198
|
+
if (isNotFound(e)) return null;
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
if (!content) return null;
|
|
202
|
+
|
|
203
|
+
const d = drop.Drop.parse(content);
|
|
204
|
+
const price = d.price as { $kind?: string; Fixed?: { amount: string }; Floor?: { amount: string } };
|
|
205
|
+
const isFixed = price.$kind === "Fixed" || price.Fixed != null;
|
|
206
|
+
const amount = (isFixed ? price.Fixed?.amount : price.Floor?.amount) ?? "0";
|
|
207
|
+
const supply = d.supply as { $kind?: string; Capped?: { max: string } };
|
|
208
|
+
const capped = supply.$kind === "Capped" || supply.Capped != null;
|
|
209
|
+
const window = d.window as {
|
|
210
|
+
$kind?: string;
|
|
211
|
+
Unbounded?: { start_timestamp_ms: string };
|
|
212
|
+
Bounded?: { start_timestamp_ms: string; end_timestamp_ms: string };
|
|
213
|
+
};
|
|
214
|
+
const bounded = window.$kind === "Bounded" || window.Bounded != null;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
id: dropId,
|
|
218
|
+
releaseId: d.release_id,
|
|
219
|
+
edition: d.edition,
|
|
220
|
+
price: { kind: isFixed ? "fixed" : "floor", amount: BigInt(amount) },
|
|
221
|
+
maxSupply: capped ? BigInt(supply.Capped?.max ?? "0") : null,
|
|
222
|
+
quantitySold: BigInt(d.quantity_sold),
|
|
223
|
+
startTimestampMs: BigInt(
|
|
224
|
+
(bounded ? window.Bounded?.start_timestamp_ms : window.Unbounded?.start_timestamp_ms) ?? "0",
|
|
225
|
+
),
|
|
226
|
+
endTimestampMs: bounded ? BigInt(window.Bounded?.end_timestamp_ms ?? "0") : null,
|
|
227
|
+
currencyType: currencyFromType(type),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The release's `CurrentDropKey` dynamic field holds a bare `ID`; the field object
|
|
232
|
+
// serializes as `Field { id, name: CurrentDropKey (unit), value: ID }`.
|
|
233
|
+
const CurrentDropField = bcs.struct("Field", { id: bcs.Address, name: bcs.bool(), value: bcs.Address });
|
|
234
|
+
|
|
235
|
+
// Unit-struct key bytes (single `0x00` for the dummy bool field).
|
|
236
|
+
const CURRENT_DROP_KEY_BYTES = drop.CurrentDropKey.serialize([false]).toBytes();
|
|
237
|
+
|
|
238
|
+
export interface GetCurrentDropParams {
|
|
239
|
+
/** The release whose live drop to resolve. */
|
|
240
|
+
releaseId: string;
|
|
241
|
+
misoDropPackageId: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolves a release's LIVE drop through the `CurrentDropKey` pointer on the
|
|
246
|
+
* release's UID, or `null` if the release has never dropped. Superseded drops are
|
|
247
|
+
* destroyed, so this — not derived-address probing — is how the current edition is
|
|
248
|
+
* found.
|
|
249
|
+
*/
|
|
250
|
+
export async function getCurrentDrop(client: ClientWithCoreApi, p: GetCurrentDropParams): Promise<DropView | null> {
|
|
251
|
+
const fieldId = deriveDynamicFieldID(
|
|
252
|
+
p.releaseId,
|
|
253
|
+
`${p.misoDropPackageId}::drop::CurrentDropKey`,
|
|
254
|
+
CURRENT_DROP_KEY_BYTES,
|
|
255
|
+
);
|
|
256
|
+
let content: Uint8Array | null | undefined;
|
|
257
|
+
try {
|
|
258
|
+
const { object } = await client.core.getObject({ objectId: fieldId, include: { content: true } });
|
|
259
|
+
content = object?.content;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (isNotFound(e)) return null;
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
if (!content) return null;
|
|
265
|
+
return getDrop(client, CurrentDropField.parse(content).value);
|
|
266
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,11 @@ export * from "./share-template.ts";
|
|
|
35
35
|
export * from "./release-graph.ts";
|
|
36
36
|
export * from "./catalog.ts";
|
|
37
37
|
|
|
38
|
+
// High-level, JSON-safe platform projections used directly by clients and by
|
|
39
|
+
// the thin HTTP read service. Namespaced because these composed view types are
|
|
40
|
+
// intentionally distinct from the lower-level contract views above.
|
|
41
|
+
export * as read from "./read/index.ts";
|
|
42
|
+
|
|
38
43
|
// First-party extensions — attached to a protocol work via its cap-gated
|
|
39
44
|
// `uid_mut` hook.
|
|
40
45
|
export * from "./credits.ts";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Artist reads: a `Party` plus every extension attached to its UID.
|
|
5
|
+
//
|
|
6
|
+
// The artist page is the clearest case for this whole layer. Its data lives in
|
|
7
|
+
// EIGHT separate dynamic fields, each its own package, each its own read — party,
|
|
8
|
+
// profile, ctas, genres, links, roles, tags, featured. miso-app fanned all eight
|
|
9
|
+
// out from the browser (`lib/party-queries.ts`, plus a second identical fan-out in
|
|
10
|
+
// `PartyEditSheet`). Here they are one call, resolved once, cached once.
|
|
11
|
+
//
|
|
12
|
+
// `roles` and `tags` are opt-in via `include`: only the owner's edit sheet reads
|
|
13
|
+
// them, and leaving them out of the public read keeps the hot path to six.
|
|
14
|
+
|
|
15
|
+
import { resolveGenreNames } from "./genres.ts";
|
|
16
|
+
import { readReleaseCover } from "./catalog.ts";
|
|
17
|
+
import { getDrop } from "../drop.ts";
|
|
18
|
+
import { getReleaseById } from "@misonetwork/sdk";
|
|
19
|
+
import { getReleaseCredits } from "../credits.ts";
|
|
20
|
+
import type { MisoClient } from "./client.ts";
|
|
21
|
+
import type { ArtistProfile, FeaturedRelease, PartyMember, PartySummary } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
/** Optional sub-resources — read only when asked for. */
|
|
24
|
+
export type ArtistInclude = "roles" | "tags";
|
|
25
|
+
|
|
26
|
+
export interface GetArtistOptions {
|
|
27
|
+
include?: readonly ArtistInclude[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Public avatar URL for a party (served from the API's R2 lane; 404 when unset). */
|
|
31
|
+
export function partyAvatarUrl(apiBaseUrl: string, partyId: string): string {
|
|
32
|
+
return `${apiBaseUrl.replace(/\/$/, "")}/media/avatar/${partyId}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An artist page, fully resolved. Throws only if the PARTY itself can't be read —
|
|
37
|
+
* every extension is optional by design (a party with no profile set is a new
|
|
38
|
+
* party, not a broken one), so each decoration read falls back to its empty value.
|
|
39
|
+
*/
|
|
40
|
+
export async function getArtistProfile(
|
|
41
|
+
client: MisoClient,
|
|
42
|
+
partyId: string,
|
|
43
|
+
options: GetArtistOptions = {},
|
|
44
|
+
): Promise<ArtistProfile> {
|
|
45
|
+
const include = new Set(options.include ?? []);
|
|
46
|
+
const { party } = client.sui;
|
|
47
|
+
|
|
48
|
+
const [profile, ctas, genreIds, links, roles, tags] = await Promise.all([
|
|
49
|
+
party.getProfile(partyId).catch(() => null),
|
|
50
|
+
party.getCtas(partyId).catch(() => []),
|
|
51
|
+
party.getGenres(partyId).catch(() => [] as string[]),
|
|
52
|
+
party.getLinks(partyId).catch(() => []),
|
|
53
|
+
include.has("roles") ? party.getRoles(partyId).catch(() => [] as string[]) : Promise.resolve(undefined),
|
|
54
|
+
include.has("tags") ? party.getTags(partyId).catch(() => [] as string[]) : Promise.resolve(undefined),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// The party read is the one that may legitimately fail (no such party), so it
|
|
58
|
+
// is awaited on its own rather than being swallowed by the batch above.
|
|
59
|
+
const entity = await party.getPartyById(partyId);
|
|
60
|
+
|
|
61
|
+
const [genres, members] = await Promise.all([
|
|
62
|
+
resolveGenreNames(client, genreIds),
|
|
63
|
+
resolveMembers(client, entity.kind === "group" ? (entity.members ?? []) : []),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: entity.id,
|
|
68
|
+
kind: entity.kind,
|
|
69
|
+
name: entity.name,
|
|
70
|
+
createdAtMs: entity.createdAtMs,
|
|
71
|
+
bioShort: profile?.bioShort ?? null,
|
|
72
|
+
bioLong: profile?.bioLong ?? null,
|
|
73
|
+
country: profile?.country ?? null,
|
|
74
|
+
languages: profile?.languages ?? [],
|
|
75
|
+
genres,
|
|
76
|
+
links: links.map((l) => ({ platform: l.platform, value: l.value, url: l.url })),
|
|
77
|
+
ctas: ctas.map((c) => ({ label: c.label, url: c.url })),
|
|
78
|
+
members,
|
|
79
|
+
...(roles !== undefined ? { roles } : {}),
|
|
80
|
+
...(tags !== undefined ? { tags } : {}),
|
|
81
|
+
avatarUrl: partyAvatarUrl(client.config.apiBaseUrl, entity.id),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Group members with names resolved. A member that fails to read is dropped. */
|
|
86
|
+
async function resolveMembers(client: MisoClient, memberIds: readonly string[]): Promise<PartyMember[]> {
|
|
87
|
+
if (memberIds.length === 0) return [];
|
|
88
|
+
const parties = await client.sui.party.getPartiesByIds([...memberIds]).catch(() => ({}) as Record<string, undefined>);
|
|
89
|
+
return memberIds.flatMap((id) => {
|
|
90
|
+
const p = parties[id];
|
|
91
|
+
return p ? [{ id, name: p.name }] : [];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Name + kind for many parties at once. Ids that don't resolve are omitted. */
|
|
96
|
+
export async function getPartySummaries(client: MisoClient, ids: readonly string[]): Promise<PartySummary[]> {
|
|
97
|
+
if (ids.length === 0) return [];
|
|
98
|
+
const parties = await client.sui.party.getPartiesByIds([...ids]);
|
|
99
|
+
return ids.flatMap((id) => {
|
|
100
|
+
const p = parties[id];
|
|
101
|
+
return p ? [{ id, name: p.name, kind: p.kind }] : [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The party's pinned pressing, resolved through to its release and cover.
|
|
107
|
+
* `null` — a successful empty — when nothing is pinned or the pin is dangling
|
|
108
|
+
* (the pressing it named has since been superseded and destroyed).
|
|
109
|
+
*/
|
|
110
|
+
export async function getFeaturedRelease(client: MisoClient, partyId: string): Promise<FeaturedRelease | null> {
|
|
111
|
+
const pressingId = await client.sui.party.getFeaturedDrop(partyId);
|
|
112
|
+
if (!pressingId) return null;
|
|
113
|
+
|
|
114
|
+
const pressing = await getDrop(client.protocol, pressingId);
|
|
115
|
+
if (!pressing) return null;
|
|
116
|
+
|
|
117
|
+
const [release, cover, credits, entity] = await Promise.all([
|
|
118
|
+
getReleaseById(client.protocol, pressing.releaseId),
|
|
119
|
+
readReleaseCover(client, pressing.releaseId),
|
|
120
|
+
getReleaseCredits(client.protocol, pressing.releaseId, client.config.protocol.releaseCredits).catch(() => null),
|
|
121
|
+
client.sui.party.getPartyById(partyId),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
// The artist line prefers the release's own PRIMARY credits and falls back to
|
|
125
|
+
// the party whose page this is — a release with no credits set still shows an
|
|
126
|
+
// artist, which is the behavior the featured card has always had.
|
|
127
|
+
const primary = (credits ?? []).filter((c) => c.roles.includes("Primary")).map((c) => c.displayName);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
pressingId,
|
|
131
|
+
releaseId: pressing.releaseId,
|
|
132
|
+
title: release.title,
|
|
133
|
+
artist: primary.length ? primary.join(", ") : entity.name,
|
|
134
|
+
coverUrl: cover?.still.url ?? null,
|
|
135
|
+
};
|
|
136
|
+
}
|