wip: refactor WebLN providers in frontend
This commit is contained in:
		
							parent
							
								
									8329da1f56
								
							
						
					
					
						commit
						a8450be083
					
				@ -23,7 +23,6 @@ import classNames from 'classnames'
 | 
				
			|||||||
import SnIcon from '@/svgs/sn.svg'
 | 
					import SnIcon from '@/svgs/sn.svg'
 | 
				
			||||||
import { useHasNewNotes } from '../use-has-new-notes'
 | 
					import { useHasNewNotes } from '../use-has-new-notes'
 | 
				
			||||||
import { useWalletLogger } from '../logger'
 | 
					import { useWalletLogger } from '../logger'
 | 
				
			||||||
import { useWebLNConfigurator } from '../webln'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Brand ({ className }) {
 | 
					export function Brand ({ className }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -257,7 +256,7 @@ export default function LoginButton ({ className }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function LogoutDropdownItem () {
 | 
					export function LogoutDropdownItem () {
 | 
				
			||||||
  const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
 | 
					  const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
 | 
				
			||||||
  const webLN = useWebLNConfigurator()
 | 
					  // const webLN = useWebLNConfigurator()
 | 
				
			||||||
  const { deleteLogs } = useWalletLogger()
 | 
					  const { deleteLogs } = useWalletLogger()
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dropdown.Item
 | 
					    <Dropdown.Item
 | 
				
			||||||
@ -267,8 +266,8 @@ export function LogoutDropdownItem () {
 | 
				
			|||||||
        if (pushSubscription) {
 | 
					        if (pushSubscription) {
 | 
				
			||||||
          await togglePushSubscription().catch(console.error)
 | 
					          await togglePushSubscription().catch(console.error)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // detach wallets
 | 
					        // TODO: detach wallets
 | 
				
			||||||
        await webLN.clearConfig().catch(console.error)
 | 
					        // await webLN.clearConfig().catch(console.error)
 | 
				
			||||||
        // delete client wallet logs to prevent leak of private data if a shared device was used
 | 
					        // delete client wallet logs to prevent leak of private data if a shared device was used
 | 
				
			||||||
        await deleteLogs(Wallet.NWC).catch(console.error)
 | 
					        await deleteLogs(Wallet.NWC).catch(console.error)
 | 
				
			||||||
        await deleteLogs(Wallet.LNbits).catch(console.error)
 | 
					        await deleteLogs(Wallet.LNbits).catch(console.error)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								components/use-local-state.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								components/use-local-state.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { useCallback, useEffect, useState } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useLocalState (storageKey, initialValue = '') {
 | 
				
			||||||
 | 
					  const [value, innerSetValue] = useState(initialValue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const value = window.localStorage.getItem(storageKey)
 | 
				
			||||||
 | 
					    innerSetValue(JSON.parse(value))
 | 
				
			||||||
 | 
					  }, [storageKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setValue = useCallback((newValue) => {
 | 
				
			||||||
 | 
					    window.localStorage.setItem(storageKey, JSON.stringify(newValue))
 | 
				
			||||||
 | 
					    innerSetValue(newValue)
 | 
				
			||||||
 | 
					  }, [storageKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clearValue = useCallback(() => {
 | 
				
			||||||
 | 
					    window.localStorage.removeItem(storageKey)
 | 
				
			||||||
 | 
					    innerSetValue(null)
 | 
				
			||||||
 | 
					  }, [storageKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [value, setValue, clearValue]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -9,7 +9,7 @@ import { Status } from './webln'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
 | 
					export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WalletCard ({ title, badges, provider, status }) {
 | 
					export function WalletCard ({ title, badges, provider, status, href }) {
 | 
				
			||||||
  const configured = isConfigured(status)
 | 
					  const configured = isConfigured(status)
 | 
				
			||||||
  let indicator = styles.disabled
 | 
					  let indicator = styles.disabled
 | 
				
			||||||
  switch (status) {
 | 
					  switch (status) {
 | 
				
			||||||
@ -42,14 +42,13 @@ export function WalletCard ({ title, badges, provider, status }) {
 | 
				
			|||||||
              </Badge>)}
 | 
					              </Badge>)}
 | 
				
			||||||
        </Card.Subtitle>
 | 
					        </Card.Subtitle>
 | 
				
			||||||
      </Card.Body>
 | 
					      </Card.Body>
 | 
				
			||||||
      {provider &&
 | 
					      <Link href={href}>
 | 
				
			||||||
        <Link href={`/settings/wallets/${provider}`}>
 | 
					 | 
				
			||||||
        <Card.Footer className={styles.attach}>
 | 
					        <Card.Footer className={styles.attach}>
 | 
				
			||||||
          {configured
 | 
					          {configured
 | 
				
			||||||
            ? <>configure<Gear width={14} height={14} /></>
 | 
					            ? <>configure<Gear width={14} height={14} /></>
 | 
				
			||||||
            : <>attach<Plug width={14} height={14} /></>}
 | 
					            : <>attach<Plug width={14} height={14} /></>}
 | 
				
			||||||
        </Card.Footer>
 | 
					        </Card.Footer>
 | 
				
			||||||
        </Link>}
 | 
					      </Link>
 | 
				
			||||||
    </Card>
 | 
					    </Card>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										70
									
								
								components/wallet-configurator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								components/wallet-configurator.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					import { WalletSecurityBanner } from './banners'
 | 
				
			||||||
 | 
					import { Form } from './form'
 | 
				
			||||||
 | 
					import { CenterLayout } from './layout'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function WalletConfigurator ({ config }) {
 | 
				
			||||||
 | 
					  const initial = config.provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <CenterLayout>
 | 
				
			||||||
 | 
					      <h2 className='pb-2'>{config.title}</h2>
 | 
				
			||||||
 | 
					      <h6 className='text-muted text-center pb-3'>use {config.title} for payments</h6>
 | 
				
			||||||
 | 
					      <WalletSecurityBanner />
 | 
				
			||||||
 | 
					      <Form
 | 
				
			||||||
 | 
					        initial={{
 | 
				
			||||||
 | 
					          url: url || '',
 | 
				
			||||||
 | 
					          adminKey: adminKey || '',
 | 
				
			||||||
 | 
					          isDefault: isDefault || false
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        schema={lnbitsSchema}
 | 
				
			||||||
 | 
					        onSubmit={async ({ isDefault, ...values }) => {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            await saveConfig(values)
 | 
				
			||||||
 | 
					            if (isDefault) setProvider(lnbits)
 | 
				
			||||||
 | 
					            toaster.success('saved settings')
 | 
				
			||||||
 | 
					            router.push('/settings/wallets')
 | 
				
			||||||
 | 
					          } catch (err) {
 | 
				
			||||||
 | 
					            console.error(err)
 | 
				
			||||||
 | 
					            toaster.danger('failed to attach: ' + err.message || err.toString?.())
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ClientInput
 | 
				
			||||||
 | 
					          initialValue={url}
 | 
				
			||||||
 | 
					          label='lnbits url'
 | 
				
			||||||
 | 
					          name='url'
 | 
				
			||||||
 | 
					          required
 | 
				
			||||||
 | 
					          autoFocus
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          initialValue={adminKey}
 | 
				
			||||||
 | 
					          label='admin key'
 | 
				
			||||||
 | 
					          name='adminKey'
 | 
				
			||||||
 | 
					          newPass
 | 
				
			||||||
 | 
					          required
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <ClientCheckbox
 | 
				
			||||||
 | 
					          disabled={!configured || isDefault || enabledProviders.length === 1}
 | 
				
			||||||
 | 
					          initialValue={isDefault}
 | 
				
			||||||
 | 
					          label='default payment method'
 | 
				
			||||||
 | 
					          name='isDefault'
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <WalletButtonBar
 | 
				
			||||||
 | 
					          status={status} onDelete={async () => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              await clearConfig()
 | 
				
			||||||
 | 
					              toaster.success('saved settings')
 | 
				
			||||||
 | 
					              router.push('/settings/wallets')
 | 
				
			||||||
 | 
					            } catch (err) {
 | 
				
			||||||
 | 
					              console.error(err)
 | 
				
			||||||
 | 
					              toaster.danger('failed to detach: ' + err.message || err.toString?.())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Form>
 | 
				
			||||||
 | 
					      <div className='mt-3 w-100'>
 | 
				
			||||||
 | 
					        <WalletLogs wallet={Wallet.LNbits} embedded />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </CenterLayout>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,29 +1,7 @@
 | 
				
			|||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | 
					import { createContext, useContext } from 'react'
 | 
				
			||||||
import { LNbitsProvider, useLNbits } from './lnbits'
 | 
					 | 
				
			||||||
import { NWCProvider, useNWC } from './nwc'
 | 
					 | 
				
			||||||
import { LNCProvider, useLNC } from './lnc'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WebLNContext = createContext({})
 | 
					const WebLNContext = createContext({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isEnabled = p => [Status.Enabled, Status.Locked].includes(p?.status)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const syncProvider = (array, provider) => {
 | 
					 | 
				
			||||||
  const idx = array.findIndex(({ name }) => provider.name === name)
 | 
					 | 
				
			||||||
  const enabled = isEnabled(provider)
 | 
					 | 
				
			||||||
  if (idx === -1) {
 | 
					 | 
				
			||||||
    // add provider to end if enabled
 | 
					 | 
				
			||||||
    return enabled ? [...array, provider] : array
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return [
 | 
					 | 
				
			||||||
    ...array.slice(0, idx),
 | 
					 | 
				
			||||||
    // remove provider if not enabled
 | 
					 | 
				
			||||||
    ...enabled ? [provider] : [],
 | 
					 | 
				
			||||||
    ...array.slice(idx + 1)
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const storageKey = 'webln:providers'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const Status = {
 | 
					export const Status = {
 | 
				
			||||||
  Initialized: 'Initialized',
 | 
					  Initialized: 'Initialized',
 | 
				
			||||||
  Enabled: 'Enabled',
 | 
					  Enabled: 'Enabled',
 | 
				
			||||||
@ -31,112 +9,14 @@ export const Status = {
 | 
				
			|||||||
  Error: 'Error'
 | 
					  Error: 'Error'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function migrateLocalStorage (oldStorageKey, newStorageKey) {
 | 
					export function WebLNProvider ({ children }) {
 | 
				
			||||||
  const item = window.localStorage.getItem(oldStorageKey)
 | 
					 | 
				
			||||||
  if (item) {
 | 
					 | 
				
			||||||
    window.localStorage.setItem(newStorageKey, item)
 | 
					 | 
				
			||||||
    window.localStorage.removeItem(oldStorageKey)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return item
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function RawWebLNProvider ({ children }) {
 | 
					 | 
				
			||||||
  const lnbits = useLNbits()
 | 
					 | 
				
			||||||
  const nwc = useNWC()
 | 
					 | 
				
			||||||
  const lnc = useLNC()
 | 
					 | 
				
			||||||
  const availableProviders = [lnbits, nwc, lnc]
 | 
					 | 
				
			||||||
  const [enabledProviders, setEnabledProviders] = useState([])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // restore order on page reload
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const storedOrder = window.localStorage.getItem(storageKey)
 | 
					 | 
				
			||||||
    if (!storedOrder) return
 | 
					 | 
				
			||||||
    const providerNames = JSON.parse(storedOrder)
 | 
					 | 
				
			||||||
    setEnabledProviders(providers => {
 | 
					 | 
				
			||||||
      return providerNames.map(name => {
 | 
					 | 
				
			||||||
        for (const p of availableProviders) {
 | 
					 | 
				
			||||||
          if (p.name === name) return p
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        console.warn(`Stored provider with name ${name} not available`)
 | 
					 | 
				
			||||||
        return null
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // keep list in sync with underlying providers
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    setEnabledProviders(providers => {
 | 
					 | 
				
			||||||
      // Sync existing provider state with new provider state
 | 
					 | 
				
			||||||
      // in the list while keeping the order they are in.
 | 
					 | 
				
			||||||
      // If provider does not exist but is enabled, it is just added to the end of the list.
 | 
					 | 
				
			||||||
      // This can be the case if we're syncing from a page reload
 | 
					 | 
				
			||||||
      // where the providers are initially not enabled.
 | 
					 | 
				
			||||||
      // If provider is no longer enabled, it is removed from the list.
 | 
					 | 
				
			||||||
      const isInitialized = p => [Status.Enabled, Status.Locked, Status.Initialized].includes(p.status)
 | 
					 | 
				
			||||||
      const newProviders = availableProviders.filter(isInitialized).reduce(syncProvider, providers)
 | 
					 | 
				
			||||||
      const newOrder = newProviders.map(({ name }) => name)
 | 
					 | 
				
			||||||
      window.localStorage.setItem(storageKey, JSON.stringify(newOrder))
 | 
					 | 
				
			||||||
      return newProviders
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [...availableProviders])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // first provider in list is the default provider
 | 
					 | 
				
			||||||
  // TODO: implement fallbacks via provider priority
 | 
					 | 
				
			||||||
  const provider = enabledProviders[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const setProvider = useCallback((defaultProvider) => {
 | 
					 | 
				
			||||||
    // move provider to the start to set it as default
 | 
					 | 
				
			||||||
    setEnabledProviders(providers => {
 | 
					 | 
				
			||||||
      const idx = providers.findIndex(({ name }) => defaultProvider.name === name)
 | 
					 | 
				
			||||||
      if (idx === -1) {
 | 
					 | 
				
			||||||
        console.warn(`tried to set unenabled provider ${defaultProvider.name} as default`)
 | 
					 | 
				
			||||||
        return providers
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return [defaultProvider, ...providers.slice(0, idx), ...providers.slice(idx + 1)]
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [setEnabledProviders])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const clearConfig = useCallback(async () => {
 | 
					 | 
				
			||||||
    lnbits.clearConfig()
 | 
					 | 
				
			||||||
    nwc.clearConfig()
 | 
					 | 
				
			||||||
    await lnc.clearConfig()
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const value = useMemo(() => ({
 | 
					 | 
				
			||||||
    provider: isEnabled(provider)
 | 
					 | 
				
			||||||
      ? { name: provider.name, sendPayment: provider.sendPayment }
 | 
					 | 
				
			||||||
      : null,
 | 
					 | 
				
			||||||
    enabledProviders,
 | 
					 | 
				
			||||||
    setProvider,
 | 
					 | 
				
			||||||
    clearConfig
 | 
					 | 
				
			||||||
  }), [provider, enabledProviders, setProvider])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <WebLNContext.Provider value={value}>
 | 
					    <WebLNContext.Provider value={null}>
 | 
				
			||||||
      {children}
 | 
					      {children}
 | 
				
			||||||
    </WebLNContext.Provider>
 | 
					    </WebLNContext.Provider>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WebLNProvider ({ children }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <LNbitsProvider>
 | 
					 | 
				
			||||||
      <NWCProvider>
 | 
					 | 
				
			||||||
        <LNCProvider>
 | 
					 | 
				
			||||||
          <RawWebLNProvider>
 | 
					 | 
				
			||||||
            {children}
 | 
					 | 
				
			||||||
          </RawWebLNProvider>
 | 
					 | 
				
			||||||
        </LNCProvider>
 | 
					 | 
				
			||||||
      </NWCProvider>
 | 
					 | 
				
			||||||
    </LNbitsProvider>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useWebLN () {
 | 
					export function useWebLN () {
 | 
				
			||||||
  const { provider } = useContext(WebLNContext)
 | 
					 | 
				
			||||||
  return provider
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useWebLNConfigurator () {
 | 
					 | 
				
			||||||
  return useContext(WebLNContext)
 | 
					  return useContext(WebLNContext)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,70 @@
 | 
				
			|||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | 
					 | 
				
			||||||
import { useWalletLogger } from '../logger'
 | 
					 | 
				
			||||||
import { Status, migrateLocalStorage } from '.'
 | 
					 | 
				
			||||||
import { bolt11Tags } from '@/lib/bolt11'
 | 
					import { bolt11Tags } from '@/lib/bolt11'
 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
import { useMe } from '../me'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
 | 
					export const name = 'LNbits'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LNbitsContext = createContext()
 | 
					export const config = {
 | 
				
			||||||
 | 
					  provider: {
 | 
				
			||||||
 | 
					    url: {
 | 
				
			||||||
 | 
					      label: 'lnbits url',
 | 
				
			||||||
 | 
					      type: 'text'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    adminKey: {
 | 
				
			||||||
 | 
					      label: 'admin key',
 | 
				
			||||||
 | 
					      type: 'password'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  card: {
 | 
				
			||||||
 | 
					    title: 'LNbits',
 | 
				
			||||||
 | 
					    badges: ['send only', 'non-custodialish'],
 | 
				
			||||||
 | 
					    href: '/settings/wallets/lnbits'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getWallet = async (baseUrl, adminKey) => {
 | 
					export function getInfo ({ config, logger }) {
 | 
				
			||||||
 | 
					  return async function () {
 | 
				
			||||||
 | 
					    const response = await getWallet(config.url, config.adminKey)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      node: {
 | 
				
			||||||
 | 
					        alias: response.name,
 | 
				
			||||||
 | 
					        pubkey: ''
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      methods: [
 | 
				
			||||||
 | 
					        'getInfo',
 | 
				
			||||||
 | 
					        'getBalance',
 | 
				
			||||||
 | 
					        'sendPayment'
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      version: '1.0',
 | 
				
			||||||
 | 
					      supports: ['lightning']
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function sendPayment ({ config, logger }) {
 | 
				
			||||||
 | 
					  return async function (bolt11) {
 | 
				
			||||||
 | 
					    const { url, adminKey } = config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hash = bolt11Tags(bolt11).payment_hash
 | 
				
			||||||
 | 
					    logger.info('sending payment:', `payment_hash=${hash}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await postPayment(url, adminKey, bolt11)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const checkResponse = await getPayment(url, adminKey, response.payment_hash)
 | 
				
			||||||
 | 
					      if (!checkResponse.preimage) {
 | 
				
			||||||
 | 
					        throw new Error('No preimage')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const preimage = checkResponse.preimage
 | 
				
			||||||
 | 
					      logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
 | 
				
			||||||
 | 
					      return { preimage }
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
 | 
				
			||||||
 | 
					      throw err
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getWallet (baseUrl, adminKey) {
 | 
				
			||||||
  const url = baseUrl.replace(/\/+$/, '')
 | 
					  const url = baseUrl.replace(/\/+$/, '')
 | 
				
			||||||
  const path = '/api/v1/wallet'
 | 
					  const path = '/api/v1/wallet'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,11 +78,12 @@ const getWallet = async (baseUrl, adminKey) => {
 | 
				
			|||||||
    const errBody = await res.json()
 | 
					    const errBody = await res.json()
 | 
				
			||||||
    throw new Error(errBody.detail)
 | 
					    throw new Error(errBody.detail)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const wallet = await res.json()
 | 
					  const wallet = await res.json()
 | 
				
			||||||
  return wallet
 | 
					  return wallet
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const postPayment = async (baseUrl, adminKey, bolt11) => {
 | 
					async function postPayment (baseUrl, adminKey, bolt11) {
 | 
				
			||||||
  const url = baseUrl.replace(/\/+$/, '')
 | 
					  const url = baseUrl.replace(/\/+$/, '')
 | 
				
			||||||
  const path = '/api/v1/payments'
 | 
					  const path = '/api/v1/payments'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,11 +99,12 @@ const postPayment = async (baseUrl, adminKey, bolt11) => {
 | 
				
			|||||||
    const errBody = await res.json()
 | 
					    const errBody = await res.json()
 | 
				
			||||||
    throw new Error(errBody.detail)
 | 
					    throw new Error(errBody.detail)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const payment = await res.json()
 | 
					  const payment = await res.json()
 | 
				
			||||||
  return payment
 | 
					  return payment
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getPayment = async (baseUrl, adminKey, paymentHash) => {
 | 
					async function getPayment (baseUrl, adminKey, paymentHash) {
 | 
				
			||||||
  const url = baseUrl.replace(/\/+$/, '')
 | 
					  const url = baseUrl.replace(/\/+$/, '')
 | 
				
			||||||
  const path = `/api/v1/payments/${paymentHash}`
 | 
					  const path = `/api/v1/payments/${paymentHash}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,150 +118,7 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
 | 
				
			|||||||
    const errBody = await res.json()
 | 
					    const errBody = await res.json()
 | 
				
			||||||
    throw new Error(errBody.detail)
 | 
					    throw new Error(errBody.detail)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const payment = await res.json()
 | 
					  const payment = await res.json()
 | 
				
			||||||
  return payment
 | 
					  return payment
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function LNbitsProvider ({ children }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const [url, setUrl] = useState('')
 | 
					 | 
				
			||||||
  const [adminKey, setAdminKey] = useState('')
 | 
					 | 
				
			||||||
  const [status, setStatus] = useState()
 | 
					 | 
				
			||||||
  const { logger } = useWalletLogger(Wallet.LNbits)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let storageKey = 'webln:provider:lnbits'
 | 
					 | 
				
			||||||
  if (me) {
 | 
					 | 
				
			||||||
    storageKey = `${storageKey}:${me.id}`
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getInfo = useCallback(async () => {
 | 
					 | 
				
			||||||
    const response = await getWallet(url, adminKey)
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      node: {
 | 
					 | 
				
			||||||
        alias: response.name,
 | 
					 | 
				
			||||||
        pubkey: ''
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      methods: [
 | 
					 | 
				
			||||||
        'getInfo',
 | 
					 | 
				
			||||||
        'getBalance',
 | 
					 | 
				
			||||||
        'sendPayment'
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      version: '1.0',
 | 
					 | 
				
			||||||
      supports: ['lightning']
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [url, adminKey])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const sendPayment = useCallback(async (bolt11) => {
 | 
					 | 
				
			||||||
    const hash = bolt11Tags(bolt11).payment_hash
 | 
					 | 
				
			||||||
    logger.info('sending payment:', `payment_hash=${hash}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const response = await postPayment(url, adminKey, bolt11)
 | 
					 | 
				
			||||||
      const checkResponse = await getPayment(url, adminKey, response.payment_hash)
 | 
					 | 
				
			||||||
      if (!checkResponse.preimage) {
 | 
					 | 
				
			||||||
        throw new Error('No preimage')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const preimage = checkResponse.preimage
 | 
					 | 
				
			||||||
      logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
 | 
					 | 
				
			||||||
      return { preimage }
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [logger, url, adminKey])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const loadConfig = useCallback(async () => {
 | 
					 | 
				
			||||||
    let configStr = window.localStorage.getItem(storageKey)
 | 
					 | 
				
			||||||
    setStatus(Status.Initialized)
 | 
					 | 
				
			||||||
    if (!configStr) {
 | 
					 | 
				
			||||||
      if (me) {
 | 
					 | 
				
			||||||
        // backwards compatibility: try old storageKey
 | 
					 | 
				
			||||||
        const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
 | 
					 | 
				
			||||||
        configStr = migrateLocalStorage(oldStorageKey, storageKey)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (!configStr) {
 | 
					 | 
				
			||||||
        logger.info('no existing config found')
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const config = JSON.parse(configStr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { url, adminKey } = config
 | 
					 | 
				
			||||||
    setUrl(url)
 | 
					 | 
				
			||||||
    setAdminKey(adminKey)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    logger.info(
 | 
					 | 
				
			||||||
      'loaded wallet config: ' +
 | 
					 | 
				
			||||||
      'adminKey=****** ' +
 | 
					 | 
				
			||||||
      `url=${url}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // validate config by trying to fetch wallet
 | 
					 | 
				
			||||||
      logger.info('trying to fetch wallet')
 | 
					 | 
				
			||||||
      await getWallet(url, adminKey)
 | 
					 | 
				
			||||||
      logger.ok('wallet found')
 | 
					 | 
				
			||||||
      setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
      logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('invalid config:', err)
 | 
					 | 
				
			||||||
      setStatus(Status.Error)
 | 
					 | 
				
			||||||
      logger.info('wallet disabled')
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [me, logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const saveConfig = useCallback(async (config) => {
 | 
					 | 
				
			||||||
    // immediately store config so it's not lost even if config is invalid
 | 
					 | 
				
			||||||
    setUrl(config.url)
 | 
					 | 
				
			||||||
    setAdminKey(config.adminKey)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // XXX This is insecure, XSS vulns could lead to loss of funds!
 | 
					 | 
				
			||||||
    //   -> check how mutiny encrypts their wallet and/or check if we can leverage web workers
 | 
					 | 
				
			||||||
    //   https://thenewstack.io/leveraging-web-workers-to-safely-store-access-tokens/
 | 
					 | 
				
			||||||
    window.localStorage.setItem(storageKey, JSON.stringify(config))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    logger.info(
 | 
					 | 
				
			||||||
      'saved wallet config: ' +
 | 
					 | 
				
			||||||
      'adminKey=****** ' +
 | 
					 | 
				
			||||||
      `url=${config.url}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // validate config by trying to fetch wallet
 | 
					 | 
				
			||||||
      logger.info('trying to fetch wallet')
 | 
					 | 
				
			||||||
      await getWallet(config.url, config.adminKey)
 | 
					 | 
				
			||||||
      logger.ok('wallet found')
 | 
					 | 
				
			||||||
      setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
      logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('invalid config:', err)
 | 
					 | 
				
			||||||
      setStatus(Status.Error)
 | 
					 | 
				
			||||||
      logger.info('wallet disabled')
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const clearConfig = useCallback(() => {
 | 
					 | 
				
			||||||
    window.localStorage.removeItem(storageKey)
 | 
					 | 
				
			||||||
    setUrl('')
 | 
					 | 
				
			||||||
    setAdminKey('')
 | 
					 | 
				
			||||||
    setStatus(undefined)
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    loadConfig().catch(console.error)
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const value = useMemo(
 | 
					 | 
				
			||||||
    () => ({ name: 'LNbits', url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment }),
 | 
					 | 
				
			||||||
    [url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment])
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <LNbitsContext.Provider value={value}>
 | 
					 | 
				
			||||||
      {children}
 | 
					 | 
				
			||||||
    </LNbitsContext.Provider>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useLNbits () {
 | 
					 | 
				
			||||||
  return useContext(LNbitsContext)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,215 +0,0 @@
 | 
				
			|||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | 
					 | 
				
			||||||
import { useWalletLogger } from '../logger'
 | 
					 | 
				
			||||||
import LNC from '@lightninglabs/lnc-web'
 | 
					 | 
				
			||||||
import { Status, migrateLocalStorage } from '.'
 | 
					 | 
				
			||||||
import { bolt11Tags } from '@/lib/bolt11'
 | 
					 | 
				
			||||||
import useModal from '../modal'
 | 
					 | 
				
			||||||
import { Form, PasswordInput, SubmitButton } from '../form'
 | 
					 | 
				
			||||||
import CancelButton from '../cancel-button'
 | 
					 | 
				
			||||||
import { Mutex } from 'async-mutex'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
import { useMe } from '../me'
 | 
					 | 
				
			||||||
import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const LNCContext = createContext()
 | 
					 | 
				
			||||||
const mutex = new Mutex()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getLNC ({ me }) {
 | 
					 | 
				
			||||||
  if (window.lnc) return window.lnc
 | 
					 | 
				
			||||||
  // backwards compatibility: migrate to new storage key
 | 
					 | 
				
			||||||
  if (me) migrateLocalStorage('lnc-web:default', `lnc-web:stacker:${me.id}`)
 | 
					 | 
				
			||||||
  window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined })
 | 
					 | 
				
			||||||
  return window.lnc
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// default password if the user hasn't set one
 | 
					 | 
				
			||||||
export const XXX_DEFAULT_PASSWORD = 'password'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function validateNarrowPerms (lnc) {
 | 
					 | 
				
			||||||
  if (!lnc.hasPerms('lnrpc.Lightning.SendPaymentSync')) {
 | 
					 | 
				
			||||||
    throw new Error('missing permission: lnrpc.Lightning.SendPaymentSync')
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (lnc.hasPerms('lnrpc.Lightning.SendCoins')) {
 | 
					 | 
				
			||||||
    throw new Error('too broad permission: lnrpc.Wallet.SendCoins')
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // TODO: need to check for more narrow permissions
 | 
					 | 
				
			||||||
  // blocked by https://github.com/lightninglabs/lnc-web/issues/112
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function LNCProvider ({ children }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const { logger } = useWalletLogger(Wallet.LNC)
 | 
					 | 
				
			||||||
  const [config, setConfig] = useState({})
 | 
					 | 
				
			||||||
  const [lnc, setLNC] = useState()
 | 
					 | 
				
			||||||
  const [status, setStatus] = useState()
 | 
					 | 
				
			||||||
  const [modal, showModal] = useModal()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getInfo = useCallback(async () => {
 | 
					 | 
				
			||||||
    logger.info('getInfo called')
 | 
					 | 
				
			||||||
    return await lnc.lightning.getInfo()
 | 
					 | 
				
			||||||
  }, [logger, lnc])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const unlock = useCallback(async (connect) => {
 | 
					 | 
				
			||||||
    if (status === Status.Enabled) return config.password
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return await new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      const cancelAndReject = async () => {
 | 
					 | 
				
			||||||
        reject(new Error('password canceled'))
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      showModal(onClose => {
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          <Form
 | 
					 | 
				
			||||||
            initial={{
 | 
					 | 
				
			||||||
              password: ''
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            onSubmit={async (values) => {
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                lnc.credentials.password = values?.password
 | 
					 | 
				
			||||||
                setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
                setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: values.password })
 | 
					 | 
				
			||||||
                logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
                onClose()
 | 
					 | 
				
			||||||
                resolve(values.password)
 | 
					 | 
				
			||||||
              } catch (err) {
 | 
					 | 
				
			||||||
                logger.error('failed attempt to unlock wallet', err)
 | 
					 | 
				
			||||||
                throw err
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <h4 className='text-center mb-3'>Unlock LNC</h4>
 | 
					 | 
				
			||||||
            <PasswordInput
 | 
					 | 
				
			||||||
              label='password'
 | 
					 | 
				
			||||||
              name='password'
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <div className='mt-5 d-flex justify-content-between'>
 | 
					 | 
				
			||||||
              <CancelButton onClick={() => { onClose(); cancelAndReject() }} />
 | 
					 | 
				
			||||||
              <SubmitButton variant='primary'>unlock</SubmitButton>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </Form>
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      }, { onClose: cancelAndReject })
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [logger, showModal, setConfig, lnc, status])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const sendPayment = useCallback(async (bolt11) => {
 | 
					 | 
				
			||||||
    const hash = bolt11Tags(bolt11).payment_hash
 | 
					 | 
				
			||||||
    logger.info('sending payment:', `payment_hash=${hash}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return await mutex.runExclusive(async () => {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const password = await unlock()
 | 
					 | 
				
			||||||
        // credentials need to be decrypted before connecting after a disconnect
 | 
					 | 
				
			||||||
        lnc.credentials.password = password || XXX_DEFAULT_PASSWORD
 | 
					 | 
				
			||||||
        await lnc.connect()
 | 
					 | 
				
			||||||
        const { paymentError, paymentPreimage: preimage } =
 | 
					 | 
				
			||||||
          await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (paymentError) throw new Error(paymentError)
 | 
					 | 
				
			||||||
        if (!preimage) throw new Error('No preimage in response')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
 | 
					 | 
				
			||||||
        return { preimage }
 | 
					 | 
				
			||||||
      } catch (err) {
 | 
					 | 
				
			||||||
        const msg = err.message || err.toString?.()
 | 
					 | 
				
			||||||
        logger.error('payment failed:', `payment_hash=${hash}`, msg)
 | 
					 | 
				
			||||||
        if (msg.includes('invoice expired')) {
 | 
					 | 
				
			||||||
          throw new InvoiceExpiredError(hash)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (msg.includes('canceled')) {
 | 
					 | 
				
			||||||
          throw new InvoiceCanceledError(hash)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        throw err
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          lnc.disconnect()
 | 
					 | 
				
			||||||
          logger.info('disconnecting after:', `payment_hash=${hash}`)
 | 
					 | 
				
			||||||
          // wait for lnc to disconnect before releasing the mutex
 | 
					 | 
				
			||||||
          await new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
            let counter = 0
 | 
					 | 
				
			||||||
            const interval = setInterval(() => {
 | 
					 | 
				
			||||||
              if (lnc.isConnected) {
 | 
					 | 
				
			||||||
                if (counter++ > 100) {
 | 
					 | 
				
			||||||
                  logger.error('failed to disconnect from lnc')
 | 
					 | 
				
			||||||
                  clearInterval(interval)
 | 
					 | 
				
			||||||
                  reject(new Error('failed to disconnect from lnc'))
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              clearInterval(interval)
 | 
					 | 
				
			||||||
              resolve()
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
          }, 50)
 | 
					 | 
				
			||||||
        } catch (err) {
 | 
					 | 
				
			||||||
          logger.error('failed to disconnect from lnc', err)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [logger, lnc, unlock])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const saveConfig = useCallback(async config => {
 | 
					 | 
				
			||||||
    setConfig(config)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      lnc.credentials.pairingPhrase = config.pairingPhrase
 | 
					 | 
				
			||||||
      await lnc.connect()
 | 
					 | 
				
			||||||
      await validateNarrowPerms(lnc)
 | 
					 | 
				
			||||||
      lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
 | 
					 | 
				
			||||||
      setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
      logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('invalid config:', err)
 | 
					 | 
				
			||||||
      setStatus(Status.Error)
 | 
					 | 
				
			||||||
      logger.info('wallet disabled')
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      lnc.disconnect()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [logger, lnc])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const clearConfig = useCallback(async () => {
 | 
					 | 
				
			||||||
    await lnc.credentials.clear(false)
 | 
					 | 
				
			||||||
    if (lnc.isConnected) lnc.disconnect()
 | 
					 | 
				
			||||||
    setStatus(undefined)
 | 
					 | 
				
			||||||
    setConfig({})
 | 
					 | 
				
			||||||
    logger.info('cleared config')
 | 
					 | 
				
			||||||
  }, [logger, lnc])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    (async () => {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const lnc = await getLNC({ me })
 | 
					 | 
				
			||||||
        setLNC(lnc)
 | 
					 | 
				
			||||||
        setStatus(Status.Initialized)
 | 
					 | 
				
			||||||
        if (lnc.credentials.isPaired) {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            // try the default password
 | 
					 | 
				
			||||||
            lnc.credentials.password = XXX_DEFAULT_PASSWORD
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            setStatus(Status.Locked)
 | 
					 | 
				
			||||||
            logger.info('wallet needs password before enabling')
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
          setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: lnc.credentials.password })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } catch (err) {
 | 
					 | 
				
			||||||
        logger.error('wallet could not be loaded:', err)
 | 
					 | 
				
			||||||
        setStatus(Status.Error)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })()
 | 
					 | 
				
			||||||
  }, [me, setStatus, setConfig, logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const value = useMemo(
 | 
					 | 
				
			||||||
    () => ({ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }),
 | 
					 | 
				
			||||||
    [status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig])
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <LNCContext.Provider value={value}>
 | 
					 | 
				
			||||||
      {children}
 | 
					 | 
				
			||||||
      {modal}
 | 
					 | 
				
			||||||
    </LNCContext.Provider>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useLNC () {
 | 
					 | 
				
			||||||
  return useContext(LNCContext)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,288 +0,0 @@
 | 
				
			|||||||
// https://github.com/getAlby/js-sdk/blob/master/src/webln/NostrWeblnProvider.ts
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | 
					 | 
				
			||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
 | 
					 | 
				
			||||||
import { parseNwcUrl } from '@/lib/url'
 | 
					 | 
				
			||||||
import { useWalletLogger } from '../logger'
 | 
					 | 
				
			||||||
import { Status, migrateLocalStorage } from '.'
 | 
					 | 
				
			||||||
import { bolt11Tags } from '@/lib/bolt11'
 | 
					 | 
				
			||||||
import { JIT_INVOICE_TIMEOUT_MS, Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
import { useMe } from '../me'
 | 
					 | 
				
			||||||
import { InvoiceExpiredError } from '../payment'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NWCContext = createContext()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function NWCProvider ({ children }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const [nwcUrl, setNwcUrl] = useState('')
 | 
					 | 
				
			||||||
  const [walletPubkey, setWalletPubkey] = useState()
 | 
					 | 
				
			||||||
  const [relayUrl, setRelayUrl] = useState()
 | 
					 | 
				
			||||||
  const [secret, setSecret] = useState()
 | 
					 | 
				
			||||||
  const [status, setStatus] = useState()
 | 
					 | 
				
			||||||
  const { logger } = useWalletLogger(Wallet.NWC)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let storageKey = 'webln:provider:nwc'
 | 
					 | 
				
			||||||
  if (me) {
 | 
					 | 
				
			||||||
    storageKey = `${storageKey}:${me.id}`
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getInfo = useCallback(async (relayUrl, walletPubkey) => {
 | 
					 | 
				
			||||||
    logger.info(`requesting info event from ${relayUrl}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let relay, sub
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      relay = await Relay.connect(relayUrl).catch(() => {
 | 
					 | 
				
			||||||
        // NOTE: passed error is undefined for some reason
 | 
					 | 
				
			||||||
        const msg = `failed to connect to ${relayUrl}`
 | 
					 | 
				
			||||||
        logger.error(msg)
 | 
					 | 
				
			||||||
        throw new Error(msg)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      logger.ok(`connected to ${relayUrl}`)
 | 
					 | 
				
			||||||
      return await new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
        const timeout = 5000
 | 
					 | 
				
			||||||
        const timer = setTimeout(() => {
 | 
					 | 
				
			||||||
          const msg = 'timeout waiting for info event'
 | 
					 | 
				
			||||||
          logger.error(msg)
 | 
					 | 
				
			||||||
          reject(new Error(msg))
 | 
					 | 
				
			||||||
          sub?.close()
 | 
					 | 
				
			||||||
        }, timeout)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let found = false
 | 
					 | 
				
			||||||
        sub = relay.subscribe([
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            kinds: [13194],
 | 
					 | 
				
			||||||
            authors: [walletPubkey]
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ], {
 | 
					 | 
				
			||||||
          onevent (event) {
 | 
					 | 
				
			||||||
            clearTimeout(timer)
 | 
					 | 
				
			||||||
            found = true
 | 
					 | 
				
			||||||
            logger.ok(`received info event from ${relayUrl}`)
 | 
					 | 
				
			||||||
            resolve(event)
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose (reason) {
 | 
					 | 
				
			||||||
            clearTimeout(timer)
 | 
					 | 
				
			||||||
            if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
 | 
					 | 
				
			||||||
              // only log if not closed by us (caller)
 | 
					 | 
				
			||||||
              const msg = 'connection closed: ' + (reason || 'unknown reason')
 | 
					 | 
				
			||||||
              logger.error(msg)
 | 
					 | 
				
			||||||
              reject(new Error(msg))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          oneose () {
 | 
					 | 
				
			||||||
            clearTimeout(timer)
 | 
					 | 
				
			||||||
            if (!found) {
 | 
					 | 
				
			||||||
              const msg = 'EOSE received without info event'
 | 
					 | 
				
			||||||
              logger.error(msg)
 | 
					 | 
				
			||||||
              reject(new Error(msg))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            sub?.close()
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      // For some reason, websocket is already in CLOSING or CLOSED state.
 | 
					 | 
				
			||||||
      // relay?.close()
 | 
					 | 
				
			||||||
      if (relay) logger.info(`closed connection to ${relayUrl}`)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const validateParams = useCallback(async ({ relayUrl, walletPubkey }) => {
 | 
					 | 
				
			||||||
    // validate connection by fetching info event
 | 
					 | 
				
			||||||
    // function needs to throw an error for formik validation to fail
 | 
					 | 
				
			||||||
    const event = await getInfo(relayUrl, walletPubkey)
 | 
					 | 
				
			||||||
    const supported = event.content.split(/[\s,]+/) // handle both spaces and commas
 | 
					 | 
				
			||||||
    logger.info('supported methods:', supported)
 | 
					 | 
				
			||||||
    if (!supported.includes('pay_invoice')) {
 | 
					 | 
				
			||||||
      const msg = 'wallet does not support pay_invoice'
 | 
					 | 
				
			||||||
      logger.error(msg)
 | 
					 | 
				
			||||||
      throw new Error(msg)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    logger.ok('wallet supports pay_invoice')
 | 
					 | 
				
			||||||
  }, [logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const loadConfig = useCallback(async () => {
 | 
					 | 
				
			||||||
    let configStr = window.localStorage.getItem(storageKey)
 | 
					 | 
				
			||||||
    setStatus(Status.Initialized)
 | 
					 | 
				
			||||||
    if (!configStr) {
 | 
					 | 
				
			||||||
      if (me) {
 | 
					 | 
				
			||||||
        // backwards compatibility: try old storageKey
 | 
					 | 
				
			||||||
        const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
 | 
					 | 
				
			||||||
        configStr = migrateLocalStorage(oldStorageKey, storageKey)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (!configStr) {
 | 
					 | 
				
			||||||
        logger.info('no existing config found')
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const config = JSON.parse(configStr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { nwcUrl } = config
 | 
					 | 
				
			||||||
    setNwcUrl(nwcUrl)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const params = parseNwcUrl(nwcUrl)
 | 
					 | 
				
			||||||
    setRelayUrl(params.relayUrl)
 | 
					 | 
				
			||||||
    setWalletPubkey(params.walletPubkey)
 | 
					 | 
				
			||||||
    setSecret(params.secret)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    logger.info(
 | 
					 | 
				
			||||||
      'loaded wallet config: ' +
 | 
					 | 
				
			||||||
      'secret=****** ' +
 | 
					 | 
				
			||||||
      `pubkey=${params.walletPubkey.slice(0, 6)}..${params.walletPubkey.slice(-6)} ` +
 | 
					 | 
				
			||||||
      `relay=${params.relayUrl}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await validateParams(params)
 | 
					 | 
				
			||||||
      setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
      logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('invalid config:', err)
 | 
					 | 
				
			||||||
      setStatus(Status.Error)
 | 
					 | 
				
			||||||
      logger.info('wallet disabled')
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [me, validateParams, logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const saveConfig = useCallback(async (config) => {
 | 
					 | 
				
			||||||
    // immediately store config so it's not lost even if config is invalid
 | 
					 | 
				
			||||||
    const { nwcUrl } = config
 | 
					 | 
				
			||||||
    setNwcUrl(nwcUrl)
 | 
					 | 
				
			||||||
    if (!nwcUrl) {
 | 
					 | 
				
			||||||
      setStatus(undefined)
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const params = parseNwcUrl(nwcUrl)
 | 
					 | 
				
			||||||
    setRelayUrl(params.relayUrl)
 | 
					 | 
				
			||||||
    setWalletPubkey(params.walletPubkey)
 | 
					 | 
				
			||||||
    setSecret(params.secret)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // XXX Even though NWC allows to configure budget,
 | 
					 | 
				
			||||||
    // this is definitely not ideal from a security perspective.
 | 
					 | 
				
			||||||
    window.localStorage.setItem(storageKey, JSON.stringify(config))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    logger.info(
 | 
					 | 
				
			||||||
      'saved wallet config: ' +
 | 
					 | 
				
			||||||
      'secret=****** ' +
 | 
					 | 
				
			||||||
      `pubkey=${params.walletPubkey.slice(0, 6)}..${params.walletPubkey.slice(-6)} ` +
 | 
					 | 
				
			||||||
      `relay=${params.relayUrl}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await validateParams(params)
 | 
					 | 
				
			||||||
      setStatus(Status.Enabled)
 | 
					 | 
				
			||||||
      logger.ok('wallet enabled')
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('invalid config:', err)
 | 
					 | 
				
			||||||
      setStatus(Status.Error)
 | 
					 | 
				
			||||||
      logger.info('wallet disabled')
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [validateParams, logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const clearConfig = useCallback(() => {
 | 
					 | 
				
			||||||
    window.localStorage.removeItem(storageKey)
 | 
					 | 
				
			||||||
    setNwcUrl('')
 | 
					 | 
				
			||||||
    setRelayUrl(undefined)
 | 
					 | 
				
			||||||
    setWalletPubkey(undefined)
 | 
					 | 
				
			||||||
    setSecret(undefined)
 | 
					 | 
				
			||||||
    setStatus(undefined)
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const sendPayment = useCallback(async (bolt11) => {
 | 
					 | 
				
			||||||
    const hash = bolt11Tags(bolt11).payment_hash
 | 
					 | 
				
			||||||
    logger.info('sending payment:', `payment_hash=${hash}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let relay, sub
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      relay = await Relay.connect(relayUrl).catch(() => {
 | 
					 | 
				
			||||||
        // NOTE: passed error is undefined for some reason
 | 
					 | 
				
			||||||
        const msg = `failed to connect to ${relayUrl}`
 | 
					 | 
				
			||||||
        logger.error(msg)
 | 
					 | 
				
			||||||
        throw new Error(msg)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      logger.ok(`connected to ${relayUrl}`)
 | 
					 | 
				
			||||||
      const ret = await new Promise(function (resolve, reject) {
 | 
					 | 
				
			||||||
        (async function () {
 | 
					 | 
				
			||||||
          // timeout since NWC is async (user needs to confirm payment in wallet)
 | 
					 | 
				
			||||||
          // timeout is same as invoice expiry
 | 
					 | 
				
			||||||
          const timeout = JIT_INVOICE_TIMEOUT_MS
 | 
					 | 
				
			||||||
          const timer = setTimeout(() => {
 | 
					 | 
				
			||||||
            const msg = 'timeout waiting for payment'
 | 
					 | 
				
			||||||
            logger.error(msg)
 | 
					 | 
				
			||||||
            reject(new InvoiceExpiredError(hash))
 | 
					 | 
				
			||||||
            sub?.close()
 | 
					 | 
				
			||||||
          }, timeout)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const payload = {
 | 
					 | 
				
			||||||
            method: 'pay_invoice',
 | 
					 | 
				
			||||||
            params: { invoice: bolt11 }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const request = finalizeEvent({
 | 
					 | 
				
			||||||
            kind: 23194,
 | 
					 | 
				
			||||||
            created_at: Math.floor(Date.now() / 1000),
 | 
					 | 
				
			||||||
            tags: [['p', walletPubkey]],
 | 
					 | 
				
			||||||
            content
 | 
					 | 
				
			||||||
          }, secret)
 | 
					 | 
				
			||||||
          await relay.publish(request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const filter = {
 | 
					 | 
				
			||||||
            kinds: [23195],
 | 
					 | 
				
			||||||
            authors: [walletPubkey],
 | 
					 | 
				
			||||||
            '#e': [request.id]
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          sub = relay.subscribe([filter], {
 | 
					 | 
				
			||||||
            async onevent (response) {
 | 
					 | 
				
			||||||
              clearTimeout(timer)
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
 | 
					 | 
				
			||||||
                if (content.error) return reject(new Error(content.error.message))
 | 
					 | 
				
			||||||
                if (content.result) return resolve({ preimage: content.result.preimage })
 | 
					 | 
				
			||||||
              } catch (err) {
 | 
					 | 
				
			||||||
                return reject(err)
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            onclose (reason) {
 | 
					 | 
				
			||||||
              clearTimeout(timer)
 | 
					 | 
				
			||||||
              if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
 | 
					 | 
				
			||||||
                // only log if not closed by us (caller)
 | 
					 | 
				
			||||||
                const msg = 'connection closed: ' + (reason || 'unknown reason')
 | 
					 | 
				
			||||||
                logger.error(msg)
 | 
					 | 
				
			||||||
                reject(new Error(msg))
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        })().catch(reject)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      const preimage = ret.preimage
 | 
					 | 
				
			||||||
      logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
 | 
					 | 
				
			||||||
      return ret
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      // For some reason, websocket is already in CLOSING or CLOSED state.
 | 
					 | 
				
			||||||
      // relay?.close()
 | 
					 | 
				
			||||||
      if (relay) logger.info(`closed connection to ${relayUrl}`)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [walletPubkey, relayUrl, secret, logger])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    loadConfig().catch(err => logger.error(err.message || err.toString?.()))
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const value = useMemo(
 | 
					 | 
				
			||||||
    () => ({ name: 'NWC', nwcUrl, relayUrl, walletPubkey, secret, status, saveConfig, clearConfig, getInfo, sendPayment }),
 | 
					 | 
				
			||||||
    [nwcUrl, relayUrl, walletPubkey, secret, status, saveConfig, clearConfig, getInfo, sendPayment])
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <NWCContext.Provider value={value}>
 | 
					 | 
				
			||||||
      {children}
 | 
					 | 
				
			||||||
    </NWCContext.Provider>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useNWC () {
 | 
					 | 
				
			||||||
  return useContext(NWCContext)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,137 +0,0 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					 | 
				
			||||||
import { Form, Input } from '@/components/form'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import { useMe } from '@/components/me'
 | 
					 | 
				
			||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
 | 
					 | 
				
			||||||
import { useApolloClient, useMutation } from '@apollo/client'
 | 
					 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { CLNAutowithdrawSchema } from '@/lib/validate'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
 | 
					 | 
				
			||||||
import { REMOVE_WALLET, UPSERT_WALLET_CLN, WALLET_BY_TYPE } from '@/fragments/wallet'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					 | 
				
			||||||
import Info from '@/components/info'
 | 
					 | 
				
			||||||
import Text from '@/components/text'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const variables = { type: Wallet.CLN.type }
 | 
					 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function CLN ({ ssrData }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const client = useApolloClient()
 | 
					 | 
				
			||||||
  const [upsertWalletCLN] = useMutation(UPSERT_WALLET_CLN, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  const [removeWallet] = useMutation(REMOVE_WALLET, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { walletByType: wallet } = ssrData || {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <CenterLayout>
 | 
					 | 
				
			||||||
      <h2 className='pb-2'>CLN</h2>
 | 
					 | 
				
			||||||
      <h6 className='text-muted text-center'>autowithdraw to your Core Lightning node via <a href='https://docs.corelightning.org/docs/rest' target='_blank' noreferrer rel='noreferrer'>CLNRest</a></h6>
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        initial={{
 | 
					 | 
				
			||||||
          socket: wallet?.wallet?.socket || '',
 | 
					 | 
				
			||||||
          rune: wallet?.wallet?.rune || '',
 | 
					 | 
				
			||||||
          cert: wallet?.wallet?.cert || '',
 | 
					 | 
				
			||||||
          ...autowithdrawInitial({ me, priority: wallet?.priority })
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        schema={CLNAutowithdrawSchema({ me })}
 | 
					 | 
				
			||||||
        onSubmit={async ({ socket, rune, cert, ...settings }) => {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await upsertWalletCLN({
 | 
					 | 
				
			||||||
              variables: {
 | 
					 | 
				
			||||||
                id: wallet?.id,
 | 
					 | 
				
			||||||
                socket,
 | 
					 | 
				
			||||||
                rune,
 | 
					 | 
				
			||||||
                cert,
 | 
					 | 
				
			||||||
                settings: {
 | 
					 | 
				
			||||||
                  ...settings,
 | 
					 | 
				
			||||||
                  autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
 | 
					 | 
				
			||||||
                  autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            console.error(err)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label='rest host and port'
 | 
					 | 
				
			||||||
          name='socket'
 | 
					 | 
				
			||||||
          hint='tor or clearnet'
 | 
					 | 
				
			||||||
          placeholder='55.5.555.55:3010'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
          autoFocus
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label={
 | 
					 | 
				
			||||||
            <div className='d-flex align-items-center'>invoice only rune
 | 
					 | 
				
			||||||
              <Info>
 | 
					 | 
				
			||||||
                <Text>
 | 
					 | 
				
			||||||
                  {'We only accept runes that *only* allow `method=invoice`.\nRun this to generate one:\n\n```lightning-cli createrune restrictions=\'["method=invoice"]\'```'}
 | 
					 | 
				
			||||||
                </Text>
 | 
					 | 
				
			||||||
              </Info>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          name='rune'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          hint='must be restricted to method=invoice'
 | 
					 | 
				
			||||||
          placeholder='S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ=='
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label={<>cert <small className='text-muted ms-2'>optional if from <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (e.g. voltage)</small></>}
 | 
					 | 
				
			||||||
          name='cert'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          hint='hex or base64 encoded'
 | 
					 | 
				
			||||||
          placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <AutowithdrawSettings />
 | 
					 | 
				
			||||||
        <WalletButtonBar
 | 
					 | 
				
			||||||
          status={!!wallet} onDelete={async () => {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await removeWallet({ variables: { id: wallet?.id } })
 | 
					 | 
				
			||||||
              toaster.success('saved settings')
 | 
					 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              console.error(err)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Form>
 | 
					 | 
				
			||||||
      <div className='mt-3 w-100'>
 | 
					 | 
				
			||||||
        <WalletLogs wallet={Wallet.CLN} embedded />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </CenterLayout>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function CLNCard ({ wallet }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <WalletCard
 | 
					 | 
				
			||||||
      title='CLN'
 | 
					 | 
				
			||||||
      badges={['receive only', 'non-custodial']}
 | 
					 | 
				
			||||||
      provider='cln'
 | 
					 | 
				
			||||||
      status={wallet !== undefined || undefined}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -2,28 +2,18 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
				
			|||||||
import Layout from '@/components/layout'
 | 
					import Layout from '@/components/layout'
 | 
				
			||||||
import styles from '@/styles/wallet.module.css'
 | 
					import styles from '@/styles/wallet.module.css'
 | 
				
			||||||
import { WalletCard } from '@/components/wallet-card'
 | 
					import { WalletCard } from '@/components/wallet-card'
 | 
				
			||||||
import { LightningAddressWalletCard } from './lightning-address'
 | 
					 | 
				
			||||||
import { LNbitsCard } from './lnbits'
 | 
					 | 
				
			||||||
import { NWCCard } from './nwc'
 | 
					 | 
				
			||||||
import { LNDCard } from './lnd'
 | 
					 | 
				
			||||||
import { CLNCard } from './cln'
 | 
					 | 
				
			||||||
import { WALLETS } from '@/fragments/wallet'
 | 
					import { WALLETS } from '@/fragments/wallet'
 | 
				
			||||||
import { useQuery } from '@apollo/client'
 | 
					 | 
				
			||||||
import PageLoading from '@/components/page-loading'
 | 
					 | 
				
			||||||
import { LNCCard } from './lnc'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
import { Wallet as W } from '@/lib/constants'
 | 
					
 | 
				
			||||||
 | 
					const wallets = [
 | 
				
			||||||
 | 
					  await import('@/components/webln/lnbits')
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
 | 
					export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Wallet ({ ssrData }) {
 | 
					export default function Wallet ({ ssrData }) {
 | 
				
			||||||
  const { data } = useQuery(WALLETS)
 | 
					  // TODO: set wallet status
 | 
				
			||||||
 | 
					  // TODO: load server wallets
 | 
				
			||||||
  if (!data && !ssrData) return <PageLoading />
 | 
					 | 
				
			||||||
  const { wallets } = data || ssrData
 | 
					 | 
				
			||||||
  const lnd = wallets.find(w => w.type === W.LND.type)
 | 
					 | 
				
			||||||
  const lnaddr = wallets.find(w => w.type === W.LnAddr.type)
 | 
					 | 
				
			||||||
  const cln = wallets.find(w => w.type === W.CLN.type)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout>
 | 
					    <Layout>
 | 
				
			||||||
@ -36,15 +26,9 @@ export default function Wallet ({ ssrData }) {
 | 
				
			|||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div className={styles.walletGrid}>
 | 
					        <div className={styles.walletGrid}>
 | 
				
			||||||
          <LightningAddressWalletCard wallet={lnaddr} />
 | 
					          {wallets.map((w, i) => (
 | 
				
			||||||
          <LNDCard wallet={lnd} />
 | 
					            <WalletCard key={i} {...w.config.card} />
 | 
				
			||||||
          <CLNCard wallet={cln} />
 | 
					          ))}
 | 
				
			||||||
          <LNbitsCard />
 | 
					 | 
				
			||||||
          <NWCCard />
 | 
					 | 
				
			||||||
          <LNCCard />
 | 
					 | 
				
			||||||
          <WalletCard title='coming soon' badges={['probably']} />
 | 
					 | 
				
			||||||
          <WalletCard title='coming soon' badges={['we hope']} />
 | 
					 | 
				
			||||||
          <WalletCard title='coming soon' badges={['tm']} />
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </Layout>
 | 
					    </Layout>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,106 +0,0 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					 | 
				
			||||||
import { Form, Input } from '@/components/form'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import { useMe } from '@/components/me'
 | 
					 | 
				
			||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
 | 
					 | 
				
			||||||
import { useApolloClient, useMutation } from '@apollo/client'
 | 
					 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { lnAddrAutowithdrawSchema } from '@/lib/validate'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
 | 
					 | 
				
			||||||
import { REMOVE_WALLET, UPSERT_WALLET_LNADDR, WALLET_BY_TYPE } from '@/fragments/wallet'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const variables = { type: Wallet.LnAddr.type }
 | 
					 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function LightningAddress ({ ssrData }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const client = useApolloClient()
 | 
					 | 
				
			||||||
  const [upsertWalletLNAddr] = useMutation(UPSERT_WALLET_LNADDR, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  const [removeWallet] = useMutation(REMOVE_WALLET, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { walletByType: wallet } = ssrData || {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <CenterLayout>
 | 
					 | 
				
			||||||
      <h2 className='pb-2'>lightning address</h2>
 | 
					 | 
				
			||||||
      <h6 className='text-muted text-center pb-3'>autowithdraw to a lightning address</h6>
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        initial={{
 | 
					 | 
				
			||||||
          address: wallet?.wallet?.address || '',
 | 
					 | 
				
			||||||
          ...autowithdrawInitial({ me, priority: wallet?.priority })
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        schema={lnAddrAutowithdrawSchema({ me })}
 | 
					 | 
				
			||||||
        onSubmit={async ({ address, ...settings }) => {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await upsertWalletLNAddr({
 | 
					 | 
				
			||||||
              variables: {
 | 
					 | 
				
			||||||
                id: wallet?.id,
 | 
					 | 
				
			||||||
                address,
 | 
					 | 
				
			||||||
                settings: {
 | 
					 | 
				
			||||||
                  ...settings,
 | 
					 | 
				
			||||||
                  autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
 | 
					 | 
				
			||||||
                  autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            console.error(err)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label='lightning address'
 | 
					 | 
				
			||||||
          name='address'
 | 
					 | 
				
			||||||
          autoComplete='off'
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
          autoFocus
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <AutowithdrawSettings />
 | 
					 | 
				
			||||||
        <WalletButtonBar
 | 
					 | 
				
			||||||
          status={!!wallet} onDelete={async () => {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await removeWallet({ variables: { id: wallet?.id } })
 | 
					 | 
				
			||||||
              toaster.success('saved settings')
 | 
					 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              console.error(err)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Form>
 | 
					 | 
				
			||||||
      <div className='mt-3 w-100'>
 | 
					 | 
				
			||||||
        <WalletLogs wallet={Wallet.LnAddr} embedded />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </CenterLayout>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function LightningAddressWalletCard ({ wallet }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <WalletCard
 | 
					 | 
				
			||||||
      title='lightning address'
 | 
					 | 
				
			||||||
      badges={['receive only', 'non-custodialish']}
 | 
					 | 
				
			||||||
      provider='lightning-address'
 | 
					 | 
				
			||||||
      status={wallet !== undefined || undefined}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,27 +1,32 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
				
			||||||
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
 | 
					import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					import { CenterLayout } from '@/components/layout'
 | 
				
			||||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
 | 
					import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
 | 
				
			||||||
import { lnbitsSchema } from '@/lib/validate'
 | 
					import { lnbitsSchema } from '@/lib/validate'
 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { useLNbits } from '@/components/webln/lnbits'
 | 
					import { useLNbits } from '@/components/webln/lnbits'
 | 
				
			||||||
import { WalletSecurityBanner } from '@/components/banners'
 | 
					import { WalletSecurityBanner } from '@/components/banners'
 | 
				
			||||||
import { useWebLNConfigurator } from '@/components/webln'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					import WalletLogs from '@/components/wallet-logs'
 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					import { Wallet } from '@/lib/constants'
 | 
				
			||||||
 | 
					import useLocalState from '@/components/use-local-state'
 | 
				
			||||||
 | 
					import { useMe } from '@/components/me'
 | 
				
			||||||
 | 
					import { useToast } from '@/components/toast'
 | 
				
			||||||
 | 
					import { useRouter } from 'next/router'
 | 
				
			||||||
 | 
					import { Status } from '@/components/webln'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
					export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function LNbits () {
 | 
					export default function LNbits () {
 | 
				
			||||||
  const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
 | 
					  const me = useMe()
 | 
				
			||||||
  const lnbits = useLNbits()
 | 
					 | 
				
			||||||
  const { name, url, adminKey, saveConfig, clearConfig, status } = lnbits
 | 
					 | 
				
			||||||
  const isDefault = provider?.name === name
 | 
					 | 
				
			||||||
  const configured = isConfigured(status)
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					  const toaster = useToast()
 | 
				
			||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let storageKey = 'webln:provider:lnbits'
 | 
				
			||||||
 | 
					  if (me) {
 | 
				
			||||||
 | 
					    storageKey = `${storageKey}:${me.id}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const [config, setConfig, clearConfig] = useLocalState(storageKey)
 | 
				
			||||||
 | 
					  const status = config ? Status.Initialized : undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <CenterLayout>
 | 
					    <CenterLayout>
 | 
				
			||||||
      <h2 className='pb-2'>LNbits</h2>
 | 
					      <h2 className='pb-2'>LNbits</h2>
 | 
				
			||||||
@ -29,15 +34,14 @@ export default function LNbits () {
 | 
				
			|||||||
      <WalletSecurityBanner />
 | 
					      <WalletSecurityBanner />
 | 
				
			||||||
      <Form
 | 
					      <Form
 | 
				
			||||||
        initial={{
 | 
					        initial={{
 | 
				
			||||||
          url: url || '',
 | 
					          url: config?.url || '',
 | 
				
			||||||
          adminKey: adminKey || '',
 | 
					          adminKey: config?.adminKey || '',
 | 
				
			||||||
          isDefault: isDefault || false
 | 
					          isDefault: config?.isDefault || false
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        schema={lnbitsSchema}
 | 
					        schema={lnbitsSchema}
 | 
				
			||||||
        onSubmit={async ({ isDefault, ...values }) => {
 | 
					        onSubmit={async ({ isDefault, ...values }) => {
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            await saveConfig(values)
 | 
					            await setConfig(values)
 | 
				
			||||||
            if (isDefault) setProvider(lnbits)
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					            toaster.success('saved settings')
 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					            router.push('/settings/wallets')
 | 
				
			||||||
          } catch (err) {
 | 
					          } catch (err) {
 | 
				
			||||||
@ -47,29 +51,29 @@ export default function LNbits () {
 | 
				
			|||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ClientInput
 | 
					        <ClientInput
 | 
				
			||||||
          initialValue={url}
 | 
					          initialValue={config?.url}
 | 
				
			||||||
          label='lnbits url'
 | 
					          label='lnbits url'
 | 
				
			||||||
          name='url'
 | 
					          name='url'
 | 
				
			||||||
          required
 | 
					          required
 | 
				
			||||||
          autoFocus
 | 
					          autoFocus
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <PasswordInput
 | 
					        <PasswordInput
 | 
				
			||||||
          initialValue={adminKey}
 | 
					          initialValue={config?.adminKey}
 | 
				
			||||||
          label='admin key'
 | 
					          label='admin key'
 | 
				
			||||||
          name='adminKey'
 | 
					          name='adminKey'
 | 
				
			||||||
          newPass
 | 
					          newPass
 | 
				
			||||||
          required
 | 
					          required
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <ClientCheckbox
 | 
					        <ClientCheckbox
 | 
				
			||||||
          disabled={!configured || isDefault || enabledProviders.length === 1}
 | 
					          disabled={false}
 | 
				
			||||||
          initialValue={isDefault}
 | 
					          initialValue={false}
 | 
				
			||||||
          label='default payment method'
 | 
					          label='default payment method'
 | 
				
			||||||
          name='isDefault'
 | 
					          name='isDefault'
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <WalletButtonBar
 | 
					        <WalletButtonBar
 | 
				
			||||||
          status={status} onDelete={async () => {
 | 
					          status={status} onDelete={async () => {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              await clearConfig()
 | 
					              clearConfig()
 | 
				
			||||||
              toaster.success('saved settings')
 | 
					              toaster.success('saved settings')
 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					              router.push('/settings/wallets')
 | 
				
			||||||
            } catch (err) {
 | 
					            } catch (err) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,122 +0,0 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					 | 
				
			||||||
import { WalletSecurityBanner } from '@/components/banners'
 | 
					 | 
				
			||||||
import { ClientCheckbox, Form, PasswordInput } from '@/components/form'
 | 
					 | 
				
			||||||
import Info from '@/components/info'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import Text from '@/components/text'
 | 
					 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					 | 
				
			||||||
import { Status, useWebLNConfigurator } from '@/components/webln'
 | 
					 | 
				
			||||||
import { XXX_DEFAULT_PASSWORD, useLNC } from '@/components/webln/lnc'
 | 
					 | 
				
			||||||
import { lncSchema } from '@/lib/validate'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { useEffect, useRef } from 'react'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function LNC () {
 | 
					 | 
				
			||||||
  const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const lnc = useLNC()
 | 
					 | 
				
			||||||
  const { status, clearConfig, saveConfig, config, name, unlock } = lnc
 | 
					 | 
				
			||||||
  const isDefault = provider?.name === name
 | 
					 | 
				
			||||||
  const unlocking = useRef(false)
 | 
					 | 
				
			||||||
  const configured = isConfigured(status)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (!unlocking.current && status === Status.Locked) {
 | 
					 | 
				
			||||||
      unlocking.current = true
 | 
					 | 
				
			||||||
      unlock()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [status, unlock])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const defaultPassword = config?.password === XXX_DEFAULT_PASSWORD
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <CenterLayout>
 | 
					 | 
				
			||||||
      <h2>Lightning Node Connect for LND</h2>
 | 
					 | 
				
			||||||
      <h6 className='text-muted text-center pb-3'>use Lightning Node Connect for LND payments</h6>
 | 
					 | 
				
			||||||
      <WalletSecurityBanner />
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        initial={{
 | 
					 | 
				
			||||||
          pairingPhrase: config?.pairingPhrase || '',
 | 
					 | 
				
			||||||
          password: (!config?.password || defaultPassword) ? '' : config.password
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        schema={lncSchema}
 | 
					 | 
				
			||||||
        onSubmit={async ({ isDefault, ...values }) => {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await saveConfig(values)
 | 
					 | 
				
			||||||
            if (isDefault) setProvider(lnc)
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            console.error(err)
 | 
					 | 
				
			||||||
            toaster.danger('failed to attach: ' + err.message || err.toString?.())
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <PasswordInput
 | 
					 | 
				
			||||||
          label={
 | 
					 | 
				
			||||||
            <div className='d-flex align-items-center'>pairing phrase
 | 
					 | 
				
			||||||
              <Info label='help'>
 | 
					 | 
				
			||||||
                <Text>
 | 
					 | 
				
			||||||
                  {'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance <budget>```\n\n```$ litcli sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.'}
 | 
					 | 
				
			||||||
                </Text>
 | 
					 | 
				
			||||||
              </Info>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          name='pairingPhrase'
 | 
					 | 
				
			||||||
          initialValue={config?.pairingPhrase}
 | 
					 | 
				
			||||||
          newPass={config?.pairingPhrase === undefined}
 | 
					 | 
				
			||||||
          readOnly={configured}
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
          autoFocus
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <PasswordInput
 | 
					 | 
				
			||||||
          label={<>password <small className='text-muted ms-2'>optional</small></>}
 | 
					 | 
				
			||||||
          name='password'
 | 
					 | 
				
			||||||
          initialValue={defaultPassword ? '' : config?.password}
 | 
					 | 
				
			||||||
          newPass={config?.password === undefined || defaultPassword}
 | 
					 | 
				
			||||||
          readOnly={configured}
 | 
					 | 
				
			||||||
          hint='encrypts your pairing phrase when stored locally'
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <ClientCheckbox
 | 
					 | 
				
			||||||
          disabled={!configured || isDefault || enabledProviders?.length === 1}
 | 
					 | 
				
			||||||
          initialValue={isDefault}
 | 
					 | 
				
			||||||
          label='default payment method'
 | 
					 | 
				
			||||||
          name='isDefault'
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <WalletButtonBar
 | 
					 | 
				
			||||||
          status={status} onDelete={async () => {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await clearConfig()
 | 
					 | 
				
			||||||
              toaster.success('saved settings')
 | 
					 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              console.error(err)
 | 
					 | 
				
			||||||
              toaster.danger('failed to detach: ' + err.message || err.toString?.())
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Form>
 | 
					 | 
				
			||||||
      <div className='mt-3 w-100'>
 | 
					 | 
				
			||||||
        <WalletLogs wallet={Wallet.LNC} embedded />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </CenterLayout>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function LNCCard () {
 | 
					 | 
				
			||||||
  const { status } = useLNC()
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <WalletCard
 | 
					 | 
				
			||||||
      title='LNC'
 | 
					 | 
				
			||||||
      badges={['send only', 'non-custodial', 'budgetable']}
 | 
					 | 
				
			||||||
      provider='lnc'
 | 
					 | 
				
			||||||
      status={status}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,137 +0,0 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					 | 
				
			||||||
import { Form, Input } from '@/components/form'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import { useMe } from '@/components/me'
 | 
					 | 
				
			||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
 | 
					 | 
				
			||||||
import { useApolloClient, useMutation } from '@apollo/client'
 | 
					 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { LNDAutowithdrawSchema } from '@/lib/validate'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
 | 
					 | 
				
			||||||
import { REMOVE_WALLET, UPSERT_WALLET_LND, WALLET_BY_TYPE } from '@/fragments/wallet'
 | 
					 | 
				
			||||||
import Info from '@/components/info'
 | 
					 | 
				
			||||||
import Text from '@/components/text'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const variables = { type: Wallet.LND.type }
 | 
					 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function LND ({ ssrData }) {
 | 
					 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const client = useApolloClient()
 | 
					 | 
				
			||||||
  const [upsertWalletLND] = useMutation(UPSERT_WALLET_LND, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  const [removeWallet] = useMutation(REMOVE_WALLET, {
 | 
					 | 
				
			||||||
    refetchQueries: ['WalletLogs'],
 | 
					 | 
				
			||||||
    onError: (err) => {
 | 
					 | 
				
			||||||
      client.refetchQueries({ include: ['WalletLogs'] })
 | 
					 | 
				
			||||||
      throw err
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { walletByType: wallet } = ssrData || {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <CenterLayout>
 | 
					 | 
				
			||||||
      <h2 className='pb-2'>LND</h2>
 | 
					 | 
				
			||||||
      <h6 className='text-muted text-center pb-3'>autowithdraw to your Lightning Labs node</h6>
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        initial={{
 | 
					 | 
				
			||||||
          socket: wallet?.wallet?.socket || '',
 | 
					 | 
				
			||||||
          macaroon: wallet?.wallet?.macaroon || '',
 | 
					 | 
				
			||||||
          cert: wallet?.wallet?.cert || '',
 | 
					 | 
				
			||||||
          ...autowithdrawInitial({ me, priority: wallet?.priority })
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        schema={LNDAutowithdrawSchema({ me })}
 | 
					 | 
				
			||||||
        onSubmit={async ({ socket, cert, macaroon, ...settings }) => {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await upsertWalletLND({
 | 
					 | 
				
			||||||
              variables: {
 | 
					 | 
				
			||||||
                id: wallet?.id,
 | 
					 | 
				
			||||||
                socket,
 | 
					 | 
				
			||||||
                macaroon,
 | 
					 | 
				
			||||||
                cert,
 | 
					 | 
				
			||||||
                settings: {
 | 
					 | 
				
			||||||
                  ...settings,
 | 
					 | 
				
			||||||
                  autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
 | 
					 | 
				
			||||||
                  autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            console.error(err)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label='grpc host and port'
 | 
					 | 
				
			||||||
          name='socket'
 | 
					 | 
				
			||||||
          hint='tor or clearnet'
 | 
					 | 
				
			||||||
          placeholder='55.5.555.55:10001'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
          autoFocus
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label={
 | 
					 | 
				
			||||||
            <div className='d-flex align-items-center'>invoice macaroon
 | 
					 | 
				
			||||||
              <Info label='privacy tip'>
 | 
					 | 
				
			||||||
                <Text>
 | 
					 | 
				
			||||||
                  {'We accept a prebaked ***invoice.macaroon*** for your convenience. To gain better privacy, generate a new macaroon as follows:\n\n```lncli bakemacaroon invoices:write invoices:read```'}
 | 
					 | 
				
			||||||
                </Text>
 | 
					 | 
				
			||||||
              </Info>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          name='macaroon'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          hint='hex or base64 encoded'
 | 
					 | 
				
			||||||
          placeholder='AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs'
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <Input
 | 
					 | 
				
			||||||
          label={<>cert <small className='text-muted ms-2'>optional if from <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (e.g. voltage)</small></>}
 | 
					 | 
				
			||||||
          name='cert'
 | 
					 | 
				
			||||||
          clear
 | 
					 | 
				
			||||||
          hint='hex or base64 encoded'
 | 
					 | 
				
			||||||
          placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <AutowithdrawSettings />
 | 
					 | 
				
			||||||
        <WalletButtonBar
 | 
					 | 
				
			||||||
          status={!!wallet} onDelete={async () => {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await removeWallet({ variables: { id: wallet?.id } })
 | 
					 | 
				
			||||||
              toaster.success('saved settings')
 | 
					 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              console.error(err)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Form>
 | 
					 | 
				
			||||||
      <div className='mt-3 w-100'>
 | 
					 | 
				
			||||||
        <WalletLogs wallet={Wallet.LND} embedded />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </CenterLayout>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function LNDCard ({ wallet }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <WalletCard
 | 
					 | 
				
			||||||
      title='LND'
 | 
					 | 
				
			||||||
      badges={['receive only', 'non-custodial']}
 | 
					 | 
				
			||||||
      provider='lnd'
 | 
					 | 
				
			||||||
      status={wallet !== undefined || undefined}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,92 +0,0 @@
 | 
				
			|||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					 | 
				
			||||||
import { Form, ClientCheckbox, PasswordInput } from '@/components/form'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
 | 
					 | 
				
			||||||
import { nwcSchema } from '@/lib/validate'
 | 
					 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { useNWC } from '@/components/webln/nwc'
 | 
					 | 
				
			||||||
import { WalletSecurityBanner } from '@/components/banners'
 | 
					 | 
				
			||||||
import { useWebLNConfigurator } from '@/components/webln'
 | 
					 | 
				
			||||||
import WalletLogs from '@/components/wallet-logs'
 | 
					 | 
				
			||||||
import { Wallet } from '@/lib/constants'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function NWC () {
 | 
					 | 
				
			||||||
  const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
 | 
					 | 
				
			||||||
  const nwc = useNWC()
 | 
					 | 
				
			||||||
  const { name, nwcUrl, saveConfig, clearConfig, status } = nwc
 | 
					 | 
				
			||||||
  const isDefault = provider?.name === name
 | 
					 | 
				
			||||||
  const configured = isConfigured(status)
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <CenterLayout>
 | 
					 | 
				
			||||||
      <h2 className='pb-2'>Nostr Wallet Connect</h2>
 | 
					 | 
				
			||||||
      <h6 className='text-muted text-center pb-3'>use Nostr Wallet Connect for payments</h6>
 | 
					 | 
				
			||||||
      <WalletSecurityBanner />
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        initial={{
 | 
					 | 
				
			||||||
          nwcUrl: nwcUrl || '',
 | 
					 | 
				
			||||||
          isDefault: isDefault || false
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        schema={nwcSchema}
 | 
					 | 
				
			||||||
        onSubmit={async ({ isDefault, ...values }) => {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await saveConfig(values)
 | 
					 | 
				
			||||||
            if (isDefault) setProvider(nwc)
 | 
					 | 
				
			||||||
            toaster.success('saved settings')
 | 
					 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					 | 
				
			||||||
          } catch (err) {
 | 
					 | 
				
			||||||
            console.error(err)
 | 
					 | 
				
			||||||
            toaster.danger('failed to attach: ' + err.message || err.toString?.())
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <PasswordInput
 | 
					 | 
				
			||||||
          initialValue={nwcUrl}
 | 
					 | 
				
			||||||
          label='connection'
 | 
					 | 
				
			||||||
          name='nwcUrl'
 | 
					 | 
				
			||||||
          newPass
 | 
					 | 
				
			||||||
          required
 | 
					 | 
				
			||||||
          autoFocus
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <ClientCheckbox
 | 
					 | 
				
			||||||
          disabled={!configured || isDefault || enabledProviders.length === 1}
 | 
					 | 
				
			||||||
          initialValue={isDefault}
 | 
					 | 
				
			||||||
          label='default payment method'
 | 
					 | 
				
			||||||
          name='isDefault'
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <WalletButtonBar
 | 
					 | 
				
			||||||
          status={status} onDelete={async () => {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await clearConfig()
 | 
					 | 
				
			||||||
              toaster.success('saved settings')
 | 
					 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              console.error(err)
 | 
					 | 
				
			||||||
              toaster.danger('failed to detach: ' + err.message || err.toString?.())
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Form>
 | 
					 | 
				
			||||||
      <div className='mt-3 w-100'>
 | 
					 | 
				
			||||||
        <WalletLogs wallet={Wallet.NWC} embedded />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </CenterLayout>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function NWCCard () {
 | 
					 | 
				
			||||||
  const { status } = useNWC()
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <WalletCard
 | 
					 | 
				
			||||||
      title='NWC'
 | 
					 | 
				
			||||||
      badges={['send only', 'non-custodialish', 'budgetable']}
 | 
					 | 
				
			||||||
      provider='nwc'
 | 
					 | 
				
			||||||
      status={status}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user