-
Notifications
You must be signed in to change notification settings - Fork 1
Jade connection #39
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
Jade connection #39
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d50478e
feat(wallet): implement wallet connection and session management
lilbonekit 7a451a0
refactor(wallet): improve wallet sync and balance handling
lilbonekit 870ce3f
feat(wallet): add address verification for Jade connector and clear w…
lilbonekit d0adc78
refactor: update wallet connector and session management
lilbonekit b025120
feat(wallet): add getXOnlyPublicKey method
lilbonekit da6326f
refactor(wallet): rename sync to syncWallet for clarity
lilbonekit 001bc58
refactor(wallet): update last receive address handling and state mana…
lilbonekit b4bae64
refactor(wallet): streamline disconnect handling and session management
lilbonekit 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 |
|---|---|---|
| @@ -1,3 +1,12 @@ | ||
| # Copy to .env and fill in values. | ||
| # For liquid mainnet use .env.liquid, for testnet use .env.liquidtestnet as a starting point. | ||
|
|
||
| VITE_API_URL=http://localhost:80 | ||
| VITE_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet | ||
| VITE_NETWORK=liquidtestnet | ||
| VITE_NETWORK=liquid | ||
| VITE_ESPLORA_BASE_URL=https://blockstream.info/liquid | ||
| VITE_WATERFALLS_URL=https://waterfalls.liquidwebwallet.org/liquid | ||
| VITE_WATERFALLS_RECIPIENT=age1xxzrgrfjm3yrwh3u6a7exgrldked0pdauvr3mx870wl6xzrwm5ps8s2h0p | ||
|
|
||
| # Optional: BIP39 mnemonic for debug software signer (dev/testnet only). | ||
| # Omit this in production — Jade hardware wallet will be used instead. | ||
| # VITE_DEBUG_MNEMONIC=abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about |
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 @@ | ||
| VITE_API_URL=http://localhost:80 | ||
| VITE_NETWORK=liquid | ||
| VITE_ESPLORA_BASE_URL=https://blockstream.info/liquid | ||
| VITE_WATERFALLS_URL=https://waterfalls.liquidwebwallet.org/liquid | ||
| VITE_WATERFALLS_RECIPIENT=age1xxzrgrfjm3yrwh3u6a7exgrldked0pdauvr3mx870wl6xzrwm5ps8s2h0p |
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,8 @@ | ||
| VITE_API_URL=http://localhost:80 | ||
| VITE_NETWORK=liquidtestnet | ||
| VITE_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet | ||
| VITE_WATERFALLS_URL=https://waterfalls.liquidwebwallet.org/liquidtestnet | ||
| VITE_WATERFALLS_RECIPIENT=age1xxzrgrfjm3yrwh3u6a7exgrldked0pdauvr3mx870wl6xzrwm5ps8s2h0p | ||
|
|
||
| # Uncomment for debug software signer (dev only): | ||
| # VITE_DEBUG_MNEMONIC=abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { useCallback, useState } from 'react' | ||
|
|
||
| export function useSessionStorage<T>(key: string): [T | null, (value: T | null) => void] { | ||
| const [value, setValueState] = useState<T | null>(() => { | ||
| try { | ||
| const raw = sessionStorage.getItem(key) | ||
| return raw ? (JSON.parse(raw) as T) : null | ||
| } catch { | ||
| return null | ||
| } | ||
| }) | ||
|
|
||
| const setValue = useCallback( | ||
| (newValue: T | null) => { | ||
| if (newValue === null) { | ||
| sessionStorage.removeItem(key) | ||
| } else { | ||
| sessionStorage.setItem(key, JSON.stringify(newValue)) | ||
|
lilbonekit marked this conversation as resolved.
|
||
| } | ||
| setValueState(newValue) | ||
| }, | ||
| [key], | ||
| ) | ||
|
|
||
| return [value, setValue] | ||
| } | ||
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,101 @@ | ||
| import type { Jade, Network, Pset, Wollet, WolletDescriptor } from 'lwk_web' | ||
|
|
||
| import type { Lwk } from '@/lwk' | ||
|
|
||
| import type { ConnectionStatus, JadeVersionInfo, WalletType } from '../types' | ||
| import type { WalletConnector } from './types' | ||
|
|
||
| /** | ||
| * Production hardware wallet connector for Jade. | ||
| * | ||
| * Jade is a WASM-backed object — it holds a Rust memory pointer internally. | ||
| * It must NOT be stored in React state. This class owns the Jade reference | ||
| * exclusively and exposes only framework-agnostic methods. | ||
| */ | ||
| export class JadeConnector implements WalletConnector { | ||
| private jade: Jade | null = null | ||
| private busy = false | ||
| private _id: string | null = null | ||
|
|
||
| constructor( | ||
| private readonly lwk: Lwk, | ||
| private readonly lwkNetwork: Network, | ||
| ) {} | ||
|
|
||
| async connect(): Promise<void> { | ||
| if (this.jade !== null) return | ||
| // HACK: The TS bindings declare this as a sync constructor, but wasm-bindgen | ||
| // generates an async constructor under the hood that returns a Promise. | ||
| // `await new this.lwk.Jade(...)` is intentional — not a mistake. | ||
| this.jade = await new this.lwk.Jade(this.lwkNetwork, true) | ||
| } | ||
|
|
||
| disconnect(): void { | ||
| if (this.jade) { | ||
| this.jade.free() | ||
| this.jade = null | ||
| } | ||
| this._id = null | ||
| } | ||
|
|
||
| get id(): string | null { | ||
| return this._id | ||
| } | ||
|
|
||
| async getVersionInfo(): Promise<JadeVersionInfo> { | ||
| if (!this.jade) throw new Error('JadeConnector: not connected') | ||
| const raw = await this.jade.getVersion() | ||
| const info = { | ||
| state: raw.JADE_STATE as JadeVersionInfo['state'], | ||
| efuseMac: raw.EFUSEMAC as string, | ||
| version: raw.JADE_VERSION as string, | ||
| } | ||
| this._id ??= info.efuseMac | ||
| return info | ||
| } | ||
|
|
||
| async getConnectionStatus(): Promise<ConnectionStatus> { | ||
| // HACK: Mutex polling and sign() share the same WebSerial port. If sign() is in | ||
| // progress (waiting for user button press), skip the poll to avoid CBOR | ||
| // frame corruption that would silently kill the signing request. | ||
| if (this.busy) throw new Error('jade:busy') | ||
| const info = await this.getVersionInfo() | ||
| return info.state === 'READY' ? 'ready' : 'locked' | ||
| } | ||
|
|
||
| async getDescriptor(variant: WalletType): Promise<WolletDescriptor> { | ||
| if (!this.jade) throw new Error('JadeConnector: not connected') | ||
| // wpkh = elwpkh native segwit; shWpkh = nested segwit (sh-wpkh). | ||
| return variant === 'Wpkh' ? this.jade.wpkh() : this.jade.shWpkh() | ||
| } | ||
|
|
||
| async signPset(pset: Pset): Promise<Pset> { | ||
| if (!this.jade) throw new Error('JadeConnector: not connected') | ||
| this.busy = true | ||
| try { | ||
| return await this.jade.sign(pset) | ||
| } finally { | ||
| this.busy = false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Ask Jade to display and confirm the receive address on-device. | ||
| * | ||
| * Jade shows the address on its screen and requires a button press to confirm. | ||
| * The returned string is the address as verified by the hardware — compare it | ||
| * against the software-derived address to detect substitution attacks. | ||
| */ | ||
| async getVerifiedReceiveAddress(variant: WalletType, wollet: Wollet): Promise<string> { | ||
| if (!this.jade) throw new Error('JadeConnector: not connected') | ||
| const addrResult = wollet.address() | ||
| const index = addrResult.index() | ||
| const path = wollet.addressFullPath(index) | ||
| const singlesig = this.lwk.Singlesig.from(variant) | ||
| return await this.jade.getReceiveAddressSingle(singlesig, path) | ||
| } | ||
|
|
||
| get isConnected(): boolean { | ||
| return this.jade !== null | ||
| } | ||
| } |
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,78 @@ | ||
| import type { Network, Pset, Signer, WolletDescriptor } from 'lwk_web' | ||
|
|
||
| import type { Lwk } from '@/lwk' | ||
|
|
||
| import type { ConnectionStatus, WalletType } from '../types' | ||
| import type { WalletConnector } from './types' | ||
|
|
||
| /** | ||
| * Software signer connector backed by a BIP39 mnemonic. | ||
| * | ||
| * Intended for dev/test only — never ship a real mnemonic in env vars. | ||
| * Gate behind VITE_DEBUG_MNEMONIC so it never runs in production builds. | ||
| * | ||
| * Signer is a WASM-backed object. It must NOT be stored in React state. | ||
| * This class owns the Signer reference exclusively. | ||
| */ | ||
| export class SeedConnector implements WalletConnector { | ||
| private signer: Signer | null = null | ||
| private _id: string | null = null | ||
|
|
||
| constructor( | ||
| private readonly lwk: Lwk, | ||
| private readonly lwkNetwork: Network, | ||
| private readonly mnemonicStr: string, | ||
| ) { | ||
| if (!mnemonicStr) throw new Error('SeedConnector: VITE_DEBUG_MNEMONIC is not set') | ||
| } | ||
|
|
||
| async connect(): Promise<void> { | ||
| if (this.signer !== null) return | ||
| const mnemonic = new this.lwk.Mnemonic(this.mnemonicStr) | ||
| this.signer = new this.lwk.Signer(mnemonic, this.lwkNetwork) | ||
| this._id = crypto.randomUUID() | ||
| } | ||
|
|
||
| disconnect(): void { | ||
| if (this.signer) { | ||
| this.signer.free() | ||
| this.signer = null | ||
| } | ||
| this._id = null | ||
| } | ||
|
|
||
| get id(): string | null { | ||
| return this._id | ||
| } | ||
|
|
||
| async getDescriptor( | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _variant: WalletType, | ||
| ): Promise<WolletDescriptor> { | ||
| if (!this.signer) throw new Error('SeedConnector: not connected') | ||
| // Signer only exposes wpkhSlip77Descriptor (native segwit + SLIP77 blinding). | ||
| // The variant param is accepted for interface compatibility but ignored here. | ||
| return this.signer.wpkhSlip77Descriptor() | ||
| } | ||
|
|
||
| async signPset(pset: Pset): Promise<Pset> { | ||
| if (!this.signer) throw new Error('SeedConnector: not connected') | ||
| // Signer.sign() is synchronous — wrap for interface compatibility. | ||
| return this.signer.sign(pset) | ||
| } | ||
|
|
||
| async getXOnlyPublicKey(): Promise<string> { | ||
| if (!this.signer) throw new Error('SeedConnector: not connected') | ||
| //github.com/BlockstreamResearch/smplx/blob/1945d11b47fff8838c3e99c210133519a9522324/crates/sdk/src/signer/core.rs#L621C1-L628C2 | ||
| const path = this.lwkNetwork.isMainnet() ? 'm/84h/1776h/0h/0/1' : 'm/84h/1h/0h/0/1' | ||
| return this.lwk.simplicityDeriveXonlyPubkey(this.signer, path).toString() | ||
| } | ||
|
|
||
| async getConnectionStatus(): Promise<ConnectionStatus> { | ||
| return 'ready' | ||
| } | ||
|
|
||
| get isConnected(): boolean { | ||
| return this.signer !== null | ||
| } | ||
| } |
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,15 @@ | ||
| import type { Pset, Wollet, WolletDescriptor } from 'lwk_web' | ||
|
|
||
| import type { ConnectionStatus, WalletType } from '../types' | ||
|
|
||
| export interface WalletConnector { | ||
| readonly id: string | null | ||
| connect(): Promise<void> | ||
| disconnect(): void | ||
| getDescriptor(variant: WalletType): Promise<WolletDescriptor> | ||
| signPset(pset: Pset): Promise<Pset> | ||
| isConnected: boolean | ||
| getConnectionStatus(): Promise<ConnectionStatus> | ||
| getXOnlyPublicKey?(): Promise<string> | ||
| getVerifiedReceiveAddress?(variant: WalletType, wollet: Wollet): Promise<string> | ||
| } |
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,13 @@ | ||
| export type WalletType = 'Wpkh' | 'ShWpkh' | ||
|
|
||
| /** Raw JADE_STATE values from getVersion() */ | ||
| export type JadeConnectionState = 'LOCKED' | 'READY' | 'UNINIT' | 'TEMP' | ||
|
|
||
| export interface JadeVersionInfo { | ||
| state: JadeConnectionState | ||
| /** EFUSEMAC — unique hardware identifier */ | ||
| efuseMac: string | ||
| version: string | ||
| } | ||
|
|
||
| export type ConnectionStatus = 'disconnected' | 'locked' | 'ready' |
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,20 @@ | ||
| import type { EsploraClient, Wollet } from 'lwk_web' | ||
|
|
||
| /** | ||
| * Syncs wallet state via waterfalls fullScan and applies the update. | ||
| * Returns the updated balance map (assetId -> satoshis as strings). | ||
| */ | ||
| export async function syncBalances( | ||
| wollet: Wollet, | ||
| esploraClient: EsploraClient, | ||
| ): Promise<Record<string, string>> { | ||
| const update = await esploraClient.fullScanToIndex(wollet, 0) | ||
| if (update) { | ||
| wollet.applyUpdate(update) | ||
| } | ||
| const result: Record<string, string> = {} | ||
| for (const [assetId, amount] of wollet.balance().entries() as [string, bigint][]) { | ||
| result[assetId] = amount.toString() | ||
| } | ||
| return result | ||
| } |
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.
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.