@parchment-trade/sdk 0.0.0-stage → 1.0.1

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 (95) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +193 -2
  3. package/REFERENCE.md +338 -0
  4. package/SECURITY.md +25 -0
  5. package/dist/general/providers/index.d.ts +2 -0
  6. package/dist/general/providers/index.js +2 -0
  7. package/dist/general/providers/parchment.d.ts +1 -0
  8. package/dist/general/providers/parchment.js +1 -0
  9. package/dist/general/providers/parchmentSdk.d.ts +23 -0
  10. package/dist/general/providers/parchmentSdk.js +11 -0
  11. package/dist/general/types/configTypes.d.ts +62 -0
  12. package/dist/general/types/configTypes.js +1 -0
  13. package/dist/general/types/index.d.ts +1 -0
  14. package/dist/general/types/index.js +1 -0
  15. package/dist/general/utils/address.d.ts +49 -0
  16. package/dist/general/utils/address.js +110 -0
  17. package/dist/general/utils/caller.d.ts +58 -0
  18. package/dist/general/utils/caller.js +182 -0
  19. package/dist/general/utils/errors.d.ts +16 -0
  20. package/dist/general/utils/errors.js +26 -0
  21. package/dist/general/utils/index.d.ts +4 -0
  22. package/dist/general/utils/index.js +4 -0
  23. package/dist/general/utils/jsBigInt.d.ts +26 -0
  24. package/dist/general/utils/jsBigInt.js +74 -0
  25. package/dist/general/utils/txShape.d.ts +8 -0
  26. package/dist/general/utils/txShape.js +39 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +4 -0
  29. package/dist/packages/index.d.ts +11 -0
  30. package/dist/packages/index.js +5 -0
  31. package/dist/packages/parchment/abortCodes.d.ts +196 -0
  32. package/dist/packages/parchment/abortCodes.js +522 -0
  33. package/dist/packages/parchment/batchClose.d.ts +268 -0
  34. package/dist/packages/parchment/batchClose.js +524 -0
  35. package/dist/packages/parchment/eventGuard.d.ts +106 -0
  36. package/dist/packages/parchment/eventGuard.js +119 -0
  37. package/dist/packages/parchment/gameCounterEvents.d.ts +59 -0
  38. package/dist/packages/parchment/gameCounterEvents.js +56 -0
  39. package/dist/packages/parchment/index.d.ts +52 -0
  40. package/dist/packages/parchment/index.js +30 -0
  41. package/dist/packages/parchment/liquidationCredit.d.ts +28 -0
  42. package/dist/packages/parchment/liquidationCredit.js +25 -0
  43. package/dist/packages/parchment/lockedMintEvents.d.ts +42 -0
  44. package/dist/packages/parchment/lockedMintEvents.js +46 -0
  45. package/dist/packages/parchment/monitoring.d.ts +369 -0
  46. package/dist/packages/parchment/monitoring.js +259 -0
  47. package/dist/packages/parchment/notes.d.ts +6 -0
  48. package/dist/packages/parchment/notes.js +1 -0
  49. package/dist/packages/parchment/onboarding.d.ts +50 -0
  50. package/dist/packages/parchment/onboarding.js +48 -0
  51. package/dist/packages/parchment/orderEffects.d.ts +207 -0
  52. package/dist/packages/parchment/orderEffects.js +268 -0
  53. package/dist/packages/parchment/orderParams.d.ts +72 -0
  54. package/dist/packages/parchment/orderParams.js +71 -0
  55. package/dist/packages/parchment/packageIds.d.ts +53 -0
  56. package/dist/packages/parchment/packageIds.js +149 -0
  57. package/dist/packages/parchment/parchment.d.ts +442 -0
  58. package/dist/packages/parchment/parchment.js +975 -0
  59. package/dist/packages/parchment/parchmentAccount.d.ts +189 -0
  60. package/dist/packages/parchment/parchmentAccount.js +273 -0
  61. package/dist/packages/parchment/parchmentMarket.d.ts +58 -0
  62. package/dist/packages/parchment/parchmentMarket.js +86 -0
  63. package/dist/packages/parchment/parchmentTypes.d.ts +2031 -0
  64. package/dist/packages/parchment/parchmentTypes.js +65 -0
  65. package/dist/packages/parchment/publicInts.d.ts +45 -0
  66. package/dist/packages/parchment/publicInts.js +383 -0
  67. package/dist/packages/parchment/reconcileEvents.d.ts +24 -0
  68. package/dist/packages/parchment/reconcileEvents.js +30 -0
  69. package/dist/packages/parchment/retainedFee.d.ts +43 -0
  70. package/dist/packages/parchment/retainedFee.js +68 -0
  71. package/dist/packages/parchment/sessionClose.d.ts +68 -0
  72. package/dist/packages/parchment/sessionClose.js +50 -0
  73. package/dist/packages/parchment/sessionKey.d.ts +78 -0
  74. package/dist/packages/parchment/sessionKey.js +154 -0
  75. package/dist/packages/parchment/slippage.d.ts +23 -0
  76. package/dist/packages/parchment/slippage.js +27 -0
  77. package/dist/packages/parchment/sponsor.d.ts +39 -0
  78. package/dist/packages/parchment/sponsor.js +48 -0
  79. package/dist/packages/parchment/stakingCalls.d.ts +33 -0
  80. package/dist/packages/parchment/stakingCalls.js +21 -0
  81. package/dist/packages/parchment/stakingEvents.d.ts +53 -0
  82. package/dist/packages/parchment/stakingEvents.js +71 -0
  83. package/dist/packages/parchment/triggerCalls.d.ts +46 -0
  84. package/dist/packages/parchment/triggerCalls.js +36 -0
  85. package/dist/packages/parchment/triggerEvents.d.ts +22 -0
  86. package/dist/packages/parchment/triggerEvents.js +33 -0
  87. package/dist/packages/parchment/triggers.d.ts +113 -0
  88. package/dist/packages/parchment/triggers.js +183 -0
  89. package/dist/packages/parchment/txBodies.d.ts +111 -0
  90. package/dist/packages/parchment/txBodies.js +683 -0
  91. package/dist/packages/parchment/txIntent.d.ts +99 -0
  92. package/dist/packages/parchment/txIntent.js +611 -0
  93. package/dist/types.d.ts +2 -0
  94. package/dist/types.js +1 -0
  95. package/package.json +58 -4
@@ -0,0 +1,524 @@
1
+ import { coinWithBalance, Inputs, Transaction } from "@mysten/sui/transactions";
2
+ import { fromBase64, normalizeSuiAddress } from "@mysten/sui/utils";
3
+ import { serviceTxViolation } from "../../general/utils/txShape.js";
4
+ import { UNCHECKED_TX, txIntentViolation, updateArgBase64 } from "./txIntent.js";
5
+ export const SUI_TX_LIMITS = {
6
+ commands: 1024,
7
+ txBytes: 128 * 1024,
8
+ events: 1024,
9
+ newObjectIds: 2048,
10
+ argumentsPerCommand: 512,
11
+ };
12
+ export const CLOSE_BATCH_BUDGET = {
13
+ commands: SUI_TX_LIMITS.commands,
14
+ txBytes: 96 * 1024,
15
+ events: 768,
16
+ newObjectIds: 1536,
17
+ argumentsPerCommand: SUI_TX_LIMITS.argumentsPerCommand,
18
+ };
19
+ export const SPONSOR_BATCH_BUDGET = {
20
+ trades: 32,
21
+ inputs: 64,
22
+ inputsPerTrade: 7,
23
+ oneTradeCommands: 8,
24
+ retiredTrades: 24,
25
+ retiredCommandsPerTrade: 6,
26
+ retiredInputsPerTrade: 16,
27
+ };
28
+ const LEG_EXECUTION = {
29
+ request: { events: 1, fundedEvents: 1, newObjectIds: 2 },
30
+ retired: { events: 26 + 2 + 1, newObjectIds: 3 },
31
+ };
32
+ export const GAME_REGISTRY_MAXIMA = {
33
+ achievements: 128,
34
+ quests: 128,
35
+ cosmetics: 128,
36
+ tierThresholds: 32,
37
+ };
38
+ export function gameEventLimitsFromDefinitions(definitions, config = {}) {
39
+ return {
40
+ achievements: definitions.achievements.length,
41
+ quests: definitions.quests.length,
42
+ cosmetics: definitions.cosmetics.length,
43
+ tierThresholds: config.tierThresholds ?? GAME_REGISTRY_MAXIMA.tierThresholds,
44
+ };
45
+ }
46
+ export function playerProgressionEvents(limits = GAME_REGISTRY_MAXIMA) {
47
+ const n = (key) => {
48
+ const value = limits[key];
49
+ if (!Number.isSafeInteger(value) || value < 0)
50
+ throw new BatchCloseError(`gameLimits.${key} must be a non-negative integer`);
51
+ return Math.min(value, GAME_REGISTRY_MAXIMA[key]);
52
+ };
53
+ const achievements = n("achievements");
54
+ const quests = n("quests");
55
+ return 3 * achievements + 2 * quests + Math.min(n("cosmetics"), achievements + quests) + n("tierThresholds") + achievements + quests + 1;
56
+ }
57
+ const REQUEST_CLOSE_TOP_UP_ARG = 7;
58
+ const SUI_FRAMEWORK = normalizeSuiAddress("0x2");
59
+ export class BatchCloseError extends Error {
60
+ constructor(message) {
61
+ super(message);
62
+ this.name = "BatchCloseError";
63
+ }
64
+ }
65
+ export function closeBatchTopUps(execFees, free) {
66
+ if (execFees.some((fee) => fee < 0n))
67
+ throw new BatchCloseError("an exec fee cannot be negative");
68
+ let available = free > 0n ? free : 0n;
69
+ return execFees.map((fee) => {
70
+ const topUp = fee > available ? fee - available : 0n;
71
+ available += topUp - fee;
72
+ return topUp;
73
+ });
74
+ }
75
+ export function batchCloseTx(legs, options) {
76
+ return assemble(legs, options, false);
77
+ }
78
+ export async function fitCloseBatch(legs, options) {
79
+ if (legs.length === 0)
80
+ throw new BatchCloseError("Nothing to close");
81
+ checkMaxTrades(options.maxTrades);
82
+ const funded = (count) => legs.slice(0, count).some((leg) => leg.topUp !== undefined && leg.topUp > 0n);
83
+ if (options.sponsored && funded(legs.length))
84
+ throw new BatchCloseError("A sponsored close takes no top-up");
85
+ if (funded(legs.length) && !options.client)
86
+ throw new BatchCloseError("Funding the close fees needs a Sui client");
87
+ const estimated = await longestFit(legs.length, async (count) => {
88
+ const usage = await closeBatchUsage(legs.slice(0, count), options);
89
+ return withinBudget(usage, options.sponsored, options.maxTrades) ? { tx: null, usage } : null;
90
+ });
91
+ if (!funded(estimated.count))
92
+ return { tx: batchCloseTx(legs.slice(0, estimated.count), options), count: estimated.count, usage: estimated.usage };
93
+ const client = largestCoinsFirst(options.client);
94
+ const resolved = await longestFit(estimated.count, async (count) => {
95
+ const fit = await resolvedBatch(legs.slice(0, count), { ...options, client });
96
+ return withinBudget(fit.usage, false) ? fit : null;
97
+ });
98
+ return { tx: resolved.tx, count: resolved.count, usage: resolved.usage };
99
+ }
100
+ async function longestFit(max, measure) {
101
+ const whole = await measure(max);
102
+ if (whole)
103
+ return { ...whole, count: max };
104
+ let lo = 0;
105
+ let hi = max;
106
+ let best = null;
107
+ while (hi - lo > 1) {
108
+ const mid = Math.floor((lo + hi) / 2);
109
+ const fits = await measure(mid);
110
+ if (fits) {
111
+ lo = mid;
112
+ best = fits;
113
+ }
114
+ else
115
+ hi = mid;
116
+ }
117
+ if (lo === 0 || !best)
118
+ throw new BatchCloseError("A close does not fit in one transaction");
119
+ return { ...best, count: lo };
120
+ }
121
+ export async function splitCloseBatches(legs, options) {
122
+ if (legs.length === 0)
123
+ throw new BatchCloseError("Nothing to close");
124
+ const batches = [];
125
+ for (let start = 0; start < legs.length;) {
126
+ if (start > 0 && legs.slice(start).some((leg) => leg.topUp !== undefined && leg.topUp > 0n)) {
127
+ throw new BatchCloseError("A later batch tops up from the wallet: fit it with fitCloseBatch after the previous transaction confirms");
128
+ }
129
+ const fit = await fitCloseBatch(legs.slice(start), options);
130
+ batches.push({ ...fit, start });
131
+ start += fit.count;
132
+ }
133
+ return batches;
134
+ }
135
+ const PLAN_LEG_COMMANDS = { request: 2, sponsoredRequest: 1, retired: 6 };
136
+ export function planCloseBatches(legs, options = {}) {
137
+ checkMaxTrades(options.maxTrades);
138
+ const commandsOf = (leg) => leg.retired ? PLAN_LEG_COMMANDS.retired : options.sponsored ? PLAN_LEG_COMMANDS.sponsoredRequest : PLAN_LEG_COMMANDS.request + (leg.funded ? 1 : 0);
139
+ const fits = (group) => {
140
+ const execution = executionOf(group, options.gameLimits);
141
+ const commands = group.reduce((n, leg) => n + commandsOf(leg), 0);
142
+ const retired = group.filter((leg) => leg.retired).length;
143
+ const usage = { ...execution, commands, trades: group.length, retired, inputs: 0 };
144
+ if (commands > CLOSE_BATCH_BUDGET.commands || execution.newObjectIds > CLOSE_BATCH_BUDGET.newObjectIds || !eventsFit(usage))
145
+ return false;
146
+ return !options.sponsored || sponsorTakes(usage, options.maxTrades);
147
+ };
148
+ const batches = [];
149
+ let current = [];
150
+ legs.forEach((leg, i) => {
151
+ if (current.length > 0 && fits([...current, i].map((j) => legs[j]))) {
152
+ current.push(i);
153
+ return;
154
+ }
155
+ if (current.length > 0)
156
+ batches.push(current);
157
+ if (!fits([leg]))
158
+ throw new BatchCloseError("A close does not fit in one transaction");
159
+ current = [i];
160
+ });
161
+ if (current.length > 0)
162
+ batches.push(current);
163
+ return batches;
164
+ }
165
+ function checkMaxTrades(maxTrades) {
166
+ if (maxTrades !== undefined && (!Number.isSafeInteger(maxTrades) || maxTrades < 1))
167
+ throw new BatchCloseError("maxTrades must be a positive integer");
168
+ }
169
+ export async function closeBatchUsage(legs, options) {
170
+ const tx = assemble(legs, options, true);
171
+ const coinCommands = legs.some((leg) => leg.topUp !== undefined && leg.topUp > 0n) ? COIN_RESOLUTION_COMMANDS : 0;
172
+ return measure(tx, legs, coinCommands, options.gameLimits);
173
+ }
174
+ async function resolvedBatch(legs, options) {
175
+ const tx = assemble(legs, options, false);
176
+ await tx.prepareForSerialization({ client: options.client });
177
+ return { tx, usage: await measure(tx, legs, 0, options.gameLimits) };
178
+ }
179
+ async function measure(tx, legs, extraCommands, gameLimits) {
180
+ const commands = tx.getData().commands;
181
+ const argumentsPerCommand = commands.reduce((max, c) => Math.max(max, commandArguments(c)), 0);
182
+ const bytes = await tx.build({ onlyTransactionKind: true });
183
+ const kinds = legs.map((leg) => {
184
+ const retired = !legIsRequest(leg.tx);
185
+ return { retired, account: retired ? (retiredAccount(leg.tx) ?? undefined) : undefined, funded: leg.topUp !== undefined && leg.topUp > 0n };
186
+ });
187
+ const execution = executionOf(kinds, gameLimits);
188
+ return {
189
+ commands: commands.length + extraCommands,
190
+ txBytes: bytes.length,
191
+ argumentsPerCommand,
192
+ inputs: tx.getData().inputs.length,
193
+ trades: legs.length,
194
+ retired: kinds.filter((k) => k.retired).length,
195
+ ...execution,
196
+ };
197
+ }
198
+ function executionOf(legs, gameLimits) {
199
+ const players = new Set();
200
+ let events = 0;
201
+ let newObjectIds = 0;
202
+ legs.forEach((leg, i) => {
203
+ if (leg.retired) {
204
+ players.add(leg.account ? normalizeSuiAddress(leg.account) : `leg ${i}`);
205
+ events += LEG_EXECUTION.retired.events;
206
+ newObjectIds += LEG_EXECUTION.retired.newObjectIds;
207
+ }
208
+ else {
209
+ events += LEG_EXECUTION.request.events + (leg.funded ? LEG_EXECUTION.request.fundedEvents : 0);
210
+ newObjectIds += LEG_EXECUTION.request.newObjectIds;
211
+ }
212
+ });
213
+ const progressionEvents = players.size * playerProgressionEvents(gameLimits);
214
+ return { events: events + progressionEvents, progressionEvents, newObjectIds };
215
+ }
216
+ const RETIRED_ACCOUNT_ARG = 2;
217
+ const RETIRED_CLOSE_FUNCTIONS = new Set(["close_retired", "close_retired_as_delegate"]);
218
+ function retiredAccount(source) {
219
+ const data = source.getData();
220
+ const commands = data.commands;
221
+ const inputs = data.inputs;
222
+ for (const c of commands) {
223
+ if (c.$kind !== "MoveCall")
224
+ continue;
225
+ const call = c.MoveCall;
226
+ if (call.module !== "trade" || !RETIRED_CLOSE_FUNCTIONS.has(call.function))
227
+ continue;
228
+ const arg = call.arguments[RETIRED_ACCOUNT_ARG];
229
+ const input = arg?.$kind === "Input" && arg.Input !== undefined ? inputs[arg.Input] : undefined;
230
+ const id = input?.Object?.SharedObject?.objectId ?? input?.Object?.ImmOrOwnedObject?.objectId ?? input?.UnresolvedObject?.objectId;
231
+ return id ? normalizeSuiAddress(id) : null;
232
+ }
233
+ return null;
234
+ }
235
+ function largestCoinsFirst(client) {
236
+ const core = client.core;
237
+ if (!core || typeof core.listCoins !== "function" || typeof core.getBalance !== "function")
238
+ return client;
239
+ const reads = core;
240
+ const coins = new Map();
241
+ const balances = new Map();
242
+ const allCoins = async (owner, coinType) => {
243
+ const all = [];
244
+ let cursor = null;
245
+ do {
246
+ const page = await reads.listCoins({ owner, coinType, cursor });
247
+ all.push(...page.objects);
248
+ cursor = page.hasNextPage ? page.cursor : null;
249
+ } while (cursor);
250
+ return all.sort((a, b) => (BigInt(b.balance) > BigInt(a.balance) ? 1 : BigInt(b.balance) < BigInt(a.balance) ? -1 : 0));
251
+ };
252
+ const listCoins = async ({ owner, coinType, cursor }) => {
253
+ const key = `${owner}:${coinType ?? ""}`;
254
+ if (!coins.has(key))
255
+ coins.set(key, allCoins(owner, coinType));
256
+ const sorted = await coins.get(key);
257
+ const at = cursor ? Number(cursor) : 0;
258
+ return { objects: sorted.slice(at, at + 1), hasNextPage: at + 1 < sorted.length, cursor: String(at + 1) };
259
+ };
260
+ const getBalance = (input) => {
261
+ const key = `${input.owner}:${input.coinType ?? ""}`;
262
+ if (!balances.has(key))
263
+ balances.set(key, reads.getBalance(input));
264
+ return balances.get(key);
265
+ };
266
+ const proxy = new Proxy(core, {
267
+ get(target, prop) {
268
+ if (prop === "listCoins")
269
+ return listCoins;
270
+ if (prop === "getBalance")
271
+ return getBalance;
272
+ const value = Reflect.get(target, prop, target);
273
+ return typeof value === "function" ? value.bind(target) : value;
274
+ },
275
+ });
276
+ return { core: proxy };
277
+ }
278
+ function withinBudget(usage, sponsored, maxTrades) {
279
+ const fits = (budget) => Object.keys(budget).every((k) => k === "events" || usage[k] <= budget[k]);
280
+ return fits(CLOSE_BATCH_BUDGET) && eventsFit(usage) && (!sponsored || sponsorTakes(usage, maxTrades));
281
+ }
282
+ function sponsorTakes(usage, maxTrades = SPONSOR_BATCH_BUDGET.trades) {
283
+ if (usage.retired > 0) {
284
+ const b = SPONSOR_BATCH_BUDGET;
285
+ return (usage.retired === usage.trades &&
286
+ usage.trades <= Math.min(maxTrades, b.retiredTrades) &&
287
+ usage.commands <= Math.max(b.oneTradeCommands, b.retiredCommandsPerTrade * usage.trades) &&
288
+ usage.inputs <= Math.max(b.inputs, b.retiredInputsPerTrade * usage.trades));
289
+ }
290
+ return (usage.trades <= maxTrades &&
291
+ usage.commands <= usage.trades &&
292
+ usage.inputs <= Math.max(SPONSOR_BATCH_BUDGET.inputs, SPONSOR_BATCH_BUDGET.inputsPerTrade * usage.trades));
293
+ }
294
+ function eventsFit(usage) {
295
+ const room = SUI_TX_LIMITS.events - usage.progressionEvents;
296
+ return room > 0 && (usage.events - usage.progressionEvents) * SUI_TX_LIMITS.events <= CLOSE_BATCH_BUDGET.events * room;
297
+ }
298
+ function commandArguments(c) {
299
+ switch (c.$kind) {
300
+ case "MoveCall":
301
+ return c.MoveCall.arguments.length;
302
+ case "SplitCoins":
303
+ return 1 + c.SplitCoins.amounts.length;
304
+ case "MergeCoins":
305
+ return 1 + c.MergeCoins.sources.length;
306
+ case "TransferObjects":
307
+ return 1 + c.TransferObjects.objects.length;
308
+ default:
309
+ return 0;
310
+ }
311
+ }
312
+ export function legIsRequest(source) {
313
+ const commands = source.getData().commands;
314
+ return commands.some((c) => c.$kind === "MoveCall" && REQUEST_CLOSE_FUNCTIONS.has(c.MoveCall.function));
315
+ }
316
+ const REQUEST_CLOSE_FUNCTIONS = new Set(["request_close", "request_close_as_delegate"]);
317
+ const COIN_RESOLUTION_COMMANDS = 2;
318
+ const PLACEHOLDER_COIN = Inputs.ObjectRef({ objectId: normalizeSuiAddress("0x0"), version: "0", digest: "11111111111111111111111111111111" });
319
+ const CLOSE_LEG_ROUTES = new Set(["request-close", "request-close-as-delegate", "close-retired", "close-retired-as-delegate"]);
320
+ function checkLeg(leg, index, sender, trust) {
321
+ const request = leg.request;
322
+ if (!request)
323
+ throw new BatchCloseError(`Close ${index + 1} has no request to check it against (pass ParchmentBuiltTx.request)`);
324
+ if (!CLOSE_LEG_ROUTES.has(request.route))
325
+ throw new BatchCloseError(`Close ${index + 1} is a ${request.route}, not a close`);
326
+ if (typeof request.body.sender !== "string" || normalizeSuiAddress(request.body.sender) !== normalizeSuiAddress(sender)) {
327
+ throw new BatchCloseError(`Close ${index + 1} was built for another sender`);
328
+ }
329
+ const violation = txIntentViolation(request.route, leg.tx, request.body, sender, trust);
330
+ if (violation !== null)
331
+ throw new BatchCloseError(`Close ${index + 1} does not match its request: ${violation}`);
332
+ }
333
+ const PUSH_STORAGE_ARG = 2;
334
+ const PUSH_FEED_INFO_ARG = 3;
335
+ const PUSH_UPDATE_ARG = 5;
336
+ function updatesOf(tx) {
337
+ const data = tx.getData();
338
+ return data.commands.flatMap((command) => {
339
+ const c = command.$kind === "MoveCall" ? command.MoveCall : undefined;
340
+ const at = c?.module === "adapter" && c.function === "quote" ? QUOTE_UPDATE_ARG : c?.module === "price_feed_storage" && c.function === "update_price_feed" ? PUSH_UPDATE_ARG : -1;
341
+ if (at === -1)
342
+ return [];
343
+ const update = c.arguments[at];
344
+ return [update?.$kind === "Input" ? (data.inputs[update.Input ?? -1]?.Pure?.bytes ?? null) : null];
345
+ });
346
+ }
347
+ function checkBatchUpdates(tx, legs) {
348
+ const carried = new Set(legs.flatMap((leg) => updatesOf(leg.tx)).filter((u) => u !== null));
349
+ if (updatesOf(tx).some((u) => u === null || !carried.has(u)))
350
+ throw new BatchCloseError("The batch prices a close with an update no close carried");
351
+ }
352
+ function assemble(legs, { sender, coinType, txTrust = UNCHECKED_TX, sponsored }, measuring) {
353
+ if (legs.length === 0)
354
+ throw new BatchCloseError("Nothing to close");
355
+ if (txTrust !== UNCHECKED_TX && (typeof txTrust !== "object" || txTrust === null)) {
356
+ throw new BatchCloseError('txTrust is "unchecked" or { packages }');
357
+ }
358
+ const trust = txTrust === UNCHECKED_TX ? null : txTrust;
359
+ const topUps = legs.map((leg) => (leg.topUp !== undefined && leg.topUp > 0n ? leg.topUp : 0n));
360
+ const total = topUps.reduce((a, b) => a + b, 0n);
361
+ const tx = new Transaction();
362
+ tx.setSender(sender);
363
+ const coins = topUps.map(() => null);
364
+ if (total > 0n) {
365
+ if (!coinType)
366
+ throw new BatchCloseError("A top-up needs the collateral coin type");
367
+ const coin = measuring ? tx.object(PLACEHOLDER_COIN) : tx.add(coinWithBalance({ type: coinType, balance: total }));
368
+ const funded = topUps.flatMap((amount, i) => (amount > 0n ? [i] : []));
369
+ const parts = funded.length > 1 ? tx.splitCoins(coin, funded.slice(0, -1).map((i) => topUps[i])) : null;
370
+ funded.forEach((legIndex, k) => {
371
+ coins[legIndex] = k < funded.length - 1 ? parts[k] : coin;
372
+ });
373
+ }
374
+ const pinnedUpdates = new Map();
375
+ const pushed = sponsored ? new Set() : null;
376
+ legs.forEach((leg, i) => {
377
+ const violation = serviceTxViolation(leg.tx, sender);
378
+ if (violation !== null)
379
+ throw new BatchCloseError(`A close has a ${violation}`);
380
+ if (trust)
381
+ checkLeg(leg, i, sender, trust);
382
+ const named = trust ? updateArgBase64(leg.request?.body.updateBase64) : null;
383
+ appendLeg(tx, leg.tx, coins[i] ?? null, pinnedUpdates, named, pushed);
384
+ });
385
+ if (trust)
386
+ checkBatchUpdates(tx, legs);
387
+ return tx;
388
+ }
389
+ const QUOTE_UPDATE_ARG = 2;
390
+ function appendLeg(tx, source, topUp, pinnedUpdates, namedUpdate, pushed) {
391
+ const data = source.getData();
392
+ let skipCommand = -1;
393
+ if (topUp) {
394
+ const calls = data.commands.flatMap((c, i) => (c.$kind === "MoveCall" && c.MoveCall.function === "request_close" ? [i] : []));
395
+ if (calls.length !== 1)
396
+ throw new BatchCloseError("A top-up needs exactly one request_close in its leg");
397
+ const arg = data.commands[calls[0]].MoveCall.arguments[REQUEST_CLOSE_TOP_UP_ARG];
398
+ const producer = arg?.$kind === "Result" ? data.commands[arg.Result ?? -1] : undefined;
399
+ const zero = producer?.$kind === "MoveCall" ? producer.MoveCall : undefined;
400
+ if (!zero || normalizeSuiAddress(zero.package) !== SUI_FRAMEWORK || zero.module !== "coin" || zero.function !== "zero") {
401
+ throw new BatchCloseError("The request's top-up is not the service's zero coin");
402
+ }
403
+ skipCommand = arg.Result;
404
+ }
405
+ const updateFeeds = new Map();
406
+ for (const command of data.commands) {
407
+ const c = command.$kind === "MoveCall" ? command.MoveCall : undefined;
408
+ if (!c || c.module !== "adapter" || c.function !== "quote")
409
+ continue;
410
+ const [storage, update, sourceId, feedId] = [c.arguments[1], c.arguments[QUOTE_UPDATE_ARG], c.arguments[4], c.arguments[5]];
411
+ const storageInput = storage?.$kind === "Input" ? data.inputs[storage.Input ?? -1] : undefined;
412
+ const storageId = storageInput?.Object?.SharedObject?.objectId ?? storageInput?.Object?.ImmOrOwnedObject?.objectId ?? storageInput?.UnresolvedObject?.objectId;
413
+ const pure = (a) => (a?.$kind === "Input" ? data.inputs[a.Input ?? -1]?.Pure?.bytes : undefined);
414
+ if (update?.$kind !== "Input" || pure(update) === undefined || !storageId)
415
+ throw new BatchCloseError("Malformed oracle quote in a close");
416
+ updateFeeds.set(update.Input, `${normalizeSuiAddress(storageId)}:${pure(sourceId) ?? ""}:${pure(feedId) ?? ""}`);
417
+ }
418
+ const inputs = data.inputs.map((input, index) => {
419
+ const feed = updateFeeds.get(index);
420
+ const pinned = feed !== undefined ? pinnedUpdates.get(feed) : undefined;
421
+ if (pinned) {
422
+ if (namedUpdate !== null && pinned.bytes !== namedUpdate) {
423
+ throw new BatchCloseError("A close names a signed update another close's update for the same feed would replace; build them with one update");
424
+ }
425
+ return pinned.arg;
426
+ }
427
+ if (input.$kind === "Pure" && input.Pure) {
428
+ const pure = tx.pure(fromBase64(input.Pure.bytes));
429
+ if (feed !== undefined)
430
+ pinnedUpdates.set(feed, { arg: pure, bytes: input.Pure.bytes });
431
+ return pure;
432
+ }
433
+ const obj = input.Object;
434
+ if (input.$kind === "Object" && obj) {
435
+ if (obj.SharedObject)
436
+ return tx.object(Inputs.SharedObjectRef(obj.SharedObject));
437
+ if (obj.ImmOrOwnedObject)
438
+ return tx.object(Inputs.ObjectRef({ ...obj.ImmOrOwnedObject, version: String(obj.ImmOrOwnedObject.version) }));
439
+ if (obj.Receiving)
440
+ return tx.object(Inputs.ReceivingRef({ ...obj.Receiving, version: String(obj.Receiving.version) }));
441
+ }
442
+ if (input.$kind === "UnresolvedObject" && input.UnresolvedObject)
443
+ return tx.object(input.UnresolvedObject.objectId);
444
+ throw new BatchCloseError(`Unsupported transaction input ${input.$kind}`);
445
+ });
446
+ const results = [];
447
+ const arg = (a) => {
448
+ switch (a.$kind) {
449
+ case "Input":
450
+ return inputs[a.Input ?? -1] ?? malformed(`input ${a.Input}`);
451
+ case "Result":
452
+ return results[a.Result ?? -1] ?? malformed(`result ${a.Result}`);
453
+ case "NestedResult": {
454
+ const [r, n] = a.NestedResult ?? [-1, -1];
455
+ const result = results[r];
456
+ return result?.[n] ?? malformed(`result ${r}`);
457
+ }
458
+ case "GasCoin":
459
+ return tx.gas;
460
+ default:
461
+ return malformed(`argument ${a.$kind}`);
462
+ }
463
+ };
464
+ const objectId = (a) => {
465
+ const input = a?.$kind === "Input" ? data.inputs[a.Input ?? -1] : undefined;
466
+ const id = input?.Object?.SharedObject?.objectId ?? input?.Object?.ImmOrOwnedObject?.objectId ?? input?.UnresolvedObject?.objectId;
467
+ return id ? normalizeSuiAddress(id) : null;
468
+ };
469
+ const repeatedPush = (c) => {
470
+ if (!pushed || c.module !== "price_feed_storage" || c.function !== "update_price_feed")
471
+ return false;
472
+ const [storage, info, update] = [objectId(c.arguments[PUSH_STORAGE_ARG]), objectId(c.arguments[PUSH_FEED_INFO_ARG]), c.arguments[PUSH_UPDATE_ARG]];
473
+ if (!storage || !info || update?.$kind !== "Input")
474
+ return false;
475
+ const feed = updateFeeds.get(update.Input);
476
+ const bytes = (feed !== undefined ? pinnedUpdates.get(feed)?.bytes : undefined) ?? data.inputs[update.Input]?.Pure?.bytes;
477
+ if (bytes === undefined)
478
+ return false;
479
+ const key = `${storage}:${info}:${bytes}`;
480
+ if (pushed.has(key))
481
+ return true;
482
+ pushed.add(key);
483
+ return false;
484
+ };
485
+ data.commands.forEach((command, index) => {
486
+ if (index === skipCommand) {
487
+ results.push(topUp);
488
+ return;
489
+ }
490
+ if (command.$kind === "MoveCall" && repeatedPush(command.MoveCall)) {
491
+ results.push(undefined);
492
+ return;
493
+ }
494
+ switch (command.$kind) {
495
+ case "MoveCall": {
496
+ const c = command.MoveCall;
497
+ results.push(tx.moveCall({ target: `${c.package}::${c.module}::${c.function}`, typeArguments: c.typeArguments, arguments: c.arguments.map(arg) }));
498
+ break;
499
+ }
500
+ case "SplitCoins": {
501
+ const c = command.SplitCoins;
502
+ results.push(tx.splitCoins(arg(c.coin), c.amounts.map(arg)));
503
+ break;
504
+ }
505
+ case "MergeCoins": {
506
+ const c = command.MergeCoins;
507
+ tx.mergeCoins(arg(c.destination), c.sources.map((s) => arg(s)));
508
+ results.push(undefined);
509
+ break;
510
+ }
511
+ case "TransferObjects": {
512
+ const c = command.TransferObjects;
513
+ tx.transferObjects(c.objects.map((o) => arg(o)), arg(c.address));
514
+ results.push(undefined);
515
+ break;
516
+ }
517
+ default:
518
+ throw new BatchCloseError(`Unsupported transaction command ${command.$kind}`);
519
+ }
520
+ });
521
+ }
522
+ function malformed(what) {
523
+ throw new BatchCloseError(`Malformed close transaction (${what})`);
524
+ }
@@ -0,0 +1,106 @@
1
+ import type { Transaction, TransactionArgument, TransactionObjectArgument } from "@mysten/sui/transactions";
2
+ /** `event_guard` reason codes (`u8`); any other value aborts `EBadReason` (0). */
3
+ export declare const EVENT_REASON: {
4
+ readonly dividend: 0;
5
+ readonly earnings: 1;
6
+ readonly macro: 2;
7
+ readonly other: 3;
8
+ };
9
+ export type EventReason = keyof typeof EVENT_REASON;
10
+ /** `event_guard::max_window_ms()`: an event pause or cap ends at most 24 h after the call that sets it. */
11
+ export declare const EVENT_GUARD_MAX_WINDOW_MS = 86400000;
12
+ type ObjectInput = string | TransactionObjectArgument;
13
+ type U64 = number | bigint | string;
14
+ interface EventGuardTarget {
15
+ /** Latest `parchment_trade` package id (call target). */
16
+ tradePackageId: string;
17
+ /** Collateral coin type `C`. */
18
+ coinType: string;
19
+ exchange: ObjectInput;
20
+ market: ObjectInput;
21
+ config: ObjectInput;
22
+ }
23
+ export interface EventKeeperCall extends EventGuardTarget {
24
+ /** `parchment::authority::AuthorityCap<PACKAGE, MARKET_KEEPER>` (`config::grant_market_keeper`). */
25
+ keeperCap: ObjectInput;
26
+ }
27
+ export interface SetEventPauseCall extends EventKeeperCall {
28
+ reason: EventReason | number;
29
+ /** Clock ms the pause lapses at: after now and at most {@link EVENT_GUARD_MAX_WINDOW_MS} from now (`EBadWindow` 1). */
30
+ untilMs: U64;
31
+ }
32
+ /**
33
+ * Keeper `event_guard::set_event_pause<C>(exchange, market, config, cap, reason, until_ms, clock)`: blocks NEW opens
34
+ * on the market until `untilMs` (requests abort `EEventPaused` 3, pending open fills refund REFUND_PAUSED). Separate
35
+ * from the admin `opens_paused` flag. Emits `EventPauseUpdatedEventV1`.
36
+ */
37
+ export declare function addSetEventPauseCall(tx: Transaction, input: SetEventPauseCall): void;
38
+ /** Keeper `event_guard::clear_event_pause<C>(exchange, market, config, cap)`. Never lifts the admin `opens_paused`. */
39
+ export declare function addClearEventPauseCall(tx: Transaction, input: EventKeeperCall): void;
40
+ export interface SetEventLeverageCapCall extends SetEventPauseCall {
41
+ /** Within the market's `min_leverage..=max_leverage` (`EBadLeverageCap` 2): tighten-only. */
42
+ maxLeverage: U64;
43
+ }
44
+ /**
45
+ * Keeper `event_guard::set_event_leverage_cap<C>(exchange, market, config, cap, max_leverage, reason, until_ms, clock)`:
46
+ * caps the leverage of NEW opens until `untilMs` (requests above it abort `EEventLeverage` 4, pending fills above it
47
+ * refund REFUND_RISK). Emits `EventLeverageCapUpdatedEventV1`.
48
+ */
49
+ export declare function addSetEventLeverageCapCall(tx: Transaction, input: SetEventLeverageCapCall): void;
50
+ /** Keeper `event_guard::clear_event_leverage_cap<C>(exchange, market, config, cap)`. */
51
+ export declare function addClearEventLeverageCapCall(tx: Transaction, input: EventKeeperCall): void;
52
+ export interface ArmEventCloseCall extends EventKeeperCall {
53
+ /** `oracle::PriceReceipt` from `oracle::begin_price` earlier in the same PTB: this market, open session. */
54
+ receipt: TransactionArgument;
55
+ }
56
+ /**
57
+ * Keeper `event_guard::arm_event_close<C>(market, exchange, config, cap, receipt, clock)`: needs an active event pause
58
+ * (`ENoEventPause` 5); arms the session force-close drain at the receipt's storage price. Then run
59
+ * `addForceCloseSessionCall` per account (pending opens refund in full). Admin cancel: `oracle::disarm_session_close`.
60
+ * Emits `SessionArmedEventV1` and `EventCloseArmedEventV1`. Consumes the receipt. Note the Move argument order
61
+ * (market before exchange), as `oracle::arm_session_close`.
62
+ */
63
+ export declare function addArmEventCloseCall(tx: Transaction, input: ArmEventCloseCall): void;
64
+ export interface EventGuardAdminClearCall extends EventGuardTarget {
65
+ /** `parchment::authority::AuthorityCap<PACKAGE, ADMIN>`. */
66
+ adminCap: ObjectInput;
67
+ }
68
+ /** Admin `event_guard::admin_clear<C>(exchange, market, config, cap)`: clears the event pause and the event cap. */
69
+ export declare function addEventGuardAdminClearCall(tx: Transaction, input: EventGuardAdminClearCall): void;
70
+ /** BCS for `event_guard::Event<EventPauseUpdatedEventV1>`; `until_ms` 0 is a clear. */
71
+ export declare const EVENT_PAUSE_UPDATED_EVENT_BCS: import("@mysten/sui/bcs").BcsStruct<{
72
+ market_id: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
73
+ until_ms: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
74
+ reason: import("@mysten/sui/bcs").BcsType<number, number, "u8">;
75
+ sender: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
76
+ admin: import("@mysten/sui/bcs").BcsType<boolean, boolean, "bool">;
77
+ }, string>;
78
+ /** BCS for `event_guard::Event<EventLeverageCapUpdatedEventV1>`; `max_leverage` 0 and `until_ms` 0 is a clear. */
79
+ export declare const EVENT_LEVERAGE_CAP_UPDATED_EVENT_BCS: import("@mysten/sui/bcs").BcsStruct<{
80
+ market_id: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
81
+ max_leverage: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
82
+ until_ms: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
83
+ reason: import("@mysten/sui/bcs").BcsType<number, number, "u8">;
84
+ sender: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
85
+ admin: import("@mysten/sui/bcs").BcsType<boolean, boolean, "bool">;
86
+ }, string>;
87
+ /** BCS for `event_guard::Event<EventCloseArmedEventV1>`; `remaining` 0 is an empty book (snapshot not armed). */
88
+ export declare const EVENT_CLOSE_ARMED_EVENT_BCS: import("@mysten/sui/bcs").BcsStruct<{
89
+ market_id: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
90
+ reason: import("@mysten/sui/bcs").BcsType<number, number, "u8">;
91
+ price_wad: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u128">;
92
+ ts_ms: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
93
+ watermark: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
94
+ remaining: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
95
+ pause_until_ms: import("@mysten/sui/bcs").BcsType<string, string | number | bigint, "u64">;
96
+ sender: import("@mysten/sui/bcs").BcsType<string, string | Uint8Array<ArrayBufferLike>, "bytes[32]">;
97
+ }, string>;
98
+ export type EventPauseUpdatedEvent = ReturnType<typeof EVENT_PAUSE_UPDATED_EVENT_BCS.parse>;
99
+ export type EventLeverageCapUpdatedEvent = ReturnType<typeof EVENT_LEVERAGE_CAP_UPDATED_EVENT_BCS.parse>;
100
+ export type EventCloseArmedEvent = ReturnType<typeof EVENT_CLOSE_ARMED_EVENT_BCS.parse>;
101
+ export declare function decodeEventPauseUpdatedEvent(bytes: Uint8Array): EventPauseUpdatedEvent | null;
102
+ export declare function decodeEventLeverageCapUpdatedEvent(bytes: Uint8Array): EventLeverageCapUpdatedEvent | null;
103
+ export declare function decodeEventCloseArmedEvent(bytes: Uint8Array): EventCloseArmedEvent | null;
104
+ /** Full event type string, e.g. `eventGuardEventType(tradeOriginalId, "EventPauseUpdatedEventV1")`. */
105
+ export declare function eventGuardEventType(tradeOriginalId: string, name: "EventPauseUpdatedEventV1" | "EventLeverageCapUpdatedEventV1" | "EventCloseArmedEventV1"): string;
106
+ export {};