diff --git a/bun.lockb b/bun.lockb index 887a7a2..9f2f726 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f5e32b7..78bf995 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,20 @@ { "name": "mina-js", - "version": "0.0.1", + "private": true, "scripts": { - "build": "tsup", - "test": "bun test", + "build": "bun run --filter '*' build", + "test": "bun run --filter '*' test", "lint": "bunx biome check .", "format": "bunx biome check . --write", "format:unsafe": "bunx biome check . --write --unsafe" }, "devDependencies": { "@biomejs/biome": "1.8.3", + "@happy-dom/global-registrator": "^14.12.3", "@tsconfig/bun": "1.0.7", "@types/bun": "1.1.6", - "tsup": "8.2.3" + "tsup": "8.2.3", + "typescript": "5.5.4" }, - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0", - "mina-signer": "3.0.7", - "zod": "3.23.8" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "exports": { - "./accounts": { - "types": "./dist/accounts/index.d.ts", - "import": "./dist/accounts/index.mjs", - "default": "./dist/accounts/index.js" - }, - "./providers": { - "types": "./dist/providers/index.d.ts", - "import": "./dist/providers/index.mjs", - "default": "./dist/providers/index.js" - } - } + "workspaces": ["packages/*"] } diff --git a/packages/accounts/package.json b/packages/accounts/package.json new file mode 100644 index 0000000..b09c180 --- /dev/null +++ b/packages/accounts/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mina-js/accounts", + "version": "0.0.1", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "bun test" + }, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0", + "mina-signer": "3.0.7" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/src/accounts/generate-mnemonic.spec.ts b/packages/accounts/src/generate-mnemonic.spec.ts similarity index 100% rename from src/accounts/generate-mnemonic.spec.ts rename to packages/accounts/src/generate-mnemonic.spec.ts diff --git a/src/accounts/generate-mnemonic.ts b/packages/accounts/src/generate-mnemonic.ts similarity index 100% rename from src/accounts/generate-mnemonic.ts rename to packages/accounts/src/generate-mnemonic.ts diff --git a/src/accounts/generate-private-key.spec.ts b/packages/accounts/src/generate-private-key.spec.ts similarity index 100% rename from src/accounts/generate-private-key.spec.ts rename to packages/accounts/src/generate-private-key.spec.ts diff --git a/src/accounts/generate-private-key.ts b/packages/accounts/src/generate-private-key.ts similarity index 100% rename from src/accounts/generate-private-key.ts rename to packages/accounts/src/generate-private-key.ts diff --git a/src/accounts/index.ts b/packages/accounts/src/index.ts similarity index 100% rename from src/accounts/index.ts rename to packages/accounts/src/index.ts diff --git a/packages/accounts/tsup.config.ts b/packages/accounts/tsup.config.ts new file mode 100644 index 0000000..6e9381b --- /dev/null +++ b/packages/accounts/tsup.config.ts @@ -0,0 +1,3 @@ +import sharedConfig from "../shared/tsup.config"; + +export default sharedConfig; diff --git a/packages/connect/bunfig.toml b/packages/connect/bunfig.toml new file mode 100644 index 0000000..6b82abb --- /dev/null +++ b/packages/connect/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = "./happy-dom.ts" diff --git a/packages/connect/happy-dom.ts b/packages/connect/happy-dom.ts new file mode 100644 index 0000000..7f712d0 --- /dev/null +++ b/packages/connect/happy-dom.ts @@ -0,0 +1,3 @@ +import { GlobalRegistrator } from "@happy-dom/global-registrator"; + +GlobalRegistrator.register(); diff --git a/packages/connect/package.json b/packages/connect/package.json new file mode 100644 index 0000000..56a0490 --- /dev/null +++ b/packages/connect/package.json @@ -0,0 +1,24 @@ +{ + "name": "@mina-js/connect", + "version": "0.0.1", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "bun test" + }, + "dependencies": { + "@mina-js/providers": "workspace:*", + "zod": "3.23.8" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/connect/src/__mocks__/provider.ts b/packages/connect/src/__mocks__/provider.ts new file mode 100644 index 0000000..ac54a6a --- /dev/null +++ b/packages/connect/src/__mocks__/provider.ts @@ -0,0 +1,16 @@ +import { mock } from "bun:test"; +import type { MinaProviderDetail } from "@mina-js/providers"; + +export const mockedProvider: MinaProviderDetail = { + info: { + name: "Pallad", + icon: "data:image/", + rdns: "co.pallad", + slug: "pallad", + }, + provider: { + request: mock(), + addListener: mock(), + removeListener: mock(), + }, +}; diff --git a/packages/connect/src/events.spec.ts b/packages/connect/src/events.spec.ts new file mode 100644 index 0000000..485f555 --- /dev/null +++ b/packages/connect/src/events.spec.ts @@ -0,0 +1,28 @@ +import { expect, it, mock } from "bun:test"; +import type { MinaAnnounceProviderEvent } from "@mina-js/providers"; +import { mockedProvider } from "./__mocks__/provider"; +import { announceProvider, requestProviders } from "./events"; + +type Resolver = (value: unknown) => void; + +it("announcec Mina Provider with window event", async () => { + const listener = + (resolve: Resolver) => + ({ detail }: MinaAnnounceProviderEvent) => { + expect(detail.info.slug).toEqual(mockedProvider.info.slug); + resolve(true); + }; + await new Promise((resolve) => { + window.addEventListener("mina:announceProvider", listener(resolve)); + announceProvider(mockedProvider); + window.removeEventListener("mina:announceProvider", listener(resolve)); + }); +}); + +it("requests Mina Provider with window event", () => { + const listener = mock(); + window.addEventListener("mina:requestProvider", listener); + requestProviders(() => {}); + expect(listener).toHaveBeenCalled(); + window.removeEventListener("mina:requestProvider", listener); +}); diff --git a/packages/connect/src/events.ts b/packages/connect/src/events.ts new file mode 100644 index 0000000..4de04ad --- /dev/null +++ b/packages/connect/src/events.ts @@ -0,0 +1,51 @@ +import type { + MinaAnnounceProviderEvent, + MinaProviderDetail, +} from "@mina-js/providers"; + +export type AnnounceProviderReturnType = () => void; + +/** + * Creates an event to announce a Mina provider and registers a handler to respond to subsequent requests. + * + * @param {MinaProviderDetail} detail - The details of the provider to announce. + * @returns {AnnounceProviderReturnType} A function to remove the event listener when it is no longer needed. + */ +export function announceProvider( + detail: MinaProviderDetail, +): AnnounceProviderReturnType { + const event: CustomEvent = new CustomEvent( + "mina:announceProvider", + { detail: Object.freeze(detail) }, + ); + window.dispatchEvent(event); + const handler = () => window.dispatchEvent(event); + window.addEventListener("mina:requestProvider", handler); + return () => window.removeEventListener("mina:requestProvider", handler); +} + +export type RequestProvidersParameters = ( + providerDetail: MinaProviderDetail, +) => void; +export type RequestProvidersReturnType = (() => void) | undefined; + +/** + * Requests providers to be announced. + * + * This function adds an event listener for "mina:announceProvider" events and + * dispatches a custom event named "mina:requestProvider" to trigger the announce. + * + * @param listener A callback function that will be called when a provider is announced. + */ +export function requestProviders( + listener: RequestProvidersParameters, +): RequestProvidersReturnType { + if (typeof window === "undefined") return; + const handler = (event: MinaAnnounceProviderEvent) => listener(event.detail); + + window.addEventListener("mina:announceProvider", handler); + + window.dispatchEvent(new CustomEvent("mina:requestProvider")); + + return () => window.removeEventListener("mina:announceProvider", handler); +} diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts new file mode 100644 index 0000000..96c500c --- /dev/null +++ b/packages/connect/src/index.ts @@ -0,0 +1,18 @@ +import type { + MinaAnnounceProviderEvent, + MinaProviderClient, + MinaRequestProviderEvent, +} from "@mina-js/providers"; + +declare global { + interface WindowEventMap { + "mina:announceProvider": MinaAnnounceProviderEvent; + "mina:requestProvider": MinaRequestProviderEvent; + } + interface Window { + mina?: MinaProviderClient | undefined; + } +} + +export * from "./store"; +export * from "./events"; diff --git a/packages/connect/src/store.ts b/packages/connect/src/store.ts new file mode 100644 index 0000000..1847c4e --- /dev/null +++ b/packages/connect/src/store.ts @@ -0,0 +1,96 @@ +import type { MinaProviderDetail } from "@mina-js/providers"; +import { requestProviders } from "./events"; + +export type Listener = (providerDetails: readonly MinaProviderDetail[]) => void; + +export type Store = { + /** + * Clears the store, including all provider details. + */ + clear(): void; + /** + * Destroys the store, including all provider details and listeners. + */ + destroy(): void; + /** + * Finds a provider detail by its slug. + */ + findProvider(args: { slug: string }): MinaProviderDetail | undefined; + /** + * Returns all provider details that have been emitted. + */ + getProviders(): readonly MinaProviderDetail[]; + /** + * Resets the store, and emits an event to request provider details. + */ + reset(): void; + /** + * Subscribes to emitted provider details. + */ + subscribe( + listener: Listener, + args?: { emitImmediately?: boolean | undefined } | undefined, + ): () => void; + + /** + * @internal + * Current state of listening listeners. + */ + _listeners(): Set; +}; + +export function createStore(): Store { + const listeners: Set = new Set(); + let providerDetails: readonly MinaProviderDetail[] = []; + + const request = () => + requestProviders((providerDetail) => { + if ( + providerDetails.some( + ({ info }) => info.slug === providerDetail.info.slug, + ) + ) + return; + + providerDetails = [...providerDetails, providerDetail]; + for (const listener of listeners) { + listener(providerDetails); + } + }); + let unwatch = request(); + + return { + _listeners() { + return listeners; + }, + clear() { + for (const listener of listeners) { + listener([]); + } + providerDetails = []; + }, + destroy() { + this.clear(); + listeners.clear(); + unwatch?.(); + }, + findProvider({ slug }) { + return providerDetails.find( + (providerDetail) => providerDetail.info.slug === slug, + ); + }, + getProviders() { + return providerDetails; + }, + reset() { + this.clear(); + unwatch?.(); + unwatch = request(); + }, + subscribe(listener, { emitImmediately } = {}) { + listeners.add(listener); + if (emitImmediately) listener(providerDetails); + return () => listeners.delete(listener); + }, + }; +} diff --git a/packages/connect/tsup.config.ts b/packages/connect/tsup.config.ts new file mode 100644 index 0000000..6e9381b --- /dev/null +++ b/packages/connect/tsup.config.ts @@ -0,0 +1,3 @@ +import sharedConfig from "../shared/tsup.config"; + +export default sharedConfig; diff --git a/packages/providers/package.json b/packages/providers/package.json new file mode 100644 index 0000000..9d8fa9b --- /dev/null +++ b/packages/providers/package.json @@ -0,0 +1,24 @@ +{ + "name": "@mina-js/providers", + "version": "0.0.1", + "type": "module", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "bun test" + }, + "dependencies": { + "zod": "3.23.8" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/src/providers/index.ts b/packages/providers/src/index.ts similarity index 100% rename from src/providers/index.ts rename to packages/providers/src/index.ts diff --git a/src/providers/types.ts b/packages/providers/src/types.ts similarity index 98% rename from src/providers/types.ts rename to packages/providers/src/types.ts index 567b7bb..68f5ccc 100644 --- a/src/providers/types.ts +++ b/packages/providers/src/types.ts @@ -30,7 +30,7 @@ export interface MinaAnnounceProviderEvent type: "mina:announceProvider"; } -export interface EIP6963RequestProviderEvent extends Event { +export interface MinaRequestProviderEvent extends Event { type: "mina:requestProvider"; } diff --git a/src/providers/validation.ts b/packages/providers/src/validation.ts similarity index 100% rename from src/providers/validation.ts rename to packages/providers/src/validation.ts diff --git a/packages/providers/tsup.config.ts b/packages/providers/tsup.config.ts new file mode 100644 index 0000000..6e9381b --- /dev/null +++ b/packages/providers/tsup.config.ts @@ -0,0 +1,3 @@ +import sharedConfig from "../shared/tsup.config"; + +export default sharedConfig; diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts new file mode 100644 index 0000000..3d230eb --- /dev/null +++ b/packages/shared/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "./dist", + format: ["esm", "cjs"], + sourcemap: true, + clean: true, + bundle: true, + dts: true, +}); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 6520f28..0000000 --- a/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "./accounts"; diff --git a/tsup.config.ts b/tsup.config.ts index 8ad86c4..91c8ce3 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/accounts/index.ts", "src/providers/index.ts"], outDir: "./dist", format: ["esm", "cjs"], sourcemap: true,