-
Notifications
You must be signed in to change notification settings - Fork 6
Midnight v1 – 01 Wallet #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
34e8de6
feat(builder): enable Midnight ecosystem in UI and feature flags
pasevin 3013cf2
feat(ui): centralize Midnight icon and restore TypeScript declarations
pasevin bcd297c
Merge branch 'main' into feat/plat-6568-midnight-adapter-wallet-conne…
pasevin dada67d
Merge branch 'main' into feat/plat-6568-midnight-adapter-wallet-conne…
pasevin b6de87d
fix(builder): typo
pasevin ffe3dd9
Merge branch 'main' into feat/plat-6568-midnight-adapter-wallet-conne…
pasevin 3d35f8b
feat(adapter-midnight): event-driven wallet with Lace implementation
pasevin 2c0cc91
Merge branch 'main' into feat/plat-6568-midnight-adapter-wallet-conne…
pasevin b5378df
docs(common): finalize Midnight adapter spec, plan, checklists, and t…
pasevin 394976a
docs(common): update stacked branch plan in spec header
pasevin 2e9ec0e
test(adapter-midnight): add wallet connection tests, enable vitest co…
pasevin eab7653
chore(adapter-midnight): add changeset for wallet tests and vitest co…
pasevin 60c2405
docs(common): update branch strategy in plan/tasks
pasevin 86dc232
chore(deps): refresh pnpm-lock.yaml after adapter-midnight devDeps up…
pasevin 38023db
test(builder): update ecosystem feature flag tests to include Midnigh…
pasevin de4cab1
chore(builder): set Midnight enabled by default in ecosystem registry
pasevin a4b74c5
Update packages/adapter-midnight/src/wallet/utils/SafeMidnightCompone…
pasevin befe221
Update packages/adapter-midnight/src/wallet/hooks/useMidnightWallet.ts
pasevin fabc38f
chore(adapter-midnight): formatting
pasevin 8a925ec
chore(builder): reinstate type-check in build script
pasevin f02184e
docs(adapter-midnight): replace MidnightWalletProvider with MidnightW…
pasevin b0bd624
build(builder): add scoped typecheck script and keep build vite-only
pasevin a67385b
build(common): standardize on typecheck script; remove duplicate type…
pasevin 232ceab
fix(adapter-midnight): address PR feedback — extract constants, remov…
pasevin e06cfc3
Midnight v1 – 02 Ingestion: contract artifacts + loader (#206)
pasevin 2e421ea
fix(deps): override ua-parser-js to v1.0.41 to avoid AGPL license
pasevin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| --- | ||
| '@openzeppelin/ui-builder-adapter-midnight': minor | ||
| --- | ||
|
|
||
| # Refactor Midnight wallet management to event-driven architecture with polling-based event emulation | ||
|
|
||
| **Architecture Changes:** | ||
|
|
||
| - Refactored wallet implementation to mirror Stellar adapter structure | ||
| - Introduced `LaceWalletImplementation` class for core wallet logic | ||
| - Added `midnightWalletImplementationManager` singleton pattern | ||
| - Created `MidnightWalletUiRoot` as the primary provider component | ||
| - Removed unnecessary `MidnightWalletProvider` wrapper for consistency | ||
| - Implemented facade functions in `connection.ts` for high-level wallet operations | ||
|
|
||
| **Event Emulation:** | ||
|
|
||
| - Lace Midnight DAppConnectorWalletAPI lacks native `onAccountChange` events | ||
| - Implemented polling-based event emulation via `api.state()` with exponential backoff | ||
| - Adaptive polling intervals: 2s initial, 5s when connected, up to 15s on errors | ||
| - Polling pauses when document is hidden (tab inactive) to reduce intrusive popups | ||
| - Polling starts only when listeners subscribe, stops when all unsubscribe | ||
|
|
||
| **UX Improvements:** | ||
|
|
||
| - Fixed repeated wallet popup issue by preventing multiple `enable()` calls | ||
| - Added `connectInFlight` guard against React Strict Mode double-effects and rapid clicks | ||
| - Implemented focus/blur heuristics to detect user dismissal of unlock popup | ||
| - 60s fallback timeout prevents infinite loading state in edge cases | ||
| - Auto-reconnect on page load for seamless UX with already-enabled wallets | ||
|
|
||
| **Documentation:** | ||
|
|
||
| - Added comprehensive inline comments explaining design decisions and limitations | ||
| - Created wallet module README documenting architecture and implementation details | ||
| - Documented all workarounds needed due to Lace API limitations |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@openzeppelin/ui-builder-adapter-midnight': patch | ||
| --- | ||
|
|
||
| Add wallet connection unit tests and Vitest configuration; fix adapter imports to use local configuration barrel. Remove temporary test seam to align with other adapters. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
packages/adapter-midnight/src/__tests__/wallet-connect.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { MidnightAdapter } from '../adapter'; | ||
| import { midnightTestnet } from '../networks/testnet'; | ||
| import * as connection from '../wallet/connection'; | ||
|
|
||
| vi.mock('../wallet/connection', () => { | ||
| const supportsMidnightWalletConnection = vi.fn().mockReturnValue(true); | ||
| const getMidnightAvailableConnectors = vi | ||
| .fn() | ||
| .mockResolvedValue([{ id: 'mnLace', name: 'Lace (Midnight)' }]); | ||
| const disconnectMidnightWallet = vi.fn().mockResolvedValue({ disconnected: true }); | ||
| const getMidnightWalletConnectionStatus = vi | ||
| .fn() | ||
| .mockReturnValue({ isConnected: true, address: 'ct1qtestaddress', status: 'connected' }); | ||
|
|
||
| return { | ||
| supportsMidnightWalletConnection, | ||
| getMidnightAvailableConnectors, | ||
| disconnectMidnightWallet, | ||
| getMidnightWalletConnectionStatus, | ||
| }; | ||
| }); | ||
|
|
||
| describe('MidnightAdapter Wallet Connection', () => { | ||
| const networkConfig = midnightTestnet; | ||
| let adapter: MidnightAdapter; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| adapter = new MidnightAdapter(networkConfig); | ||
| }); | ||
|
|
||
| it('supports wallet connection when Lace is available', () => { | ||
| expect(adapter.supportsWalletConnection()).toBe(true); | ||
| }); | ||
|
|
||
| it('returns available connectors (Lace)', async () => { | ||
| const connectors = await adapter.getAvailableConnectors(); | ||
| expect(Array.isArray(connectors)).toBe(true); | ||
| expect(connectors[0]).toEqual({ id: 'mnLace', name: 'Lace (Midnight)' }); | ||
| }); | ||
|
|
||
| it('connectWallet is not supported (use ConnectButton path)', async () => { | ||
| const result = await adapter.connectWallet('mnLace'); | ||
| expect(result.connected).toBe(false); | ||
| expect(result.error).toBeDefined(); | ||
| }); | ||
|
|
||
| it('disconnects wallet via connection facade', async () => { | ||
| const res = await adapter.disconnectWallet(); | ||
| expect(res.disconnected).toBe(true); | ||
| // Narrow the type to access the mocked function without using 'any' | ||
| const mocked = connection as unknown as { | ||
| disconnectMidnightWallet: (...args: unknown[]) => unknown; | ||
| }; | ||
| expect(mocked.disconnectMidnightWallet).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('maps wallet connection status and injects chainId', () => { | ||
| const status = adapter.getWalletConnectionStatus(); | ||
| expect(status.isConnected).toBe(true); | ||
| expect(status.address).toBe('ct1qtestaddress'); | ||
| expect(status.chainId).toBe(networkConfig.id); | ||
| }); | ||
|
|
||
| it('exposes provider root, hooks facade, and wallet components', () => { | ||
| const Provider = adapter.getEcosystemReactUiContextProvider(); | ||
| const hooks = adapter.getEcosystemReactHooks(); | ||
| const components = adapter.getEcosystemWalletComponents(); | ||
|
|
||
| expect(typeof Provider).toBe('function'); | ||
| expect(hooks).toBeDefined(); | ||
| const typedHooks = hooks as { useAccount?: unknown } | undefined; | ||
| expect(typeof (typedHooks?.useAccount as unknown as () => unknown)).toBe('function'); | ||
| expect(components).toBeDefined(); | ||
| const typedComponents = components as | ||
| | { ConnectButton?: unknown; AccountDisplay?: unknown } | ||
| | undefined; | ||
| expect(typeof (typedComponents?.ConnectButton as unknown as () => unknown)).toBe('function'); | ||
| expect(typeof (typedComponents?.AccountDisplay as unknown as () => unknown)).toBe('function'); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Midnight Adapter Wallet Module | ||
|
|
||
| This directory contains the wallet integration layer for the Midnight adapter, providing all wallet‑related UI, hooks, context, and utilities using the Lace Midnight wallet. | ||
|
|
||
| ## Architectural Approach: Polling-Based Event Emulation | ||
|
|
||
| The Midnight adapter uses a polling-based architecture to emulate wallet events: | ||
|
|
||
| - The Lace Midnight wallet (`DAppConnectorWalletAPI`) does not expose native `onAccountChange` events. | ||
| - `LaceWalletImplementation` polls `api.state()` to detect account changes and emits custom events to listeners. | ||
| - Exponential backoff and visibility-aware polling minimize intrusive popups during wallet unlock. | ||
|
|
||
| ## Purpose | ||
|
|
||
| - UI Environment Provision: `MidnightWalletUiRoot` provides a stable provider root for wallet state. | ||
| - Facade Hooks: `midnightFacadeHooks` expose standardized hooks for connection and account status. | ||
| - UI Components: Custom‑styled wallet components (`ConnectButton`, `AccountDisplay`). | ||
| - Event Emulation: Polling-based change detection with listener-driven polling lifecycle. | ||
|
|
||
| ## Directory Structure | ||
|
|
||
| ```text | ||
| wallet/ | ||
| ├── components/ # Wallet UI components & root | ||
| │ ├── MidnightWalletUiRoot.tsx | ||
| │ ├── account/ | ||
| │ └── connect/ | ||
| ├── context/ | ||
| ├── hooks/ | ||
| ├── implementation/ # Lace wallet implementation | ||
| ├── utils/ | ||
| └── connection.ts | ||
| ``` | ||
|
|
||
| ## Key Components & Concepts | ||
|
|
||
| - Adapter UI methods: | ||
| - `getEcosystemReactUiContextProvider()` → `MidnightWalletUiRoot` | ||
| - `getEcosystemReactHooks()` → `midnightFacadeHooks` | ||
| - `getEcosystemWalletComponents()` → custom components | ||
| - `midnightWalletImplementationManager`: Singleton controller for wallet implementation instance | ||
| - `MidnightWalletUiRoot`: Stable provider that wires the implementation and context | ||
| - Facade hooks: `useMidnightAccount`, `useMidnightConnect`, `useMidnightDisconnect` | ||
|
|
||
| ## Key Characteristics | ||
|
|
||
| - Single wallet support (Lace Midnight only) | ||
| - Polling-based event emulation (no native event handlers) | ||
| - Focus/blur heuristics for dismissal detection (no native `onDismiss` event) | ||
| - No network switching component (Midnight wallets do not switch networks dynamically) | ||
| - Custom UI components matching the Builder's design system | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| ### Event Emulation | ||
|
|
||
| - `LaceWalletImplementation.onWalletConnectionChange()` emulates native events. | ||
| - Polling starts when listeners subscribe, stops when all listeners unsubscribe. | ||
| - Adaptive polling intervals: 2s initial, 5s when connected, up to 15s exponential backoff on errors. | ||
| - Pauses polling when document is hidden (tab inactive) to reduce intrusive popups. | ||
|
|
||
| ### Dismissal Detection | ||
|
|
||
| - Wallet popup does not expose `onDismiss` or `onReject` events. | ||
| - `MidnightWalletUiRoot` monitors `window.focus` events to infer dismissal. | ||
| - When focus returns and no address is set, UI stops "Connecting…" and unsubscribes (stops polling). | ||
| - 60s fallback timeout prevents infinite loading in edge cases. | ||
|
|
||
| ### Multiple Popup Prevention | ||
|
|
||
| - `LaceWalletImplementation.connect()` uses `connectInFlight` guard to prevent multiple `enable()` calls. | ||
| - React Strict Mode double-effects and rapid button clicks are handled gracefully. | ||
| - No immediate `api.state()` reads after `enable()` to avoid re-prompting locked wallets. | ||
|
|
||
| ## Usage in Application | ||
|
|
||
| `MidnightWalletUiRoot` is returned by the adapter and used by the Builder's `WalletStateProvider`. Use `useWalletState()` and facade hooks from `@openzeppelin/ui-builder-react-core` to render the wallet UI components from `getEcosystemWalletComponents()`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.