Conversation
Adding model validation and tests for tx-history
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive validation and schema assertions to the transaction history API models and controllers. It extends support for additional tokens and networks, adds new optional fields for liquidity provider tracking, implements strict input validation patterns, and includes extensive acceptance tests to verify the validation behavior.
Changes:
- Extended TOKENS enum to include ETH, USDT, USDC, DAI, WBTC, BNB, and Fiat
- Extended NETWORKS enum to include Ethereum, BNB Smart Chain, and Lightning Network
- Added
liquidityProviderNameandliquidityProviderIdoptional fields to TxHistory model with validation - Added comprehensive input validation patterns to controller endpoints (address, page, txHash)
- Added txHash validation pattern to RegisterPayload model
- Added 533 lines of acceptance tests covering positive and negative validation scenarios
- Modified test script to run both unit and acceptance tests together
- Downgraded nock package from v14 to v13
- Fixed import ordering in fireblocks acceptance test
- Updated input sanitization test to verify SQL injection prevention in txHash field
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/models/tx-history.model.ts | Added new tokens/networks to enums, added liquidityProviderName and liquidityProviderId fields with validation |
| src/models/register-payload.model.ts | Added regex validation pattern for txHash field to prevent injection attacks |
| src/controllers/tx-history.controller.ts | Added parameter validation schemas for address, page, and txHash with regex patterns and error responses |
| src/tests/acceptance/tx-history.acceptance.ts | New comprehensive acceptance test suite with 533 lines testing all validation scenarios |
| src/tests/acceptance/input-sanitization.acceptance.ts | Updated SQL injection test to expect rejection (422) instead of acceptance, removed skipped hex data test |
| src/tests/acceptance/fireblocks.controller.acceptance.ts | Reordered imports to follow consistent pattern |
| package.json | Modified test script to run both unit and acceptance tests; downgraded nock from v14 to v13 |
| package-lock.json | Updated dependency tree reflecting nock downgrade and removed related dependencies |
| import {SearchableModel} from './rsk/searchable-model'; | ||
|
|
||
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS'] as const; | ||
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'teks-staging'] as const; |
There was a problem hiding this comment.
The value 'teks-staging' added to SOURCES uses lowercase with a hyphen, which is inconsistent with all other values in the array that use uppercase letters (e.g., 'FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS'). This inconsistency could cause confusion for API consumers and violates the established naming convention for this enum. Consider renaming it to 'TEKS_STAGING' or 'TEKS-STAGING' to match the existing pattern.
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'teks-staging'] as const; | |
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'TEKS-STAGING'] as const; |
|
|
||
|
|
There was a problem hiding this comment.
There are two blank lines (96-97) in the middle of the method implementation. This appears to be unintentional and reduces code readability. Remove the extra blank line to maintain consistent code formatting.
| } | ||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
The method verifyTransactionExistsOnBlockchain returns true for networks other than 'bitcoin' and 'rootstock', but the NETWORKS constant now includes 'Ethereum', 'BNB Smart Chain', and 'Lightning Network'. This means transactions from these networks will be accepted without any blockchain verification. This creates a security risk where invalid transaction hashes could be stored for these networks. Either implement verification for the newly-added networks or explicitly return false for unsupported networks to prevent storing unverified transactions.
| } | |
| } else { | |
| this.logger.warn( | |
| `[verifyTransactionExists] Unsupported network for verification: ${txHistory.fromNetworkName}`, | |
| ); | |
| return false; | |
| } |
| required: true, | ||
| schema: { | ||
| type: 'string', | ||
| pattern: '^[a-fA-F0-9]{64}$|^0x[a-fA-F0-9]{64}$', |
There was a problem hiding this comment.
The pattern ^[a-fA-F0-9]{64}$|^0x[a-fA-F0-9]{64}$ will always match with the first alternative ^[a-fA-F0-9]{64}$ for strings without the '0x' prefix because the regex alternation is evaluated left-to-right. However, due to the anchor ^ at the start of the second alternative, a string like '0x1234...' will fail the first alternative and then be tested by the second. The pattern works correctly but could be more efficiently written as ^(0x)?[a-fA-F0-9]{64}$. Consider refactoring for clarity and consistency with the pattern used in tx-history.model.ts line 29: ^(0x[a-fA-F0-9]{64}|[a-fA-F0-9]{64})$
| txHistory.fromAmount = rskTransaction.value?.toString() ?? ''; | ||
| txHistory.userAddress = rskTransaction.from?.toString() ?? ''; | ||
| } else { | ||
| return null as unknown as TxHistory; |
There was a problem hiding this comment.
Using null as unknown as TxHistory to return a falsy value is an anti-pattern. The method should have a clearer return type. Consider changing the method signature to return Promise<boolean> and return true when the transaction exists and false when it doesn't, instead of using null casts. This would make the code more readable and type-safe.
| it('should store a valid transaction and return 200', async () => { | ||
| const requestBody = getValidRequestBody(); | ||
|
|
||
| storeTransactionStub = sinon.stub(TxHistoryService.prototype, 'storeTransaction').resolves(true); | ||
|
|
||
| const res = await client | ||
| .post('/tx-history') | ||
| .send(requestBody) | ||
| .expect(200); | ||
|
|
||
| expect(res.body).to.be.empty(); | ||
| sinon.assert.calledOnce(storeTransactionStub); | ||
| }); |
There was a problem hiding this comment.
The acceptance test stubs TxHistoryService.storeTransaction but does not stub BitcoinService.getTx or RskNodeService.getTransaction. Since the storeTransaction endpoint now calls verifyTransactionExistsOnBlockchain (which calls these services) before storing, the acceptance tests will likely fail or make real network calls. You should stub these services in the acceptance tests to prevent network calls and ensure tests are isolated.
| return !!transaction; | ||
| } | ||
|
|
||
| private async verifyTransactionExistsOnBlockchain(txHistory: TxHistory): Promise<TxHistory> { |
There was a problem hiding this comment.
The method verifyTransactionExistsOnBlockchain returns TxHistory (or null cast as TxHistory), but it's being used as a boolean in line 57 with if (!transactionExists). When the method returns null (cast as TxHistory), this will be falsy and work as expected. However, when it returns a TxHistory object, the truthiness check will pass. The return type should be changed to Promise<boolean> or Promise<TxHistory | null>, and the usage should be updated accordingly. Additionally, the method mutates the input txHistory parameter (lines 93-94, 97-98) and returns it, which is confusing since the caller doesn't use the returned value except for the truthiness check.
| this.logger.warn( | ||
| `[verifyTransactionExists] Transaction not found for ${txHistory.fromNetworkName}: ${txHistory.txHash} ${errorMessage}`, | ||
| ); | ||
| return null as unknown as TxHistory; |
There was a problem hiding this comment.
Using null as unknown as TxHistory to return a falsy value is an anti-pattern. The method should have a clearer return type. Consider changing the method signature to return Promise<boolean> and return true when the transaction exists and false when it doesn't, instead of using null casts. This would make the code more readable and type-safe.
| } catch (error: unknown) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| this.logger.warn( | ||
| `[verifyTransactionExists] Transaction not found for ${txHistory.fromNetworkName}: ${txHistory.txHash} ${errorMessage}`, |
There was a problem hiding this comment.
The log message references '[verifyTransactionExists]' but the method name is 'verifyTransactionExistsOnBlockchain'. Update the log message to use the correct method name for consistency and easier debugging.
| `[verifyTransactionExists] Transaction not found for ${txHistory.fromNetworkName}: ${txHistory.txHash} ${errorMessage}`, | |
| `[verifyTransactionExistsOnBlockchain] Transaction not found for ${txHistory.fromNetworkName}: ${txHistory.txHash} ${errorMessage}`, |
| import {SearchableModel} from './rsk/searchable-model'; | ||
|
|
||
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS'] as const; | ||
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'teks-staging'] as const; |
There was a problem hiding this comment.
The value 'teks-staging' in SOURCES uses kebab-case while all other values use SCREAMING_SNAKE_CASE. This is inconsistent with the established naming pattern. Consider renaming to 'TEKS_STAGING' to maintain consistency.
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'teks-staging'] as const; | |
| export const SOURCES = ['FLYOVER', 'SWAP', 'POWPEG', 'UNION', 'LIFI', 'SYMBIOSIS', 'TEKS_STAGING'] as const; |
| txHistory.fromAmount = btcTransaction.amount.toString(); | ||
| txHistory.userAddress = btcTransaction.address; |
There was a problem hiding this comment.
The code attempts to access btcTransaction.amount and btcTransaction.address properties (lines 93-94), but the BitcoinTx model does not define these properties. The BitcoinTx model only has properties like txid, vin, vout, valueOut, valueIn, etc. This will result in undefined values being assigned to txHistory.fromAmount and txHistory.userAddress, causing the verification to fail silently. You need to extract the amount and address from the available properties (e.g., from vout for amount and vin for address) or update the BitcoinService.getTx to return the required properties.
| txHistory.fromAmount = btcTransaction.amount.toString(); | |
| txHistory.userAddress = btcTransaction.address; | |
| const amount = | |
| (btcTransaction as any).valueOut ?? | |
| ((btcTransaction as any).vout | |
| ? (btcTransaction as any).vout.reduce( | |
| (sum: number, output: any) => sum + (output?.value ?? 0), | |
| 0, | |
| ) | |
| : undefined); | |
| txHistory.fromAmount = amount != null ? amount.toString() : ''; | |
| const inputAddress = | |
| (btcTransaction as any).vin?.[0]?.addr ?? | |
| (btcTransaction as any).vin?.[0]?.addresses?.[0] ?? | |
| (btcTransaction as any).vout?.[0]?.scriptPubKey?.addresses?.[0] ?? | |
| ''; | |
| txHistory.userAddress = inputAddress; |
| @@ -1,3 +1,4 @@ | |||
| /* eslint-disable no-param-reassign */ | |||
There was a problem hiding this comment.
The eslint-disable directive for no-param-reassign is applied to the entire file, but parameter reassignment only occurs in the verifyTransactionExistsOnBlockchain method (lines 93-94, 97-98). Consider moving this directive to only disable the rule for the specific method or lines where it's needed, rather than the entire file.
| @@ -89,11 +162,25 @@ export class TxHistoryController { | |||
| }, | |||
| }, | |||
| }, | |||
| '400': { | |||
| description: 'Invalid address or page parameter', | |||
| }, | |||
| }, | |||
| }) | |||
| async getTransactionHistory( | |||
| @param.query.string('address', {required: true}) address: string, | |||
| @param.query.number('page') page: number = 1, | |||
| @param.query.string('address', { | |||
| required: true, | |||
| schema: { | |||
| type: 'string', | |||
| pattern: '^(0x[a-fA-F0-9]{40}|[13mn][a-km-zA-HJ-NP-Z1-9]{25,34}|2[a-km-zA-HJ-NP-Z1-9]{25,34}|(bc1q|tb1q)[0-9a-z]{38,59}|(bc1p|tb1p)[0-9a-z]{39,59})$', | |||
| }, | |||
| }) address: string, | |||
| @param.query.number('page', { | |||
| schema: { | |||
| type: 'number', | |||
| minimum: 1, | |||
| }, | |||
| }) page: number = 1, | |||
| ): Promise<Response> { | |||
| try { | |||
| if (!address) { | |||
| @@ -118,6 +205,18 @@ export class TxHistoryController { | |||
| } | |||
|
|
|||
| @get('/tx-history/{txHash}', { | |||
| parameters: [ | |||
| { | |||
| name: 'txHash', | |||
| in: 'path', | |||
| required: true, | |||
| schema: { | |||
| type: 'string', | |||
| pattern: '^[a-fA-F0-9]{64}$|^0x[a-fA-F0-9]{64}$', | |||
| }, | |||
| description: 'Must be a valid transaction hash', | |||
| }, | |||
| ], | |||
| responses: { | |||
| '200': { | |||
| description: 'Transaction details', | |||
| @@ -127,13 +226,21 @@ export class TxHistoryController { | |||
| }, | |||
| }, | |||
| }, | |||
| '400': { | |||
| description: 'Invalid transaction hash format', | |||
| }, | |||
| '404': { | |||
| description: 'Transaction not found', | |||
| }, | |||
| }, | |||
| }) | |||
| async getTransactionByHash( | |||
| @param.path.string('txHash') txHash: string, | |||
| @param.path.string('txHash', { | |||
| schema: { | |||
| type: 'string', | |||
| pattern: '^[a-fA-F0-9]{64}$|^0x[a-fA-F0-9]{64}$', | |||
| }, | |||
There was a problem hiding this comment.
The address validation pattern is duplicated in three places: in the parameters array (lines 113-123), in the @param decorator (lines 171-177), and in the GET endpoint parameters. The txHash pattern is also duplicated (lines 208-218 and 238-242). Consider extracting these patterns to constants to improve maintainability and reduce duplication.
| "only-coverage": "lb-nyc report --reporter=lcov", | ||
| "pretest": "npm run rebuild", | ||
| "test": "npm run unit-test", | ||
| "test": "npm run pretest && lb-nyc mocha --recursive 'dist/__tests__/**/*.unit.js' 'dist/__tests__/**/*.acceptance.js' --timeout 30000 --reporter spec", |
There was a problem hiding this comment.
The test script has been changed to run both unit and acceptance tests together instead of just unit tests. This changes the behavior of npm test to run a more comprehensive test suite. Ensure this change is intentional and that CI/CD pipelines are updated accordingly, as the test execution time will likely increase significantly.
| "test": "npm run pretest && lb-nyc mocha --recursive 'dist/__tests__/**/*.unit.js' 'dist/__tests__/**/*.acceptance.js' --timeout 30000 --reporter spec", | |
| "test": "npm run unit-test", |
e87b06f to
bc54f68
Compare
| const requestBody = getValidRequestBody(); | ||
|
|
||
| storeTransactionStub = sinon.stub(TxHistoryService.prototype, 'storeTransaction').resolves(true); | ||
| const getTransactionByHashStub = sinon.stub(TxHistoryService.prototype, 'getTransactionByHash').resolves(null); |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, to fix an unused variable where the expression’s side effect is required, you should keep the expression but remove the variable binding. Here, we still want to stub TxHistoryService.prototype.getTransactionByHash so that the real implementation is not called, but we do not need to store the stub instance in getTransactionByHashStub.
Concretely, in src/__tests__/acceptance/tx-history.acceptance.ts, within the "should store a valid transaction and return 200" test, replace the line that declares and assigns getTransactionByHashStub with a plain sinon.stub(...) call that is not assigned to any variable. No imports or other lines need to change, and we do not need to touch storeTransactionStub or any other code.
| @@ -72,7 +72,7 @@ | ||
| const requestBody = getValidRequestBody(); | ||
|
|
||
| storeTransactionStub = sinon.stub(TxHistoryService.prototype, 'storeTransaction').resolves(true); | ||
| const getTransactionByHashStub = sinon.stub(TxHistoryService.prototype, 'getTransactionByHash').resolves(null); | ||
| sinon.stub(TxHistoryService.prototype, 'getTransactionByHash').resolves(null); | ||
|
|
||
| const res = await client | ||
| .post('/tx-history') |
|



No description provided.