diff --git a/.changeset/loose-lemons-smell.md b/.changeset/loose-lemons-smell.md new file mode 100644 index 00000000..13bcf7e9 --- /dev/null +++ b/.changeset/loose-lemons-smell.md @@ -0,0 +1,5 @@ +--- +"@reactive-dot/wallet-mimir": minor +--- + +Added Mimir wallet integration. diff --git a/apps/docs/react/getting-started/connect-wallets.mdx b/apps/docs/react/getting-started/connect-wallets.mdx index d872c56c..1e9abb93 100644 --- a/apps/docs/react/getting-started/connect-wallets.mdx +++ b/apps/docs/react/getting-started/connect-wallets.mdx @@ -29,19 +29,27 @@ npm install @reactive-dot/wallet-walletconnect npm install @reactive-dot/wallet-ledger ``` +### [Mimir](https://mimir.global/) + +```bash npm2yarn +npm install @reactive-dot/wallet-mimir +``` + ## Add wallets to the config ```ts title="config.ts" import { defineConfig } from "@reactive-dot/core"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; import { LedgerWallet } from "@reactive-dot/wallet-ledger"; +import { MimirWalletProvider } from "@reactive-dot/wallet-mimir"; import { WalletConnect } from "@reactive-dot/wallet-walletconnect"; export const config = defineConfig({ // ... wallets: [ new InjectedWalletProvider(), - new LedgerWalet(), + new LedgerWallet(), + new MimirWalletProvider(), new WalletConnect({ projectId: "WALLET_CONNECT_PROJECT_ID", providerOptions: { diff --git a/apps/docs/vue/getting-started/connect-wallets.mdx b/apps/docs/vue/getting-started/connect-wallets.mdx index d75e5eb4..03f4c799 100644 --- a/apps/docs/vue/getting-started/connect-wallets.mdx +++ b/apps/docs/vue/getting-started/connect-wallets.mdx @@ -29,19 +29,27 @@ npm install @reactive-dot/wallet-walletconnect npm install @reactive-dot/wallet-ledger ``` +### [Mimir](https://mimir.global/) + +```bash npm2yarn +npm install @reactive-dot/wallet-mimir +``` + ## Add wallets to the config ```ts title="config.ts" import { defineConfig } from "@reactive-dot/core"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; import { LedgerWallet } from "@reactive-dot/wallet-ledger"; +import { MimirWalletProvider } from "@reactive-dot/wallet-mimir"; import { WalletConnect } from "@reactive-dot/wallet-walletconnect"; export const config = defineConfig({ // ... wallets: [ new InjectedWalletProvider(), - new LedgerWalet(), + new LedgerWallet(), + new MimirWalletProvider(), new WalletConnect({ projectId: "WALLET_CONNECT_PROJECT_ID", providerOptions: { diff --git a/examples/react/package.json b/examples/react/package.json index 26f919b2..06bdeb0a 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -13,6 +13,7 @@ "@polkadot-api/descriptors": "portal:.papi/descriptors", "@reactive-dot/react": "workspace:^", "@reactive-dot/wallet-ledger": "workspace:^", + "@reactive-dot/wallet-mimir": "workspace:^", "@reactive-dot/wallet-walletconnect": "workspace:^", "date-fns": "^4.1.0", "jotai-devtools": "^0.11.0", diff --git a/examples/react/src/config.ts b/examples/react/src/config.ts index 9e1e3db3..1ec9266a 100644 --- a/examples/react/src/config.ts +++ b/examples/react/src/config.ts @@ -11,6 +11,7 @@ import { defineConfig } from "@reactive-dot/core"; import { createLightClientProvider } from "@reactive-dot/core/providers/light-client.js"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; import { LedgerWallet } from "@reactive-dot/wallet-ledger"; +import { MimirWalletProvider } from "@reactive-dot/wallet-mimir"; import { WalletConnect } from "@reactive-dot/wallet-walletconnect"; const lightClientProvider = createLightClientProvider(); @@ -55,6 +56,7 @@ export const config = defineConfig({ targetChains: ["polkadot", "kusama", "westend"], wallets: [ new InjectedWalletProvider({ originName: "ReactiveDOT React Example" }), + new MimirWalletProvider({ originName: "ReactiveDOT React Example" }), new LedgerWallet(), new WalletConnect({ projectId: "68f5b7e972a51cf379b127f51a791c34", diff --git a/nx.json b/nx.json index 7c228260..2a44a5da 100644 --- a/nx.json +++ b/nx.json @@ -14,5 +14,5 @@ "cache": true } }, - "parallel": 9 + "parallel": 10 } diff --git a/packages/core/src/wallets/injected/wallet.ts b/packages/core/src/wallets/injected/wallet.ts index 19106805..a8e3c751 100644 --- a/packages/core/src/wallets/injected/wallet.ts +++ b/packages/core/src/wallets/injected/wallet.ts @@ -6,7 +6,7 @@ import { type InjectedExtension, type InjectedPolkadotAccount, } from "polkadot-api/pjs-signer"; -import { BehaviorSubject, Observable } from "rxjs"; +import { BehaviorSubject, Observable, of } from "rxjs"; import { map, switchMap } from "rxjs/operators"; export type InjectedWalletOptions = WalletOptions & { originName?: string }; @@ -33,7 +33,7 @@ export class InjectedWallet extends Wallet { } } - connected$ = this.#extension$.pipe( + readonly connected$ = this.#extension$.pipe( map((extension) => extension !== undefined), ); @@ -53,21 +53,20 @@ export class InjectedWallet extends Wallet { } readonly accounts$ = this.#extension$.pipe( - switchMap( - (extension) => - new Observable((subscriber) => { - if (extension === undefined) { - subscriber.next([]); - } else { - subscriber.next(this.#withIds(extension.getAccounts())); - subscriber.add( - extension.subscribe((accounts) => - subscriber.next(this.#withIds(accounts)), - ), - ); - } - }), - ), + switchMap((extension) => { + if (extension === undefined) { + return of([]); + } + + return new Observable((subscriber) => { + subscriber.next(this.#withIds(extension.getAccounts())); + subscriber.add( + extension.subscribe((accounts) => + subscriber.next(this.#withIds(accounts)), + ), + ); + }); + }), ); override getAccounts() { diff --git a/packages/wallet-mimir/.gitignore b/packages/wallet-mimir/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/packages/wallet-mimir/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/wallet-mimir/CHANGELOG.md b/packages/wallet-mimir/CHANGELOG.md new file mode 100644 index 00000000..a5720e44 --- /dev/null +++ b/packages/wallet-mimir/CHANGELOG.md @@ -0,0 +1,174 @@ +# @reactive-dot/wallet-ledger + +## 0.16.25 + +### Patch Changes + +- Updated dependencies [[`b6c5cc7`](https://github.com/tien/reactive-dot/commit/b6c5cc7a9d4ba82b2d8c890cfcc569fe6703951f)]: + - @reactive-dot/core@0.33.0 + +## 0.16.24 + +### Patch Changes + +- [#495](https://github.com/tien/reactive-dot/pull/495) [`3372262`](https://github.com/tien/reactive-dot/commit/33722622b1a8104e71ae3ce0776f7ef9609da922) Thanks [@tien](https://github.com/tien)! - Excluded tests from bundle. + +- Updated dependencies []: + - @reactive-dot/core@0.32.0 + +## 0.16.23 + +### Patch Changes + +- Updated dependencies [[`776d1ef`](https://github.com/tien/reactive-dot/commit/776d1ef29777550fdcec83b11713e53a68624d14)]: + - @reactive-dot/core@0.31.0 + +## 0.16.22 + +### Patch Changes + +- Updated dependencies [[`821f21b`](https://github.com/tien/reactive-dot/commit/821f21b511b4c7ef8b0eff2e3f9eb0a3addb36ac), [`dcc8c24`](https://github.com/tien/reactive-dot/commit/dcc8c241c7543bebecdc73438f627d6f7fd0610e)]: + - @reactive-dot/core@0.30.0 + +## 0.16.21 + +### Patch Changes + +- Updated dependencies [[`6e1ded0`](https://github.com/tien/reactive-dot/commit/6e1ded07876d9ee6471830038e8910c369f14a4b)]: + - @reactive-dot/core@0.29.0 + +## 0.16.20 + +### Patch Changes + +- Updated dependencies [[`aeef030`](https://github.com/tien/reactive-dot/commit/aeef0303347668d7c53de3373f581b95a723fb17)]: + - @reactive-dot/core@0.27.1 + +## 0.16.19 + +### Patch Changes + +- Updated dependencies [[`f1d984f`](https://github.com/tien/reactive-dot/commit/f1d984f0347de0928e09ab9b99a9989586031d52)]: + - @reactive-dot/core@0.27.0 + +## 0.16.18 + +### Patch Changes + +- Updated dependencies [[`a3da0de`](https://github.com/tien/reactive-dot/commit/a3da0de4207499ff6e766f7affd08d086803a897)]: + - @reactive-dot/core@0.26.2 + +## 0.16.17 + +### Patch Changes + +- Updated dependencies [[`a638b48`](https://github.com/tien/reactive-dot/commit/a638b48e595f5dd6d87141f12f62616b507f3ed8), [`e5c37d0`](https://github.com/tien/reactive-dot/commit/e5c37d04fbdf5515c09f65875c4f8f6c6c1c5f01)]: + - @reactive-dot/core@0.26.1 + +## 0.16.16 + +### Patch Changes + +- Updated dependencies [[`ee5d6a3`](https://github.com/tien/reactive-dot/commit/ee5d6a305cd1bfe9213ea82d5c81d0e1bcce2dfa)]: + - @reactive-dot/core@0.26.0 + +## 0.16.15 + +### Patch Changes + +- Updated dependencies [[`ed4e82d`](https://github.com/tien/reactive-dot/commit/ed4e82d3eed9499f0c59d3bb1fceb151ce1e305a)]: + - @reactive-dot/core@0.25.1 + +## 0.16.14 + +### Patch Changes + +- [#304](https://github.com/tien/reactive-dot/pull/304) [`0958ce1`](https://github.com/tien/reactive-dot/commit/0958ce1f6c06f6e163b4ce6e8f012caf4fb34040) Thanks [@tien](https://github.com/tien)! - Added default implementation for `Wallet.getAccounts`. + +- Updated dependencies [[`bbda9ef`](https://github.com/tien/reactive-dot/commit/bbda9ef093e87a96d6eb23ba51464ec02ba08bb2), [`0958ce1`](https://github.com/tien/reactive-dot/commit/0958ce1f6c06f6e163b4ce6e8f012caf4fb34040), [`13c5dae`](https://github.com/tien/reactive-dot/commit/13c5dae1a0ca5500d798ac31e3a8b81bc9d3f78a)]: + - @reactive-dot/core@0.24.1 + +## 0.16.13 + +### Patch Changes + +- Updated dependencies [[`2bdab49`](https://github.com/tien/reactive-dot/commit/2bdab4925c736a81245936fb4034984dd4211f23)]: + - @reactive-dot/core@0.24.0 + +## 0.16.12 + +### Patch Changes + +- Updated dependencies [[`fccd977`](https://github.com/tien/reactive-dot/commit/fccd9778365d71a6903560513455f033fded0b4c)]: + - @reactive-dot/core@0.23.0 + +## 0.16.11 + +### Patch Changes + +- Updated dependencies [[`02b5633`](https://github.com/tien/reactive-dot/commit/02b56338948e32463b9b3e682340a25920386d91)]: + - @reactive-dot/core@0.22.0 + +## 0.16.10 + +### Patch Changes + +- Updated dependencies [[`2c30634`](https://github.com/tien/reactive-dot/commit/2c3063493977b78c95312b507332cced8296e66b)]: + - @reactive-dot/core@0.21.0 + +## 0.16.9 + +### Patch Changes + +- Updated dependencies [[`08e5517`](https://github.com/tien/reactive-dot/commit/08e5517f01bb24285ef4684f6de27753e3a9f2e9)]: + - @reactive-dot/core@0.20.0 + +## 0.16.8 + +### Patch Changes + +- Updated dependencies [[`98bb09e`](https://github.com/tien/reactive-dot/commit/98bb09e623805cf772dd42ce1ed144f569a71bae)]: + - @reactive-dot/core@0.19.0 + +## 0.16.7 + +### Patch Changes + +- Updated dependencies [[`42d6d34`](https://github.com/tien/reactive-dot/commit/42d6d343bb299d56b14a18dd0d7e54c90d20c1b6)]: + - @reactive-dot/core@0.18.0 + +## 0.16.6 + +### Patch Changes + +- [#257](https://github.com/tien/reactive-dot/pull/257) [`ce4db82`](https://github.com/tien/reactive-dot/commit/ce4db82577957a7d029c072d953b4c5e6e6462aa) Thanks [@tien](https://github.com/tien)! - Fixed `Wallet.getAccounts` incorrectly used `lastValueFrom` instead of `firstValueFrom`. + +## 0.16.5 + +### Patch Changes + +- Updated dependencies []: + - @reactive-dot/core@0.16.5 + +## 0.16.3 + +### Patch Changes + +- [#219](https://github.com/tien/reactive-dot/pull/219) [`50107c5`](https://github.com/tien/reactive-dot/commit/50107c56c8b8e6bc1adb3a1f6dc9cda60150838a) Thanks [@tien](https://github.com/tien)! - Sort Ledger accounts by derivation path. + +## 0.16.1 + +### Patch Changes + +- [#214](https://github.com/tien/reactive-dot/pull/214) [`24913cb`](https://github.com/tien/reactive-dot/commit/24913cb9340e8f2752269d1abcbdf900ecfdabf8) Thanks [@tien](https://github.com/tien)! - Fixed incorrect CommonJS import for Buffer polyfill. + +## 0.16.0 + +### Minor Changes + +- [#168](https://github.com/tien/reactive-dot/pull/168) [`1c4fdee`](https://github.com/tien/reactive-dot/commit/1c4fdee520b066254c48ba58562c50d75473da69) Thanks [@tien](https://github.com/tien)! - Added Ledger wallet integration. + +### Patch Changes + +- Updated dependencies [[`1c4fdee`](https://github.com/tien/reactive-dot/commit/1c4fdee520b066254c48ba58562c50d75473da69)]: + - @reactive-dot/core@0.16.0 diff --git a/packages/wallet-mimir/eslint.config.js b/packages/wallet-mimir/eslint.config.js new file mode 100644 index 00000000..52e9273c --- /dev/null +++ b/packages/wallet-mimir/eslint.config.js @@ -0,0 +1,4 @@ +import recommended from "@reactive-dot/eslint-config"; +import tseslint from "typescript-eslint"; + +export default tseslint.config(...recommended); diff --git a/packages/wallet-mimir/package.json b/packages/wallet-mimir/package.json new file mode 100644 index 00000000..a6b9cf69 --- /dev/null +++ b/packages/wallet-mimir/package.json @@ -0,0 +1,47 @@ +{ + "name": "@reactive-dot/wallet-mimir", + "version": "0.0.0", + "description": "Mimir adapter for Reactive DOT", + "keywords": [ + "substrate", + "polkadot", + "mimir" + ], + "homepage": "https://reactivedot.dev/", + "bugs": { + "url": "https://github.com/tien/reactive-dot/issues", + "email": "tien.nguyenkhac@icloud.com" + }, + "license": "LGPL-3.0-or-later", + "author": "Tiến Nguyễn Khắc (https://tien.zone/)", + "repository": { + "type": "git", + "url": "https://github.com/tien/reactive-dot.git", + "directory": "packages/wallet-mimir" + }, + "type": "module", + "files": [ + "src", + "build" + ], + "exports": "./build/index.js", + "scripts": { + "dev": "tsc --build --watch", + "build": "rm -rf build && tsc --build", + "lint": "eslint src", + "test": "vitest" + }, + "dependencies": { + "@mimirdev/apps-inject": "^3.1.1", + "@mimirdev/papi-signer": "^3.1.0", + "@reactive-dot/core": "workspace:^" + }, + "devDependencies": { + "@reactive-dot/eslint-config": "workspace:^", + "@tsconfig/recommended": "^1.0.8", + "@tsconfig/strictest": "^2.0.5", + "eslint": "^9.21.0", + "typescript": "^5.7.3", + "vitest": "^3.0.6" + } +} diff --git a/packages/wallet-mimir/src/index.test.ts b/packages/wallet-mimir/src/index.test.ts new file mode 100644 index 00000000..cd1c4906 --- /dev/null +++ b/packages/wallet-mimir/src/index.test.ts @@ -0,0 +1,10 @@ +import * as exports from "./index.js"; +import { expect, it } from "vitest"; + +it("should match inline snapshot", () => + expect(Object.keys(exports)).toMatchInlineSnapshot(` + [ + "MimirWalletProvider", + "MimirWallet", + ] + `)); diff --git a/packages/wallet-mimir/src/index.ts b/packages/wallet-mimir/src/index.ts new file mode 100644 index 00000000..dcfe171c --- /dev/null +++ b/packages/wallet-mimir/src/index.ts @@ -0,0 +1,2 @@ +export { MimirWalletProvider } from "./mimir-wallet-provider.js"; +export { MimirWallet } from "./mimir-wallet.js"; diff --git a/packages/wallet-mimir/src/mimir-wallet-provider.test.ts b/packages/wallet-mimir/src/mimir-wallet-provider.test.ts new file mode 100644 index 00000000..0307f810 --- /dev/null +++ b/packages/wallet-mimir/src/mimir-wallet-provider.test.ts @@ -0,0 +1,33 @@ +import { MimirWalletProvider } from "./mimir-wallet-provider.js"; +import { MimirWallet } from "./mimir-wallet.js"; +import { isMimirReady } from "@mimirdev/apps-inject"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@mimirdev/apps-inject"); +vi.mock("./mimir-wallet.js"); + +let provider: MimirWalletProvider; + +beforeEach(() => { + provider = new MimirWalletProvider(); + + vi.clearAllMocks(); +}); + +describe("getWallets", () => { + it("should return empty array when Mimir is not ready", async () => { + vi.mocked(isMimirReady).mockResolvedValue(null); + const wallets = await provider.getWallets(); + + expect(wallets).toEqual([]); + expect(isMimirReady).toHaveBeenCalled(); + }); + + it("should return MimirWallet instance when Mimir is ready", async () => { + vi.mocked(isMimirReady).mockResolvedValue("origin"); + const wallets = await provider.getWallets(); + + expect(wallets).toHaveLength(1); + expect(wallets[0]).toBeInstanceOf(MimirWallet); + }); +}); diff --git a/packages/wallet-mimir/src/mimir-wallet-provider.ts b/packages/wallet-mimir/src/mimir-wallet-provider.ts new file mode 100644 index 00000000..f0f1c0c2 --- /dev/null +++ b/packages/wallet-mimir/src/mimir-wallet-provider.ts @@ -0,0 +1,19 @@ +import { MimirWallet, type MimirWalletOptions } from "./mimir-wallet.js"; +import { isMimirReady } from "@mimirdev/apps-inject"; +import { WalletProvider } from "@reactive-dot/core/wallets.js"; + +export class MimirWalletProvider extends WalletProvider { + constructor(private readonly options?: MimirWalletOptions) { + super(); + } + + async getWallets() { + const origin = await isMimirReady(); + + if (origin === null) { + return []; + } + + return [new MimirWallet(this.options)]; + } +} diff --git a/packages/wallet-mimir/src/mimir-wallet.test.ts b/packages/wallet-mimir/src/mimir-wallet.test.ts new file mode 100644 index 00000000..7cae4efe --- /dev/null +++ b/packages/wallet-mimir/src/mimir-wallet.test.ts @@ -0,0 +1,183 @@ +import { MimirWallet } from "./mimir-wallet.js"; +import { MimirPAPISigner } from "@mimirdev/papi-signer"; +import { ReactiveDotError, Storage as WalletStorage } from "@reactive-dot/core"; +import { firstValueFrom } from "rxjs"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@mimirdev/papi-signer"); + +let wallet: MimirWallet; + +beforeEach(() => { + const inMemorySimpleStorage = { + items: new Map(), + getItem(key: string) { + return this.items.get(key) ?? null; + }, + removeItem(key: string) { + this.items.delete(key); + }, + setItem(key: string, value: string) { + this.items.set(key, value); + }, + }; + + const inMemoryStorage = new WalletStorage({ + prefix: "@reactive-dot", + storage: inMemorySimpleStorage, + }); + + wallet = new MimirWallet({ + originName: "test-origin", + storage: inMemoryStorage, + }); + + vi.clearAllMocks(); +}); + +afterEach(() => { + wallet.disconnect(); +}); + +describe("constructor", () => { + it("should create instance with correct id and name", () => { + expect(wallet.id).toBe("mimir"); + expect(wallet.name).toBe("Mimir"); + }); +}); + +describe("connect", () => { + it("should connect successfully", async () => { + const mockSigner = { + enable: vi.fn().mockResolvedValue({ result: true }), + getAccounts: vi.fn().mockResolvedValue([]), + }; + + vi.mocked(MimirPAPISigner).mockImplementation( + () => mockSigner as unknown as MimirPAPISigner, + ); + + await wallet.connect(); + + expect(mockSigner.enable).toHaveBeenCalledWith("test-origin"); + expect(await firstValueFrom(wallet.connected$)).toBeTruthy(); + }); + + it("should throw error on failed connection", async () => { + const mockSigner = { + enable: vi.fn().mockResolvedValue({ result: false }), + }; + + vi.mocked(MimirPAPISigner).mockImplementation( + () => mockSigner as unknown as MimirPAPISigner, + ); + + await expect(wallet.connect()).rejects.toThrow(ReactiveDotError); + }); +}); + +describe("$accounts", () => { + it("should emit an empty array when not connected", async () => { + const emittedAccounts = await firstValueFrom(wallet.accounts$); + expect(emittedAccounts).toEqual([]); + }); + + it("should emit updated accounts when subscribeAccounts callback is triggered", async () => { + // Prepare a controlled subscribeAccounts callback. + let accountsCallback: ((accounts: unknown[]) => void) | undefined; + const subscribeAccountsMock = vi.fn((cb: (accounts: unknown[]) => void) => { + accountsCallback = cb; + // Return a dummy unsubscribe function. + return () => {}; + }); + + const mockSigner = { + enable: vi.fn().mockResolvedValue({ result: true }), + getAccounts: vi.fn().mockResolvedValue([]), + subscribeAccounts: subscribeAccountsMock, + getPolkadotSigner: vi + .fn() + .mockImplementation((address: string) => ({ address })), + }; + + vi.mocked(MimirPAPISigner).mockImplementation( + () => mockSigner as unknown as MimirPAPISigner, + ); + + await wallet.connect(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let emittedAccounts: any[] = []; + const subscription = wallet.accounts$.subscribe((accounts) => { + emittedAccounts = accounts; + }); + + // Ensure subscribeAccounts was called. + expect(subscribeAccountsMock).toHaveBeenCalled(); + + // Trigger the subscription callback with mock account data. + const mockAccountsData = [{ address: "account1" }, { address: "account2" }]; + if (accountsCallback) { + accountsCallback(mockAccountsData); + } + + // Allow for asynchronous propagation. + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(emittedAccounts.length).toBe(2); + expect(emittedAccounts[0].address).toBe("account1"); + expect(emittedAccounts[0].id).toBe("0"); + expect(emittedAccounts[1].address).toBe("account2"); + expect(emittedAccounts[1].id).toBe("1"); + + subscription.unsubscribe(); + }); +}); + +describe("getAccounts", () => { + it("should throw error when not connected", async () => { + await expect(wallet.getAccounts()).rejects.toThrow( + "Mimir is not connected", + ); + }); + + it("should return accounts when connected", async () => { + const mockAccounts = [{ address: "test-address" }]; + const mockSigner = { + enable: vi.fn().mockResolvedValue({ result: true }), + getAccounts: vi.fn().mockResolvedValue(mockAccounts), + getPolkadotSigner: vi.fn().mockReturnValue({}), + }; + + vi.mocked(MimirPAPISigner).mockImplementation( + () => mockSigner as unknown as MimirPAPISigner, + ); + + await wallet.connect(); + const accounts = await wallet.getAccounts(); + + expect(accounts[0].address).toBe("test-address"); + expect(accounts[0].id).toBe("0"); + }); +}); + +describe("initialize", () => { + it("should connect if previously connected", async () => { + const mockSigner = { + enable: vi.fn().mockResolvedValue({ result: true }), + getAccounts: vi.fn().mockResolvedValue([]), + }; + + vi.mocked(MimirPAPISigner).mockImplementation( + () => mockSigner as unknown as MimirPAPISigner, + ); + + // @ts-expect-error using protected method for testing + wallet.storage.setItem("connected", JSON.stringify(true)); + + const connectSpy = vi.spyOn(wallet, "connect"); + await wallet.initialize(); + + expect(connectSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/wallet-mimir/src/mimir-wallet.ts b/packages/wallet-mimir/src/mimir-wallet.ts new file mode 100644 index 00000000..89060044 --- /dev/null +++ b/packages/wallet-mimir/src/mimir-wallet.ts @@ -0,0 +1,94 @@ +import type { InjectedAccount } from "@mimirdev/apps-transports"; +import { MimirPAPISigner } from "@mimirdev/papi-signer"; +import { ReactiveDotError } from "@reactive-dot/core"; +import { + type PolkadotSignerAccount, + Wallet, + type WalletOptions, +} from "@reactive-dot/core/wallets.js"; +import { BehaviorSubject, Observable, of } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; + +export type MimirWalletOptions = WalletOptions & { originName?: string }; + +export class MimirWallet extends Wallet { + readonly id = "mimir"; + + readonly name = "Mimir"; + + readonly #mimir$ = new BehaviorSubject( + undefined, + ); + + readonly connected$ = this.#mimir$.pipe(map((mimir) => mimir !== undefined)); + + readonly accounts$ = this.#mimir$.pipe( + switchMap((mimir) => { + if (mimir === undefined) { + return of([]); + } + + return new Observable((subscriber) => { + subscriber.add( + mimir.subscribeAccounts((accounts) => + subscriber.next(this.#toPolkadotSignerAccount(mimir, accounts)), + ), + ); + }); + }), + ); + + constructor(options?: MimirWalletOptions) { + super(options); + } + + override async getAccounts() { + const mimir = this.#mimir$.getValue(); + + if (mimir === undefined) { + throw new ReactiveDotError("Mimir is not connected"); + } + + return this.#toPolkadotSignerAccount(mimir, await mimir.getAccounts()); + } + + async initialize() { + if (this.storage.getItem("connected") !== null) { + await this.connect(); + } + } + + async connect() { + const signer = new MimirPAPISigner(); + + const { result } = await signer.enable( + this.options?.originName ?? globalThis.origin, + ); + + if (!result) { + throw new ReactiveDotError("Failed to connect to Mimir"); + } + + this.#mimir$.next(signer); + this.storage.setItem("connected", JSON.stringify(true)); + } + + disconnect() { + this.#mimir$.next(undefined); + this.storage.removeItem("connected"); + } + + #toPolkadotSignerAccount( + mimir: MimirPAPISigner, + accounts: InjectedAccount[], + ) { + return accounts.map( + (account, index) => + ({ + id: index.toString(), + polkadotSigner: mimir.getPolkadotSigner(account.address), + ...account, + }) satisfies PolkadotSignerAccount, + ); + } +} diff --git a/packages/wallet-mimir/tsconfig.json b/packages/wallet-mimir/tsconfig.json new file mode 100644 index 00000000..e720ff51 --- /dev/null +++ b/packages/wallet-mimir/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": [ + "@tsconfig/recommended/tsconfig.json", + "@tsconfig/strictest/tsconfig.json" + ], + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "build" + }, + "include": ["src"], + "exclude": ["**/*.test.*"] +} diff --git a/packages/wallet-mimir/vitest.config.ts b/packages/wallet-mimir/vitest.config.ts new file mode 100644 index 00000000..d7b613c2 --- /dev/null +++ b/packages/wallet-mimir/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({}); diff --git a/yarn.lock b/yarn.lock index ce1e9444..4bf6b1f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4221,6 +4221,45 @@ __metadata: languageName: node linkType: hard +"@mimirdev/apps-inject@npm:^3.1.1": + version: 3.1.1 + resolution: "@mimirdev/apps-inject@npm:3.1.1" + dependencies: + "@mimirdev/apps-sdk": "npm:^3.1.0" + checksum: 10c0/0a7467b902c79763e7f98d505117d5ed1aa373a35dd42977e5320308af5fe2b34374d09d542e3cd3b370bd7c49f3abd1a923ed3dcec8646b7fdc8b627f67a8c7 + languageName: node + linkType: hard + +"@mimirdev/apps-sdk@npm:^3.1.0": + version: 3.1.0 + resolution: "@mimirdev/apps-sdk@npm:3.1.0" + dependencies: + eventemitter3: "npm:^5.0.1" + checksum: 10c0/ea386ae9addc278c2b1bd2ca2c8782212b5bd8f4c506fc2faa8262c699aaa1e558c683e96884d00d1fd18a36a3aa3dd1c396864767bc05ca3799876995ead97e + languageName: node + linkType: hard + +"@mimirdev/apps-transports@npm:^3.1.0": + version: 3.1.0 + resolution: "@mimirdev/apps-transports@npm:3.1.0" + checksum: 10c0/66505dc5792348793b81e6c4a13147d0a4459a7584b788cd928ebe128cc1fe29ff1ab1c547d88043bb0074ab44a7ad97965525b6229e38665814568ad05c8eab + languageName: node + linkType: hard + +"@mimirdev/papi-signer@npm:^3.1.0": + version: 3.1.0 + resolution: "@mimirdev/papi-signer@npm:3.1.0" + dependencies: + "@mimirdev/apps-sdk": "npm:^3.1.0" + "@mimirdev/apps-transports": "npm:^3.1.0" + peerDependencies: + "@polkadot-api/polkadot-signer": "*" + "@polkadot-api/substrate-bindings": "*" + "@polkadot-api/utils": "*" + checksum: 10c0/81deafbcfe9455e0ae79d3b0de947909a90625402087da647f926800716e9bbd0a4997ae4bbe07047b9f556c26c21a3075fba7d7060b7e73959f0fd1aa8ded92 + languageName: node + linkType: hard + "@motionone/animation@npm:^10.15.1, @motionone/animation@npm:^10.18.0": version: 10.18.0 resolution: "@motionone/animation@npm:10.18.0" @@ -5022,6 +5061,7 @@ __metadata: "@reactive-dot/eslint-config": "workspace:^" "@reactive-dot/react": "workspace:^" "@reactive-dot/wallet-ledger": "workspace:^" + "@reactive-dot/wallet-mimir": "workspace:^" "@reactive-dot/wallet-walletconnect": "workspace:^" "@tsconfig/recommended": "npm:^1.0.8" "@tsconfig/strictest": "npm:^2.0.5" @@ -5129,6 +5169,22 @@ __metadata: languageName: unknown linkType: soft +"@reactive-dot/wallet-mimir@workspace:^, @reactive-dot/wallet-mimir@workspace:packages/wallet-mimir": + version: 0.0.0-use.local + resolution: "@reactive-dot/wallet-mimir@workspace:packages/wallet-mimir" + dependencies: + "@mimirdev/apps-inject": "npm:^3.1.1" + "@mimirdev/papi-signer": "npm:^3.1.0" + "@reactive-dot/core": "workspace:^" + "@reactive-dot/eslint-config": "workspace:^" + "@tsconfig/recommended": "npm:^1.0.8" + "@tsconfig/strictest": "npm:^2.0.5" + eslint: "npm:^9.21.0" + typescript: "npm:^5.7.3" + vitest: "npm:^3.0.6" + languageName: unknown + linkType: soft + "@reactive-dot/wallet-walletconnect@workspace:^, @reactive-dot/wallet-walletconnect@workspace:packages/wallet-walletconnect": version: 0.0.0-use.local resolution: "@reactive-dot/wallet-walletconnect@workspace:packages/wallet-walletconnect" @@ -10831,6 +10887,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "events@npm:3.3.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0"