Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3b55991
feat: add clientPayment table
lissavxo Jul 14, 2025
04ffce3
feat: add generatePaymentId endpoint
lissavxo Jul 14, 2025
735d9dd
fix: parse address paymentId
lissavxo Aug 11, 2025
33ccad2
refactor: clean up
lissavxo Aug 21, 2025
b8308eb
refactor: clean up
lissavxo Aug 25, 2025
7a03089
feat: add amount to client payment
lissavxo Aug 26, 2025
abd8fc9
fix: update clientpayment method
lissavxo Aug 27, 2025
8078a46
feat: add generatePaymentId endpoint
lissavxo Jul 14, 2025
ad196aa
feat: update paymentId
lissavxo Aug 14, 2025
7a95368
feat: update client payment comfirmed
lissavxo Aug 15, 2025
8d3f911
fix: generatePaymentId duplicated
lissavxo Aug 18, 2025
74e1ce3
fix: typo confirmed
lissavxo Aug 21, 2025
8713202
refactor: clean up
lissavxo Aug 21, 2025
442a319
fix: check amount to update clientPayment
lissavxo Aug 27, 2025
157cf0d
fix: post params paymentId
lissavxo Aug 27, 2025
47ec902
chore: remove id from client payment
lissavxo Sep 3, 2025
f0e2622
chore: paymentId id
lissavxo Sep 4, 2025
15736e1
chore: rename clientPayments obj
lissavxo Sep 12, 2025
ed212f5
feat: add clientPayment table
lissavxo Jul 14, 2025
c8c79e7
refactor: clean up
lissavxo Aug 21, 2025
30c0133
refactor: clean up
lissavxo Aug 25, 2025
5542ede
feat: add amount to client payment
lissavxo Aug 26, 2025
eefbd7a
feat: update client payment comfirmed
lissavxo Aug 15, 2025
c1c9a72
fix: check amount updateClientPaymentStatus
lissavxo Sep 15, 2025
e86ecc3
refactor: clean up
lissavxo Sep 15, 2025
ea5eee2
fix: update client payment
lissavxo Sep 23, 2025
9a87a26
fix: getClientPayment
lissavxo Sep 24, 2025
c215cc9
feat: allow cors paymentid
lissavxo Sep 25, 2025
f9db933
chore: remove duplicated migration
lissavxo Oct 2, 2025
dc63345
feat: consider invalid address
chedieck Oct 2, 2025
723cdb4
fix: method not allowed status code
chedieck Oct 2, 2025
ab4b944
refactor: useless const
chedieck Oct 2, 2025
22b7370
test: handleUpdateClientPaymentStatus
lissavxo Oct 2, 2025
b5f13d5
fix: check address handleUpdateClientPaymentStatus
lissavxo Oct 6, 2025
962c953
fix: transaction service PRICES_CONNECTION_TIMEOUT
lissavxo Oct 7, 2025
6ff0d94
fix: ignore status confirmed handleUpdateClientPaymentStatus
lissavxo Oct 7, 2025
33eb251
refactor: remove useless question marks
chedieck Oct 14, 2025
92b2ecc
fix: typo
chedieck Oct 14, 2025
d80801e
feat: remove other cors allowances
chedieck Oct 15, 2025
e8aadce
refactor: early return if for better reading
chedieck Oct 15, 2025
f31fccb
refactor: remove rebug comment
chedieck Oct 15, 2025
7f700ce
refactor: more early returns
chedieck Oct 15, 2025
21f4c8f
feat: parse address
chedieck Oct 15, 2025
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
2 changes: 1 addition & 1 deletion constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const RESPONSE_MESSAGES = {
INVALID_OUTPUT_SCRIPT_LENGTH_500: (l: number) => { return { statusCode: 500, message: `Invalid outputScript length ${l}` } },
FAILED_TO_PARSE_TX_OP_RETURN_500: { statusCode: 500, message: 'Failed to parse OP_RETURN data in Tx.' },
PAYBUTTON_ID_NOT_PROVIDED_400: { statusCode: 400, message: 'Paybutton id not provided' },
METHOD_NOT_ALLOWED: { statusCode: 500, message: 'Method not allowed.' },
METHOD_NOT_ALLOWED_405: { statusCode: 405, message: 'Method not allowed.' },
USER_HAS_NO_ORGANIZATION_400: { statusCode: 400, message: 'User does not belong to an organization.' },
USER_ALREADY_HAS_ORGANIZATION_400: { statusCode: 400, message: 'User already belongs to an organization.' },
USER_HAS_ALREADY_USED_INVITE_400: { statusCode: 400, message: 'You have already used this invite.' },
Expand Down
8 changes: 4 additions & 4 deletions pages/api/paybutton/download/transactions/[paybuttonId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { fetchUserProfileFromId } from 'services/userService'
export default async (req: any, res: any): Promise<void> => {
try {
if (req.method !== 'GET') {
throw new Error(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.message)
throw new Error(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.message)
}
if ((req.query.paybuttonId === undefined)) {
throw new Error(RESPONSE_MESSAGES.PAYBUTTON_ID_NOT_PROVIDED_400.message)
Expand Down Expand Up @@ -59,9 +59,9 @@ export default async (req: any, res: any): Promise<void> => {
res.status(RESPONSE_MESSAGES.PAYBUTTON_ID_NOT_PROVIDED_400.statusCode)
.json(RESPONSE_MESSAGES.PAYBUTTON_ID_NOT_PROVIDED_400)
break
case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.message:
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.message:
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405)
break
case RESPONSE_MESSAGES.MISSING_PRICE_FOR_TRANSACTION_400.message:
res.status(RESPONSE_MESSAGES.MISSING_PRICE_FOR_TRANSACTION_400.statusCode)
Expand Down
2 changes: 1 addition & 1 deletion pages/api/paybutton/transactions/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ export default async (req: any, res: any): Promise<void> => {
}
}
} else {
res.status(405).json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
res.status(405).json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405)
}
}
8 changes: 4 additions & 4 deletions pages/api/payments/download/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { fetchAllPaymentsByUserId } from 'services/transactionService'
export default async (req: any, res: any): Promise<void> => {
try {
if (req.method !== 'GET') {
throw new Error(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.message)
throw new Error(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.message)
}

await setSession(req, res)
Expand Down Expand Up @@ -57,9 +57,9 @@ export default async (req: any, res: any): Promise<void> => {
await downloadTxsFile(res, quoteSlug, timezone, transactions, userId)
} catch (error: any) {
switch (error.message) {
case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.message:
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.message:
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405)
break
case RESPONSE_MESSAGES.MISSING_PRICE_FOR_TRANSACTION_400.message:
res.status(RESPONSE_MESSAGES.MISSING_PRICE_FOR_TRANSACTION_400.statusCode)
Expand Down
40 changes: 40 additions & 0 deletions pages/api/payments/paymentId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Decimal } from '@prisma/client/runtime/library'
import { generatePaymentId } from 'services/transactionService'
import { parseAddress, parseCreatePaymentIdPOSTRequest } from 'utils/validators'
import { RESPONSE_MESSAGES } from 'constants/index'
import { runMiddleware } from 'utils/index'

import Cors from 'cors'

const cors = Cors({
methods: ['POST']
})

export default async (req: any, res: any): Promise<void> => {
await runMiddleware(req, res, cors)
if (req.method === 'POST') {
try {
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined

const paymentId = await generatePaymentId(address, amount)
Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix unsafe type cast for amount parameter.

Line 19 performs an unsafe type cast of values.amount to Decimal | undefined. According to the validator code in utils/validators.ts, parseCreatePaymentIdPOSTRequest returns amount as a string or undefined, not a Decimal. This cast does not perform any conversion and will cause type mismatches when passed to generatePaymentId, which expects Prisma.Decimal.

Apply this diff to properly convert the amount:

+import { Prisma } from '@prisma/client'
+
       const values = parseCreatePaymentIdPOSTRequest(req.body)
       const address = parseAddress(values.address)
-      const amount = values.amount as Decimal | undefined
+      const amount = values.amount !== undefined 
+        ? new Prisma.Decimal(values.amount) 
+        : undefined
 
       const paymentId = await generatePaymentId(address, amount)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined
const paymentId = await generatePaymentId(address, amount)
import { Prisma } from '@prisma/client'
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount !== undefined
? new Prisma.Decimal(values.amount)
: undefined
const paymentId = await generatePaymentId(address, amount)


res.status(200).json({ paymentId })
} catch (error: any) {
switch (error.message) {
case RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message:
res.status(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.statusCode).json(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400)
break
case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message:
res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400)
break
default:
res.status(500).json({ statusCode: 500, message: error.message })
}
}
} else {
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE `ClientPayment` (
`paymentId` VARCHAR(191) NOT NULL,
`status` ENUM('PENDING', 'ADDED_TO_MEMPOOL', 'CONFIRMED') NOT NULL,
`addressString` VARCHAR(191) NOT NULL,
`amount` DECIMAL(65, 30) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,

PRIMARY KEY (`paymentId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `ClientPayment` ADD CONSTRAINT `ClientPayment_addressString_fkey` FOREIGN KEY (`addressString`) REFERENCES `Address`(`address`) ON DELETE RESTRICT ON UPDATE CASCADE;
17 changes: 17 additions & 0 deletions prisma-local/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ model Address {
syncing Boolean @default(false)
paybuttons AddressesOnButtons[]
transactions Transaction[]
clientPayments ClientPayment[]

@@index([networkId], map: "Address_networkId_fkey")
}
Expand Down Expand Up @@ -271,3 +272,19 @@ model Invoice {
@@unique([invoiceNumber, userId], map: "Invoice_invoiceNumber_userId_unique_constraint")
@@unique([transactionId, userId], map: "Invoice_transactionId_userId_unique_constraint")
}

enum ClientPaymentStatus {
PENDING
ADDED_TO_MEMPOOL
CONFIRMED
}

model ClientPayment {
paymentId String @id
status ClientPaymentStatus
addressString String
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
41 changes: 39 additions & 2 deletions services/chronikService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
upsertTransaction,
getSimplifiedTransactions,
getSimplifiedTrasaction,
connectAllTransactionsToPrices
connectAllTransactionsToPrices,
updateClientPaymentStatus,
getClientPayment
} from './transactionService'
import { Address, Prisma } from '@prisma/client'
import { Address, Prisma, ClientPaymentStatus } from '@prisma/client'
import xecaddr from 'xecaddrjs'
import { getAddressPrefix, satoshisToUnit } from 'utils/index'
import { fetchAddressesArray, fetchAllAddressesForNetworkId, getEarliestUnconfirmedTxTimestampForAddress, getLatestConfirmedTxTimestampForAddress, setSyncing, setSyncingBatch, updateLastSynced, updateManyLastSynced } from './addressService'
Expand All @@ -28,6 +30,7 @@ import { appendTxsToFile } from 'prisma-local/seeds/transactions'
import { PHASE_PRODUCTION_BUILD } from 'next/dist/shared/lib/constants'
import { syncPastDaysNewerPrices } from './priceService'
import { AddressType } from 'ecashaddrjs/dist/types'
import { DecimalJsLike } from '@prisma/client/runtime/library'

const decoder = new TextDecoder()

Expand Down Expand Up @@ -551,10 +554,38 @@ export class ChronikBlockchainClient {
}
}

private async handleUpdateClientPaymentStatus (
txAmount: string | number | Prisma.Decimal | DecimalJsLike,
opReturn: string | undefined, status: ClientPaymentStatus,
txAddress: string): Promise<void> {
const parsedOpReturn = parseOpReturnData(opReturn ?? '')
const paymentId = parsedOpReturn.paymentId
if (paymentId === undefined || paymentId === '') {
return
}
const clientPayment = await getClientPayment(paymentId)
if (clientPayment === null || clientPayment.status === 'CONFIRMED') {
return
}
if (clientPayment.amount !== null) {
if (Number(clientPayment.amount) === Number(txAmount) &&
(clientPayment.addressString === txAddress)) {
await updateClientPaymentStatus(paymentId, status)
}
} else {
if (clientPayment.addressString === txAddress) {
await updateClientPaymentStatus(paymentId, status)
}
}
}
Comment on lines +557 to +580
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix typo, property access, Decimal comparison, validation, and error handling.

Several critical issues:

  1. Line 560: Typo in parameter name txAdress (missing 'd') — should be txAddress
  2. Lines 571, 575: clientPayment?.addressString doesn't exist. Based on getClientPayment returning { include: { address: true } }, use clientPayment.address.address instead
  3. Line 570: Incorrect Decimal comparison using Number() equality. Prisma Decimal must be compared via .equals() method
  4. Line 563: Missing paymentId format validation. Should validate against /^[0-9a-fA-F]{32}$/ before DB lookup
  5. Missing error handling: Wrap DB calls in try-catch to handle unknown paymentIds gracefully

Apply this diff:

-  private async handleUpdateClientPaymentStatus (
-    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
-    opReturn: string | undefined, status: ClientPaymentStatus,
-    txAdress: string): Promise<void> {
+  private async handleUpdateClientPaymentStatus (
+    txAmount: string | number | Prisma.Decimal | DecimalJsLike,
+    opReturn: string | undefined,
+    status: ClientPaymentStatus,
+    txAddress: string
+  ): Promise<void> {
     const parsedOpReturn = parseOpReturnData(opReturn ?? '')
     const paymentId = parsedOpReturn.paymentId
-    if (paymentId !== undefined && paymentId !== '') {
+    
+    // Validate paymentId format (32 hex characters)
+    if (typeof paymentId !== 'string' || !/^[0-9a-fA-F]{32}$/.test(paymentId)) {
+      return
+    }
+    
+    try {
       const clientPayment = await getClientPayment(paymentId)
-      if (clientPayment !== null) {
-        if (clientPayment.status === 'CONFIRMED') {
-          return
-        }
-        if (clientPayment?.amount !== null) {
-          if (Number(clientPayment?.amount) === Number(txAmount) &&
-            (clientPayment?.addressString === txAdress)) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        } else {
-          if (clientPayment?.addressString === txAdress) {
-            await updateClientPaymentStatus(paymentId, status)
-          }
-        }
+      if (clientPayment === null) {
+        return
+      }
+      
+      // Prevent status downgrades
+      if (clientPayment.status === 'CONFIRMED' && status === 'ADDED_TO_MEMPOOL') {
+        return
+      }
+      
+      // Verify transaction address matches payment address
+      if (clientPayment.address.address !== txAddress) {
+        return
+      }
+      
+      // Check amount if specified
+      if (clientPayment.amount !== null) {
+        const clientAmount = new Prisma.Decimal(clientPayment.amount)
+        const txAmountDecimal = new Prisma.Decimal(txAmount.toString())
+        if (!clientAmount.equals(txAmountDecimal)) {
+          return
+        }
       }
+      
+      await updateClientPaymentStatus(paymentId, status)
+    } catch (error) {
+      console.error(`Failed to update client payment status: ${String(error)}`)
     }
   }
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 557 to 581, fix several issues:
rename parameter txAdress to txAddress (and update all uses inside the
function); validate parsed paymentId against /^[0-9a-fA-F]{32}$/ before calling
getClientPayment and return early if invalid; wrap the DB calls
(getClientPayment and updateClientPaymentStatus) in a try-catch and handle/log
errors gracefully to avoid unhandled rejections; when checking stored address
use clientPayment.address.address (per include: { address: true }) instead of
clientPayment?.addressString; and compare Prisma Decimal values using the
Decimal equals method (e.g., clientPayment.amount.equals(txAmount) or convert
txAmount to Decimal) rather than Number(...) equality. Ensure the function
returns/short-circuits cleanly on invalid/missing values.


private async processWsMessage (msg: WsMsgClient): Promise<void> {
// delete unconfirmed transaction from our database
// if they were cancelled and not confirmed
if (msg.type === 'Tx') {
const transaction = await this.chronik.tx(msg.txid)
const addressesWithTransactions = await this.getAddressesForTransaction(transaction)

if (msg.msgType === 'TX_REMOVED_FROM_MEMPOOL') {
console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`)
const transactionsToDelete = await fetchUnconfirmedTransactions(msg.txid)
Expand All @@ -569,6 +600,10 @@ export class ChronikBlockchainClient {
} else if (msg.msgType === 'TX_CONFIRMED') {
console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`)
this.confirmedTxsHashesFromLastBlock = [...this.confirmedTxsHashesFromLastBlock, msg.txid]
for (const addressWithTransaction of addressesWithTransactions) {
const { amount, opReturn } = addressWithTransaction.transaction
await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address)
}
Comment on lines +603 to +606
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify call sites after fixing handleUpdateClientPaymentStatus.

The call to handleUpdateClientPaymentStatus looks correct with proper parameters (amount, opReturn, CONFIRMED status, address). Once the method is fixed per the previous comment, this should work as expected.

After applying the fixes to handleUpdateClientPaymentStatus, verify that:

  1. CONFIRMED status updates work correctly
  2. Amount matching validation works with Prisma Decimal
  3. Address validation prevents mismatched updates
  4. Errors are logged without crashing the WebSocket handler

🏁 Script executed:

#!/bin/bash
# Verify the handleUpdateClientPaymentStatus usage in TX_CONFIRMED and TX_ADDED_TO_MEMPOOL handlers
rg -n "handleUpdateClientPaymentStatus" services/chronikService.ts -A2 -B2

Length of output: 878


🏁 Script executed:

#!/bin/bash
rg -n "private async handleUpdateClientPaymentStatus" -A200 services/chronikService.ts

Length of output: 10453


Handle errors & use precise Decimal comparison in handleUpdateClientPaymentStatus

  • Wrap both await updateClientPaymentStatus(...) calls in a try/catch inside handleUpdateClientPaymentStatus, logging errors rather than letting them bubble up and crash the WebSocket handler.
  • Replace Number(clientPayment.amount) === Number(txAmount) with a Decimal‐aware check, e.g.:
    if (clientPayment.amount.equals(new Prisma.Decimal(txAmount))) {  }
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 604 to 607, the loop calls
handleUpdateClientPaymentStatus which currently lets internal update errors
bubble up and uses Number(...) for amount comparison; modify
handleUpdateClientPaymentStatus to wrap both await
updateClientPaymentStatus(...) calls in a try/catch that logs the error (so the
WebSocket handler doesn't crash) and continues, and change the numeric
comparison to use a Decimal-aware check (e.g., create a Prisma.Decimal from
txAmount and call .equals on clientPayment.amount) instead of
Number(clientPayment.amount) === Number(txAmount).

} else if (msg.msgType === 'TX_ADDED_TO_MEMPOOL') {
if (this.isAlreadyBeingProcessed(msg.txid, false)) {
return
Expand All @@ -588,6 +623,8 @@ export class ChronikBlockchainClient {
if (created) { // only execute trigger for newly added txs
await executeAddressTriggers(broadcastTxData, tx.address.networkId)
}
const { amount, opReturn } = addressWithTransaction.transaction
await this.handleUpdateClientPaymentStatus(amount, opReturn, 'ADDED_TO_MEMPOOL' as ClientPaymentStatus, addressWithTransaction.address.address)
}
}
this.mempoolTxsBeingProcessed -= 1
Expand Down
56 changes: 54 additions & 2 deletions services/transactionService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import prisma from 'prisma-local/clientInstance'
import { Prisma, Transaction } from '@prisma/client'
import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT } from 'constants/index'
import { Prisma, Transaction, ClientPaymentStatus } from '@prisma/client'
import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, NETWORK_IDS_FROM_SLUGS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT } from 'constants/index'
import { fetchAddressBySubstring, fetchAddressById, fetchAddressesByPaybuttonId, addressExists } from 'services/addressService'
import { AllPrices, QuoteValues, fetchPricesForNetworkAndTimestamp, flattenTimestamp } from 'services/priceService'
import _ from 'lodash'
Expand All @@ -9,6 +9,8 @@ import { SimplifiedTransaction } from 'ws-service/types'
import { OpReturnData, parseAddress } from 'utils/validators'
import { generatePaymentFromTxWithInvoices } from 'redis/paymentCache'
import { ButtonDisplayData, Payment } from 'redis/types'
import { v4 as uuidv4 } from 'uuid'
import { multiBlockchainClient } from 'services/chronikService'

Comment on lines +12 to 14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Break circular dependency with chronikService.

Top-level import of multiBlockchainClient creates a cycle (chronikService → transactionService → chronikService) that can yield undefined bindings at runtime. Lazy import inside the function avoids it.

Apply:

-import { v4 as uuidv4 } from 'uuid'
-import { multiBlockchainClient } from 'services/chronikService'
+import { v4 as uuidv4 } from 'uuid'

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In services/transactionService.ts around lines 12 to 14, the top-level import of
multiBlockchainClient creates a circular dependency with chronikService; remove
the top-level import and perform a lazy/dynamic import of multiBlockchainClient
inside the function(s) that use it (e.g., call const { multiBlockchainClient } =
await import('services/chronikService') or use require(...) if not in an async
context) so the module is resolved at call-time and avoids undefined bindings at
runtime; update all call sites in this file to use the locally-imported
multiBlockchainClient instance.

export function getTransactionValue (transaction: TransactionWithPrices | TransactionsWithPaybuttonsAndPrices | SimplifiedTransaction): QuoteValues {
const ret: QuoteValues = {
Expand Down Expand Up @@ -974,3 +976,53 @@ export const fetchDistinctPaymentYearsByUser = async (userId: string): Promise<n

return years.map(y => y.year)
}

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
const rawUUID = uuidv4()
const cleanUUID = rawUUID.replace(/-/g, '')
const status = 'PENDING' as ClientPaymentStatus
address = parseAddress(address)
const prefix = address.split(':')[0].toLowerCase()
const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
const isAddressRegistered = await addressExists(address)

const clientPayment = await prisma.clientPayment.create({
data: {
address: {
connectOrCreate: {
where: { address },
create: {
address,
networkId
}
}
},
paymentId: cleanUUID,
status,
amount
},
include: {
address: true
}
})

if (!isAddressRegistered) {
void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
}

return clientPayment.paymentId
}

export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
}
Comment on lines +1015 to +1021
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for non-existent payment IDs

The updateClientPaymentStatus function should handle cases where the payment ID doesn't exist.

 export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
-  await prisma.clientPayment.update({
-    where: { paymentId },
-    data: { status }
-  })
+  try {
+    await prisma.clientPayment.update({
+      where: { paymentId },
+      data: { status }
+    })
+  } catch (error: any) {
+    if (error.code === 'P2025') {
+      console.warn(`Client payment not found: ${paymentId}`)
+    } else {
+      throw error
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
}
export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
try {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
} catch (error: any) {
if (error.code === 'P2025') {
console.warn(`Client payment not found: ${paymentId}`)
} else {
throw error
}
}
}
🤖 Prompt for AI Agents
In services/transactionService.ts around lines 1004–1010, the
updateClientPaymentStatus currently calls prisma.clientPayment.update without
handling the case where paymentId doesn't exist; wrap the update in a try/catch
(or use prisma.clientPayment.updateMany and check the returned count) and handle
the "not found" case explicitly — on Prisma's P2025 error (or when count === 0)
either throw a clear NotFoundError or return a controlled result, and ensure the
function signature and callers are adjusted to expect/propagate that error.


export const getClientPayment = async (paymentId: string): Promise<Prisma.ClientPaymentGetPayload<{ include: { address: true } }> | null> => {
return await prisma.clientPayment.findUnique({
where: { paymentId },
include: { address: true }
})
}
Loading