diff --git a/apps/demo.lasereyes.build/hooks/useUtxos.tsx b/apps/demo.lasereyes.build/hooks/useUtxos.tsx index ddfc58c6..402604ce 100644 --- a/apps/demo.lasereyes.build/hooks/useUtxos.tsx +++ b/apps/demo.lasereyes.build/hooks/useUtxos.tsx @@ -21,7 +21,10 @@ const UtxoContext = createContext(undefined) export const UtxoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { paymentAddress, network } = useLaserEyes() + const { paymentAddress, network } = useLaserEyes((x) => ({ + paymentAddress: x.paymentAddress, + network: x.network, + })) const mempoolUrl = `${getMempoolSpaceUrl(network)}/api/address/${paymentAddress}/utxo` const [utxos, setUtxos] = useState([]) diff --git a/packages/lasereyes-core/src/client/index.ts b/packages/lasereyes-core/src/client/index.ts index 16ce46dc..dbf70c34 100644 --- a/packages/lasereyes-core/src/client/index.ts +++ b/packages/lasereyes-core/src/client/index.ts @@ -52,7 +52,6 @@ export class LaserEyesClient { }, readonly config?: Config ) { - console.log('LaserEyesClient constructor') this.$store = stores.$store this.$network = stores.$network this.$providerMap = { @@ -75,7 +74,6 @@ export class LaserEyesClient { listenKeys(this.$store, ['isInitializing'], (v, oldValue) => { if (this.disposed) { - console.warn('Client disposed, ignoring isInitializing change') return } @@ -122,7 +120,6 @@ export class LaserEyesClient { async connect(defaultWallet: ProviderType) { if (this.disposed) { - console.warn('Client disposed, ignoring connect') return } @@ -186,10 +183,12 @@ export class LaserEyesClient { localStorage?.removeItem(LOCAL_STORAGE_DEFAULT_WALLET) } - switchNetwork(network: NetworkType) { + async switchNetwork(network: NetworkType): Promise { try { if (this.$store.get().provider) { - this.$providerMap[this.$store.get().provider!]?.switchNetwork(network) + await this.$providerMap[this.$store.get().provider!]?.switchNetwork( + network + ) } } catch (error) { if (error instanceof Error) { diff --git a/packages/lasereyes-core/src/client/providers/index.ts b/packages/lasereyes-core/src/client/providers/index.ts index bf3e9e87..8547a9fe 100644 --- a/packages/lasereyes-core/src/client/providers/index.ts +++ b/packages/lasereyes-core/src/client/providers/index.ts @@ -47,7 +47,7 @@ export abstract class WalletProvider { return [this.$store.get().address, this.$store.get().paymentAddress] } - switchNetwork(_network: NetworkType): void { + async switchNetwork(_network: NetworkType): Promise { this.parent.disconnect() throw UNSUPPORTED_PROVIDER_METHOD_ERROR } diff --git a/packages/lasereyes-core/src/client/utils.ts b/packages/lasereyes-core/src/client/utils.ts index 9acc4655..71643017 100644 --- a/packages/lasereyes-core/src/client/utils.ts +++ b/packages/lasereyes-core/src/client/utils.ts @@ -22,7 +22,7 @@ export function triggerDOMShakeHack(callback: VoidFunction) { const node = document.createTextNode(' ') document.body.appendChild(node) node.remove() - callback() + Promise.resolve().then(callback) }, 1500) } } diff --git a/packages/lasereyes-react/lib/providers/context.ts b/packages/lasereyes-react/lib/providers/context.ts index 757cae57..2dd1c5a4 100644 --- a/packages/lasereyes-react/lib/providers/context.ts +++ b/packages/lasereyes-react/lib/providers/context.ts @@ -1,60 +1,55 @@ import { - ContentType, - MAINNET, + createStores, + LaserEyesClient, + LaserEyesStoreType, NetworkType, - ProviderType, } from '@omnisat/lasereyes-core' import { createContext } from 'react' import { LaserEyesContextType } from './types' +import { MapStore, WritableAtom } from 'nanostores' -export const initialContext = { - hasUnisat: false, - hasXverse: false, - hasOyl: false, - hasMagicEden: false, - hasOkx: false, - hasOrange: false, - hasOpNet: false, - hasLeather: false, - hasPhantom: false, - hasSparrow: false, - hasWizz: false, - isInitializing: true, - connected: false, - isConnecting: false, - publicKey: '', - address: '', - paymentAddress: '', - paymentPublicKey: '', - balance: undefined, - network: MAINNET as NetworkType, - library: null, - provider: null, - accounts: [], - connect: async (_network: ProviderType) => { }, - disconnect: () => { }, - requestAccounts: async () => [], - getNetwork: async () => MAINNET, - switchNetwork: async (_network: NetworkType) => { }, - getPublicKey: async () => '', +const { $store, $network } = createStores() +export const defaultMethods = { + connect: async () => {}, + disconnect: () => {}, getBalance: async () => '', getInscriptions: async () => [], - sendBTC: async (_to: string, _amount: number) => '', - signMessage: async (_message: string) => '', - signPsbt: async (_tx: string) => { - return { - signedPsbtHex: '', - signedPsbtBase64: '', - txId: '', - } - }, - pushPsbt: async (_tx: string) => { - return '' - }, - inscribe: async (_content: any, _: ContentType) => '', - isCreatingCommit: false, - isInscribing: false, + getNetwork: async () => '', + getPublicKey: async () => '', + pushPsbt: async () => '', + signMessage: async () => '', + requestAccounts: async () => [], + sendBTC: async () => '', + signPsbt: async () => ({ + signedPsbtBase64: '', + signedPsbtHex: '', + }), + switchNetwork: async () => {}, + inscribe: async () => '', } - -export const LaserEyesContext = - createContext(initialContext) +export const LaserEyesStoreContext = createContext<{ + $store: MapStore + $network: WritableAtom + client: LaserEyesClient | null + methods: Pick< + LaserEyesContextType, + | 'switchNetwork' + | 'signPsbt' + | 'signMessage' + | 'requestAccounts' + | 'sendBTC' + | 'inscribe' + | 'getPublicKey' + | 'getNetwork' + | 'pushPsbt' + | 'getInscriptions' + | 'getBalance' + | 'disconnect' + | 'connect' + > +}>({ + $store, + $network, + client: null, + methods: defaultMethods, +}) diff --git a/packages/lasereyes-react/lib/providers/hooks.ts b/packages/lasereyes-react/lib/providers/hooks.ts index 6f862df2..1abe225e 100644 --- a/packages/lasereyes-react/lib/providers/hooks.ts +++ b/packages/lasereyes-react/lib/providers/hooks.ts @@ -1,7 +1,60 @@ -import { useContext } from 'react' -import { LaserEyesContext } from './context' +import { useContext, useMemo } from 'react' +import { LaserEyesStoreContext } from './context' import { LaserEyesContextType } from './types' +import { computed, keepMount, onNotify } from 'nanostores' +import { useStore } from '@nanostores/react' +import { compareValues } from '../utils/comparison' -export const useLaserEyes = (): LaserEyesContextType => { - return useContext(LaserEyesContext) +export function useLaserEyes(): LaserEyesContextType +export function useLaserEyes(selector: (x: LaserEyesContextType) => T): T +export function useLaserEyes( + selector?: (x: LaserEyesContextType) => T +): T | LaserEyesContextType { + const { $network, $store, methods } = useContext(LaserEyesStoreContext) + + const $computedStore = useMemo(() => { + const computedStore = computed([$store, $network], (store, network) => { + const value = { + paymentAddress: store.paymentAddress, + address: store.address, + publicKey: store.publicKey, + paymentPublicKey: store.paymentPublicKey, + library: {}, + network, + accounts: store.accounts, + balance: Number(store.balance), + connected: store.connected, + isConnecting: store.isConnecting, + isInitializing: store.isInitializing, + provider: store.provider, + hasLeather: store.hasProvider.leather ?? false, + hasMagicEden: store.hasProvider['magic-eden'] ?? false, + hasOkx: store.hasProvider.okx ?? false, + hasOyl: store.hasProvider.oyl ?? false, + hasOrange: store.hasProvider.orange ?? false, + hasOpNet: store.hasProvider.op_net ?? false, + hasPhantom: store.hasProvider.phantom ?? false, + hasUnisat: store.hasProvider.unisat ?? false, + hasSparrow: store.hasProvider.sparrow ?? false, + hasWizz: store.hasProvider.wizz ?? false, + hasXverse: store.hasProvider.xverse ?? false, + ...methods, + } + if (typeof selector === 'function') { + return selector(value) + } + return value + }) + + keepMount(computedStore) + onNotify(computedStore, ({ oldValue, abort }) => { + if (compareValues(oldValue, computedStore.value)) { + abort() + } + }) + + return computedStore + }, [$network, $store, selector, methods]) + + return useStore($computedStore) } diff --git a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx index e04e2cab..33b3e74d 100644 --- a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx +++ b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx @@ -1,17 +1,16 @@ 'use client' -import { ReactNode, useEffect, useMemo, useState, useCallback } from 'react' -import { LaserEyesContext, initialContext } from './context' +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { defaultMethods, LaserEyesStoreContext } from './context' import { Config, - LaserEyesClient, - MAINNET, + ContentType, createConfig, createStores, - ContentType, - ProviderType, + LaserEyesClient, + MAINNET, NetworkType, + ProviderType, } from '@omnisat/lasereyes-core' -import { useStore } from '@nanostores/react' export default function LaserEyesProvider({ config, @@ -25,97 +24,128 @@ export default function LaserEyesProvider({ () => createConfig(config ?? { network: MAINNET }), [config] ) - const [client, setClient] = useState() - - const { - address, - paymentAddress, - publicKey, - paymentPublicKey, - accounts, - balance, - connected, - hasProvider, - isConnecting, - isInitializing, - provider, - } = useStore(clientStores.$store) - const library = {} - const network = useStore(clientStores.$network) + const [client, setClient] = useState(null) useEffect(() => { const c = new LaserEyesClient(clientStores, clientConfig) - setClient(c) + setClient(() => c) c.initialize() return () => c.dispose() }, [clientConfig, clientStores]) - const connect = useCallback(async (defaultWallet: ProviderType) => await client?.connect(defaultWallet), [client]) + const connect = useCallback( + async (defaultWallet: ProviderType) => await client?.connect(defaultWallet), + [client] + ) const disconnect = useCallback(() => client?.disconnect(), [client]) - const getBalance = useCallback(async () => - (await (client?.getBalance() ?? initialContext.getBalance()))?.toString() ?? "", [client]) - const getInscriptions = useCallback(async (offset?: number, limit?: number) => - (await client?.getInscriptions(offset, limit)) ?? initialContext.getInscriptions(), [client]) - const getNetwork = useCallback(() => client?.getNetwork() ?? initialContext.getNetwork(), [client]) - const getPublicKey = useCallback(async () => - (await client?.getPublicKey()) ?? initialContext.getPublicKey(), [client]) - const pushPsbt = useCallback((tx: string) => client?.pushPsbt(tx) ?? initialContext.pushPsbt(tx), [client]) - const signMessage = useCallback(async (message: string, toSignAddress?: string) => - (await client?.signMessage(message, toSignAddress)) ?? initialContext.signMessage(message), [client]) - const requestAccounts = useCallback(async () => - (await client?.requestAccounts()) ?? initialContext.requestAccounts(), [client]) - const sendBTC = useCallback(async (to: string, amount: number) => - (await client?.sendBTC.call(client, to, amount)) ?? initialContext.sendBTC(to, amount), [client]) - const signPsbt = useCallback(async (psbt: string, finalize?: boolean, broadcast?: boolean) => - (await client?.signPsbt.call(client, psbt, finalize, broadcast)) ?? initialContext.signPsbt(psbt), [client]) - const switchNetwork = useCallback(async (network: NetworkType) => { - await client?.switchNetwork.call(client, network) - }, [client]) - const inscribe = useCallback(async (content: string, mimeType: ContentType) => - (await client?.inscribe.call(client, content, mimeType)) ?? initialContext.inscribe(content, mimeType), [client]) + const getBalance = useCallback( + async () => + ( + await (client?.getBalance() ?? defaultMethods.getBalance()) + )?.toString() ?? '', + [client] + ) + const getInscriptions = useCallback( + async (offset?: number, limit?: number) => + (await client?.getInscriptions(offset, limit)) ?? + defaultMethods.getInscriptions(), + [client] + ) + const getNetwork = useCallback( + () => client?.getNetwork() ?? defaultMethods.getNetwork(), + [client] + ) + const getPublicKey = useCallback( + async () => (await client?.getPublicKey()) ?? defaultMethods.getPublicKey(), + [client] + ) + const pushPsbt = useCallback( + (tx: string) => client?.pushPsbt(tx) ?? defaultMethods.pushPsbt(), + [client] + ) + const signMessage = useCallback( + async (message: string, toSignAddress?: string) => + (await client?.signMessage(message, toSignAddress)) ?? + defaultMethods.signMessage(), + [client] + ) + const requestAccounts = useCallback( + async () => + (await client?.requestAccounts()) ?? defaultMethods.requestAccounts(), + [client] + ) + const sendBTC = useCallback( + async (to: string, amount: number) => + (await client?.sendBTC.call(client, to, amount)) ?? + defaultMethods.sendBTC(), + [client] + ) + const signPsbt = useCallback( + async (psbt: string, finalize?: boolean, broadcast?: boolean) => + (await client?.signPsbt.call(client, psbt, finalize, broadcast)) ?? + defaultMethods.signPsbt(), + [client] + ) + const switchNetwork = useCallback( + async (network: NetworkType) => + await client?.switchNetwork.call(client, network), + [client] + ) + const inscribe = useCallback( + async (content: string, mimeType: ContentType) => + (await client?.inscribe.call(client, content, mimeType)) ?? + defaultMethods.inscribe(), + [client] + ) + + // TODO: Move method definitions into useMemo here + const methods = useMemo(() => { + if (!client) { + return defaultMethods + } + + return { + connect, + disconnect, + getBalance, + getInscriptions, + getNetwork, + getPublicKey, + pushPsbt, + signMessage, + requestAccounts, + sendBTC, + signPsbt, + switchNetwork, + inscribe, + } + }, [ + client, + connect, + disconnect, + getBalance, + getInscriptions, + getNetwork, + getPublicKey, + inscribe, + pushPsbt, + requestAccounts, + sendBTC, + signMessage, + signPsbt, + switchNetwork, + ]) return ( - {children} - + ) } diff --git a/packages/lasereyes-react/lib/utils/comparison.ts b/packages/lasereyes-react/lib/utils/comparison.ts new file mode 100644 index 00000000..6f4b688c --- /dev/null +++ b/packages/lasereyes-react/lib/utils/comparison.ts @@ -0,0 +1,20 @@ +function checkProperties(a: { [x: string]: any }, b: { [x: string]: any }) { + return Object.keys(a).every(function (p) { + return ( + Object.prototype.hasOwnProperty.call(b, p) && + (b[p] === a[p] || + (typeof a[p] == 'number' && + typeof b[p] == 'number' && + isNaN(b[p]) && + isNaN(a[p]))) + ) + }) +} + +// Compare a to b and b to a +export function compareValues(a: any, b: any) { + if (typeof a === 'object' && typeof b === 'object') { + return checkProperties(a, b) && checkProperties(b, a) + } + return a === b +} diff --git a/packages/lasereyes/index.ts b/packages/lasereyes/index.ts index d12bc665..d0438898 100644 --- a/packages/lasereyes/index.ts +++ b/packages/lasereyes/index.ts @@ -13,3 +13,4 @@ export { XverseLogo, LaserEyesLogo, } from '@omnisat/lasereyes-react' +export type { LaserEyesContextType } from '@omnisat/lasereyes-react'