@misofm/platform 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -45
- package/dist/auth.d.ts +8 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +78 -59
- package/dist/auth.js.map +1 -1
- package/dist/catalog.d.ts +5 -5
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +33 -21
- package/dist/catalog.js.map +1 -1
- package/dist/client.d.ts +17 -30
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +53 -75
- package/dist/client.js.map +1 -1
- package/dist/cover.d.ts +4 -3
- package/dist/cover.d.ts.map +1 -1
- package/dist/cover.js +28 -17
- package/dist/cover.js.map +1 -1
- package/dist/credits.d.ts +12 -10
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +60 -33
- package/dist/credits.js.map +1 -1
- package/dist/deployments.d.ts +21 -13
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +84 -37
- package/dist/deployments.js.map +1 -1
- package/dist/errors.d.ts +91 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts +3 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +9 -4
- package/dist/execute.js.map +1 -1
- package/dist/genre.d.ts +25 -41
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +71 -20
- package/dist/genre.js.map +1 -1
- package/dist/party/client.d.ts +16 -13
- package/dist/party/client.d.ts.map +1 -1
- package/dist/party/client.js +33 -22
- package/dist/party/client.js.map +1 -1
- package/dist/party/extensions/links.d.ts +2 -1
- package/dist/party/extensions/links.d.ts.map +1 -1
- package/dist/party/extensions/links.js +2 -1
- package/dist/party/extensions/links.js.map +1 -1
- package/dist/party/queries.d.ts +12 -11
- package/dist/party/queries.d.ts.map +1 -1
- package/dist/party/queries.js +63 -57
- package/dist/party/queries.js.map +1 -1
- package/dist/party/types.d.ts +31 -20
- package/dist/party/types.d.ts.map +1 -1
- package/dist/party/types.js +82 -1
- package/dist/party/types.js.map +1 -1
- package/dist/pressing.d.ts +55 -35
- package/dist/pressing.d.ts.map +1 -1
- package/dist/pressing.js +99 -76
- package/dist/pressing.js.map +1 -1
- package/dist/queries.d.ts +1 -1
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +5 -4
- package/dist/queries.js.map +1 -1
- package/dist/read/artist.d.ts +6 -4
- package/dist/read/artist.d.ts.map +1 -1
- package/dist/read/artist.js +29 -27
- package/dist/read/artist.js.map +1 -1
- package/dist/read/catalog.d.ts +20 -17
- package/dist/read/catalog.d.ts.map +1 -1
- package/dist/read/catalog.js +120 -141
- package/dist/read/catalog.js.map +1 -1
- package/dist/read/client.d.ts +10 -9
- package/dist/read/client.d.ts.map +1 -1
- package/dist/read/client.js.map +1 -1
- package/dist/read/genres.d.ts +5 -3
- package/dist/read/genres.d.ts.map +1 -1
- package/dist/read/genres.js +18 -15
- package/dist/read/genres.js.map +1 -1
- package/dist/read/index.d.ts.map +1 -1
- package/dist/read/index.js +7 -1
- package/dist/read/index.js.map +1 -1
- package/dist/read/receipts.d.ts +15 -6
- package/dist/read/receipts.d.ts.map +1 -1
- package/dist/read/receipts.js +73 -60
- package/dist/read/receipts.js.map +1 -1
- package/dist/read/wallet.d.ts +11 -9
- package/dist/read/wallet.d.ts.map +1 -1
- package/dist/read/wallet.js +117 -132
- package/dist/read/wallet.js.map +1 -1
- package/dist/read/works.d.ts +9 -16
- package/dist/read/works.d.ts.map +1 -1
- package/dist/read/works.js +48 -117
- package/dist/read/works.js.map +1 -1
- package/dist/recording-extensions.d.ts +8 -7
- package/dist/recording-extensions.d.ts.map +1 -1
- package/dist/recording-extensions.js +30 -63
- package/dist/recording-extensions.js.map +1 -1
- package/dist/release-extensions.d.ts +3 -2
- package/dist/release-extensions.d.ts.map +1 -1
- package/dist/release-extensions.js +9 -10
- package/dist/release-extensions.js.map +1 -1
- package/dist/royalty.d.ts +51 -51
- package/dist/royalty.d.ts.map +1 -1
- package/dist/royalty.js +93 -11
- package/dist/royalty.js.map +1 -1
- package/dist/share.d.ts +7 -6
- package/dist/share.d.ts.map +1 -1
- package/dist/share.js +37 -30
- package/dist/share.js.map +1 -1
- package/dist/vault.d.ts +20 -8
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +31 -15
- package/dist/vault.js.map +1 -1
- package/package.json +11 -4
- package/src/auth.ts +89 -70
- package/src/catalog.ts +33 -49
- package/src/client.ts +117 -97
- package/src/cover.ts +17 -26
- package/src/credits.ts +43 -75
- package/src/deployments.ts +103 -69
- package/src/errors.ts +156 -0
- package/src/execute.ts +10 -6
- package/src/genre.ts +38 -23
- package/src/party/client.ts +35 -23
- package/src/party/extensions/links.ts +3 -2
- package/src/party/queries.ts +77 -65
- package/src/party/types.ts +52 -48
- package/src/pressing.ts +158 -161
- package/src/queries.ts +5 -5
- package/src/read/artist.ts +47 -42
- package/src/read/catalog.ts +200 -223
- package/src/read/client.ts +12 -11
- package/src/read/genres.ts +24 -16
- package/src/read/index.ts +7 -1
- package/src/read/receipts.ts +128 -67
- package/src/read/wallet.ts +194 -162
- package/src/read/works.ts +75 -194
- package/src/recording-extensions.ts +38 -92
- package/src/release-extensions.ts +7 -12
- package/src/royalty.ts +117 -20
- package/src/share.ts +44 -29
- package/src/vault.ts +47 -24
package/src/read/client.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// Party now shares the same network SDK registration and deployment as protocol
|
|
17
17
|
// core, so every read hangs off one `sui.miso` namespace.
|
|
18
18
|
|
|
19
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client";
|
|
19
20
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
20
21
|
import { SuiGraphQLClient } from "@mysten/sui/graphql";
|
|
21
22
|
import { miso, type MisoProtocolClient } from "@misofm/musicos/client";
|
|
@@ -28,25 +29,25 @@ function definedOverrides<T extends object>(value: T): Partial<T> {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* rather than an `any` smeared across the codebase.
|
|
32
|
+
* The transport slice every `@misofm/platform/read` function needs to run its
|
|
33
|
+
* `Effect`: `Effect.provide(SuiClient.layer(client.protocol))`. `sui` (below)
|
|
34
|
+
* satisfies this structurally, since it is a real `ClientWithCoreApi`.
|
|
35
35
|
*/
|
|
36
|
-
export type ProtocolClient =
|
|
36
|
+
export type ProtocolClient = ClientWithCoreApi;
|
|
37
37
|
|
|
38
|
-
/** A GraphQL client in the shape
|
|
38
|
+
/** A GraphQL client in the shape `@misofm/effect`'s `SuiGraphQL` service wants. */
|
|
39
39
|
export type ProtocolGraphQLClient = SuiGraphQLClient;
|
|
40
40
|
|
|
41
41
|
export interface MisoClient {
|
|
42
42
|
config: MisoConfig;
|
|
43
43
|
/** gRPC data plane (object-model core only; Party is `client.party`). */
|
|
44
44
|
sui: SuiGrpcClient & { miso: MisoProtocolClient };
|
|
45
|
-
/** The same client,
|
|
45
|
+
/** The same client, for `Effect.provide(SuiClient.layer(client.protocol))`. */
|
|
46
46
|
protocol: ProtocolClient;
|
|
47
|
-
/** GraphQL RPC,
|
|
47
|
+
/** GraphQL RPC, for `Effect.provide(SuiGraphQL.layer(client.graphql))`. */
|
|
48
48
|
graphql: ProtocolGraphQLClient;
|
|
49
|
-
/** The raw GraphQL client
|
|
49
|
+
/** The raw GraphQL client — identical to `graphql`; both names are kept for
|
|
50
|
+
* source compatibility with callers migrating from the pre-Effect client. */
|
|
50
51
|
graphqlRaw: SuiGraphQLClient;
|
|
51
52
|
/** Party identity and profile reads/builders, bound to `config.partyos`/`config.party`. */
|
|
52
53
|
party: PartyPlatformClient;
|
|
@@ -85,8 +86,8 @@ export function createMisoClient(options: CreateMisoClientOptions = {}): MisoCli
|
|
|
85
86
|
return {
|
|
86
87
|
config,
|
|
87
88
|
sui,
|
|
88
|
-
protocol: sui
|
|
89
|
-
graphql: graphqlRaw
|
|
89
|
+
protocol: sui,
|
|
90
|
+
graphql: graphqlRaw,
|
|
90
91
|
graphqlRaw,
|
|
91
92
|
party,
|
|
92
93
|
};
|
package/src/read/genres.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// object carries a screaming-snake name ("HIP_HOP"). There is no id→name table
|
|
6
6
|
// anywhere off-chain, so the names are read from the objects and humanized.
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { Effect } from "effect";
|
|
9
|
+
import { getObjectsContent, type SuiClient } from "@misofm/effect";
|
|
10
|
+
import { Genre as GenreBcs } from "../contracts/genre/genre.ts";
|
|
9
11
|
|
|
10
12
|
/** "HIP_HOP" → "Hip Hop". */
|
|
11
13
|
function humanize(name: string): string {
|
|
@@ -18,20 +20,26 @@ function humanize(name: string): string {
|
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Resolve `Genre` object ids to display names. Best-effort: an id that fails to
|
|
21
|
-
* read
|
|
23
|
+
* read, or whose content does not parse as a `Genre`, is dropped rather than
|
|
24
|
+
* failing the artist page around it.
|
|
22
25
|
*/
|
|
23
|
-
export
|
|
26
|
+
export const resolveGenreNames = Effect.fn("resolveGenreNames")(function* (
|
|
27
|
+
ids: string[],
|
|
28
|
+
): Effect.fn.Return<string[], never, SuiClient> {
|
|
24
29
|
if (ids.length === 0) return [];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
30
|
+
return yield* getObjectsContent(ids).pipe(
|
|
31
|
+
Effect.map((contentById) => {
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const { content } of contentById.values()) {
|
|
34
|
+
try {
|
|
35
|
+
const name = GenreBcs.parse(content).name;
|
|
36
|
+
if (name) out.push(humanize(name));
|
|
37
|
+
} catch {
|
|
38
|
+
// Malformed content — drop this genre rather than fail the page.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}),
|
|
43
|
+
Effect.catch(() => Effect.succeed([])),
|
|
44
|
+
);
|
|
45
|
+
});
|
package/src/read/index.ts
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Browser- and server-compatible. The HTTP API is a thin cached transport over
|
|
12
12
|
// this same surface; direct clients can use it without going through the API.
|
|
13
|
+
// Every read returns `Effect<A, E, SuiClient | SuiGraphQL>`; `createMisoClient`
|
|
14
|
+
// bundles the transport, config, and Party client — provide its `sui`/`graphqlRaw`
|
|
15
|
+
// as the Effect services once, at your program's boundary:
|
|
13
16
|
//
|
|
14
17
|
// const miso = createMisoClient({ network: "testnet" })
|
|
15
|
-
// const
|
|
18
|
+
// const layer = Layer.mergeAll(SuiClient.layer(miso.sui), SuiGraphQL.layer(miso.graphqlRaw))
|
|
19
|
+
// const pressing = await Effect.runPromise(
|
|
20
|
+
// getPressingDetail(pressingId, miso.config).pipe(Effect.provide(layer)),
|
|
21
|
+
// )
|
|
16
22
|
|
|
17
23
|
export { createMisoClient } from "./client.ts";
|
|
18
24
|
export type { MisoClient, CreateMisoClientOptions } from "./client.ts";
|
package/src/read/receipts.ts
CHANGED
|
@@ -26,10 +26,13 @@ import {
|
|
|
26
26
|
getCompositionsByIds,
|
|
27
27
|
} from "@misofm/musicos";
|
|
28
28
|
import { normalizeStructTag, normalizeSuiAddress } from "@mysten/sui/utils";
|
|
29
|
+
import { Effect } from "effect";
|
|
30
|
+
import { SuiClient, SuiGraphQL, SuiRpcError, type BcsDecodeError, type ObjectTypeMismatchError } from "@misofm/effect";
|
|
29
31
|
import { getPressingDetail } from "./catalog.ts";
|
|
30
|
-
import type { MisoClient } from "./client.ts";
|
|
31
32
|
import { int } from "./internal/scalars.ts";
|
|
32
33
|
import { requireRecordSalesDeployment } from "../deployments.ts";
|
|
34
|
+
import { MalformedRecordSoldEventError, ReceiptNotFoundError, ReleaseNotFoundError, RecordPurchaseNotFoundError } from "../errors.ts";
|
|
35
|
+
import type { MisoConfig } from "./config.ts";
|
|
33
36
|
import type { Price, PressingDetail, PurchaseReceipt, RecordSale, TrackRoyalty } from "./types.ts";
|
|
34
37
|
import { getWorkAddressesByShareTypes } from "./works.ts";
|
|
35
38
|
|
|
@@ -46,10 +49,6 @@ const RECORD_SOLD_EVENT_NAME = "listing::RecordSoldEvent";
|
|
|
46
49
|
*/
|
|
47
50
|
const FULLNODE_WAIT_MS = 5_000;
|
|
48
51
|
|
|
49
|
-
export class MalformedRecordSoldEventError extends Error {
|
|
50
|
-
override readonly name = "MalformedRecordSoldEventError";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
52
|
/** Composition royalty rate (bps) per recording id. Sparse — an unresolved parent has no entry. */
|
|
54
53
|
type CompositionRates = Record<string, number | undefined>;
|
|
55
54
|
|
|
@@ -225,7 +224,7 @@ export function findRecordSales(
|
|
|
225
224
|
!pricing || embeddedCurrency !== currencyType ||
|
|
226
225
|
!validPricingRelationship(purchasePrice, pricing) ||
|
|
227
226
|
s.edition <= 0 || s.number <= 0
|
|
228
|
-
) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent");
|
|
227
|
+
) throw new MalformedRecordSoldEventError({ reason: "malformed canonical RecordSoldEvent" });
|
|
229
228
|
sales.push({
|
|
230
229
|
listingId: s.listing_id,
|
|
231
230
|
pressingId: s.pressing_id,
|
|
@@ -245,13 +244,13 @@ export function findRecordSales(
|
|
|
245
244
|
// Never reinterpret malformed canonical BCS through a looser JSON view.
|
|
246
245
|
if (e.bcs.length > 0) {
|
|
247
246
|
if (error instanceof MalformedRecordSoldEventError) throw error;
|
|
248
|
-
throw new MalformedRecordSoldEventError(
|
|
249
|
-
`malformed canonical RecordSoldEvent BCS: ${error instanceof Error ? error.message : String(error)}`,
|
|
250
|
-
);
|
|
247
|
+
throw new MalformedRecordSoldEventError({
|
|
248
|
+
reason: `malformed canonical RecordSoldEvent BCS: ${error instanceof Error ? error.message : String(error)}`,
|
|
249
|
+
});
|
|
251
250
|
}
|
|
252
251
|
}
|
|
253
252
|
const fromJson = e.json && saleFromJson(e.json, currencyType);
|
|
254
|
-
if (!fromJson) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent JSON");
|
|
253
|
+
if (!fromJson) throw new MalformedRecordSoldEventError({ reason: "malformed canonical RecordSoldEvent JSON" });
|
|
255
254
|
sales.push(fromJson);
|
|
256
255
|
}
|
|
257
256
|
return sales;
|
|
@@ -270,23 +269,37 @@ export function findRecordSale(
|
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
/** Every canonical sale, read from the fullnode in event order. */
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
const salesFromFullnode = Effect.fn("salesFromFullnode")(function* (
|
|
273
|
+
txDigest: string,
|
|
274
|
+
config: MisoConfig,
|
|
275
|
+
): Effect.fn.Return<
|
|
276
|
+
RecordSale[],
|
|
277
|
+
ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
|
|
278
|
+
SuiClient
|
|
279
|
+
> {
|
|
280
|
+
const client = yield* SuiClient;
|
|
281
|
+
const result = yield* Effect.tryPromise({
|
|
282
|
+
try: (signal) =>
|
|
283
|
+
client.core.waitForTransaction({
|
|
284
|
+
digest: txDigest,
|
|
285
|
+
include: { events: true },
|
|
286
|
+
timeout: FULLNODE_WAIT_MS,
|
|
287
|
+
signal,
|
|
288
|
+
}),
|
|
289
|
+
catch: (cause) => new SuiRpcError({ operation: "waitForTransaction", cause }),
|
|
278
290
|
});
|
|
279
291
|
if (result.$kind !== "Transaction" || !result.Transaction.status.success) {
|
|
280
|
-
|
|
292
|
+
return yield* new ReceiptNotFoundError({ digest: txDigest });
|
|
281
293
|
}
|
|
282
|
-
const sales = requireRecordSalesDeployment(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
sales.recordShopPackageId,
|
|
286
|
-
|
|
287
|
-
|
|
294
|
+
const sales = requireRecordSalesDeployment(config.recordSales);
|
|
295
|
+
// `findRecordSales` only ever throws `MalformedRecordSoldEventError`.
|
|
296
|
+
const found = yield* Effect.try({
|
|
297
|
+
try: () => findRecordSales(result.Transaction.events, sales.recordShopPackageId),
|
|
298
|
+
catch: (cause) => cause as MalformedRecordSoldEventError,
|
|
299
|
+
});
|
|
300
|
+
if (found.length === 0) return yield* new RecordPurchaseNotFoundError({ digest: txDigest });
|
|
288
301
|
return found;
|
|
289
|
-
}
|
|
302
|
+
});
|
|
290
303
|
|
|
291
304
|
const TX_EVENTS_QUERY = `query TransactionEvents($digest: String!) {
|
|
292
305
|
transaction(digest: $digest) {
|
|
@@ -307,18 +320,35 @@ interface TxEventsResult {
|
|
|
307
320
|
}
|
|
308
321
|
|
|
309
322
|
/** Every canonical sale, read from the GraphQL indexer in event order. */
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
const salesFromIndexer = Effect.fn("salesFromIndexer")(function* (
|
|
324
|
+
txDigest: string,
|
|
325
|
+
config: MisoConfig,
|
|
326
|
+
): Effect.fn.Return<
|
|
327
|
+
RecordSale[],
|
|
328
|
+
ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
|
|
329
|
+
SuiGraphQL
|
|
330
|
+
> {
|
|
331
|
+
const client = yield* SuiGraphQL;
|
|
332
|
+
const { data, errors } = yield* Effect.tryPromise({
|
|
333
|
+
try: () =>
|
|
334
|
+
client.query<TxEventsResult, { digest: string }>({
|
|
335
|
+
query: TX_EVENTS_QUERY,
|
|
336
|
+
variables: { digest: txDigest },
|
|
337
|
+
}),
|
|
338
|
+
catch: (cause) => new SuiRpcError({ operation: "transactionEvents", cause }),
|
|
314
339
|
});
|
|
315
|
-
if (errors?.length)
|
|
340
|
+
if (errors?.length) {
|
|
341
|
+
return yield* new SuiRpcError({
|
|
342
|
+
operation: "transactionEvents",
|
|
343
|
+
cause: new AggregateError(errors.map((e) => new Error(e.message)), "Transaction events query failed"),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
316
346
|
|
|
317
347
|
const effects = data?.transaction?.effects;
|
|
318
|
-
if (!effects)
|
|
319
|
-
if (effects.status !== "SUCCESS")
|
|
348
|
+
if (!effects) return yield* new ReceiptNotFoundError({ digest: txDigest });
|
|
349
|
+
if (effects.status !== "SUCCESS") return yield* new ReceiptNotFoundError({ digest: txDigest });
|
|
320
350
|
|
|
321
|
-
const salesDeployment = requireRecordSalesDeployment(
|
|
351
|
+
const salesDeployment = requireRecordSalesDeployment(config.recordSales);
|
|
322
352
|
const sales: RecordSale[] = [];
|
|
323
353
|
for (const node of effects.events?.nodes ?? []) {
|
|
324
354
|
const contents = node.contents;
|
|
@@ -332,19 +362,29 @@ async function salesFromIndexer(client: MisoClient, txDigest: string): Promise<R
|
|
|
332
362
|
typeof contents.json === "object" && contents.json !== null
|
|
333
363
|
? saleFromJson(contents.json as Record<string, unknown>, currencyType)
|
|
334
364
|
: null;
|
|
335
|
-
if (!sale)
|
|
365
|
+
if (!sale) return yield* new MalformedRecordSoldEventError({ digest: txDigest, reason: "malformed canonical RecordSoldEvent JSON" });
|
|
336
366
|
sales.push(sale);
|
|
337
367
|
}
|
|
338
368
|
if (sales.length > 0) return sales;
|
|
339
|
-
|
|
340
|
-
}
|
|
369
|
+
return yield* new RecordPurchaseNotFoundError({ digest: txDigest });
|
|
370
|
+
});
|
|
341
371
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
372
|
+
const salesFromChain = Effect.fn("salesFromChain")(function* (
|
|
373
|
+
txDigest: string,
|
|
374
|
+
config: MisoConfig,
|
|
375
|
+
): Effect.fn.Return<
|
|
376
|
+
RecordSale[],
|
|
377
|
+
ReceiptNotFoundError | RecordPurchaseNotFoundError | MalformedRecordSoldEventError | SuiRpcError,
|
|
378
|
+
SuiClient | SuiGraphQL
|
|
379
|
+
> {
|
|
380
|
+
return yield* salesFromFullnode(txDigest, config).pipe(
|
|
381
|
+
Effect.catchIf(
|
|
382
|
+
(error): error is ReceiptNotFoundError | RecordPurchaseNotFoundError | SuiRpcError =>
|
|
383
|
+
error._tag !== "MalformedRecordSoldEventError",
|
|
384
|
+
() => salesFromIndexer(txDigest, config),
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
348
388
|
|
|
349
389
|
/**
|
|
350
390
|
* Each recording's parent composition royalty rate, in basis points, keyed by
|
|
@@ -357,11 +397,18 @@ async function salesFromChain(client: MisoClient, txDigest: string): Promise<Rec
|
|
|
357
397
|
* Best-effort by design: any failure returns the rates resolved so far, and the
|
|
358
398
|
* receipt drops to a track-level breakdown rather than failing the whole page.
|
|
359
399
|
*/
|
|
360
|
-
|
|
400
|
+
const compositionRatesByRecording = Effect.fn("compositionRatesByRecording")(function* (
|
|
401
|
+
recordingIds: string[],
|
|
402
|
+
config: MisoConfig,
|
|
403
|
+
): Effect.fn.Return<CompositionRates, SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
|
|
361
404
|
if (recordingIds.length === 0) return {};
|
|
362
405
|
|
|
406
|
+
const client = yield* SuiClient;
|
|
363
407
|
const shareTypeByRecording: Record<string, string> = {};
|
|
364
|
-
const { objects } =
|
|
408
|
+
const { objects } = yield* Effect.tryPromise({
|
|
409
|
+
try: (signal) => client.core.getObjects({ objectIds: [...new Set(recordingIds)], signal }),
|
|
410
|
+
catch: (cause) => new SuiRpcError({ operation: "getObjects", cause }),
|
|
411
|
+
});
|
|
365
412
|
for (const obj of objects) {
|
|
366
413
|
if (obj instanceof Error || !obj.type) continue;
|
|
367
414
|
try {
|
|
@@ -375,13 +422,12 @@ async function compositionRatesByRecording(client: MisoClient, recordingIds: str
|
|
|
375
422
|
const shareTypes = Object.values(shareTypeByRecording);
|
|
376
423
|
if (shareTypes.length === 0) return {};
|
|
377
424
|
|
|
378
|
-
const addresses =
|
|
379
|
-
client.graphql,
|
|
425
|
+
const addresses = yield* getWorkAddressesByShareTypes(
|
|
380
426
|
{ compositions: shareTypes, recordings: [] },
|
|
381
|
-
|
|
427
|
+
config.deployment.musicos,
|
|
382
428
|
);
|
|
383
429
|
const compositionIds = Object.values(addresses.compositions).filter((id): id is string => !!id);
|
|
384
|
-
const compositions =
|
|
430
|
+
const compositions = yield* getCompositionsByIds(compositionIds);
|
|
385
431
|
|
|
386
432
|
const rates: CompositionRates = {};
|
|
387
433
|
for (const [recordingId, shareType] of Object.entries(shareTypeByRecording)) {
|
|
@@ -390,7 +436,7 @@ async function compositionRatesByRecording(client: MisoClient, recordingIds: str
|
|
|
390
436
|
if (composition) rates[recordingId] = int(composition.royaltyRate.value);
|
|
391
437
|
}
|
|
392
438
|
return rates;
|
|
393
|
-
}
|
|
439
|
+
});
|
|
394
440
|
|
|
395
441
|
/**
|
|
396
442
|
* Split `paid` across the release's tracks by their `splitBps`, then split each
|
|
@@ -428,18 +474,31 @@ export function breakdown(
|
|
|
428
474
|
* The sale identifies the Pressing before its detail is fetched. `null` only when
|
|
429
475
|
* that immutable Pressing cannot be read; it is never superseded or destroyed.
|
|
430
476
|
*/
|
|
431
|
-
|
|
432
|
-
|
|
477
|
+
type ReceiptError =
|
|
478
|
+
| ReceiptNotFoundError
|
|
479
|
+
| RecordPurchaseNotFoundError
|
|
480
|
+
| MalformedRecordSoldEventError
|
|
481
|
+
| ObjectTypeMismatchError
|
|
482
|
+
| ReleaseNotFoundError
|
|
483
|
+
| BcsDecodeError
|
|
484
|
+
| SuiRpcError;
|
|
485
|
+
|
|
486
|
+
const hydratePurchaseReceipt = Effect.fn("hydratePurchaseReceipt")(function* (
|
|
433
487
|
sale: RecordSale,
|
|
434
|
-
|
|
435
|
-
|
|
488
|
+
config: MisoConfig,
|
|
489
|
+
): Effect.fn.Return<
|
|
490
|
+
PurchaseReceipt | null,
|
|
491
|
+
ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
|
|
492
|
+
SuiClient | SuiGraphQL
|
|
493
|
+
> {
|
|
494
|
+
const detail = yield* getPressingDetail(sale.pressingId, config);
|
|
436
495
|
if (!detail) return null;
|
|
437
496
|
|
|
438
497
|
// Best-effort: a failed composition lookup costs the sub-rows, not the page.
|
|
439
|
-
const rates =
|
|
440
|
-
client,
|
|
498
|
+
const rates = yield* compositionRatesByRecording(
|
|
441
499
|
detail.release.tracks.map((t) => t.recordingId),
|
|
442
|
-
|
|
500
|
+
config,
|
|
501
|
+
).pipe(Effect.catch(() => Effect.succeed({} as CompositionRates)));
|
|
443
502
|
|
|
444
503
|
return {
|
|
445
504
|
sale,
|
|
@@ -447,30 +506,32 @@ async function hydratePurchaseReceipt(
|
|
|
447
506
|
price: sale.pricing.amount,
|
|
448
507
|
tracks: breakdown(detail.release.tracks, sale.purchasePrice, rates),
|
|
449
508
|
};
|
|
450
|
-
}
|
|
509
|
+
});
|
|
451
510
|
|
|
452
511
|
/** Read and hydrate every canonical Record sale in transaction event order. */
|
|
453
|
-
export
|
|
454
|
-
client: MisoClient,
|
|
512
|
+
export const getPurchaseReceipts = Effect.fn("getPurchaseReceipts")(function* (
|
|
455
513
|
txDigest: string,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
514
|
+
config: MisoConfig,
|
|
515
|
+
): Effect.fn.Return<PurchaseReceipt[], ReceiptError, SuiClient | SuiGraphQL> {
|
|
516
|
+
const sales = yield* salesFromChain(txDigest, config);
|
|
517
|
+
const hydrated = yield* Effect.forEach(sales, (sale) => hydratePurchaseReceipt(sale, config), {
|
|
518
|
+
concurrency: "unbounded",
|
|
519
|
+
});
|
|
459
520
|
if (hydrated.some((receipt) => receipt === null)) {
|
|
460
521
|
throw new Error(`A canonical Record sale in transaction ${txDigest} references an unreadable Pressing`);
|
|
461
522
|
}
|
|
462
523
|
return hydrated as PurchaseReceipt[];
|
|
463
|
-
}
|
|
524
|
+
});
|
|
464
525
|
|
|
465
526
|
/** Read one canonical receipt selected by its Record ID. Selection is mandatory:
|
|
466
527
|
* one transaction may purchase multiple Records from the same Pressing. */
|
|
467
|
-
export
|
|
468
|
-
client: MisoClient,
|
|
528
|
+
export const getPurchaseReceipt = Effect.fn("getPurchaseReceipt")(function* (
|
|
469
529
|
txDigest: string,
|
|
470
530
|
recordId: string,
|
|
471
|
-
|
|
472
|
-
|
|
531
|
+
config: MisoConfig,
|
|
532
|
+
): Effect.fn.Return<PurchaseReceipt | null, ReceiptError, SuiClient | SuiGraphQL> {
|
|
533
|
+
const sales = yield* salesFromChain(txDigest, config);
|
|
473
534
|
const wanted = normalizeSuiAddress(recordId);
|
|
474
535
|
const sale = sales.find((candidate) => normalizeSuiAddress(candidate.recordId) === wanted);
|
|
475
|
-
return sale ? hydratePurchaseReceipt(
|
|
476
|
-
}
|
|
536
|
+
return sale ? yield* hydratePurchaseReceipt(sale, config) : null;
|
|
537
|
+
});
|