@interest-protocol/blast-core 0.1.0 → 0.2.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 (101) hide show
  1. package/dist/aborts.d.ts +13 -0
  2. package/dist/aborts.d.ts.map +1 -0
  3. package/dist/aborts.js +32 -0
  4. package/dist/aborts.js.map +1 -0
  5. package/dist/bcs.d.ts +226 -7
  6. package/dist/bcs.d.ts.map +1 -1
  7. package/dist/bcs.js +227 -1
  8. package/dist/bcs.js.map +1 -1
  9. package/dist/bps.d.ts +25 -0
  10. package/dist/bps.d.ts.map +1 -0
  11. package/dist/bps.js +64 -0
  12. package/dist/bps.js.map +1 -0
  13. package/dist/bytecode-template.d.ts +14 -3
  14. package/dist/bytecode-template.d.ts.map +1 -1
  15. package/dist/bytecode-template.js +61 -6
  16. package/dist/bytecode-template.js.map +1 -1
  17. package/dist/deployment.d.ts +59 -0
  18. package/dist/deployment.d.ts.map +1 -0
  19. package/dist/deployment.js +172 -0
  20. package/dist/deployment.js.map +1 -0
  21. package/dist/effects.d.ts +69 -0
  22. package/dist/effects.d.ts.map +1 -0
  23. package/dist/effects.js +143 -0
  24. package/dist/effects.js.map +1 -0
  25. package/dist/fee-split.d.ts +55 -0
  26. package/dist/fee-split.d.ts.map +1 -0
  27. package/dist/fee-split.js +69 -0
  28. package/dist/fee-split.js.map +1 -0
  29. package/dist/index.d.ts +10 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +10 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/integers.d.ts +2 -0
  34. package/dist/integers.d.ts.map +1 -1
  35. package/dist/integers.js +8 -0
  36. package/dist/integers.js.map +1 -1
  37. package/dist/merkle.d.ts +6 -1
  38. package/dist/merkle.d.ts.map +1 -1
  39. package/dist/merkle.js +8 -1
  40. package/dist/merkle.js.map +1 -1
  41. package/dist/migration-math.d.ts +13 -2
  42. package/dist/migration-math.d.ts.map +1 -1
  43. package/dist/migration-math.js +34 -6
  44. package/dist/migration-math.js.map +1 -1
  45. package/dist/migration.d.ts +25 -0
  46. package/dist/migration.d.ts.map +1 -0
  47. package/dist/migration.js +43 -0
  48. package/dist/migration.js.map +1 -0
  49. package/dist/payout.d.ts +21 -0
  50. package/dist/payout.d.ts.map +1 -0
  51. package/dist/payout.js +36 -0
  52. package/dist/payout.js.map +1 -0
  53. package/dist/protocol-fees.d.ts +63 -0
  54. package/dist/protocol-fees.d.ts.map +1 -0
  55. package/dist/protocol-fees.js +117 -0
  56. package/dist/protocol-fees.js.map +1 -0
  57. package/dist/rewards/allocations.d.ts +16 -17
  58. package/dist/rewards/allocations.d.ts.map +1 -1
  59. package/dist/rewards/allocations.js +47 -43
  60. package/dist/rewards/allocations.js.map +1 -1
  61. package/dist/roles.d.ts +17 -0
  62. package/dist/roles.d.ts.map +1 -0
  63. package/dist/roles.js +32 -0
  64. package/dist/roles.js.map +1 -0
  65. package/dist/treasury.d.ts +58 -0
  66. package/dist/treasury.d.ts.map +1 -0
  67. package/dist/treasury.js +61 -0
  68. package/dist/treasury.js.map +1 -0
  69. package/package.json +39 -11
  70. package/src/aborts.ts +38 -0
  71. package/src/bcs.ts +374 -1
  72. package/src/bps.ts +92 -0
  73. package/src/bytecode-template.ts +78 -6
  74. package/src/deployment.ts +230 -0
  75. package/src/effects.ts +247 -0
  76. package/src/fee-split.ts +103 -0
  77. package/src/index.ts +10 -1
  78. package/src/integers.ts +15 -0
  79. package/src/merkle.ts +11 -1
  80. package/src/migration-math.ts +38 -6
  81. package/src/migration.ts +69 -0
  82. package/src/payout.ts +57 -0
  83. package/src/protocol-fees.ts +174 -0
  84. package/src/rewards/allocations.ts +52 -67
  85. package/src/roles.ts +39 -0
  86. package/src/treasury.ts +94 -0
  87. package/dist/rewards/keeper.d.ts +0 -17
  88. package/dist/rewards/keeper.d.ts.map +0 -1
  89. package/dist/rewards/keeper.js +0 -21
  90. package/dist/rewards/keeper.js.map +0 -1
  91. package/dist/rewards/state.d.ts +0 -55
  92. package/dist/rewards/state.d.ts.map +0 -1
  93. package/dist/rewards/state.js +0 -80
  94. package/dist/rewards/state.js.map +0 -1
  95. package/dist/timelock.d.ts +0 -15
  96. package/dist/timelock.d.ts.map +0 -1
  97. package/dist/timelock.js +0 -30
  98. package/dist/timelock.js.map +0 -1
  99. package/src/rewards/keeper.ts +0 -45
  100. package/src/rewards/state.ts +0 -113
  101. package/src/timelock.ts +0 -47
package/src/bcs.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BcsType } from "@mysten/sui/bcs";
1
2
  import { bcs } from "@mysten/sui/bcs";
2
3
  import {
3
4
  normalizeStructTag,
@@ -18,11 +19,22 @@ const asciiStringBcs = bcs.struct("AsciiString", {
18
19
  bytes: bcs.vector(bcs.u8()),
19
20
  });
20
21
 
22
+ /**
23
+ * `std::type_name::TypeName`. Move stores the type with every address as 64
24
+ * hex digits and no `0x`; the SDK reads and writes normalized struct tags, so
25
+ * `serialize(parse(bytes))` returns the Move bytes.
26
+ */
21
27
  export const moveTypeNameBcs = bcs
22
28
  .struct("TypeName", { name: asciiStringBcs })
23
29
  .transform({
24
30
  input: (value: string) => ({
25
- name: { bytes: Array.from(new TextEncoder().encode(value)) },
31
+ name: {
32
+ bytes: Array.from(
33
+ new TextEncoder().encode(
34
+ normalizeStructTag(value).replace(/0x([0-9a-f]{64})/g, "$1"),
35
+ ),
36
+ ),
37
+ },
26
38
  }),
27
39
  output: (value) =>
28
40
  normalizeStructTag(
@@ -35,7 +47,368 @@ export const u64Bcs = bcs.u64().transform({
35
47
  output: (value) => BigInt(value),
36
48
  });
37
49
 
50
+ /** A `Balance<T>` decoded as its atomic amount. */
51
+ export const balanceBcs = bcs.struct("Balance", { value: u64Bcs }).transform({
52
+ input: (value: bigint) => ({ value }),
53
+ output: (balance) => balance.value,
54
+ });
55
+
38
56
  export const addressBcs = bcs.Address.transform({
39
57
  input: (value: string) => normalizeSuiAddress(value),
40
58
  output: (value) => normalizeSuiAddress(value),
41
59
  });
60
+
61
+ /**
62
+ * A zero-field Move key such as `TradeFeesKey()`: Move gives it one
63
+ * `dummy_field: bool`, so it encodes as one byte.
64
+ */
65
+ export const unitKeyBcs = bcs
66
+ .struct("Key", { dummy_field: bcs.bool() })
67
+ .transform({
68
+ input: (_: null) => ({ dummy_field: false }),
69
+ output: (): null => null,
70
+ });
71
+
72
+ /** `0x2::vec_set::VecSet<E>`: its `contents` vector, decoded as the members. */
73
+ export function vecSetBcs<E, Input>(element: BcsType<E, Input>) {
74
+ return bcs.struct("VecSet", { contents: bcs.vector(element) }).transform({
75
+ input: (members: Input[]) => ({ contents: members }),
76
+ output: (set): E[] => set.contents,
77
+ });
78
+ }
79
+
80
+ /** `0x2::table::Table<K, V>`: its UID and size; entries are dynamic fields of `id`. */
81
+ export const tableBcs = bcs.struct("Table", {
82
+ id: suiObjectIdBcs,
83
+ size: u64Bcs,
84
+ });
85
+
86
+ /** `blast_admin::delayed::Proposal<T>`: the value and the delay in force once it applies. */
87
+ export type Proposal<T> = { value: T; delay_ms: bigint; ready_at_ms: bigint };
88
+
89
+ export function proposalBcs<T, Input>(value: BcsType<T, Input>) {
90
+ return bcs.struct("Proposal", {
91
+ value,
92
+ delay_ms: u64Bcs,
93
+ ready_at_ms: u64Bcs,
94
+ });
95
+ }
96
+
97
+ /** `blast_admin::delayed::Delayed<K, T>`: readiness never applies a value implicitly. */
98
+ export type Delayed<K, T> = {
99
+ parent: string;
100
+ key: K;
101
+ value: T;
102
+ delay_ms: bigint;
103
+ pending: Proposal<T> | null;
104
+ };
105
+
106
+ /** Exact `blast_admin::delayed::Delayed<K, T>` field order. */
107
+ export function delayedBcs<K, KeyInput, T, Input>(
108
+ key: BcsType<K, KeyInput>,
109
+ value: BcsType<T, Input>,
110
+ ) {
111
+ return bcs.struct("Delayed", {
112
+ parent: suiObjectIdBcs,
113
+ key,
114
+ value,
115
+ delay_ms: u64Bcs,
116
+ pending: bcs.option(proposalBcs(value)),
117
+ });
118
+ }
119
+
120
+ /** `blast_admin::delayed_action::Change<A>`: an action, or a change of delay only. */
121
+ export type DelayedChange<A> =
122
+ | { kind: "Action"; action: A }
123
+ | { kind: "Delay" };
124
+
125
+ export function delayedChangeBcs<A, Input>(action: BcsType<A, Input>) {
126
+ return bcs.enum("Change", { Action: action, Delay: null }).transform({
127
+ input: (change: DelayedChange<Input>) =>
128
+ change.kind === "Action"
129
+ ? { Action: change.action }
130
+ : { Delay: true as const },
131
+ output: (change): DelayedChange<A> =>
132
+ change.$kind === "Action"
133
+ ? { kind: "Action", action: change.Action as A }
134
+ : { kind: "Delay" },
135
+ });
136
+ }
137
+
138
+ /** `blast_admin::delayed_action::DelayedAction<K, A>`: a one-shot action slot. */
139
+ export type DelayedAction<K, A> = {
140
+ parent: string;
141
+ key: K;
142
+ delay_ms: bigint;
143
+ pending: Proposal<DelayedChange<A>> | null;
144
+ };
145
+
146
+ export function delayedActionBcs<K, KeyInput, A, Input>(
147
+ key: BcsType<K, KeyInput>,
148
+ action: BcsType<A, Input>,
149
+ ) {
150
+ return bcs.struct("DelayedAction", {
151
+ parent: suiObjectIdBcs,
152
+ key,
153
+ delay_ms: u64Bcs,
154
+ pending: bcs.option(proposalBcs(delayedChangeBcs(action))),
155
+ });
156
+ }
157
+
158
+ /** `blast_admin::delayed::PendingDelay`: a proposed next delay, ready from `ready_at_ms`. */
159
+ export type PendingDelay = { delay_ms: bigint; ready_at_ms: bigint };
160
+
161
+ export const pendingDelayBcs = bcs.struct("PendingDelay", {
162
+ delay_ms: u64Bcs,
163
+ ready_at_ms: u64Bcs,
164
+ });
165
+
166
+ /**
167
+ * `blast_admin::delayed::Deadline`: a pending admission's or entry change's
168
+ * fixed deadline and the applied delay it was scheduled under; a later delay
169
+ * change never moves it.
170
+ */
171
+ export type Deadline = { delay_ms: bigint; ready_at_ms: bigint };
172
+
173
+ export const deadlineBcs = bcs.struct("Deadline", {
174
+ delay_ms: u64Bcs,
175
+ ready_at_ms: u64Bcs,
176
+ });
177
+
178
+ /** `blast_admin::delayed_set::DelayedSet<K, E>`: admissions wait; removals are immediate. */
179
+ export type DelayedSet<K, E> = {
180
+ parent: string;
181
+ key: K;
182
+ members: E[];
183
+ delay_ms: bigint;
184
+ admissions: { member: E; deadline: Deadline }[];
185
+ pending_delay: PendingDelay | null;
186
+ };
187
+
188
+ /**
189
+ * `blast_admin::delayed::max_delay_ms`: no delayed value, action, set or map
190
+ * accepts a longer delay, in milliseconds (30 days); one aborts with
191
+ * `blast_admin` error 15 (`EInvalidDelay`).
192
+ */
193
+ export const MAX_DELAY_MS = 2_592_000_000n;
194
+
195
+ /**
196
+ * `delayed::is_guarded`: the applied delay is nonzero and no proposal would
197
+ * set it to zero. `package_admin::unpause` requires every policy guarded.
198
+ */
199
+ export function isDelayedGuarded(self: Delayed<unknown, unknown>): boolean {
200
+ return self.delay_ms > 0n && (self.pending?.delay_ms ?? 1n) > 0n;
201
+ }
202
+
203
+ /** `delayed_action::is_guarded`, as `isDelayedGuarded`. */
204
+ export function isDelayedActionGuarded(
205
+ self: DelayedAction<unknown, unknown>,
206
+ ): boolean {
207
+ return self.delay_ms > 0n && (self.pending?.delay_ms ?? 1n) > 0n;
208
+ }
209
+
210
+ /** `delayed_set::is_guarded`: also no admission scheduled earlier is pending. */
211
+ export function isDelayedSetGuarded(
212
+ self: DelayedSet<unknown, unknown>,
213
+ ): boolean {
214
+ return (
215
+ self.delay_ms > 0n &&
216
+ self.admissions.length === 0 &&
217
+ (self.pending_delay?.delay_ms ?? 1n) > 0n
218
+ );
219
+ }
220
+
221
+ export function delayedSetBcs<K, KeyInput, E, Input>(
222
+ key: BcsType<K, KeyInput>,
223
+ member: BcsType<E, Input>,
224
+ ) {
225
+ return bcs
226
+ .struct("DelayedSet", {
227
+ parent: suiObjectIdBcs,
228
+ key,
229
+ members: bcs.struct("VecSet", { contents: bcs.vector(member) }),
230
+ delay_ms: u64Bcs,
231
+ admissions: bcs.struct("VecMap", {
232
+ contents: bcs.vector(
233
+ bcs.struct("Entry", { key: member, value: deadlineBcs }),
234
+ ),
235
+ }),
236
+ pending_delay: bcs.option(pendingDelayBcs),
237
+ })
238
+ .transform({
239
+ input: (set: {
240
+ parent: string;
241
+ key: KeyInput;
242
+ members: Input[];
243
+ delay_ms: bigint;
244
+ admissions: { member: Input; deadline: Deadline }[];
245
+ pending_delay: PendingDelay | null;
246
+ }) => ({
247
+ parent: set.parent,
248
+ key: set.key,
249
+ members: { contents: set.members },
250
+ delay_ms: set.delay_ms,
251
+ admissions: {
252
+ contents: set.admissions.map(({ member, deadline }) => ({
253
+ key: member,
254
+ value: deadline,
255
+ })),
256
+ },
257
+ pending_delay: set.pending_delay,
258
+ }),
259
+ output: (set): DelayedSet<K, E> => ({
260
+ parent: set.parent,
261
+ key: set.key,
262
+ members: set.members.contents,
263
+ delay_ms: set.delay_ms,
264
+ admissions: set.admissions.contents.map(({ key, value }) => ({
265
+ member: key,
266
+ deadline: value,
267
+ })),
268
+ pending_delay: set.pending_delay,
269
+ }),
270
+ });
271
+ }
272
+
273
+ /** `blast_admin::delayed_set::SetChange<E>`: an admission, or a delay change. */
274
+ export type DelayedSetChange<E> =
275
+ | { kind: "Admission"; member: E }
276
+ | { kind: "Delay" };
277
+
278
+ export function delayedSetChangeBcs<E, Input>(member: BcsType<E, Input>) {
279
+ return bcs.enum("SetChange", { Admission: member, Delay: null }).transform({
280
+ input: (change: DelayedSetChange<Input>) =>
281
+ change.kind === "Admission"
282
+ ? { Admission: change.member }
283
+ : { Delay: true as const },
284
+ output: (change): DelayedSetChange<E> =>
285
+ change.$kind === "Admission"
286
+ ? { kind: "Admission", member: change.Admission as E }
287
+ : { kind: "Delay" },
288
+ });
289
+ }
290
+
291
+ /** `blast_admin::delayed_map::EntryProposal<V>`: a value, or null to clear, and its fixed deadline. */
292
+ export type EntryProposal<V> = { value: V | null; deadline: Deadline };
293
+
294
+ export function entryProposalBcs<V, Input>(value: BcsType<V, Input>) {
295
+ return bcs.struct("EntryProposal", {
296
+ value: bcs.option(value),
297
+ deadline: deadlineBcs,
298
+ });
299
+ }
300
+
301
+ /** `blast_admin::delayed_map::MapChange<E, V>`: an entry change, or a delay change. */
302
+ export type DelayedMapChange<E, V> =
303
+ | { kind: "Entry"; entry: E; value: V | null }
304
+ | { kind: "Delay" };
305
+
306
+ export function delayedMapChangeBcs<E, EntryInput, V, Input>(
307
+ entry: BcsType<E, EntryInput>,
308
+ value: BcsType<V, Input>,
309
+ ) {
310
+ return bcs
311
+ .enum("MapChange", {
312
+ Entry: bcs.struct("Entry", { entry, value: bcs.option(value) }),
313
+ Delay: null,
314
+ })
315
+ .transform({
316
+ input: (change: DelayedMapChange<EntryInput, Input>) =>
317
+ change.kind === "Entry"
318
+ ? { Entry: { entry: change.entry, value: change.value } }
319
+ : { Delay: true as const },
320
+ output: (change): DelayedMapChange<E, V> =>
321
+ change.$kind === "Entry"
322
+ ? {
323
+ kind: "Entry",
324
+ entry: change.Entry.entry as E,
325
+ value: change.Entry.value as V | null,
326
+ }
327
+ : { kind: "Delay" },
328
+ });
329
+ }
330
+
331
+ /**
332
+ * `blast_admin::delayed_map::DelayedMap<K, E, V>`: applied entries in a
333
+ * `Table` (read by dynamic field), one delay, and inline pending changes.
334
+ */
335
+ export type DelayedMap<K, E, V> = {
336
+ parent: string;
337
+ key: K;
338
+ entries: { id: string; size: bigint };
339
+ delay_ms: bigint;
340
+ pending: { entry: E; proposal: EntryProposal<V> }[];
341
+ pending_delay: PendingDelay | null;
342
+ };
343
+
344
+ export function delayedMapBcs<K, KeyInput, E, EntryInput, V, Input>(
345
+ key: BcsType<K, KeyInput>,
346
+ entry: BcsType<E, EntryInput>,
347
+ value: BcsType<V, Input>,
348
+ ) {
349
+ return bcs
350
+ .struct("DelayedMap", {
351
+ parent: suiObjectIdBcs,
352
+ key,
353
+ entries: tableBcs,
354
+ delay_ms: u64Bcs,
355
+ pending: bcs.struct("VecMap", {
356
+ contents: bcs.vector(
357
+ bcs.struct("Entry", { key: entry, value: entryProposalBcs(value) }),
358
+ ),
359
+ }),
360
+ pending_delay: bcs.option(pendingDelayBcs),
361
+ })
362
+ .transform({
363
+ input: (map: {
364
+ parent: string;
365
+ key: KeyInput;
366
+ entries: { id: string; size: bigint };
367
+ delay_ms: bigint;
368
+ pending: {
369
+ entry: EntryInput;
370
+ proposal: { value: Input | null; deadline: Deadline };
371
+ }[];
372
+ pending_delay: PendingDelay | null;
373
+ }) => ({
374
+ parent: map.parent,
375
+ key: map.key,
376
+ entries: map.entries,
377
+ delay_ms: map.delay_ms,
378
+ pending: {
379
+ contents: map.pending.map(({ entry, proposal }) => ({
380
+ key: entry,
381
+ value: proposal,
382
+ })),
383
+ },
384
+ pending_delay: map.pending_delay,
385
+ }),
386
+ output: (map): DelayedMap<K, E, V> => ({
387
+ parent: map.parent,
388
+ key: map.key,
389
+ entries: map.entries,
390
+ delay_ms: map.delay_ms,
391
+ pending: map.pending.contents.map(({ key, value }) => ({
392
+ entry: key,
393
+ proposal: value as EntryProposal<V>,
394
+ })),
395
+ pending_delay: map.pending_delay,
396
+ }),
397
+ });
398
+ }
399
+
400
+ /**
401
+ * A `DelayedSet` admission view: the admitted members, the applied delay, the
402
+ * proposed next delay, and each scheduled admission's deadline by member. Used
403
+ * for venue, witness and claim-signer allowlists alike.
404
+ */
405
+ export function admission<E extends string>(set: DelayedSet<unknown, E>) {
406
+ return {
407
+ members: [...set.members],
408
+ delayMs: set.delay_ms,
409
+ pendingDelay: set.pending_delay,
410
+ pendingMembers: Object.fromEntries(
411
+ set.admissions.map(({ member, deadline }) => [member, deadline]),
412
+ ) as Readonly<Record<E, Deadline>>,
413
+ };
414
+ }
package/src/bps.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import { u64Bcs } from "./bcs.ts";
3
+ import type { BasisPoints } from "./integers.ts";
4
+ import {
5
+ BASIS_POINTS_DENOMINATOR,
6
+ mulDivUp,
7
+ requireBasisPoints,
8
+ requireU64,
9
+ } from "./integers.ts";
10
+
11
+ /** `blast_core::constants::max_lp_fee_bps`: Blast's largest LP-fee share of each asset. */
12
+ export const MAX_LP_FEE_BPS = 3_000n;
13
+ /** `blast_core::constants::max_routes`: the most routes one fee split holds. */
14
+ export const MAX_ROUTES = 20;
15
+ /** `blast_core::constants::max_vesting_duration_ms`: the technical bound on a schedule. */
16
+ export const MAX_VESTING_DURATION_MS = 2n ** 63n;
17
+
18
+ /** BCS layout of the positional `blast_core::bps::Bps(u64)`. */
19
+ export const bpsBcs = bcs.struct("Bps", { value: u64Bcs }).transform({
20
+ input: (value: bigint) => ({ value: requireBps(value) }),
21
+ output: (value) => value.value,
22
+ });
23
+
24
+ /** Mirrors `bps::new`: a rate from zero through 10,000 basis points. */
25
+ export function requireBps(value: bigint): BasisPoints {
26
+ return requireBasisPoints("bps", value);
27
+ }
28
+
29
+ /**
30
+ * Mirrors `bps::apply_up`: `ceil(amount * bps / 10,000)` over atomic units,
31
+ * at most `amount`.
32
+ */
33
+ export function applyBpsUp(amount: bigint, bps: bigint): bigint {
34
+ return mulDivUp(
35
+ requireU64("amount", amount),
36
+ requireBps(bps),
37
+ BASIS_POINTS_DENOMINATOR,
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Mirrors `bps::apportion`: largest-remainder apportionment of `total` atoms
43
+ * over `rates`, which must sum to exactly 10,000. The `roundedUp` part takes
44
+ * its ceiling and every other part its floor; the leftover atoms go one each
45
+ * to the largest remaining remainders, earlier parts first. The amounts sum
46
+ * to `total`. Throws where the contract aborts (`EInvalidApportionment`).
47
+ */
48
+ export function apportionBps(
49
+ rates: readonly bigint[],
50
+ total: bigint,
51
+ roundedUp: number | null,
52
+ ): bigint[] {
53
+ requireU64("total", total);
54
+ rates.forEach(requireBps);
55
+ if (
56
+ rates.reduce((sum, rate) => sum + rate, 0n) !== BASIS_POINTS_DENOMINATOR ||
57
+ (roundedUp !== null &&
58
+ !(
59
+ Number.isInteger(roundedUp) &&
60
+ roundedUp >= 0 &&
61
+ roundedUp < rates.length
62
+ ))
63
+ ) {
64
+ throw new RangeError(
65
+ "apportioned rates must sum to 10,000 and name a rounded-up part inside them",
66
+ );
67
+ }
68
+ const products = rates.map((rate) => total * rate);
69
+ const amounts = products.map((product, index) =>
70
+ index === roundedUp
71
+ ? (product + BASIS_POINTS_DENOMINATOR - 1n) / BASIS_POINTS_DENOMINATOR
72
+ : product / BASIS_POINTS_DENOMINATOR,
73
+ );
74
+ const order = products
75
+ .map((product, index) => ({
76
+ index,
77
+ remainder: product % BASIS_POINTS_DENOMINATOR,
78
+ }))
79
+ .filter(({ index }) => index !== roundedUp)
80
+ .sort((left, right) =>
81
+ left.remainder === right.remainder
82
+ ? left.index - right.index
83
+ : left.remainder > right.remainder
84
+ ? -1
85
+ : 1,
86
+ );
87
+ const leftover = Number(total - amounts.reduce((sum, a) => sum + a, 0n));
88
+ for (const { index } of order.slice(0, leftover)) {
89
+ amounts[index] = (amounts[index] ?? 0n) + 1n;
90
+ }
91
+ return amounts;
92
+ }
@@ -2,6 +2,12 @@ import initBytecodeTemplate, {
2
2
  deserialize,
3
3
  } from "@mysten/move-bytecode-template";
4
4
  import { bcs } from "@mysten/sui/bcs";
5
+ import {
6
+ isValidSuiObjectId,
7
+ MOVE_STDLIB_ADDRESS,
8
+ normalizeSuiObjectId,
9
+ SUI_FRAMEWORK_ADDRESS,
10
+ } from "@mysten/sui/utils";
5
11
 
6
12
  // Shared by every generated-package template: one WASM initialization, module
7
13
  // table checks, and constant patching by type and default value.
@@ -32,6 +38,12 @@ export type BytecodeTemplateInitOptions = {
32
38
  module_or_path?: unknown;
33
39
  };
34
40
 
41
+ const SYSTEM_ADDRESSES = new Set([
42
+ normalizeSuiObjectId("0x0"),
43
+ MOVE_STDLIB_ADDRESS,
44
+ SUI_FRAMEWORK_ADDRESS,
45
+ ]);
46
+
35
47
  let initialization: Promise<void> | undefined;
36
48
 
37
49
  /**
@@ -54,6 +66,66 @@ export function initializeBytecodeTemplate(
54
66
  return initialization;
55
67
  }
56
68
 
69
+ /** A deployed package's original ID that a generated package links: valid and not a system address. */
70
+ export function requireDependencyPackageId(
71
+ name: string,
72
+ packageId: string,
73
+ ): string {
74
+ if (typeof packageId !== "string") {
75
+ throw new TypeError(`${name} must be a string`);
76
+ }
77
+ const normalized = normalizeSuiObjectId(packageId);
78
+ if (!isValidSuiObjectId(normalized)) {
79
+ throw new TypeError(`${name} must be a valid Sui package ID`);
80
+ }
81
+ if (SYSTEM_ADDRESSES.has(normalized)) {
82
+ throw new RangeError(`${name} must identify a deployed Blast package`);
83
+ }
84
+ return normalized;
85
+ }
86
+
87
+ /**
88
+ * Binds every non-system address of the template to the deployed package
89
+ * whose modules it names: `bindings` maps a module name to that package's
90
+ * original ID. Each placeholder address must be claimed by exactly one
91
+ * package through the modules it holds, and each binding must claim one, so
92
+ * a template that gains or loses a dependency fails here instead of linking
93
+ * a placeholder.
94
+ */
95
+ export function patchDependencyAddresses(
96
+ label: string,
97
+ compiled: CompiledModule,
98
+ bindings: Readonly<Record<string, string>>,
99
+ ): void {
100
+ const claimed = new Set<string>();
101
+ compiled.address_identifiers.forEach((address, index) => {
102
+ if (SYSTEM_ADDRESSES.has(normalizeSuiObjectId(address))) return;
103
+ const packageIds = new Set(
104
+ compiled.module_handles.flatMap((handle) => {
105
+ const module = compiled.identifiers[handle.name];
106
+ const packageId =
107
+ handle.address === index && module !== undefined
108
+ ? bindings[module]
109
+ : undefined;
110
+ if (packageId !== undefined && module !== undefined) {
111
+ claimed.add(module);
112
+ }
113
+ return packageId === undefined ? [] : [packageId];
114
+ }),
115
+ );
116
+ if (packageIds.size !== 1) {
117
+ throw new TypeError(`${label} has an unbound dependency address`);
118
+ }
119
+ const [packageId] = packageIds as Set<string>;
120
+ compiled.address_identifiers[index] = (packageId as string).slice(2);
121
+ });
122
+ for (const module of Object.keys(bindings)) {
123
+ if (!claimed.has(module)) {
124
+ throw new TypeError(`${label} does not link ${module}`);
125
+ }
126
+ }
127
+ }
128
+
57
129
  /** Deserializes a template module and checks the tables patching reads. */
58
130
  export function deserializeTemplate(
59
131
  label: string,
@@ -192,20 +264,20 @@ export function assertIdentifierSlots(
192
264
  }
193
265
 
194
266
  /**
195
- * Derives the module and one-time-witness names from a symbol, the same
196
- * transformation as Blast V1: the witness is the uppercased symbol and the
197
- * module its lowercase form, spaces becoming underscores.
267
+ * Defaults the module to the lowercased symbol, with spaces becoming underscores.
268
+ * Sui requires its one-time witness to be the uppercase module name, including
269
+ * when the caller overrides the module name.
198
270
  */
199
271
  export function deriveTemplateIdentifiers(
200
272
  symbol: string,
201
273
  requestedModuleName?: string,
202
274
  ): { moduleName: string; witnessName: string } {
203
275
  const normalizedSymbol = requireNonEmptyString("symbol", symbol);
204
- const witnessName = normalizedSymbol.toUpperCase().replaceAll(" ", "_");
276
+ const symbolIdentifier = normalizedSymbol.toUpperCase().replaceAll(" ", "_");
205
277
  const moduleName =
206
278
  requestedModuleName ?? normalizedSymbol.toLowerCase().replaceAll(" ", "_");
207
279
 
208
- if (!/^[A-Z][A-Z0-9_]*$/.test(witnessName)) {
280
+ if (!/^[A-Z][A-Z0-9_]*$/.test(symbolIdentifier)) {
209
281
  throw new RangeError(
210
282
  "symbol must produce an uppercase Move witness identifier",
211
283
  );
@@ -215,7 +287,7 @@ export function deriveTemplateIdentifiers(
215
287
  "moduleName must start with a lowercase letter and contain only lowercase letters, digits, or underscores",
216
288
  );
217
289
  }
218
- return { moduleName, witnessName };
290
+ return { moduleName, witnessName: moduleName.toUpperCase() };
219
291
  }
220
292
 
221
293
  export function requireString(name: string, value: string): string {