@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.
- package/AGENTS.md +209 -0
- package/LICENSE +21 -0
- package/LLMS.md +6707 -0
- package/README.md +300 -0
- package/dist/domain/bcs.d.ts +101 -0
- package/dist/domain/bcs.d.ts.map +1 -0
- package/dist/domain/bcs.js +183 -0
- package/dist/domain/bcs.js.map +1 -0
- package/dist/domain/errors.d.ts +595 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +476 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/executed.d.ts +325 -0
- package/dist/domain/executed.d.ts.map +1 -0
- package/dist/domain/executed.js +253 -0
- package/dist/domain/executed.js.map +1 -0
- package/dist/domain/journal-entry.d.ts +468 -0
- package/dist/domain/journal-entry.d.ts.map +1 -0
- package/dist/domain/journal-entry.js +63 -0
- package/dist/domain/journal-entry.js.map +1 -0
- package/dist/domain/schemas.d.ts +950 -0
- package/dist/domain/schemas.d.ts.map +1 -0
- package/dist/domain/schemas.js +571 -0
- package/dist/domain/schemas.js.map +1 -0
- package/dist/domain/sui-schema.d.ts +12 -0
- package/dist/domain/sui-schema.d.ts.map +1 -0
- package/dist/domain/sui-schema.js +12 -0
- package/dist/domain/sui-schema.js.map +1 -0
- package/dist/extension.d.ts +7 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +7 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +18 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +18 -0
- package/dist/internal.js.map +1 -0
- package/dist/journal.d.ts +51 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +18 -0
- package/dist/journal.js.map +1 -0
- package/dist/script.d.ts +7 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +7 -0
- package/dist/script.js.map +1 -0
- package/dist/services/Journal.d.ts +76 -0
- package/dist/services/Journal.d.ts.map +1 -0
- package/dist/services/Journal.js +70 -0
- package/dist/services/Journal.js.map +1 -0
- package/dist/services/JournalKeyValueStore.d.ts +73 -0
- package/dist/services/JournalKeyValueStore.d.ts.map +1 -0
- package/dist/services/JournalKeyValueStore.js +155 -0
- package/dist/services/JournalKeyValueStore.js.map +1 -0
- package/dist/services/Script.d.ts +210 -0
- package/dist/services/Script.d.ts.map +1 -0
- package/dist/services/Script.js +441 -0
- package/dist/services/Script.js.map +1 -0
- package/dist/services/Signer.d.ts +132 -0
- package/dist/services/Signer.d.ts.map +1 -0
- package/dist/services/Signer.js +158 -0
- package/dist/services/Signer.js.map +1 -0
- package/dist/services/SubmitConfig.d.ts +175 -0
- package/dist/services/SubmitConfig.d.ts.map +1 -0
- package/dist/services/SubmitConfig.js +52 -0
- package/dist/services/SubmitConfig.js.map +1 -0
- package/dist/services/Sui.d.ts +295 -0
- package/dist/services/Sui.d.ts.map +1 -0
- package/dist/services/Sui.js +414 -0
- package/dist/services/Sui.js.map +1 -0
- package/dist/services/SuiCore.d.ts +227 -0
- package/dist/services/SuiCore.d.ts.map +1 -0
- package/dist/services/SuiCore.js +400 -0
- package/dist/services/SuiCore.js.map +1 -0
- package/dist/services/SuiCoreFake.d.ts +278 -0
- package/dist/services/SuiCoreFake.d.ts.map +1 -0
- package/dist/services/SuiCoreFake.js +1003 -0
- package/dist/services/SuiCoreFake.js.map +1 -0
- package/dist/services/SuiExtension.d.ts +208 -0
- package/dist/services/SuiExtension.d.ts.map +1 -0
- package/dist/services/SuiExtension.js +355 -0
- package/dist/services/SuiExtension.js.map +1 -0
- package/dist/services/SuiGraphQL.d.ts +98 -0
- package/dist/services/SuiGraphQL.d.ts.map +1 -0
- package/dist/services/SuiGraphQL.js +118 -0
- package/dist/services/SuiGraphQL.js.map +1 -0
- package/dist/services/Tx.d.ts +687 -0
- package/dist/services/Tx.d.ts.map +1 -0
- package/dist/services/Tx.js +1224 -0
- package/dist/services/Tx.js.map +1 -0
- package/dist/testing.d.ts +88 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +163 -0
- package/dist/testing.js.map +1 -0
- package/dist/tx.d.ts +24 -0
- package/dist/tx.d.ts.map +1 -0
- package/dist/tx.js +24 -0
- package/dist/tx.js.map +1 -0
- package/docs/extensions.md +1685 -0
- package/examples/extension-template/README.md +170 -0
- package/examples/extension-template/package.json +44 -0
- package/examples/extension-template/scripts/check-package.ts +147 -0
- package/examples/extension-template/src/Escrow.ts +445 -0
- package/examples/extension-template/src/Platform.ts +136 -0
- package/examples/extension-template/src/errors.ts +61 -0
- package/examples/extension-template/src/extension.ts +63 -0
- package/examples/extension-template/src/index.ts +34 -0
- package/examples/extension-template/src/schema.ts +158 -0
- package/examples/extension-template/src/upstream.ts +66 -0
- package/examples/extension-template/test/escrow.test.ts +559 -0
- package/examples/extension-template/tsconfig.build.json +26 -0
- package/examples/extension-template/tsconfig.json +35 -0
- 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
|