diff --git a/api/resolvers/vault.js b/api/resolvers/vault.js index 1211adaf5..a1258605d 100644 --- a/api/resolvers/vault.js +++ b/api/resolvers/vault.js @@ -1,11 +1,23 @@ import { E_VAULT_KEY_EXISTS, GqlAuthenticationError, GqlInputError } from '@/lib/error' +import { getWalletByType } from '@/wallets/common' +import { deleteVault, hasVault, vaultNewSchematoTypedef, vaultPrismaFragments } from '@/wallets/vault' export default { Query: { getVaultEntries: async (parent, args, { me, models }) => { if (!me) throw new GqlAuthenticationError() - return await models.vaultEntry.findMany({ where: { userId: me.id } }) + const wallets = await models.wallet.findMany({ + where: { userId: me.id }, + include: vaultPrismaFragments.include() + }) + + const vaultEntries = [] + for (const wallet of wallets) { + vaultEntries.push(...vaultNewSchematoTypedef(wallet).vaultEntries) + } + + return vaultEntries } }, Mutation: { @@ -13,30 +25,37 @@ export default { updateVaultKey: async (parent, { entries, hash }, { me, models }) => { if (!me) throw new GqlAuthenticationError() if (!hash) throw new GqlInputError('hash required') - const txs = [] const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } }) if (oldKeyHash) { - if (oldKeyHash !== hash) { - throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS) - } else { + if (oldKeyHash === hash) { return true } - } else { - txs.push(models.user.update({ - where: { id: me.id }, - data: { vaultKeyHash: hash } - })) + throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS) } - for (const entry of entries) { - txs.push(models.vaultEntry.update({ - where: { userId_key: { userId: me.id, key: entry.key } }, - data: { value: entry.value, iv: entry.iv } - })) - } - await models.$transaction(txs) - return true + return await models.$transaction(async tx => { + const wallets = await tx.wallet.findMany({ where: { userId: me.id } }) + for (const wallet of wallets) { + const def = getWalletByType(wallet.type) + await tx.wallet.update({ + where: { id: wallet.id }, + data: { + [def.walletField]: { + update: vaultPrismaFragments.upsert({ ...wallet, vaultEntries: entries }) + } + } + }) + } + + // optimistic concurrency control: make sure the user's vault key didn't change while we were updating the wallets + await tx.user.update({ + where: { id: me.id, vaultKeyHash: oldKeyHash }, + data: { vaultKeyHash: hash } + }) + + return true + }) }, clearVault: async (parent, args, { me, models }) => { if (!me) throw new GqlAuthenticationError() @@ -45,7 +64,10 @@ export default { where: { id: me.id }, data: { vaultKeyHash: '' } })) - txs.push(models.vaultEntry.deleteMany({ where: { userId: me.id } })) + + const wallets = await models.wallet.findMany({ where: { userId: me.id } }) + txs.push(...wallets.filter(hasVault).map(wallet => deleteVault(models, wallet))) + await models.$transaction(txs) return true } diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 1417f8906..663a5f89d 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -29,6 +29,7 @@ import { canReceive, getWalletByType } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' import { timeoutSignal, withTimeout } from '@/lib/time' +import { deleteVault, hasVault, vaultNewSchematoTypedef, vaultPrismaFragments } from '@/wallets/vault' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') @@ -43,11 +44,13 @@ function injectResolvers (resolvers) { // this mutation was sent from an unsynced client // to pass validation, we need to add the existing vault entries for validation // in case the client is removing the receiving config - existingVaultEntries = await models.vaultEntry.findMany({ + const wallet = await models.wallet.findUnique({ where: { - walletId: Number(data.id) - } + id: Number(data.id) + }, + include: vaultPrismaFragments.include() }) + existingVaultEntries = vaultNewSchematoTypedef(wallet).vaultEntries } const validData = await validateWallet(walletDef, @@ -159,10 +162,8 @@ const resolvers = { throw new GqlAuthenticationError() } - return await models.wallet.findMany({ - include: { - vaultEntries: true - }, + const wallets = await models.wallet.findMany({ + include: vaultPrismaFragments.include(), where: { userId: me.id }, @@ -170,6 +171,8 @@ const resolvers = { priority: 'asc' } }) + + return wallets.map(vaultNewSchematoTypedef) }, withdrawl: getWithdrawl, direct: async (parent, { id }, { me, models }) => { @@ -569,7 +572,11 @@ const resolvers = { } const logger = walletLogger({ wallet, models }) - await models.wallet.delete({ where: { userId: me.id, id: Number(id) } }) + + await models.$transaction([ + hasVault(wallet) ? deleteVault(models, wallet) : null, + models.wallet.delete({ where: { userId: me.id, id: Number(id) } }) + ].filter(Boolean)) if (canReceive({ def: getWalletByType(wallet.type), config: wallet.wallet })) { logger.info('details for receiving deleted') @@ -838,15 +845,12 @@ async function upsertWallet ( const txs = [] - if (id) { - const oldVaultEntries = await models.vaultEntry.findMany({ where: { userId: me.id, walletId: Number(id) } }) + const walletWithVault = { ...wallet, vaultEntries } - // createMany is the set difference of the new - old - // deleteMany is the set difference of the old - new - // updateMany is the intersection of the old and new - const difference = (a = [], b = [], key = 'key') => a.filter(x => !b.find(y => y[key] === x[key])) - const intersectionMerge = (a = [], b = [], key = 'key') => a.filter(x => b.find(y => y[key] === x[key])) - .map(x => ({ [key]: x[key], ...b.find(y => y[key] === x[key]) })) + if (id) { + const dbWallet = await models.wallet.findUnique({ + where: { id: Number(id), userId: me.id } + }) txs.push( models.wallet.update({ @@ -854,62 +858,28 @@ async function upsertWallet ( data: { enabled, priority, - // client only wallets have no receive config and thus don't have their own table - ...(Object.keys(recvConfig).length > 0 - ? { - [wallet.field]: { - upsert: { - create: recvConfig, - update: recvConfig - } - } - } - : {}), - ...(vaultEntries - ? { - vaultEntries: { - deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({ - userId: me.id, key - })), - create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({ - key, iv, value, userId: me.id - })), - update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({ - where: { userId_key: { userId: me.id, key } }, - data: { value, iv } - })) - } - } - : {}) - + [wallet.field]: { + upsert: { + create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) }, + update: { ...recvConfig, ...vaultPrismaFragments.upsert(walletWithVault) } + }, + // XXX the check is required because the update would fail if there is no row to delete ... + update: hasVault(dbWallet) ? vaultPrismaFragments.deleteMissing(walletWithVault) : undefined + } }, - include: { - vaultEntries: true - } + include: vaultPrismaFragments.include(walletWithVault) }) ) } else { txs.push( models.wallet.create({ - include: { - vaultEntries: true - }, + include: vaultPrismaFragments.include(walletWithVault), data: { enabled, priority, userId: me.id, type: wallet.type, - // client only wallets have no receive config and thus don't have their own table - ...(Object.keys(recvConfig).length > 0 ? { [wallet.field]: { create: recvConfig } } : {}), - ...(vaultEntries - ? { - vaultEntries: { - createMany: { - data: vaultEntries?.map(({ key, iv, value }) => ({ key, iv, value, userId: me.id })) - } - } - } - : {}) + [wallet.field]: { create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) } } } }) ) @@ -946,7 +916,7 @@ async function upsertWallet ( } const [upsertedWallet] = await models.$transaction(txs) - return upsertedWallet + return vaultNewSchematoTypedef(upsertedWallet) } export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) { diff --git a/prisma/migrations/20250411012334_vault_refactor/migration.sql b/prisma/migrations/20250411012334_vault_refactor/migration.sql new file mode 100644 index 000000000..f5bef0600 --- /dev/null +++ b/prisma/migrations/20250411012334_vault_refactor/migration.sql @@ -0,0 +1,208 @@ +/* + Warnings: + + - A unique constraint covering the columns `[apiKeyId]` on the table `WalletBlink` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[currencyId]` on the table `WalletBlink` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[adminKeyId]` on the table `WalletLNbits` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[nwcUrlId]` on the table `WalletNWC` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[primaryPasswordId]` on the table `WalletPhoenixd` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "WalletBlink" + ADD COLUMN "apiKeyId" INTEGER, + ADD COLUMN "currencyId" INTEGER; + +-- AlterTable +ALTER TABLE "WalletLNbits" ADD COLUMN "adminKeyId" INTEGER; + +-- AlterTable +ALTER TABLE "WalletNWC" ADD COLUMN "nwcUrlId" INTEGER; + +-- AlterTable +ALTER TABLE "WalletPhoenixd" ADD COLUMN "primaryPasswordId" INTEGER; + +-- CreateTable +CREATE TABLE "Vault" ( + "id" SERIAL NOT NULL, + "iv" TEXT NOT NULL, + "value" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Vault_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WalletLNC" ( + "id" SERIAL NOT NULL, + "walletId" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "pairingPhraseId" INTEGER, + "localKeyId" INTEGER, + "remoteKeyId" INTEGER, + "serverHostId" INTEGER, + + CONSTRAINT "WalletLNC_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WalletWebLN" ( + "id" SERIAL NOT NULL, + "walletId" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "WalletWebLN_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletBlink_apiKeyId_key" ON "WalletBlink"("apiKeyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletBlink_currencyId_key" ON "WalletBlink"("currencyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNbits_adminKeyId_key" ON "WalletLNbits"("adminKeyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletNWC_nwcUrlId_key" ON "WalletNWC"("nwcUrlId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletPhoenixd_primaryPasswordId_key" ON "WalletPhoenixd"("primaryPasswordId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_walletId_key" ON "WalletLNC"("walletId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_pairingPhraseId_key" ON "WalletLNC"("pairingPhraseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_localKeyId_key" ON "WalletLNC"("localKeyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_remoteKeyId_key" ON "WalletLNC"("remoteKeyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_serverHostId_key" ON "WalletLNC"("serverHostId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletWebLN_walletId_key" ON "WalletWebLN"("walletId"); + +-- AddForeignKey +ALTER TABLE "WalletLNbits" ADD CONSTRAINT "WalletLNbits_adminKeyId_fkey" FOREIGN KEY ("adminKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletNWC" ADD CONSTRAINT "WalletNWC_nwcUrlId_fkey" FOREIGN KEY ("nwcUrlId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletBlink" ADD CONSTRAINT "WalletBlink_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletBlink" ADD CONSTRAINT "WalletBlink_currencyId_fkey" FOREIGN KEY ("currencyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletPhoenixd" ADD CONSTRAINT "WalletPhoenixd_primaryPasswordId_fkey" FOREIGN KEY ("primaryPasswordId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_pairingPhraseId_fkey" FOREIGN KEY ("pairingPhraseId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_localKeyId_fkey" FOREIGN KEY ("localKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_remoteKeyId_fkey" FOREIGN KEY ("remoteKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_serverHostId_fkey" FOREIGN KEY ("serverHostId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletWebLN" ADD CONSTRAINT "WalletWebLN_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE TRIGGER wallet_lnc_as_jsonb +AFTER INSERT OR UPDATE ON "WalletLNC" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); + +CREATE TRIGGER wallet_webln_as_jsonb +AFTER INSERT OR UPDATE ON "WalletWebLN" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); + +CREATE OR REPLACE FUNCTION migrate_wallet_vault() +RETURNS void AS +$$ +DECLARE + vaultEntry "VaultEntry"%ROWTYPE; +BEGIN + INSERT INTO "WalletWebLN"("walletId") SELECT id FROM "Wallet" WHERE type = 'WEBLN'; + INSERT INTO "WalletLNC"("walletId") SELECT id from "Wallet" WHERE type = 'LNC'; + + FOR vaultEntry IN SELECT * FROM "VaultEntry" LOOP + DECLARE + vaultId INT; + walletType "WalletType"; + BEGIN + INSERT INTO "Vault" ("iv", "value") + VALUES (vaultEntry."iv", vaultEntry."value") + RETURNING id INTO vaultId; + + SELECT type INTO walletType + FROM "Wallet" + WHERE id = vaultEntry."walletId"; + + CASE walletType + WHEN 'LNBITS' THEN + UPDATE "WalletLNbits" + SET "adminKeyId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + WHEN 'NWC' THEN + UPDATE "WalletNWC" + SET "nwcUrlId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + WHEN 'BLINK' THEN + IF vaultEntry."key" = 'apiKey' THEN + UPDATE "WalletBlink" + SET "apiKeyId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + ELSE + UPDATE "WalletBlink" + SET "currencyId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + END IF; + WHEN 'PHOENIXD' THEN + UPDATE "WalletPhoenixd" + SET "primaryPasswordId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + WHEN 'LNC' THEN + IF vaultEntry."key" = 'pairingPhrase' THEN + UPDATE "WalletLNC" + SET "pairingPhraseId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + ELSIF vaultEntry."key" = 'localKey' THEN + UPDATE "WalletLNC" + SET "localKeyId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + ELSIF vaultEntry."key" = 'remoteKey' THEN + UPDATE "WalletLNC" + SET "remoteKeyId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + ELSIF vaultEntry."key" = 'serverHost' THEN + UPDATE "WalletLNC" + SET "serverHostId" = vaultId + WHERE "walletId" = vaultEntry."walletId"; + END IF; + END CASE; + END; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +SELECT migrate_wallet_vault(); +DROP FUNCTION migrate_wallet_vault(); + +ALTER TABLE "VaultEntry" DROP CONSTRAINT "VaultEntry_userId_fkey"; +ALTER TABLE "VaultEntry" DROP CONSTRAINT "VaultEntry_walletId_fkey"; +DROP TABLE "VaultEntry"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 49c4b858e..4baf89a39 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -146,7 +146,6 @@ model User { oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees") vaultKeyHash String @default("") walletsUpdatedAt DateTime? - vaultEntries VaultEntry[] @relation("VaultEntries") proxyReceive Boolean @default(true) directReceive Boolean @default(true) DirectPaymentReceived DirectPayment[] @relation("DirectPaymentReceived") @@ -240,8 +239,9 @@ model Wallet { walletNWC WalletNWC? walletPhoenixd WalletPhoenixd? walletBlink WalletBlink? + walletLNC WalletLNC? + walletWebLN WalletWebLN? - vaultEntries VaultEntry[] @relation("VaultEntries") withdrawals Withdrawl[] InvoiceForward InvoiceForward[] DirectPayment DirectPayment[] @@ -250,20 +250,22 @@ model Wallet { @@index([priority]) } -model VaultEntry { +model Vault { id Int @id @default(autoincrement()) - key String @db.Text iv String @db.Text value String @db.Text - userId Int - walletId Int? - user User @relation(fields: [userId], references: [id], onDelete: Cascade, name: "VaultEntries") - wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: Cascade, name: "VaultEntries") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - @@unique([userId, key]) - @@index([walletId]) + walletLNbits WalletLNbits? + walletNWC WalletNWC? + walletBlinkApiKey WalletBlink? @relation("blinkApiKeySend") + walletBlinkCurrency WalletBlink? @relation("blinkCurrencySend") + walletPhoenixd WalletPhoenixd? + walletLNCPairingPhrase WalletLNC? @relation("lncPairingPhrase") + walletLNCRemoteKey WalletLNC? @relation("lncRemoteKey") + walletLNCServerHost WalletLNC? @relation("lncServerHost") + walletLNCLocalKey WalletLNC? @relation("lncLocalKey") } model WalletLog { @@ -322,6 +324,8 @@ model WalletLNbits { updatedAt DateTime @default(now()) @updatedAt @map("updated_at") url String invoiceKey String? + adminKeyId Int? @unique + adminKey Vault? @relation(fields: [adminKeyId], references: [id]) } model WalletNWC { @@ -331,6 +335,8 @@ model WalletNWC { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") nwcUrlRecv String? + nwcUrlId Int? @unique + nwcUrl Vault? @relation(fields: [nwcUrlId], references: [id]) } model WalletBlink { @@ -341,6 +347,10 @@ model WalletBlink { updatedAt DateTime @default(now()) @updatedAt @map("updated_at") apiKeyRecv String? currencyRecv String? + apiKeyId Int? @unique + apiKey Vault? @relation("blinkApiKeySend", fields: [apiKeyId], references: [id]) + currencyId Int? @unique + currency Vault? @relation("blinkCurrencySend", fields: [currencyId], references: [id]) } model WalletPhoenixd { @@ -351,6 +361,32 @@ model WalletPhoenixd { updatedAt DateTime @default(now()) @updatedAt @map("updated_at") url String secondaryPassword String? + primaryPasswordId Int? @unique + primaryPassword Vault? @relation(fields: [primaryPasswordId], references: [id]) +} + +model WalletLNC { + id Int @id @default(autoincrement()) + walletId Int @unique + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + pairingPhraseId Int? @unique + pairingPhrase Vault? @relation("lncPairingPhrase", fields: [pairingPhraseId], references: [id]) + localKeyId Int? @unique + localKey Vault? @relation("lncLocalKey", fields: [localKeyId], references: [id]) + remoteKeyId Int? @unique + remoteKey Vault? @relation("lncRemoteKey", fields: [remoteKeyId], references: [id]) + serverHostId Int? @unique + serverHost Vault? @relation("lncServerHost", fields: [serverHostId], references: [id]) +} + +model WalletWebLN { + id Int @id @default(autoincrement()) + walletId Int @unique + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") } model Mute { diff --git a/wallets/vault.js b/wallets/vault.js new file mode 100644 index 000000000..e99339a5a --- /dev/null +++ b/wallets/vault.js @@ -0,0 +1,114 @@ +import { getWalletByType } from '@/wallets/common' +import walletDefs from '@/wallets/client' + +export const vaultPrismaFragments = { + create: createFragment, + upsert: upsertFragment, + deleteMissing: deleteMissingFragment, + deleteAll: deleteAllFragment, + include: includeFragment +} + +function createFragment (wallet) { + return wallet.vaultEntries?.reduce((acc, { key, iv, value }) => ({ + ...acc, + [key]: { + create: { iv, value } + } + }), {}) +} + +function upsertFragment (wallet) { + return wallet.vaultEntries?.reduce((acc, { key, iv, value }) => ({ + ...acc, + [key]: { + upsert: { + create: { iv, value }, + update: { iv, value } + } + } + }), {}) +} + +function deleteMissingFragment (wallet) { + const del = deleteAllFragment(wallet) + for (const { key: name } of wallet.vaultEntries) { + delete del[name] + } + return del +} + +function deleteAllFragment (wallet) { + const def = getWalletByType(wallet.type) + const names = vaultFieldNames(def) + return names.reduce((acc, name) => ({ + ...acc, + [name]: { delete: true } + }), {}) +} + +function includeFragment (wallet) { + const include = walletDefs.reduce((acc, def) => { + const names = vaultFieldNames(def) + if (names.length === 0) return acc + + return { + ...acc, + [def.walletField]: { + include: names.reduce((acc2, name) => ({ + ...acc2, + [name]: true + }), {}) + } + } + }, {}) + + if (wallet) { + const def = getWalletByType(wallet.type) + const names = vaultFieldNames(def) + if (names.length === 0) return {} + return { [def.walletField]: include[def.walletField] } + } + + return include +} + +function vaultFieldNames (walletDef) { + return walletDef.fields.filter(f => f.clientOnly).map(f => f.name) +} + +export function vaultNewSchematoTypedef (wallet) { + // this function converts a wallet row from the db with the new schema + // to the expected GraphQL typedef since the client has not yet been updated. + // + // For example, the query for the LNbits wallet now returns the wallet as (url,invoiceKey,adminKey) + // but the client expects wallet and vaultEntries separated, see api/typedefs/wallet.js. + // + // === TODO: remove this function after client update === + const def = getWalletByType(wallet.type) + + const newVaultEntries = [] + for (const name of vaultFieldNames(def)) { + const newVaultEntry = wallet?.[def.walletField]?.[name] + if (newVaultEntry) newVaultEntries.push({ ...newVaultEntry, key: name }) + } + + return { + ...wallet, + vaultEntries: newVaultEntries + } +} + +export function deleteVault (models, wallet) { + const def = getWalletByType(wallet.type) + return models[def.walletField].update({ + where: { walletId: wallet.id }, + data: vaultPrismaFragments.deleteAll(wallet) + }) +} + +export function hasVault (wallet) { + const def = getWalletByType(wallet.type) + const vaultNames = vaultFieldNames(def) + return vaultNames.some(name => wallet.wallet?.[`${name}Id`]) +} diff --git a/worker/wallet.js b/worker/wallet.js index ab504d087..49dea5abd 100644 --- a/worker/wallet.js +++ b/worker/wallet.js @@ -14,6 +14,7 @@ import { import { payingActionConfirmed, payingActionFailed } from './payingAction' import { canReceive, getWalletByType } from '@/wallets/common' import { notifyNewStreak, notifyStreakLost } from '@/lib/webPush' +import { hasVault, vaultPrismaFragments } from '@/wallets/vault' export async function subscribeToWallet (args) { await subscribeToDeposits(args) @@ -296,15 +297,13 @@ export async function checkWallet ({ data: { userId }, models }) { userId, enabled: true }, - include: { - vaultEntries: true - } + include: vaultPrismaFragments.include() }) const { hasRecvWallet: oldHasRecvWallet, hasSendWallet: oldHasSendWallet } = await tx.user.findUnique({ where: { id: userId } }) const newHasRecvWallet = wallets.some(({ type, wallet }) => canReceive({ def: getWalletByType(type), config: wallet })) - const newHasSendWallet = wallets.some(({ vaultEntries }) => vaultEntries.length > 0) + const newHasSendWallet = wallets.some(hasVault) await tx.user.update({ where: { id: userId },