@misofm/sdk 0.15.11 → 0.16.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 +145 -53
- package/dist/client.d.ts +156 -48
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +289 -48
- package/dist/client.js.map +1 -1
- package/dist/contracts/composition_royalty_pool/composition_royalty_pool.d.ts +11 -11
- package/dist/contracts/composition_royalty_pool/composition_royalty_pool.d.ts.map +1 -1
- package/dist/contracts/composition_royalty_pool/composition_royalty_pool.js +9 -9
- package/dist/contracts/composition_royalty_pool/composition_royalty_pool.js.map +1 -1
- package/dist/contracts/miso_record/deps/std/type_name.d.ts.map +1 -0
- package/dist/contracts/miso_record/deps/std/type_name.js.map +1 -0
- package/dist/contracts/miso_record/deps/sui/vec_set.d.ts +21 -0
- package/dist/contracts/miso_record/deps/sui/vec_set.d.ts.map +1 -0
- package/dist/contracts/miso_record/deps/sui/vec_set.js +20 -0
- package/dist/contracts/miso_record/deps/sui/vec_set.js.map +1 -0
- package/dist/contracts/miso_record/pressing.d.ts +327 -0
- package/dist/contracts/miso_record/pressing.d.ts.map +1 -0
- package/dist/contracts/miso_record/pressing.js +345 -0
- package/dist/contracts/miso_record/pressing.js.map +1 -0
- package/dist/contracts/miso_record/record.d.ts +190 -0
- package/dist/contracts/miso_record/record.d.ts.map +1 -0
- package/dist/contracts/miso_record/record.js +226 -0
- package/dist/contracts/miso_record/record.js.map +1 -0
- package/dist/contracts/miso_record_shop/deps/std/type_name.d.ts +18 -0
- package/dist/contracts/miso_record_shop/deps/std/type_name.d.ts.map +1 -0
- package/dist/contracts/miso_record_shop/deps/std/type_name.js +20 -0
- package/dist/contracts/miso_record_shop/deps/std/type_name.js.map +1 -0
- package/dist/contracts/miso_record_shop/listing.d.ts +387 -0
- package/dist/contracts/miso_record_shop/listing.d.ts.map +1 -0
- package/dist/contracts/miso_record_shop/listing.js +380 -0
- package/dist/contracts/miso_record_shop/listing.js.map +1 -0
- package/dist/contracts/miso_record_shop/witness.d.ts +7 -0
- package/dist/contracts/miso_record_shop/witness.d.ts.map +1 -0
- package/dist/contracts/miso_record_shop/witness.js +9 -0
- package/dist/contracts/miso_record_shop/witness.js.map +1 -0
- package/dist/contracts/recording_royalty_pool/recording_royalty_pool.d.ts +10 -12
- package/dist/contracts/recording_royalty_pool/recording_royalty_pool.d.ts.map +1 -1
- package/dist/contracts/recording_royalty_pool/recording_royalty_pool.js +8 -10
- package/dist/contracts/recording_royalty_pool/recording_royalty_pool.js.map +1 -1
- package/dist/contracts.d.ts +3 -3
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +5 -4
- package/dist/contracts.js.map +1 -1
- package/dist/deployments.d.ts +37 -4
- package/dist/deployments.d.ts.map +1 -1
- package/dist/deployments.js +40 -2
- package/dist/deployments.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mix.d.ts +144 -0
- package/dist/mix.d.ts.map +1 -0
- package/dist/mix.js +656 -0
- package/dist/mix.js.map +1 -0
- package/dist/pressing.d.ts +72 -156
- package/dist/pressing.d.ts.map +1 -1
- package/dist/pressing.js +269 -261
- package/dist/pressing.js.map +1 -1
- package/dist/publication.d.ts +5 -2
- package/dist/publication.d.ts.map +1 -1
- package/dist/publication.js +57 -32
- package/dist/publication.js.map +1 -1
- package/dist/read/catalog.d.ts +8 -5
- package/dist/read/catalog.d.ts.map +1 -1
- package/dist/read/catalog.js +47 -35
- package/dist/read/catalog.js.map +1 -1
- package/dist/read/config.d.ts +6 -4
- package/dist/read/config.d.ts.map +1 -1
- package/dist/read/config.js +1 -2
- package/dist/read/config.js.map +1 -1
- package/dist/read/index.d.ts +1 -1
- package/dist/read/index.d.ts.map +1 -1
- package/dist/read/index.js +1 -1
- package/dist/read/index.js.map +1 -1
- package/dist/read/internal/scalars.d.ts.map +1 -1
- package/dist/read/internal/scalars.js +4 -3
- package/dist/read/internal/scalars.js.map +1 -1
- package/dist/read/internal/walrus.d.ts.map +1 -1
- package/dist/read/internal/walrus.js +2 -1
- package/dist/read/internal/walrus.js.map +1 -1
- package/dist/read/receipts.d.ts +21 -12
- package/dist/read/receipts.d.ts.map +1 -1
- package/dist/read/receipts.js +159 -56
- package/dist/read/receipts.js.map +1 -1
- package/dist/read/types.d.ts +34 -24
- package/dist/read/types.d.ts.map +1 -1
- package/dist/read/wallet.d.ts.map +1 -1
- package/dist/read/wallet.js +31 -46
- package/dist/read/wallet.js.map +1 -1
- package/dist/vault.d.ts +40 -11
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +94 -22
- package/dist/vault.js.map +1 -1
- package/package.json +10 -3
- package/src/client.ts +386 -72
- package/src/contracts/composition_royalty_pool/composition_royalty_pool.ts +15 -15
- package/src/contracts/miso_record/deps/sui/vec_set.ts +21 -0
- package/src/contracts/miso_record/pressing.ts +522 -0
- package/src/contracts/miso_record/record.ts +338 -0
- package/src/contracts/miso_record_shop/deps/std/type_name.ts +22 -0
- package/src/contracts/miso_record_shop/listing.ts +602 -0
- package/src/contracts/miso_record_shop/witness.ts +11 -0
- package/src/contracts/recording_royalty_pool/recording_royalty_pool.ts +14 -16
- package/src/contracts.ts +5 -4
- package/src/deployments.ts +131 -38
- package/src/index.ts +2 -0
- package/src/mix.ts +910 -0
- package/src/pressing.ts +428 -423
- package/src/publication.ts +62 -33
- package/src/read/catalog.ts +49 -34
- package/src/read/config.ts +7 -7
- package/src/read/index.ts +9 -1
- package/src/read/internal/scalars.ts +4 -2
- package/src/read/internal/walrus.ts +2 -1
- package/src/read/receipts.ts +181 -66
- package/src/read/types.ts +35 -22
- package/src/read/wallet.ts +29 -50
- package/src/vault.ts +142 -32
- package/dist/contracts/miso_pressing/certificate.d.ts +0 -89
- package/dist/contracts/miso_pressing/certificate.d.ts.map +0 -1
- package/dist/contracts/miso_pressing/certificate.js +0 -108
- package/dist/contracts/miso_pressing/certificate.js.map +0 -1
- package/dist/contracts/miso_pressing/deps/std/type_name.d.ts.map +0 -1
- package/dist/contracts/miso_pressing/deps/std/type_name.js.map +0 -1
- package/dist/contracts/miso_pressing/listing.d.ts +0 -434
- package/dist/contracts/miso_pressing/listing.d.ts.map +0 -1
- package/dist/contracts/miso_pressing/listing.js +0 -401
- package/dist/contracts/miso_pressing/listing.js.map +0 -1
- package/dist/contracts/miso_pressing/pressing.d.ts +0 -334
- package/dist/contracts/miso_pressing/pressing.d.ts.map +0 -1
- package/dist/contracts/miso_pressing/pressing.js +0 -298
- package/dist/contracts/miso_pressing/pressing.js.map +0 -1
- package/src/contracts/miso_pressing/certificate.ts +0 -164
- package/src/contracts/miso_pressing/listing.ts +0 -592
- package/src/contracts/miso_pressing/pressing.ts +0 -399
- /package/dist/contracts/{miso_pressing → miso_record}/deps/std/type_name.d.ts +0 -0
- /package/dist/contracts/{miso_pressing → miso_record}/deps/std/type_name.js +0 -0
- /package/src/contracts/{miso_pressing → miso_record}/deps/std/type_name.ts +0 -0
package/src/read/receipts.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// What one record sale actually was, read back from its transaction.
|
|
5
5
|
//
|
|
6
|
-
// The transaction digest is the receipt's identity: everything shown is
|
|
6
|
+
// The transaction digest plus Record ID is the receipt's identity: everything shown is
|
|
7
7
|
// re-derived from chain state, so a receipt link survives a refresh, a deep link,
|
|
8
|
-
// or a share to another device. `listing::
|
|
8
|
+
// or a share to another device. `listing::purchase` emits `RecordSoldEvent` carrying
|
|
9
9
|
// the copy that was minted, its number in the run, and what the buyer paid.
|
|
10
10
|
//
|
|
11
11
|
// Two reads, in order, because a receipt is opened at two very different ages:
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
// indexer the old path — GraphQL keeps transactions the fullnode has pruned.
|
|
15
15
|
// This is what makes a months-old receipt link still open.
|
|
16
16
|
//
|
|
17
|
-
// Where the money goes is PRESENTATIONAL, not a second on-chain fact: `
|
|
17
|
+
// Where the money goes is PRESENTATIONAL, not a second on-chain fact: `purchase`
|
|
18
18
|
// forwards the whole payment to the release's funds accumulator (there is no
|
|
19
19
|
// platform fee), and the royalty layer splits it downstream by the same terms this
|
|
20
20
|
// read walks. We show the buyer that arithmetic, in the same truncating integer
|
|
21
21
|
// math the chain does.
|
|
22
22
|
|
|
23
|
-
import * as listingContract from "../contracts/
|
|
23
|
+
import * as listingContract from "../contracts/miso_record_shop/listing.ts";
|
|
24
24
|
import {
|
|
25
25
|
extractTypeParams2,
|
|
26
26
|
getCompositionsByIds,
|
|
@@ -28,13 +28,14 @@ import {
|
|
|
28
28
|
import { normalizeStructTag, normalizeSuiAddress } from "@mysten/sui/utils";
|
|
29
29
|
import { getPressingDetail } from "./catalog.ts";
|
|
30
30
|
import type { MisoClient } from "./client.ts";
|
|
31
|
-
import { int
|
|
31
|
+
import { int } from "./internal/scalars.ts";
|
|
32
|
+
import { requireRecordSalesDeployment } from "../deployments.ts";
|
|
32
33
|
import type { Price, PressingDetail, PurchaseReceipt, RecordSale, TrackRoyalty } from "./types.ts";
|
|
33
34
|
import { getWorkAddressesByShareTypes } from "./works.ts";
|
|
34
35
|
|
|
35
36
|
const BPS = 10_000n;
|
|
36
37
|
|
|
37
|
-
/** `
|
|
38
|
+
/** `miso_record_shop::listing::RecordSoldEvent<Currency>`. */
|
|
38
39
|
const RECORD_SOLD_EVENT_NAME = "listing::RecordSoldEvent";
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -44,6 +45,10 @@ const RECORD_SOLD_EVENT_NAME = "listing::RecordSoldEvent";
|
|
|
44
45
|
*/
|
|
45
46
|
const FULLNODE_WAIT_MS = 5_000;
|
|
46
47
|
|
|
48
|
+
export class MalformedRecordSoldEventError extends Error {
|
|
49
|
+
override readonly name = "MalformedRecordSoldEventError";
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/** Composition royalty rate (bps) per recording id. Sparse — an unresolved parent has no entry. */
|
|
48
53
|
type CompositionRates = Record<string, number | undefined>;
|
|
49
54
|
|
|
@@ -58,18 +63,45 @@ function priceFromUnknown(value: unknown): Price | null {
|
|
|
58
63
|
if (!value || typeof value !== "object") return null;
|
|
59
64
|
const variants = value as {
|
|
60
65
|
$kind?: string;
|
|
61
|
-
Fixed?:
|
|
62
|
-
Floor?:
|
|
63
|
-
fixed?: { amount?: unknown };
|
|
64
|
-
floor?: { amount?: unknown };
|
|
66
|
+
Fixed?: unknown;
|
|
67
|
+
Floor?: unknown;
|
|
65
68
|
};
|
|
66
|
-
const fixed = variants.$kind === "Fixed" ||
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const fixed = variants.$kind === "Fixed" || Object.hasOwn(variants, "Fixed");
|
|
70
|
+
const floor = variants.$kind === "Floor" || Object.hasOwn(variants, "Floor");
|
|
71
|
+
if (fixed === floor) return null;
|
|
72
|
+
const amount = coerceBigInt(fixed ? variants.Fixed : variants.Floor);
|
|
73
|
+
return amount == null || amount <= 0n
|
|
74
|
+
? null
|
|
75
|
+
: { kind: fixed ? "fixed" : "floor", amount: amount.toString() };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function typeName(value: unknown): string | null {
|
|
79
|
+
const raw = typeof value === "string"
|
|
80
|
+
? value
|
|
81
|
+
: value && typeof value === "object"
|
|
82
|
+
? (value as Record<string, unknown>).name
|
|
83
|
+
: null;
|
|
84
|
+
if (typeof raw !== "string" || !raw) return null;
|
|
85
|
+
try { return normalizeStructTag(raw); } catch { return null; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function positiveU32(value: unknown): number | null {
|
|
89
|
+
const bigint = coerceBigInt(value);
|
|
90
|
+
return bigint == null || bigint <= 0n || bigint > 0xffff_ffffn
|
|
91
|
+
? null
|
|
92
|
+
: Number(bigint);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function positiveU16(value: unknown): number | null {
|
|
96
|
+
const number = positiveU32(value);
|
|
97
|
+
return number == null || number > 0xffff ? null : number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validPricingRelationship(purchasePrice: bigint, pricing: Price): boolean {
|
|
101
|
+
const configured = BigInt(pricing.amount);
|
|
102
|
+
return pricing.kind === "fixed"
|
|
103
|
+
? purchasePrice === configured
|
|
104
|
+
: purchasePrice >= configured;
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
/**
|
|
@@ -80,21 +112,39 @@ function saleFromJson(
|
|
|
80
112
|
json: Record<string, unknown>,
|
|
81
113
|
currencyType: string,
|
|
82
114
|
): RecordSale | null {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
115
|
+
const edition = positiveU16(json.edition);
|
|
116
|
+
const number = positiveU32(json.number);
|
|
117
|
+
const purchasePrice = coerceBigInt(json.purchase_price);
|
|
118
|
+
const purchasedTimestampMs = coerceBigInt(json.purchased_timestamp_ms);
|
|
85
119
|
const recordId = json.record_id;
|
|
86
|
-
const
|
|
87
|
-
|
|
120
|
+
const pricing = priceFromUnknown(json.pricing);
|
|
121
|
+
const embeddedCurrency = typeName(json.purchase_currency);
|
|
122
|
+
if (
|
|
123
|
+
edition == null ||
|
|
124
|
+
number == null ||
|
|
125
|
+
purchasePrice == null || purchasePrice <= 0n ||
|
|
126
|
+
purchasedTimestampMs == null ||
|
|
127
|
+
!pricing || !validPricingRelationship(purchasePrice, pricing) ||
|
|
128
|
+
embeddedCurrency !== currencyType ||
|
|
129
|
+
typeof recordId !== "string" || !recordId ||
|
|
130
|
+
typeof json.listing_id !== "string" || !json.listing_id ||
|
|
131
|
+
typeof json.pressing_id !== "string" || !json.pressing_id ||
|
|
132
|
+
typeof json.release_id !== "string" || !json.release_id ||
|
|
133
|
+
typeof json.purchased_by !== "string" || !json.purchased_by
|
|
134
|
+
) return null;
|
|
88
135
|
return {
|
|
89
|
-
listingId:
|
|
90
|
-
pressingId:
|
|
91
|
-
releaseId:
|
|
136
|
+
listingId: json.listing_id,
|
|
137
|
+
pressingId: json.pressing_id,
|
|
138
|
+
releaseId: json.release_id,
|
|
92
139
|
recordId,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
140
|
+
edition,
|
|
141
|
+
number,
|
|
142
|
+
purchaseCurrency: embeddedCurrency,
|
|
143
|
+
purchasePrice: purchasePrice.toString(),
|
|
144
|
+
pricing,
|
|
96
145
|
currencyType,
|
|
97
|
-
|
|
146
|
+
purchasedBy: json.purchased_by,
|
|
147
|
+
purchasedTimestampMs: purchasedTimestampMs.toString(),
|
|
98
148
|
};
|
|
99
149
|
}
|
|
100
150
|
|
|
@@ -105,9 +155,9 @@ function saleFromJson(
|
|
|
105
155
|
* be decoded as a Miso sale. Normalize both package/type tags and require exactly
|
|
106
156
|
* one (possibly nested) currency type argument.
|
|
107
157
|
*/
|
|
108
|
-
function recordSoldCurrencyType(
|
|
158
|
+
export function recordSoldCurrencyType(
|
|
109
159
|
eventType: string,
|
|
110
|
-
|
|
160
|
+
recordShopPackageId: string,
|
|
111
161
|
): string | null {
|
|
112
162
|
let tag: string;
|
|
113
163
|
try {
|
|
@@ -116,7 +166,7 @@ function recordSoldCurrencyType(
|
|
|
116
166
|
return null;
|
|
117
167
|
}
|
|
118
168
|
|
|
119
|
-
const prefix = `${normalizeSuiAddress(
|
|
169
|
+
const prefix = `${normalizeSuiAddress(recordShopPackageId)}::${RECORD_SOLD_EVENT_NAME}`;
|
|
120
170
|
if (!tag.startsWith(`${prefix}<`) || !tag.endsWith(">")) return null;
|
|
121
171
|
|
|
122
172
|
const typeArgument = tag.slice(prefix.length + 1, -1);
|
|
@@ -136,56 +186,86 @@ function recordSoldCurrencyType(
|
|
|
136
186
|
|
|
137
187
|
export function isRecordSoldEventType(
|
|
138
188
|
eventType: string,
|
|
139
|
-
|
|
189
|
+
recordShopPackageId: string,
|
|
140
190
|
): boolean {
|
|
141
|
-
return recordSoldCurrencyType(eventType,
|
|
191
|
+
return recordSoldCurrencyType(eventType, recordShopPackageId) !== null;
|
|
142
192
|
}
|
|
143
193
|
|
|
144
194
|
/**
|
|
145
195
|
* The sale a transaction recorded, or null if it emitted none. BCS is the stable
|
|
146
196
|
* shape (a JSON projection's field names vary by transport), so it is tried first.
|
|
147
197
|
*/
|
|
148
|
-
export function
|
|
198
|
+
export function findRecordSales(
|
|
149
199
|
events: {
|
|
150
200
|
eventType: string;
|
|
151
201
|
bcs: Uint8Array;
|
|
152
202
|
json: Record<string, unknown> | null;
|
|
153
203
|
}[],
|
|
154
|
-
|
|
155
|
-
): RecordSale
|
|
204
|
+
recordShopPackageId: string,
|
|
205
|
+
): RecordSale[] {
|
|
206
|
+
const sales: RecordSale[] = [];
|
|
156
207
|
for (const e of events) {
|
|
157
208
|
const currencyType = recordSoldCurrencyType(
|
|
158
209
|
e.eventType,
|
|
159
|
-
|
|
210
|
+
recordShopPackageId,
|
|
160
211
|
);
|
|
161
212
|
if (!currencyType) continue;
|
|
162
213
|
try {
|
|
163
214
|
// The generated struct decodes addresses to 0x-hex and u64s to decimal strings.
|
|
164
215
|
const s = listingContract.RecordSoldEvent.parse(e.bcs);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
const pricing = priceFromUnknown(s.pricing);
|
|
217
|
+
const embeddedCurrency = typeName(s.purchase_currency);
|
|
218
|
+
const purchasePrice = BigInt(s.purchase_price);
|
|
219
|
+
if (
|
|
220
|
+
!pricing || embeddedCurrency !== currencyType ||
|
|
221
|
+
!validPricingRelationship(purchasePrice, pricing) ||
|
|
222
|
+
s.edition <= 0 || s.number <= 0
|
|
223
|
+
) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent");
|
|
224
|
+
sales.push({
|
|
168
225
|
listingId: s.listing_id,
|
|
169
226
|
pressingId: s.pressing_id,
|
|
170
227
|
releaseId: s.release_id,
|
|
171
228
|
recordId: s.record_id,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
edition: s.edition,
|
|
230
|
+
number: s.number,
|
|
231
|
+
purchaseCurrency: embeddedCurrency,
|
|
232
|
+
purchasePrice: s.purchase_price,
|
|
233
|
+
pricing,
|
|
175
234
|
currencyType,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
235
|
+
purchasedBy: s.purchased_by,
|
|
236
|
+
purchasedTimestampMs: s.purchased_timestamp_ms,
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Never reinterpret malformed canonical BCS through a looser JSON view.
|
|
241
|
+
if (e.bcs.length > 0) {
|
|
242
|
+
if (error instanceof MalformedRecordSoldEventError) throw error;
|
|
243
|
+
throw new MalformedRecordSoldEventError(
|
|
244
|
+
`malformed canonical RecordSoldEvent BCS: ${error instanceof Error ? error.message : String(error)}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
180
247
|
}
|
|
181
248
|
const fromJson = e.json && saleFromJson(e.json, currencyType);
|
|
182
|
-
if (fromJson)
|
|
249
|
+
if (!fromJson) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent JSON");
|
|
250
|
+
sales.push(fromJson);
|
|
183
251
|
}
|
|
184
|
-
return
|
|
252
|
+
return sales;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Select one canonical sale by its Record ID. Selection is always explicit. */
|
|
256
|
+
export function findRecordSale(
|
|
257
|
+
events: Parameters<typeof findRecordSales>[0],
|
|
258
|
+
recordShopPackageId: string,
|
|
259
|
+
recordId: string,
|
|
260
|
+
): RecordSale | null {
|
|
261
|
+
const sales = findRecordSales(events, recordShopPackageId);
|
|
262
|
+
return sales.find(
|
|
263
|
+
(sale) => normalizeSuiAddress(sale.recordId) === normalizeSuiAddress(recordId),
|
|
264
|
+
) ?? null;
|
|
185
265
|
}
|
|
186
266
|
|
|
187
|
-
/**
|
|
188
|
-
async function
|
|
267
|
+
/** Every canonical sale, read from the fullnode in event order. */
|
|
268
|
+
async function salesFromFullnode(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
|
|
189
269
|
const result = await client.sui.waitForTransaction({
|
|
190
270
|
digest: txDigest,
|
|
191
271
|
include: { events: true },
|
|
@@ -194,12 +274,13 @@ async function saleFromFullnode(client: MisoClient, txDigest: string): Promise<R
|
|
|
194
274
|
if (result.$kind !== "Transaction" || !result.Transaction.status.success) {
|
|
195
275
|
throw new Error(`Transaction did not succeed: ${txDigest}`);
|
|
196
276
|
}
|
|
197
|
-
const
|
|
277
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
278
|
+
const found = findRecordSales(
|
|
198
279
|
result.Transaction.events,
|
|
199
|
-
|
|
280
|
+
sales.recordShopPackageId,
|
|
200
281
|
);
|
|
201
|
-
if (
|
|
202
|
-
return
|
|
282
|
+
if (found.length === 0) throw new Error(`No record purchase in transaction ${txDigest}`);
|
|
283
|
+
return found;
|
|
203
284
|
}
|
|
204
285
|
|
|
205
286
|
const TX_EVENTS_QUERY = `query TransactionEvents($digest: String!) {
|
|
@@ -220,8 +301,8 @@ interface TxEventsResult {
|
|
|
220
301
|
} | null;
|
|
221
302
|
}
|
|
222
303
|
|
|
223
|
-
/**
|
|
224
|
-
async function
|
|
304
|
+
/** Every canonical sale, read from the GraphQL indexer in event order. */
|
|
305
|
+
async function salesFromIndexer(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
|
|
225
306
|
const { data, errors } = await client.graphqlRaw.query<TxEventsResult, { digest: string }>({
|
|
226
307
|
query: TX_EVENTS_QUERY,
|
|
227
308
|
variables: { digest: txDigest },
|
|
@@ -232,23 +313,34 @@ async function saleFromIndexer(client: MisoClient, txDigest: string): Promise<Re
|
|
|
232
313
|
if (!effects) throw new Error(`Transaction not found: ${txDigest}`);
|
|
233
314
|
if (effects.status !== "SUCCESS") throw new Error(`Transaction did not succeed: ${txDigest}`);
|
|
234
315
|
|
|
316
|
+
const salesDeployment = requireRecordSalesDeployment(client.config.recordSales);
|
|
317
|
+
const sales: RecordSale[] = [];
|
|
235
318
|
for (const node of effects.events?.nodes ?? []) {
|
|
236
319
|
const contents = node.contents;
|
|
237
320
|
if (!contents) continue;
|
|
238
321
|
const currencyType = recordSoldCurrencyType(
|
|
239
322
|
contents.type.repr,
|
|
240
|
-
|
|
323
|
+
salesDeployment.recordShopPackageId,
|
|
241
324
|
);
|
|
242
325
|
if (!currencyType) continue;
|
|
243
326
|
const sale =
|
|
244
327
|
typeof contents.json === "object" && contents.json !== null
|
|
245
328
|
? saleFromJson(contents.json as Record<string, unknown>, currencyType)
|
|
246
329
|
: null;
|
|
247
|
-
if (sale)
|
|
330
|
+
if (!sale) throw new MalformedRecordSoldEventError("malformed canonical RecordSoldEvent JSON");
|
|
331
|
+
sales.push(sale);
|
|
248
332
|
}
|
|
333
|
+
if (sales.length > 0) return sales;
|
|
249
334
|
throw new Error(`No record purchase in transaction ${txDigest}`);
|
|
250
335
|
}
|
|
251
336
|
|
|
337
|
+
async function salesFromChain(client: MisoClient, txDigest: string): Promise<RecordSale[]> {
|
|
338
|
+
return salesFromFullnode(client, txDigest).catch((error) => {
|
|
339
|
+
if (error instanceof MalformedRecordSoldEventError) throw error;
|
|
340
|
+
return salesFromIndexer(client, txDigest);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
252
344
|
/**
|
|
253
345
|
* Each recording's parent composition royalty rate, in basis points, keyed by
|
|
254
346
|
* recording id.
|
|
@@ -331,13 +423,10 @@ export function breakdown(
|
|
|
331
423
|
* The sale identifies the Pressing before its detail is fetched. `null` only when
|
|
332
424
|
* that immutable Pressing cannot be read; it is never superseded or destroyed.
|
|
333
425
|
*/
|
|
334
|
-
|
|
426
|
+
async function hydratePurchaseReceipt(
|
|
335
427
|
client: MisoClient,
|
|
336
|
-
|
|
428
|
+
sale: RecordSale,
|
|
337
429
|
): Promise<PurchaseReceipt | null> {
|
|
338
|
-
const sale = await saleFromFullnode(client, txDigest).catch(() =>
|
|
339
|
-
saleFromIndexer(client, txDigest),
|
|
340
|
-
);
|
|
341
430
|
const detail = await getPressingDetail(client, sale.pressingId);
|
|
342
431
|
if (!detail) return null;
|
|
343
432
|
|
|
@@ -350,7 +439,33 @@ export async function getPurchaseReceipt(
|
|
|
350
439
|
return {
|
|
351
440
|
sale,
|
|
352
441
|
detail,
|
|
353
|
-
price: sale.
|
|
354
|
-
tracks: breakdown(detail.release.tracks, sale.
|
|
442
|
+
price: sale.pricing.amount,
|
|
443
|
+
tracks: breakdown(detail.release.tracks, sale.purchasePrice, rates),
|
|
355
444
|
};
|
|
356
445
|
}
|
|
446
|
+
|
|
447
|
+
/** Read and hydrate every canonical Record sale in transaction event order. */
|
|
448
|
+
export async function getPurchaseReceipts(
|
|
449
|
+
client: MisoClient,
|
|
450
|
+
txDigest: string,
|
|
451
|
+
): Promise<PurchaseReceipt[]> {
|
|
452
|
+
const sales = await salesFromChain(client, txDigest);
|
|
453
|
+
const hydrated = await Promise.all(sales.map((sale) => hydratePurchaseReceipt(client, sale)));
|
|
454
|
+
if (hydrated.some((receipt) => receipt === null)) {
|
|
455
|
+
throw new Error(`A canonical Record sale in transaction ${txDigest} references an unreadable Pressing`);
|
|
456
|
+
}
|
|
457
|
+
return hydrated as PurchaseReceipt[];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Read one canonical receipt selected by its Record ID. Selection is mandatory:
|
|
461
|
+
* one transaction may purchase multiple Records from the same Pressing. */
|
|
462
|
+
export async function getPurchaseReceipt(
|
|
463
|
+
client: MisoClient,
|
|
464
|
+
txDigest: string,
|
|
465
|
+
recordId: string,
|
|
466
|
+
): Promise<PurchaseReceipt | null> {
|
|
467
|
+
const sales = await salesFromChain(client, txDigest);
|
|
468
|
+
const wanted = normalizeSuiAddress(recordId);
|
|
469
|
+
const sale = sales.find((candidate) => normalizeSuiAddress(candidate.recordId) === wanted);
|
|
470
|
+
return sale ? hydratePurchaseReceipt(client, sale) : null;
|
|
471
|
+
}
|
package/src/read/types.ts
CHANGED
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
/** Lifecycle state shared by compositions, recordings, and releases. */
|
|
24
24
|
export type WorkState =
|
|
25
|
-
| { type: "
|
|
26
|
-
| { type: "Published"; timestampMs: number };
|
|
25
|
+
{ type: "Initialized" } | { type: "Published"; timestampMs: number };
|
|
27
26
|
|
|
28
27
|
/** A cover image's location on Walrus, plus the URL to fetch it from. */
|
|
29
28
|
export interface CoverImage {
|
|
@@ -77,6 +76,8 @@ export interface TrackView {
|
|
|
77
76
|
disc: number;
|
|
78
77
|
/** Base64url Walrus blob id for the master stream, when attached on-chain. */
|
|
79
78
|
masterBlobId?: string;
|
|
79
|
+
/** Base64url Walrus blob id for the public mix-delivery descriptor. */
|
|
80
|
+
mixBlobId?: string;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/** A release with everything a page renders: metadata, cover, credits, tracklist. */
|
|
@@ -118,13 +119,10 @@ export interface Currency {
|
|
|
118
119
|
export interface PressingView {
|
|
119
120
|
id: string;
|
|
120
121
|
releaseId: string;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
| { kind: "paused" };
|
|
126
|
-
/** Records pressed so far, decimal string. The run is intentionally uncapped. */
|
|
127
|
-
supply: string;
|
|
122
|
+
edition: number;
|
|
123
|
+
supply: number;
|
|
124
|
+
maxSupply: number | null;
|
|
125
|
+
distributors: string[];
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
/** One permanent currency offer derived from a Pressing. */
|
|
@@ -132,9 +130,9 @@ export interface ListingView {
|
|
|
132
130
|
id: string;
|
|
133
131
|
pressingId: string;
|
|
134
132
|
releaseId: string;
|
|
135
|
-
|
|
133
|
+
pricing: Price;
|
|
136
134
|
currency: Currency;
|
|
137
|
-
/** The per-currency switch
|
|
135
|
+
/** The per-currency purchase switch. */
|
|
138
136
|
state: "enabled" | "disabled";
|
|
139
137
|
}
|
|
140
138
|
|
|
@@ -181,8 +179,9 @@ export interface PressingPreview {
|
|
|
181
179
|
title: string;
|
|
182
180
|
subtitle: string | null;
|
|
183
181
|
coverUrl: string | null;
|
|
184
|
-
|
|
185
|
-
supply:
|
|
182
|
+
edition: number;
|
|
183
|
+
supply: number;
|
|
184
|
+
maxSupply: number | null;
|
|
186
185
|
trackCount: number;
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -284,9 +283,18 @@ export interface OwnedRecord {
|
|
|
284
283
|
id: string;
|
|
285
284
|
/** Full on-chain type, e.g. `<miso_record>::record::Record`. */
|
|
286
285
|
type: string;
|
|
287
|
-
releaseId: string
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
releaseId: string;
|
|
287
|
+
pressingId: string;
|
|
288
|
+
edition: number;
|
|
289
|
+
number: number;
|
|
290
|
+
/** Defining Move type name of the currency used for the purchase. */
|
|
291
|
+
purchaseCurrency: string;
|
|
292
|
+
/** Lossless u64 base-unit amount actually paid. */
|
|
293
|
+
purchasePrice: string;
|
|
294
|
+
/** Transaction sender that purchased the Record. */
|
|
295
|
+
purchasedBy: string;
|
|
296
|
+
/** Lossless u64 Unix-millisecond purchase timestamp. */
|
|
297
|
+
purchasedTimestampMs: string;
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
/** A party the wallet administers (holds the `PartyAdminCap` for). */
|
|
@@ -364,15 +372,20 @@ export interface RecordSale {
|
|
|
364
372
|
releaseId: string;
|
|
365
373
|
/** The `Record` object minted to the buyer. */
|
|
366
374
|
recordId: string;
|
|
367
|
-
|
|
368
|
-
number
|
|
369
|
-
|
|
370
|
-
|
|
375
|
+
edition: number;
|
|
376
|
+
/** This copy's number in the edition. */
|
|
377
|
+
number: number;
|
|
378
|
+
/** Embedded defining TypeName; validated against the event generic. */
|
|
379
|
+
purchaseCurrency: string;
|
|
380
|
+
/** What the buyer actually paid, in base units. */
|
|
381
|
+
purchasePrice: string;
|
|
371
382
|
/** The fixed or floor price that the Listing accepted at this sale. */
|
|
372
|
-
|
|
383
|
+
pricing: Price;
|
|
373
384
|
/** Currency type carried by the `RecordSoldEvent` generic argument. */
|
|
374
385
|
currencyType: string;
|
|
375
|
-
|
|
386
|
+
purchasedBy: string;
|
|
387
|
+
/** Lossless chain-clock timestamp captured by `listing::purchase`. */
|
|
388
|
+
purchasedTimestampMs: string;
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
/** One track's share of the paid amount, and how that share splits again. */
|
package/src/read/wallet.ts
CHANGED
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
import type { MisoClient } from "./client.ts";
|
|
23
23
|
import { int, u64 } from "./internal/scalars.ts";
|
|
24
24
|
import * as vaultContract from "../contracts/vault/vault.ts";
|
|
25
|
+
import * as recordContract from "../contracts/miso_record/record.ts";
|
|
26
|
+
import { deriveRecordId } from "../pressing.ts";
|
|
27
|
+
import { requireRecordSalesDeployment } from "../deployments.ts";
|
|
25
28
|
import type {
|
|
26
29
|
Balance,
|
|
27
30
|
OwnedParty,
|
|
@@ -125,29 +128,14 @@ export function classifyVaultedWorkAdminCapType(
|
|
|
125
128
|
|
|
126
129
|
// ── Records ──────────────────────────────────────────────────────────────────
|
|
127
130
|
|
|
128
|
-
function coerceNumber(v: unknown): number | null {
|
|
129
|
-
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
130
|
-
if (typeof v === "bigint") return Number(v);
|
|
131
|
-
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function record(value: unknown): Record<string, unknown> | null {
|
|
136
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
137
|
-
? (value as Record<string, unknown>)
|
|
138
|
-
: null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
131
|
/**
|
|
142
|
-
* A wallet record is trusted only when
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* arbitrary certificate that anyone can use with `record::new`.
|
|
132
|
+
* A wallet record is trusted only when it is the exact, non-generic Record type
|
|
133
|
+
* from the configured package. Checking the parsed tag (rather than a prefix or
|
|
134
|
+
* suffix) rejects retired namespaces and legacy/arbitrary generic wrappers.
|
|
146
135
|
*/
|
|
147
136
|
function isCanonicalRecordType(
|
|
148
137
|
type: string,
|
|
149
138
|
recordPackageId: string,
|
|
150
|
-
pressingPackageId: string,
|
|
151
139
|
): boolean {
|
|
152
140
|
let tag: ReturnType<typeof parseStructTag>;
|
|
153
141
|
try {
|
|
@@ -160,34 +148,11 @@ function isCanonicalRecordType(
|
|
|
160
148
|
tag.address !== normalizeSuiAddress(recordPackageId) ||
|
|
161
149
|
tag.module !== "record" ||
|
|
162
150
|
tag.name !== "Record" ||
|
|
163
|
-
tag.typeParams.length !==
|
|
151
|
+
tag.typeParams.length !== 0
|
|
164
152
|
) {
|
|
165
153
|
return false;
|
|
166
154
|
}
|
|
167
|
-
|
|
168
|
-
const certificate = tag.typeParams[0];
|
|
169
|
-
return (
|
|
170
|
-
certificate !== undefined &&
|
|
171
|
-
typeof certificate !== "string" &&
|
|
172
|
-
certificate.address === normalizeSuiAddress(pressingPackageId) &&
|
|
173
|
-
certificate.module === "certificate" &&
|
|
174
|
-
certificate.name === "Certificate" &&
|
|
175
|
-
certificate.typeParams.length === 0
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* This copy's number in its pressing run. The current Record keeps issuance
|
|
181
|
-
* details inside its embedded Certificate, so the canonical JSON shape is
|
|
182
|
-
* `{ release_id, certificate: { number } }`.
|
|
183
|
-
*/
|
|
184
|
-
function readRecordNumber(json: Record<string, unknown>): number | null {
|
|
185
|
-
return coerceNumber(record(json.certificate)?.number);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function readReleaseId(json: Record<string, unknown>): string | null {
|
|
189
|
-
const r = json.release_id;
|
|
190
|
-
return typeof r === "string" && r ? r : null;
|
|
155
|
+
return true;
|
|
191
156
|
}
|
|
192
157
|
|
|
193
158
|
/**
|
|
@@ -199,8 +164,8 @@ function readReleaseId(json: Record<string, unknown>): string | null {
|
|
|
199
164
|
export async function getOwnedRecords(client: MisoClient, owner: string): Promise<OwnedRecord[]> {
|
|
200
165
|
const out: OwnedRecord[] = [];
|
|
201
166
|
let cursor: string | null = null;
|
|
202
|
-
const
|
|
203
|
-
const
|
|
167
|
+
const sales = requireRecordSalesDeployment(client.config.recordSales);
|
|
168
|
+
const recordType = `${sales.recordPackageId}::record::Record`;
|
|
204
169
|
|
|
205
170
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
206
171
|
// Annotated: `cursor` is both an input and assigned from the result, which
|
|
@@ -211,18 +176,32 @@ export async function getOwnedRecords(client: MisoClient, owner: string): Promis
|
|
|
211
176
|
type: recordType,
|
|
212
177
|
cursor,
|
|
213
178
|
limit: 50,
|
|
214
|
-
include: {
|
|
179
|
+
include: { content: true },
|
|
215
180
|
});
|
|
216
181
|
for (const obj of res.objects) {
|
|
217
|
-
if (!isCanonicalRecordType(obj.type,
|
|
182
|
+
if (!isCanonicalRecordType(obj.type, sales.recordPackageId)) {
|
|
218
183
|
continue;
|
|
219
184
|
}
|
|
220
|
-
|
|
185
|
+
if (!obj.content) throw new Error(`Record ${obj.objectId} has no BCS content`);
|
|
186
|
+
const record = recordContract.Record.parse(obj.content);
|
|
187
|
+
if (normalizeSuiAddress(record.id) !== normalizeSuiAddress(obj.objectId)) {
|
|
188
|
+
throw new Error(`Record ${obj.objectId} has mismatched embedded UID ${record.id}`);
|
|
189
|
+
}
|
|
190
|
+
const derived = deriveRecordId(record.pressing_id, record.number, sales.recordPackageId);
|
|
191
|
+
if (normalizeSuiAddress(derived) !== normalizeSuiAddress(obj.objectId)) {
|
|
192
|
+
throw new Error(`Record ${obj.objectId} is not derived from its Pressing and number`);
|
|
193
|
+
}
|
|
221
194
|
out.push({
|
|
222
195
|
id: obj.objectId,
|
|
223
196
|
type: obj.type,
|
|
224
|
-
releaseId:
|
|
225
|
-
|
|
197
|
+
releaseId: record.release_id,
|
|
198
|
+
pressingId: record.pressing_id,
|
|
199
|
+
edition: record.edition,
|
|
200
|
+
number: record.number,
|
|
201
|
+
purchaseCurrency: normalizeStructTag(record.purchase_currency.name),
|
|
202
|
+
purchasePrice: record.purchase_price,
|
|
203
|
+
purchasedBy: record.purchased_by,
|
|
204
|
+
purchasedTimestampMs: record.purchased_timestamp_ms,
|
|
226
205
|
});
|
|
227
206
|
}
|
|
228
207
|
if (!res.hasNextPage) break;
|