From 45ea9c20539f0e6b0dfa2da679045079ee6945e9 Mon Sep 17 00:00:00 2001 From: broody Date: Tue, 14 Jan 2025 17:01:10 -1000 Subject: [PATCH] Encode slot chain id and add testing --- packages/controller/jest.config.ts | 13 ++ packages/controller/package.json | 4 + .../src/__tests__/parseChainId.test.ts | 60 ++++++ packages/controller/src/controller.ts | 25 +-- packages/controller/src/utils.ts | 28 +++ pnpm-lock.yaml | 178 ++++++++++++++++++ 6 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 packages/controller/jest.config.ts create mode 100644 packages/controller/src/__tests__/parseChainId.test.ts diff --git a/packages/controller/jest.config.ts b/packages/controller/jest.config.ts new file mode 100644 index 000000000..abcbccadc --- /dev/null +++ b/packages/controller/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, +}; + +export default config; \ No newline at end of file diff --git a/packages/controller/package.json b/packages/controller/package.json index ff79e5cfb..52c45ea05 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -10,6 +10,7 @@ "build": "pnpm build:deps", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", + "test": "jest", "version": "pnpm pkg get version" }, "exports": { @@ -42,7 +43,10 @@ }, "devDependencies": { "@cartridge/tsconfig": "workspace:*", + "@types/jest": "^29.5.14", "@types/node": "^20.6.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", "typescript": "^5.4.5" } } diff --git a/packages/controller/src/__tests__/parseChainId.test.ts b/packages/controller/src/__tests__/parseChainId.test.ts new file mode 100644 index 000000000..9b3a43abe --- /dev/null +++ b/packages/controller/src/__tests__/parseChainId.test.ts @@ -0,0 +1,60 @@ +import { constants, shortString } from "starknet"; +import { parseChainId } from "../utils"; + +describe("parseChainId", () => { + describe("Starknet chains", () => { + test("identifies mainnet", () => { + expect( + parseChainId(new URL("https://api.cartridge.gg/x/starknet/mainnet")), + ).toBe(constants.StarknetChainId.SN_MAIN); + }); + + test("identifies sepolia", () => { + expect( + parseChainId(new URL("https://api.cartridge.gg/x/starknet/sepolia")), + ).toBe(constants.StarknetChainId.SN_SEPOLIA); + }); + }); + + describe("Project-specific chains", () => { + test("identifies slot chain", () => { + expect( + parseChainId(new URL("https://api.cartridge.gg/x/slot/katana")), + ).toBe(shortString.encodeShortString("WP_SLOT")); + }); + + test("identifies slot chain on localhost", () => { + expect(parseChainId(new URL("http://localhost:8001/x/slot/katana"))).toBe( + shortString.encodeShortString("WP_SLOT"), + ); + }); + + test("identifies slot chain with hyphenated name", () => { + expect( + parseChainId( + new URL("https://api.cartridge.gg/x/my-slot-chain/katana"), + ), + ).toBe(shortString.encodeShortString("WP_MY_SLOT_CHAIN")); + }); + + test("identifies slot mainnet chain", () => { + expect( + parseChainId(new URL("https://api.cartridge.gg/x/slot/mainnet")), + ).toBe(shortString.encodeShortString("GG_SLOT")); + }); + }); + + describe("Error cases", () => { + test("throws error for unsupported URL format", () => { + expect(() => + parseChainId(new URL("https://api.example.com/unsupported")), + ).toThrow("Chain https://api.example.com/unsupported not supported"); + }); + + test("throws error for URLs without proper chain identifiers", () => { + expect(() => + parseChainId(new URL("https://api.example.com/v1/starknet")), + ).toThrow("Chain https://api.example.com/v1/starknet not supported"); + }); + }); +}); diff --git a/packages/controller/src/controller.ts b/packages/controller/src/controller.ts index e162aea54..a4ce8f131 100644 --- a/packages/controller/src/controller.ts +++ b/packages/controller/src/controller.ts @@ -16,9 +16,10 @@ import { Chain, } from "./types"; import BaseProvider from "./provider"; -import { constants, WalletAccount } from "starknet"; +import { WalletAccount } from "starknet"; import { Policy } from "@cartridge/presets"; import { AddStarknetChainParameters, ChainId } from "@starknet-io/types-js"; +import { parseChainId } from "./utils"; export default class ControllerProvider extends BaseProvider { private keychain?: AsyncMethodReturns; @@ -34,28 +35,8 @@ export default class ControllerProvider extends BaseProvider { const chains = new Map(); for (const chain of options.chains) { - let chainId: ChainId | undefined; const url = new URL(chain.rpcUrl); - const parts = url.pathname.split("/"); - if (parts.includes("starknet")) { - if (parts.includes("mainnet")) { - chainId = constants.StarknetChainId.SN_MAIN; - } else if (parts.includes("sepolia")) { - chainId = constants.StarknetChainId.SN_SEPOLIA; - } - } else if (parts.length >= 3) { - const projectName = parts[2]; - if (parts.includes("katana")) { - chainId = `WP_${projectName.toUpperCase()}` as ChainId; - } else if (parts.includes("mainnet")) { - chainId = `GG_${projectName.toUpperCase()}` as ChainId; - } - } - - if (!chainId) { - throw new Error(`Chain ${chain.rpcUrl} not supported`); - } - + const chainId = parseChainId(url); chains.set(chainId, chain); } diff --git a/packages/controller/src/utils.ts b/packages/controller/src/utils.ts index c12aa8a21..d50b4b19e 100644 --- a/packages/controller/src/utils.ts +++ b/packages/controller/src/utils.ts @@ -2,13 +2,16 @@ import { addAddressPadding, Call, CallData, + constants, getChecksumAddress, hash, + shortString, typedData, TypedDataRevision, } from "starknet"; import wasm from "@cartridge/account-wasm/controller"; import { Policies, SessionPolicies } from "@cartridge/presets"; +import { ChainId } from "@starknet-io/types-js"; // Whitelist of allowed property names to prevent prototype pollution const ALLOWED_PROPERTIES = new Set([ @@ -129,3 +132,28 @@ export function humanizeString(str: string): string { .replace(/^\w/, (c) => c.toUpperCase()) ); } + +export function parseChainId(url: URL): ChainId { + const parts = url.pathname.split("/"); + + if (parts.includes("starknet")) { + if (parts.includes("mainnet")) { + return constants.StarknetChainId.SN_MAIN; + } else if (parts.includes("sepolia")) { + return constants.StarknetChainId.SN_SEPOLIA; + } + } else if (parts.length >= 3) { + const projectName = parts[2]; + if (parts.includes("katana")) { + return shortString.encodeShortString( + `WP_${projectName.toUpperCase().replace(/-/g, "_")}`, + ) as ChainId; + } else if (parts.includes("mainnet")) { + return shortString.encodeShortString( + `GG_${projectName.toUpperCase().replace(/-/g, "_")}`, + ) as ChainId; + } + } + + throw new Error(`Chain ${url.toString()} not supported`); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3bbf33d9..1847e1c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,9 +241,18 @@ importers: '@cartridge/tsconfig': specifier: workspace:* version: link:../tsconfig + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^20.6.0 version: 20.16.11 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + ts-jest: + specifier: ^29.2.5 + version: 29.2.5(@babel/core@7.25.8)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.8))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)))(typescript@5.5.4) typescript: specifier: ^5.4.5 version: 5.5.4 @@ -6186,6 +6195,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -9055,6 +9068,9 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -11296,6 +11312,30 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.2.5: + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + ts-log@2.2.7: resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} @@ -14581,6 +14621,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.16.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -19868,6 +19943,10 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.0) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -20366,6 +20445,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@22.10.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 @@ -22874,6 +22968,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@22.10.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)) @@ -22955,6 +23068,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)): + dependencies: + '@babel/core': 7.25.8 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.25.8) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.16.11 + ts-node: 10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.10.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)): dependencies: '@babel/core': 7.25.8 @@ -23320,6 +23464,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.10.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@22.10.1)(typescript@5.5.4)) @@ -23611,6 +23767,8 @@ snapshots: lodash.isequal@4.5.0: {} + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -26041,6 +26199,26 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.2.5(@babel/core@7.25.8)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.8))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)))(typescript@5.5.4): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.16.11)(ts-node@10.9.2(@swc/core@1.7.35(@swc/helpers@0.5.5))(@types/node@20.16.11)(typescript@5.5.4)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.5.4 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.25.8 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.25.8) + esbuild: 0.23.1 + ts-log@2.2.7: {} ts-mixer@6.0.4: {}