@lunatest/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/browser.d.ts +6 -0
  2. package/dist/browser.js +5 -0
  3. package/dist/config/lua-config.js +2 -43
  4. package/dist/config/read-source.d.ts +3 -0
  5. package/dist/config/read-source.js +56 -0
  6. package/dist/coverage/catalog.d.ts +14 -0
  7. package/dist/coverage/catalog.js +91 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.js +3 -0
  10. package/dist/mocks/provider.js +1 -3
  11. package/dist/presets/__tests__/registry.test.ts +213 -0
  12. package/dist/presets/loader.d.ts +6 -0
  13. package/dist/presets/loader.js +40 -0
  14. package/dist/presets/loader.ts +58 -0
  15. package/dist/presets/project-sources.node.d.ts +2 -0
  16. package/dist/presets/project-sources.node.js +44 -0
  17. package/dist/presets/project-sources.node.ts +57 -0
  18. package/dist/presets/protocol/aave.lua +82 -0
  19. package/dist/presets/protocol/curve.lua +82 -0
  20. package/dist/presets/protocol/uniswap_v2.lua +92 -0
  21. package/dist/presets/protocol/uniswap_v3.lua +144 -0
  22. package/dist/presets/registry.d.ts +59 -0
  23. package/dist/presets/registry.js +483 -0
  24. package/dist/presets/registry.ts +784 -0
  25. package/dist/presets/wallet/demo_sepolia.lua +66 -0
  26. package/dist/presets/wallet/empty_wallet.lua +54 -0
  27. package/dist/provider/luna-provider.d.ts +3 -0
  28. package/dist/provider/luna-provider.js +66 -5
  29. package/dist/runner/assert.js +23 -1
  30. package/dist/runtime/bridge.js +10 -2
  31. package/dist/runtime/engine.js +6 -2
  32. package/dist/runtime/scenario-runtime.d.ts +8 -0
  33. package/dist/runtime/scenario-runtime.js +9 -0
  34. package/dist/scenario/index.d.ts +7 -1
  35. package/dist/scenario/index.js +8 -0
  36. package/package.json +13 -3
@@ -0,0 +1,66 @@
1
+ return {
2
+ manifest = {
3
+ id = "demo_sepolia",
4
+ label = "Demo Sepolia Wallet",
5
+ description = "Seeded Luna Wallet for Sepolia demos.",
6
+ kind = "wallet",
7
+ supportedChains = { 11155111 },
8
+ defaultSession = {
9
+ enabled = false,
10
+ connected = false,
11
+ chainId = "0xaa36a7",
12
+ accounts = { "0x1111111111111111111111111111111111111111" },
13
+ permissions = {},
14
+ assets = {
15
+ nativeBalance = "1",
16
+ tokens = {},
17
+ },
18
+ },
19
+ paramsSchema = {
20
+ {
21
+ key = "address",
22
+ label = "Address",
23
+ type = "address",
24
+ required = true,
25
+ default = "0x1111111111111111111111111111111111111111",
26
+ },
27
+ {
28
+ key = "chainId",
29
+ label = "Chain",
30
+ type = "chainId",
31
+ required = true,
32
+ default = 11155111,
33
+ options = { 11155111 },
34
+ },
35
+ {
36
+ key = "nativeBalance",
37
+ label = "Native Balance",
38
+ type = "string",
39
+ default = "1",
40
+ },
41
+ },
42
+ recommendedControls = { "address", "nativeBalance" },
43
+ },
44
+
45
+ materialize = function(params)
46
+ local address = params.address or "0x1111111111111111111111111111111111111111"
47
+ local chainId = tonumber(params.chainId or 11155111)
48
+ local nativeBalance = tostring(params.nativeBalance or "1")
49
+
50
+ return {
51
+ resolvedParams = {
52
+ address = address,
53
+ chainId = chainId,
54
+ nativeBalance = nativeBalance,
55
+ },
56
+ defaultSession = {
57
+ chainId = string.format("0x%x", chainId),
58
+ accounts = { address },
59
+ assets = {
60
+ nativeBalance = nativeBalance,
61
+ tokens = {},
62
+ },
63
+ },
64
+ }
65
+ end,
66
+ }
@@ -0,0 +1,54 @@
1
+ return {
2
+ manifest = {
3
+ id = "empty_wallet",
4
+ label = "Empty Wallet",
5
+ description = "Disconnected or zero-state wallet baseline.",
6
+ kind = "wallet",
7
+ supportedChains = { 1, 11155111 },
8
+ defaultSession = {
9
+ enabled = false,
10
+ connected = false,
11
+ chainId = "0x1",
12
+ accounts = { "0x1111111111111111111111111111111111111111" },
13
+ permissions = {},
14
+ assets = {
15
+ nativeBalance = "0",
16
+ tokens = {},
17
+ },
18
+ },
19
+ paramsSchema = {
20
+ {
21
+ key = "address",
22
+ label = "Address",
23
+ type = "address",
24
+ required = true,
25
+ default = "0x1111111111111111111111111111111111111111",
26
+ },
27
+ {
28
+ key = "chainId",
29
+ label = "Chain",
30
+ type = "chainId",
31
+ required = true,
32
+ default = 1,
33
+ options = { 1, 11155111 },
34
+ },
35
+ },
36
+ recommendedControls = { "address", "chainId" },
37
+ },
38
+
39
+ materialize = function(params)
40
+ local address = params.address or "0x1111111111111111111111111111111111111111"
41
+ local chainId = tonumber(params.chainId or 1)
42
+
43
+ return {
44
+ resolvedParams = {
45
+ address = address,
46
+ chainId = chainId,
47
+ },
48
+ defaultSession = {
49
+ chainId = string.format("0x%x", chainId),
50
+ accounts = { address },
51
+ },
52
+ }
53
+ end,
54
+ }
@@ -1,3 +1,4 @@
1
+ import { type LunaWalletSession } from "@lunatest/contracts";
1
2
  export type Eip1193Request = {
2
3
  method: string;
3
4
  params?: unknown[];
@@ -6,6 +7,7 @@ export type LunaProviderOptions = {
6
7
  chainId?: string;
7
8
  accounts?: string[];
8
9
  balances?: Record<string, string>;
10
+ wallet?: Partial<LunaWalletSession>;
9
11
  callHandler?: (input: Record<string, unknown>) => Promise<string> | string;
10
12
  };
11
13
  type ProviderListener = (...args: unknown[]) => void;
@@ -13,6 +15,7 @@ export declare class LunaProvider {
13
15
  private chainId;
14
16
  private accounts;
15
17
  private balances;
18
+ private wallet;
16
19
  private callHandler?;
17
20
  private txCounter;
18
21
  private subCounter;
@@ -1,10 +1,9 @@
1
- function normalizeAddress(value) {
2
- return value.toLowerCase();
3
- }
1
+ import { createLunaWalletSession, extractPermissionKeys, normalizeAddress, normalizeWalletPermissions, } from "@lunatest/contracts";
4
2
  export class LunaProvider {
5
3
  chainId;
6
4
  accounts;
7
5
  balances;
6
+ wallet;
8
7
  callHandler;
9
8
  txCounter;
10
9
  subCounter;
@@ -17,6 +16,14 @@ export class LunaProvider {
17
16
  normalizeAddress(key),
18
17
  value,
19
18
  ]));
19
+ this.wallet = createLunaWalletSession({
20
+ chainId: options.wallet?.chainId ?? this.chainId,
21
+ accounts: options.wallet?.accounts ?? this.accounts,
22
+ connected: options.wallet?.connected ??
23
+ (options.wallet ? false : this.accounts.length > 0),
24
+ enabled: options.wallet?.enabled ?? true,
25
+ permissions: options.wallet?.permissions,
26
+ });
20
27
  this.callHandler = options.callHandler;
21
28
  this.txCounter = 1n;
22
29
  this.subCounter = 1;
@@ -52,10 +59,35 @@ export class LunaProvider {
52
59
  async request(payload) {
53
60
  const method = payload.method;
54
61
  if (method === "eth_chainId") {
55
- return this.chainId;
62
+ return this.wallet.enabled ? this.wallet.chainId : this.chainId;
56
63
  }
57
64
  if (method === "eth_accounts") {
58
- return this.accounts;
65
+ if (!this.wallet.enabled) {
66
+ return this.accounts;
67
+ }
68
+ const hasAccountsPermission = this.wallet.permissions.some((permission) => permission.parentCapability === "eth_accounts");
69
+ if (!this.wallet.connected || !hasAccountsPermission) {
70
+ return [];
71
+ }
72
+ return [...this.wallet.accounts];
73
+ }
74
+ if (method === "eth_requestAccounts") {
75
+ const previousAccounts = this.wallet.connected
76
+ ? [...this.wallet.accounts]
77
+ : [];
78
+ this.wallet.connected = true;
79
+ this.wallet.permissions = normalizeWalletPermissions([
80
+ ...this.wallet.permissions,
81
+ "eth_accounts",
82
+ ]);
83
+ this.accounts = [...this.wallet.accounts];
84
+ this.emit("accountsChanged", [...this.wallet.accounts]);
85
+ if (previousAccounts.length === 0 && this.wallet.accounts.length > 0) {
86
+ this.emit("connect", {
87
+ chainId: this.wallet.chainId,
88
+ });
89
+ }
90
+ return [...this.wallet.accounts];
59
91
  }
60
92
  if (method === "eth_getBalance") {
61
93
  const [address] = payload.params ?? [];
@@ -104,9 +136,38 @@ export class LunaProvider {
104
136
  throw new Error("wallet_switchEthereumChain requires chainId");
105
137
  }
106
138
  this.chainId = chainId;
139
+ this.wallet.chainId = chainId;
107
140
  this.emit("chainChanged", chainId);
108
141
  return null;
109
142
  }
143
+ if (method === "wallet_requestPermissions") {
144
+ const keys = extractPermissionKeys(payload.params);
145
+ this.wallet.permissions = normalizeWalletPermissions([
146
+ ...this.wallet.permissions,
147
+ ...keys,
148
+ ]);
149
+ if (keys.includes("eth_accounts")) {
150
+ this.wallet.connected = true;
151
+ this.accounts = [...this.wallet.accounts];
152
+ this.emit("accountsChanged", [...this.wallet.accounts]);
153
+ }
154
+ return [...this.wallet.permissions];
155
+ }
156
+ if (method === "wallet_getPermissions") {
157
+ return [...this.wallet.permissions];
158
+ }
159
+ if (method === "wallet_revokePermissions") {
160
+ const keys = new Set(extractPermissionKeys(payload.params));
161
+ const revoked = this.wallet.permissions.filter((permission) => keys.has(permission.parentCapability));
162
+ this.wallet.permissions = this.wallet.permissions.filter((permission) => !keys.has(permission.parentCapability));
163
+ if (keys.has("eth_accounts")) {
164
+ this.wallet.connected = false;
165
+ this.emit("accountsChanged", []);
166
+ }
167
+ return revoked.map((permission) => ({
168
+ parentCapability: permission.parentCapability,
169
+ }));
170
+ }
110
171
  if (method === "eth_subscribe") {
111
172
  const id = `0xsub${this.subCounter.toString(16).padStart(4, "0")}`;
112
173
  this.subCounter += 1;
@@ -2,7 +2,29 @@ function format(value) {
2
2
  return JSON.stringify(value, null, 2);
3
3
  }
4
4
  function deepEqual(left, right) {
5
- return JSON.stringify(left) === JSON.stringify(right);
5
+ if (Object.is(left, right)) {
6
+ return true;
7
+ }
8
+ if (Array.isArray(left) || Array.isArray(right)) {
9
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
10
+ return false;
11
+ }
12
+ return left.every((item, index) => deepEqual(item, right[index]));
13
+ }
14
+ if (typeof left === "object" &&
15
+ left !== null &&
16
+ typeof right === "object" &&
17
+ right !== null) {
18
+ const leftRecord = left;
19
+ const rightRecord = right;
20
+ const leftKeys = Object.keys(leftRecord).sort();
21
+ const rightKeys = Object.keys(rightRecord).sort();
22
+ if (!deepEqual(leftKeys, rightKeys)) {
23
+ return false;
24
+ }
25
+ return leftKeys.every((key) => deepEqual(leftRecord[key], rightRecord[key]));
26
+ }
27
+ return false;
6
28
  }
7
29
  function buildResult(expected, actual) {
8
30
  const pass = deepEqual(expected, actual);
@@ -14,6 +14,9 @@ function isPlainObject(value) {
14
14
  Object.getPrototypeOf(value) === Object.prototype);
15
15
  }
16
16
  function cloneSupported(value) {
17
+ if (value === undefined) {
18
+ return undefined;
19
+ }
17
20
  if (value === null ||
18
21
  typeof value === "string" ||
19
22
  typeof value === "number" ||
@@ -21,13 +24,18 @@ function cloneSupported(value) {
21
24
  return value;
22
25
  }
23
26
  if (Array.isArray(value)) {
24
- return value.map((item) => cloneSupported(item));
27
+ return value.map((item) => {
28
+ if (item === undefined) {
29
+ throw new Error("Unsupported value type in array: undefined");
30
+ }
31
+ return cloneSupported(item);
32
+ });
25
33
  }
26
34
  if (isPlainObject(value)) {
27
35
  const cloned = {};
28
36
  for (const [key, nested] of Object.entries(value)) {
29
37
  if (nested === undefined) {
30
- throw new Error(`Unsupported value type at ${key}: undefined`);
38
+ continue;
31
39
  }
32
40
  Object.defineProperty(cloned, key, {
33
41
  value: cloneSupported(nested),
@@ -54,8 +54,9 @@ export async function createRuntime(rawOptions = {}) {
54
54
  throw new Error(`Function not found: ${targetName}`);
55
55
  }
56
56
  const normalizedArgs = toLuaArgs(args);
57
- const orderedArgs = Object.values(normalizedArgs);
58
- const result = await Promise.resolve(target(...orderedArgs));
57
+ const result = isPlainObject(normalizedArgs) && Object.keys(normalizedArgs).length === 0
58
+ ? await Promise.resolve(target())
59
+ : await Promise.resolve(target(normalizedArgs));
59
60
  return fromLuaValue(result);
60
61
  },
61
62
  register(name, hostFn) {
@@ -83,3 +84,6 @@ export async function createRuntime(rawOptions = {}) {
83
84
  },
84
85
  };
85
86
  }
87
+ function isPlainObject(value) {
88
+ return typeof value === "object" && value !== null && !Array.isArray(value);
89
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import type { CoverageMetadata } from "@lunatest/contracts";
2
3
  import { type RouteMock } from "@lunatest/contracts";
3
4
  export type { RouteMock } from "@lunatest/contracts";
4
5
  export declare const LuaConfigSchema: z.ZodObject<{
@@ -11,6 +12,12 @@ export declare const LuaConfigSchema: z.ZodObject<{
11
12
  when: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
12
13
  then_ui: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
13
14
  then_state: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
15
+ not_present: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
+ coverage: z.ZodOptional<z.ZodObject<{
17
+ features: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
18
+ states: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
19
+ components: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
20
+ }, z.core.$strip>>;
14
21
  intercept: z.ZodOptional<z.ZodObject<{
15
22
  routes: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
16
23
  endpointType: z.ZodLiteral<"ethereum">;
@@ -58,6 +65,7 @@ export declare const LuaConfigSchema: z.ZodObject<{
58
65
  }, z.core.$strip>>;
59
66
  }, z.core.$loose>;
60
67
  export type LuaConfig = z.infer<typeof LuaConfigSchema>;
68
+ export type { CoverageMetadata };
61
69
  export type ScenarioRuntime = {
62
70
  getConfig: () => LuaConfig;
63
71
  getRouteMocks: () => RouteMock[];
@@ -1,6 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { deepClone, deepMerge } from "@lunatest/contracts";
3
3
  const StringRecordSchema = z.record(z.string(), z.unknown());
4
+ const CoverageMetadataSchema = z
5
+ .object({
6
+ features: z.array(z.string().min(1)).optional(),
7
+ states: z.array(z.string().min(1)).optional(),
8
+ components: z.array(z.string().min(1)).optional(),
9
+ })
10
+ .partial();
4
11
  const EthereumRouteSchema = z.object({
5
12
  endpointType: z.literal("ethereum"),
6
13
  method: z.string().min(1),
@@ -69,6 +76,8 @@ export const LuaConfigSchema = z
69
76
  when: StringRecordSchema.optional(),
70
77
  then_ui: StringRecordSchema.optional(),
71
78
  then_state: StringRecordSchema.optional(),
79
+ not_present: z.array(z.string().min(1)).optional(),
80
+ coverage: CoverageMetadataSchema.optional(),
72
81
  intercept: z
73
82
  .object({
74
83
  routes: z.array(RouteMockSchema).optional(),
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import type { CoverageMetadata } from "@lunatest/contracts";
2
3
  declare const StageSchema: z.ZodObject<{
3
4
  name: z.ZodString;
4
5
  on: z.ZodOptional<z.ZodString>;
@@ -17,9 +18,15 @@ declare const ScenarioSchema: z.ZodObject<{
17
18
  }, z.core.$strip>>>;
18
19
  not_present: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
20
  timing_ms: z.ZodOptional<z.ZodNumber>;
21
+ coverage: z.ZodOptional<z.ZodObject<{
22
+ features: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
23
+ states: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
24
+ components: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
25
+ }, z.core.$strip>>;
20
26
  }, z.core.$loose>;
21
27
  export type ScenarioStage = z.infer<typeof StageSchema>;
22
28
  export type ParsedScenario = z.infer<typeof ScenarioSchema>;
29
+ export type { CoverageMetadata };
23
30
  export type StageMachine = {
24
31
  current: () => ScenarioStage;
25
32
  next: () => ScenarioStage;
@@ -40,4 +47,3 @@ export type VirtualClock = {
40
47
  export declare function parseScenario(input: unknown): ParsedScenario;
41
48
  export declare function createStageMachine(stages: ScenarioStage[]): StageMachine;
42
49
  export declare function createVirtualClock(initialMs?: number): VirtualClock;
43
- export {};
@@ -1,5 +1,12 @@
1
1
  import { z } from "zod";
2
2
  const RecordValueSchema = z.record(z.string(), z.unknown());
3
+ const CoverageMetadataSchema = z
4
+ .object({
5
+ features: z.array(z.string().min(1)).optional(),
6
+ states: z.array(z.string().min(1)).optional(),
7
+ components: z.array(z.string().min(1)).optional(),
8
+ })
9
+ .partial();
3
10
  const StageSchema = z.object({
4
11
  name: z.string().min(1),
5
12
  on: z.string().min(1).optional(),
@@ -18,6 +25,7 @@ const ScenarioSchema = z
18
25
  stages: z.array(StageSchema).optional(),
19
26
  not_present: z.array(z.string().min(1)).optional(),
20
27
  timing_ms: z.number().int().nonnegative().optional(),
28
+ coverage: CoverageMetadataSchema.optional(),
21
29
  })
22
30
  .passthrough();
23
31
  export function parseScenario(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunatest/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,11 @@
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
13
  },
14
+ "./browser": {
15
+ "types": "./dist/browser.d.ts",
16
+ "import": "./dist/browser.js",
17
+ "default": "./dist/browser.js"
18
+ },
14
19
  "./package.json": "./package.json"
15
20
  },
16
21
  "publishConfig": {
@@ -28,9 +33,14 @@
28
33
  "devDependencies": {
29
34
  "fast-check": "^4.5.3"
30
35
  },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/songforthemute/lunatest",
39
+ "directory": "packages/core"
40
+ },
31
41
  "scripts": {
32
- "build": "tsc -p tsconfig.json",
42
+ "build": "tsc -p tsconfig.json && node ./scripts/copy-preset-assets.mjs",
33
43
  "test": "vitest run",
34
- "lint": "tsc -p tsconfig.json --noEmit"
44
+ "lint": "tsc -p tsconfig.lint.json --pretty false"
35
45
  }
36
46
  }