From 5e132066d842e978d250fbd1d1161dbfdcb2e2fd Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Tue, 30 Sep 2025 17:45:12 -0500 Subject: [PATCH 1/4] fix(voucher): validate pending status in onchain payments --- .../app/api/lnurlw/callback/[id]/route.ts | 25 ++++++++++++++++--- .../mutation/redeem-withdraw-link-on-chain.ts | 6 +++++ apps/voucher/services/db/index.ts | 2 +- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts index 1ccd8a5998..90e46d6f04 100644 --- a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts +++ b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts @@ -1,7 +1,11 @@ import { NextRequest } from "next/server" -import { lockVoucherK1 } from "@/services/lock" -import { getWithdrawLinkByK1Query, updateWithdrawLinkStatus } from "@/services/db" +import { lockVoucherSecret } from "@/services/lock" +import { + getWithdrawLinkByK1Query, + updateWithdrawLinkStatus, + WithdrawLink, +} from "@/services/db" import { createMemo, getWalletDetails, decodeInvoice } from "@/utils/helpers" import { PaymentSendResult, Status } from "@/lib/graphql/generated" import { escrowApolloClient } from "@/services/galoy/client/escrow" @@ -33,7 +37,22 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri if (usdWallet instanceof Error || !usdWallet) return Response.json({ status: "ERROR", reason: "Internal Server Error" }) - const result = await lockVoucherK1(k1, async () => { + let withdrawLink: WithdrawLink | undefined | Error + try { + withdrawLink = await getWithdrawLinkByK1Query({ k1 }) + } catch (error) { + console.error("error paying lnurlw", error) + return new Error("Internal Server Error") + } + + if (!withdrawLink) return new Error("Withdraw link not found") + + if (withdrawLink instanceof Error) return new Error("Internal Server Error") + + if (withdrawLink.id !== id) + return Response.json({ error: "Invalid Request", status: 400 }) + + return lockVoucherSecret(withdrawLink.voucherSecret, async () => { try { const withdrawLink = await getWithdrawLinkByK1Query({ k1 }) 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 index 7f549e438a..5054aad312 100644 --- a/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts +++ b/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts @@ -51,6 +51,12 @@ export const redeemWithdrawLinkOnChain = async ( return new Error("Withdraw link not found") } + if (getWithdrawLinkBySecretResponse.status === Status.Pending) { + return new Error( + "Withdrawal link is in pending state. Please contact support if the error persists.", + ) + } + if (getWithdrawLinkBySecretResponse.status === Status.Paid) { return new Error("Withdraw link claimed") } diff --git a/apps/voucher/services/db/index.ts b/apps/voucher/services/db/index.ts index c3625ecf2e..dfe7fa9d5a 100644 --- a/apps/voucher/services/db/index.ts +++ b/apps/voucher/services/db/index.ts @@ -5,7 +5,7 @@ import { knex } from "./knex" import { generateCode, generateRandomHash } from "@/utils/helpers" import { Status } from "@/lib/graphql/generated" -type WithdrawLink = { +export type WithdrawLink = { id: string userId: string identifierCode: string From 4d4b1c1142e3686b29754f899b1a76a60e067956 Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Tue, 30 Sep 2025 17:51:08 -0500 Subject: [PATCH 2/4] fix: spell check typos --- .../public/{pheonix-logo.png => phoenix-logo.png} | Bin core/api/src/servers/exporter.ts | 2 +- .../src/notification_event/price_changed.rs | 2 +- flake.nix | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename apps/pay/public/{pheonix-logo.png => phoenix-logo.png} (100%) diff --git a/apps/pay/public/pheonix-logo.png b/apps/pay/public/phoenix-logo.png similarity index 100% rename from apps/pay/public/pheonix-logo.png rename to apps/pay/public/phoenix-logo.png diff --git a/core/api/src/servers/exporter.ts b/core/api/src/servers/exporter.ts index 1b7afda838..932bba9ac1 100644 --- a/core/api/src/servers/exporter.ts +++ b/core/api/src/servers/exporter.ts @@ -332,7 +332,7 @@ const getWalletBalance = async (walletId: WalletId): Promise => { } return walletBalance } catch (err) { - logger.warn({ err }, `Could not get wallet balace for id: ${walletId}.`) + logger.warn({ err }, `Could not get wallet balance for id: ${walletId}.`) return 0 } finally { inProgressBalanceQueries.delete(inProgressKey) diff --git a/core/notifications/src/notification_event/price_changed.rs b/core/notifications/src/notification_event/price_changed.rs index 662e24529a..6294f2162a 100644 --- a/core/notifications/src/notification_event/price_changed.rs +++ b/core/notifications/src/notification_event/price_changed.rs @@ -40,7 +40,7 @@ pub struct PriceChanged { impl PriceChanged { const NOTIFICATION_THRESHOLD: ChangePercentage = ChangePercentage(5.0); - #[allow(deprecated)] // recommndation is try_days but Option.unwrap() is not yet const fn + #[allow(deprecated)] // recommendation is try_days but Option.unwrap() is not yet const fn const COOL_OFF_PERIOD: chrono::Duration = chrono::Duration::days(2); pub fn should_notify(&self, last_trigger: Option>) -> bool { diff --git a/flake.nix b/flake.nix index 8a9ecbf738..52891c4055 100644 --- a/flake.nix +++ b/flake.nix @@ -176,7 +176,7 @@ cp -rpv build/$name-$system/app/* "$out/" # Need to escape this shell variable which should not be - # iterpreted in Nix as a variable nor a shell variable when run + # interpreted in Nix as a variable nor a shell variable when run # but rather a literal string which happens to be a shell # variable. Nuclear arms race of quoting and escaping special # characters to make this work... From 7c9f5a6cebd09b2742aaff25395733409facd231 Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Tue, 30 Sep 2025 18:09:14 -0500 Subject: [PATCH 3/4] fix: lint issues --- apps/voucher/app/api/lnurlw/callback/[id]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts index 90e46d6f04..347733da3e 100644 --- a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts +++ b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts @@ -52,7 +52,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri if (withdrawLink.id !== id) return Response.json({ error: "Invalid Request", status: 400 }) - return lockVoucherSecret(withdrawLink.voucherSecret, async () => { + const result = await lockVoucherSecret(withdrawLink.voucherSecret, async () => { try { const withdrawLink = await getWithdrawLinkByK1Query({ k1 }) From c756290a5a5f43c1c5bfbdd7ad0e3393ff4c07e8 Mon Sep 17 00:00:00 2001 From: Juan P Lopez Date: Tue, 30 Sep 2025 18:24:34 -0500 Subject: [PATCH 4/4] fix: return error types --- .../app/api/lnurlw/callback/[id]/route.ts | 10 +++++-- .../mutation/redeem-withdraw-link-on-chain.ts | 30 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts index 347733da3e..92f80f693c 100644 --- a/apps/voucher/app/api/lnurlw/callback/[id]/route.ts +++ b/apps/voucher/app/api/lnurlw/callback/[id]/route.ts @@ -42,12 +42,16 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri withdrawLink = await getWithdrawLinkByK1Query({ k1 }) } catch (error) { console.error("error paying lnurlw", error) - return new Error("Internal Server Error") + return Response.json({ status: "ERROR", reason: "Internal Server Error" }) } - if (!withdrawLink) return new Error("Withdraw link not found") + if (!withdrawLink) { + return Response.json({ status: "ERROR", reason: "Withdraw link not found" }) + } - if (withdrawLink instanceof Error) return new Error("Internal Server Error") + if (withdrawLink instanceof Error) { + return Response.json({ status: "ERROR", reason: "Internal Server Error" }) + } if (withdrawLink.id !== id) return Response.json({ error: "Invalid Request", status: 400 }) 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 index 5054aad312..c99abf90d2 100644 --- a/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts +++ b/apps/voucher/graphql/resolvers/mutation/redeem-withdraw-link-on-chain.ts @@ -41,23 +41,22 @@ export const redeemWithdrawLinkOnChain = async ( if (!escrowUsdWallet || !escrowUsdWallet.id) return new Error("Internal Server Error") return lockVoucherSecret(voucherSecret, async () => { - const getWithdrawLinkBySecretResponse = await getWithdrawLinkBySecret({ + const withdrawLink = await getWithdrawLinkBySecret({ voucherSecret, }) - if (getWithdrawLinkBySecretResponse instanceof Error) - return getWithdrawLinkBySecretResponse + if (withdrawLink instanceof Error) return withdrawLink - if (!getWithdrawLinkBySecretResponse) { + if (!withdrawLink) { return new Error("Withdraw link not found") } - if (getWithdrawLinkBySecretResponse.status === Status.Pending) { + if (withdrawLink.status === Status.Pending) { return new Error( "Withdrawal link is in pending state. Please contact support if the error persists.", ) } - if (getWithdrawLinkBySecretResponse.status === Status.Paid) { + if (withdrawLink.status === Status.Paid) { return new Error("Withdraw link claimed") } @@ -65,7 +64,7 @@ export const redeemWithdrawLinkOnChain = async ( client: escrowClient, input: { address: onChainAddress, - amount: getWithdrawLinkBySecretResponse.voucherAmountInCents, + amount: withdrawLink.voucherAmountInCents, walletId: escrowUsdWallet?.id, speed: PayoutSpeed.Fast, }, @@ -73,14 +72,13 @@ export const redeemWithdrawLinkOnChain = async ( if (onChainUsdTxFeeResponse instanceof Error) return onChainUsdTxFeeResponse const totalAmountToBePaid = - getWithdrawLinkBySecretResponse.voucherAmountInCents - - onChainUsdTxFeeResponse.onChainUsdTxFee.amount + withdrawLink.voucherAmountInCents - onChainUsdTxFeeResponse.onChainUsdTxFee.amount if (totalAmountToBePaid <= 0) return new Error("This Voucher Cannot Withdraw On Chain amount is less than fees") const response = await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, + id: withdrawLink.id, status: Status.Paid, }) @@ -92,9 +90,9 @@ export const redeemWithdrawLinkOnChain = async ( address: onChainAddress, amount: totalAmountToBePaid, memo: createMemo({ - voucherAmountInCents: getWithdrawLinkBySecretResponse.voucherAmountInCents, - commissionPercentage: getWithdrawLinkBySecretResponse.commissionPercentage, - identifierCode: getWithdrawLinkBySecretResponse.identifierCode, + voucherAmountInCents: withdrawLink.voucherAmountInCents, + commissionPercentage: withdrawLink.commissionPercentage, + identifierCode: withdrawLink.identifierCode, }), speed: PayoutSpeed.Fast, walletId: escrowUsdWallet?.id, @@ -103,7 +101,7 @@ export const redeemWithdrawLinkOnChain = async ( if (onChainUsdPaymentSendResponse instanceof Error) { await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, + id: withdrawLink.id, status: Status.Active, }) return onChainUsdPaymentSendResponse @@ -111,7 +109,7 @@ export const redeemWithdrawLinkOnChain = async ( if (onChainUsdPaymentSendResponse.onChainUsdPaymentSend.errors.length > 0) { await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, + id: withdrawLink.id, status: Status.Active, }) return new Error( @@ -124,7 +122,7 @@ export const redeemWithdrawLinkOnChain = async ( PaymentSendResult.Success ) { await updateWithdrawLinkStatus({ - id: getWithdrawLinkBySecretResponse.id, + id: withdrawLink.id, status: Status.Active, }) return new Error(