This commit is contained in:
Sewmina (server) 2025-08-14 01:52:03 +08:00
commit 502420a1b2
15 changed files with 6290 additions and 0 deletions

94
.gitignore vendored Normal file
View File

@ -0,0 +1,94 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
dist/
build/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

105
README.md Normal file
View File

@ -0,0 +1,105 @@
# SolPay - Node.js TypeScript Application
A modern Node.js application built with TypeScript, featuring Express.js for the web server and comprehensive testing setup.
## Features
- 🚀 **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
## Prerequisites
- Node.js (v16 or higher)
- npm or yarn
## Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd solpay
```
2. Install dependencies:
```bash
npm install
```
## Available Scripts
- **`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
## Development
To start developing:
```bash
npm run dev
```
This will start the server using ts-node, which automatically compiles TypeScript on the fly.
## Building for Production
```bash
npm run build
npm start
```
## Project Structure
```
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

15
jest.config.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
};

5534
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "solpay",
"version": "1.0.0",
"description": "A Node.js TypeScript application",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"watch": "tsc --watch",
"clean": "rm -rf dist",
"test": "jest"
},
"keywords": [
"nodejs",
"typescript",
"solpay"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/express": "^5.0.3",
"@types/jest": "^29.5.8",
"@types/node": "^20.10.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.0",
"typescript": "^5.3.0"
},
"dependencies": {
"@solana/web3.js": "^1.98.4",
"dotenv": "^17.2.1",
"express": "^4.18.2"
}
}

View File

@ -0,0 +1,77 @@
import { config } from '../config';
describe('Config', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});
describe('port configuration', () => {
it('should use default port 3000 when PORT is not set', () => {
delete process.env.PORT;
const { config: testConfig } = require('../config');
expect(testConfig.port).toBe(3000);
});
it('should use PORT environment variable when set', () => {
process.env.PORT = '8080';
const { config: testConfig } = require('../config');
expect(testConfig.port).toBe(8080);
});
it('should parse string port to number', () => {
process.env.PORT = '5000';
const { config: testConfig } = require('../config');
expect(typeof testConfig.port).toBe('number');
expect(testConfig.port).toBe(5000);
});
});
describe('nodeEnv configuration', () => {
it('should use default nodeEnv "development" when NODE_ENV is not set', () => {
delete process.env.NODE_ENV;
const { config: testConfig } = require('../config');
expect(testConfig.nodeEnv).toBe('development');
});
it('should use NODE_ENV environment variable when set', () => {
process.env.NODE_ENV = 'production';
const { config: testConfig } = require('../config');
expect(testConfig.nodeEnv).toBe('production');
});
});
describe('logLevel configuration', () => {
it('should use default logLevel "info" when LOG_LEVEL is not set', () => {
delete process.env.LOG_LEVEL;
const { config: testConfig } = require('../config');
expect(testConfig.logLevel).toBe('info');
});
it('should use LOG_LEVEL environment variable when set', () => {
process.env.LOG_LEVEL = 'debug';
const { config: testConfig } = require('../config');
expect(testConfig.logLevel).toBe('debug');
});
});
describe('config interface', () => {
it('should have all required properties', () => {
expect(config).toHaveProperty('port');
expect(config).toHaveProperty('nodeEnv');
expect(config).toHaveProperty('logLevel');
});
it('should have correct types', () => {
expect(typeof config.port).toBe('number');
expect(typeof config.nodeEnv).toBe('string');
expect(typeof config.logLevel).toBe('string');
});
});
});

View File

@ -0,0 +1,3 @@
//spl token transfer tx : 2Dq3cW3D7z75QNfjiTfDu3GgS2gaqrqmoaqzDoomTkJqT2ThSyNDNf1tTz8SgXmzSon9FHCyN61EFkAdkJquhbJf
//sol transfer tx : 3c6AiQmRxnAKSQyQ3C6jPoEmhGPHieQ2uJWuBQjdMA2TPXMWeGtL6PmdUGrMdojFoazq1NUQnDb9qCq1o4sxYvZ8

View File

@ -0,0 +1,85 @@
import { Logger } from '../utils/logger';
describe('Logger', () => {
let logger: Logger;
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
logger = new Logger('debug');
consoleSpy = jest.spyOn(console, 'info').mockImplementation();
});
afterEach(() => {
consoleSpy.mockRestore();
});
describe('constructor', () => {
it('should set default log level to info when no level provided', () => {
const defaultLogger = new Logger();
expect(defaultLogger).toBeInstanceOf(Logger);
});
it('should parse log level correctly', () => {
const debugLogger = new Logger('debug');
const infoLogger = new Logger('info');
const warnLogger = new Logger('warn');
const errorLogger = new Logger('error');
expect(debugLogger).toBeInstanceOf(Logger);
expect(infoLogger).toBeInstanceOf(Logger);
expect(warnLogger).toBeInstanceOf(Logger);
expect(errorLogger).toBeInstanceOf(Logger);
});
});
describe('info method', () => {
it('should log info messages when log level is info or lower', () => {
logger.info('Test info message');
expect(consoleSpy).toHaveBeenCalled();
});
it('should not log info messages when log level is warn or higher', () => {
const warnLogger = new Logger('warn');
warnLogger.info('Test info message');
expect(consoleSpy).not.toHaveBeenCalled();
});
});
describe('debug method', () => {
it('should log debug messages when log level is debug', () => {
logger.debug('Test debug message');
expect(consoleSpy).not.toHaveBeenCalled();
});
it('should not log debug messages when log level is info or higher', () => {
const infoLogger = new Logger('info');
infoLogger.debug('Test debug message');
expect(consoleSpy).not.toHaveBeenCalled();
});
});
describe('warn method', () => {
it('should log warn messages when log level is warn or lower', () => {
logger.warn('Test warn message');
expect(consoleSpy).not.toHaveBeenCalled();
});
});
describe('error method', () => {
it('should log error messages when log level is error or lower', () => {
logger.error('Test error message');
expect(consoleSpy).not.toHaveBeenCalled();
});
});
describe('context logging', () => {
it('should include context in log messages', () => {
const context = { userId: '123', action: 'login' };
logger.info('User action', context);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('User action')
);
});
});
});

31
src/config/index.ts Normal file
View File

@ -0,0 +1,31 @@
import dotenv from 'dotenv';
import { Environment } from '../types';
// Load environment variables from .env file
dotenv.config();
export interface Config {
port: number;
nodeEnv: Environment;
logLevel: 'error' | 'warn' | 'info' | 'debug';
}
// Validate and export environment configuration
export const config: Config = {
port: Number(process.env.PORT) || 3000,
nodeEnv: (process.env.NODE_ENV as Environment) || 'development',
logLevel: (process.env.LOG_LEVEL as Config['logLevel']) || 'info'
};
// Validate required environment variables
if (!config.port || isNaN(config.port)) {
throw new Error('Invalid PORT configuration');
}
if (!['development', 'staging', 'production'].includes(config.nodeEnv)) {
throw new Error('Invalid NODE_ENV configuration');
}
if (!['error', 'warn', 'info', 'debug'].includes(config.logLevel)) {
throw new Error('Invalid LOG_LEVEL configuration');
}

7
src/data/index.ts Normal file
View File

@ -0,0 +1,7 @@
import { clusterApiUrl } from "@solana/web3.js";
import { config } from "../config";
const CLUSTER_API_PROD = clusterApiUrl("mainnet-beta");
const CLUSTER_API_DEV = clusterApiUrl("devnet");
export const CLUSTER_API = config.nodeEnv === "production" ? CLUSTER_API_PROD : CLUSTER_API_DEV;

46
src/index.ts Normal file
View File

@ -0,0 +1,46 @@
import express, { Request, Response } from 'express';
import { config } from './config';
import { logger } from './utils/logger';
import { getTransaction } from './utils/explorer';
logger.info("Starting server...");
const app = express();
const PORT = config.port;
logger.info(`port: ${PORT}`);
logger.info(`nodeEnv: ${config.nodeEnv}`);
logger.info(`logLevel: ${config.logLevel}`);
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic route
app.get('/', (_req: Request, res: Response) => {
res.json({
message: 'Welcome to SolPay API',
version: '1.0.0',
timestamp: new Date().toISOString()
});
});
// Health check endpoint
app.get('/health', (_req: Request, res: Response) => {
res.json({
status: 'OK',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
app.get('/tx/:txHash', async (req: Request, res: Response) => {
const txHash = req.params.txHash;
const transaction = await getTransaction(txHash);
res.json(transaction);
});
// Start server
app.listen(PORT, () => {
logger.info(`🚀 Server is running on port ${PORT}`);
logger.info(`📖 API documentation available at http://localhost:${PORT}`);
});
export default app;

67
src/types/index.ts Normal file
View File

@ -0,0 +1,67 @@
// API Response types
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
error?: string;
timestamp: string;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Request types
export interface PaginationQuery {
page?: number;
limit?: number;
}
// Error types
export interface AppError extends Error {
statusCode: number;
isOperational: boolean;
}
// User types (example)
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
// Validation types
export interface ValidationError {
field: string;
message: string;
value?: any;
}
// Environment types
export type Environment = 'development' | 'staging' | 'production';
// Database types (example)
export interface DatabaseConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
}
export interface TransferData {
sender: string;
receiver: string;
amount: number;
mint: string;
time: number;
blockhash: string;
}

86
src/utils/explorer.ts Normal file
View File

@ -0,0 +1,86 @@
import { CLUSTER_API } from "../data";
import { Connection } from "@solana/web3.js";
import { TransferData } from "../types";
export async function getTransaction(txHash: string) {
const connection = new Connection(CLUSTER_API);
const transaction = await connection.getParsedTransaction(txHash, {
maxSupportedTransactionVersion: 0,
});
const transaction_formatted = extractTokenTransferData(transaction);
return transaction_formatted;
}
export function extractNativeSolTransferData(transaction: any): TransferData | null {
try {
// Check if this is a native SOL transfer
const instructions = transaction.transaction.message.instructions;
const systemTransferInstruction = instructions.find((instruction: any) =>
instruction.program === "system" && instruction.parsed?.type === "transfer"
);
if (!systemTransferInstruction) {
return null; // Not a native SOL transfer
}
const transferInfo = systemTransferInstruction.parsed.info;
return {
sender: transferInfo.source,
receiver: transferInfo.destination,
amount: transferInfo.lamports,
mint: "So11111111111111111111111111111111111111111", // Native SOL mint address
time: transaction.blockTime,
blockhash: transaction.transaction.message.recentBlockhash
};
} catch (error) {
console.error("Error extracting native SOL transfer data:", error);
return null;
}
}
export function extractSplTokenTransferData(transaction: any): TransferData | null {
try {
// Check if this is an SPL token transfer
const instructions = transaction.transaction.message.instructions;
const tokenTransferInstruction = instructions.find((instruction: any) =>
instruction.program === "spl-token" && instruction.parsed?.type === "transferChecked"
);
if (!tokenTransferInstruction) {
return null; // Not an SPL token transfer
}
const transferInfo = tokenTransferInstruction.parsed.info;
return {
sender: transferInfo.source,
receiver: transferInfo.destination,
amount: parseInt(transferInfo.tokenAmount.amount),
mint: transferInfo.mint,
time: transaction.blockTime,
blockhash: transaction.transaction.message.recentBlockhash
};
} catch (error) {
console.error("Error extracting SPL token transfer data:", error);
return null;
}
}
export function extractTokenTransferData(transaction: any): TransferData | null {
// Try to extract native SOL transfer first
const nativeSolData = extractNativeSolTransferData(transaction);
if (nativeSolData) {
return nativeSolData;
}
// Try to extract SPL token transfer
const splTokenData = extractSplTokenTransferData(transaction);
if (splTokenData) {
return splTokenData;
}
return null; // Not a recognized token transfer
}

74
src/utils/logger.ts Normal file
View File

@ -0,0 +1,74 @@
export enum LogLevel {
ERROR = 'error',
WARN = 'warn',
INFO = 'info',
DEBUG = 'debug'
}
export interface LogMessage {
level: LogLevel;
message: string;
timestamp: string;
context?: Record<string, any>;
}
export class Logger {
private logLevel: LogLevel;
constructor(logLevel: string = 'info') {
this.logLevel = this.parseLogLevel(logLevel);
}
private parseLogLevel(level: string): LogLevel {
switch (level.toLowerCase()) {
case 'error': return LogLevel.ERROR;
case 'warn': return LogLevel.WARN;
case 'info': return LogLevel.INFO;
case 'debug': return LogLevel.DEBUG;
default: return LogLevel.INFO;
}
}
private shouldLog(level: LogLevel): boolean {
const levels = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG];
return levels.indexOf(level) <= levels.indexOf(this.logLevel);
}
private formatMessage(level: LogLevel, message: string, context?: Record<string, any>): string {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
if (context) {
return `${prefix} ${message} ${JSON.stringify(context)}`;
}
return `${prefix} ${message}`;
}
error(message: string, context?: Record<string, any>): void {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatMessage(LogLevel.ERROR, message, context));
}
}
warn(message: string, context?: Record<string, any>): void {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatMessage(LogLevel.WARN, message, context));
}
}
info(message: string, context?: Record<string, any>): void {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatMessage(LogLevel.INFO, message, context));
}
}
debug(message: string, context?: Record<string, any>): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatMessage(LogLevel.DEBUG, message, context));
}
}
}
// Create default logger instance
export const logger = new Logger(process.env.LOG_LEVEL);

31
tsconfig.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
]
}