@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
@@ -0,0 +1,230 @@
1
+ import type {
2
+ Transaction,
3
+ TransactionObjectInput,
4
+ TransactionResult,
5
+ } from "@mysten/sui/transactions";
6
+ import { normalizeStructTag, normalizeSuiObjectId } from "@mysten/sui/utils";
7
+ import { MAX_VESTING_DURATION_MS, requireBps } from "./bps.ts";
8
+ import type { Destination, FeeSplit } from "./fee-split.ts";
9
+ import { normalizeFeeSplit } from "./fee-split.ts";
10
+ import { requireU64 } from "./integers.ts";
11
+ import type { VestingTerms } from "./payout.ts";
12
+ import type { SharedObjectIdentity } from "./sui.ts";
13
+ import { normalizeSharedObjectIdentity } from "./sui.ts";
14
+
15
+ /**
16
+ * The `blast_core` package and its shared `blast_treasury::Treasury`. Calls
17
+ * target `packageId`; types and roles name `originalPackageId`.
18
+ */
19
+ export type CoreDeployment = {
20
+ packageId: string;
21
+ originalPackageId: string;
22
+ treasury: SharedObjectIdentity;
23
+ };
24
+
25
+ export function normalizeCoreDeployment(
26
+ deployment: CoreDeployment,
27
+ ): CoreDeployment {
28
+ return {
29
+ packageId: normalizeSuiObjectId(deployment.packageId),
30
+ originalPackageId: normalizeSuiObjectId(deployment.originalPackageId),
31
+ treasury: normalizeSharedObjectIdentity(
32
+ "core.treasury",
33
+ deployment.treasury,
34
+ ),
35
+ };
36
+ }
37
+
38
+ /** `<core original>::<module>::<name>`, normalized. */
39
+ export function coreType(
40
+ core: Pick<CoreDeployment, "originalPackageId">,
41
+ module: string,
42
+ name: string,
43
+ ): string {
44
+ return normalizeStructTag(
45
+ `${normalizeSuiObjectId(core.originalPackageId)}::${module}::${name}`,
46
+ );
47
+ }
48
+
49
+ /**
50
+ * `blast_core` value constructors and treasury calls. Each method returns a
51
+ * PTB step; the constructors return the value as a transaction result that
52
+ * the product or vault call consuming it takes by value.
53
+ */
54
+ export class CoreCalls {
55
+ readonly deployment: CoreDeployment;
56
+
57
+ constructor(deployment: CoreDeployment) {
58
+ this.deployment = normalizeCoreDeployment(deployment);
59
+ }
60
+
61
+ /** `bps::new`: aborts above 10,000. */
62
+ bps(value: bigint) {
63
+ const bps = requireBps(value);
64
+ return (tx: Transaction): TransactionResult =>
65
+ tx.moveCall({
66
+ target: this.#target("bps", "new"),
67
+ arguments: [tx.pure.u64(bps)],
68
+ });
69
+ }
70
+
71
+ /**
72
+ * `fee_split::new` over one `fee_split::route` per route, each built from
73
+ * its `*_destination` constructor and `bps::new`, in the given order.
74
+ */
75
+ feeSplit(split: FeeSplit) {
76
+ const { routes } = normalizeFeeSplit(split);
77
+ return (tx: Transaction): TransactionResult => {
78
+ const elements = routes.map(({ destination, bps }) =>
79
+ tx.moveCall({
80
+ target: this.#target("fee_split", "route"),
81
+ arguments: [this.#destination(tx, destination), this.bps(bps)(tx)],
82
+ }),
83
+ );
84
+ return tx.moveCall({
85
+ target: this.#target("fee_split", "new"),
86
+ arguments: [
87
+ tx.makeMoveVec({
88
+ type: coreType(this.deployment, "fee_split", "Route"),
89
+ elements,
90
+ }),
91
+ ],
92
+ });
93
+ };
94
+ }
95
+
96
+ /**
97
+ * `payout::vesting_terms`; rejects terms the contract aborts on: positive
98
+ * steps whose total fits `MAX_VESTING_DURATION_MS` and a cliff within it.
99
+ */
100
+ vestingTerms(terms: VestingTerms) {
101
+ const cliff = requireU64("cliffDelayMs", terms.cliffDelayMs);
102
+ const period = requireU64("periodDurationMs", terms.periodDurationMs);
103
+ const periods = requireU64("periods", terms.periods);
104
+ if (
105
+ period === 0n ||
106
+ periods === 0n ||
107
+ period > MAX_VESTING_DURATION_MS / periods ||
108
+ cliff > period * periods
109
+ ) {
110
+ throw new RangeError(
111
+ "vesting needs positive steps whose total and cliff fit the bound",
112
+ );
113
+ }
114
+ return (tx: Transaction): TransactionResult =>
115
+ tx.moveCall({
116
+ target: this.#target("payout", "vesting_terms"),
117
+ arguments: [
118
+ tx.pure.u64(cliff),
119
+ tx.pure.u64(period),
120
+ tx.pure.u64(periods),
121
+ ],
122
+ });
123
+ }
124
+
125
+ /** `payout::recipient`: paid outright when `vestingTerms` is null. */
126
+ recipient(options: {
127
+ destination: string;
128
+ vestingTerms: VestingTerms | null;
129
+ }) {
130
+ const terms = options.vestingTerms
131
+ ? this.vestingTerms(options.vestingTerms)
132
+ : null;
133
+ const termsType = coreType(this.deployment, "payout", "VestingTerms");
134
+ return (tx: Transaction): TransactionResult =>
135
+ tx.moveCall({
136
+ target: this.#target("payout", "recipient"),
137
+ arguments: [
138
+ tx.pure.address(options.destination),
139
+ terms
140
+ ? tx.moveCall({
141
+ target: "0x1::option::some",
142
+ typeArguments: [termsType],
143
+ arguments: [terms(tx)],
144
+ })
145
+ : tx.moveCall({
146
+ target: "0x1::option::none",
147
+ typeArguments: [termsType],
148
+ }),
149
+ ],
150
+ });
151
+ }
152
+
153
+ /**
154
+ * `blast_treasury::schedule_destination`: a `TreasuryAdmin` witness
155
+ * schedules `destination` and the next change's `delayMs`.
156
+ */
157
+ scheduleDestination(options: {
158
+ adminWitness: TransactionObjectInput;
159
+ destination: string;
160
+ delayMs: bigint;
161
+ }) {
162
+ const delayMs = requireU64("delayMs", options.delayMs);
163
+ return (tx: Transaction): void => {
164
+ tx.moveCall({
165
+ target: this.#target("blast_treasury", "schedule_destination"),
166
+ arguments: [
167
+ this.#treasury(tx, true),
168
+ tx.object(options.adminWitness),
169
+ tx.pure.address(options.destination),
170
+ tx.pure.u64(delayMs),
171
+ tx.object.clock(),
172
+ ],
173
+ });
174
+ };
175
+ }
176
+
177
+ /** `blast_treasury::apply_destination`: anyone, once ready. */
178
+ applyDestination() {
179
+ return (tx: Transaction): void => {
180
+ tx.moveCall({
181
+ target: this.#target("blast_treasury", "apply_destination"),
182
+ arguments: [this.#treasury(tx, true), tx.object.clock()],
183
+ });
184
+ };
185
+ }
186
+
187
+ /** `blast_treasury::cancel_destination` with a `CancelAdmin` witness. */
188
+ cancelDestination(options: { adminWitness: TransactionObjectInput }) {
189
+ return (tx: Transaction): void => {
190
+ tx.moveCall({
191
+ target: this.#target("blast_treasury", "cancel_destination"),
192
+ arguments: [this.#treasury(tx, true), tx.object(options.adminWitness)],
193
+ });
194
+ };
195
+ }
196
+
197
+ #destination(tx: Transaction, destination: Destination) {
198
+ switch (destination.kind) {
199
+ case "Address":
200
+ return tx.moveCall({
201
+ target: this.#target("fee_split", "address_destination"),
202
+ arguments: [tx.pure.address(destination.address)],
203
+ });
204
+ case "Owner":
205
+ return tx.moveCall({
206
+ target: this.#target("fee_split", "owner_destination"),
207
+ });
208
+ case "AutoLp":
209
+ return tx.moveCall({
210
+ target: this.#target("fee_split", "auto_lp_destination"),
211
+ });
212
+ case "HolderRewards":
213
+ return tx.moveCall({
214
+ target: this.#target("fee_split", "holder_rewards_destination"),
215
+ });
216
+ case "Burn":
217
+ return tx.moveCall({
218
+ target: this.#target("fee_split", "burn_destination"),
219
+ });
220
+ }
221
+ }
222
+
223
+ #target(module: string, functionName: string): string {
224
+ return `${this.deployment.packageId}::${module}::${functionName}`;
225
+ }
226
+
227
+ #treasury(tx: Transaction, mutable: boolean) {
228
+ return tx.sharedObjectRef({ ...this.deployment.treasury, mutable });
229
+ }
230
+ }
package/src/effects.ts ADDED
@@ -0,0 +1,247 @@
1
+ import type { SuiClientTypes } from "@mysten/sui/client";
2
+ import type { Transaction, TransactionResult } from "@mysten/sui/transactions";
3
+ import {
4
+ normalizeStructTag,
5
+ normalizeSuiAddress,
6
+ normalizeSuiObjectId,
7
+ parseStructTag,
8
+ SUI_FRAMEWORK_ADDRESS,
9
+ } from "@mysten/sui/utils";
10
+ import type { SharedObjectIdentity, SharedObjectReference } from "./sui.ts";
11
+ import { normalizeSharedObjectIdentity } from "./sui.ts";
12
+
13
+ // Effects readers shared by every generated-package publication: a product's
14
+ // coin package creates its currency through the product, which seals the
15
+ // `TreasuryCap` under `blast_core::sealed_treasury_cap`, and sends the
16
+ // `Currency` to the Coin Registry. Effects are the authority for created
17
+ // objects, owners and versions; events never are.
18
+
19
+ /**
20
+ * The parts of one executed transaction these readers need: its effects and
21
+ * the types of the objects they touched (Core API `include: { effects: true,
22
+ * objectTypes: true }`).
23
+ */
24
+ export type TransactionEffectsInput = {
25
+ effects: SuiClientTypes.TransactionEffects;
26
+ objectTypes: Readonly<Record<string, string>>;
27
+ };
28
+
29
+ /** An owned or receiving object at one version. */
30
+ export type ObjectVersionReference = {
31
+ objectId: string;
32
+ version: string;
33
+ digest: string;
34
+ };
35
+
36
+ /** `sui::object::sui_coin_registry_address()`. */
37
+ export const COIN_REGISTRY_ID = normalizeSuiObjectId("0xc");
38
+
39
+ /** The objects a successful transaction created. */
40
+ export function createdObjects(
41
+ result: TransactionEffectsInput,
42
+ ): SuiClientTypes.ChangedObject[] {
43
+ if (!result.effects.status.success) {
44
+ throw new TypeError("transaction did not succeed");
45
+ }
46
+ return result.effects.changedObjects.filter(
47
+ (object) => object.idOperation === "Created",
48
+ );
49
+ }
50
+
51
+ /** The one package a publication created. */
52
+ export function readPublishedPackage(
53
+ created: readonly SuiClientTypes.ChangedObject[],
54
+ ): string {
55
+ const packages = created.filter(
56
+ (object) => object.outputState === "PackageWrite",
57
+ );
58
+ if (packages.length !== 1) {
59
+ throw new TypeError("publication must create exactly one package");
60
+ }
61
+ return normalizeSuiObjectId(packages[0]!.objectId);
62
+ }
63
+
64
+ /** The parsed type of a touched object, or undefined for a package. */
65
+ export function typeOf(
66
+ result: TransactionEffectsInput,
67
+ objectId: string,
68
+ ): ReturnType<typeof parseStructTag> | undefined {
69
+ const type =
70
+ result.objectTypes[normalizeSuiObjectId(objectId)] ??
71
+ result.objectTypes[objectId];
72
+ return type === undefined || type === "package"
73
+ ? undefined
74
+ : parseStructTag(type);
75
+ }
76
+
77
+ /** Whether the object is `0x2::coin_registry::Currency<coinType>`. */
78
+ export function isCurrencyOf(
79
+ result: TransactionEffectsInput,
80
+ objectId: string,
81
+ coinType: string,
82
+ ): boolean {
83
+ const tag = typeOf(result, objectId);
84
+ const parameter = tag?.typeParams[0];
85
+ return (
86
+ tag?.address === normalizeSuiAddress(SUI_FRAMEWORK_ADDRESS) &&
87
+ tag.module === "coin_registry" &&
88
+ tag.name === "Currency" &&
89
+ tag.typeParams.length === 1 &&
90
+ parameter !== undefined &&
91
+ normalizeStructTag(parameter) === coinType
92
+ );
93
+ }
94
+
95
+ /**
96
+ * The `<core>::sealed_treasury_cap::SealedTreasuryCap<Coin>` objects a
97
+ * publication created, each with its sealed coin type, in effects order.
98
+ */
99
+ export function sealedTreasuryCaps(
100
+ result: TransactionEffectsInput,
101
+ created: readonly SuiClientTypes.ChangedObject[],
102
+ coreOriginalPackageId: string,
103
+ ): { object: SuiClientTypes.ChangedObject; coinType: string }[] {
104
+ const core = normalizeSuiAddress(coreOriginalPackageId);
105
+ return created.flatMap((object) => {
106
+ const tag = typeOf(result, object.objectId);
107
+ const parameter = tag?.typeParams[0];
108
+ return tag?.address === core &&
109
+ tag.module === "sealed_treasury_cap" &&
110
+ tag.name === "SealedTreasuryCap" &&
111
+ parameter !== undefined &&
112
+ typeof parameter !== "string"
113
+ ? [{ object, coinType: normalizeStructTag(parameter) }]
114
+ : [];
115
+ });
116
+ }
117
+
118
+ /**
119
+ * The one sealed `TreasuryCap<coinType>` a publication sent to `owner`;
120
+ * `where` names the expectation in the error.
121
+ */
122
+ export function readSealedTreasuryCap(
123
+ result: TransactionEffectsInput,
124
+ created: readonly SuiClientTypes.ChangedObject[],
125
+ coreOriginalPackageId: string,
126
+ coinType: string,
127
+ owner: string,
128
+ where: string,
129
+ ): ObjectVersionReference & { owner: string } {
130
+ const caps = sealedTreasuryCaps(result, created, coreOriginalPackageId)
131
+ .filter(
132
+ (cap) =>
133
+ cap.coinType === coinType &&
134
+ cap.object.outputOwner?.$kind === "AddressOwner" &&
135
+ normalizeSuiAddress(cap.object.outputOwner.AddressOwner) === owner,
136
+ )
137
+ .map(({ object }) => object);
138
+ const [cap] = caps;
139
+ if (caps.length !== 1 || !cap) {
140
+ throw new TypeError(
141
+ `publication must send exactly one SealedTreasuryCap<Coin> ${where}`,
142
+ );
143
+ }
144
+ return { ...versionReference(cap), owner };
145
+ }
146
+
147
+ /** The one `Currency<coinType>` a publication sent to the Coin Registry address. */
148
+ export function readPendingCurrency(
149
+ result: TransactionEffectsInput,
150
+ created: readonly SuiClientTypes.ChangedObject[],
151
+ coinType: string,
152
+ ): ObjectVersionReference {
153
+ const currencies = created.filter(
154
+ (object) =>
155
+ isCurrencyOf(result, object.objectId, coinType) &&
156
+ object.outputOwner?.$kind === "AddressOwner" &&
157
+ normalizeSuiAddress(object.outputOwner.AddressOwner) === COIN_REGISTRY_ID,
158
+ );
159
+ if (currencies.length !== 1) {
160
+ throw new TypeError(
161
+ "publication must send exactly one Currency<Coin> to the Coin Registry",
162
+ );
163
+ }
164
+ return versionReference(currencies[0]!);
165
+ }
166
+
167
+ /**
168
+ * Reads the shared `Currency<coinType>` that
169
+ * `coin_registry::finalize_registration` created at its derived address;
170
+ * mutable for the product's `finalize`.
171
+ */
172
+ export function readCurrencyRegistration(
173
+ result: TransactionEffectsInput,
174
+ coinType: string,
175
+ ): SharedObjectReference<true> {
176
+ const coin = normalizeStructTag(coinType);
177
+ const currencies = createdObjects(result).filter(
178
+ (object) =>
179
+ object.outputOwner?.$kind === "Shared" &&
180
+ isCurrencyOf(result, object.objectId, coin),
181
+ );
182
+ if (currencies.length !== 1) {
183
+ throw new TypeError("registration must share exactly one Currency<Coin>");
184
+ }
185
+ return sharedReference(currencies[0]!);
186
+ }
187
+
188
+ /**
189
+ * `0x2::coin_registry::finalize_registration<Coin>(registry, currency)`:
190
+ * shares a published coin's pending `Currency` from the Coin Registry
191
+ * address (`readPendingCurrency`). Anyone may run it; `coinRegistry` is the
192
+ * shared registry `0xc` with its initial shared version, so it builds
193
+ * offline.
194
+ */
195
+ export function finalizeCurrencyRegistration(options: {
196
+ coinType: string;
197
+ currency: ObjectVersionReference;
198
+ coinRegistry: SharedObjectIdentity;
199
+ }): (tx: Transaction) => TransactionResult {
200
+ const coinType = normalizeStructTag(options.coinType);
201
+ const currency = {
202
+ objectId: normalizeSuiObjectId(options.currency.objectId),
203
+ version: options.currency.version,
204
+ digest: options.currency.digest,
205
+ };
206
+ const coinRegistry = normalizeSharedObjectIdentity(
207
+ "coinRegistry",
208
+ options.coinRegistry,
209
+ );
210
+ return (tx) =>
211
+ tx.moveCall({
212
+ target: `${SUI_FRAMEWORK_ADDRESS}::coin_registry::finalize_registration`,
213
+ typeArguments: [coinType],
214
+ arguments: [
215
+ tx.sharedObjectRef({ ...coinRegistry, mutable: true }),
216
+ tx.receivingRef(currency),
217
+ ],
218
+ });
219
+ }
220
+
221
+ /** An owned object's ID, output version and digest. */
222
+ export function versionReference(
223
+ object: SuiClientTypes.ChangedObject,
224
+ ): ObjectVersionReference {
225
+ if (object.outputVersion === null || object.outputDigest === null) {
226
+ throw new TypeError(`object ${object.objectId} has no output version`);
227
+ }
228
+ return {
229
+ objectId: normalizeSuiObjectId(object.objectId),
230
+ version: object.outputVersion,
231
+ digest: object.outputDigest,
232
+ };
233
+ }
234
+
235
+ /** A shared object's identity, borrowed mutably. */
236
+ export function sharedReference(
237
+ object: SuiClientTypes.ChangedObject,
238
+ ): SharedObjectReference<true> {
239
+ if (object.outputOwner?.$kind !== "Shared") {
240
+ throw new TypeError(`object ${object.objectId} is not shared`);
241
+ }
242
+ return {
243
+ objectId: normalizeSuiObjectId(object.objectId),
244
+ initialSharedVersion: object.outputOwner.Shared.initialSharedVersion,
245
+ mutable: true,
246
+ };
247
+ }
@@ -0,0 +1,103 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import { normalizeSuiAddress } from "@mysten/sui/utils";
3
+ import { addressBcs } from "./bcs.ts";
4
+ import { apportionBps, bpsBcs, MAX_ROUTES, requireBps } from "./bps.ts";
5
+ import { BASIS_POINTS_DENOMINATOR } from "./integers.ts";
6
+
7
+ /**
8
+ * `blast_core::fee_split::Destination`: where one route's part of each
9
+ * LP-fee collection goes. The vault decides what each special kind does.
10
+ */
11
+ export type Destination =
12
+ | { kind: "Address"; address: string }
13
+ | { kind: "Owner" }
14
+ | { kind: "AutoLp" }
15
+ | { kind: "HolderRewards" }
16
+ | { kind: "Burn" };
17
+
18
+ /** `blast_core::fee_split::Route`: one destination and its positive share. */
19
+ export type Route = { destination: Destination; bps: bigint };
20
+
21
+ /** `blast_core::fee_split::FeeSplit`: routes summing to exactly 10,000. */
22
+ export type FeeSplit = { routes: Route[] };
23
+
24
+ /** BCS of `Destination`, in Move variant order. */
25
+ export const destinationBcs = bcs
26
+ .enum("Destination", {
27
+ Address: addressBcs,
28
+ Owner: null,
29
+ AutoLp: null,
30
+ HolderRewards: null,
31
+ Burn: null,
32
+ })
33
+ .transform({
34
+ input: (destination: Destination) =>
35
+ destination.kind === "Address"
36
+ ? { Address: destination.address }
37
+ : ({ [destination.kind]: true } as
38
+ | { Owner: true }
39
+ | { AutoLp: true }
40
+ | { HolderRewards: true }
41
+ | { Burn: true }),
42
+ output: (destination): Destination =>
43
+ destination.$kind === "Address"
44
+ ? { kind: "Address", address: destination.Address }
45
+ : { kind: destination.$kind },
46
+ });
47
+
48
+ export const routeBcs = bcs.struct("Route", {
49
+ destination: destinationBcs,
50
+ bps: bpsBcs,
51
+ });
52
+
53
+ export const feeSplitBcs = bcs.struct("FeeSplit", {
54
+ routes: bcs.vector(routeBcs),
55
+ });
56
+
57
+ /**
58
+ * Checks `split` as far as `fee_split::new` would reject it: at most 20
59
+ * routes, each positive (and an address route nonzero), no destination
60
+ * twice, summing to exactly 10,000. Returns the routes with normalized
61
+ * addresses, in the given order (the order `apportion` pays them).
62
+ */
63
+ export function normalizeFeeSplit(split: FeeSplit): FeeSplit {
64
+ if (split.routes.length > MAX_ROUTES) {
65
+ throw new RangeError(`a fee split holds at most ${MAX_ROUTES} routes`);
66
+ }
67
+ const routes = split.routes.map(({ destination, bps }): Route => {
68
+ if (requireBps(bps) === 0n) {
69
+ throw new RangeError("a fee-split route needs a positive share");
70
+ }
71
+ if (destination.kind !== "Address") return { destination, bps };
72
+ const address = normalizeSuiAddress(destination.address);
73
+ if (address === normalizeSuiAddress("0x0")) {
74
+ throw new RangeError("a fee-split route cannot pay the zero address");
75
+ }
76
+ return { destination: { kind: "Address", address }, bps };
77
+ });
78
+ const keys = routes.map(({ destination }) =>
79
+ destination.kind === "Address" ? destination.address : destination.kind,
80
+ );
81
+ if (new Set(keys).size !== keys.length) {
82
+ throw new RangeError("a fee split names each destination at most once");
83
+ }
84
+ if (
85
+ routes.reduce((sum, route) => sum + route.bps, 0n) !==
86
+ BASIS_POINTS_DENOMINATOR
87
+ ) {
88
+ throw new RangeError("fee-split routes must sum to 10,000 basis points");
89
+ }
90
+ return { routes };
91
+ }
92
+
93
+ /**
94
+ * Mirrors `fee_split::apportion`: `total` atoms split across the routes
95
+ * exactly, in route order, by `bps::apportion` with no rounded-up part.
96
+ */
97
+ export function apportionFeeSplit(split: FeeSplit, total: bigint): bigint[] {
98
+ return apportionBps(
99
+ split.routes.map(({ bps }) => bps),
100
+ total,
101
+ null,
102
+ );
103
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,22 @@
1
+ export * from "./aborts.ts";
1
2
  export * from "./arrays.ts";
2
3
  export * from "./bcs.ts";
4
+ export * from "./bps.ts";
3
5
  export * from "./bytecode-template.ts";
4
6
  export * from "./cetus.ts";
5
7
  export * from "./coins.ts";
8
+ export * from "./deployment.ts";
9
+ export * from "./effects.ts";
6
10
  export * from "./events.ts";
11
+ export * from "./fee-split.ts";
7
12
  export * from "./gas.ts";
8
13
  export * from "./integers.ts";
9
14
  export * from "./merkle.ts";
15
+ export * from "./migration.ts";
10
16
  export * from "./migration-math.ts";
17
+ export * from "./payout.ts";
18
+ export * from "./protocol-fees.ts";
11
19
  export * from "./ptb.ts";
20
+ export * from "./roles.ts";
12
21
  export * from "./sui.ts";
13
- export * from "./timelock.ts";
22
+ export * from "./treasury.ts";
package/src/integers.ts CHANGED
@@ -45,6 +45,21 @@ export function requireBasisPoints(name: string, value: bigint): BasisPoints {
45
45
  return value as BasisPoints;
46
46
  }
47
47
 
48
+ /** Requires a basis-point value no greater than a contract's compiled maximum. */
49
+ export function requireBasisPointsAtMost(
50
+ name: string,
51
+ value: bigint,
52
+ maximum: bigint,
53
+ ): BasisPoints {
54
+ const bps = requireBasisPoints(name, value);
55
+ if (bps > maximum) {
56
+ throw new RangeError(
57
+ `${name} exceeds ${maximum.toLocaleString("en-US")} basis points`,
58
+ );
59
+ }
60
+ return bps;
61
+ }
62
+
48
63
  /** Adds two values and requires the result to remain a Move `u64`. */
49
64
  export function checkedAddU64(name: string, left: bigint, right: bigint): U64 {
50
65
  return requireU64(name, left + right);
package/src/merkle.ts CHANGED
@@ -4,7 +4,17 @@ export function blake2b256(bytes: Uint8Array): Uint8Array {
4
4
  return blake2b(bytes, { dkLen: 32 });
5
5
  }
6
6
 
7
- /** Mirrors the contracts' Merkle `node`: `blake2b256(0x01 || left || right)`. */
7
+ /**
8
+ * Mirrors `blast_core::merkle::leaf`: `blake2b256(0x00 || domain || payload)`,
9
+ * `domain` naming the tree's purpose (UTF-8) and `payload` the leaf's BCS.
10
+ */
11
+ export function merkleLeaf(domain: string, payload: Uint8Array): Uint8Array {
12
+ return blake2b256(
13
+ new Uint8Array([0, ...new TextEncoder().encode(domain), ...payload]),
14
+ );
15
+ }
16
+
17
+ /** Mirrors `blast_core::merkle::node`: `blake2b256(0x01 || left || right)`. */
8
18
  export function merkleNode(left: Uint8Array, right: Uint8Array): Uint8Array {
9
19
  return blake2b256(new Uint8Array([1, ...left, ...right]));
10
20
  }