final
This commit is contained in:
parent
502420a1b2
commit
7f8a87cfa2
154
README.md
154
README.md
|
|
@ -1,105 +1,93 @@
|
|||
# SolPay - Node.js TypeScript Application
|
||||
# SolPay API
|
||||
|
||||
A modern Node.js application built with TypeScript, featuring Express.js for the web server and comprehensive testing setup.
|
||||
A Node.js TypeScript application for handling Solana transactions.
|
||||
|
||||
## Features
|
||||
## Setup
|
||||
|
||||
- 🚀 **TypeScript** - Full TypeScript support with strict type checking
|
||||
- 🧪 **Testing** - Jest testing framework with TypeScript support
|
||||
- 🔧 **Development** - Hot reload with ts-node for development
|
||||
- 📦 **Build System** - TypeScript compiler with source maps
|
||||
- 🎯 **Modern ES2020** - Latest JavaScript features
|
||||
### 1. Install Dependencies
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v16 or higher)
|
||||
- npm or yarn
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd solpay
|
||||
npm install mysql2
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
### 2. Environment Variables
|
||||
|
||||
Create a `.env` file in the root directory with the following variables:
|
||||
|
||||
```env
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password_here
|
||||
DB_NAME=solpay
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
### 3. Database Setup
|
||||
|
||||
- **`npm run dev`** - Start development server with hot reload
|
||||
- **`npm run build`** - Build the TypeScript code to JavaScript
|
||||
- **`npm start`** - Start the production server (requires build first)
|
||||
- **`npm run watch`** - Watch for changes and rebuild automatically
|
||||
- **`npm test`** - Run tests
|
||||
- **`npm run clean`** - Clean build output
|
||||
Make sure your MySQL database is running and the `solpay` database exists with the `transactions` table.
|
||||
|
||||
## Development
|
||||
## API Endpoints
|
||||
|
||||
To start developing:
|
||||
### Create New Transaction
|
||||
- **URL**: `GET /tx/new`
|
||||
- **Method**: `GET`
|
||||
- **Query Parameters**:
|
||||
- `tx` (required): Transaction hash
|
||||
- `target_address` (required): Target wallet address
|
||||
- `amount` (required): Transaction amount
|
||||
- `token_mint` (required): Token mint address
|
||||
- `token_program` (required): Token program address
|
||||
- `validated_at` (required): Validation timestamp
|
||||
- `sender_address` (required): Sender wallet address
|
||||
- `metadata` (optional): Additional metadata
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3000/tx/new?tx=abc123&target_address=target123&amount=1.5&token_mint=mint123&token_program=program123&validated_at=2024-01-01T00:00:00Z&sender_address=sender123&metadata=test"
|
||||
```
|
||||
|
||||
### Get All Transactions
|
||||
- **URL**: `GET /tx`
|
||||
- **Method**: `GET`
|
||||
|
||||
### Get Transaction by Hash
|
||||
- **URL**: `GET /tx/:txHash`
|
||||
- **Method**: `GET`
|
||||
|
||||
## Running the Application
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This will start the server using ts-node, which automatically compiles TypeScript on the fly.
|
||||
|
||||
## Building for Production
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
## Database Schema
|
||||
|
||||
The `transactions` table should have the following structure:
|
||||
|
||||
```sql
|
||||
CREATE TABLE transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tx VARCHAR(255) NOT NULL,
|
||||
target_address VARCHAR(255) NOT NULL,
|
||||
amount VARCHAR(255) NOT NULL,
|
||||
token_mint VARCHAR(255) NOT NULL,
|
||||
token_program VARCHAR(255) NOT NULL,
|
||||
validated_at VARCHAR(255) NOT NULL,
|
||||
sender_address VARCHAR(255) NOT NULL,
|
||||
metadata TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
solpay/
|
||||
├── src/ # TypeScript source code
|
||||
├── dist/ # Compiled JavaScript (generated)
|
||||
├── tests/ # Test files
|
||||
├── package.json # Dependencies and scripts
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── jest.config.js # Jest testing configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Run tests with coverage:
|
||||
|
||||
```bash
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
The project uses strict TypeScript settings for better code quality:
|
||||
|
||||
- Strict mode enabled
|
||||
- No implicit any types
|
||||
- Source maps for debugging
|
||||
- Declaration files generation
|
||||
- ES2020 target
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests for new functionality
|
||||
5. Ensure all tests pass
|
||||
6. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details
|
||||
|
|
|
|||
112
VALIDATION_SERVICE.md
Normal file
112
VALIDATION_SERVICE.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Transaction Validation Service
|
||||
|
||||
The Transaction Validation Service automatically validates unvalidated transactions against blockchain data every 5 seconds.
|
||||
|
||||
## Overview
|
||||
|
||||
The service runs as a background process that:
|
||||
1. Fetches all transactions with `validated_at = NULL` from the database
|
||||
2. Retrieves transaction data from the Solana blockchain using the explorer utility
|
||||
3. Compares database transaction data with blockchain data
|
||||
4. Updates validated transactions with `validated_at = NOW()`
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Validation**: Runs every 5 seconds without manual intervention
|
||||
- **Blockchain Verification**: Compares transaction details with actual blockchain data
|
||||
- **Data Integrity**: Ensures sender, receiver, amount, and token mint addresses match
|
||||
- **Rate Limiting**: Includes delays between API calls to respect blockchain API limits
|
||||
- **Graceful Shutdown**: Properly stops validation when the server shuts down
|
||||
- **Monitoring**: Provides status endpoints to monitor validation progress
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Get Validation Status
|
||||
```
|
||||
GET /validation/status
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"validationService": {
|
||||
"isRunning": true,
|
||||
"hasInterval": true
|
||||
},
|
||||
"statistics": {
|
||||
"unvalidated": 5,
|
||||
"validated": 25,
|
||||
"total": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Start Validation Service
|
||||
```
|
||||
POST /validation/start
|
||||
```
|
||||
|
||||
### Stop Validation Service
|
||||
```
|
||||
POST /validation/stop
|
||||
```
|
||||
|
||||
## Validation Logic
|
||||
|
||||
A transaction is considered valid if:
|
||||
|
||||
1. **Sender Address**: Database `sender_address` matches blockchain `sender`
|
||||
2. **Receiver Address**: Database `target_address` matches blockchain `receiver`
|
||||
3. **Amount**: Database `amount` matches blockchain `amount`
|
||||
4. **Token Mint**: Database `token_mint` matches blockchain `mint`
|
||||
|
||||
## Database Schema
|
||||
|
||||
The `transactions` table must include:
|
||||
- `validated_at` field (TIMESTAMP, can be NULL)
|
||||
- `created_at` field (TIMESTAMP)
|
||||
- `updated_at` field (TIMESTAMP)
|
||||
|
||||
## Configuration
|
||||
|
||||
The service automatically starts when the server starts and stops when the server shuts down. It can also be manually controlled via the API endpoints.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Failed validations are logged but don't stop the service
|
||||
- Blockchain API errors are handled gracefully
|
||||
- Database connection issues are logged
|
||||
- The service continues running even if individual validations fail
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Processes transactions sequentially to avoid overwhelming the blockchain API
|
||||
- Includes 100ms delays between individual transaction validations
|
||||
- Runs every 5 seconds to balance responsiveness with API usage
|
||||
- Logs validation progress for monitoring and debugging
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test script to verify the service works correctly:
|
||||
|
||||
```bash
|
||||
npx ts-node src/test-validation.ts
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
Monitor the service through:
|
||||
- Application logs (look for validation-related log entries)
|
||||
- `/validation/status` endpoint for real-time status
|
||||
- Database queries on the `validated_at` field
|
||||
- Transaction counts (validated vs unvalidated)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Service not starting**: Check database connection and permissions
|
||||
2. **No validations occurring**: Verify transactions exist with `validated_at = NULL`
|
||||
3. **Validation failures**: Check blockchain API connectivity and transaction data integrity
|
||||
4. **High API usage**: Consider increasing the validation interval or reducing batch sizes
|
||||
37
database-schema.sql
Normal file
37
database-schema.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- SolPay Database Schema
|
||||
-- This schema includes the validated_at field needed for transaction validation
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS solpay;
|
||||
USE solpay;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tx VARCHAR(255) NOT NULL UNIQUE,
|
||||
target_address VARCHAR(255) NOT NULL,
|
||||
amount VARCHAR(255) NOT NULL,
|
||||
token_mint VARCHAR(255) NOT NULL,
|
||||
token_program VARCHAR(255) NOT NULL,
|
||||
sender_address VARCHAR(255) NOT NULL,
|
||||
metadata TEXT,
|
||||
validated_at TIMESTAMP NULL DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_tx (tx),
|
||||
INDEX idx_validated_at (validated_at),
|
||||
INDEX idx_sender_address (sender_address),
|
||||
INDEX idx_target_address (target_address),
|
||||
INDEX idx_created_at (created_at)
|
||||
);
|
||||
|
||||
-- Insert sample data for testing
|
||||
INSERT INTO transactions (tx, target_address, amount, token_mint, token_program, sender_address, metadata, validated_at) VALUES
|
||||
('sample_tx_1', 'receiver_address_1', '1000000', 'So11111111111111111111111111111111111111111', '11111111111111111111111111111111', 'sender_address_1', 'Sample transaction 1', NULL),
|
||||
('sample_tx_2', 'receiver_address_2', '500000', 'So11111111111111111111111111111111111111111', '11111111111111111111111111111111', 'sender_address_2', 'Sample transaction 2', NULL),
|
||||
('sample_tx_3', 'receiver_address_3', '750000', 'So11111111111111111111111111111111111111111', '11111111111111111111111111111111', 'sender_address_3', 'Sample transaction 3', NULL);
|
||||
|
||||
-- Show the table structure
|
||||
DESCRIBE transactions;
|
||||
|
||||
-- Show sample data
|
||||
SELECT * FROM transactions;
|
||||
124
package-lock.json
generated
124
package-lock.json
generated
|
|
@ -11,7 +11,8 @@
|
|||
"dependencies": {
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
|
|
@ -1529,6 +1530,15 @@
|
|||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
|
|
@ -2187,6 +2197,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
|
@ -2635,6 +2654,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
|
@ -2996,6 +3024,12 @@
|
|||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
|
|
@ -3875,6 +3909,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -3885,6 +3925,21 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
|
||||
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
|
|
@ -4060,6 +4115,59 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.14.3",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz",
|
||||
"integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.1",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"long": "^5.2.1",
|
||||
"lru.min": "^1.0.0",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
|
||||
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru-cache": "^7.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
|
|
@ -4681,6 +4789,11 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/seq-queue": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
|
|
@ -4849,6 +4962,15 @@
|
|||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/mysql": "^2.15.24",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.0",
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
"dependencies": {
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^3.14.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,320 @@
|
|||
//spl token transfer tx : 2Dq3cW3D7z75QNfjiTfDu3GgS2gaqrqmoaqzDoomTkJqT2ThSyNDNf1tTz8SgXmzSon9FHCyN61EFkAdkJquhbJf
|
||||
//sol transfer tx : 3c6AiQmRxnAKSQyQ3C6jPoEmhGPHieQ2uJWuBQjdMA2TPXMWeGtL6PmdUGrMdojFoazq1NUQnDb9qCq1o4sxYvZ8
|
||||
import { getTransaction, extractNativeSolTransferData, extractSplTokenTransferData, extractTokenTransferData } from '../utils/explorer';
|
||||
|
||||
// Mock the Solana connection
|
||||
jest.mock('@solana/web3.js', () => ({
|
||||
Connection: jest.fn().mockImplementation(() => ({
|
||||
getParsedTransaction: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock the data module
|
||||
jest.mock('../data', () => ({
|
||||
CLUSTER_API: 'https://api.devnet.solana.com'
|
||||
}));
|
||||
|
||||
describe('Explorer Utility Tests', () => {
|
||||
// Sample transaction hashes from the comments
|
||||
const SPL_TOKEN_TX_HASH = '2Dq3cW3D7z75QNfjiTfDu3GgS2gaqrqmoaqzDoomTkJqT2ThSyNDNf1tTz8SgXmzSon9FHCyN61EFkAdkJquhbJf';
|
||||
const SOL_TRANSFER_TX_HASH = '3c6AiQmRxnAKSQyQ3C6jPoEmhGPHieQ2uJWuBQjdMA2TPXMWeGtL6PmdUGrMdojFoazq1NUQnDb9qCq1o4sxYvZ8';
|
||||
|
||||
// Mock transaction data for SPL token transfer
|
||||
const mockSplTokenTransaction = {
|
||||
blockTime: 1640995200,
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: [
|
||||
{
|
||||
program: 'spl-token',
|
||||
parsed: {
|
||||
type: 'transferChecked',
|
||||
info: {
|
||||
source: 'SourceWallet123',
|
||||
destination: 'DestWallet456',
|
||||
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
tokenAmount: {
|
||||
amount: '1000000'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
recentBlockhash: 'mockBlockhash123'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock transaction data for native SOL transfer
|
||||
const mockSolTransaction = {
|
||||
blockTime: 1640995200,
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: [
|
||||
{
|
||||
program: 'system',
|
||||
parsed: {
|
||||
type: 'transfer',
|
||||
info: {
|
||||
source: 'SourceWallet789',
|
||||
destination: 'DestWallet012',
|
||||
lamports: 1000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
recentBlockhash: 'mockBlockhash456'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock transaction data for non-transfer transaction
|
||||
const mockNonTransferTransaction = {
|
||||
blockTime: 1640995200,
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: [
|
||||
{
|
||||
program: 'other-program',
|
||||
parsed: {
|
||||
type: 'other-action'
|
||||
}
|
||||
}
|
||||
],
|
||||
recentBlockhash: 'mockBlockhash789'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getTransaction', () => {
|
||||
it('should fetch and parse a transaction successfully', async () => {
|
||||
const { Connection } = require('@solana/web3.js');
|
||||
const mockConnection = {
|
||||
getParsedTransaction: jest.fn().mockResolvedValue(mockSplTokenTransaction)
|
||||
};
|
||||
Connection.mockImplementation(() => mockConnection);
|
||||
|
||||
const result = await getTransaction(SPL_TOKEN_TX_HASH);
|
||||
|
||||
expect(mockConnection.getParsedTransaction).toHaveBeenCalledWith(SPL_TOKEN_TX_HASH, {
|
||||
maxSupportedTransactionVersion: 0
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle connection errors gracefully', async () => {
|
||||
const { Connection } = require('@solana/web3.js');
|
||||
const mockConnection = {
|
||||
getParsedTransaction: jest.fn().mockRejectedValue(new Error('Connection failed'))
|
||||
};
|
||||
Connection.mockImplementation(() => mockConnection);
|
||||
|
||||
await expect(getTransaction(SPL_TOKEN_TX_HASH)).rejects.toThrow('Connection failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractNativeSolTransferData', () => {
|
||||
it('should extract native SOL transfer data correctly', () => {
|
||||
const result = extractNativeSolTransferData(mockSolTransaction);
|
||||
|
||||
expect(result).toEqual({
|
||||
sender: 'cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc',
|
||||
receiver: 'SP13fTsRrdB2vwJBk88bddCkz4v9N1mHncmTyqkUTTp',
|
||||
amount: 1000000000,
|
||||
mint: 'So11111111111111111111111111111111111111111',
|
||||
time: 1755106952,
|
||||
blockhash: '55sznG5B254dnr4WiM1EBHDE5kUfHHyZiY7sBSGGsBYE'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for non-native SOL transfers', () => {
|
||||
const result = extractNativeSolTransferData(mockSplTokenTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-transfer transactions', () => {
|
||||
const result = extractNativeSolTransferData(mockNonTransferTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle malformed transaction data gracefully', () => {
|
||||
const malformedTransaction = {
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = extractNativeSolTransferData(malformedTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors and return null', () => {
|
||||
const invalidTransaction = null;
|
||||
const result = extractNativeSolTransferData(invalidTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSplTokenTransferData', () => {
|
||||
it('should extract SPL token transfer data correctly', () => {
|
||||
const result = extractSplTokenTransferData(mockSplTokenTransaction);
|
||||
|
||||
expect(result).toEqual({
|
||||
sender: '4aHYndCFTBAg8zCLsdTF8mWUPdSXj9NqJXdti4BBdYKP',
|
||||
receiver: '7jnoV3yYmaFFa3FYtpTHY4kz6wer8ntEUKZr9hmouets',
|
||||
amount: 1000000000000,
|
||||
mint: 'diNoZ1L9UEiJMv8i43BjZoPEe2tywwaBTBnKQYf28FT',
|
||||
time: 1755106862,
|
||||
blockhash: 'GvxNLLizh6oCodnuYX7NVb7xBrtTpAHDTsWMPGtSg2eV'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for non-SPL token transfers', () => {
|
||||
const result = extractSplTokenTransferData(mockSolTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-transfer transactions', () => {
|
||||
const result = extractSplTokenTransferData(mockNonTransferTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle malformed transaction data gracefully', () => {
|
||||
const malformedTransaction = {
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = extractSplTokenTransferData(malformedTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors and return null', () => {
|
||||
const invalidTransaction = null;
|
||||
const result = extractSplTokenTransferData(invalidTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTokenTransferData', () => {
|
||||
it('should extract native SOL transfer data when available', () => {
|
||||
const result = extractTokenTransferData(mockSolTransaction);
|
||||
|
||||
expect(result).toEqual({
|
||||
sender: 'SourceWallet789',
|
||||
receiver: 'DestWallet012',
|
||||
amount: 1000000000,
|
||||
mint: 'So11111111111111111111111111111111111111111',
|
||||
time: 1640995200,
|
||||
blockhash: 'mockBlockhash456'
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract SPL token transfer data when native SOL is not available', () => {
|
||||
const result = extractTokenTransferData(mockSplTokenTransaction);
|
||||
|
||||
expect(result).toEqual({
|
||||
sender: 'SourceWallet123',
|
||||
receiver: 'DestWallet456',
|
||||
amount: 1000000,
|
||||
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
time: 1640995200,
|
||||
blockhash: 'mockBlockhash123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for non-transfer transactions', () => {
|
||||
const result = extractTokenTransferData(mockNonTransferTransaction);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should prioritize native SOL transfers over SPL token transfers', () => {
|
||||
// Create a transaction with both types of transfers
|
||||
const mixedTransaction = {
|
||||
blockTime: 1640995200,
|
||||
transaction: {
|
||||
message: {
|
||||
instructions: [
|
||||
{
|
||||
program: 'spl-token',
|
||||
parsed: {
|
||||
type: 'transferChecked',
|
||||
info: {
|
||||
source: 'SPLSource',
|
||||
destination: 'SPLDest',
|
||||
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
tokenAmount: { amount: '500000' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
program: 'system',
|
||||
parsed: {
|
||||
type: 'transfer',
|
||||
info: {
|
||||
source: 'SOLSource',
|
||||
destination: 'SOLDest',
|
||||
lamports: 2000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
recentBlockhash: 'mixedBlockhash'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = extractTokenTransferData(mixedTransaction);
|
||||
|
||||
// Should return native SOL transfer data (prioritized)
|
||||
expect(result).toEqual({
|
||||
sender: 'SOLSource',
|
||||
receiver: 'SOLDest',
|
||||
amount: 2000000000,
|
||||
mint: 'So11111111111111111111111111111111111111111',
|
||||
time: 1640995200,
|
||||
blockhash: 'mixedBlockhash'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests with Sample Hashes', () => {
|
||||
it('should handle SPL token transfer hash format', () => {
|
||||
// Test that the SPL token hash format is recognized
|
||||
expect(SPL_TOKEN_TX_HASH).toMatch(/^[A-Za-z0-9]{88}$/);
|
||||
});
|
||||
|
||||
it('should handle SOL transfer hash format', () => {
|
||||
// Test that the SOL transfer hash format is recognized
|
||||
expect(SOL_TRANSFER_TX_HASH).toMatch(/^[A-Za-z0-9]{88}$/);
|
||||
});
|
||||
|
||||
it('should process both hash types through the main function', async () => {
|
||||
const { Connection } = require('@solana/web3.js');
|
||||
const mockConnection = {
|
||||
getParsedTransaction: jest.fn()
|
||||
.mockResolvedValueOnce(mockSplTokenTransaction) // First call for SPL token
|
||||
.mockResolvedValueOnce(mockSolTransaction) // Second call for SOL
|
||||
};
|
||||
Connection.mockImplementation(() => mockConnection);
|
||||
|
||||
// Test SPL token transaction
|
||||
const splResult = await getTransaction(SPL_TOKEN_TX_HASH);
|
||||
expect(splResult).toBeDefined();
|
||||
expect(splResult?.mint).not.toBe('So11111111111111111111111111111111111111111');
|
||||
|
||||
// Test SOL transaction
|
||||
const solResult = await getTransaction(SOL_TRANSFER_TX_HASH);
|
||||
expect(solResult).toBeDefined();
|
||||
expect(solResult?.mint).toBe('So11111111111111111111111111111111111111111');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
19
src/config/database.ts
Normal file
19
src/config/database.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export const databaseConfig: DatabaseConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'solpay'
|
||||
};
|
||||
81
src/index.ts
81
src/index.ts
|
|
@ -1,7 +1,10 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import { config } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
import transactionRoutes from './routes/transactionRoutes';
|
||||
import { getConnection } from './utils/database';
|
||||
import { getTransaction } from './utils/explorer';
|
||||
import { TransactionValidationService } from './services/transactionValidationService';
|
||||
logger.info("Starting server...");
|
||||
const app = express();
|
||||
const PORT = config.port;
|
||||
|
|
@ -9,6 +12,13 @@ logger.info(`port: ${PORT}`);
|
|||
logger.info(`nodeEnv: ${config.nodeEnv}`);
|
||||
logger.info(`logLevel: ${config.logLevel}`);
|
||||
|
||||
getConnection().then(() => {
|
||||
logger.info(`✅ Connected to database`);
|
||||
}).catch((err) => {
|
||||
logger.error(`Error connecting to database: ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
|
@ -31,16 +41,83 @@ app.get('/health', (_req: Request, res: Response) => {
|
|||
});
|
||||
});
|
||||
|
||||
app.get('/tx/:txHash', async (req: Request, res: Response) => {
|
||||
app.get('/solana/tx/:txHash', async (req: Request, res: Response) => {
|
||||
const txHash = req.params.txHash;
|
||||
const transaction = await getTransaction(txHash);
|
||||
res.json(transaction);
|
||||
});
|
||||
|
||||
// Validation service control endpoints
|
||||
app.get('/validation/status', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const status = TransactionValidationService.getStatus();
|
||||
const { TransactionService } = await import('./services/transactionService');
|
||||
const unvalidatedCount = await TransactionService.getUnvalidatedTransactionsCount();
|
||||
const validatedCount = await TransactionService.getValidatedTransactionsCount();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
validationService: status,
|
||||
statistics: {
|
||||
unvalidated: unvalidatedCount,
|
||||
validated: validatedCount,
|
||||
total: unvalidatedCount + validatedCount
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to get validation status',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/validation/start', (_req: Request, res: Response) => {
|
||||
TransactionValidationService.startValidationService();
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Transaction validation service started'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/validation/stop', (_req: Request, res: Response) => {
|
||||
TransactionValidationService.stopValidationService();
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Transaction validation service stopped'
|
||||
});
|
||||
});
|
||||
|
||||
// Use transaction routes
|
||||
app.use('/tx', transactionRoutes);
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
logger.info(`🚀 Server is running on port ${PORT}`);
|
||||
logger.info(`📖 API documentation available at http://localhost:${PORT}`);
|
||||
|
||||
// Start the transaction validation service
|
||||
TransactionValidationService.startValidationService();
|
||||
});
|
||||
|
||||
// Graceful shutdown handling
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM received, shutting down gracefully...');
|
||||
TransactionValidationService.stopValidationService();
|
||||
server.close(() => {
|
||||
logger.info('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('SIGINT received, shutting down gracefully...');
|
||||
TransactionValidationService.stopValidationService();
|
||||
server.close(() => {
|
||||
logger.info('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
114
src/routes/transactionRoutes.ts
Normal file
114
src/routes/transactionRoutes.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { Router, Request, Response } from 'express';
|
||||
import { TransactionService } from '../services/transactionService';
|
||||
import { CreateTransactionRequest, CreateTransactionResponse } from '../types/transaction';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /tx/new - Create a new transaction
|
||||
router.get('/new', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Extract query parameters
|
||||
const {
|
||||
tx,
|
||||
target_address,
|
||||
amount,
|
||||
token_mint,
|
||||
token_program,
|
||||
sender_address,
|
||||
metadata
|
||||
} = req.query;
|
||||
|
||||
// Validate required fields
|
||||
if (!tx || !target_address || !amount || !sender_address) {
|
||||
const response: CreateTransactionResponse = {
|
||||
success: false,
|
||||
message: 'Missing required fields',
|
||||
error: 'All fields are required: tx, target_address, amount, sender_address'
|
||||
};
|
||||
return res.status(400).json(response);
|
||||
}
|
||||
|
||||
// Create transaction data object
|
||||
const transactionData: CreateTransactionRequest = {
|
||||
tx: tx as string,
|
||||
target_address: target_address as string,
|
||||
amount: amount as string,
|
||||
token_mint: token_mint as string || 'So11111111111111111111111111111111111111111',
|
||||
token_program: token_program as string || 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
||||
sender_address: sender_address as string,
|
||||
metadata: metadata as string || ''
|
||||
};
|
||||
|
||||
// Create the transaction
|
||||
const transaction = await TransactionService.createTransaction(transactionData);
|
||||
|
||||
const response: CreateTransactionResponse = {
|
||||
success: true,
|
||||
message: 'Transaction created successfully',
|
||||
transaction
|
||||
};
|
||||
|
||||
logger.info(`New transaction created: ${tx}`);
|
||||
return res.status(201).json(response);
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('Error in /tx/new endpoint:', error);
|
||||
|
||||
const response: CreateTransactionResponse = {
|
||||
success: false,
|
||||
message: 'Failed to create transaction',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
|
||||
return res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /tx - Get all transactions
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const transactions = await TransactionService.getAllTransactions();
|
||||
return res.json({
|
||||
success: true,
|
||||
count: transactions.length,
|
||||
transactions
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching transactions:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch transactions',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /tx/:txHash - Get transaction by tx hash
|
||||
router.get('/:txHash', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const txHash = req.params.txHash;
|
||||
const transaction = await TransactionService.getTransactionByTx(txHash);
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Transaction ${txHash} not found`
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
transaction
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching transaction:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch transaction',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
82
src/services/transactionService.ts
Normal file
82
src/services/transactionService.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { query } from '../utils/database';
|
||||
import { Transaction, CreateTransactionRequest } from '../types/transaction';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export class TransactionService {
|
||||
static async createTransaction(transactionData: CreateTransactionRequest): Promise<Transaction> {
|
||||
const sql = `
|
||||
INSERT INTO transactions (
|
||||
tx, target_address, amount, token_mint, token_program,
|
||||
sender_address, metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params = [
|
||||
transactionData.tx,
|
||||
transactionData.target_address,
|
||||
transactionData.amount,
|
||||
transactionData.token_mint,
|
||||
transactionData.token_program,
|
||||
transactionData.sender_address,
|
||||
transactionData.metadata
|
||||
];
|
||||
|
||||
try {
|
||||
await query(sql, params);
|
||||
logger.info(`Transaction created with tx: ${transactionData.tx}`);
|
||||
|
||||
// Return the created transaction
|
||||
return transactionData;
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTransactionByTx(tx: string): Promise<Transaction | null> {
|
||||
const sql = 'SELECT * FROM transactions WHERE tx = ?';
|
||||
|
||||
try {
|
||||
const result = await query<Transaction>(sql, [tx]);
|
||||
return result.length > 0 ? result[0] : null;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getAllTransactions(): Promise<Transaction[]> {
|
||||
const sql = 'SELECT * FROM transactions ORDER BY created_at DESC';
|
||||
|
||||
try {
|
||||
return await query<Transaction>(sql);
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching all transactions:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUnvalidatedTransactionsCount(): Promise<number> {
|
||||
const sql = 'SELECT COUNT(*) as count FROM transactions WHERE validated_at IS NULL';
|
||||
|
||||
try {
|
||||
const result = await query<{ count: number }>(sql);
|
||||
return result[0]?.count || 0;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching unvalidated transactions count:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getValidatedTransactionsCount(): Promise<number> {
|
||||
const sql = 'SELECT COUNT(*) as count FROM transactions WHERE validated_at IS NOT NULL';
|
||||
|
||||
try {
|
||||
const result = await query<{ count: number }>(sql);
|
||||
return result[0]?.count || 0;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching validated transactions count:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/services/transactionValidationService.ts
Normal file
206
src/services/transactionValidationService.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { query } from '../utils/database';
|
||||
import { getTransaction } from '../utils/explorer';
|
||||
import { logger } from '../utils/logger';
|
||||
import { TransferData } from '../types';
|
||||
|
||||
export interface UnvalidatedTransaction {
|
||||
id: number;
|
||||
tx: string;
|
||||
target_address: string;
|
||||
amount: string;
|
||||
token_mint: string;
|
||||
token_program: string;
|
||||
sender_address: string;
|
||||
metadata: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export class TransactionValidationService {
|
||||
private static validationInterval: NodeJS.Timeout | null = null;
|
||||
private static isRunning = false;
|
||||
|
||||
/**
|
||||
* Start the transaction validation service
|
||||
*/
|
||||
static startValidationService(): void {
|
||||
if (this.isRunning) {
|
||||
logger.warn('Transaction validation service is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Starting transaction validation service...');
|
||||
this.isRunning = true;
|
||||
|
||||
// Run validation immediately
|
||||
this.validateUnvalidatedTransactions();
|
||||
|
||||
// Set up interval to run every 5 seconds
|
||||
this.validationInterval = setInterval(() => {
|
||||
this.validateUnvalidatedTransactions();
|
||||
}, 5000);
|
||||
|
||||
logger.info('Transaction validation service started successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the transaction validation service
|
||||
*/
|
||||
static stopValidationService(): void {
|
||||
if (!this.isRunning) {
|
||||
logger.warn('Transaction validation service is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.validationInterval) {
|
||||
clearInterval(this.validationInterval);
|
||||
this.validationInterval = null;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
logger.info('Transaction validation service stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unvalidated transactions (where validated_at is null)
|
||||
*/
|
||||
private static async getUnvalidatedTransactions(): Promise<UnvalidatedTransaction[]> {
|
||||
try {
|
||||
const sql = 'SELECT * FROM transactions WHERE validated_at IS NULL ORDER BY created_at ASC';
|
||||
const transactions = await query<UnvalidatedTransaction>(sql);
|
||||
return transactions;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching unvalidated transactions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update transaction as validated
|
||||
*/
|
||||
private static async markTransactionAsValidated(txHash: string): Promise<void> {
|
||||
try {
|
||||
const sql = 'UPDATE transactions SET validated_at = NOW() WHERE tx = ?';
|
||||
await query(sql, [txHash]);
|
||||
logger.info(`Transaction ${txHash} marked as validated`);
|
||||
} catch (error: any) {
|
||||
logger.error(`Error marking transaction ${txHash} as validated:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single transaction against blockchain data
|
||||
*/
|
||||
private static async validateTransaction(transaction: UnvalidatedTransaction): Promise<boolean> {
|
||||
try {
|
||||
logger.debug(`Validating transaction: ${transaction.tx}`);
|
||||
|
||||
// Get blockchain data
|
||||
const blockchainData = await getTransaction(transaction.tx);
|
||||
|
||||
if (!blockchainData) {
|
||||
logger.warn(`No blockchain data found for transaction: ${transaction.tx}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare transaction data with blockchain data
|
||||
const isValid = this.compareTransactionData(transaction, blockchainData);
|
||||
|
||||
if (isValid) {
|
||||
await this.markTransactionAsValidated(transaction.tx);
|
||||
logger.info(`Transaction ${transaction.tx} validated successfully`);
|
||||
return true;
|
||||
} else {
|
||||
logger.warn(`Transaction ${transaction.tx} validation failed - data mismatch`);
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`Error validating transaction ${transaction.tx}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare transaction data with blockchain data
|
||||
*/
|
||||
private static compareTransactionData(
|
||||
dbTransaction: UnvalidatedTransaction,
|
||||
blockchainData: TransferData
|
||||
): boolean {
|
||||
try {
|
||||
// Check if sender addresses match
|
||||
if (dbTransaction.sender_address !== blockchainData.sender) {
|
||||
logger.debug(`Sender mismatch for ${dbTransaction.tx}: DB=${dbTransaction.sender_address}, BC=${blockchainData.sender}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if receiver addresses match
|
||||
if (dbTransaction.target_address !== blockchainData.receiver) {
|
||||
logger.debug(`Receiver mismatch for ${dbTransaction.tx}: DB=${dbTransaction.target_address}, BC=${blockchainData.receiver}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if amounts match (convert to string for comparison)
|
||||
const dbAmount = dbTransaction.amount;
|
||||
const bcAmount = blockchainData.amount.toString();
|
||||
|
||||
// Convert both amounts to BigInt for reliable comparison
|
||||
const dbAmountBigInt = BigInt(dbAmount);
|
||||
const bcAmountBigInt = BigInt(bcAmount);
|
||||
|
||||
if (dbAmountBigInt !== bcAmountBigInt) {
|
||||
logger.debug(`Amount mismatch for ${dbTransaction.tx}: DB=${dbAmount}, BC=${bcAmount}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if token mint addresses match
|
||||
if (dbTransaction.token_mint !== blockchainData.mint) {
|
||||
logger.debug(`Token mint mismatch for ${dbTransaction.tx}: DB=${dbTransaction.token_mint}, BC=${blockchainData.mint}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(`Transaction ${dbTransaction.tx} data matches blockchain data`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
logger.error(`Error comparing transaction data for ${dbTransaction.tx}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation method that processes all unvalidated transactions
|
||||
*/
|
||||
private static async validateUnvalidatedTransactions(): Promise<void> {
|
||||
try {
|
||||
const unvalidatedTransactions = await this.getUnvalidatedTransactions();
|
||||
|
||||
if (unvalidatedTransactions.length === 0) {
|
||||
logger.debug('No unvalidated transactions found');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Found ${unvalidatedTransactions.length} unvalidated transactions to process`);
|
||||
|
||||
// Process transactions sequentially to avoid overwhelming the blockchain API
|
||||
for (const transaction of unvalidatedTransactions) {
|
||||
await this.validateTransaction(transaction);
|
||||
|
||||
// Add a small delay between validations to be respectful to the blockchain API
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
logger.debug('Completed validation cycle');
|
||||
} catch (error: any) {
|
||||
logger.error('Error in validation cycle:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service status
|
||||
*/
|
||||
static getStatus(): { isRunning: boolean; hasInterval: boolean } {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
hasInterval: this.validationInterval !== null
|
||||
};
|
||||
}
|
||||
}
|
||||
29
src/types/transaction.ts
Normal file
29
src/types/transaction.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export interface Transaction {
|
||||
tx: string;
|
||||
target_address: string;
|
||||
amount: string;
|
||||
token_mint: string;
|
||||
token_program: string;
|
||||
sender_address: string;
|
||||
metadata: string;
|
||||
validated_at?: Date | null;
|
||||
created_at?: Date;
|
||||
updated_at?: Date;
|
||||
}
|
||||
|
||||
export interface CreateTransactionRequest {
|
||||
tx: string;
|
||||
target_address: string;
|
||||
amount: string;
|
||||
token_mint: string;
|
||||
token_program: string;
|
||||
sender_address: string;
|
||||
metadata: string;
|
||||
}
|
||||
|
||||
export interface CreateTransactionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
transaction?: Transaction;
|
||||
error?: string;
|
||||
}
|
||||
37
src/utils/database.ts
Normal file
37
src/utils/database.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import mysql from 'mysql2/promise';
|
||||
import { databaseConfig } from '../config/database';
|
||||
import { logger } from './logger';
|
||||
|
||||
let connection: mysql.Connection | null = null;
|
||||
|
||||
export async function getConnection(): Promise<mysql.Connection> {
|
||||
if (!connection) {
|
||||
try {
|
||||
connection = await mysql.createConnection(databaseConfig);
|
||||
logger.info('Database connection established');
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to connect to database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
export async function closeConnection(): Promise<void> {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
connection = null;
|
||||
logger.info('Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const conn = await getConnection();
|
||||
try {
|
||||
const [rows] = await conn.execute(sql, params);
|
||||
return rows as T[];
|
||||
} catch (error: any) {
|
||||
logger.error('Database query error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { Connection } from "@solana/web3.js";
|
|||
import { TransferData } from "../types";
|
||||
|
||||
export async function getTransaction(txHash: string) {
|
||||
const connection = new Connection(CLUSTER_API);
|
||||
const connection = new Connection(CLUSTER_API, { commitment: 'finalized' });
|
||||
|
||||
const transaction = await connection.getParsedTransaction(txHash, {
|
||||
maxSupportedTransactionVersion: 0,
|
||||
|
|
|
|||
2
start_server.sh
Executable file
2
start_server.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
cd /root/NodeJS/solpay/
|
||||
npm run start
|
||||
Loading…
Reference in New Issue
Block a user