Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 15 additions & 58 deletions wallets/client/context/hooks.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { useEffect, useState } from 'react'
import { useMe } from '@/components/me'
import {
useWalletsQuery, useGenerateRandomKey, useSetKey, useLoadKey, useLoadOldKey,
useWalletMigrationMutation, CryptoKeyRequiredError, useIsWrongKey,
useWalletLogger
useWalletsQuery, useGenerateRandomKey, useSetKey, useIsWrongKey, useWalletLogger, useDeleteOldDb
} from '@/wallets/client/hooks'
import { SET_WALLETS, WRONG_KEY, KEY_MATCH, useWalletsDispatch, WALLETS_QUERY_ERROR, KEY_STORAGE_UNAVAILABLE } from '@/wallets/client/context'
import { useIndexedDB } from '@/components/use-indexeddb'
Expand Down Expand Up @@ -43,8 +41,7 @@ export function useKeyInit () {

const generateRandomKey = useGenerateRandomKey()
const setKey = useSetKey()
const loadKey = useLoadKey()
const loadOldKey = useLoadOldKey()
const deleteOldDb = useDeleteOldDb()
const [db, setDb] = useState(null)
const { open } = useIndexedDB()

Expand Down Expand Up @@ -73,13 +70,12 @@ export function useKeyInit () {

async function keyInit () {
try {
// TODO(wallet-v2): remove migration code
// and delete the old IndexedDB after wallet v2 has been released for some time
// delete the old IndexedDB since wallet v2 has been released 2 months ago
await deleteOldDb()

// load old key and create random key before opening transaction in case we need them
// create random key before opening transaction in case we need it
// because we can't run async code in a transaction because it will close the transaction
// see https://javascript.info/indexeddb#transactions-autocommit
const oldKeyAndHash = await loadOldKey()
const { key: randomKey, hash: randomHash } = await generateRandomKey()

// run read and write in one transaction to avoid race conditions
Expand All @@ -99,12 +95,6 @@ export function useKeyInit () {
return resolve(read.result)
}

if (oldKeyAndHash) {
// return key+hash found in old db
logger.debug('key init: key found in old IndexedDB')
return resolve(oldKeyAndHash)
}

// no key found, write and return generated random key
const updatedAt = Date.now()
const write = tx.objectStore('vault').put({ key: randomKey, hash: randomHash, updatedAt }, 'key')
Expand All @@ -129,52 +119,19 @@ export function useKeyInit () {
}
}
keyInit()
}, [me?.id, db, generateRandomKey, loadOldKey, setKey, loadKey, logger])
}, [me?.id, db, deleteOldDb, generateRandomKey, setKey, logger])
}

// TODO(wallet-v2): remove migration code
// =============================================================
// ****** Below is the migration code for WALLET v1 -> v2 ******
// remove when we can assume migration is complete (if ever)
// =============================================================

export function useWalletMigration () {
export function useDeleteLocalWallets () {
const { me } = useMe()
const { migrate: walletMigration, ready } = useWalletMigrationMutation()

useEffect(() => {
if (!me?.id || !ready) return

async function migrate () {
const localWallets = Object.entries(window.localStorage)
.filter(([key]) => key.startsWith('wallet:'))
.filter(([key]) => key.split(':').length < 3 || key.endsWith(me.id))
.reduce((acc, [key, value]) => {
try {
const config = JSON.parse(value)
acc.push({ key, ...config })
} catch (err) {
console.error(`useLocalWallets: ${key}: invalid JSON:`, err)
}
return acc
}, [])

await Promise.allSettled(
localWallets.map(async ({ key, ...localWallet }) => {
const name = key.split(':')[1].toUpperCase()
try {
await walletMigration({ ...localWallet, name })
window.localStorage.removeItem(key)
} catch (err) {
if (err instanceof CryptoKeyRequiredError) {
// key not set yet, skip this wallet
return
}
console.error(`${name}: wallet migration failed:`, err)
}
})
)
}
migrate()
}, [ready, me?.id, walletMigration])
if (!me?.id) return

// we used to store wallets locally so this makes sure we delete them if there are any left over
Object.keys(window.localStorage)
.filter((key) => key.startsWith('wallet:'))
.filter((key) => key.split(':').length < 3 || key.endsWith(me.id))
.forEach((key) => window.localStorage.removeItem(key))
}, [me?.id])
}
11 changes: 2 additions & 9 deletions wallets/client/context/provider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, useContext, useReducer } from 'react'
import walletsReducer from './reducer'
import { useServerWallets, useKeyInit, useWalletMigration } from './hooks'
import { useServerWallets, useKeyInit, useDeleteLocalWallets } from './hooks'
import { useAutoRetryPayIns } from '@/components/payIn/hooks/use-auto-retry-pay-ins'
import { WebLnProvider } from '@/wallets/lib/protocols/webln'

Expand Down Expand Up @@ -81,14 +81,7 @@ function WalletHooks ({ children }) {
useServerWallets()
useAutoRetryPayIns()
useKeyInit()

// TODO(wallet-v2): remove migration code
// =============================================================
// ****** Below is the migration code for WALLET v1 -> v2 ******
// remove when we can assume migration is complete (if ever)
// =============================================================

useWalletMigration()
useDeleteLocalWallets()

return children
}
16 changes: 4 additions & 12 deletions wallets/client/hooks/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,14 @@ export class CryptoKeyRequiredError extends Error {
}
}

export function useLoadKey () {
const { get } = useIndexedDB()

return useCallback(async () => {
return await get('vault', 'key')
}, [get])
}

export function useLoadOldKey () {
export function useDeleteOldDb () {
const { me } = useMe()
const oldDbName = me?.id ? `app:storage:${me?.id}:vault` : undefined
const { get } = useIndexedDB(oldDbName)
const { deleteDb } = useIndexedDB(oldDbName)

return useCallback(async () => {
return await get('vault', 'key')
}, [get])
return await deleteDb()
}, [deleteDb])
}

export function useSetKey () {
Expand Down
124 changes: 3 additions & 121 deletions wallets/client/hooks/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,16 @@ import {
} from '@/wallets/client/fragments'
import { gql, useApolloClient, useMutation, useQuery } from '@apollo/client'
import { useDecryption, useEncryption, useSetKey, useWalletLoggerFactory, useWalletsUpdatedAt, WalletStatus } from '@/wallets/client/hooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
isEncryptedField, isTemplate, isWallet, protocolAvailable, protocolClientSchema, protocolLogName, reverseProtocolRelationName,
walletLud16Domain
isEncryptedField, isTemplate, isWallet, protocolAvailable, protocolLogName, reverseProtocolRelationName, walletLud16Domain
} from '@/wallets/lib/util'
import { protocolTestSendPayment } from '@/wallets/client/protocols'
import { timeoutSignal } from '@/lib/time'
import { FAST_POLL_INTERVAL_MS, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
import { useToast } from '@/components/toast'
import { useMe } from '@/components/me'
import { useTemplates, useWallets, useWalletsLoading } from '@/wallets/client/context'
import { useTemplates, useWallets } from '@/wallets/client/context'
import { requestPersistentStorage } from '@/components/use-indexeddb'

export function useWalletsQuery () {
Expand Down Expand Up @@ -452,127 +451,10 @@ function useEncryptConfig (defaultProtocol, options = {}) {
return useMemo(() => ({ encryptConfig, ready }), [encryptConfig, ready])
}

// TODO(wallet-v2): remove migration code
// =============================================================
// ****** Below is the migration code for WALLET v1 -> v2 ******
// remove when we can assume migration is complete (if ever)
// =============================================================

export function useWalletMigrationMutation () {
const wallets = useWallets()
const loading = useWalletsLoading()
const client = useApolloClient()
const { encryptConfig, ready } = useEncryptConfig()

// XXX We use a ref for the wallets to avoid duplicate wallets
// Without a ref, the migrate callback would depend on the wallets and thus update every time the migration creates a wallet.
// This update would then cause the useEffect in wallets/client/context/hooks that triggers the migration to run again before the first migration is complete.
const walletsRef = useRef(wallets)
useEffect(() => {
if (!loading) walletsRef.current = wallets
}, [loading])

const migrate = useCallback(async ({ name, enabled, ...configV1 }) => {
const protocol = { name, send: true }

const configV2 = migrateConfig(protocol, configV1)

const isSameProtocol = (p) => {
const sameName = p.name === protocol.name
const sameSend = p.send === protocol.send
const sameConfig = Object.keys(p.config)
.filter(k => !['__typename', 'id'].includes(k))
.every(k => p.config[k] === configV2[k])
return sameName && sameSend && sameConfig
}

const exists = walletsRef.current.some(w => w.name === name && w.protocols.some(isSameProtocol))
if (exists) return

const schema = protocolClientSchema(protocol)
await schema.validate(configV2)

const encrypted = await encryptConfig(configV2, { protocol })

// decide if we create a new wallet (templateName) or use an existing one (walletId)
const templateName = getWalletTemplateName(protocol)
let walletId
const wallet = walletsRef.current.find(w =>
w.name === name && !w.protocols.some(p => p.name === protocol.name && p.send)
)
if (wallet) {
walletId = Number(wallet.id)
}

await client.mutate({
mutation: protocolUpsertMutation(protocol),
variables: {
...(walletId ? { walletId } : { templateName }),
enabled,
...encrypted
}
})
}, [client, encryptConfig])

return useMemo(() => ({ migrate, ready: ready && !loading }), [migrate, ready, loading])
}

export function useUpdateKeyHash () {
const [mutate] = useMutation(UPDATE_KEY_HASH)

return useCallback(async (keyHash) => {
await mutate({ variables: { keyHash } })
}, [mutate])
}

function migrateConfig (protocol, config) {
switch (protocol.name) {
case 'LNBITS':
return {
url: config.url,
apiKey: config.adminKey
}
case 'PHOENIXD':
return {
url: config.url,
apiKey: config.primaryPassword
}
case 'BLINK':
return {
url: config.url,
apiKey: config.apiKey,
currency: config.currency
}
case 'LNC':
return {
pairingPhrase: config.pairingPhrase,
localKey: config.localKey,
remoteKey: config.remoteKey,
serverHost: config.serverHost
}
case 'WEBLN':
return {}
case 'NWC':
return {
url: config.nwcUrl
}
default:
return config
}
}

function getWalletTemplateName (protocol) {
switch (protocol.name) {
case 'LNBITS':
case 'PHOENIXD':
case 'BLINK':
case 'NWC':
return protocol.name
case 'LNC':
return 'LND'
case 'WEBLN':
return 'ALBY'
default:
return null
}
}