diff --git a/packages/lasereyes-core/src/client/helpers/sparrow.ts b/packages/lasereyes-core/src/client/helpers/sparrow.ts new file mode 100644 index 00000000..5659f965 --- /dev/null +++ b/packages/lasereyes-core/src/client/helpers/sparrow.ts @@ -0,0 +1,72 @@ +import { NetworkType, SparrowWalletProvider } from '../..' + +const pendingRequests: Record | undefined> = {} + +function waitForConsoleKey(key: string): Promise { + if (pendingRequests[key]) { + console.warn(`Multiple requests for "${key}" detected`) + return pendingRequests[key] + } + + const p = new Promise((resolve) => { + const originalConsoleLog = console.log + console.log = (...args: any[]) => { + // originalConsoleLog.apply(console, args) + + if (args.length > 0 && typeof args[0] === 'string') { + console.log = originalConsoleLog + pendingRequests[key] = undefined + resolve(args[0]) + } + } + + originalConsoleLog( + `Please log a value for "${key}" using \n console.log('') \n to continue.` + ) + }) + pendingRequests[key] = p + return p +} + +export class DefaultSparrowWalletProvider implements SparrowWalletProvider { + async requestAccounts(): Promise<[string, string]> { + const address = await waitForConsoleKey('address') + if (!address) throw new Error('No address provided') + + const paymentAddress = await waitForConsoleKey('paymentAddress') + if (!paymentAddress) throw new Error('No payment address provided') + + return [address, paymentAddress] + } + + async signMessage(message: string): Promise { + console.log(`sign this message in sparrow wallet:`) + console.log('') + console.log(`${message}`) + console.log('') + return await waitForConsoleKey('message to sign') + } + + async signPsbt(psbtBase64: string): Promise { + console.log(`sign this in sparrow wallet:`) + console.log('') + console.log(`${psbtBase64}`) + console.log('') + return await waitForConsoleKey('signed psbt hex') + } + + async getPublicKey(): Promise { + const publicKey = await waitForConsoleKey('publicKey') + if (!publicKey) throw new Error('No public key provided') + return publicKey + } + + // TODO: Implement network switching between mainnet and testnet + async getNetwork(): Promise { + return 'mainnet' + } + + async switchNetwork(_: NetworkType): Promise { + return + } +} diff --git a/packages/lasereyes-core/src/client/index.ts b/packages/lasereyes-core/src/client/index.ts index a80bb0ab..16ce46dc 100644 --- a/packages/lasereyes-core/src/client/index.ts +++ b/packages/lasereyes-core/src/client/index.ts @@ -1,4 +1,4 @@ -import { MapStore, WritableAtom, subscribeKeys } from 'nanostores' +import { MapStore, WritableAtom, listenKeys } from 'nanostores' import { Config, ContentType, NetworkType, ProviderType } from '../types' import { @@ -36,8 +36,12 @@ export class LaserEyesClient { readonly $store: MapStore readonly $network: WritableAtom readonly $providerMap: Partial> + private disposed = false dispose() { + this.disposed = true + this.$store.off() + this.$network.off() Object.values(this.$providerMap).forEach((provider) => provider?.dispose()) } @@ -48,6 +52,7 @@ export class LaserEyesClient { }, readonly config?: Config ) { + console.log('LaserEyesClient constructor') this.$store = stores.$store this.$network = stores.$network this.$providerMap = { @@ -63,18 +68,27 @@ export class LaserEyesClient { [XVERSE]: new XVerseProvider(stores, this, config), [WIZZ]: new WizzProvider(stores, this, config), } + } + + initialize() { this.$network.listen(this.watchNetworkChange.bind(this)) - subscribeKeys(this.$store, ['isInitializing'], (v) => - this.handleIsInitializingChanged(v.isInitializing) - ) + listenKeys(this.$store, ['isInitializing'], (v, oldValue) => { + if (this.disposed) { + console.warn('Client disposed, ignoring isInitializing change') + return + } + + if (v.isInitializing !== oldValue.isInitializing) + return this.handleIsInitializingChanged(v.isInitializing) + }) - if (config && config.network) { - this.$network.set(config.network) + if (this.config && this.config.network) { + this.$network.set(this.config.network) this.getNetwork().then((foundNetwork) => { try { - if (config.network !== foundNetwork) { - this.switchNetwork(config.network) + if (this.config!.network !== foundNetwork) { + this.switchNetwork(this.config!.network) } } catch (e) { this.disconnect() @@ -107,6 +121,11 @@ export class LaserEyesClient { } async connect(defaultWallet: ProviderType) { + if (this.disposed) { + console.warn('Client disposed, ignoring connect') + return + } + this.$store.setKey('isConnecting', true) try { localStorage?.setItem(LOCAL_STORAGE_DEFAULT_WALLET, defaultWallet) @@ -118,6 +137,7 @@ export class LaserEyesClient { this.$store.setKey('connected', true) this.$store.setKey('provider', defaultWallet) } catch (error) { + console.error('Error during connect:', error) this.$store.setKey('isConnecting', false) this.disconnect() throw error @@ -150,14 +170,19 @@ export class LaserEyesClient { } disconnect() { - this.$store.setKey('connected', false) - this.$store.setKey('provider', undefined) - this.$store.setKey('address', '') - this.$store.setKey('paymentAddress', '') - this.$store.setKey('publicKey', '') - this.$store.setKey('paymentPublicKey', '') - this.$store.setKey('balance', undefined) - this.$store.setKey('accounts', []) + this.$store.set({ + provider: undefined, + address: '', + paymentAddress: '', + publicKey: '', + paymentPublicKey: '', + balance: undefined, + accounts: [], + connected: false, + isConnecting: false, + isInitializing: false, + hasProvider: this.$store.get().hasProvider, + }) localStorage?.removeItem(LOCAL_STORAGE_DEFAULT_WALLET) } @@ -360,10 +385,7 @@ export class LaserEyesClient { try { return await this.$providerMap[ this.$store.get().provider! - ]?.getInscriptions( - offset, - limit - ) + ]?.getInscriptions(offset, limit) } catch (error) { if (error instanceof Error) { if (error.message.toLowerCase().includes('not implemented')) { diff --git a/packages/lasereyes-core/src/client/providers/sparrow.ts b/packages/lasereyes-core/src/client/providers/sparrow.ts index d8fa0031..a7158a87 100644 --- a/packages/lasereyes-core/src/client/providers/sparrow.ts +++ b/packages/lasereyes-core/src/client/providers/sparrow.ts @@ -1,45 +1,29 @@ - import * as bitcoin from 'bitcoinjs-lib' import { WalletProvider } from '.' import { NetworkType, ProviderType } from '../../types' import { SPARROW } from '../../constants/wallets' import { listenKeys, MapStore } from 'nanostores' -import { createSendBtcPsbt, getBTCBalance, isMainnetNetwork } from '../../lib/helpers' +import { + createSendBtcPsbt, + getBTCBalance, + isMainnetNetwork, +} from '../../lib/helpers' import { keysToPersist, PersistedKey } from '../utils' import { persistentMap } from '@nanostores/persistent' -import { LaserEyesStoreType } from '../types' - -let consoleOverridden = false; -function waitForConsoleKey(key: string): Promise { - return new Promise((resolve) => { - if (consoleOverridden) { - console.warn(`Already waiting for console input for "${key}"!`); - return; - } - - consoleOverridden = true; - const originalConsoleLog = console.log; - - console.log = (...args: any[]) => { - originalConsoleLog.apply(console, args); - - if (args.length > 0 && typeof args[0] === "string") { - console.log = originalConsoleLog; - consoleOverridden = false; - resolve(args[0]); - } - }; - - originalConsoleLog(`Please log a value for "${key}" using \n console.log('') \n to continue.`); - }); -} +import { LaserEyesStoreType, SparrowWalletProvider } from '../types' +import { DefaultSparrowWalletProvider } from '../helpers/sparrow' const SPARROW_WALLET_PERSISTENCE_KEY = 'SPARROW_CONNECTED_WALLET_STATE' export default class SparrowProvider extends WalletProvider { + public get library(): SparrowWalletProvider | undefined { + return (window as any)?.SparrowWalletProvider + } + public get network(): NetworkType { return this.$network.get() } + observer?: MutationObserver $valueStore: MapStore> = persistentMap( SPARROW_WALLET_PERSISTENCE_KEY, @@ -55,6 +39,11 @@ export default class SparrowProvider extends WalletProvider { initialize() { if (typeof window !== 'undefined' && typeof document !== 'undefined') { this.observer = new window.MutationObserver(() => { + if (!this.library) { + // Create a new instance of the SparrowWalletProvider if it's not already available + ;(window as any).SparrowWalletProvider = + new DefaultSparrowWalletProvider() + } this.$store.setKey('hasProvider', { ...this.$store.get().hasProvider, [SPARROW]: true, @@ -86,7 +75,6 @@ export default class SparrowProvider extends WalletProvider { }) } - removeSubscriber?: Function watchStateChange( @@ -117,41 +105,38 @@ export default class SparrowProvider extends WalletProvider { this.observer?.disconnect() } - async connect(_: ProviderType): Promise { try { - const { address: foundAddress, paymentAddress: foundPaymentAddress } = this.$valueStore!.get() + const { address: foundAddress, paymentAddress: foundPaymentAddress } = + this.$valueStore!.get() if (foundAddress && foundPaymentAddress) { if (foundAddress.startsWith('tb1') && isMainnetNetwork(this.network)) { this.disconnect() } else { this.restorePersistedValues() + this.$store.setKey('provider', SPARROW) + this.$store.setKey('connected', true) return } } - - const address = await waitForConsoleKey("address"); - if (!address) throw new Error("No address provided"); - - const paymentAddress = await waitForConsoleKey("paymentAddress"); - if (!paymentAddress) throw new Error("No payment address provided"); - - const publicKey = await waitForConsoleKey("publicKey"); - if (!publicKey) throw new Error("No public key provided"); - - const paymentPublicKey = await waitForConsoleKey("paymentPublicKey"); - if (!paymentPublicKey) throw new Error("No payment public key provided"); - - this.$store.setKey("provider", SPARROW); - this.$store.setKey("accounts", [address, paymentAddress]); - this.$store.setKey("address", address); - this.$store.setKey("paymentAddress", paymentAddress); - this.$store.setKey("publicKey", publicKey); - this.$store.setKey("paymentPublicKey", paymentPublicKey); - this.$store.setKey("connected", true); + if (!this.library) throw new Error("Sparrow wallet isn't supported") + const accounts = await this.library.requestAccounts() + if (!accounts) throw new Error('No accounts found') + await this.getNetwork().then((network) => { + if (this.network !== network) { + this.switchNetwork(this.network) + } + }) + const publicKey = await this.library.getPublicKey() + if (!publicKey) throw new Error('No public key found') + this.$store.setKey('accounts', accounts) + this.$store.setKey('address', accounts[0]) + this.$store.setKey('paymentAddress', accounts[1]) + this.$store.setKey('publicKey', publicKey) + this.$store.setKey('paymentPublicKey', publicKey) } catch (error) { - this.disconnect(); - console.error("Error during connect:", error); + this.disconnect() + console.error('Error during connect:', error) } } @@ -170,23 +155,15 @@ export default class SparrowProvider extends WalletProvider { 7 ) - console.log(`sign this send psbt in with sparrow wallet:`) - console.log('') - console.log(`${psbtBase64}`) - console.log('') - const signedAndFinalizedPsbt = await waitForConsoleKey("signedAndFinalizedPsbt") - if (!signedAndFinalizedPsbt) throw new Error('No signed PSBT provided'); + const signedAndFinalizedPsbt = await this.library!.signPsbt(psbtBase64) + if (!signedAndFinalizedPsbt) throw new Error('No signed PSBT provided') const txId = await this.pushPsbt(signedAndFinalizedPsbt) - if (!txId) throw new Error('send failed, no txid returned'); + if (!txId) throw new Error('send failed, no txid returned') return txId } async signMessage(message: string, _?: string | undefined): Promise { - console.log(`sign this message in sparrow wallet:`) - console.log("") - console.log(`${message}`) - console.log("") - return await waitForConsoleKey("message to sign") + return await this.library!.signMessage(message) } async signPsbt( @@ -197,18 +174,14 @@ export default class SparrowProvider extends WalletProvider { broadcast?: boolean | undefined ): Promise< | { - signedPsbtHex: string | undefined - signedPsbtBase64: string | undefined - txId?: string | undefined - } + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string | undefined + } | undefined > { const preSigned = bitcoin.Psbt.fromBase64(psbtBase64) - console.log(`sign this in sparrow wallet:`) - console.log('') - console.log(`${psbtBase64}`) - console.log('') - const signedPsbt = await waitForConsoleKey("signed psbt hex") + const signedPsbt = await this.library!.signPsbt(psbtBase64) if (finalize && broadcast) { const txId = await this.pushPsbt(signedPsbt) @@ -227,7 +200,7 @@ export default class SparrowProvider extends WalletProvider { } async getPublicKey() { - const publicKey = await waitForConsoleKey("publicKey") + const publicKey = await this.library!.getPublicKey() this.$store.setKey('publicKey', publicKey) return publicKey } diff --git a/packages/lasereyes-core/src/client/types.ts b/packages/lasereyes-core/src/client/types.ts index d518469c..a6a0b423 100644 --- a/packages/lasereyes-core/src/client/types.ts +++ b/packages/lasereyes-core/src/client/types.ts @@ -1,4 +1,4 @@ -import { ProviderType } from '../types' +import { NetworkType, ProviderType } from '../types' export type LaserEyesStoreType = { provider: ProviderType | undefined @@ -13,3 +13,13 @@ export type LaserEyesStoreType = { balance: bigint | undefined hasProvider: Record } + +export interface SparrowWalletProvider { + requestAccounts(): Promise + getPublicKey(): Promise + getNetwork(): Promise + switchNetwork(network: NetworkType): Promise + signMessage(message: string): Promise + signPsbt(psbtBase64: string): Promise +} + diff --git a/packages/lasereyes-react/lib/providers/context.ts b/packages/lasereyes-react/lib/providers/context.ts index dbfe18f5..757cae57 100644 --- a/packages/lasereyes-react/lib/providers/context.ts +++ b/packages/lasereyes-react/lib/providers/context.ts @@ -7,7 +7,7 @@ import { import { createContext } from 'react' import { LaserEyesContextType } from './types' -const initialContext = { +export const initialContext = { hasUnisat: false, hasXverse: false, hasOyl: false, diff --git a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx index 279f0f07..ddcbe933 100644 --- a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx +++ b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx @@ -1,6 +1,6 @@ 'use client' -import { ReactNode, useMemo } from 'react' -import { LaserEyesContext } from './context' +import { ReactNode, useEffect, useMemo, useState } from 'react' +import { LaserEyesContext, initialContext } from './context' import { Config, LaserEyesClient, @@ -18,13 +18,13 @@ export default function LaserEyesProvider({ config?: Config children: ReactNode | ReactNode[] }) { - const client = useMemo(() => { - const c = new LaserEyesClient( - createStores(), - createConfig(config ?? { network: MAINNET }) - ) - return c - }, [config]) + const clientStores = useMemo(() => createStores(), []) + const clientConfig = useMemo( + () => createConfig(config ?? { network: MAINNET }), + [config] + ) + const [client, setClient] = useState() + const { address, paymentAddress, @@ -37,9 +37,16 @@ export default function LaserEyesProvider({ isConnecting, isInitializing, provider, - } = useStore(client.$store) + } = useStore(clientStores.$store) const library = {} - const network = useStore(client.$network) + const network = useStore(clientStores.$network) + + useEffect(() => { + const c = new LaserEyesClient(clientStores, clientConfig) + setClient(c) + c.initialize() + return () => c.dispose() + }, [clientConfig, clientStores]) return ( - ((await client.getBalance.call(client)) ?? '').toString(), + ((await client?.getBalance.call(client)) ?? initialContext.getBalance())?.toString(), getInscriptions: async (offset, limit) => - (await client.getInscriptions.call(client, offset, limit)) ?? [], - getNetwork: client.getNetwork.bind(client), + (await client?.getInscriptions.call(client, offset, limit)) ?? initialContext.getInscriptions(), + getNetwork: client?.getNetwork.bind(client) ?? initialContext.getNetwork, getPublicKey: async () => - (await client.getPublicKey.call(client)) ?? '', - pushPsbt: client.pushPsbt.bind(client), + (await client?.getPublicKey.call(client)) ?? initialContext.getPublicKey(), + pushPsbt: client?.pushPsbt.bind(client) ?? initialContext.pushPsbt, signMessage: async (message: string, toSignAddress?: string) => - (await client.signMessage.call(client, message, toSignAddress)) ?? '', + (await client?.signMessage.call(client, message, toSignAddress)) ?? initialContext.signMessage(message), requestAccounts: async () => - (await client.requestAccounts.call(client)) ?? [], + (await client?.requestAccounts.call(client)) ?? initialContext.requestAccounts(), sendBTC: async (to, amount) => - (await client.sendBTC.call(client, to, amount)) ?? '', + (await client?.sendBTC.call(client, to, amount)) ?? initialContext.sendBTC(to, amount), signPsbt: async (psbt, finalize, broadcast) => - (await client.signPsbt.call(client, psbt, finalize, broadcast)) ?? { - signedPsbtBase64: '', - signedPsbtHex: '', - }, + (await client?.signPsbt.call(client, psbt, finalize, broadcast)) ?? initialContext.signPsbt(psbt), switchNetwork: async (network) => { - await client.switchNetwork.call(client, network) + await client?.switchNetwork.call(client, network) }, inscribe: async (content, mimeType: ContentType) => - (await client.inscribe.call(client, content, mimeType)) ?? '', + (await client?.inscribe.call(client, content, mimeType)) ?? initialContext.inscribe(content, mimeType), }} > {children}