* Wallet flow * Prepopulate fields of complementary protocol * Remove TODO about one mutation for save We need to save protocols in separate mutations so we can use the wallet id returned by the first protocol save for the following protocol saves and save them all to the same wallet. * Fix badges not updated on wallet delete * Fix useProtocol call * Fix lightning address save via prompt * Don't pass share as attribute to DOM * Fix useCallback dependency * Progress numbers as SVGs * Fix progress line margins * Remove unused saveWallet arguments * Update cache with settings response * Fix line does not connect with number 1 * Don't reuse page nav arrows in form nav * Fix missing SVG hover style * Fix missing space in wallet save log message * Reuse CSS from nav.module.css * align buttons and their icons/text * center form progress line * increase top padding of form on smaller screens * provide margin above button bar on settings form --------- Co-authored-by: k00b <k00b@stacker.news>
250 lines
6.5 KiB
JavaScript
250 lines
6.5 KiB
JavaScript
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
|
import { mapWalletResolveTypes } from '@/wallets/server/resolvers/util'
|
|
import { removeWalletProtocol, upsertWalletProtocol, updateWalletBadges } from './protocol'
|
|
import { validateSchema, walletSettingsSchema } from '@/lib/validate'
|
|
|
|
const WalletOrTemplate = {
|
|
__resolveType: walletOrTemplate => walletOrTemplate.__resolveType
|
|
}
|
|
|
|
const Wallet = {
|
|
name: wallet => wallet.template.name,
|
|
send: wallet => walletStatus(wallet, 'send'),
|
|
receive: wallet => walletStatus(wallet, 'receive')
|
|
}
|
|
|
|
const WalletTemplate = {
|
|
send: walletTemplate => walletTemplate.sendProtocols.length > 0 ? 'OK' : 'DISABLED',
|
|
receive: walletTemplate => walletTemplate.recvProtocols.length > 0 ? 'OK' : 'DISABLED',
|
|
protocols: walletTemplate => {
|
|
return [
|
|
...walletTemplate.sendProtocols.map(protocol => ({
|
|
id: `WalletTemplate-${walletTemplate.name}-${protocol}-send`,
|
|
name: protocol,
|
|
send: true
|
|
})),
|
|
...walletTemplate.recvProtocols.map(protocol => ({
|
|
id: `WalletTemplate-${walletTemplate.name}-${protocol}-recv`,
|
|
name: protocol,
|
|
send: false
|
|
}))
|
|
]
|
|
}
|
|
}
|
|
|
|
export const resolvers = {
|
|
WalletOrTemplate,
|
|
Wallet,
|
|
WalletTemplate,
|
|
Query: {
|
|
wallets,
|
|
wallet,
|
|
walletSettings
|
|
},
|
|
Mutation: {
|
|
updateWalletEncryption,
|
|
updateKeyHash,
|
|
resetWallets,
|
|
setWalletPriorities,
|
|
disablePassphraseExport,
|
|
setWalletSettings,
|
|
deleteWallet
|
|
}
|
|
}
|
|
|
|
async function wallets (parent, args, { me, models }) {
|
|
if (!me) {
|
|
throw new GqlAuthenticationError()
|
|
}
|
|
|
|
let wallets = await models.wallet.findMany({
|
|
where: {
|
|
userId: me.id
|
|
},
|
|
include: {
|
|
template: true,
|
|
protocols: true
|
|
},
|
|
orderBy: [
|
|
{ priority: 'asc' },
|
|
{ id: 'asc' }
|
|
]
|
|
})
|
|
|
|
let walletTemplates = await models.walletTemplate.findMany()
|
|
|
|
wallets = wallets.map(mapWalletResolveTypes)
|
|
walletTemplates = walletTemplates.map(t => {
|
|
return {
|
|
...t,
|
|
__resolveType: 'WalletTemplate'
|
|
}
|
|
})
|
|
|
|
return [...wallets, ...walletTemplates]
|
|
}
|
|
|
|
async function wallet (parent, { id, name }, { me, models }) {
|
|
if (!me) {
|
|
throw new GqlAuthenticationError()
|
|
}
|
|
|
|
if (id) {
|
|
const wallet = await models.wallet.findUnique({
|
|
where: { id: Number(id), userId: me.id },
|
|
include: {
|
|
template: true,
|
|
protocols: true
|
|
}
|
|
})
|
|
if (!wallet) throw new GqlInputError('wallet not found')
|
|
|
|
return mapWalletResolveTypes(wallet)
|
|
}
|
|
|
|
const template = await models.walletTemplate.findUnique({ where: { name } })
|
|
return { ...template, __resolveType: 'WalletTemplate' }
|
|
}
|
|
|
|
function walletStatus (wallet, type) {
|
|
const protocols = wallet.protocols.filter(protocol => type === 'send' ? protocol.send : !protocol.send)
|
|
|
|
const disabled = protocols.every(protocol => !protocol.enabled)
|
|
if (disabled) return 'DISABLED'
|
|
|
|
const ok = protocols.every(protocol => protocol.status === 'OK')
|
|
if (ok) return 'OK'
|
|
|
|
const error = protocols.every(protocol => protocol.status === 'ERROR')
|
|
if (error) return 'ERROR'
|
|
|
|
return 'WARNING'
|
|
}
|
|
|
|
async function walletSettings (parent, args, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
return await models.user.findUnique({ where: { id: me.id } })
|
|
}
|
|
|
|
async function updateWalletEncryption (parent, { keyHash, wallets }, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
if (!keyHash) throw new GqlInputError('hash required')
|
|
|
|
const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } })
|
|
|
|
return await models.$transaction(async tx => {
|
|
for (const { id: walletId, protocols } of wallets) {
|
|
for (const { name, send, config } of protocols) {
|
|
const mutation = upsertWalletProtocol({ name, send })
|
|
await mutation(parent, { walletId, ignoreKeyHash: true, ...config }, { me, models: tx, tx })
|
|
}
|
|
}
|
|
|
|
// optimistic concurrency control:
|
|
// make sure the user's vault key didn't change while we were updating the protocols
|
|
await tx.user.update({
|
|
where: { id: me.id, vaultKeyHash: oldKeyHash },
|
|
data: {
|
|
vaultKeyHash: keyHash,
|
|
showPassphrase: false,
|
|
vaultKeyHashUpdatedAt: new Date()
|
|
}
|
|
})
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
async function updateKeyHash (parent, { keyHash }, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
const count = await models.$executeRaw`
|
|
UPDATE users
|
|
SET "vaultKeyHash" = ${keyHash}, "vaultKeyHashUpdatedAt" = NOW()
|
|
WHERE id = ${me.id}
|
|
AND "vaultKeyHash" = ''
|
|
`
|
|
|
|
return count > 0
|
|
}
|
|
|
|
async function deleteWallet (parent, { id }, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
await models.$transaction(async tx => {
|
|
await tx.wallet.delete({ where: { id: Number(id), userId: me.id } })
|
|
await updateWalletBadges({ userId: me.id, tx })
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
async function resetWallets (parent, { newKeyHash }, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
const { vaultKeyHash: oldHash } = await models.user.findUnique({ where: { id: me.id } })
|
|
|
|
await models.$transaction(async tx => {
|
|
const protocols = await tx.walletProtocol.findMany({
|
|
where: {
|
|
send: true,
|
|
wallet: {
|
|
userId: me.id
|
|
}
|
|
}
|
|
})
|
|
|
|
for (const protocol of protocols) {
|
|
await removeWalletProtocol(parent, { id: protocol.id }, { me, tx })
|
|
}
|
|
|
|
await tx.user.update({
|
|
where: { id: me.id, vaultKeyHash: oldHash },
|
|
// TODO(wallet-v2): nullable vaultKeyHash column
|
|
data: {
|
|
vaultKeyHash: newKeyHash,
|
|
showPassphrase: true,
|
|
vaultKeyHashUpdatedAt: new Date()
|
|
}
|
|
})
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
async function disablePassphraseExport (parent, args, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
await models.user.update({ where: { id: me.id }, data: { showPassphrase: false } })
|
|
|
|
return true
|
|
}
|
|
|
|
async function setWalletPriorities (parent, { priorities }, { me, models }) {
|
|
if (!me) {
|
|
throw new GqlAuthenticationError()
|
|
}
|
|
|
|
await models.$transaction(async tx => {
|
|
for (const { id, priority } of priorities) {
|
|
await tx.wallet.update({
|
|
where: { userId: me.id, id: Number(id) },
|
|
data: { priority }
|
|
})
|
|
}
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
async function setWalletSettings (parent, { settings }, { me, models }) {
|
|
if (!me) throw new GqlAuthenticationError()
|
|
|
|
await validateSchema(walletSettingsSchema, settings)
|
|
|
|
await models.user.update({ where: { id: me.id }, data: settings })
|
|
|
|
return settings
|
|
}
|