@misofm/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +248 -0
  3. package/dist/catalog.d.ts +31 -0
  4. package/dist/catalog.d.ts.map +1 -0
  5. package/dist/catalog.js +45 -0
  6. package/dist/catalog.js.map +1 -0
  7. package/dist/client.d.ts +134 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +113 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/contracts/miso_pressing/certificate.d.ts +78 -0
  12. package/dist/contracts/miso_pressing/certificate.d.ts.map +1 -0
  13. package/dist/contracts/miso_pressing/certificate.js +91 -0
  14. package/dist/contracts/miso_pressing/certificate.js.map +1 -0
  15. package/dist/contracts/miso_pressing/deps/std/type_name.d.ts +18 -0
  16. package/dist/contracts/miso_pressing/deps/std/type_name.d.ts.map +1 -0
  17. package/dist/contracts/miso_pressing/deps/std/type_name.js +20 -0
  18. package/dist/contracts/miso_pressing/deps/std/type_name.js.map +1 -0
  19. package/dist/contracts/miso_pressing/listing.d.ts +449 -0
  20. package/dist/contracts/miso_pressing/listing.d.ts.map +1 -0
  21. package/dist/contracts/miso_pressing/listing.js +416 -0
  22. package/dist/contracts/miso_pressing/listing.js.map +1 -0
  23. package/dist/contracts/miso_pressing/pressing.d.ts +365 -0
  24. package/dist/contracts/miso_pressing/pressing.d.ts.map +1 -0
  25. package/dist/contracts/miso_pressing/pressing.js +335 -0
  26. package/dist/contracts/miso_pressing/pressing.js.map +1 -0
  27. package/dist/contracts/utils/index.d.ts +31 -0
  28. package/dist/contracts/utils/index.d.ts.map +1 -0
  29. package/dist/contracts/utils/index.js +162 -0
  30. package/dist/contracts/utils/index.js.map +1 -0
  31. package/dist/execute.d.ts +17 -0
  32. package/dist/execute.d.ts.map +1 -0
  33. package/dist/execute.js +21 -0
  34. package/dist/execute.js.map +1 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +27 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/pressing.d.ts +223 -0
  40. package/dist/pressing.d.ts.map +1 -0
  41. package/dist/pressing.js +280 -0
  42. package/dist/pressing.js.map +1 -0
  43. package/dist/queries.d.ts +2 -0
  44. package/dist/queries.d.ts.map +1 -0
  45. package/dist/queries.js +8 -0
  46. package/dist/queries.js.map +1 -0
  47. package/dist/release-graph.d.ts +93 -0
  48. package/dist/release-graph.d.ts.map +1 -0
  49. package/dist/release-graph.js +143 -0
  50. package/dist/release-graph.js.map +1 -0
  51. package/dist/share-template.d.ts +3 -0
  52. package/dist/share-template.d.ts.map +1 -0
  53. package/dist/share-template.js +46 -0
  54. package/dist/share-template.js.map +1 -0
  55. package/dist/share.d.ts +66 -0
  56. package/dist/share.d.ts.map +1 -0
  57. package/dist/share.js +156 -0
  58. package/dist/share.js.map +1 -0
  59. package/dist/transactions.d.ts +177 -0
  60. package/dist/transactions.d.ts.map +1 -0
  61. package/dist/transactions.js +237 -0
  62. package/dist/transactions.js.map +1 -0
  63. package/package.json +99 -0
  64. package/src/catalog.ts +77 -0
  65. package/src/client.ts +249 -0
  66. package/src/contracts/miso_pressing/certificate.ts +129 -0
  67. package/src/contracts/miso_pressing/deps/std/type_name.ts +22 -0
  68. package/src/contracts/miso_pressing/listing.ts +621 -0
  69. package/src/contracts/miso_pressing/pressing.ts +454 -0
  70. package/src/contracts/utils/index.ts +229 -0
  71. package/src/execute.ts +34 -0
  72. package/src/index.ts +29 -0
  73. package/src/pressing.ts +497 -0
  74. package/src/queries.ts +9 -0
  75. package/src/release-graph.ts +279 -0
  76. package/src/share-template.ts +55 -0
  77. package/src/share.ts +258 -0
  78. package/src/transactions.ts +440 -0
@@ -0,0 +1,279 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Single-PTB publisher for a whole release graph: creates every composition and
5
+ // recording, optionally attaches royalty pools, derives the release id ON-CHAIN
6
+ // (so recordings need not be shared first), builds the deals/tracks/release,
7
+ // then shares everything and transfers the admin caps — all in ONE transaction.
8
+ //
9
+ // Share currencies must already be published + initialized (their Currency objects
10
+ // shared on-chain); their ids/types are passed in. Ordering is load-bearing:
11
+ // 1. composition::new → recording::new(&comp) (borrow-before-share) + pool attach
12
+ // 2. release::derive_release_id(<recording::id results>) → deal::new(cap, &rec,
13
+ // derivedId, split) → track::new → release::new
14
+ // 3. publish (share) comps + recs + release, disperse supply, transfer caps
15
+ // Steps 1–2 borrow the works; step 3 consumes them into share_object.
16
+ //
17
+ // Release tracks come in three shapes (see TrackNode): recordings created in
18
+ // THIS PTB, existing recordings authorized by their admin cap (deal created
19
+ // inline), and existing recordings authorized by a pre-made Deal in hand.
20
+ // Pre-made deals pin the release id, so they cannot coexist with fresh
21
+ // recordings — the builder rejects that mix up front.
22
+ //
23
+ // This is the whole-graph orchestration half of the opinionated publish flow —
24
+ // it composes `@misonetwork/sdk`'s bare `composition`/`recording`/`deal`/`track`/
25
+ // `release` call bindings, its `createRelease` primitive and
26
+ // `attachCompositionRoyaltyPool`/`attachRecordingRoyaltyPool`, plus this
27
+ // package's own `disperseShares`/`finalizeRelease`, into one PTB.
28
+
29
+ import { Transaction, type TransactionObjectArgument } from "@mysten/sui/transactions";
30
+ import {
31
+ contracts,
32
+ attachCompositionRoyaltyPool,
33
+ attachRecordingRoyaltyPool,
34
+ createRelease,
35
+ PROTOCOL_MAX_ROYALTY_RATE_BPS,
36
+ } from "@misonetwork/sdk";
37
+ import { disperseShares, finalizeRelease, type ShareRecipient } from "./transactions.ts";
38
+
39
+ const { composition, recording, deal, track, release } = contracts;
40
+
41
+ /** A royalty pool to attach to a work, in the same PTB. */
42
+ export interface RoyaltyPoolNode {
43
+ currency: string;
44
+ /** `composition_royalty_pool` / `recording_royalty_pool` package id. */
45
+ extensionPackageId: string;
46
+ /** Base `royalty_pool` lib package id. */
47
+ royaltyPoolPackageId: string;
48
+ }
49
+
50
+ export interface CompositionNode {
51
+ shareType: string;
52
+ shareCurrencyId: string;
53
+ shareTreasuryCapId: string;
54
+ title: string;
55
+ royaltyRateBps: number;
56
+ shareRecipients: ShareRecipient[];
57
+ /** Recipient of the CompositionAdminCap (the owner). */
58
+ adminAddress: string;
59
+ royaltyPool?: RoyaltyPoolNode;
60
+ }
61
+
62
+ export interface RecordingNode {
63
+ shareType: string;
64
+ shareCurrencyId: string;
65
+ shareTreasuryCapId: string;
66
+ /** The parent composition's share type. */
67
+ compositionShareType: string;
68
+ /** Index into `compositions[]` if the parent is created in THIS PTB… */
69
+ parentCompositionIndex?: number;
70
+ /** …or the on-chain composition object id if the parent already exists. */
71
+ parentCompositionId?: string;
72
+ shareRecipients: ShareRecipient[];
73
+ adminAddress: string;
74
+ /**
75
+ * Slippage guard for `recording::new` against an EXISTING on-chain parent —
76
+ * pass the composition rate you observed (default: protocol max). Ignored for
77
+ * parents created in this PTB, whose exact known rate is pinned automatically.
78
+ */
79
+ maxRoyaltyRateBps?: number;
80
+ royaltyPool?: RoyaltyPoolNode;
81
+ }
82
+
83
+ /** A track whose recording is created in THIS PTB (its deal is created inline). */
84
+ export interface FreshTrackNode {
85
+ /** Index into `recordings[]`. */
86
+ recordingIndex: number;
87
+ splitBps: number;
88
+ }
89
+
90
+ /**
91
+ * A track over an EXISTING recording, authorized by its admin cap (held by the
92
+ * sender) — the deal is created inline against the on-chain-derived release id.
93
+ * This is the only way to mix existing recordings with ones created in this PTB.
94
+ */
95
+ export interface CapTrackNode {
96
+ recordingId: string;
97
+ recordingAdminCapId: string;
98
+ recordingShareType: string;
99
+ compositionShareType: string;
100
+ splitBps: number;
101
+ }
102
+
103
+ /**
104
+ * A track over an EXISTING recording, authorized by a pre-made `Deal` held by
105
+ * the sender. A deal pins the exact release id — a digest over ALL track
106
+ * recording ids — so deal-backed tracks cannot coexist with `FreshTrackNode`s
107
+ * (a fresh recording's id is unknowable when the deal was created).
108
+ * `splitBps` must be the value stored in the deal (read it from the object).
109
+ */
110
+ export interface DealTrackNode {
111
+ dealId: string;
112
+ recordingId: string;
113
+ recordingShareType: string;
114
+ compositionShareType: string;
115
+ splitBps: number;
116
+ }
117
+
118
+ export type TrackNode = FreshTrackNode | CapTrackNode | DealTrackNode;
119
+
120
+ const isFresh = (t: TrackNode): t is FreshTrackNode => "recordingIndex" in t;
121
+ const isDealBacked = (t: TrackNode): t is DealTrackNode => "dealId" in t;
122
+
123
+ export interface ReleaseNode {
124
+ title: string;
125
+ /** u256 nonce as a decimal string (deterministic release id). */
126
+ nonce: string;
127
+ adminAddress: string;
128
+ releaseRegistryId: string;
129
+ /** The ordered tracklist. Display grouping (discs/sides) is extension data. */
130
+ tracks: TrackNode[];
131
+ }
132
+
133
+ export interface PublishReleaseGraphParams {
134
+ compositions: CompositionNode[];
135
+ recordings: RecordingNode[];
136
+ release?: ReleaseNode;
137
+ misoPackageId: string;
138
+ minatoPackageId: string;
139
+ }
140
+
141
+ interface Parts {
142
+ work: TransactionObjectArgument;
143
+ adminCap: TransactionObjectArgument;
144
+ balance: TransactionObjectArgument;
145
+ }
146
+
147
+ /** Builds the entire graph in a single transaction (see file header for ordering). */
148
+ export function publishReleaseGraph(params: PublishReleaseGraphParams): (tx: Transaction) => void {
149
+ const { misoPackageId: pkg, minatoPackageId } = params;
150
+ return (tx) => {
151
+ // ── 1) Create every composition, then every recording (borrow-before-share) ──
152
+ const comps: Parts[] = params.compositions.map((c) => {
153
+ const r = tx.add(
154
+ composition._new({
155
+ package: pkg,
156
+ typeArguments: [c.shareType],
157
+ arguments: [tx.pure.string(c.title), tx.pure.u16(c.royaltyRateBps), tx.object(c.shareCurrencyId), tx.object(c.shareTreasuryCapId)],
158
+ }),
159
+ );
160
+ const parts: Parts = { work: r[0]!, adminCap: r[1]!, balance: r[2]! };
161
+ if (c.royaltyPool) {
162
+ attachCompositionRoyaltyPool(tx, {
163
+ composition: parts.work,
164
+ adminCap: parts.adminCap,
165
+ compositionShareType: c.shareType,
166
+ currency: c.royaltyPool.currency,
167
+ compositionRoyaltyPoolPackageId: c.royaltyPool.extensionPackageId,
168
+ royaltyPoolPackageId: c.royaltyPool.royaltyPoolPackageId,
169
+ });
170
+ }
171
+ return parts;
172
+ });
173
+
174
+ const recTypeArgs = (i: number): [string, string] => {
175
+ const n = params.recordings[i]!;
176
+ return [n.shareType, n.compositionShareType];
177
+ };
178
+
179
+ const recs: Parts[] = params.recordings.map((rec, i) => {
180
+ const parent = rec.parentCompositionIndex !== undefined ? comps[rec.parentCompositionIndex]!.work : tx.object(rec.parentCompositionId!);
181
+ // Slippage guard: a parent created in this PTB has a known rate — pin it
182
+ // exactly. An existing parent uses the caller's observed rate (or accepts
183
+ // up to the protocol max).
184
+ const maxRoyaltyRateBps =
185
+ rec.parentCompositionIndex !== undefined
186
+ ? params.compositions[rec.parentCompositionIndex]!.royaltyRateBps
187
+ : (rec.maxRoyaltyRateBps ?? PROTOCOL_MAX_ROYALTY_RATE_BPS);
188
+ const typeArgs = recTypeArgs(i);
189
+ const r = tx.add(
190
+ recording._new({ package: pkg, typeArguments: typeArgs, arguments: [parent, tx.object(rec.shareCurrencyId), tx.object(rec.shareTreasuryCapId), tx.pure.u16(maxRoyaltyRateBps)] }),
191
+ );
192
+ const parts: Parts = { work: r[0]!, adminCap: r[1]!, balance: r[2]! };
193
+ if (rec.royaltyPool) {
194
+ attachRecordingRoyaltyPool(tx, {
195
+ recording: parts.work,
196
+ adminCap: parts.adminCap,
197
+ recordingShareType: rec.shareType,
198
+ compositionShareType: rec.compositionShareType,
199
+ currency: rec.royaltyPool.currency,
200
+ recordingRoyaltyPoolPackageId: rec.royaltyPool.extensionPackageId,
201
+ royaltyPoolPackageId: rec.royaltyPool.royaltyPoolPackageId,
202
+ });
203
+ }
204
+ return parts;
205
+ });
206
+
207
+ // ── 2) Release: derive the id on-chain, then deals → tracks → release ──
208
+ if (params.release) {
209
+ const rel = params.release;
210
+ const ordered = rel.tracks;
211
+ if (ordered.some(isDealBacked) && ordered.some(isFresh)) {
212
+ throw new Error(
213
+ "A release cannot mix pre-made deals with recordings created in this transaction: " +
214
+ "a deal pins the exact release id, whose digest includes every track's recording id, " +
215
+ "and a fresh recording's id is unknowable when the deal was created. " +
216
+ "Authorize the existing recordings with their admin caps instead.",
217
+ );
218
+ }
219
+
220
+ // The derived id feeds the deals created inline (fresh/cap-backed tracks).
221
+ // Pre-made deals already store it, so an all-deal release skips the derive.
222
+ let releaseId: TransactionObjectArgument | undefined;
223
+ if (!ordered.every(isDealBacked)) {
224
+ const idVec = tx.makeMoveVec({
225
+ type: "0x2::object::ID",
226
+ elements: ordered.map((t) =>
227
+ isFresh(t)
228
+ ? tx.add(recording.id({ package: pkg, typeArguments: recTypeArgs(t.recordingIndex), arguments: [recs[t.recordingIndex]!.work] }))
229
+ : tx.pure.id(t.recordingId),
230
+ ),
231
+ });
232
+ const splitVec = tx.makeMoveVec({ type: "u64", elements: ordered.map((t) => tx.pure.u64(t.splitBps)) });
233
+ releaseId = tx.add(
234
+ release.deriveReleaseId({ package: pkg, arguments: [idVec, splitVec, tx.pure.u256(BigInt(rel.nonce)), tx.object(rel.releaseRegistryId)] }),
235
+ );
236
+ }
237
+
238
+ const trackArgs = ordered.map((t) => {
239
+ if (isDealBacked(t)) {
240
+ const typeArgs: [string, string] = [t.recordingShareType, t.compositionShareType];
241
+ return tx.add(track._new({ package: pkg, typeArguments: typeArgs, arguments: [tx.object(t.dealId), tx.object(t.recordingId)] }));
242
+ }
243
+ if (isFresh(t)) {
244
+ const typeArgs = recTypeArgs(t.recordingIndex);
245
+ const parts = recs[t.recordingIndex]!;
246
+ const dealArg = tx.add(deal._new({ package: pkg, typeArguments: typeArgs, arguments: [parts.adminCap, parts.work, releaseId!, tx.pure.u16(t.splitBps)] }));
247
+ return tx.add(track._new({ package: pkg, typeArguments: typeArgs, arguments: [dealArg, parts.work] }));
248
+ }
249
+ const typeArgs: [string, string] = [t.recordingShareType, t.compositionShareType];
250
+ const rec = tx.object(t.recordingId);
251
+ const dealArg = tx.add(
252
+ deal._new({ package: pkg, typeArguments: typeArgs, arguments: [tx.object(t.recordingAdminCapId), rec, releaseId!, tx.pure.u16(t.splitBps)] }),
253
+ );
254
+ return tx.add(track._new({ package: pkg, typeArguments: typeArgs, arguments: [dealArg, rec] }));
255
+ });
256
+ const trackVec = tx.makeMoveVec({ type: `${pkg}::track::Track`, elements: trackArgs });
257
+ const releaseParts = createRelease(
258
+ tx,
259
+ { title: rel.title, nonce: rel.nonce, releaseRegistryId: rel.releaseRegistryId, misoPackageId: pkg },
260
+ trackVec,
261
+ );
262
+ finalizeRelease(tx, { ...releaseParts, adminAddress: rel.adminAddress, misoPackageId: pkg });
263
+ }
264
+
265
+ // ── 3) Share every work, disperse its supply, transfer admin caps ──
266
+ params.compositions.forEach((c, i) => {
267
+ const parts = comps[i]!;
268
+ disperseShares(tx, minatoPackageId, c.shareType, parts.balance, c.shareRecipients);
269
+ tx.add(composition.publish({ package: pkg, typeArguments: [c.shareType], arguments: [parts.work, parts.adminCap] }));
270
+ tx.transferObjects([parts.adminCap], c.adminAddress);
271
+ });
272
+ params.recordings.forEach((rec, i) => {
273
+ const parts = recs[i]!;
274
+ tx.add(recording.publish({ package: pkg, typeArguments: recTypeArgs(i), arguments: [parts.work, parts.adminCap] }));
275
+ disperseShares(tx, minatoPackageId, rec.shareType, parts.balance, rec.shareRecipients);
276
+ tx.transferObjects([parts.adminCap], rec.adminAddress);
277
+ });
278
+ };
279
+ }
@@ -0,0 +1,55 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Embedded share-currency bytecode template: the compiled `share` Move package
5
+ // (deps 0x1/0x2, network-agnostic). `patchInitializer` stamps the INITIALIZER
6
+ // address into a fresh copy before publish. Kept as a typed const rather than a
7
+ // JSON asset so the SDK is self-contained — no file read, no path resolution, and
8
+ // it bundles cleanly. Regenerate from the compiled package if the share module
9
+ // changes.
10
+
11
+ import type { PackageBytecode } from "./transactions.ts";
12
+
13
+ export const SHARE_TEMPLATE: PackageBytecode = {
14
+ "modules": [
15
+ "oRzrCwYAAAAKAQAOAg4gAy4gBE4EBVJHB5kB1QEI7gJgBs4DNwqFBAYMiwQ4ABEBEgIHAggCCQIPAhMAAggAAQMHAAMEDAEAAQQACAAEAQABAAEFBgQABgUCAAANAAEAARQFBgACCwUEAAQKCgIBAAQOCAkBCAYQAwQABAcDBwUIAQgBCAEHCAMHCAYBCwIBCAAAAQYIBgEFAQoCAQgBAQgABwcIAwIIAQgBCAEIAQcIBgILBAEJAAsCAQkAAgsEAQkABwgGDENvaW5SZWdpc3RyeRNDdXJyZW5jeUluaXRpYWxpemVyBVNoYXJlBlN0cmluZwtUcmVhc3VyeUNhcAlUeENvbnRleHQDVUlEB2FkZHJlc3MEY29pbg1jb2luX3JlZ2lzdHJ5IGZpbmFsaXplX2FuZF9kZWxldGVfbWV0YWRhdGFfY2FwCmZyb21fYnl0ZXMCaWQKaW5pdGlhbGl6ZQxuZXdfY3VycmVuY3kGb2JqZWN0BnNlbmRlcgVzaGFyZQZzdHJpbmcKdHhfY29udGV4dAR1dGY4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCgIhIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwgAAAAAAAAAAAoCBgVTSEFSRQACAQwIBQABAAABHAoELhEFBwARAiEECAUOCwQBCwMBBwEnCwMxBgcCEQELAAsBCwIKBDgADAULBDgBCwUCAA=="
16
+ ],
17
+ "dependencies": [
18
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
19
+ "0x0000000000000000000000000000000000000000000000000000000000000002"
20
+ ],
21
+ "digest": [
22
+ 191,
23
+ 44,
24
+ 36,
25
+ 112,
26
+ 72,
27
+ 10,
28
+ 163,
29
+ 173,
30
+ 88,
31
+ 79,
32
+ 123,
33
+ 99,
34
+ 33,
35
+ 92,
36
+ 255,
37
+ 215,
38
+ 122,
39
+ 199,
40
+ 22,
41
+ 101,
42
+ 247,
43
+ 141,
44
+ 222,
45
+ 237,
46
+ 187,
47
+ 42,
48
+ 28,
49
+ 13,
50
+ 201,
51
+ 193,
52
+ 173,
53
+ 204
54
+ ]
55
+ };
package/src/share.ts ADDED
@@ -0,0 +1,258 @@
1
+ // Copyright (c) Miso Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Share-currency provisioning.
5
+ //
6
+ // Every composition and every recording is backed by its own fixed-supply
7
+ // (10M @ 6dp) share currency — an independently published `share` package whose
8
+ // `INITIALIZER` constant is patched to an authorized address before publish.
9
+ // Creating one is two transactions that CANNOT share a PTB: a `moveCall` target
10
+ // must name a concrete package id, but a package published in the same PTB has no
11
+ // id until it executes, so `${pkg}::share::initialize` must run in a later tx.
12
+ //
13
+ // The bytecode is identical for every currency (only the baked-in INITIALIZER
14
+ // matters, and it is the same signer for all), so publishes are fungible: a whole
15
+ // batch can be published — up to 5 `tx.publish` per PTB (the protocol's
16
+ // `max_publish_or_upgrade_per_ptb` cap) — and run concurrently through a
17
+ // `ParallelTransactionExecutor`. Initialization is likewise batched. The caller
18
+ // assigns packages to work slots and supplies per-package metadata, so each
19
+ // currency still gets a descriptive, work-specific name.
20
+
21
+ import type { ParallelTransactionExecutor } from "@mysten/sui/transactions";
22
+ import type { ClientWithCoreApi } from "@mysten/sui/client";
23
+ import type { Signer } from "@mysten/sui/cryptography";
24
+ import { fromBase64, fromHex, toBase64 } from "@mysten/sui/utils";
25
+ import { update_constants } from "@mysten/move-bytecode-template";
26
+ import {
27
+ execThunks,
28
+ publishedPackageId,
29
+ allPublishedPackageIds,
30
+ createdByType,
31
+ allCreatedByType,
32
+ type ExecResult,
33
+ } from "@misonetwork/sdk";
34
+
35
+ import { publishShareCurrency, initializeShareCurrency, type PackageBytecode } from "./transactions.ts";
36
+ import { SHARE_TEMPLATE } from "./share-template.ts";
37
+ import { executeViaExecutor } from "./execute.ts";
38
+
39
+ /** The protocol cap on Publish/Upgrade commands per programmable transaction. */
40
+ const PUBLISHES_PER_PTB = 5;
41
+ /** Initializes batched per PTB (each is one moveCall + transfer; all share the 0xc registry). */
42
+ const INITS_PER_PTB = 10;
43
+
44
+ // Address-free type markers: gRPC object types carry fully-normalized addresses
45
+ // (e.g. 0x0000…0002::coin::TreasuryCap), so we match on module::type rather than a
46
+ // short "0x2" prefix that would never match.
47
+ const CURRENCY_TYPE = "::coin_registry::Currency<";
48
+ const TREASURY_CAP_TYPE = "::coin::TreasuryCap<";
49
+
50
+ export interface ShareCurrency {
51
+ /** The published share package id. */
52
+ packageId: string;
53
+ /** The `Currency<Share>` object id. */
54
+ currencyId: string;
55
+ /** The fully-qualified share type, `${packageId}::share::Share`. */
56
+ shareType: string;
57
+ /** The `TreasuryCap<Share>` object id (held by the treasury-cap recipient). */
58
+ treasuryCapId: string;
59
+ /** Net gas (MIST) attributed to this currency, if tracked per-item (else 0). */
60
+ gasUsed: number;
61
+ }
62
+
63
+ export interface ShareCurrencyMeta {
64
+ name: string;
65
+ description: string;
66
+ iconUrl?: string;
67
+ }
68
+
69
+ // ── Bytecode patching ────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Patches the `INITIALIZER` constant in the share module bytecode, replacing the
73
+ * 32-byte zero-address placeholder with `initializerAddress`. The template is left
74
+ * untouched; a fresh patched copy is returned. `share::initialize` asserts the
75
+ * sender equals this address, so it must be the signer that will initialize.
76
+ */
77
+ export function patchInitializer(template: PackageBytecode, initializerAddress: string): PackageBytecode {
78
+ const moduleBytes = fromBase64(template.modules[0]!);
79
+
80
+ // BCS-encoded vector<u8> of 32 zero bytes: [length=32, 0×32].
81
+ const oldValue = new Uint8Array(33);
82
+ oldValue[0] = 32;
83
+
84
+ // BCS-encoded vector<u8> of the initializer's 32 address bytes.
85
+ const newValue = new Uint8Array(33);
86
+ newValue[0] = 32;
87
+ newValue.set(fromHex(initializerAddress), 1);
88
+
89
+ const patched = update_constants(moduleBytes, newValue, oldValue, "Vector(U8)");
90
+ return { ...template, modules: [toBase64(patched)] };
91
+ }
92
+
93
+ // ── Single (sequential) ──────────────────────────────────────────────────────
94
+
95
+ export interface CreateShareCurrencyParams {
96
+ /** The share bytecode template. Defaults to the embedded {@link SHARE_TEMPLATE}. */
97
+ template?: PackageBytecode;
98
+ name: string;
99
+ description: string;
100
+ iconUrl?: string;
101
+ /** Address baked in as INITIALIZER; must be the signer. Defaults to the signer. */
102
+ initializerAddress?: string;
103
+ /** Recipient of the minted `TreasuryCap`. Defaults to the signer. */
104
+ treasuryCapRecipient?: string;
105
+ }
106
+
107
+ /** Publishes + initializes a fresh share currency in two sequential txs. */
108
+ export async function createShareCurrency(
109
+ client: ClientWithCoreApi,
110
+ signer: Signer,
111
+ params: CreateShareCurrencyParams,
112
+ ): Promise<ShareCurrency> {
113
+ const signerAddress = signer.toSuiAddress();
114
+ const initializer = params.initializerAddress ?? signerAddress;
115
+ const recipient = params.treasuryCapRecipient ?? signerAddress;
116
+
117
+ // Tx 1: publish the share package with the initializer baked in.
118
+ const patched = patchInitializer(params.template ?? SHARE_TEMPLATE, initializer);
119
+ const publishRes = await execThunks(client, signer, publishShareCurrency(patched));
120
+ const packageId = publishedPackageId(publishRes);
121
+
122
+ // Tx 2: initialize → creates the Currency object + transfers the TreasuryCap.
123
+ const initRes = await execThunks(
124
+ client,
125
+ signer,
126
+ initializeShareCurrency({
127
+ shareCurrencyPackageId: packageId,
128
+ name: params.name,
129
+ description: params.description,
130
+ iconUrl: params.iconUrl ?? "",
131
+ treasuryCapRecipient: recipient,
132
+ }),
133
+ );
134
+
135
+ return {
136
+ packageId,
137
+ currencyId: createdByType(initRes, CURRENCY_TYPE),
138
+ shareType: `${packageId}::share::Share`,
139
+ treasuryCapId: createdByType(initRes, TREASURY_CAP_TYPE),
140
+ gasUsed: publishRes.gasUsed + initRes.gasUsed,
141
+ };
142
+ }
143
+
144
+ // ── Batched (parallel) ─────────────────────────────────────────────────────
145
+
146
+ /** Splits an array into fixed-size chunks. */
147
+ function chunk<T>(items: T[], size: number): T[][] {
148
+ const out: T[][] = [];
149
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
150
+ return out;
151
+ }
152
+
153
+ /**
154
+ * Publishes `count` fresh share-currency packages, batched at ≤5 publishes per PTB
155
+ * and run concurrently through the executor. Returns the published package ids
156
+ * (fungible — order is not meaningful; the caller assigns them to work slots).
157
+ */
158
+ export async function publishShareCurrencies(
159
+ executor: ParallelTransactionExecutor,
160
+ initializerAddress: string,
161
+ count: number,
162
+ template: PackageBytecode = SHARE_TEMPLATE,
163
+ ): Promise<{ packageIds: string[]; gasUsed: number }> {
164
+ if (count <= 0) return { packageIds: [], gasUsed: 0 };
165
+
166
+ // One patched template, reused for every publish (identical bytecode → distinct packages).
167
+ const patched = patchInitializer(template, initializerAddress);
168
+
169
+ const batchSizes: number[] = [];
170
+ for (let remaining = count; remaining > 0; remaining -= PUBLISHES_PER_PTB) {
171
+ batchSizes.push(Math.min(PUBLISHES_PER_PTB, remaining));
172
+ }
173
+
174
+ const results = await Promise.all(
175
+ batchSizes.map((n) => executeViaExecutor(executor, ...Array.from({ length: n }, () => publishShareCurrency(patched)))),
176
+ );
177
+
178
+ const packageIds = results.flatMap(allPublishedPackageIds);
179
+ const gasUsed = results.reduce((sum, r) => sum + r.gasUsed, 0);
180
+ if (packageIds.length !== count) {
181
+ throw new Error(`Expected ${count} published share packages, got ${packageIds.length}.`);
182
+ }
183
+ return { packageIds, gasUsed };
184
+ }
185
+
186
+ /** Re-associates one init PTB's created objects back to their packages by share type. */
187
+ function currenciesFromResult(res: ExecResult, batchPkgs: string[]): ShareCurrency[] {
188
+ const currencies = allCreatedByType(res, CURRENCY_TYPE);
189
+ const treasuries = allCreatedByType(res, TREASURY_CAP_TYPE);
190
+ return batchPkgs.map((pkg) => {
191
+ const shareType = `${pkg}::share::Share`;
192
+ const currency = currencies.find((o) => o.objectType.includes(shareType));
193
+ const treasury = treasuries.find((o) => o.objectType.includes(shareType));
194
+ if (!currency) throw new Error(`No Currency<${shareType}> created during initialize.`);
195
+ if (!treasury) throw new Error(`No TreasuryCap<${shareType}> created during initialize.`);
196
+ return { packageId: pkg, currencyId: currency.objectId, shareType, treasuryCapId: treasury.objectId, gasUsed: 0 };
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Initializes each published package into a `Currency<Share>` + `TreasuryCap`,
202
+ * batched and run concurrently. `metaOf` supplies each package's display metadata,
203
+ * so a currency destined for a specific work is named for it.
204
+ *
205
+ * `share::initialize` is NON-idempotent (`coin_registry` registers one `Currency`
206
+ * per type), so a batch must never be re-run. Batches run under `Promise.allSettled`
207
+ * and `onBatch` fires for each SUCCEEDED batch as it lands — letting the caller
208
+ * persist that batch immediately. If any batch fails, the succeeded ones are already
209
+ * reported and this throws, so a resume re-initializes only the still-missing packages.
210
+ */
211
+ export async function initializeShareCurrencies(
212
+ executor: ParallelTransactionExecutor,
213
+ signerAddress: string,
214
+ packageIds: string[],
215
+ metaOf: (packageId: string) => ShareCurrencyMeta,
216
+ onBatch?: (currencies: ShareCurrency[], gasUsed: number) => void | Promise<void>,
217
+ ): Promise<{ currencies: ShareCurrency[]; gasUsed: number }> {
218
+ if (packageIds.length === 0) return { currencies: [], gasUsed: 0 };
219
+
220
+ const batches = chunk(packageIds, INITS_PER_PTB);
221
+ const settled = await Promise.allSettled(
222
+ batches.map(async (batchPkgs) => {
223
+ const res = await executeViaExecutor(
224
+ executor,
225
+ ...batchPkgs.map((pkg) => {
226
+ const meta = metaOf(pkg);
227
+ return initializeShareCurrency({
228
+ shareCurrencyPackageId: pkg,
229
+ name: meta.name,
230
+ description: meta.description,
231
+ iconUrl: meta.iconUrl ?? "",
232
+ treasuryCapRecipient: signerAddress,
233
+ });
234
+ }),
235
+ );
236
+ const batchCurrencies = currenciesFromResult(res, batchPkgs);
237
+ if (onBatch) await onBatch(batchCurrencies, res.gasUsed);
238
+ return { batchCurrencies, gasUsed: res.gasUsed };
239
+ }),
240
+ );
241
+
242
+ const currencies: ShareCurrency[] = [];
243
+ const errors: unknown[] = [];
244
+ let gasUsed = 0;
245
+ for (const s of settled) {
246
+ if (s.status === "fulfilled") {
247
+ currencies.push(...s.value.batchCurrencies);
248
+ gasUsed += s.value.gasUsed;
249
+ } else {
250
+ errors.push(s.reason);
251
+ }
252
+ }
253
+ if (errors.length > 0) {
254
+ const msg = errors.map((e) => (e instanceof Error ? e.message : String(e))).join("; ");
255
+ throw new Error(`${errors.length}/${batches.length} initialize batch(es) failed: ${msg}`);
256
+ }
257
+ return { currencies, gasUsed };
258
+ }