@unconfirmed/sui-effect 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 (115) hide show
  1. package/AGENTS.md +209 -0
  2. package/LICENSE +21 -0
  3. package/LLMS.md +6707 -0
  4. package/README.md +300 -0
  5. package/dist/domain/bcs.d.ts +101 -0
  6. package/dist/domain/bcs.d.ts.map +1 -0
  7. package/dist/domain/bcs.js +183 -0
  8. package/dist/domain/bcs.js.map +1 -0
  9. package/dist/domain/errors.d.ts +595 -0
  10. package/dist/domain/errors.d.ts.map +1 -0
  11. package/dist/domain/errors.js +476 -0
  12. package/dist/domain/errors.js.map +1 -0
  13. package/dist/domain/executed.d.ts +325 -0
  14. package/dist/domain/executed.d.ts.map +1 -0
  15. package/dist/domain/executed.js +253 -0
  16. package/dist/domain/executed.js.map +1 -0
  17. package/dist/domain/journal-entry.d.ts +468 -0
  18. package/dist/domain/journal-entry.d.ts.map +1 -0
  19. package/dist/domain/journal-entry.js +63 -0
  20. package/dist/domain/journal-entry.js.map +1 -0
  21. package/dist/domain/schemas.d.ts +950 -0
  22. package/dist/domain/schemas.d.ts.map +1 -0
  23. package/dist/domain/schemas.js +571 -0
  24. package/dist/domain/schemas.js.map +1 -0
  25. package/dist/domain/sui-schema.d.ts +12 -0
  26. package/dist/domain/sui-schema.d.ts.map +1 -0
  27. package/dist/domain/sui-schema.js +12 -0
  28. package/dist/domain/sui-schema.js.map +1 -0
  29. package/dist/extension.d.ts +7 -0
  30. package/dist/extension.d.ts.map +1 -0
  31. package/dist/extension.js +7 -0
  32. package/dist/extension.js.map +1 -0
  33. package/dist/index.d.ts +31 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +31 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/internal.d.ts +18 -0
  38. package/dist/internal.d.ts.map +1 -0
  39. package/dist/internal.js +18 -0
  40. package/dist/internal.js.map +1 -0
  41. package/dist/journal.d.ts +51 -0
  42. package/dist/journal.d.ts.map +1 -0
  43. package/dist/journal.js +18 -0
  44. package/dist/journal.js.map +1 -0
  45. package/dist/script.d.ts +7 -0
  46. package/dist/script.d.ts.map +1 -0
  47. package/dist/script.js +7 -0
  48. package/dist/script.js.map +1 -0
  49. package/dist/services/Journal.d.ts +76 -0
  50. package/dist/services/Journal.d.ts.map +1 -0
  51. package/dist/services/Journal.js +70 -0
  52. package/dist/services/Journal.js.map +1 -0
  53. package/dist/services/JournalKeyValueStore.d.ts +73 -0
  54. package/dist/services/JournalKeyValueStore.d.ts.map +1 -0
  55. package/dist/services/JournalKeyValueStore.js +155 -0
  56. package/dist/services/JournalKeyValueStore.js.map +1 -0
  57. package/dist/services/Script.d.ts +210 -0
  58. package/dist/services/Script.d.ts.map +1 -0
  59. package/dist/services/Script.js +441 -0
  60. package/dist/services/Script.js.map +1 -0
  61. package/dist/services/Signer.d.ts +132 -0
  62. package/dist/services/Signer.d.ts.map +1 -0
  63. package/dist/services/Signer.js +158 -0
  64. package/dist/services/Signer.js.map +1 -0
  65. package/dist/services/SubmitConfig.d.ts +175 -0
  66. package/dist/services/SubmitConfig.d.ts.map +1 -0
  67. package/dist/services/SubmitConfig.js +52 -0
  68. package/dist/services/SubmitConfig.js.map +1 -0
  69. package/dist/services/Sui.d.ts +295 -0
  70. package/dist/services/Sui.d.ts.map +1 -0
  71. package/dist/services/Sui.js +414 -0
  72. package/dist/services/Sui.js.map +1 -0
  73. package/dist/services/SuiCore.d.ts +227 -0
  74. package/dist/services/SuiCore.d.ts.map +1 -0
  75. package/dist/services/SuiCore.js +400 -0
  76. package/dist/services/SuiCore.js.map +1 -0
  77. package/dist/services/SuiCoreFake.d.ts +278 -0
  78. package/dist/services/SuiCoreFake.d.ts.map +1 -0
  79. package/dist/services/SuiCoreFake.js +1003 -0
  80. package/dist/services/SuiCoreFake.js.map +1 -0
  81. package/dist/services/SuiExtension.d.ts +208 -0
  82. package/dist/services/SuiExtension.d.ts.map +1 -0
  83. package/dist/services/SuiExtension.js +355 -0
  84. package/dist/services/SuiExtension.js.map +1 -0
  85. package/dist/services/SuiGraphQL.d.ts +98 -0
  86. package/dist/services/SuiGraphQL.d.ts.map +1 -0
  87. package/dist/services/SuiGraphQL.js +118 -0
  88. package/dist/services/SuiGraphQL.js.map +1 -0
  89. package/dist/services/Tx.d.ts +687 -0
  90. package/dist/services/Tx.d.ts.map +1 -0
  91. package/dist/services/Tx.js +1224 -0
  92. package/dist/services/Tx.js.map +1 -0
  93. package/dist/testing.d.ts +88 -0
  94. package/dist/testing.d.ts.map +1 -0
  95. package/dist/testing.js +163 -0
  96. package/dist/testing.js.map +1 -0
  97. package/dist/tx.d.ts +24 -0
  98. package/dist/tx.d.ts.map +1 -0
  99. package/dist/tx.js +24 -0
  100. package/dist/tx.js.map +1 -0
  101. package/docs/extensions.md +1685 -0
  102. package/examples/extension-template/README.md +170 -0
  103. package/examples/extension-template/package.json +44 -0
  104. package/examples/extension-template/scripts/check-package.ts +147 -0
  105. package/examples/extension-template/src/Escrow.ts +445 -0
  106. package/examples/extension-template/src/Platform.ts +136 -0
  107. package/examples/extension-template/src/errors.ts +61 -0
  108. package/examples/extension-template/src/extension.ts +63 -0
  109. package/examples/extension-template/src/index.ts +34 -0
  110. package/examples/extension-template/src/schema.ts +158 -0
  111. package/examples/extension-template/src/upstream.ts +66 -0
  112. package/examples/extension-template/test/escrow.test.ts +559 -0
  113. package/examples/extension-template/tsconfig.build.json +26 -0
  114. package/examples/extension-template/tsconfig.json +35 -0
  115. package/package.json +87 -0
@@ -0,0 +1,1003 @@
1
+ /**
2
+ * `SuiCoreFake`: an in-memory `SuiCore` for tests.
3
+ *
4
+ * It holds a map of objects keyed by id with their version and BCS content, the
5
+ * Clock object `0x6`, and scripted outcomes for the methods a transaction
6
+ * lifecycle touches. Anything that is not scripted dies with a message naming
7
+ * the method, so a test never silently passes against a stub.
8
+ *
9
+ * There is no Move execution here and no real BCS validation of a transaction:
10
+ * localnet covers those.
11
+ *
12
+ * @since 0.1.0
13
+ */
14
+ import { bcs } from "@mysten/sui/bcs";
15
+ import { ObjectError, SimulationError, TransactionError } from "@mysten/sui/client";
16
+ import { Inputs, TransactionDataBuilder } from "@mysten/sui/transactions";
17
+ import { normalizeStructTag, normalizeSuiAddress, SUI_CLOCK_OBJECT_ID, SUI_TYPE_ARG, toBase58 } from "@mysten/sui/utils";
18
+ import { Context, Effect, Layer, Option } from "effect";
19
+ import { typeMatches } from "../domain/bcs.js";
20
+ import { DefectMarker, makeFromClient, SuiCore } from "./SuiCore.js";
21
+ /** The BCS layout of `0x2::clock::Clock`. */
22
+ export const ClockBcs = bcs.struct("Clock", {
23
+ id: bcs.Address,
24
+ timestamp_ms: bcs.U64
25
+ });
26
+ /** The Move type of the Clock object. */
27
+ export const CLOCK_TYPE = "0x2::clock::Clock";
28
+ /** A deterministic base58 32-byte digest, for fixtures. */
29
+ export const fakeDigest = (seed) => {
30
+ const bytes = new Uint8Array(32);
31
+ bytes[0] = (seed % 255) + 1;
32
+ bytes[31] = 7;
33
+ return toBase58(bytes);
34
+ };
35
+ /** Builders for {@link FakeOutcome}. */
36
+ export const FakeOutcome = {
37
+ /** The call succeeds with these effects. */
38
+ succeed: (value = {}) => ({ _tag: "succeed", value }),
39
+ /** The call resolves with an on-chain failure (`FailedTransaction`). */
40
+ failWith: (reason, value = {}) => ({ _tag: "failWith", reason, value }),
41
+ /** The call rejects with a gRPC status, as `RpcError` would. */
42
+ transportError: (status) => ({ _tag: "transportError", status }),
43
+ /** The call rejects the way a missing transaction does. */
44
+ notFound: () => ({ _tag: "notFound" }),
45
+ /**
46
+ * The call never settles until the caller's `AbortSignal` fires, then rejects
47
+ * with a `DEADLINE_EXCEEDED`. `found` says whether a later `getTransaction`
48
+ * for the same digest should find the transaction.
49
+ */
50
+ timeoutThen: (found) => ({ _tag: "timeoutThen", found })
51
+ };
52
+ const DEFAULT_GAS = {
53
+ computationCost: "1000000",
54
+ storageCost: "2000000",
55
+ storageRebate: "980000",
56
+ nonRefundableStorageFee: "20000"
57
+ };
58
+ const addressOwner = (address) => ({
59
+ $kind: "AddressOwner",
60
+ AddressOwner: normalizeSuiAddress(address)
61
+ });
62
+ const clockObject = (timestampMs) => ({
63
+ objectId: SUI_CLOCK_OBJECT_ID,
64
+ type: CLOCK_TYPE,
65
+ version: 1n,
66
+ digest: fakeDigest(6),
67
+ owner: { $kind: "Shared", Shared: { initialSharedVersion: "1" } },
68
+ content: ClockBcs.serialize({
69
+ id: SUI_CLOCK_OBJECT_ID,
70
+ timestamp_ms: timestampMs.toString()
71
+ }).toBytes()
72
+ });
73
+ /**
74
+ * The SDK shape of an object, with the optional fields served only when the
75
+ * caller asked for them — the way a real node behaves, and what lets a test
76
+ * assert that `Tx.reconcile` sent `previousTransaction` in its include set.
77
+ */
78
+ const toSdkObject = (object, include) => ({
79
+ objectId: normalizeSuiAddress(object.objectId),
80
+ version: object.version.toString(),
81
+ digest: object.digest ?? fakeDigest(Number(object.version % 200n) + 1),
82
+ owner: object.owner ?? addressOwner("0x1"),
83
+ type: object.type,
84
+ content: include === undefined || include.content === true ? object.content : undefined,
85
+ previousTransaction: include?.previousTransaction === true
86
+ ? object.previousTransaction ?? null
87
+ : undefined,
88
+ objectBcs: undefined,
89
+ json: undefined,
90
+ display: undefined
91
+ });
92
+ const changedObject = (change, kind) => ({
93
+ objectId: normalizeSuiAddress(change.objectId),
94
+ inputState: kind === "created" ? "DoesNotExist" : "Exists",
95
+ inputVersion: kind === "created"
96
+ ? null
97
+ : (change.inputVersion ?? (change.version ?? 2n) - 1n).toString(),
98
+ inputDigest: kind === "created" ? null : fakeDigest(11),
99
+ inputOwner: kind === "created" ? null : (change.owner ?? addressOwner("0x1")),
100
+ outputState: change.outputState ?? (kind === "deleted" ? "DoesNotExist" : "ObjectWrite"),
101
+ outputVersion: kind === "deleted" ? null : (change.version ?? 2n).toString(),
102
+ outputDigest: kind === "deleted" ? null : fakeDigest(12),
103
+ outputOwner: kind === "deleted" ? null : (change.owner ?? addressOwner("0x1")),
104
+ idOperation: kind === "created" ? "Created" : kind === "deleted" ? "Deleted" : "None"
105
+ });
106
+ /** One scripted command result with both of the arrays the wire shape requires. */
107
+ const toCommandResult = (result) => ({
108
+ returnValues: result.returnValues ?? [],
109
+ mutatedReferences: result.mutatedReferences ?? []
110
+ });
111
+ const commandResultsOf = (execution) => (execution.commandResults ?? []).map(toCommandResult);
112
+ const executionToTransaction = (execution, status, digest) => {
113
+ const changes = [
114
+ ...(execution.created ?? []).map((change) => changedObject(change, "created")),
115
+ ...(execution.mutated ?? []).map((change) => changedObject(change, "mutated")),
116
+ ...(execution.deleted ?? []).map((change) => changedObject(change, "deleted"))
117
+ ];
118
+ const objectTypes = {};
119
+ for (const change of [
120
+ ...(execution.created ?? []),
121
+ ...(execution.mutated ?? []),
122
+ ...(execution.deleted ?? [])
123
+ ]) {
124
+ objectTypes[normalizeSuiAddress(change.objectId)] = change.type;
125
+ }
126
+ return {
127
+ digest,
128
+ signatures: [],
129
+ epoch: "1",
130
+ timestampMs: execution.timestampMs ?? null,
131
+ checkpoint: execution.checkpoint ?? null,
132
+ status,
133
+ balanceChanges: execution.balanceChanges ?? [],
134
+ effects: {
135
+ bcs: null,
136
+ version: 2,
137
+ status,
138
+ gasUsed: execution.gasUsed ?? DEFAULT_GAS,
139
+ transactionDigest: digest,
140
+ gasObject: null,
141
+ eventsDigest: null,
142
+ dependencies: [],
143
+ lamportVersion: "2",
144
+ changedObjects: changes,
145
+ unchangedConsensusObjects: [],
146
+ auxiliaryDataDigest: null
147
+ },
148
+ events: execution.events ?? [],
149
+ objectTypes,
150
+ transaction: undefined,
151
+ bcs: undefined
152
+ };
153
+ };
154
+ /** A gRPC-shaped rejection, so `mapSdkError` reads `code` off it. */
155
+ /** The epoch the fake reports when a script does not set one. */
156
+ export const DEFAULT_EPOCH = 100n;
157
+ /** A plausible `SystemStateInfo` around the one field anything here reads. */
158
+ const systemState = (epoch, timestampMs) => ({
159
+ systemStateVersion: "2",
160
+ epoch: epoch.toString(),
161
+ protocolVersion: "70",
162
+ referenceGasPrice: "1000",
163
+ epochStartTimestampMs: timestampMs.toString(),
164
+ safeMode: false,
165
+ safeModeStorageRewards: "0",
166
+ safeModeComputationRewards: "0",
167
+ safeModeStorageRebates: "0",
168
+ safeModeNonRefundableStorageFee: "0",
169
+ parameters: {
170
+ epochDurationMs: "86400000",
171
+ stakeSubsidyStartEpoch: "0",
172
+ maxValidatorCount: "150",
173
+ minValidatorJoiningStake: "30000000000000000",
174
+ validatorLowStakeThreshold: "20000000000000000",
175
+ validatorLowStakeGracePeriod: "7"
176
+ },
177
+ storageFund: { totalObjectStorageRebates: "0", nonRefundableBalance: "0" },
178
+ stakeSubsidy: {
179
+ balance: "0",
180
+ distributionCounter: "0",
181
+ currentDistributionAmount: "0",
182
+ stakeSubsidyPeriodLength: "10",
183
+ stakeSubsidyDecreaseRate: 1000
184
+ }
185
+ });
186
+ /**
187
+ * Whether two dynamic-field key encodings are the same bytes.
188
+ *
189
+ * A scripted entry with **no** `bcs` matches any key of its type, which is what
190
+ * a test that only cares about the type wants; two entries of one type on one
191
+ * parent are told apart by their bytes, which is what a test that cares about
192
+ * the key encoding needs and what matching on `name.type` alone made
193
+ * impossible. Never fails.
194
+ */
195
+ const sameBytes = (left, right) => {
196
+ if (left === undefined || right === undefined)
197
+ return true;
198
+ if (left.length !== right.length)
199
+ return false;
200
+ for (let index = 0; index < left.length; index++) {
201
+ if (left[index] !== right[index])
202
+ return false;
203
+ }
204
+ return true;
205
+ };
206
+ const rpcError = (status) => {
207
+ const error = new Error(`fake transport error: ${status}`);
208
+ Object.assign(error, { code: status, name: "RpcError" });
209
+ return error;
210
+ };
211
+ const abortError = () => {
212
+ const error = new Error("The operation was aborted");
213
+ Object.assign(error, { name: "AbortError", code: "DEADLINE_EXCEEDED" });
214
+ return error;
215
+ };
216
+ /** Thrown, and re-thrown as a defect, when a test reaches an unscripted method. */
217
+ export class FakeUnimplemented extends Error {
218
+ method;
219
+ [DefectMarker] = true;
220
+ constructor(method) {
221
+ super(`SuiCoreFake: ${method} is not scripted. Add it to the script, or use a narrower test.`);
222
+ this.method = method;
223
+ this.name = "FakeUnimplemented";
224
+ }
225
+ }
226
+ const unimplemented = (method) => {
227
+ throw new FakeUnimplemented(method);
228
+ };
229
+ /**
230
+ * The in-memory `SuiCore`.
231
+ *
232
+ * Three things about it that a test has to know, because they are not visible
233
+ * from the outside:
234
+ *
235
+ * - **Its `client` implements `$extend`.** `SuiCoreFakeState.client` is a
236
+ * `ClientWithCoreApi`, so `fake.client.$extend(myExtension(options))` gives a
237
+ * derived Promise face over the fake and a test can exercise a registration
238
+ * exactly the way a consumer writes it, with no network.
239
+ * - **`listOwnedObjects` filters like a node.** The `type` option goes through
240
+ * the same `typeMatches` rule as the BCS bridge, so a bare tag matches every
241
+ * instantiation of a generic rather than only its own spelling.
242
+ * - **`getDynamicField` matches on `name.type` *and* `name.bcs`.** An entry
243
+ * scripted without `bcs` still matches any key of its type, which is what a
244
+ * test that only cares about the type wants; two entries of one type on one
245
+ * parent are told apart by their bytes, so a test can prove which key encoding
246
+ * a lookup used.
247
+ * - **Call recording is reached through `SuiTest.calls`.** Every call the fake
248
+ * received, oldest first, optionally filtered by method:
249
+ * `yield* SuiTest.calls("getDynamicField")`. That is how a test asserts what
250
+ * an extension *sent* — the include set on a read, the fact that a recipe
251
+ * fragment reached exactly one `executeTransaction` — rather than only what it
252
+ * got back.
253
+ * - **`getTransaction` can be answered by digest.** `FakeScript.transactions`
254
+ * (and `SuiTest.recordTransaction`) answers a specific digest before the
255
+ * ordered `getTransaction` script is consulted, which is what `Tx.reconcile`
256
+ * needs when it follows a pinned object's `previousTransaction` to find out
257
+ * which version the consuming transaction took. `FakeChange.inputVersion` is
258
+ * how a test says which version that was.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * import { Effect } from "effect"
263
+ * import { SuiCoreFake } from "@unconfirmed/sui-effect/testing"
264
+ *
265
+ * const layer = SuiCoreFake.layer({ chainId: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S" })
266
+ * ```
267
+ */
268
+ export class SuiCoreFake extends Context.Service()("@unconfirmed/sui-effect/testing/SuiCoreFake") {
269
+ /**
270
+ * Provides both `SuiCore` (backed by the fake) and `SuiCoreFake` (the handle a
271
+ * test drives it with). Never fails.
272
+ */
273
+ static layer = (script = {}) => {
274
+ const fake = Layer.effect(SuiCoreFake, makeState(script));
275
+ const core = Layer.effect(SuiCore, Effect.gen(function* () {
276
+ const state = yield* SuiCoreFake;
277
+ return state.core;
278
+ })).pipe(Layer.provide(fake));
279
+ return Layer.merge(fake, core);
280
+ };
281
+ }
282
+ const makeState = (script) => Effect.sync(() => {
283
+ const objects = new Map();
284
+ for (const object of script.objects ?? []) {
285
+ objects.set(normalizeSuiAddress(object.objectId), object);
286
+ }
287
+ const history = new Map();
288
+ for (const object of script.objects ?? []) {
289
+ const id = normalizeSuiAddress(object.objectId);
290
+ history.set(id, new Map([[object.version.toString(), object.previousTransaction]]));
291
+ }
292
+ return makeInternal(script, {
293
+ objects,
294
+ history,
295
+ coins: [...(script.coins ?? [])],
296
+ deleted: new Set(),
297
+ clockTimestampMs: script.clockTimestampMs ?? 1700000000000n,
298
+ epoch: script.epoch ?? DEFAULT_EPOCH,
299
+ calls: [],
300
+ aborted: 0,
301
+ cursors: { simulate: 0, execute: 0, getTransaction: 0, buildSimulate: 0 },
302
+ scripts: {
303
+ simulate: script.simulate ?? [],
304
+ execute: script.execute ?? [],
305
+ getTransaction: script.getTransaction ?? [],
306
+ buildSimulate: script.buildSimulate ?? []
307
+ },
308
+ pendingDigests: new Map(),
309
+ knownTransactions: new Map(),
310
+ byDigest: new Map(Object.entries(script.transactions ?? {}))
311
+ });
312
+ });
313
+ const makeInternal = (script, state) => {
314
+ const pageSize = script.pageSize ?? 50;
315
+ const chainId = script.chainId ?? "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S";
316
+ /**
317
+ * The most recent `AbortSignal` the fake was handed.
318
+ *
319
+ * The SDK's `Transaction#build` takes no signal; the only way one reaches a
320
+ * resolver is through the `client` it was given, one Core call at a time. The
321
+ * fake's resolve plugin makes a real Core call for the gas price, so this
322
+ * holds the signal `Tx.build` injected — which is what lets a scripted
323
+ * `buildSimulate: [FakeOutcome.timeoutThen(...)]` block until the build is
324
+ * interrupted, and therefore what lets a test prove that an interrupted build
325
+ * cancels the request it started.
326
+ */
327
+ let lastSignal;
328
+ const record = (method, options) => {
329
+ const signal = signalOf(options);
330
+ if (signal !== undefined)
331
+ lastSignal = signal;
332
+ state.calls.push({ method, options });
333
+ };
334
+ /** Remembers which transaction produced one version of one object. */
335
+ const remember = (objectId, version, by) => {
336
+ const id = normalizeSuiAddress(objectId);
337
+ const versions = state.history.get(id) ?? new Map();
338
+ versions.set(version.toString(), by);
339
+ state.history.set(id, versions);
340
+ };
341
+ const isCoin = (type) => {
342
+ try {
343
+ return normalizeStructTag(type).startsWith("0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<");
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ };
349
+ /** The object ids a set of signed bytes names as gas payment. */
350
+ const paymentOfBytes = (bytes) => {
351
+ try {
352
+ return (TransactionDataBuilder.fromBytes(bytes).gasData.payment ?? []).map((ref) => normalizeSuiAddress(ref.objectId));
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ };
358
+ /**
359
+ * Evolves the fake's coin set the way an execution would: a coin the
360
+ * transaction deleted is gone, a coin it mutated takes its new version and
361
+ * balance, a gas coin is bumped because gas is always mutated, and a coin the
362
+ * transaction created joins the set.
363
+ *
364
+ * Without this, gas selection in a second build picks the same coins at the
365
+ * same versions the first build already spent, and no lifecycle test can tell
366
+ * a correct selection from a stale one.
367
+ */
368
+ const evolveCoins = (execution, payment) => {
369
+ const deleted = new Set((execution.deleted ?? []).map((change) => normalizeSuiAddress(change.objectId)));
370
+ const mutated = new Map((execution.mutated ?? []).map((change) => [normalizeSuiAddress(change.objectId), change]));
371
+ const next = [];
372
+ for (const coin of state.coins) {
373
+ const id = normalizeSuiAddress(coin.objectId);
374
+ if (deleted.has(id))
375
+ continue;
376
+ const change = mutated.get(id);
377
+ if (change !== undefined) {
378
+ next.push({
379
+ ...coin,
380
+ version: (change.version ?? BigInt(coin.version) + 1n).toString(),
381
+ ...(change.balance === undefined ? {} : { balance: change.balance.toString() })
382
+ });
383
+ continue;
384
+ }
385
+ if (payment.includes(id)) {
386
+ next.push({ ...coin, version: (BigInt(coin.version) + 1n).toString() });
387
+ continue;
388
+ }
389
+ next.push(coin);
390
+ }
391
+ for (const change of execution.created ?? []) {
392
+ if (!isCoin(change.type))
393
+ continue;
394
+ const id = normalizeSuiAddress(change.objectId);
395
+ if (next.some((coin) => normalizeSuiAddress(coin.objectId) === id))
396
+ continue;
397
+ next.push({
398
+ objectId: id,
399
+ version: (change.version ?? 2n).toString(),
400
+ digest: fakeDigest(13),
401
+ owner: change.owner ?? addressOwner("0x1"),
402
+ type: change.type,
403
+ balance: (change.balance ?? 0n).toString()
404
+ });
405
+ }
406
+ state.coins = next;
407
+ };
408
+ const next = (key) => {
409
+ const outcomes = state.scripts[key];
410
+ if (outcomes.length === 0)
411
+ return undefined;
412
+ const index = Math.min(state.cursors[key], outcomes.length - 1);
413
+ state.cursors[key] = state.cursors[key] + 1;
414
+ return outcomes[index];
415
+ };
416
+ const waitForAbort = (signal) => new Promise((_resolve, reject) => {
417
+ if (signal.aborted) {
418
+ state.aborted += 1;
419
+ reject(abortError());
420
+ return;
421
+ }
422
+ signal.addEventListener("abort", () => {
423
+ state.aborted += 1;
424
+ reject(abortError());
425
+ }, { once: true });
426
+ });
427
+ const lookup = (objectId) => {
428
+ const id = normalizeSuiAddress(objectId);
429
+ if (id === normalizeSuiAddress(SUI_CLOCK_OBJECT_ID)) {
430
+ return clockObject(state.clockTimestampMs);
431
+ }
432
+ if (state.deleted.has(id)) {
433
+ throw new ObjectError("deleted", `object ${id} was deleted`, {
434
+ reason: "deleted",
435
+ objectId: id
436
+ });
437
+ }
438
+ const object = state.objects.get(id);
439
+ if (object === undefined) {
440
+ throw new ObjectError("notFound", `object ${id} does not exist`, {
441
+ reason: "notFound",
442
+ objectId: id
443
+ });
444
+ }
445
+ return object;
446
+ };
447
+ const applyOutcome = async (outcome, method, digest, executingPayment = []) => {
448
+ switch (outcome._tag) {
449
+ case "transportError":
450
+ throw rpcError(outcome.status);
451
+ case "notFound":
452
+ throw new TransactionError("notFound", digest);
453
+ case "timeoutThen":
454
+ return unimplemented("timeoutThen is handled before applyOutcome");
455
+ case "failWith": {
456
+ const resolved = outcome.value.digest ?? digest;
457
+ if (method === "simulate") {
458
+ throw new SimulationError(`Transaction resolution failed: ${outcome.reason.message}`, {
459
+ executionError: outcome.reason
460
+ });
461
+ }
462
+ return {
463
+ transaction: executionToTransaction(outcome.value, { success: false, error: outcome.reason }, resolved),
464
+ failed: true,
465
+ commandResults: commandResultsOf(outcome.value)
466
+ };
467
+ }
468
+ case "succeed": {
469
+ const resolved = outcome.value.digest ?? digest;
470
+ if (method === "execute") {
471
+ for (const change of [
472
+ ...(outcome.value.created ?? []),
473
+ ...(outcome.value.mutated ?? [])
474
+ ]) {
475
+ const id = normalizeSuiAddress(change.objectId);
476
+ const existing = state.objects.get(id);
477
+ const version = change.version ?? (existing === undefined ? 2n : existing.version + 1n);
478
+ state.objects.set(id, {
479
+ objectId: id,
480
+ type: change.type,
481
+ version,
482
+ content: existing?.content ?? new Uint8Array(),
483
+ // The executing transaction is now what last mutated it, which is
484
+ // what `Tx.reconcile`'s evidence rules read back.
485
+ previousTransaction: resolved,
486
+ ...(change.owner === undefined ? {} : { owner: change.owner })
487
+ });
488
+ remember(id, version, resolved);
489
+ }
490
+ for (const change of outcome.value.deleted ?? []) {
491
+ const id = normalizeSuiAddress(change.objectId);
492
+ const existing = state.objects.get(id);
493
+ // The version the delete produced is still history: it is what
494
+ // names this transaction as the one that consumed the version
495
+ // before it.
496
+ remember(id, change.version ?? (existing === undefined ? 2n : existing.version + 1n), resolved);
497
+ state.objects.delete(id);
498
+ state.deleted.add(id);
499
+ }
500
+ evolveCoins(outcome.value, executingPayment);
501
+ }
502
+ return {
503
+ transaction: executionToTransaction(outcome.value, { success: true, error: null }, resolved),
504
+ failed: false,
505
+ commandResults: commandResultsOf(outcome.value)
506
+ };
507
+ }
508
+ }
509
+ };
510
+ const page = (items, cursor, limit) => {
511
+ const start = cursor === null || cursor === undefined ? 0 : Number(cursor);
512
+ const size = limit ?? pageSize;
513
+ const slice = items.slice(start, start + size);
514
+ const end = start + slice.length;
515
+ const hasNextPage = end < items.length;
516
+ return { items: slice, hasNextPage, cursor: hasNextPage ? String(end) : null };
517
+ };
518
+ const signalOf = (options) => {
519
+ if (typeof options !== "object" || options === null)
520
+ return undefined;
521
+ const signal = options.signal;
522
+ return signal instanceof AbortSignal ? signal : undefined;
523
+ };
524
+ const pending = async (method, options) => {
525
+ const signal = signalOf(options);
526
+ if (signal === undefined) {
527
+ return unimplemented(`${method} (the fake needs an AbortSignal to model a timeout)`);
528
+ }
529
+ return waitForAbort(signal);
530
+ };
531
+ const METHOD_NAMES = {
532
+ simulate: "simulateTransaction",
533
+ execute: "executeTransaction",
534
+ getTransaction: "getTransaction"
535
+ };
536
+ const settle = async (method, digest, options, executingPayment = []) => {
537
+ const outcome = next(method) ?? unimplemented(METHOD_NAMES[method]);
538
+ if (outcome._tag === "timeoutThen") {
539
+ state.pendingDigests.set(digest, outcome.found);
540
+ return pending(method, options);
541
+ }
542
+ return applyOutcome(outcome, method, digest, executingPayment);
543
+ };
544
+ const asResult = (result) => result.failed
545
+ ? { $kind: "FailedTransaction", FailedTransaction: result.transaction }
546
+ : { $kind: "Transaction", Transaction: result.transaction };
547
+ const coinsOf = (owner, coinType) => {
548
+ const address = normalizeSuiAddress(owner);
549
+ const wanted = normalizeStructTag(`0x2::coin::Coin<${coinType ?? SUI_TYPE_ARG}>`);
550
+ return state.coins.filter((coin) => {
551
+ if (coin.owner.$kind !== "AddressOwner")
552
+ return false;
553
+ if (normalizeSuiAddress(coin.owner.AddressOwner) !== address)
554
+ return false;
555
+ try {
556
+ return normalizeStructTag(coin.type) === wanted;
557
+ }
558
+ catch {
559
+ return false;
560
+ }
561
+ });
562
+ };
563
+ /**
564
+ * The fake's stand-in for the transport's resolve plugin, so
565
+ * `transaction.build({ client })` works with no network.
566
+ *
567
+ * It fills the gas price from the scripted reference gas price, the gas
568
+ * budget from `script.gasBudget` (default 50 MIST-per-unit times the price),
569
+ * gas payment from the scripted coins the payer owns when it is unset (an
570
+ * explicit `[]` is left alone, the way a sponsored transaction wants it), and
571
+ * resolves every `UnresolvedObject` input from the object map, choosing a
572
+ * shared, immutable or owned reference from the stored owner. Everything else
573
+ * — argument normalization, BCS layout, validation — is the SDK's own
574
+ * `TransactionDataBuilder`.
575
+ *
576
+ * It does not run Move, so it cannot resolve an `UnresolvedPure` whose type
577
+ * only the function signature knows: use `tx.pure.u64(...)` and friends.
578
+ */
579
+ const resolvePlugin = async (transactionData, options, proceed) => {
580
+ // A real resolver reads the reference gas price through the client it was
581
+ // given, and that is the only place an `AbortSignal` reaches a resolver at
582
+ // all: `BuildTransactionOptions` has no signal field. Going through the
583
+ // client rather than reading the script directly is what makes an
584
+ // interrupted `Tx.build` observable here.
585
+ const core = options.client?.core ?? client.core;
586
+ const { referenceGasPrice } = await core.getReferenceGasPrice({});
587
+ // A real transport's resolver simulates to choose the gas budget, which is
588
+ // why `Tx.build` can fail with `SimulationFailed`. The fake does it only
589
+ // when a test scripted it.
590
+ const budgetOutcome = state.scripts.buildSimulate.length === 0
591
+ ? undefined
592
+ : next("buildSimulate");
593
+ if (budgetOutcome !== undefined) {
594
+ if (budgetOutcome._tag === "timeoutThen") {
595
+ // Never settles until the build is interrupted, which is exactly what a
596
+ // resolver's in-flight simulate does.
597
+ if (lastSignal === undefined) {
598
+ return unimplemented("buildSimulate timeoutThen (no AbortSignal reached the resolver; build through Tx.build)");
599
+ }
600
+ await waitForAbort(lastSignal);
601
+ }
602
+ await applyOutcome(budgetOutcome, "simulate", fakeDigest(state.calls.length + 200));
603
+ }
604
+ if (!options.onlyTransactionKind) {
605
+ if (!transactionData.gasData.price) {
606
+ transactionData.gasData.price = String(referenceGasPrice);
607
+ }
608
+ if (!transactionData.gasData.budget) {
609
+ transactionData.gasData.budget = String(script.gasBudget ?? 50000000n);
610
+ }
611
+ }
612
+ transactionData.inputs.forEach((input, index) => {
613
+ const unresolved = input.UnresolvedObject;
614
+ if (!unresolved)
615
+ return;
616
+ const id = normalizeSuiAddress(unresolved.objectId);
617
+ const object = lookup(id);
618
+ const owner = object.owner ?? addressOwner("0x1");
619
+ const initialSharedVersion = owner.$kind === "Shared"
620
+ ? owner.Shared.initialSharedVersion
621
+ : owner.$kind === "ConsensusAddressOwner"
622
+ ? owner.ConsensusAddressOwner.startVersion
623
+ : null;
624
+ const shared = unresolved.initialSharedVersion ?? initialSharedVersion;
625
+ transactionData.inputs[index] = shared
626
+ ? Inputs.SharedObjectRef({
627
+ objectId: id,
628
+ initialSharedVersion: shared,
629
+ mutable: unresolved.mutable ?? true
630
+ })
631
+ : Inputs.ObjectRef({
632
+ objectId: id,
633
+ digest: unresolved.digest ?? toSdkObject(object).digest,
634
+ version: unresolved.version ?? object.version.toString()
635
+ });
636
+ });
637
+ if (!options.onlyTransactionKind && transactionData.gasData.payment == null) {
638
+ const payer = transactionData.gasData.owner ?? transactionData.sender;
639
+ // A gas coin may not also be an input of the transaction: the SDK
640
+ // documents the prohibition and a validator enforces it. Resolving the
641
+ // inputs first and then excluding them is what a real transport does.
642
+ const inputIds = new Set();
643
+ for (const input of transactionData.inputs) {
644
+ const owned = input.Object?.ImmOrOwnedObject ?? input.Object?.Receiving;
645
+ if (owned !== undefined)
646
+ inputIds.add(normalizeSuiAddress(owned.objectId));
647
+ }
648
+ const coins = payer === null
649
+ ? []
650
+ : coinsOf(payer).filter((coin) => !inputIds.has(normalizeSuiAddress(coin.objectId)));
651
+ transactionData.gasData.payment = coins.map((coin) => ({
652
+ objectId: coin.objectId,
653
+ version: coin.version,
654
+ digest: coin.digest
655
+ }));
656
+ }
657
+ await proceed();
658
+ };
659
+ /**
660
+ * Refuses a submission that carries fewer signatures than the bytes name
661
+ * distinct signing addresses.
662
+ *
663
+ * A transaction takes a signature from its sender and, when its gas owner is
664
+ * someone else, from that party too; one signature on sponsored bytes is
665
+ * something a validator rejects outright. The fake used to accept it, which
666
+ * is how a whole family of sponsorship and locking tests passed while
667
+ * describing a transaction that could never land.
668
+ */
669
+ const assertSignatures = (bytes, signatures) => {
670
+ let required;
671
+ try {
672
+ const data = TransactionDataBuilder.fromBytes(bytes);
673
+ required = [...new Set([data.sender, data.gasData.owner]
674
+ .filter((address) => typeof address === "string" && address.length > 0)
675
+ .map((address) => normalizeSuiAddress(address)))];
676
+ }
677
+ catch {
678
+ return;
679
+ }
680
+ if (signatures.length < required.length) {
681
+ const error = new Error(`fake validator: these bytes name ${required.length} signer(s) (${required.join(", ")})` +
682
+ ` and carry ${signatures.length} signature(s)`);
683
+ Object.assign(error, { code: "INVALID_ARGUMENT", name: "RpcError" });
684
+ throw error;
685
+ }
686
+ };
687
+ /** The digest of the bytes that were handed to us, as the network derives it. */
688
+ const digestOfBytes = (bytes) => {
689
+ try {
690
+ return TransactionDataBuilder.getDigestFromBytes(bytes);
691
+ }
692
+ catch {
693
+ return fakeDigest(state.calls.length + 1);
694
+ }
695
+ };
696
+ const core = {
697
+ getObject: async (options) => {
698
+ record("getObject", options);
699
+ return { object: toSdkObject(lookup(options.objectId), options.include) };
700
+ },
701
+ getObjects: async (options) => {
702
+ record("getObjects", options);
703
+ return {
704
+ objects: options.objectIds.map((id) => {
705
+ try {
706
+ return toSdkObject(lookup(id), options.include);
707
+ }
708
+ catch (cause) {
709
+ return cause;
710
+ }
711
+ })
712
+ };
713
+ },
714
+ listOwnedObjects: async (options) => {
715
+ record("listOwnedObjects", options);
716
+ const owner = normalizeSuiAddress(options.owner);
717
+ const owned = [...state.objects.values()].filter((object) => {
718
+ const objectOwner = object.owner ?? addressOwner("0x1");
719
+ if (objectOwner.$kind !== "AddressOwner")
720
+ return false;
721
+ if (normalizeSuiAddress(objectOwner.AddressOwner) !== owner)
722
+ return false;
723
+ // A node matches a bare type filter against every instantiation of a
724
+ // generic; exact string equality here would hide `Composition<Share>`
725
+ // from a `streamOwnedObjects(owner, { type: "pkg::m::Composition" })`.
726
+ return options.type === undefined || typeMatches(options.type, object.type);
727
+ });
728
+ const result = page(owned, options.cursor, options.limit);
729
+ return {
730
+ objects: result.items.map((object) => toSdkObject(object, options.include)),
731
+ hasNextPage: result.hasNextPage,
732
+ cursor: result.cursor
733
+ };
734
+ },
735
+ listCoins: async (options) => {
736
+ record("listCoins", options);
737
+ const result = page(coinsOf(options.owner, options.coinType), options.cursor, options.limit);
738
+ return {
739
+ objects: result.items,
740
+ hasNextPage: result.hasNextPage,
741
+ cursor: result.cursor
742
+ };
743
+ },
744
+ listDynamicFields: async (options) => {
745
+ record("listDynamicFields", options);
746
+ const fields = script.dynamicFields?.[normalizeSuiAddress(options.parentId)] ?? [];
747
+ const result = page(fields, options.cursor, options.limit);
748
+ return {
749
+ dynamicFields: result.items,
750
+ hasNextPage: result.hasNextPage,
751
+ cursor: result.cursor
752
+ };
753
+ },
754
+ getDynamicField: async (options) => {
755
+ record("getDynamicField", options);
756
+ const parent = normalizeSuiAddress(options.parentId);
757
+ const entry = (script.dynamicFields?.[parent] ?? []).find((field) => field.name.type === options.name.type && sameBytes(field.name.bcs, options.name.bcs));
758
+ if (entry === undefined) {
759
+ throw new ObjectError("notFound", "dynamic field not found", {
760
+ reason: "notFound",
761
+ objectId: parent
762
+ });
763
+ }
764
+ const value = script.dynamicFieldValues?.[entry.fieldId] ?? {
765
+ type: entry.valueType,
766
+ bcs: new Uint8Array()
767
+ };
768
+ return {
769
+ dynamicField: {
770
+ ...entry,
771
+ value,
772
+ version: "1",
773
+ digest: fakeDigest(21),
774
+ previousTransaction: null
775
+ }
776
+ };
777
+ },
778
+ getDynamicObjectField: async (options) => {
779
+ record("getDynamicObjectField", options);
780
+ return unimplemented("getDynamicObjectField");
781
+ },
782
+ getBalance: async (options) => {
783
+ record("getBalance", options);
784
+ const coinType = options.coinType ?? "0x2::sui::SUI";
785
+ const balance = (script.balances ?? []).find((item) => item.coinType === coinType);
786
+ return {
787
+ balance: balance ?? { coinType, balance: "0", coinBalance: "0", addressBalance: "0" }
788
+ };
789
+ },
790
+ listBalances: async (options) => {
791
+ record("listBalances", options);
792
+ const result = page(script.balances ?? [], options.cursor, options.limit);
793
+ return {
794
+ balances: result.items,
795
+ hasNextPage: result.hasNextPage,
796
+ cursor: result.cursor
797
+ };
798
+ },
799
+ getCoinMetadata: async (options) => {
800
+ record("getCoinMetadata", options);
801
+ return unimplemented("getCoinMetadata");
802
+ },
803
+ getTransaction: async (options) => {
804
+ record("getTransaction", options);
805
+ const pending = state.pendingDigests.get(options.digest);
806
+ if (pending === false)
807
+ throw new TransactionError("notFound", options.digest);
808
+ // Keyed by digest, so following one object's `previousTransaction` to the
809
+ // transaction that consumed it does not eat an entry of the ordered
810
+ // script, which is about *this* transaction and not about that one.
811
+ const recorded = state.byDigest.get(options.digest);
812
+ if (recorded !== undefined && recorded._tag !== "timeoutThen") {
813
+ return asResult(await applyOutcome(recorded, "getTransaction", options.digest));
814
+ }
815
+ const known = state.knownTransactions.get(options.digest);
816
+ if (known !== undefined && state.scripts.getTransaction.length === 0) {
817
+ return asResult(known);
818
+ }
819
+ return asResult(await settle("getTransaction", options.digest, options));
820
+ },
821
+ executeTransaction: async (options) => {
822
+ record("executeTransaction", options);
823
+ const digest = digestOfBytes(options.transaction);
824
+ assertSignatures(options.transaction, options.signatures ?? []);
825
+ const known = state.knownTransactions.get(digest);
826
+ if (known !== undefined) {
827
+ // Re-submitting identical bytes is what `Tx.submit` does after a
828
+ // retryable transport failure, and a validator answers with the
829
+ // recorded result rather than executing again. Re-applying the scripted
830
+ // changes here took an object from version 3 to version 5 and made
831
+ // replay idempotency untestable.
832
+ return asResult(known);
833
+ }
834
+ const settled = await settle("execute", digest, options, paymentOfBytes(options.transaction));
835
+ state.knownTransactions.set(digest, settled);
836
+ return asResult(settled);
837
+ },
838
+ signAndExecuteTransaction: async (options) => {
839
+ record("signAndExecuteTransaction", options);
840
+ return unimplemented("signAndExecuteTransaction");
841
+ },
842
+ waitForTransaction: async (options) => {
843
+ record("waitForTransaction", options);
844
+ const digest = "digest" in options && options.digest !== undefined
845
+ ? options.digest
846
+ : fakeDigest(1);
847
+ const pending = state.pendingDigests.get(digest);
848
+ if (pending === false)
849
+ throw new TransactionError("notFound", digest);
850
+ // A transaction this fake executed is visible: the visibility wait
851
+ // `Tx.submit` makes after a successful execute must not consume the
852
+ // `getTransaction` script a reconcile test set up.
853
+ const known = state.knownTransactions.get(digest);
854
+ if (known !== undefined)
855
+ return asResult(known);
856
+ return asResult(await settle("getTransaction", digest, options));
857
+ },
858
+ simulateTransaction: async (options) => {
859
+ record("simulateTransaction", options);
860
+ const digest = options.transaction instanceof Uint8Array
861
+ ? digestOfBytes(options.transaction)
862
+ : fakeDigest(state.calls.length + 100);
863
+ const result = await settle("simulate", digest, options);
864
+ return { ...asResult(result), commandResults: result.commandResults };
865
+ },
866
+ listTransactions: async (options) => {
867
+ record("listTransactions", options);
868
+ return unimplemented("listTransactions");
869
+ },
870
+ listEvents: async (options) => {
871
+ record("listEvents", options);
872
+ return unimplemented("listEvents");
873
+ },
874
+ getReferenceGasPrice: async (options) => {
875
+ record("getReferenceGasPrice", options);
876
+ return { referenceGasPrice: (script.referenceGasPrice ?? 1000n).toString() };
877
+ },
878
+ getCurrentSystemState: async (options) => {
879
+ record("getCurrentSystemState", options);
880
+ return { systemState: systemState(state.epoch, state.clockTimestampMs) };
881
+ },
882
+ getProtocolConfig: async (options) => {
883
+ record("getProtocolConfig", options);
884
+ return unimplemented("getProtocolConfig");
885
+ },
886
+ getChainIdentifier: async (options) => {
887
+ record("getChainIdentifier", options);
888
+ return { chainIdentifier: chainId };
889
+ },
890
+ getMoveFunction: async (options) => {
891
+ record("getMoveFunction", options);
892
+ return unimplemented("getMoveFunction");
893
+ },
894
+ verifyZkLoginSignature: async (options) => {
895
+ record("verifyZkLoginSignature", options);
896
+ return unimplemented("verifyZkLoginSignature");
897
+ },
898
+ resolveNameServiceAddress: async (options) => {
899
+ record("resolveNameServiceAddress", options);
900
+ return unimplemented("resolveNameServiceAddress");
901
+ },
902
+ defaultNameServiceName: async (options) => {
903
+ record("defaultNameServiceName", options);
904
+ return unimplemented("defaultNameServiceName");
905
+ },
906
+ mvr: {
907
+ resolvePackage: async (options) => {
908
+ record("mvr.resolvePackage", options);
909
+ return unimplemented("mvr.resolvePackage");
910
+ },
911
+ resolveType: async (options) => {
912
+ record("mvr.resolveType", options);
913
+ return unimplemented("mvr.resolveType");
914
+ },
915
+ resolve: async (options) => {
916
+ record("mvr.resolve", options);
917
+ return unimplemented("mvr.resolve");
918
+ }
919
+ },
920
+ resolveTransactionPlugin: () => resolvePlugin
921
+ };
922
+ /**
923
+ * The historical object read, in the JSON-RPC `sui_tryGetPastObject` shape.
924
+ *
925
+ * `SuiCore.getObjectAtVersion` duck-types this method, so implementing it is
926
+ * what gives the fake a transaction history and lets `Tx.reconcile` identify
927
+ * the consumer of a pinned version the way it does against a real node. A
928
+ * version the fake never recorded answers `VersionNotFound`, which is
929
+ * evidence of nothing.
930
+ */
931
+ const tryGetPastObject = async (options) => {
932
+ record("tryGetPastObject", options);
933
+ const id = normalizeSuiAddress(options.id);
934
+ const versions = state.history.get(id);
935
+ const version = String(options.version);
936
+ if (versions === undefined || !versions.has(version)) {
937
+ return { status: "VersionNotFound", details: [id, version] };
938
+ }
939
+ return {
940
+ status: "VersionFound",
941
+ details: {
942
+ objectId: id,
943
+ version,
944
+ previousTransaction: versions.get(version) ?? null
945
+ }
946
+ };
947
+ };
948
+ const client = {
949
+ network: script.network ?? "localnet",
950
+ cache: undefined,
951
+ core,
952
+ tryGetPastObject,
953
+ // The SDK's registration mechanism, implemented so an extension's derived
954
+ // Promise face can be tested exactly the way a consumer writes it:
955
+ // `client.$extend(myExtension())` — variadic, and chainable, because the
956
+ // SDK's is both and a test that chains two registrations must not silently
957
+ // lose the first.
958
+ $extend(...registrations) {
959
+ const base = (this ?? client);
960
+ return Object.assign(Object.create(Object.getPrototypeOf(base) ?? Object.prototype), base, Object.fromEntries(
961
+ // `client`, not `base`: the registration gets the one underlying
962
+ // client object, so two registrations on it share a base `Sui` the
963
+ // way they do against a real client.
964
+ registrations.map((registration) => [registration.name, registration.register(client)])));
965
+ }
966
+ };
967
+ return {
968
+ core: makeFromClient(client),
969
+ client,
970
+ calls: Effect.sync(() => [...state.calls]),
971
+ aborted: Effect.sync(() => state.aborted),
972
+ readObject: (objectId) => Effect.sync(() => Option.fromNullishOr(state.objects.get(normalizeSuiAddress(objectId)))),
973
+ setObject: (object) => Effect.sync(() => {
974
+ const id = normalizeSuiAddress(object.objectId);
975
+ state.objects.set(id, object);
976
+ state.deleted.delete(id);
977
+ // Every version the fake has ever served is history, which is what a
978
+ // versioned read asks for. `SuiTest.bumpVersion(id, { consumedBy })`
979
+ // lands here, so "someone else spent this version" is recorded at the
980
+ // version it produced.
981
+ remember(id, object.version, object.previousTransaction);
982
+ }),
983
+ deleteObject: (objectId) => Effect.sync(() => {
984
+ const id = normalizeSuiAddress(objectId);
985
+ state.objects.delete(id);
986
+ state.deleted.add(id);
987
+ }),
988
+ setClock: (timestampMs) => Effect.sync(() => {
989
+ state.clockTimestampMs = timestampMs;
990
+ }),
991
+ setEpoch: (epoch) => Effect.sync(() => {
992
+ state.epoch = epoch;
993
+ }),
994
+ recordTransaction: (digest, outcome) => Effect.sync(() => {
995
+ state.byDigest.set(digest, outcome);
996
+ }),
997
+ setOutcomes: (method, outcomes) => Effect.sync(() => {
998
+ state.scripts = { ...state.scripts, [method]: outcomes };
999
+ state.cursors = { ...state.cursors, [method]: 0 };
1000
+ })
1001
+ };
1002
+ };
1003
+ //# sourceMappingURL=SuiCoreFake.js.map