@movebridge/testing 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,953 @@
1
+ // src/faker.ts
2
+ var SeededRandom = class {
3
+ state;
4
+ constructor(seed) {
5
+ this.state = seed;
6
+ }
7
+ next() {
8
+ let t = this.state += 1831565813;
9
+ t = Math.imul(t ^ t >>> 15, t | 1);
10
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
11
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
12
+ }
13
+ nextInt(min, max) {
14
+ return Math.floor(this.next() * (max - min + 1)) + min;
15
+ }
16
+ nextBigInt(min, max) {
17
+ const range = max - min + 1n;
18
+ const randomFraction = this.next();
19
+ return min + BigInt(Math.floor(Number(range) * randomFraction));
20
+ }
21
+ pick(array2) {
22
+ return array2[this.nextInt(0, array2.length - 1)];
23
+ }
24
+ };
25
+ function createFaker(options) {
26
+ const seed = options?.seed ?? Date.now();
27
+ const random = new SeededRandom(seed);
28
+ function randomHex(length) {
29
+ const chars = "0123456789abcdef";
30
+ let result = "";
31
+ for (let i = 0; i < length; i++) {
32
+ result += chars[random.nextInt(0, 15)];
33
+ }
34
+ return result;
35
+ }
36
+ function randomTimestamp() {
37
+ const now = Date.now();
38
+ const dayAgo = now - 24 * 60 * 60 * 1e3;
39
+ return String(random.nextInt(dayAgo, now) * 1e3);
40
+ }
41
+ function randomSequenceNumber() {
42
+ return String(random.nextInt(0, 1e6));
43
+ }
44
+ return {
45
+ /**
46
+ * Generates a valid random Movement address
47
+ */
48
+ fakeAddress() {
49
+ return `0x${randomHex(64)}`;
50
+ },
51
+ /**
52
+ * Generates a random balance within optional bounds
53
+ */
54
+ fakeBalance(options2) {
55
+ const min = options2?.min ? BigInt(options2.min) : 0n;
56
+ const max = options2?.max ? BigInt(options2.max) : 10000000000n;
57
+ return random.nextBigInt(min, max).toString();
58
+ },
59
+ /**
60
+ * Generates a complete fake transaction
61
+ */
62
+ fakeTransaction() {
63
+ return {
64
+ hash: `0x${randomHex(64)}`,
65
+ sender: `0x${randomHex(64)}`,
66
+ sequenceNumber: randomSequenceNumber(),
67
+ payload: {
68
+ type: "entry_function_payload",
69
+ function: `0x${randomHex(64)}::module::function`,
70
+ typeArguments: [],
71
+ arguments: []
72
+ },
73
+ timestamp: randomTimestamp()
74
+ };
75
+ },
76
+ /**
77
+ * Generates a fake transaction response
78
+ */
79
+ fakeTransactionResponse(success = true) {
80
+ return {
81
+ hash: `0x${randomHex(64)}`,
82
+ success,
83
+ vmStatus: success ? "Executed successfully" : "Move abort",
84
+ gasUsed: String(random.nextInt(100, 1e4)),
85
+ events: []
86
+ };
87
+ },
88
+ /**
89
+ * Generates a fake resource with specified type
90
+ */
91
+ fakeResource(type) {
92
+ return {
93
+ type,
94
+ data: {
95
+ value: String(random.nextInt(0, 1e6))
96
+ }
97
+ };
98
+ },
99
+ /**
100
+ * Generates a fake contract event with specified type
101
+ */
102
+ fakeEvent(type) {
103
+ return {
104
+ type,
105
+ sequenceNumber: randomSequenceNumber(),
106
+ data: {
107
+ value: String(random.nextInt(0, 1e6))
108
+ }
109
+ };
110
+ },
111
+ /**
112
+ * Generates a fake wallet state
113
+ */
114
+ fakeWalletState(connected = false) {
115
+ if (connected) {
116
+ return {
117
+ connected: true,
118
+ address: `0x${randomHex(64)}`,
119
+ publicKey: `0x${randomHex(64)}`
120
+ };
121
+ }
122
+ return {
123
+ connected: false,
124
+ address: null,
125
+ publicKey: null
126
+ };
127
+ },
128
+ /**
129
+ * Generates a fake transaction hash
130
+ */
131
+ fakeTransactionHash() {
132
+ return `0x${randomHex(64)}`;
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/tracker.ts
138
+ import { MovementError } from "@movebridge/core";
139
+ function createCallTracker() {
140
+ const calls = [];
141
+ function deepEqual(a, b) {
142
+ if (a === b) return true;
143
+ if (a === null || b === null) return false;
144
+ if (typeof a !== typeof b) return false;
145
+ if (Array.isArray(a) && Array.isArray(b)) {
146
+ if (a.length !== b.length) return false;
147
+ return a.every((item, index) => deepEqual(item, b[index]));
148
+ }
149
+ if (typeof a === "object" && typeof b === "object") {
150
+ const aKeys = Object.keys(a);
151
+ const bKeys = Object.keys(b);
152
+ if (aKeys.length !== bKeys.length) return false;
153
+ return aKeys.every(
154
+ (key) => deepEqual(
155
+ a[key],
156
+ b[key]
157
+ )
158
+ );
159
+ }
160
+ return false;
161
+ }
162
+ return {
163
+ /**
164
+ * Records a method call
165
+ */
166
+ recordCall(method, args, result, error) {
167
+ calls.push({
168
+ method,
169
+ arguments: args,
170
+ timestamp: Date.now(),
171
+ result,
172
+ error
173
+ });
174
+ },
175
+ /**
176
+ * Gets all calls to a specific method
177
+ */
178
+ getCalls(method) {
179
+ return calls.filter((call) => call.method === method);
180
+ },
181
+ /**
182
+ * Gets the count of calls to a specific method
183
+ */
184
+ getCallCount(method) {
185
+ return calls.filter((call) => call.method === method).length;
186
+ },
187
+ /**
188
+ * Gets all recorded calls
189
+ */
190
+ getAllCalls() {
191
+ return [...calls];
192
+ },
193
+ /**
194
+ * Asserts that a method was called at least once
195
+ */
196
+ assertCalled(method) {
197
+ const count = this.getCallCount(method);
198
+ if (count === 0) {
199
+ throw new MovementError(
200
+ `Expected method "${method}" to be called, but it was never called`,
201
+ "INVALID_ARGUMENT",
202
+ { method, expected: "at least 1 call", actual: 0 }
203
+ );
204
+ }
205
+ },
206
+ /**
207
+ * Asserts that a method was called exactly N times
208
+ */
209
+ assertCalledTimes(method, times) {
210
+ const count = this.getCallCount(method);
211
+ if (count !== times) {
212
+ throw new MovementError(
213
+ `Expected method "${method}" to be called ${times} times, but it was called ${count} times`,
214
+ "INVALID_ARGUMENT",
215
+ { method, expected: times, actual: count }
216
+ );
217
+ }
218
+ },
219
+ /**
220
+ * Asserts that a method was called with specific arguments
221
+ */
222
+ assertCalledWith(method, ...expectedArgs) {
223
+ const methodCalls = this.getCalls(method);
224
+ if (methodCalls.length === 0) {
225
+ throw new MovementError(
226
+ `Expected method "${method}" to be called with arguments, but it was never called`,
227
+ "INVALID_ARGUMENT",
228
+ { method, expectedArgs, actualCalls: [] }
229
+ );
230
+ }
231
+ const hasMatch = methodCalls.some(
232
+ (call) => deepEqual(call.arguments, expectedArgs)
233
+ );
234
+ if (!hasMatch) {
235
+ throw new MovementError(
236
+ `Expected method "${method}" to be called with specific arguments, but no matching call found`,
237
+ "INVALID_ARGUMENT",
238
+ {
239
+ method,
240
+ expectedArgs,
241
+ actualCalls: methodCalls.map((c) => c.arguments)
242
+ }
243
+ );
244
+ }
245
+ },
246
+ /**
247
+ * Asserts that a method was never called
248
+ */
249
+ assertNotCalled(method) {
250
+ const count = this.getCallCount(method);
251
+ if (count > 0) {
252
+ throw new MovementError(
253
+ `Expected method "${method}" to not be called, but it was called ${count} times`,
254
+ "INVALID_ARGUMENT",
255
+ { method, expected: 0, actual: count }
256
+ );
257
+ }
258
+ },
259
+ /**
260
+ * Clears all recorded calls
261
+ */
262
+ clearCalls() {
263
+ calls.length = 0;
264
+ }
265
+ };
266
+ }
267
+
268
+ // src/simulator.ts
269
+ function createNetworkSimulator() {
270
+ let latency = 0;
271
+ let networkErrorEnabled = false;
272
+ let rateLimitMax = null;
273
+ let rateLimitCalls = 0;
274
+ const timeoutMethods = /* @__PURE__ */ new Set();
275
+ return {
276
+ /**
277
+ * Sets the latency for all mock responses
278
+ */
279
+ simulateLatency(ms) {
280
+ latency = ms;
281
+ },
282
+ /**
283
+ * Causes a specific method to timeout
284
+ */
285
+ simulateTimeout(method) {
286
+ timeoutMethods.add(method);
287
+ },
288
+ /**
289
+ * Enables network error simulation for all calls
290
+ */
291
+ simulateNetworkError() {
292
+ networkErrorEnabled = true;
293
+ },
294
+ /**
295
+ * Sets up rate limiting after N calls
296
+ */
297
+ simulateRateLimit(maxCalls) {
298
+ rateLimitMax = maxCalls;
299
+ rateLimitCalls = 0;
300
+ },
301
+ /**
302
+ * Resets all simulation settings
303
+ */
304
+ resetSimulation() {
305
+ latency = 0;
306
+ networkErrorEnabled = false;
307
+ rateLimitMax = null;
308
+ rateLimitCalls = 0;
309
+ timeoutMethods.clear();
310
+ },
311
+ /**
312
+ * Gets the current latency setting
313
+ */
314
+ getLatency() {
315
+ return latency;
316
+ },
317
+ /**
318
+ * Checks if network error is enabled
319
+ */
320
+ isNetworkErrorEnabled() {
321
+ return networkErrorEnabled;
322
+ },
323
+ /**
324
+ * Checks if a method is set to timeout
325
+ */
326
+ isMethodTimedOut(method) {
327
+ return timeoutMethods.has(method);
328
+ },
329
+ /**
330
+ * Gets remaining rate limit calls (null if no limit)
331
+ */
332
+ getRateLimitRemaining() {
333
+ if (rateLimitMax === null) return null;
334
+ return Math.max(0, rateLimitMax - rateLimitCalls);
335
+ },
336
+ /**
337
+ * Increments the rate limit call counter
338
+ */
339
+ incrementRateLimitCalls() {
340
+ rateLimitCalls++;
341
+ }
342
+ };
343
+ }
344
+ async function applySimulation(simulator, method, fn) {
345
+ if (simulator.isNetworkErrorEnabled()) {
346
+ throw new Error("Network error: Connection failed");
347
+ }
348
+ if (simulator.isMethodTimedOut(method)) {
349
+ throw new Error(`Timeout: Method "${method}" timed out`);
350
+ }
351
+ const remaining = simulator.getRateLimitRemaining();
352
+ if (remaining !== null && remaining <= 0) {
353
+ throw new Error("Rate limited: Too many requests");
354
+ }
355
+ if (remaining !== null) {
356
+ simulator.incrementRateLimitCalls();
357
+ }
358
+ const latency = simulator.getLatency();
359
+ if (latency > 0) {
360
+ await new Promise((resolve) => setTimeout(resolve, latency));
361
+ }
362
+ return fn();
363
+ }
364
+
365
+ // src/mock-client.ts
366
+ function createMockClient(deps) {
367
+ const { tracker, simulator, faker } = deps;
368
+ const mocks = /* @__PURE__ */ new Map();
369
+ const onceMocks = /* @__PURE__ */ new Map();
370
+ async function getResponse(method, defaultFn) {
371
+ const onceMock = onceMocks.get(method);
372
+ if (onceMock) {
373
+ onceMocks.delete(method);
374
+ if (onceMock.error) {
375
+ throw onceMock.error;
376
+ }
377
+ return onceMock.response;
378
+ }
379
+ const mock = mocks.get(method);
380
+ if (mock) {
381
+ if (mock.error) {
382
+ throw mock.error;
383
+ }
384
+ return mock.response;
385
+ }
386
+ return defaultFn();
387
+ }
388
+ async function wrapCall(method, args, defaultFn) {
389
+ try {
390
+ const result = await applySimulation(simulator, method, async () => {
391
+ return getResponse(method, defaultFn);
392
+ });
393
+ tracker.recordCall(method, args, result);
394
+ return result;
395
+ } catch (error) {
396
+ tracker.recordCall(method, args, void 0, error);
397
+ throw error;
398
+ }
399
+ }
400
+ return {
401
+ /**
402
+ * Configures a persistent mock response for a method
403
+ */
404
+ mockResponse(method, response) {
405
+ mocks.set(method, { response });
406
+ },
407
+ /**
408
+ * Configures a mock error for a method
409
+ */
410
+ mockError(method, error) {
411
+ mocks.set(method, { error });
412
+ },
413
+ /**
414
+ * Configures a one-time mock response for a method
415
+ */
416
+ mockResponseOnce(method, response) {
417
+ onceMocks.set(method, { response });
418
+ },
419
+ /**
420
+ * Clears all mock configurations
421
+ */
422
+ clearMocks() {
423
+ mocks.clear();
424
+ onceMocks.clear();
425
+ },
426
+ /**
427
+ * Gets account balance (mocked)
428
+ */
429
+ async getAccountBalance(address) {
430
+ return wrapCall(
431
+ "getAccountBalance",
432
+ [address],
433
+ () => faker.fakeBalance()
434
+ );
435
+ },
436
+ /**
437
+ * Gets account resources (mocked)
438
+ */
439
+ async getAccountResources(address) {
440
+ return wrapCall("getAccountResources", [address], () => [
441
+ faker.fakeResource("0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>")
442
+ ]);
443
+ },
444
+ /**
445
+ * Gets transaction by hash (mocked)
446
+ */
447
+ async getTransaction(hash) {
448
+ return wrapCall("getTransaction", [hash], () => {
449
+ const tx = faker.fakeTransaction();
450
+ return { ...tx, hash };
451
+ });
452
+ },
453
+ /**
454
+ * Waits for transaction confirmation (mocked)
455
+ */
456
+ async waitForTransaction(hash) {
457
+ return wrapCall("waitForTransaction", [hash], () => {
458
+ const response = faker.fakeTransactionResponse(true);
459
+ return { ...response, hash };
460
+ });
461
+ }
462
+ };
463
+ }
464
+
465
+ // src/harness.ts
466
+ function createTestHarness(config) {
467
+ const seed = config?.seed ?? Date.now();
468
+ const defaultLatency = config?.defaultLatency ?? 0;
469
+ const faker = createFaker({ seed });
470
+ const tracker = createCallTracker();
471
+ const simulator = createNetworkSimulator();
472
+ if (defaultLatency > 0) {
473
+ simulator.simulateLatency(defaultLatency);
474
+ }
475
+ const client = createMockClient({ tracker, simulator, faker });
476
+ return {
477
+ client,
478
+ tracker,
479
+ simulator,
480
+ faker,
481
+ /**
482
+ * Cleans up all state - call after each test
483
+ */
484
+ cleanup() {
485
+ client.clearMocks();
486
+ tracker.clearCalls();
487
+ simulator.resetSimulation();
488
+ },
489
+ /**
490
+ * Resets to initial state while keeping configuration
491
+ */
492
+ reset() {
493
+ client.clearMocks();
494
+ tracker.clearCalls();
495
+ }
496
+ };
497
+ }
498
+
499
+ // src/validators/address.ts
500
+ import { MovementError as MovementError2 } from "@movebridge/core";
501
+ var ADDRESS_REGEX = /^(0[xX])?[a-fA-F0-9]{1,64}$/;
502
+ function isValidAddress(address) {
503
+ if (typeof address !== "string") {
504
+ return false;
505
+ }
506
+ return ADDRESS_REGEX.test(address);
507
+ }
508
+ function validateAddress(address) {
509
+ const result = getAddressValidationDetails(address);
510
+ if (!result.valid) {
511
+ throw new MovementError2(
512
+ `Invalid address: ${address} - ${result.error}`,
513
+ "INVALID_ADDRESS",
514
+ { address, reason: result.error }
515
+ );
516
+ }
517
+ }
518
+ function normalizeAddress(address) {
519
+ validateAddress(address);
520
+ const withoutPrefix = address.startsWith("0x") || address.startsWith("0X") ? address.slice(2) : address;
521
+ const padded = withoutPrefix.padStart(64, "0");
522
+ return `0x${padded.toLowerCase()}`;
523
+ }
524
+ function getAddressValidationDetails(address) {
525
+ if (typeof address !== "string") {
526
+ return {
527
+ valid: false,
528
+ error: "Address must be a string"
529
+ };
530
+ }
531
+ if (address.length === 0) {
532
+ return {
533
+ valid: false,
534
+ error: "Address cannot be empty"
535
+ };
536
+ }
537
+ const hasPrefix = address.startsWith("0x") || address.startsWith("0X");
538
+ const hexPart = hasPrefix ? address.slice(2) : address;
539
+ if (hexPart.length === 0) {
540
+ return {
541
+ valid: false,
542
+ error: "Address must contain hex characters after 0x prefix"
543
+ };
544
+ }
545
+ if (hexPart.length > 64) {
546
+ return {
547
+ valid: false,
548
+ error: `Address too long: ${hexPart.length} hex characters (max 64)`
549
+ };
550
+ }
551
+ if (!/^[a-fA-F0-9]+$/.test(hexPart)) {
552
+ return {
553
+ valid: false,
554
+ error: "Address contains invalid characters (must be hex: 0-9, a-f, A-F)"
555
+ };
556
+ }
557
+ const normalized = `0x${hexPart.padStart(64, "0").toLowerCase()}`;
558
+ return {
559
+ valid: true,
560
+ normalized
561
+ };
562
+ }
563
+
564
+ // src/validators/transaction.ts
565
+ import { MovementError as MovementError3 } from "@movebridge/core";
566
+ var FUNCTION_REGEX = /^0x[a-fA-F0-9]{1,64}::[a-zA-Z_][a-zA-Z0-9_]*::[a-zA-Z_][a-zA-Z0-9_]*$/;
567
+ function validateTransferPayload(payload) {
568
+ if (!isValidAddress(payload.to)) {
569
+ throw new MovementError3(
570
+ `Invalid recipient address: ${payload.to}`,
571
+ "INVALID_ADDRESS",
572
+ { address: payload.to, field: "to" }
573
+ );
574
+ }
575
+ validateAmount(payload.amount);
576
+ if (payload.coinType !== void 0) {
577
+ if (typeof payload.coinType !== "string" || payload.coinType.length === 0) {
578
+ throw new MovementError3(
579
+ "Invalid coin type: must be a non-empty string",
580
+ "INVALID_ARGUMENT",
581
+ { argument: "coinType", value: payload.coinType }
582
+ );
583
+ }
584
+ }
585
+ return true;
586
+ }
587
+ function validateEntryFunctionPayload(payload) {
588
+ if (!FUNCTION_REGEX.test(payload.function)) {
589
+ throw new MovementError3(
590
+ `Invalid function identifier: ${payload.function}`,
591
+ "INVALID_ARGUMENT",
592
+ {
593
+ argument: "function",
594
+ value: payload.function,
595
+ expectedFormat: "0xADDRESS::module::function"
596
+ }
597
+ );
598
+ }
599
+ if (!Array.isArray(payload.typeArguments)) {
600
+ throw new MovementError3(
601
+ "typeArguments must be an array",
602
+ "INVALID_ARGUMENT",
603
+ { argument: "typeArguments", value: payload.typeArguments }
604
+ );
605
+ }
606
+ for (let i = 0; i < payload.typeArguments.length; i++) {
607
+ if (typeof payload.typeArguments[i] !== "string") {
608
+ throw new MovementError3(
609
+ `typeArguments[${i}] must be a string`,
610
+ "INVALID_ARGUMENT",
611
+ { argument: `typeArguments[${i}]`, value: payload.typeArguments[i] }
612
+ );
613
+ }
614
+ }
615
+ if (!Array.isArray(payload.arguments)) {
616
+ throw new MovementError3(
617
+ "arguments must be an array",
618
+ "INVALID_ARGUMENT",
619
+ { argument: "arguments", value: payload.arguments }
620
+ );
621
+ }
622
+ return true;
623
+ }
624
+ function validatePayload(payload) {
625
+ if (!payload || typeof payload !== "object") {
626
+ throw new MovementError3(
627
+ "Payload must be an object",
628
+ "INVALID_ARGUMENT",
629
+ { argument: "payload", value: payload }
630
+ );
631
+ }
632
+ if (payload.type !== "entry_function_payload") {
633
+ throw new MovementError3(
634
+ `Unsupported payload type: ${payload.type}`,
635
+ "INVALID_ARGUMENT",
636
+ { argument: "type", value: payload.type, supported: ["entry_function_payload"] }
637
+ );
638
+ }
639
+ return validateEntryFunctionPayload({
640
+ function: payload.function,
641
+ typeArguments: payload.typeArguments,
642
+ arguments: payload.arguments
643
+ });
644
+ }
645
+ function validateAmount(amount) {
646
+ if (typeof amount !== "string") {
647
+ throw new MovementError3(
648
+ "Amount must be a string",
649
+ "INVALID_ARGUMENT",
650
+ { argument: "amount", value: amount }
651
+ );
652
+ }
653
+ const numValue = Number(amount);
654
+ if (isNaN(numValue)) {
655
+ throw new MovementError3(
656
+ `Invalid amount: "${amount}" is not a valid number`,
657
+ "INVALID_ARGUMENT",
658
+ { argument: "amount", value: amount, reason: "not a number" }
659
+ );
660
+ }
661
+ if (numValue < 0) {
662
+ throw new MovementError3(
663
+ `Invalid amount: "${amount}" cannot be negative`,
664
+ "INVALID_ARGUMENT",
665
+ { argument: "amount", value: amount, reason: "negative value" }
666
+ );
667
+ }
668
+ if (numValue === 0) {
669
+ throw new MovementError3(
670
+ `Invalid amount: "${amount}" cannot be zero`,
671
+ "INVALID_ARGUMENT",
672
+ { argument: "amount", value: amount, reason: "zero value" }
673
+ );
674
+ }
675
+ if (!Number.isInteger(numValue)) {
676
+ throw new MovementError3(
677
+ `Invalid amount: "${amount}" must be a whole number (no decimals)`,
678
+ "INVALID_ARGUMENT",
679
+ { argument: "amount", value: amount, reason: "contains decimals" }
680
+ );
681
+ }
682
+ }
683
+
684
+ // src/validators/schema.ts
685
+ import {
686
+ object,
687
+ string,
688
+ boolean,
689
+ array,
690
+ record,
691
+ unknown,
692
+ literal,
693
+ nullable,
694
+ validate
695
+ } from "superstruct";
696
+ import { MovementError as MovementError4 } from "@movebridge/core";
697
+ var PREDEFINED_SCHEMAS = {
698
+ Resource: object({
699
+ type: string(),
700
+ data: record(string(), unknown())
701
+ }),
702
+ Transaction: object({
703
+ hash: string(),
704
+ sender: string(),
705
+ sequenceNumber: string(),
706
+ payload: object({
707
+ type: literal("entry_function_payload"),
708
+ function: string(),
709
+ typeArguments: array(string()),
710
+ arguments: array(unknown())
711
+ }),
712
+ timestamp: string()
713
+ }),
714
+ TransactionResponse: object({
715
+ hash: string(),
716
+ success: boolean(),
717
+ vmStatus: string(),
718
+ gasUsed: string(),
719
+ events: array(
720
+ object({
721
+ type: string(),
722
+ sequenceNumber: string(),
723
+ data: record(string(), unknown())
724
+ })
725
+ )
726
+ }),
727
+ WalletState: object({
728
+ connected: boolean(),
729
+ address: nullable(string()),
730
+ publicKey: nullable(string())
731
+ }),
732
+ ContractEvent: object({
733
+ type: string(),
734
+ sequenceNumber: string(),
735
+ data: record(string(), unknown())
736
+ })
737
+ };
738
+ var customSchemas = /* @__PURE__ */ new Map();
739
+ function getSchema(name) {
740
+ if (name in PREDEFINED_SCHEMAS) {
741
+ return PREDEFINED_SCHEMAS[name];
742
+ }
743
+ const customSchema = customSchemas.get(name);
744
+ if (customSchema) {
745
+ return customSchema;
746
+ }
747
+ throw new MovementError4(
748
+ `Unknown schema: ${name}`,
749
+ "INVALID_ARGUMENT",
750
+ {
751
+ schemaName: name,
752
+ availableSchemas: [
753
+ ...Object.keys(PREDEFINED_SCHEMAS),
754
+ ...customSchemas.keys()
755
+ ]
756
+ }
757
+ );
758
+ }
759
+ function validateSchema(data, schemaName) {
760
+ const schema = getSchema(schemaName);
761
+ const [error] = validate(data, schema);
762
+ return error === void 0;
763
+ }
764
+ function getValidationErrors(data, schemaName) {
765
+ const schema = getSchema(schemaName);
766
+ const [error] = validate(data, schema);
767
+ if (!error) {
768
+ return [];
769
+ }
770
+ return convertStructError(error);
771
+ }
772
+ function registerSchema(name, schema) {
773
+ if (name in PREDEFINED_SCHEMAS) {
774
+ throw new MovementError4(
775
+ `Cannot override predefined schema: ${name}`,
776
+ "INVALID_ARGUMENT",
777
+ { schemaName: name }
778
+ );
779
+ }
780
+ customSchemas.set(name, schema);
781
+ }
782
+ function hasSchema(name) {
783
+ return name in PREDEFINED_SCHEMAS || customSchemas.has(name);
784
+ }
785
+ function convertStructError(error) {
786
+ const errors = [];
787
+ for (const failure of error.failures()) {
788
+ errors.push({
789
+ path: failure.path.join(".") || "(root)",
790
+ expected: failure.type,
791
+ received: getTypeOf(failure.value),
792
+ message: failure.message
793
+ });
794
+ }
795
+ return errors;
796
+ }
797
+ function getTypeOf(value) {
798
+ if (value === null) return "null";
799
+ if (value === void 0) return "undefined";
800
+ if (Array.isArray(value)) return "array";
801
+ return typeof value;
802
+ }
803
+
804
+ // src/snapshots.ts
805
+ function createSnapshotUtils() {
806
+ const snapshots = /* @__PURE__ */ new Map();
807
+ function serialize(data) {
808
+ return JSON.stringify(data, null, 2);
809
+ }
810
+ function generateDiff(expected, actual) {
811
+ const expectedLines = expected.split("\n");
812
+ const actualLines = actual.split("\n");
813
+ const diff = [];
814
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
815
+ for (let i = 0; i < maxLines; i++) {
816
+ const expectedLine = expectedLines[i];
817
+ const actualLine = actualLines[i];
818
+ if (expectedLine === actualLine) {
819
+ diff.push(` ${expectedLine ?? ""}`);
820
+ } else {
821
+ if (expectedLine !== void 0) {
822
+ diff.push(`- ${expectedLine}`);
823
+ }
824
+ if (actualLine !== void 0) {
825
+ diff.push(`+ ${actualLine}`);
826
+ }
827
+ }
828
+ }
829
+ return diff.join("\n");
830
+ }
831
+ return {
832
+ /**
833
+ * Creates a new snapshot
834
+ */
835
+ createSnapshot(data, name) {
836
+ snapshots.set(name, serialize(data));
837
+ },
838
+ /**
839
+ * Matches data against an existing snapshot
840
+ * Creates a new snapshot if one doesn't exist
841
+ */
842
+ matchSnapshot(data, name) {
843
+ const serialized = serialize(data);
844
+ const existing = snapshots.get(name);
845
+ if (existing === void 0) {
846
+ snapshots.set(name, serialized);
847
+ return { match: true };
848
+ }
849
+ if (existing === serialized) {
850
+ return { match: true };
851
+ }
852
+ return {
853
+ match: false,
854
+ diff: generateDiff(existing, serialized)
855
+ };
856
+ },
857
+ /**
858
+ * Updates an existing snapshot with new data
859
+ */
860
+ updateSnapshot(data, name) {
861
+ snapshots.set(name, serialize(data));
862
+ },
863
+ /**
864
+ * Deletes a snapshot
865
+ */
866
+ deleteSnapshot(name) {
867
+ snapshots.delete(name);
868
+ },
869
+ /**
870
+ * Lists all snapshot names
871
+ */
872
+ listSnapshots() {
873
+ return Array.from(snapshots.keys());
874
+ }
875
+ };
876
+ }
877
+
878
+ // src/integration.ts
879
+ import { MovementError as MovementError5 } from "@movebridge/core";
880
+ function createIntegrationUtils(network) {
881
+ if (network !== "testnet") {
882
+ throw new MovementError5(
883
+ "Integration tests can only be run on testnet to prevent accidental mainnet usage",
884
+ "INVALID_ARGUMENT",
885
+ { network, allowed: ["testnet"] }
886
+ );
887
+ }
888
+ return {
889
+ /**
890
+ * Creates a new test account and funds it from the faucet
891
+ * Note: This requires network access and should only be used in integration tests
892
+ */
893
+ async createTestAccount() {
894
+ throw new MovementError5(
895
+ "createTestAccount requires network access. Use in integration tests only.",
896
+ "NETWORK_ERROR",
897
+ { reason: "Not implemented for unit tests" }
898
+ );
899
+ },
900
+ /**
901
+ * Waits for an account to be funded
902
+ */
903
+ async waitForFunding(address, timeout = 3e4) {
904
+ throw new MovementError5(
905
+ "waitForFunding requires network access. Use in integration tests only.",
906
+ "NETWORK_ERROR",
907
+ { address, timeout, reason: "Not implemented for unit tests" }
908
+ );
909
+ },
910
+ /**
911
+ * Cleans up a test account by transferring remaining balance
912
+ */
913
+ async cleanupTestAccount(account) {
914
+ throw new MovementError5(
915
+ "cleanupTestAccount requires network access. Use in integration tests only.",
916
+ "NETWORK_ERROR",
917
+ { address: account.address, reason: "Not implemented for unit tests" }
918
+ );
919
+ },
920
+ /**
921
+ * Executes a callback with a temporary test account
922
+ */
923
+ async withTestAccount(callback) {
924
+ const account = await this.createTestAccount();
925
+ try {
926
+ return await callback(account);
927
+ } finally {
928
+ await this.cleanupTestAccount(account);
929
+ }
930
+ }
931
+ };
932
+ }
933
+ export {
934
+ PREDEFINED_SCHEMAS,
935
+ createCallTracker,
936
+ createFaker,
937
+ createIntegrationUtils,
938
+ createMockClient,
939
+ createNetworkSimulator,
940
+ createSnapshotUtils,
941
+ createTestHarness,
942
+ getAddressValidationDetails,
943
+ getValidationErrors,
944
+ hasSchema,
945
+ isValidAddress,
946
+ normalizeAddress,
947
+ registerSchema,
948
+ validateAddress,
949
+ validateEntryFunctionPayload,
950
+ validatePayload,
951
+ validateSchema,
952
+ validateTransferPayload
953
+ };