import { describe, it, expect } from 'vitest' import { toBip122WireAccount } from './BridgeParent' /** * Regression guard for the bip122 account wire shape. * * Framed bip122 connect failed for every wallet until `toBip122WireAccount` * existed: Comlink marshals with the structured clone algorithm, which copies * only OWN ENUMERABLE properties, and MetaMask's bitcoin account is a class * instance whose `address`/`publicKey` live as accessors on the prototype. The * child therefore received `{}` and the adapter's first * `bytesToHex(account.publicKey)` threw "bytes is not iterable" — before the * request ever reached the wallet. * * `structuredClone` is used directly rather than a real MessageChannel: it IS * the algorithm `postMessage` applies, and isolating it keeps the failing * mechanism visible instead of buried in transport setup. */ /** * Stand-in for a wallet-native account. * * The `#` private fields are load-bearing — they are what makes * `Object.getOwnPropertyNames(account)` empty, exactly as observed on * MetaMask's real account object. A fixture using ordinary properties would * survive a structured clone and would therefore pass against the very bug * this file exists to catch. */ class WalletNativeAccount { readonly #address: string readonly #publicKey: Uint8Array readonly #purpose?: string constructor(address: string, publicKey: Uint8Array, purpose?: string) { this.#address = address this.#publicKey = publicKey this.#purpose = purpose } get address(): string { return this.#address } get publicKey(): Uint8Array { return this.#publicKey } get purpose(): string | undefined { return this.#purpose } } const ADDRESS = 'bc1qjxztncefrl5czfyfxd449t8tht634g9cnaz5ka' const PUBLIC_KEY = Uint8Array.from([3, 65, 234, 69, 78, 93, 165, 185, 148]) describe('toBip122WireAccount', () => { it('confirms the flattening this transform exists to prevent', () => { const account = new WalletNativeAccount(ADDRESS, PUBLIC_KEY, 'payment') // The accessors are on the prototype, so the instance owns nothing... expect(Object.getOwnPropertyNames(account)).toEqual([]) // ...and a structured clone consequently carries no data at all. const cloned = structuredClone(account) as Record expect(Object.keys(cloned)).toEqual([]) expect(cloned.address).toBeUndefined() expect(cloned.publicKey).toBeUndefined() }) it('survives a structured clone with every field the adapter reads', () => { const account = new WalletNativeAccount(ADDRESS, PUBLIC_KEY, 'payment') const cloned = structuredClone(toBip122WireAccount(account)) // These three are exactly what wallet-standard.ts `getAccounts` consumes. expect(cloned.address).toBe(ADDRESS) expect(cloned.publicKey).toEqual([3, 65, 234, 69, 78, 93, 165, 185, 148]) expect(cloned.purpose).toBe('payment') }) it('sends publicKey as a plain array so the wire carries no typed-array assumption', () => { const wire = toBip122WireAccount( new WalletNativeAccount(ADDRESS, PUBLIC_KEY) ) expect(Array.isArray(wire.publicKey)).toBe(true) expect(wire.publicKey).not.toBeInstanceOf(Uint8Array) }) it('omits purpose when the wallet does not report one', () => { const wire = toBip122WireAccount( new WalletNativeAccount(ADDRESS, PUBLIC_KEY) ) expect('purpose' in wire).toBe(false) }) it('marshals a wallet that already hands back plain data identically', () => { // Not every bip122 wallet uses accessor-backed classes; a plain object with // a number-array publicKey must produce the same wire shape. const wire = toBip122WireAccount({ address: ADDRESS, publicKey: [3, 65, 234], purpose: 'ordinal' }) expect(wire).toEqual({ address: ADDRESS, publicKey: [3, 65, 234], purpose: 'ordinal' }) }) it('tolerates a missing publicKey rather than throwing mid-discovery', () => { // Discovery maps EVERY account; one malformed entry must not take down the // whole wallet list. const wire = toBip122WireAccount({ address: ADDRESS }) expect(wire.publicKey).toEqual([]) }) })