From 1ea9cfbcc83fc56471b5b106fb0d50b19acab139 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Fri, 21 Jun 2024 12:07:17 +0530 Subject: [PATCH] refactor(voucher): resolvers in saperator files (#4507) --- .../app/api/lnurlw/[unique-hash]/route.ts | 10 +- .../app/api/lnurlw/callback/[id]/route.ts | 27 +- apps/voucher/graphql/resolvers.ts | 393 ------------------ apps/voucher/graphql/resolvers/index.ts | 18 + .../mutation/create-withdraw-link.ts | 277 ++++++++++++ .../mutation/redeem-withdraw-link-on-chain.ts | 136 ++++++ .../resolvers/query/get-withdraw-link.ts | 11 + .../query/get-withdraw-links-by-userId.ts | 28 ++ apps/voucher/utils/helpers.ts | 16 + 9 files changed, 514 insertions(+), 402 deletions(-) delete mode 100644 apps/voucher/graphql/resolvers.ts create mode 100644 apps/voucher/graphql/resolvers/index.ts create mode 100644 apps/voucher/graphql/resolvers/mutation/create-withdraw-link.ts create mode 100644 apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts create mode 100644 apps/voucher/graphql/resolvers/query/get-withdraw-link.ts create mode 100644 apps/voucher/graphql/resolvers/query/get-withdraw-links-by-userId.ts diff --git a/apps/voucher/app/api/lnurlw/[unique-hash]/route.ts b/apps/voucher/app/api/lnurlw/[unique-hash]/route.ts index 7e644ffae2..fc2164ee8c 100644 --- a/apps/voucher/app/api/lnurlw/[unique-hash]/route.ts +++ b/apps/voucher/app/api/lnurlw/[unique-hash]/route.ts @@ -18,8 +18,14 @@ export async function GET( return Response.json({ error: "Withdraw link not found", status: 404 }) if (withdrawLink instanceof Error) return Response.json({ error: "Internal Server Error", status: 500 }) - if (withdrawLink.status === Status.Paid) - return Response.json({ error: "Withdraw link claimed", status: 500 }) + if (withdrawLink.status === Status.Pending) + return Response.json({ + error: + "Withdrawal link is in pending state. Please contact support if the error persists.", + status: 500, + }) + if (withdrawLink.status !== Status.Active) + return Response.json({ error: "Withdraw link is not Active", status: 500 }) const client = escrowApolloClient() const realTimePriceResponse = await getRealtimePriceQuery({ diff --git a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts index 62e7816398..4f4286f743 100644 --- a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts +++ b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts @@ -32,9 +32,9 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri try { const withdrawLink = await getWithdrawLinkByK1Query({ k1 }) - if (!withdrawLink) { + + if (!withdrawLink) return Response.json({ status: "ERROR", reason: "Withdraw link not found" }) - } if (withdrawLink instanceof Error) return Response.json({ status: "ERROR", reason: "Internal Server Error" }) @@ -42,9 +42,15 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri if (withdrawLink.id !== id) return Response.json({ status: "ERROR", reason: "Invalid Request" }) - // locking so some else can't use this link at the same time - if (withdrawLink.status === Status.Paid) - return Response.json({ status: "ERROR", reason: "Withdraw link claimed" }) + if (withdrawLink.status === Status.Pending) + return Response.json({ + status: "ERROR", + reason: + "Withdrawal link is in pending state. Please contact support if the error persists.", + }) + + if (withdrawLink.status !== Status.Active) + return Response.json({ status: "ERROR", reason: "Withdraw link is not Active" }) const client = escrowApolloClient() @@ -78,8 +84,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri // } // } - await updateWithdrawLinkStatus({ id, status: Status.Paid }) - + await updateWithdrawLinkStatus({ id, status: Status.Pending }) const lnInvoicePaymentSendResponse = await lnInvoicePaymentSend({ client, input: { @@ -111,6 +116,13 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri }) } + if ( + lnInvoicePaymentSendResponse?.lnInvoicePaymentSend?.status === + PaymentSendResult.Pending + ) { + return Response.json({ status: "ERROR", reason: "Payment went on Pending state" }) + } + if ( lnInvoicePaymentSendResponse?.lnInvoicePaymentSend?.status !== PaymentSendResult.Success @@ -119,6 +131,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri return Response.json({ status: "ERROR", reason: "Payment not paid" }) } + await updateWithdrawLinkStatus({ id, status: Status.Paid }) return Response.json({ status: "OK" }) } catch (error) { console.error("error paying lnurlw", error) diff --git a/apps/voucher/graphql/resolvers.ts b/apps/voucher/graphql/resolvers.ts deleted file mode 100644 index 20ee5b4807..0000000000 --- a/apps/voucher/graphql/resolvers.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { getServerSession } from "next-auth" - -import { - convertCentsToSats, - createMemo, - getWalletDetails, - getWalletDetailsFromWalletId, -} from "@/utils/helpers" -import { - createWithdrawLinkMutation, - getWithdrawLinksByUserIdQuery, - updateWithdrawLinkStatus, - getWithdrawLinkBySecret, -} from "@/services/db" - -import { authOptions } from "@/app/api/auth/[...nextauth]/auth" -import { - RedeemWithdrawLinkOnChainResultStatus, - PaymentSendResult, - PayoutSpeed, - Status, - WalletCurrency, -} from "@/lib/graphql/generated" -import { getRealtimePriceQuery } from "@/services/galoy/query/get-real-time-price" -import { intraLedgerBtcPaymentSend } from "@/services/galoy/mutation/send-payment-intraledger/btc" -import { fetchUserData } from "@/services/galoy/query/me" -import { intraLedgerUsdPaymentSend } from "@/services/galoy/mutation/send-payment-intraledger/usd" -import { escrowApolloClient } from "@/services/galoy/client/escrow" -import { onChainUsdTxFee } from "@/services/galoy/query/on-chain-usd-tx-fee" -import { onChainUsdPaymentSend } from "@/services/galoy/mutation/on-chain-payment-sned" -import { amountCalculator } from "@/lib/amount-calculator" - -import { env } from "@/env" - -const resolvers = { - Query: { - getWithdrawLink: async (_: undefined, args: { voucherSecret: string }) => { - const { voucherSecret } = args - if (!isValidVoucherSecret(voucherSecret)) { - return new Error("Invalid voucher secret") - } - const res = await getWithdrawLinkBySecret({ voucherSecret }) - return res - }, - - getWithdrawLinksByUserId: async ( - _: undefined, - args: { - status?: string - }, - ) => { - const { status } = args - - const session = await getServerSession(authOptions) - const userData = session?.userData - if (!userData || !userData?.me?.defaultAccount?.wallets) { - return new Error("Unauthorized") - } - - const data = await getWithdrawLinksByUserIdQuery({ - userId: userData.me.id, - status, - }) - if (data instanceof Error) { - return new Error("Internal server error") - } - return data - }, - }, - - Mutation: { - createWithdrawLink: async ( - _: undefined, - args: { - input: { - salesAmountInCents: number - walletId: string - commissionPercentage: number - displayVoucherPrice: string - displayCurrency: string - } - }, - ) => { - const { commissionPercentage, salesAmountInCents, walletId } = args.input - const platformFeesInPpm = env.PLATFORM_FEES_IN_PPM - const session = await getServerSession(authOptions) - const userData = session?.userData - if (!userData || !userData?.me?.defaultAccount?.wallets) { - return new Error("Unauthorized") - } - - if (salesAmountInCents <= 0) return new Error("Invalid sales amount") - - const voucherAmountInCents = Number( - amountCalculator - .voucherAmountAfterPlatformFeesAndCommission({ - voucherPrice: salesAmountInCents, - commissionPercentage, - platformFeesInPpm, - }) - .toFixed(0), - ) - - if (voucherAmountInCents <= 0) return new Error("Invalid Voucher Amount") - if (commissionPercentage && commissionPercentage < 0) - return new Error("Invalid commission percentage") - - const escrowClient = escrowApolloClient() - const escrowData = await fetchUserData({ client: escrowClient }) - - if (escrowData instanceof Error) return escrowData - - if (!escrowData.me?.defaultAccount.wallets) { - return new Error("Internal Server Error") - } - - const { usdWallet: escrowUsdWallet } = getWalletDetails({ - wallets: escrowData.me?.defaultAccount.wallets, - }) - - if (!escrowUsdWallet || !escrowUsdWallet.id) - return new Error("Internal Server Error") - - const platformFeeInCents = Number( - amountCalculator - .platformFeesAmount({ - voucherPrice: salesAmountInCents, - platformFeesInPpm, - }) - .toFixed(0), - ) - const userWalletDetails = getWalletDetailsFromWalletId({ - wallets: userData.me?.defaultAccount.wallets, - walletId, - }) - - if (!userWalletDetails) return new Error("Wallet not found") - - if (userWalletDetails.walletCurrency === WalletCurrency.Btc) { - const realtimePrice = await getRealtimePriceQuery({ client: escrowClient }) - - if (realtimePrice instanceof Error) return realtimePrice - - const amountInSats = convertCentsToSats({ - response: realtimePrice, - cents: voucherAmountInCents, - }) - - if (amountInSats > userWalletDetails.balance) - return new Error("amount is more than wallet balance") - - const createWithdrawLinkResponse = await createWithdrawLinkMutation({ - commissionPercentage, - voucherAmountInCents, - salesAmountInCents, - userId: userData.me.id, - platformFee: platformFeeInCents, - displayVoucherPrice: args.input.displayVoucherPrice, - displayCurrency: args.input.displayCurrency, - }) - - if (createWithdrawLinkResponse instanceof Error) return createWithdrawLinkResponse - - const btcPaymentResponse = await intraLedgerBtcPaymentSend({ - token: session.accessToken, - amount: amountInSats, - memo: createMemo({ - voucherAmountInCents, - commissionPercentage, - identifierCode: createWithdrawLinkResponse.identifierCode, - }), - recipientWalletId: escrowUsdWallet?.id, - walletId: walletId, - }) - - if (btcPaymentResponse instanceof Error) return btcPaymentResponse - - if (btcPaymentResponse.intraLedgerPaymentSend.errors.length > 0) - return new Error(btcPaymentResponse.intraLedgerPaymentSend.errors[0].message) - - if ( - btcPaymentResponse.intraLedgerPaymentSend.status === PaymentSendResult.Success - ) { - const response = await updateWithdrawLinkStatus({ - id: createWithdrawLinkResponse.id, - status: Status.Active, - }) - return response - } - - return new Error("Payment failed") - } else if (userWalletDetails.walletCurrency === WalletCurrency.Usd) { - if (voucherAmountInCents > userWalletDetails.balance) - return new Error("amount is more than wallet balance") - - const createWithdrawLinkResponse = await createWithdrawLinkMutation({ - commissionPercentage, - voucherAmountInCents, - salesAmountInCents, - userId: userData.me.id, - platformFee: platformFeeInCents, - displayVoucherPrice: args.input.displayVoucherPrice, - displayCurrency: args.input.displayCurrency, - }) - - if (createWithdrawLinkResponse instanceof Error) return createWithdrawLinkResponse - const voucherAmountWithFeesInCents = Number( - amountCalculator - .voucherAmountAfterCommission({ - voucherPrice: salesAmountInCents, - commissionPercentage, - }) - .toFixed(0), - ) - - const usdPaymentResponse = await intraLedgerUsdPaymentSend({ - token: session.accessToken, - amount: voucherAmountWithFeesInCents, - memo: createMemo({ - voucherAmountInCents, - commissionPercentage, - identifierCode: createWithdrawLinkResponse.identifierCode, - }), - recipientWalletId: escrowUsdWallet?.id, - walletId: walletId, - }) - - if (usdPaymentResponse instanceof Error) return usdPaymentResponse - - if (usdPaymentResponse.intraLedgerUsdPaymentSend.errors.length > 0) - return new Error(usdPaymentResponse.intraLedgerUsdPaymentSend.errors[0].message) - - if ( - usdPaymentResponse.intraLedgerUsdPaymentSend.status === - PaymentSendResult.Success - ) { - const response = await updateWithdrawLinkStatus({ - id: createWithdrawLinkResponse.id, - status: Status.Active, - }) - return response - } - - return new Error("Payment failed") - } else { - return new Error("Invalid wallet Id for user") - } - }, - redeemWithdrawLinkOnChain: async ( - _: undefined, - args: { - input: { - voucherSecret: string - onChainAddress: string - } - }, - ) => { - const { voucherSecret, onChainAddress } = args.input - if (!isValidVoucherSecret(voucherSecret)) { - return new Error("Invalid voucher secret") - } - - const escrowClient = escrowApolloClient() - const escrowData = await fetchUserData({ client: escrowClient }) - - if (escrowData instanceof Error) return escrowData - - if (!escrowData.me?.defaultAccount.wallets) { - return new Error("Internal Server Error") - } - - const { usdWallet: escrowUsdWallet } = getWalletDetails({ - wallets: escrowData.me?.defaultAccount.wallets, - }) - - if (!escrowUsdWallet || !escrowUsdWallet.id) - return new Error("Internal Server Error") - - const getWithdrawLinkBySecretResponse = await getWithdrawLinkBySecret({ - voucherSecret, - }) - if (getWithdrawLinkBySecretResponse instanceof Error) - return getWithdrawLinkBySecretResponse - - if (!getWithdrawLinkBySecretResponse) { - return new Error("Withdraw link not found") - } - - if (getWithdrawLinkBySecretResponse.status === Status.Paid) { - return new Error("Withdraw link claimed") - } - - const onChainUsdTxFeeResponse = await onChainUsdTxFee({ - client: escrowClient, - input: { - address: onChainAddress, - amount: getWithdrawLinkBySecretResponse.voucherAmountInCents, - walletId: escrowUsdWallet?.id, - speed: PayoutSpeed.Fast, - }, - }) - - if (onChainUsdTxFeeResponse instanceof Error) return onChainUsdTxFeeResponse - - if ( - onChainUsdTxFeeResponse.onChainUsdTxFee.amount >= - getWithdrawLinkBySecretResponse.voucherAmountInCents - ) - return new Error("This Voucher Cannot Withdraw On Chain amount is less than fees") - - const response = await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, - status: Status.Paid, - }) - - if (response instanceof Error) return response - - const onChainUsdPaymentSendResponse = await onChainUsdPaymentSend({ - client: escrowClient, - input: { - address: onChainAddress, - amount: getWithdrawLinkBySecretResponse.voucherAmountInCents, - memo: createMemo({ - voucherAmountInCents: getWithdrawLinkBySecretResponse.voucherAmountInCents, - commissionPercentage: getWithdrawLinkBySecretResponse.commissionPercentage, - identifierCode: getWithdrawLinkBySecretResponse.identifierCode, - }), - speed: PayoutSpeed.Fast, - walletId: escrowUsdWallet?.id, - }, - }) - - if (onChainUsdPaymentSendResponse instanceof Error) { - await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, - status: Status.Active, - }) - return onChainUsdPaymentSendResponse - } - - if (onChainUsdPaymentSendResponse.onChainUsdPaymentSend.errors.length > 0) { - await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, - status: Status.Active, - }) - return new Error( - onChainUsdPaymentSendResponse.onChainUsdPaymentSend.errors[0].message, - ) - } - - if ( - onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status !== - PaymentSendResult.Success - ) { - await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, - status: Status.Active, - }) - return new Error( - `Transaction not successful got status ${onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status}`, - ) - } - - if ( - onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status === - PaymentSendResult.Success - ) { - return { - status: RedeemWithdrawLinkOnChainResultStatus.Success, - message: "Payment successful", - } - } - }, - }, -} - -export default resolvers - -const isValidVoucherSecret = (voucherSecret: string) => { - if (!voucherSecret) { - return false - } - - if (voucherSecret.length !== 12) { - return false - } - - if (!voucherSecret.match(/^[0-9a-zA-Z]+$/)) { - return false - } - - return true -} diff --git a/apps/voucher/graphql/resolvers/index.ts b/apps/voucher/graphql/resolvers/index.ts new file mode 100644 index 0000000000..4153ce912d --- /dev/null +++ b/apps/voucher/graphql/resolvers/index.ts @@ -0,0 +1,18 @@ +import { getWithdrawLink } from "./query/get-withdraw-link" +import { getWithdrawLinksByUserId } from "./query/get-withdraw-links-by-userId" +import { createWithdrawLink } from "./mutation/create-withdraw-link" +import { redeemWithdrawLinkOnChain } from "./mutation/redeem-withdraw-link-on-chain" + +const resolvers = { + Query: { + getWithdrawLink, + getWithdrawLinksByUserId, + }, + + Mutation: { + createWithdrawLink, + redeemWithdrawLinkOnChain, + }, +} + +export default resolvers diff --git a/apps/voucher/graphql/resolvers/mutation/create-withdraw-link.ts b/apps/voucher/graphql/resolvers/mutation/create-withdraw-link.ts new file mode 100644 index 0000000000..e852526c10 --- /dev/null +++ b/apps/voucher/graphql/resolvers/mutation/create-withdraw-link.ts @@ -0,0 +1,277 @@ +import { getServerSession } from "next-auth" + +import { + convertCentsToSats, + createMemo, + getWalletDetails, + getWalletDetailsFromWalletId, +} from "@/utils/helpers" +import { createWithdrawLinkMutation, updateWithdrawLinkStatus } from "@/services/db" + +import { authOptions } from "@/app/api/auth/[...nextauth]/auth" +import { PaymentSendResult, Status, WalletCurrency } from "@/lib/graphql/generated" +import { getRealtimePriceQuery } from "@/services/galoy/query/get-real-time-price" +import { intraLedgerBtcPaymentSend } from "@/services/galoy/mutation/send-payment-intraledger/btc" +import { fetchUserData } from "@/services/galoy/query/me" +import { intraLedgerUsdPaymentSend } from "@/services/galoy/mutation/send-payment-intraledger/usd" +import { escrowApolloClient } from "@/services/galoy/client/escrow" +import { amountCalculator } from "@/lib/amount-calculator" + +import { env } from "@/env" + +export const createWithdrawLink = async ( + _: undefined, + args: { + input: { + salesAmountInCents: number + walletId: string + commissionPercentage: number + displayVoucherPrice: string + displayCurrency: string + } + }, +) => { + const { commissionPercentage, salesAmountInCents, walletId } = args.input + const platformFeesInPpm = env.PLATFORM_FEES_IN_PPM + const session = await getServerSession(authOptions) + const userData = session?.userData + if (!userData || !userData?.me?.defaultAccount?.wallets) { + return new Error("Unauthorized") + } + + if (salesAmountInCents <= 0) return new Error("Invalid sales amount") + + // amount that would be sent to escrow + const voucherAmountAfterCommission = Number( + amountCalculator + .voucherAmountAfterCommission({ + voucherPrice: salesAmountInCents, + commissionPercentage, + }) + .toFixed(0), + ) + + // amount that would be sent to user + const voucherAmountAfterPlatformFeesAndCommission = Number( + amountCalculator + .voucherAmountAfterPlatformFeesAndCommission({ + voucherPrice: salesAmountInCents, + commissionPercentage, + platformFeesInPpm, + }) + .toFixed(0), + ) + + const platformFeeInCents = Number( + amountCalculator + .platformFeesAmount({ + voucherPrice: salesAmountInCents, + platformFeesInPpm, + }) + .toFixed(0), + ) + + if ( + voucherAmountAfterPlatformFeesAndCommission <= 0 || + voucherAmountAfterCommission <= 0 + ) + return new Error("Invalid Voucher Amount") + + if (commissionPercentage && commissionPercentage < 0) + return new Error("Invalid commission percentage") + + const escrowClient = escrowApolloClient() + const escrowData = await fetchUserData({ client: escrowClient }) + if (escrowData instanceof Error) return escrowData + if (!escrowData.me?.defaultAccount.wallets) { + return new Error("Internal Server Error") + } + + const { usdWallet: escrowUsdWallet } = getWalletDetails({ + wallets: escrowData.me?.defaultAccount.wallets, + }) + if (!escrowUsdWallet || !escrowUsdWallet.id) return new Error("Internal Server Error") + + const userWalletDetails = getWalletDetailsFromWalletId({ + wallets: userData.me?.defaultAccount.wallets, + walletId, + }) + + if (!userWalletDetails) return new Error("Wallet not found") + + if (userWalletDetails.walletCurrency === WalletCurrency.Btc) { + const realtimePrice = await getRealtimePriceQuery({ client: escrowClient }) + if (realtimePrice instanceof Error) return realtimePrice + + const voucherAmountAfterCommissionInSats = convertCentsToSats({ + response: realtimePrice, + cents: voucherAmountAfterCommission, + }) + return handleBtcWalletPayment({ + voucherAmountAfterPlatformFeesAndCommission, + userWalletBalance: userWalletDetails.balance, + voucherAmountAfterCommissionInSats, + salesAmountInCents, + commissionPercentage, + platformFeeInCents, + userId: userData.me?.id, + escrowUsdWalletId: escrowUsdWallet.id, + walletId, + displayVoucherPrice: args.input.displayVoucherPrice, + displayCurrency: args.input.displayCurrency, + accessToken: session.accessToken, + }) + } else if (userWalletDetails.walletCurrency === WalletCurrency.Usd) { + return handleUsdWalletPayment({ + voucherAmountAfterPlatformFeesAndCommission, + userWalletBalance: userWalletDetails.balance, + voucherAmountAfterCommission, + salesAmountInCents, + commissionPercentage, + platformFeeInCents, + accessToken: session.accessToken, + escrowUsdWalletId: escrowUsdWallet.id, + walletId, + userId: userData.me?.id, + displayVoucherPrice: args.input.displayVoucherPrice, + displayCurrency: args.input.displayCurrency, + }) + } else { + return new Error("Invalid wallet Id for user") + } +} + +export const handleUsdWalletPayment = async ({ + voucherAmountAfterPlatformFeesAndCommission, + userWalletBalance, + voucherAmountAfterCommission, + salesAmountInCents, + commissionPercentage, + platformFeeInCents, + accessToken, + escrowUsdWalletId, + walletId, + userId, + displayVoucherPrice, + displayCurrency, +}: { + voucherAmountAfterPlatformFeesAndCommission: number + userWalletBalance: number + voucherAmountAfterCommission: number + salesAmountInCents: number + commissionPercentage: number + platformFeeInCents: number + accessToken: string + escrowUsdWalletId: string + walletId: string + userId: string + displayVoucherPrice: string + displayCurrency: string +}) => { + if (voucherAmountAfterCommission > userWalletBalance) + return new Error("amount is more than wallet balance") + + const createWithdrawLinkResponse = await createWithdrawLinkMutation({ + commissionPercentage, + voucherAmountInCents: voucherAmountAfterPlatformFeesAndCommission, + salesAmountInCents, + userId, + platformFee: platformFeeInCents, + displayVoucherPrice, + displayCurrency, + }) + if (createWithdrawLinkResponse instanceof Error) return createWithdrawLinkResponse + + const usdPaymentResponse = await intraLedgerUsdPaymentSend({ + token: accessToken, + amount: voucherAmountAfterCommission, + memo: createMemo({ + voucherAmountInCents: voucherAmountAfterPlatformFeesAndCommission, + commissionPercentage, + identifierCode: createWithdrawLinkResponse.identifierCode, + }), + recipientWalletId: escrowUsdWalletId, + walletId, + }) + if (usdPaymentResponse instanceof Error) return usdPaymentResponse + if (usdPaymentResponse.intraLedgerUsdPaymentSend.errors.length > 0) + return new Error(usdPaymentResponse.intraLedgerUsdPaymentSend.errors[0].message) + + if (usdPaymentResponse.intraLedgerUsdPaymentSend.status === PaymentSendResult.Success) { + const response = await updateWithdrawLinkStatus({ + id: createWithdrawLinkResponse.id, + status: Status.Active, + }) + return response + } + + return new Error("Payment failed") +} + +export const handleBtcWalletPayment = async ({ + voucherAmountAfterPlatformFeesAndCommission, + userWalletBalance, + voucherAmountAfterCommissionInSats, + salesAmountInCents, + commissionPercentage, + platformFeeInCents, + userId, + escrowUsdWalletId, + walletId, + displayVoucherPrice, + displayCurrency, + accessToken, +}: { + voucherAmountAfterPlatformFeesAndCommission: number + userWalletBalance: number + voucherAmountAfterCommissionInSats: number + salesAmountInCents: number + commissionPercentage: number + platformFeeInCents: number + userId: string + escrowUsdWalletId: string + walletId: string + displayVoucherPrice: string + displayCurrency: string + accessToken: string +}) => { + if (voucherAmountAfterCommissionInSats > userWalletBalance) + return new Error("amount is more than wallet balance") + + const createWithdrawLinkResponse = await createWithdrawLinkMutation({ + commissionPercentage, + voucherAmountInCents: voucherAmountAfterPlatformFeesAndCommission, + salesAmountInCents, + userId, + platformFee: platformFeeInCents, + displayVoucherPrice, + displayCurrency, + }) + + if (createWithdrawLinkResponse instanceof Error) return createWithdrawLinkResponse + + const btcPaymentResponse = await intraLedgerBtcPaymentSend({ + token: accessToken, + amount: voucherAmountAfterCommissionInSats, + memo: createMemo({ + voucherAmountInCents: voucherAmountAfterPlatformFeesAndCommission, + commissionPercentage, + identifierCode: createWithdrawLinkResponse.identifierCode, + }), + recipientWalletId: escrowUsdWalletId, + walletId, + }) + if (btcPaymentResponse instanceof Error) return btcPaymentResponse + if (btcPaymentResponse.intraLedgerPaymentSend.errors.length > 0) + return new Error(btcPaymentResponse.intraLedgerPaymentSend.errors[0].message) + + if (btcPaymentResponse.intraLedgerPaymentSend.status === PaymentSendResult.Success) { + const response = await updateWithdrawLinkStatus({ + id: createWithdrawLinkResponse.id, + status: Status.Active, + }) + return response + } + + return new Error("Payment failed") +} diff --git a/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts b/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts new file mode 100644 index 0000000000..4a74a9b25f --- /dev/null +++ b/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts @@ -0,0 +1,136 @@ +import { createMemo, getWalletDetails, isValidVoucherSecret } from "@/utils/helpers" +import { updateWithdrawLinkStatus, getWithdrawLinkBySecret } from "@/services/db" + +import { + RedeemWithdrawLinkOnChainResultStatus, + PaymentSendResult, + PayoutSpeed, + Status, +} from "@/lib/graphql/generated" +import { fetchUserData } from "@/services/galoy/query/me" +import { escrowApolloClient } from "@/services/galoy/client/escrow" +import { onChainUsdTxFee } from "@/services/galoy/query/on-chain-usd-tx-fee" +import { onChainUsdPaymentSend } from "@/services/galoy/mutation/on-chain-payment-sned" + +export const redeemWithdrawLinkOnChain = async ( + _: undefined, + args: { + input: { + voucherSecret: string + onChainAddress: string + } + }, +) => { + const { voucherSecret, onChainAddress } = args.input + if (!isValidVoucherSecret(voucherSecret)) { + return new Error("Invalid voucher secret") + } + + const escrowClient = escrowApolloClient() + const escrowData = await fetchUserData({ client: escrowClient }) + + if (escrowData instanceof Error) return escrowData + if (!escrowData.me?.defaultAccount.wallets) { + return new Error("Internal Server Error") + } + + const { usdWallet: escrowUsdWallet } = getWalletDetails({ + wallets: escrowData.me?.defaultAccount.wallets, + }) + if (!escrowUsdWallet || !escrowUsdWallet.id) return new Error("Internal Server Error") + + const getWithdrawLinkBySecretResponse = await getWithdrawLinkBySecret({ + voucherSecret, + }) + if (getWithdrawLinkBySecretResponse instanceof Error) + return getWithdrawLinkBySecretResponse + + if (!getWithdrawLinkBySecretResponse) { + return new Error("Withdraw link not found") + } + + if (getWithdrawLinkBySecretResponse.status === Status.Paid) { + return new Error("Withdraw link claimed") + } + + const onChainUsdTxFeeResponse = await onChainUsdTxFee({ + client: escrowClient, + input: { + address: onChainAddress, + amount: getWithdrawLinkBySecretResponse.voucherAmountInCents, + walletId: escrowUsdWallet?.id, + speed: PayoutSpeed.Fast, + }, + }) + + if (onChainUsdTxFeeResponse instanceof Error) return onChainUsdTxFeeResponse + + if ( + onChainUsdTxFeeResponse.onChainUsdTxFee.amount >= + getWithdrawLinkBySecretResponse.voucherAmountInCents + ) + return new Error("This Voucher Cannot Withdraw On Chain amount is less than fees") + + const response = await updateWithdrawLinkStatus({ + id: getWithdrawLinkBySecretResponse.id, + status: Status.Paid, + }) + + if (response instanceof Error) return response + + const onChainUsdPaymentSendResponse = await onChainUsdPaymentSend({ + client: escrowClient, + input: { + address: onChainAddress, + amount: getWithdrawLinkBySecretResponse.voucherAmountInCents, + memo: createMemo({ + voucherAmountInCents: getWithdrawLinkBySecretResponse.voucherAmountInCents, + commissionPercentage: getWithdrawLinkBySecretResponse.commissionPercentage, + identifierCode: getWithdrawLinkBySecretResponse.identifierCode, + }), + speed: PayoutSpeed.Fast, + walletId: escrowUsdWallet?.id, + }, + }) + + if (onChainUsdPaymentSendResponse instanceof Error) { + await updateWithdrawLinkStatus({ + id: getWithdrawLinkBySecretResponse.id, + status: Status.Active, + }) + return onChainUsdPaymentSendResponse + } + + if (onChainUsdPaymentSendResponse.onChainUsdPaymentSend.errors.length > 0) { + await updateWithdrawLinkStatus({ + id: getWithdrawLinkBySecretResponse.id, + status: Status.Active, + }) + return new Error( + onChainUsdPaymentSendResponse.onChainUsdPaymentSend.errors[0].message, + ) + } + + if ( + onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status !== + PaymentSendResult.Success + ) { + await updateWithdrawLinkStatus({ + id: getWithdrawLinkBySecretResponse.id, + status: Status.Active, + }) + return new Error( + `Transaction not successful got status ${onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status}`, + ) + } + + if ( + onChainUsdPaymentSendResponse.onChainUsdPaymentSend.status === + PaymentSendResult.Success + ) { + return { + status: RedeemWithdrawLinkOnChainResultStatus.Success, + message: "Payment successful", + } + } +} diff --git a/apps/voucher/graphql/resolvers/query/get-withdraw-link.ts b/apps/voucher/graphql/resolvers/query/get-withdraw-link.ts new file mode 100644 index 0000000000..1659e09cb6 --- /dev/null +++ b/apps/voucher/graphql/resolvers/query/get-withdraw-link.ts @@ -0,0 +1,11 @@ +import { getWithdrawLinkBySecret } from "@/services/db" +import { isValidVoucherSecret } from "@/utils/helpers" + +export const getWithdrawLink = async (_: undefined, args: { voucherSecret: string }) => { + const { voucherSecret } = args + if (!isValidVoucherSecret(voucherSecret)) { + return new Error("Invalid voucher secret") + } + const res = await getWithdrawLinkBySecret({ voucherSecret }) + return res +} diff --git a/apps/voucher/graphql/resolvers/query/get-withdraw-links-by-userId.ts b/apps/voucher/graphql/resolvers/query/get-withdraw-links-by-userId.ts new file mode 100644 index 0000000000..f3081c6ee7 --- /dev/null +++ b/apps/voucher/graphql/resolvers/query/get-withdraw-links-by-userId.ts @@ -0,0 +1,28 @@ +import { getServerSession } from "next-auth" + +import { getWithdrawLinksByUserIdQuery } from "@/services/db" +import { authOptions } from "@/app/api/auth/[...nextauth]/auth" + +export const getWithdrawLinksByUserId = async ( + _: undefined, + args: { + status?: string + }, +) => { + const { status } = args + + const session = await getServerSession(authOptions) + const userData = session?.userData + if (!userData || !userData?.me?.defaultAccount?.wallets) { + return new Error("Unauthorized") + } + + const data = await getWithdrawLinksByUserIdQuery({ + userId: userData.me.id, + status, + }) + if (data instanceof Error) { + return new Error("Internal server error") + } + return data +} diff --git a/apps/voucher/utils/helpers.ts b/apps/voucher/utils/helpers.ts index 31e1ec4c83..d20fc681d2 100644 --- a/apps/voucher/utils/helpers.ts +++ b/apps/voucher/utils/helpers.ts @@ -155,3 +155,19 @@ export const createMemo = ({ commissionPercentage ? `with ${commissionPercentage}% Commission` : "" }, Voucher Identifier Code: ${identifierCode}` } + +export const isValidVoucherSecret = (voucherSecret: string) => { + if (!voucherSecret) { + return false + } + + if (voucherSecret.length !== 12) { + return false + } + + if (!voucherSecret.match(/^[0-9a-zA-Z]+$/)) { + return false + } + + return true +}