-
Notifications
You must be signed in to change notification settings - Fork 4
feat: update clientPayment status #1043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b55991
04ffce3
735d9dd
33ccad2
b8308eb
7a03089
abd8fc9
8078a46
ad196aa
7a95368
8d3f911
74e1ce3
8713202
442a319
157cf0d
47ec902
f0e2622
15736e1
ed212f5
c8c79e7
30c0133
5542ede
eefbd7a
c1c9a72
e86ecc3
ea5eee2
9a87a26
c215cc9
f9db933
dc63345
723cdb4
ab4b944
22b7370
b5f13d5
962c953
6ff0d94
33eb251
92b2ecc
d80801e
e8aadce
f31fccb
7f700ce
21f4c8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
|
||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,11 @@ import { | |
| upsertTransaction, | ||
| getSimplifiedTransactions, | ||
| getSimplifiedTrasaction, | ||
| connectAllTransactionsToPrices | ||
| connectAllTransactionsToPrices, | ||
| updateClientPaymentStatus, | ||
| getClientPayment | ||
| } from './transactionService' | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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' | ||
|
|
@@ -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() | ||
|
|
||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix typo, property access, Decimal comparison, validation, and error handling. Several critical issues:
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 |
||
|
|
||
| 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) | ||
|
|
@@ -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) { | ||
chedieck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { amount, opReturn } = addressWithTransaction.transaction | ||
| await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address) | ||
| } | ||
|
Comment on lines
+603
to
+606
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify call sites after fixing handleUpdateClientPaymentStatus. The call to After applying the fixes to
🏁 Script executed: #!/bin/bash
# Verify the handleUpdateClientPaymentStatus usage in TX_CONFIRMED and TX_ADDED_TO_MEMPOOL handlers
rg -n "handleUpdateClientPaymentStatus" services/chronikService.ts -A2 -B2Length of output: 878 🏁 Script executed: #!/bin/bash
rg -n "private async handleUpdateClientPaymentStatus" -A200 services/chronikService.tsLength of output: 10453 Handle errors & use precise Decimal comparison in handleUpdateClientPaymentStatus
🤖 Prompt for AI Agents |
||
| } else if (msg.msgType === 'TX_ADDED_TO_MEMPOOL') { | ||
| if (this.isAlreadyBeingProcessed(msg.txid, false)) { | ||
| return | ||
|
|
@@ -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 | ||
|
|
||
| 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' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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'
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| export function getTransactionValue (transaction: TransactionWithPrices | TransactionsWithPaybuttonsAndPrices | SimplifiedTransaction): QuoteValues { | ||||||||||||||||||||||||||||||||||||||||||||
| const ret: QuoteValues = { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||||||
chedieck marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
chedieck marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const updateClientPaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||
| await prisma.clientPayment.update({ | ||||||||||||||||||||||||||||||||||||||||||||
| where: { paymentId }, | ||||||||||||||||||||||||||||||||||||||||||||
| data: { status } | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1015
to
+1021
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for non-existent payment IDs The 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getClientPayment = async (paymentId: string): Promise<Prisma.ClientPaymentGetPayload<{ include: { address: true } }> | null> => { | ||||||||||||||||||||||||||||||||||||||||||||
| return await prisma.clientPayment.findUnique({ | ||||||||||||||||||||||||||||||||||||||||||||
| where: { paymentId }, | ||||||||||||||||||||||||||||||||||||||||||||
| include: { address: true } | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix unsafe type cast for amount parameter.
Line 19 performs an unsafe type cast of
values.amounttoDecimal | undefined. According to the validator code inutils/validators.ts,parseCreatePaymentIdPOSTRequestreturnsamountas a string or undefined, not a Decimal. This cast does not perform any conversion and will cause type mismatches when passed togeneratePaymentId, which expectsPrisma.Decimal.Apply this diff to properly convert the amount:
📝 Committable suggestion