@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// The bigint→JSON boundary. Every u64/u128 the protocol SDKs hand back arrives as
|
|
5
|
+
// a `bigint`, which `JSON.stringify` throws on; these convert once, here, so no
|
|
6
|
+
// view type can carry one out of the SDK (see the header of ../types.ts).
|
|
7
|
+
|
|
8
|
+
/** A u64/u128 as it may arrive off the wire, in BCS, or from a JSON projection. */
|
|
9
|
+
export type NumericLike = bigint | number | string | null | undefined;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base-unit scalar → decimal string. Returns `"0"` for anything unreadable, which
|
|
13
|
+
* is the honest projection of "the chain gave us no number" for quantities and
|
|
14
|
+
* amounts. Use {@link u64OrNull} where absent must stay distinguishable from zero.
|
|
15
|
+
*/
|
|
16
|
+
export function u64(v: NumericLike): string {
|
|
17
|
+
return u64OrNull(v) ?? "0";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Base-unit scalar → decimal string, or `null` when absent/unparseable. */
|
|
21
|
+
export function u64OrNull(v: NumericLike): string | null {
|
|
22
|
+
if (v == null) return null;
|
|
23
|
+
if (typeof v === "bigint") return v.toString();
|
|
24
|
+
if (typeof v === "number") return Number.isFinite(v) ? BigInt(Math.trunc(v)).toString() : null;
|
|
25
|
+
const s = v.trim();
|
|
26
|
+
return /^\d+$/.test(s) ? BigInt(s).toString() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A millisecond timestamp → `number`. Milliseconds stay numbers (they are far
|
|
31
|
+
* inside Number.MAX_SAFE_INTEGER and `new Date()` wants one), unlike token
|
|
32
|
+
* amounts which must not lose precision.
|
|
33
|
+
*/
|
|
34
|
+
export function msOrNull(v: NumericLike): number | null {
|
|
35
|
+
const s = u64OrNull(v);
|
|
36
|
+
if (s === null) return null;
|
|
37
|
+
const n = Number(s);
|
|
38
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** As {@link msOrNull}, defaulting to `0` (the epoch) when absent. */
|
|
42
|
+
export function ms(v: NumericLike): number {
|
|
43
|
+
return msOrNull(v) ?? 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A plain integer field (bps, edition, counts) → `number`, defaulting to `0`. */
|
|
47
|
+
export function int(v: NumericLike): number {
|
|
48
|
+
if (typeof v === "number" && Number.isFinite(v)) return Math.trunc(v);
|
|
49
|
+
if (typeof v === "bigint") return Number(v);
|
|
50
|
+
if (typeof v === "string" && /^-?\d+$/.test(v.trim())) return Number(v.trim());
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Integer comparison over two decimal strings — `a >= b` without going through Number. */
|
|
55
|
+
export function gte(a: string, b: string): boolean {
|
|
56
|
+
return BigInt(a) >= BigInt(b);
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { bcs } from "@mysten/sui/bcs";
|
|
5
|
+
import { toBase64 } from "@mysten/sui/utils";
|
|
6
|
+
|
|
7
|
+
const u256 = bcs.u256();
|
|
8
|
+
|
|
9
|
+
function toBase64Url(bytes: Uint8Array): string {
|
|
10
|
+
return toBase64(bytes).replace(/=*$/, "").replaceAll("+", "-").replaceAll("/", "_");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function u256ToB64Url(value: bigint | string): string {
|
|
14
|
+
return toBase64Url(u256.serialize(BigInt(value)).toBytes());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Build Walrus's 37-byte quilt-patch id. All fields use BCS little-endian. */
|
|
18
|
+
export function quiltPatchId(
|
|
19
|
+
quiltId: bigint | string,
|
|
20
|
+
version: number,
|
|
21
|
+
startIndex: number,
|
|
22
|
+
endIndex: number,
|
|
23
|
+
): string {
|
|
24
|
+
const bytes = new Uint8Array(37);
|
|
25
|
+
bytes.set(u256.serialize(BigInt(quiltId)).toBytes(), 0);
|
|
26
|
+
bytes[32] = version;
|
|
27
|
+
bytes[33] = startIndex & 0xff;
|
|
28
|
+
bytes[34] = (startIndex >> 8) & 0xff;
|
|
29
|
+
bytes[35] = endIndex & 0xff;
|
|
30
|
+
bytes[36] = (endIndex >> 8) & 0xff;
|
|
31
|
+
return toBase64Url(bytes);
|
|
32
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// What one record sale actually was, read back from its transaction.
|
|
5
|
+
//
|
|
6
|
+
// The transaction digest is the receipt's identity: everything shown is
|
|
7
|
+
// re-derived from chain state, so a receipt link survives a refresh, a deep link,
|
|
8
|
+
// or a share to another device. `drop::buy` emits ONE `RecordSoldEvent` carrying
|
|
9
|
+
// the copy that was minted, its number in the run, and what the buyer paid.
|
|
10
|
+
//
|
|
11
|
+
// Two reads, in order, because a receipt is opened at two very different ages:
|
|
12
|
+
// fullnode the fresh path — usually the instant the transaction lands, possibly
|
|
13
|
+
// before this node has it, hence `waitForTransaction`.
|
|
14
|
+
// indexer the old path — GraphQL keeps transactions the fullnode has pruned.
|
|
15
|
+
// This is what makes a months-old receipt link still open.
|
|
16
|
+
//
|
|
17
|
+
// Where the money goes is PRESENTATIONAL, not a second on-chain fact: `buy`
|
|
18
|
+
// forwards the whole payment to the release's funds accumulator (there is no
|
|
19
|
+
// platform fee), and the royalty layer splits it downstream by the same terms this
|
|
20
|
+
// read walks. We show the buyer that arithmetic, in the same truncating integer
|
|
21
|
+
// math the chain does.
|
|
22
|
+
|
|
23
|
+
import * as dropContract from "../contracts/miso_drop/drop.ts";
|
|
24
|
+
import {
|
|
25
|
+
extractTypeParams2,
|
|
26
|
+
getCompositionsByIds,
|
|
27
|
+
} from "@misonetwork/sdk";
|
|
28
|
+
import { getPressingDetail } from "./catalog.ts";
|
|
29
|
+
import type { MisoClient } from "./client.ts";
|
|
30
|
+
import { int, u64 } from "./internal/scalars.ts";
|
|
31
|
+
import type { PressingDetail, PurchaseReceipt, RecordSale, TrackRoyalty } from "./types.ts";
|
|
32
|
+
import { getWorkAddressesByShareTypes } from "./works.ts";
|
|
33
|
+
|
|
34
|
+
const BPS = 10_000n;
|
|
35
|
+
|
|
36
|
+
/** `…::drop::RecordSoldEvent<Currency>` — the one event `drop::buy` emits. */
|
|
37
|
+
const RECORD_SOLD_EVENT = "::drop::RecordSoldEvent";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How long to wait for the fullnode to have the transaction before falling back
|
|
41
|
+
* to the indexer. Propagation is p95 ~330ms, so this only ever runs long for a
|
|
42
|
+
* digest the node genuinely doesn't have.
|
|
43
|
+
*/
|
|
44
|
+
const FULLNODE_WAIT_MS = 5_000;
|
|
45
|
+
|
|
46
|
+
/** Composition royalty rate (bps) per recording id. Sparse — an unresolved parent has no entry. */
|
|
47
|
+
type CompositionRates = Record<string, number | undefined>;
|
|
48
|
+
|
|
49
|
+
function coerceBigInt(v: unknown): bigint | null {
|
|
50
|
+
if (typeof v === "bigint") return v;
|
|
51
|
+
if (typeof v === "number" && Number.isFinite(v)) return BigInt(Math.trunc(v));
|
|
52
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return BigInt(v);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The event's JSON projection — the fallback for transports that surface `json`
|
|
58
|
+
* but no BCS. Field names are the Move ones (snake_case).
|
|
59
|
+
*/
|
|
60
|
+
function saleFromJson(json: Record<string, unknown>): RecordSale | null {
|
|
61
|
+
const number = coerceBigInt(json.number);
|
|
62
|
+
const paid = coerceBigInt(json.paid);
|
|
63
|
+
const recordId = json.record_id;
|
|
64
|
+
if (number == null || paid == null || typeof recordId !== "string") return null;
|
|
65
|
+
return {
|
|
66
|
+
dropId: typeof json.drop_id === "string" ? json.drop_id : "",
|
|
67
|
+
releaseId: typeof json.release_id === "string" ? json.release_id : "",
|
|
68
|
+
edition: int(coerceBigInt(json.edition) ?? 0n),
|
|
69
|
+
recordId,
|
|
70
|
+
number: number.toString(),
|
|
71
|
+
paid: paid.toString(),
|
|
72
|
+
buyer: typeof json.buyer === "string" ? json.buyer : "",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The sale a transaction recorded, or null if it emitted none. BCS is the stable
|
|
78
|
+
* shape (a JSON projection's field names vary by transport), so it is tried first.
|
|
79
|
+
*/
|
|
80
|
+
function findSale(events: { eventType: string; bcs: Uint8Array; json: Record<string, unknown> | null }[]): RecordSale | null {
|
|
81
|
+
for (const e of events) {
|
|
82
|
+
if (!e.eventType.includes(RECORD_SOLD_EVENT)) continue;
|
|
83
|
+
try {
|
|
84
|
+
// The generated struct decodes addresses to 0x-hex and u64s to decimal strings.
|
|
85
|
+
const s = dropContract.RecordSoldEvent.parse(e.bcs);
|
|
86
|
+
return {
|
|
87
|
+
dropId: s.drop_id,
|
|
88
|
+
releaseId: s.release_id,
|
|
89
|
+
edition: int(s.edition),
|
|
90
|
+
recordId: s.record_id,
|
|
91
|
+
number: u64(s.number),
|
|
92
|
+
paid: u64(s.paid),
|
|
93
|
+
buyer: s.buyer,
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
// Unparseable BCS (or none surfaced) — fall through to the JSON view.
|
|
97
|
+
}
|
|
98
|
+
const fromJson = e.json && saleFromJson(e.json);
|
|
99
|
+
if (fromJson) return fromJson;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The sale, read from the fullnode. */
|
|
105
|
+
async function saleFromFullnode(client: MisoClient, txDigest: string): Promise<RecordSale> {
|
|
106
|
+
const result = await client.sui.waitForTransaction({
|
|
107
|
+
digest: txDigest,
|
|
108
|
+
include: { events: true },
|
|
109
|
+
timeout: FULLNODE_WAIT_MS,
|
|
110
|
+
});
|
|
111
|
+
if (result.$kind !== "Transaction" || !result.Transaction.status.success) {
|
|
112
|
+
throw new Error(`Transaction did not succeed: ${txDigest}`);
|
|
113
|
+
}
|
|
114
|
+
const sale = findSale(result.Transaction.events);
|
|
115
|
+
if (!sale) throw new Error(`No record purchase in transaction ${txDigest}`);
|
|
116
|
+
return sale;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const TX_EVENTS_QUERY = `query TransactionEvents($digest: String!) {
|
|
120
|
+
transaction(digest: $digest) {
|
|
121
|
+
effects {
|
|
122
|
+
status
|
|
123
|
+
events { nodes { contents { type { repr } json } } }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}`;
|
|
127
|
+
|
|
128
|
+
interface TxEventsResult {
|
|
129
|
+
transaction: {
|
|
130
|
+
effects: {
|
|
131
|
+
status: string;
|
|
132
|
+
events: { nodes: { contents: { type: { repr: string }; json: unknown } | null }[] } | null;
|
|
133
|
+
} | null;
|
|
134
|
+
} | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The sale, read from the GraphQL indexer — the path for a pruned transaction. */
|
|
138
|
+
async function saleFromIndexer(client: MisoClient, txDigest: string): Promise<RecordSale> {
|
|
139
|
+
const { data, errors } = await client.graphqlRaw.query<TxEventsResult, { digest: string }>({
|
|
140
|
+
query: TX_EVENTS_QUERY,
|
|
141
|
+
variables: { digest: txDigest },
|
|
142
|
+
});
|
|
143
|
+
if (errors?.length) throw new Error(errors[0]!.message);
|
|
144
|
+
|
|
145
|
+
const effects = data?.transaction?.effects;
|
|
146
|
+
if (!effects) throw new Error(`Transaction not found: ${txDigest}`);
|
|
147
|
+
if (effects.status !== "SUCCESS") throw new Error(`Transaction did not succeed: ${txDigest}`);
|
|
148
|
+
|
|
149
|
+
for (const node of effects.events?.nodes ?? []) {
|
|
150
|
+
const contents = node.contents;
|
|
151
|
+
if (!contents?.type.repr.includes(RECORD_SOLD_EVENT)) continue;
|
|
152
|
+
const sale =
|
|
153
|
+
typeof contents.json === "object" && contents.json !== null
|
|
154
|
+
? saleFromJson(contents.json as Record<string, unknown>)
|
|
155
|
+
: null;
|
|
156
|
+
if (sale) return sale;
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`No record purchase in transaction ${txDigest}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Each recording's parent composition royalty rate, in basis points, keyed by
|
|
163
|
+
* recording id.
|
|
164
|
+
*
|
|
165
|
+
* A `Recording<RecordingShare, CompositionShare>` names its parent by SHARE TYPE,
|
|
166
|
+
* not by id, so the hop is: recording type tag → composition share type →
|
|
167
|
+
* (GraphQL) composition id → (Core) the composition's `royaltyRate`.
|
|
168
|
+
*
|
|
169
|
+
* Best-effort by design: any failure returns the rates resolved so far, and the
|
|
170
|
+
* receipt drops to a track-level breakdown rather than failing the whole page.
|
|
171
|
+
*/
|
|
172
|
+
async function compositionRatesByRecording(client: MisoClient, recordingIds: string[]): Promise<CompositionRates> {
|
|
173
|
+
if (recordingIds.length === 0) return {};
|
|
174
|
+
|
|
175
|
+
const shareTypeByRecording: Record<string, string> = {};
|
|
176
|
+
const { objects } = await client.protocol.core.getObjects({ objectIds: [...new Set(recordingIds)] });
|
|
177
|
+
for (const obj of objects) {
|
|
178
|
+
if (obj instanceof Error || !obj.type) continue;
|
|
179
|
+
try {
|
|
180
|
+
const [, compositionShareType] = extractTypeParams2(obj.type);
|
|
181
|
+
shareTypeByRecording[obj.objectId] = compositionShareType;
|
|
182
|
+
} catch {
|
|
183
|
+
// A recording type we can't read the parent off — skip this track.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const shareTypes = Object.values(shareTypeByRecording);
|
|
188
|
+
if (shareTypes.length === 0) return {};
|
|
189
|
+
|
|
190
|
+
const addresses = await getWorkAddressesByShareTypes(
|
|
191
|
+
client.graphql,
|
|
192
|
+
{ compositions: shareTypes, recordings: [] },
|
|
193
|
+
client.config.protocol.miso,
|
|
194
|
+
);
|
|
195
|
+
const compositionIds = Object.values(addresses.compositions).filter((id): id is string => !!id);
|
|
196
|
+
const compositions = await getCompositionsByIds(client.protocol, compositionIds);
|
|
197
|
+
|
|
198
|
+
const rates: CompositionRates = {};
|
|
199
|
+
for (const [recordingId, shareType] of Object.entries(shareTypeByRecording)) {
|
|
200
|
+
const compositionId = addresses.compositions[shareType];
|
|
201
|
+
const composition = compositionId ? compositions[compositionId] : undefined;
|
|
202
|
+
if (composition) rates[recordingId] = int(composition.royaltyRate.value);
|
|
203
|
+
}
|
|
204
|
+
return rates;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Split `paid` across the release's tracks by their `splitBps`, then split each
|
|
209
|
+
* track's share between its composition and its recording. All BigInt — the same
|
|
210
|
+
* truncating integer math the on-chain royalty layer does, so the numbers shown
|
|
211
|
+
* are the numbers paid.
|
|
212
|
+
*/
|
|
213
|
+
export function breakdown(
|
|
214
|
+
tracks: PressingDetail["release"]["tracks"],
|
|
215
|
+
paid: string,
|
|
216
|
+
compositionRates: CompositionRates,
|
|
217
|
+
): TrackRoyalty[] {
|
|
218
|
+
const paidUnits = BigInt(paid);
|
|
219
|
+
return tracks.map((track) => {
|
|
220
|
+
const amount = (paidUnits * BigInt(track.splitBps)) / BPS;
|
|
221
|
+
const rate = compositionRates[track.recordingId];
|
|
222
|
+
const base = {
|
|
223
|
+
no: track.no,
|
|
224
|
+
title: track.title,
|
|
225
|
+
recordingId: track.recordingId,
|
|
226
|
+
splitBps: track.splitBps,
|
|
227
|
+
amount: amount.toString(),
|
|
228
|
+
};
|
|
229
|
+
if (rate == null) return { ...base, composition: null, recording: null };
|
|
230
|
+
const composition = (amount * BigInt(rate)) / BPS;
|
|
231
|
+
return { ...base, composition: composition.toString(), recording: (amount - composition).toString() };
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The receipt for `txDigest`, a purchase from `pressingId`.
|
|
237
|
+
*
|
|
238
|
+
* The sale and the pressing detail are fetched together — arriving from the buy
|
|
239
|
+
* button costs one round of work, not two. `null` when the pressing no longer
|
|
240
|
+
* exists (a superseded edition), which the caller renders as "not found" rather
|
|
241
|
+
* than as an error.
|
|
242
|
+
*/
|
|
243
|
+
export async function getPurchaseReceipt(
|
|
244
|
+
client: MisoClient,
|
|
245
|
+
pressingId: string,
|
|
246
|
+
txDigest: string,
|
|
247
|
+
): Promise<PurchaseReceipt | null> {
|
|
248
|
+
const [sale, detail] = await Promise.all([
|
|
249
|
+
saleFromFullnode(client, txDigest).catch(() => saleFromIndexer(client, txDigest)),
|
|
250
|
+
getPressingDetail(client, pressingId),
|
|
251
|
+
]);
|
|
252
|
+
if (!detail) return null;
|
|
253
|
+
|
|
254
|
+
// Best-effort: a failed composition lookup costs the sub-rows, not the page.
|
|
255
|
+
const rates = await compositionRatesByRecording(
|
|
256
|
+
client,
|
|
257
|
+
detail.release.tracks.map((t) => t.recordingId),
|
|
258
|
+
).catch(() => ({}));
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
sale,
|
|
262
|
+
detail,
|
|
263
|
+
price: detail.pressing.price.amount,
|
|
264
|
+
tracks: breakdown(detail.release.tracks, sale.paid, rates),
|
|
265
|
+
};
|
|
266
|
+
}
|