purchase notify update for negative values#9
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds support for negative amounts in purchase notifications to track token deposits (balance decreases) to the contract. The changes allow the system to record both positive purchases and negative deposits in the same Purchase schema.
Changes:
- Modified the DTO validation to accept negative number strings using a regex pattern
- Added a 'type' field to the Purchase schema metadata to distinguish between 'PURCHASE' and 'DEPOSIT' records
- Implemented deposit handling logic that bypasses on-chain validation and converts amounts to wei
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| packages/backend/src/modules/marketplace/dto/notify-purchase.dto.ts | Updated validation to allow negative amounts using regex pattern instead of IsNumberString |
| packages/backend/src/database/schemas/purchase.schema.ts | Added optional 'type' field to metadata to track PURCHASE vs DEPOSIT transactions |
| packages/backend/src/modules/marketplace/services/purchase-tracker.service.ts | Implemented deposit handling with negative amounts, added early return path for deposits that skips on-chain validation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (isDeposit) { | ||
| // Handle token deposit to contract (balance decrease) | ||
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | ||
|
|
||
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | ||
| const depositAmountInWei = (BigInt(amountWithoutSign) * BigInt(10 ** 18)).toString(); | ||
| const blockNumber = dto.blockNumber ? parseInt(dto.blockNumber) : 0; | ||
|
|
||
| // Record as a negative purchase to track balance decrease | ||
| const purchase = await this.purchaseModel.create({ | ||
| txHash: dto.txHash, | ||
| assetId: dto.assetId, | ||
| investorWallet: investorWallet.toLowerCase(), // Store as negative in wei | ||
| tokenAddress: asset.token?.address || '', | ||
| amount: '-' + depositAmountInWei, | ||
| price: '0', // No price for deposits | ||
| totalPayment: '0', // No payment for deposits | ||
| blockNumber: blockNumber, | ||
| blockTimestamp: new Date(), | ||
| status: 'CONFIRMED', | ||
| metadata: { | ||
| assetName: `${asset.metadata?.invoiceNumber} - ${asset.metadata?.buyerName}`, | ||
| industry: asset.metadata?.industry, | ||
| riskTier: asset.metadata?.riskTier, | ||
| type: 'DEPOSIT', // Mark as deposit | ||
| }, | ||
| }); | ||
|
|
||
| this.logger.log(`Token deposit recorded: ${purchase._id}, amount in wei: -${depositAmountInWei}`); | ||
|
|
||
| return { | ||
| success: true, | ||
| purchaseId: purchase._id, | ||
| assetId: dto.assetId, | ||
| amount: '-' + depositAmountInWei, | ||
| totalPayment: '0', | ||
| tokenAddress: asset.token?.address, | ||
| type: 'DEPOSIT', | ||
| }; | ||
| } |
There was a problem hiding this comment.
The deposit functionality is not covered by any tests. Since this introduces a new critical code path that bypasses transaction validation and manipulates portfolio balances, comprehensive test coverage is essential to prevent regressions and ensure the deposit logic works correctly with both positive and negative amounts.
| const isDeposit = dto.amount.startsWith('-'); | ||
|
|
||
| if (isDeposit) { | ||
| // Handle token deposit to contract (balance decrease) | ||
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | ||
|
|
||
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign |
There was a problem hiding this comment.
Using startsWith('-') to detect deposits is fragile and could fail if the amount string has leading whitespace or uses a different representation. Consider using a more robust approach such as checking if the first non-whitespace character is '-' using trim(), or better yet, adding an explicit field in the DTO to indicate the transaction type.
| const isDeposit = dto.amount.startsWith('-'); | |
| if (isDeposit) { | |
| // Handle token deposit to contract (balance decrease) | |
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | |
| // Convert amount to wei (if not already in wei) | |
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | |
| const normalizedAmount = dto.amount.trim(); | |
| const isDeposit = normalizedAmount.startsWith('-'); | |
| if (isDeposit) { | |
| // Handle token deposit to contract (balance decrease) | |
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | |
| // Convert amount to wei (if not already in wei) | |
| const amountWithoutSign = normalizedAmount.substring(1); // Remove negative sign |
There was a problem hiding this comment.
@kaushalya4s5s7 this works but yes robustness is well appriciated dont think of a specific case u can solve while solving , think how many cases this code can pass, nothing can pass everycase but the more we support the more better
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | ||
| const depositAmountInWei = (BigInt(amountWithoutSign) * BigInt(10 ** 18)).toString(); | ||
| const blockNumber = dto.blockNumber ? parseInt(dto.blockNumber) : 0; |
There was a problem hiding this comment.
Very right thing to do is to fetch the tx details just in case , use optional between frontend provided and blockchain provided
| price: '0', // No price for deposits | ||
| totalPayment: '0', // No payment for deposits | ||
| blockNumber: blockNumber, | ||
| blockTimestamp: new Date(), |
There was a problem hiding this comment.
For deposits, blockTimestamp is set to "new Date()" (current time) instead of the actual transaction timestamp. This is inconsistent with how purchases handle timestamps (line 123 uses the actual block timestamp). This inconsistency could cause issues with chronological ordering of transactions and inaccurate historical data analysis.
There was a problem hiding this comment.
again same thing if data can be fetched and presented then why not
| return { | ||
| success: true, | ||
| purchaseId: purchase._id, | ||
| assetId: dto.assetId, | ||
| amount: '-' + depositAmountInWei, | ||
| totalPayment: '0', | ||
| tokenAddress: asset.token?.address, | ||
| type: 'DEPOSIT', | ||
| }; | ||
| } |
There was a problem hiding this comment.
Unlike regular purchases which send a user notification (lines 167-181), deposits do not trigger any notification to the user. This inconsistency could leave users unaware of important balance changes in their portfolio, especially for negative deposits which decrease their holdings.
There was a problem hiding this comment.
exactly @kaushalya4s5s7 Deposit shall send a notif that asset from ur portfolio has been deposited it will give them a sense of assurance Use whats integrated best.
| if (isDeposit) { | ||
| // Handle token deposit to contract (balance decrease) | ||
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | ||
|
|
||
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | ||
| const depositAmountInWei = (BigInt(amountWithoutSign) * BigInt(10 ** 18)).toString(); | ||
| const blockNumber = dto.blockNumber ? parseInt(dto.blockNumber) : 0; | ||
|
|
||
| // Record as a negative purchase to track balance decrease | ||
| const purchase = await this.purchaseModel.create({ | ||
| txHash: dto.txHash, | ||
| assetId: dto.assetId, | ||
| investorWallet: investorWallet.toLowerCase(), // Store as negative in wei | ||
| tokenAddress: asset.token?.address || '', | ||
| amount: '-' + depositAmountInWei, | ||
| price: '0', // No price for deposits | ||
| totalPayment: '0', // No payment for deposits | ||
| blockNumber: blockNumber, | ||
| blockTimestamp: new Date(), | ||
| status: 'CONFIRMED', | ||
| metadata: { | ||
| assetName: `${asset.metadata?.invoiceNumber} - ${asset.metadata?.buyerName}`, | ||
| industry: asset.metadata?.industry, | ||
| riskTier: asset.metadata?.riskTier, | ||
| type: 'DEPOSIT', // Mark as deposit | ||
| }, | ||
| }); | ||
|
|
||
| this.logger.log(`Token deposit recorded: ${purchase._id}, amount in wei: -${depositAmountInWei}`); | ||
|
|
||
| return { | ||
| success: true, | ||
| purchaseId: purchase._id, | ||
| assetId: dto.assetId, | ||
| amount: '-' + depositAmountInWei, | ||
| totalPayment: '0', | ||
| tokenAddress: asset.token?.address, | ||
| type: 'DEPOSIT', | ||
| }; | ||
| } |
There was a problem hiding this comment.
This change introduces negative amounts into the Purchase records, but other parts of the codebase that query and sum purchase.amount values are not updated to handle this. Specifically, asset-lifecycle.service.ts (lines 1128, 1132, 1278, 1299) and bid-tracker.service.ts (lines 557-564) sum purchase amounts without filtering out deposits. This will cause incorrect calculations for tokensSold and listing.sold. All queries that aggregate purchase amounts must be updated to either exclude deposits (filter by metadata.type != 'DEPOSIT') or handle negative values appropriately in the business logic.
| if (isDeposit) { | ||
| // Handle token deposit to contract (balance decrease) | ||
| this.logger.log(`Processing token deposit (balance decrease): ${dto.amount}`); | ||
|
|
||
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | ||
| const depositAmountInWei = (BigInt(amountWithoutSign) * BigInt(10 ** 18)).toString(); | ||
| const blockNumber = dto.blockNumber ? parseInt(dto.blockNumber) : 0; | ||
|
|
||
| // Record as a negative purchase to track balance decrease | ||
| const purchase = await this.purchaseModel.create({ | ||
| txHash: dto.txHash, | ||
| assetId: dto.assetId, | ||
| investorWallet: investorWallet.toLowerCase(), // Store as negative in wei | ||
| tokenAddress: asset.token?.address || '', | ||
| amount: '-' + depositAmountInWei, | ||
| price: '0', // No price for deposits | ||
| totalPayment: '0', // No payment for deposits | ||
| blockNumber: blockNumber, | ||
| blockTimestamp: new Date(), | ||
| status: 'CONFIRMED', | ||
| metadata: { | ||
| assetName: `${asset.metadata?.invoiceNumber} - ${asset.metadata?.buyerName}`, | ||
| industry: asset.metadata?.industry, | ||
| riskTier: asset.metadata?.riskTier, | ||
| type: 'DEPOSIT', // Mark as deposit | ||
| }, | ||
| }); | ||
|
|
||
| this.logger.log(`Token deposit recorded: ${purchase._id}, amount in wei: -${depositAmountInWei}`); | ||
|
|
||
| return { | ||
| success: true, | ||
| purchaseId: purchase._id, | ||
| assetId: dto.assetId, | ||
| amount: '-' + depositAmountInWei, | ||
| totalPayment: '0', | ||
| tokenAddress: asset.token?.address, | ||
| type: 'DEPOSIT', | ||
| }; | ||
| } |
There was a problem hiding this comment.
The deposit flow bypasses on-chain transaction validation entirely. Unlike purchases which use validatePurchaseTransaction() to verify the transaction on-chain, deposits are recorded without any blockchain verification. This creates a security vulnerability where malicious actors could submit fake deposit transactions without actual on-chain events, potentially manipulating portfolio balances.
| investorWallet: investorWallet.toLowerCase(), // Store as negative in wei | ||
| tokenAddress: asset.token?.address || '', | ||
| amount: '-' + depositAmountInWei, |
There was a problem hiding this comment.
The comment "Store as negative in wei" is placed next to the investorWallet field but appears to be intended for the amount field on line 75. This misleading comment could confuse future developers about what should be stored as negative.
| investorWallet: investorWallet.toLowerCase(), // Store as negative in wei | |
| tokenAddress: asset.token?.address || '', | |
| amount: '-' + depositAmountInWei, | |
| investorWallet: investorWallet.toLowerCase(), | |
| tokenAddress: asset.token?.address || '', | |
| amount: '-' + depositAmountInWei, // Store as negative in wei |
…le looking into onchain event
| // Convert amount to wei (if not already in wei) | ||
| const amountWithoutSign = dto.amount.substring(1); // Remove negative sign | ||
| const depositAmountInWei = (BigInt(amountWithoutSign) * BigInt(10 ** 18)).toString(); | ||
| const blockNumber = dto.blockNumber ? parseInt(dto.blockNumber) : 0; |
There was a problem hiding this comment.
Very right thing to do is to fetch the tx details just in case , use optional between frontend provided and blockchain provided
No description provided.