├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── config │ └── typeorm.config.ts ├── data-source.ts ├── index.ts ├── main.ts ├── message │ ├── message.entity.ts │ ├── message.module.ts │ ├── message.service.spec.ts │ └── message.service.ts ├── redis-cache │ ├── redis-cache.module.ts │ ├── redis-cache.service.spec.ts │ └── redis-cache.service.ts ├── setting │ ├── setting.controller.spec.ts │ ├── setting.controller.ts │ ├── setting.entity.ts │ ├── setting.module.ts │ ├── setting.service.spec.ts │ └── setting.service.ts ├── telegram │ ├── invite.update.ts │ ├── screen │ │ ├── contract.screen.ts │ │ ├── invite.screen.ts │ │ ├── screen.module.ts │ │ ├── screen.service.spec.ts │ │ ├── screen.service.ts │ │ ├── setting.screen.ts │ │ ├── start.screen.ts │ │ └── wallet.screen.ts │ ├── setting.update.ts │ ├── telegram.module.ts │ ├── telegram.service.spec.ts │ ├── telegram.service.ts │ ├── telegram.update.ts │ └── wallet.update.ts ├── token-info │ ├── jupiter-price-response-dto │ │ ├── data-item.dto.ts │ │ ├── depth.dto.ts │ │ ├── extra-info.dto.ts │ │ ├── last-swapped-price.dto.ts │ │ ├── price-impact-ratio.dto.ts │ │ ├── quoted-price.dto.ts │ │ └── response.dto.ts │ ├── pump-fun │ │ ├── pump-fun-response.dto.ts │ │ ├── pump-fun.module.ts │ │ ├── pump-fun.service.spec.ts │ │ └── pump-fun.service.ts │ ├── token-info.dto.ts │ ├── token-info.entity.ts │ ├── token-info.module.ts │ ├── token-info.service.spec.ts │ └── token-info.service.ts ├── trade │ ├── jito.service.ts │ ├── result.consumer.ts │ ├── solana.service.ts │ ├── trade.entity.ts │ ├── trade.module.ts │ ├── trade.service.spec.ts │ └── trade.service.ts ├── user │ ├── user.entity.ts │ ├── user.module.ts │ ├── user.service.spec.ts │ └── user.service.ts └── wallet │ ├── wallet.entity.ts │ ├── wallet.module.ts │ ├── wallet.service.spec.ts │ └── wallet.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # REDIS 2 | REDIS_HOST= 3 | REDIS_PORT= 4 | REDIS_PASSWORD= 5 | 6 | # SHYFT 7 | SHYFT_API_KEY= 8 | 9 | # DATABASE 10 | DATABASE_TYPE= 11 | DATABASE_HOST= 12 | DATABASE_PORT= 13 | DATABASE_USER= 14 | DATABASE_PASSWORD= 15 | DATABASE_NAME= 16 | 17 | # Solana 18 | SOLANA_ENDPOINT= 19 | 20 | # Telegram 21 | TELEGRAM_BOT_TOKEN= 22 | 23 | # Fee 24 | FEE_RECIPIENT_ADDRESS= 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ 7 | .env 8 | src/migrations 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Trading Bot for Telegram 2 | 3 | ## 简介 4 | 5 | 一个用于Telegram的Solana交易机器人,功能与 Banana Gun/ PepeBooster 等交易机器人类似。 6 | 功能完成度约95%,部署后可以已经可以进行交易,一些设置功能还在使用默认值。 7 | 8 | ## 技术说明 9 | 10 | 使用了Nest.js框架,TypeOrm/MySql数据库(你也可以使用Postgresql,我也准备如果部署生产就改用Postgresql)。 11 | 交易提交方面使用 Jito,目前测试下来感觉速度不错,交易上链速度很快,成功率高。有些地方为了节约Api使用次数,用了Redis缓存,同时检测交易结果时也用了Redis队列。 12 | 暂时还没有做模块间的微服务化,但感觉如果用户量上来之后应该做一下更好。 13 | 14 | ## 使用说明 15 | 16 | ```bash 17 | npm install 18 | npm run build 19 | npm run start 20 | ``` 21 | 22 | 需要在.env中配置相关参数(数据库,redis,telegram bot token等),具体细节不再赘述。 23 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradingbot", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "ts-node src/index.ts", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "migration:run": "pnpm run typeorm migration:run -- -d src/config/typeorm.config.ts", 22 | "migration:generate": "pnpm typeorm -- -d src/config/typeorm.config.ts migration:generate ./src/migrations/$npm_config_name", 23 | "migration:create": "pnpm typeorm -- migration:create ./src/migrations/$npm_config_name", 24 | "migration:revert": "pnpm typeorm -- -d src/config/typeorm.config.ts migration:revert", 25 | "typeorm": "typeorm-ts-node-commonjs", 26 | "migration:gen": "pnpm typeorm migration:generate .\\src\\migrations\\NewMigration -d .\\src\\data-source.ts", 27 | "migration:r": "pnpm typeorm migration:run -d .\\src\\data-source.ts" 28 | }, 29 | "dependencies": { 30 | "@jup-ag/api": "^6.0.29", 31 | "@nestjs/bullmq": "^10.2.1", 32 | "@nestjs/cache-manager": "^2.2.2", 33 | "@nestjs/common": "^10.4.4", 34 | "@nestjs/config": "^3.2.3", 35 | "@nestjs/core": "^10.4.4", 36 | "@nestjs/microservices": "^10.4.4", 37 | "@nestjs/platform-express": "^10.4.4", 38 | "@nestjs/typeorm": "^10.0.2", 39 | "@solana/spl-token": "^0.4.8", 40 | "@solana/web3.js": "^1.95.3", 41 | "bs58": "^6.0.0", 42 | "bullmq": "^5.18.0", 43 | "cache-manager": "^5.7.6", 44 | "cache-manager-redis-store": "^3.0.1", 45 | "class-transformer": "^0.5.1", 46 | "class-validator": "^0.14.1", 47 | "ioredis": "^5.4.1", 48 | "lodash": "^4.17.21", 49 | "nestjs-telegraf": "^2.8.1", 50 | "pg": "^8.13.0", 51 | "redis": "^4.7.0", 52 | "reflect-metadata": "^0.2.2", 53 | "rxjs": "^7.8.1", 54 | "telegraf": "^4.16.3", 55 | "typeorm": "0.3.21-dev.e7649d2" 56 | }, 57 | "devDependencies": { 58 | "@nestjs/cli": "^10.4.5", 59 | "@nestjs/schematics": "^10.1.4", 60 | "@nestjs/testing": "^10.4.4", 61 | "@types/bs58": "^4.0.4", 62 | "@types/express": "^4.17.21", 63 | "@types/jest": "^29.5.13", 64 | "@types/lodash": "^4.17.10", 65 | "@types/node": "^20.16.10", 66 | "@types/supertest": "^6.0.2", 67 | "@typescript-eslint/eslint-plugin": "^8.8.0", 68 | "@typescript-eslint/parser": "^8.8.0", 69 | "eslint": "^8.57.1", 70 | "eslint-config-prettier": "^9.1.0", 71 | "eslint-plugin-prettier": "^5.2.1", 72 | "jest": "^29.7.0", 73 | "mysql2": "^3.11.3", 74 | "nodemon": "^3.1.7", 75 | "prettier": "^3.3.3", 76 | "source-map-support": "^0.5.21", 77 | "supertest": "^7.0.0", 78 | "ts-jest": "^29.2.5", 79 | "ts-loader": "^9.5.1", 80 | "ts-node": "10.9.1", 81 | "tsconfig-paths": "^4.2.0", 82 | "tsx": "^4.19.1", 83 | "typescript": "5.6.2" 84 | }, 85 | "jest": { 86 | "moduleFileExtensions": [ 87 | "js", 88 | "json", 89 | "ts" 90 | ], 91 | "rootDir": "src", 92 | "testRegex": ".*\\.spec\\.ts$", 93 | "transform": { 94 | "^.+\\.(t|j)s$": "ts-jest" 95 | }, 96 | "collectCoverageFrom": [ 97 | "**/*.(t|j)s" 98 | ], 99 | "coverageDirectory": "../coverage", 100 | "testEnvironment": "node" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { TokenInfoModule } from './token-info/token-info.module'; 5 | import { ConfigModule, ConfigService } from '@nestjs/config'; 6 | import { RedisCacheModule } from './redis-cache/redis-cache.module'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { typeOrmConfig } from './config/typeorm.config'; 9 | import { WalletModule } from './wallet/wallet.module'; 10 | import { TradeModule } from './trade/trade.module'; 11 | import { UserModule } from './user/user.module'; 12 | import { MessageModule } from './message/message.module'; 13 | import { SettingModule } from './setting/setting.module'; 14 | import { TelegramModule } from './telegram/telegram.module'; 15 | import { TelegrafModule } from 'nestjs-telegraf'; 16 | import { BullModule } from '@nestjs/bullmq'; 17 | 18 | @Module({ 19 | imports: [ 20 | TokenInfoModule, 21 | ConfigModule.forRoot({ isGlobal: true }), 22 | RedisCacheModule, 23 | TypeOrmModule.forRootAsync({ 24 | imports: [ConfigModule], 25 | inject: [ConfigService], 26 | useFactory: typeOrmConfig, 27 | }), 28 | UserModule, 29 | WalletModule, 30 | TradeModule, 31 | MessageModule, 32 | SettingModule, 33 | TelegramModule, 34 | TelegrafModule.forRootAsync({ 35 | imports: [ConfigModule], 36 | inject: [ConfigService], 37 | useFactory: (configService: ConfigService) => ({ 38 | token: configService.get('TELEGRAM_BOT_TOKEN'), 39 | include: [TelegramModule], 40 | }), 41 | }), 42 | BullModule.forRootAsync({ 43 | imports: [ConfigModule], 44 | inject: [ConfigService], 45 | useFactory: (configService: ConfigService) => ({ 46 | connection: { 47 | host: configService.get('REDIS_HOST'), 48 | port: configService.get('REDIS_PORT'), 49 | password: configService.get('REDIS_PASSWORD'), 50 | }, 51 | }), 52 | }), 53 | ], 54 | controllers: [AppController], 55 | providers: [AppService], 56 | }) 57 | export class AppModule {} 58 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/config/typeorm.config.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | export const typeOrmConfig = ( 5 | configService: ConfigService, 6 | ): TypeOrmModuleOptions => ({ 7 | type: 'mysql', 8 | host: configService.get('DATABASE_HOST'), 9 | port: configService.get('DATABASE_PORT'), 10 | username: configService.get('DATABASE_USER'), 11 | password: configService.get('DATABASE_PASSWORD'), 12 | database: configService.get('DATABASE_NAME'), 13 | entities: [__dirname + '/../**/*.entity{.ts,.js}'], 14 | migrations: [__dirname + '/../migrations/*{.ts,.js}'], 15 | synchronize: false, // 生产环境下应设为 false 16 | autoLoadEntities: true, 17 | }); 18 | -------------------------------------------------------------------------------- /src/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { config } from 'dotenv'; 4 | import { TokenInfo } from './token-info/token-info.entity'; 5 | import { User } from './user/user.entity'; 6 | import { Wallet } from './wallet/wallet.entity'; 7 | import { Trade } from './trade/trade.entity'; 8 | import { Message } from './message/message.entity'; 9 | import { Setting } from './setting/setting.entity'; 10 | 11 | config(); // 加载 .env 文件中的环境变量 12 | 13 | const configService = new ConfigService(); 14 | 15 | export const AppDataSource = new DataSource({ 16 | type: 'mysql', 17 | host: configService.get('DATABASE_HOST'), 18 | port: configService.get('DATABASE_PORT'), 19 | username: configService.get('DATABASE_USER'), 20 | password: configService.get('DATABASE_PASSWORD'), 21 | database: configService.get('DATABASE_NAME'), 22 | entities: [TokenInfo, User, Wallet, Trade, Message, Setting], 23 | migrations: ['src/migrations/*.ts'], 24 | synchronize: false, 25 | }); 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from './data-source'; 2 | import 'reflect-metadata'; 3 | 4 | AppDataSource.initialize() 5 | .then(async () => { 6 | console.log( 7 | 'Here you can setup and run express / fastify / any other framework.', 8 | ); 9 | }) 10 | .catch((error) => console.log(error)); 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import 'reflect-metadata'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | await app.listen(3001); 8 | } 9 | 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /src/message/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { User } from '../user/user.entity'; 3 | 4 | @Entity() 5 | export class Message { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ nullable: true }) 10 | mint: string; 11 | 12 | @Column() 13 | msgId: number; 14 | 15 | @Column({ nullable: true }) 16 | isBuy: boolean; 17 | 18 | @ManyToOne(() => User, (user) => user.messages) 19 | user: User; 20 | 21 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 22 | created_at: Date; 23 | } 24 | -------------------------------------------------------------------------------- /src/message/message.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Message } from './message.entity'; 4 | import { UserModule } from '../user/user.module'; 5 | import { MessageService } from './message.service'; 6 | import { UserService } from '../user/user.service'; 7 | import { User } from '../user/user.entity'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([Message, User]), UserModule], 11 | providers: [MessageService, UserService], 12 | }) 13 | export class MessageModule {} 14 | -------------------------------------------------------------------------------- /src/message/message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessageService } from './message.service'; 3 | 4 | describe('MessageService', () => { 5 | let service: MessageService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MessageService], 10 | }).compile(); 11 | 12 | service = module.get(MessageService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/message/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Message } from './message.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { UserService } from '../user/user.service'; 6 | 7 | @Injectable() 8 | export class MessageService { 9 | constructor( 10 | @InjectRepository(Message) 11 | private readonly messageRepository: Repository, 12 | private readonly userService: UserService, 13 | ) {} 14 | 15 | async create(userId: number, msgId: number, mint?: string, isBuy?: boolean) { 16 | const user = await this.userService.findUser(userId); 17 | if (user) { 18 | const message = this.messageRepository.create({ 19 | user: { id: userId }, 20 | msgId, 21 | mint, 22 | isBuy, 23 | }); 24 | await this.messageRepository.save(message); 25 | } 26 | } 27 | 28 | async getData(userId: number, msgId: number) { 29 | const msg = await this.messageRepository.findOneBy({ 30 | user: { id: userId }, 31 | msgId, 32 | }); 33 | if (msg) { 34 | return msg; 35 | } else { 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/redis-cache/redis-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { RedisCacheService } from './redis-cache.service'; 4 | import { CacheModule, CacheStore } from '@nestjs/cache-manager'; 5 | import { redisStore } from 'cache-manager-redis-store'; 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule, 10 | CacheModule.registerAsync({ 11 | imports: [ConfigModule], 12 | useFactory: async (config: ConfigService) => { 13 | const store = await redisStore({ 14 | socket: { 15 | host: config.get('REDIS_HOST'), 16 | port: config.get('REDIS_PORT'), 17 | }, 18 | password: config.get('REDIS_PASSWORD'), 19 | ttl: 10, 20 | }); 21 | 22 | return { 23 | store: store as unknown as CacheStore, 24 | }; 25 | }, 26 | inject: [ConfigService], 27 | }), 28 | ], 29 | providers: [RedisCacheService], 30 | exports: [RedisCacheService, CacheModule], 31 | }) 32 | export class RedisCacheModule {} 33 | -------------------------------------------------------------------------------- /src/redis-cache/redis-cache.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RedisCacheService } from './redis-cache.service'; 3 | import { CACHE_MANAGER, CacheModule, CacheStore } from '@nestjs/cache-manager'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | import { redisStore } from 'cache-manager-redis-store'; 6 | import { Cache } from 'cache-manager'; 7 | 8 | describe('RedisCacheService', () => { 9 | let service: RedisCacheService; 10 | let cacheManager: Cache; 11 | let configService: ConfigService; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | providers: [RedisCacheService], 16 | imports: [ 17 | ConfigModule.forRoot({}), 18 | CacheModule.registerAsync({ 19 | imports: [ConfigModule], 20 | useFactory: async (config: ConfigService) => { 21 | const store = await redisStore({ 22 | socket: { 23 | host: config.get('REDIS_HOST'), 24 | port: config.get('REDIS_PORT'), 25 | }, 26 | password: config.get('REDIS_PASSWORD'), 27 | ttl: 600, 28 | }); 29 | console.log(config.get('REDIS_HOST')); 30 | return { 31 | store: store as unknown as CacheStore, 32 | }; 33 | }, 34 | inject: [ConfigService], 35 | }), 36 | ], 37 | }).compile(); 38 | 39 | cacheManager = module.get(CACHE_MANAGER); 40 | service = module.get(RedisCacheService); 41 | configService = module.get(ConfigService); 42 | }); 43 | 44 | it('should get from cache', async () => { 45 | console.log(cacheManager.store); 46 | await service.set('key', 'value', 6); 47 | expect(await cacheManager.get('key')).toEqual('value'); 48 | }); 49 | 50 | it('should get env', async () => { 51 | expect(configService.get('REDIS_HOST')).toEqual('localhost'); 52 | }); 53 | 54 | it('should be defined', () => { 55 | expect(service).toBeDefined(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/redis-cache/redis-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import { Cache } from 'cache-manager'; 3 | import { CACHE_MANAGER } from '@nestjs/cache-manager'; 4 | 5 | @Injectable() 6 | export class RedisCacheService { 7 | constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {} 8 | 9 | async get(key: string): Promise { 10 | return await this.cache.get(key); 11 | } 12 | 13 | async set(key: string, value: any, ttl?: number): Promise { 14 | return await this.cache.set(key, value, ttl); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/setting/setting.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SettingController } from './setting.controller'; 3 | 4 | describe('SettingController', () => { 5 | let controller: SettingController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [SettingController], 10 | }).compile(); 11 | 12 | controller = module.get(SettingController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/setting/setting.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | import { SettingService } from './setting.service'; 4 | 5 | @Controller('setting') 6 | export class SettingController { 7 | constructor(private readonly settingService: SettingService) {} 8 | 9 | @MessagePattern({ cmd: 'setSlippage' }) 10 | async setSlippage(data: { userId: number; slippage: number }) { 11 | await this.settingService.setSlippage(data.userId, data.slippage); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/setting/setting.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | OneToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { User } from '../user/user.entity'; 9 | import { Wallet } from '../wallet/wallet.entity'; 10 | 11 | @Entity() 12 | export class Setting { 13 | @PrimaryGeneratedColumn() 14 | id: number; 15 | 16 | @OneToOne(() => User, (user) => user.setting) 17 | @JoinColumn() 18 | user: User; 19 | 20 | @Column({ default: 30 }) 21 | slippage: number; // in percentage 22 | 23 | @Column({ default: 500000 }) 24 | jitoFee: number; 25 | 26 | @OneToOne(() => Wallet, (wallet) => wallet.setting) 27 | @JoinColumn() 28 | wallet: Wallet; 29 | } 30 | -------------------------------------------------------------------------------- /src/setting/setting.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SettingService } from './setting.service'; 3 | import { UserModule } from '../user/user.module'; 4 | import { SettingController } from './setting.controller'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { Setting } from './setting.entity'; 7 | import { User } from '../user/user.entity'; 8 | import { WalletModule } from '../wallet/wallet.module'; 9 | import { Wallet } from '../wallet/wallet.entity'; 10 | 11 | @Module({ 12 | imports: [ 13 | UserModule, 14 | WalletModule, 15 | TypeOrmModule.forFeature([Setting, User, Wallet]), 16 | ], 17 | providers: [SettingService], 18 | controllers: [SettingController], 19 | exports: [SettingService], 20 | }) 21 | export class SettingModule {} 22 | -------------------------------------------------------------------------------- /src/setting/setting.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SettingService } from './setting.service'; 3 | import { UserModule } from '../user/user.module'; 4 | import { WalletModule } from '../wallet/wallet.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { User } from '../user/user.entity'; 7 | import { Wallet } from '../wallet/wallet.entity'; 8 | import { Setting } from './setting.entity'; 9 | import { ConfigModule, ConfigService } from '@nestjs/config'; 10 | import { typeOrmConfig } from '../config/typeorm.config'; 11 | 12 | describe('SettingService', () => { 13 | let service: SettingService; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | providers: [SettingService, ConfigService], 18 | imports: [ 19 | UserModule, 20 | WalletModule, 21 | ConfigModule.forRoot({}), 22 | TypeOrmModule.forRootAsync({ 23 | imports: [ConfigModule], 24 | inject: [ConfigService], 25 | useFactory: typeOrmConfig, 26 | }), 27 | TypeOrmModule.forFeature([Setting, User, Wallet]), 28 | ], 29 | }).compile(); 30 | 31 | service = module.get(SettingService); 32 | }); 33 | 34 | it('should be defined', () => { 35 | expect(service).toBeDefined(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/setting/setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserService } from '../user/user.service'; 3 | import { Setting } from './setting.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { WalletService } from '../wallet/wallet.service'; 7 | import { Wallet } from '../wallet/wallet.entity'; 8 | 9 | @Injectable() 10 | export class SettingService { 11 | constructor( 12 | private readonly userService: UserService, 13 | private readonly walletService: WalletService, 14 | @InjectRepository(Setting) 15 | private readonly settingRepository: Repository, 16 | @InjectRepository(Wallet) 17 | private readonly walletRepository: Repository, 18 | ) {} 19 | 20 | async setWallet(userId: number, address: string) { 21 | const user = await this.userService.findUser(userId); // 查找用户 22 | const wallet = await this.walletRepository.findOne({ 23 | where: { address }, 24 | relations: { user: true }, 25 | }); 26 | 27 | if (wallet.user.id === user.id) { 28 | await this.settingRepository.upsert( 29 | { user, wallet }, 30 | { conflictPaths: ['user'], skipUpdateIfNoValuesChanged: true }, 31 | ); 32 | } 33 | } 34 | 35 | async getWallet(userId: number) { 36 | return await this.settingRepository 37 | .findOne({ 38 | where: { user: { id: userId } }, 39 | relations: { user: true, wallet: true }, 40 | }) 41 | .then((setting) => setting?.wallet); 42 | } 43 | 44 | async setJitoFee(userId: number, jitoFee: number) { 45 | const user = await this.userService.findUser(userId); // 查找用户 46 | if (user) { 47 | // 查找与该用户相关的设置 48 | const existingSetting = await this.settingRepository.findOneBy({ user }); 49 | 50 | // 使用 upsert 插入或更新设置 51 | await this.settingRepository.upsert( 52 | { 53 | user, // 关联的用户 54 | jitoFee, // 要设置的滑点值 55 | ...(existingSetting ? { id: existingSetting.id } : {}), // 如果有现有设置,保留其 id 以更新 56 | }, 57 | ['user'], // 使用 `user` 字段作为冲突字段 58 | ); 59 | } 60 | } 61 | 62 | async getJitoFee(userId: number) { 63 | return this.settingRepository 64 | .findOneBy({ user: { id: userId } }) 65 | .then((setting) => setting?.jitoFee); 66 | } 67 | 68 | async setSlippage(userId: number, slippage: number) { 69 | const user = await this.userService.findUser(userId); // 查找用户 70 | if (user) { 71 | // 查找与该用户相关的设置 72 | const existingSetting = await this.settingRepository.findOneBy({ user }); 73 | 74 | // 使用 upsert 插入或更新设置 75 | await this.settingRepository.upsert( 76 | { 77 | user, // 关联的用户 78 | slippage, // 要设置的滑点值 79 | ...(existingSetting ? { id: existingSetting.id } : {}), // 如果有现有设置,保留其 id 以更新 80 | }, 81 | ['user'], // 使用 `user` 字段作为冲突字段 82 | ); 83 | } 84 | } 85 | 86 | async getSlippage(userId: number) { 87 | return this.settingRepository 88 | .findOneBy({ user: { id: userId } }) 89 | .then((setting) => setting?.slippage); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/telegram/invite.update.ts: -------------------------------------------------------------------------------- 1 | import { Action, Ctx, Update } from 'nestjs-telegraf'; 2 | import { Context } from 'telegraf'; 3 | import { InviteScreen } from './screen/invite.screen'; 4 | 5 | @Update() 6 | export class InviteUpdate { 7 | constructor(private readonly inviteScreen: InviteScreen) {} 8 | 9 | @Action('invite') 10 | async invite(@Ctx() ctx: Context) { 11 | await this.inviteScreen.getInviteScreen(ctx); 12 | } 13 | 14 | @Action('invite_refresh') 15 | async refresh(@Ctx() ctx: Context) { 16 | await this.inviteScreen.refreshInviteScreen(ctx); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/telegram/screen/contract.screen.ts: -------------------------------------------------------------------------------- 1 | import { InlineKeyboardButton } from 'telegraf/typings/core/types/typegram'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { UserService } from '../../user/user.service'; 4 | import { WalletService } from '../../wallet/wallet.service'; 5 | import { SettingService } from '../../setting/setting.service'; 6 | import { SolanaService } from '../../trade/solana.service'; 7 | import { TradeService } from '../../trade/trade.service'; 8 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 9 | 10 | @Injectable() 11 | export class ContractScreen { 12 | constructor( 13 | private readonly userService: UserService, 14 | private readonly walletService: WalletService, 15 | private readonly settingService: SettingService, 16 | private readonly solanaService: SolanaService, 17 | private readonly tradeService: TradeService, 18 | ) {} 19 | 20 | async getContractScreen(data: any, userId: number) { 21 | const caption = await this.buildCaption( 22 | data.name, 23 | data.symbol, 24 | data.mint, 25 | data.price, 26 | data.supply, 27 | userId, 28 | ); 29 | 30 | return { 31 | caption, 32 | inline_keyboards: await this.inline_keyboards(userId), 33 | }; 34 | } 35 | 36 | async buildCaption( 37 | name: string, 38 | symbol: string, 39 | mint: string, 40 | price: number, 41 | supply: number, 42 | userId: number, 43 | ) { 44 | const mc = price * supply; 45 | const currentWallet = await this.settingService.getWallet(userId); 46 | const walletBalance = await this.walletService.getBalance( 47 | currentWallet.address, 48 | ); 49 | const currentTokenBalance = await this.solanaService.getTokenFmtBalance( 50 | currentWallet.address, 51 | mint, 52 | ); 53 | const profit = await this.tradeService.calculateProfit(userId, mint); 54 | let caption = ''; 55 | caption += 56 | `🌳 代币: ${name ?? 'undefined'} (${symbol ?? 'undefined'}) ` + 57 | `${mint}\n\n`; 58 | 59 | caption += 60 | `💲 价格(USD): ${price}\n` + `📊 市值(USD): ${mc}\n\n`; 61 | 62 | caption += `💳 钱包: ${currentWallet.address}\n`; 63 | caption += `💳 SOL 余额: ${walletBalance} SOL\n`; 64 | caption += `💳 ${name} 余额: ${currentTokenBalance} ${symbol}\n\n`; 65 | caption += `💲 持仓价值: $${(currentTokenBalance * price).toFixed(2)}`; 66 | caption += `💲 当前盈利: ${profit / LAMPORTS_PER_SOL} SOL`; 67 | return caption; 68 | } 69 | 70 | async inline_keyboards(userId: number): Promise { 71 | const wallets = await this.walletService.getWallets(userId); 72 | return [ 73 | [{ text: '🖼 生成收益图', callback_data: 'pnl_card' }], 74 | wallets 75 | ? [ 76 | ...wallets.map((wallet) => { 77 | return { 78 | text: wallet.address, 79 | callback_data: 'change_wallet_' + wallet.address, 80 | }; 81 | }), 82 | ] 83 | : [], 84 | [{ text: '🟢 买', callback_data: 'nothing' }], 85 | [ 86 | { text: '买 0.1 SOL', callback_data: 'buy_0.1' }, 87 | { text: '买 0.5 SOL', callback_data: 'buy_0.5' }, 88 | ], 89 | [ 90 | { text: '买 1 SOL', callback_data: 'buy_1' }, 91 | { text: '买 5 SOL', callback_data: 'buy_5' }, 92 | ], 93 | [{ text: '买 X SOL', callback_data: 'buy_custom' }], 94 | [{ text: '🔴 卖', callback_data: 'nothing' }], 95 | [ 96 | { text: '卖 10%', callback_data: 'sell_10' }, 97 | { text: '卖 20%', callback_data: 'sell_20' }, 98 | ], 99 | [ 100 | { text: '卖一半', callback_data: 'sell_50' }, 101 | { text: '全卖', callback_data: 'sell_100' }, 102 | ], 103 | [{ text: '卖 X %', callback_data: 'sell_custom' }], 104 | [ 105 | { text: '🔄 刷新', callback_data: 'contract_refresh' }, 106 | { text: '❌ 关闭', callback_data: 'delete_message' }, 107 | ], 108 | ]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/telegram/screen/invite.screen.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Context, Markup } from 'telegraf'; 3 | import { UserService } from '../../user/user.service'; 4 | import { SettingService } from '../../setting/setting.service'; 5 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 6 | 7 | @Injectable() 8 | export class InviteScreen { 9 | constructor( 10 | private readonly userService: UserService, 11 | private readonly settingService: SettingService, 12 | ) {} 13 | 14 | async buildInviteScreen(ctx: Context) { 15 | const userId = ctx.from.id; 16 | const childrenCount = await this.userService.getChildrenCount(userId); 17 | const currentWallet = await this.settingService.getWallet(userId); 18 | const referralCode = 19 | await this.userService.getOrGenerateReferralCode(userId); 20 | const totalReferralBalance = 21 | (await this.userService.getReferralBalance(userId)) / LAMPORTS_PER_SOL; 22 | const withdrawableBalance = 23 | (await this.userService.getWithdrawBalance(userId)) / LAMPORTS_PER_SOL; 24 | const caption = 25 | `🔗 邀请链接: https://t.me/khetzoo_bot?start=${referralCode}\n` + 26 | `👥 累计邀请:${childrenCount}人\n` + 27 | `💳 收款地址: ${currentWallet.address}\n` + 28 | `💵 总邀请奖励: ${totalReferralBalance} SOL\n` + 29 | `💵 当前已提现: ${withdrawableBalance} SOL`; 30 | 31 | return { 32 | caption, 33 | inline_keyboards: [ 34 | [ 35 | { text: '🔄 刷新 🔄', callback_data: 'invite_refresh' }, 36 | { text: '❌ 关闭 ❌', callback_data: 'delete_message' }, 37 | ], 38 | ], 39 | }; 40 | } 41 | 42 | async getInviteScreen(ctx: Context) { 43 | const screen = await this.buildInviteScreen(ctx); 44 | return ctx.replyWithHTML( 45 | screen.caption, 46 | Markup.inlineKeyboard(screen.inline_keyboards), 47 | ); 48 | } 49 | 50 | async refreshInviteScreen(ctx: Context) { 51 | const screen = await this.buildInviteScreen(ctx); 52 | try { 53 | return ctx.editMessageText(screen.caption, { 54 | parse_mode: 'HTML', 55 | reply_markup: { 56 | inline_keyboard: screen.inline_keyboards, 57 | }, 58 | }); 59 | } catch (e) { 60 | console.log(e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/telegram/screen/screen.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { ScreenService } from './screen.service'; 3 | import { ContractScreen } from './contract.screen'; 4 | import { UserModule } from '../../user/user.module'; 5 | import { WalletModule } from '../../wallet/wallet.module'; 6 | import { StartScreen } from './start.screen'; 7 | import { SettingModule } from '../../setting/setting.module'; 8 | import { WalletScreen } from './wallet.screen'; 9 | import { InviteScreen } from './invite.screen'; 10 | import { SettingScreen } from './setting.screen'; 11 | import { TradeModule } from '../../trade/trade.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | UserModule, 16 | WalletModule, 17 | SettingModule, 18 | forwardRef(() => TradeModule), 19 | ], 20 | providers: [ 21 | ScreenService, 22 | ContractScreen, 23 | StartScreen, 24 | WalletScreen, 25 | InviteScreen, 26 | SettingScreen, 27 | SettingModule, 28 | ], 29 | exports: [ScreenService, WalletScreen, InviteScreen, SettingScreen], 30 | }) 31 | export class ScreenModule {} 32 | -------------------------------------------------------------------------------- /src/telegram/screen/screen.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ScreenService } from './screen.service'; 3 | 4 | describe('ScreenService', () => { 5 | let service: ScreenService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ScreenService], 10 | }).compile(); 11 | 12 | service = module.get(ScreenService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/telegram/screen/screen.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ContractScreen } from './contract.screen'; 3 | import { StartScreen } from './start.screen'; 4 | 5 | @Injectable() 6 | export class ScreenService { 7 | constructor( 8 | private readonly contractScreen: ContractScreen, 9 | private readonly startScreen: StartScreen, 10 | ) {} 11 | 12 | getContractScreen(data: any, userId: number) { 13 | return this.contractScreen.getContractScreen(data, userId); 14 | } 15 | 16 | getStartScreen(userId: number) { 17 | return this.startScreen.getStartScreen(userId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/telegram/screen/setting.screen.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class SettingScreen { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /src/telegram/screen/start.screen.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { WalletService } from '../../wallet/wallet.service'; 3 | import { UserService } from '../../user/user.service'; 4 | import { SettingService } from '../../setting/setting.service'; 5 | 6 | @Injectable() 7 | export class StartScreen { 8 | constructor( 9 | private readonly walletService: WalletService, 10 | private readonly userService: UserService, 11 | private readonly settingService: SettingService, 12 | ) {} 13 | 14 | async getStartScreen(userId: number) { 15 | const user = await this.userService.findUser(userId); 16 | const wallet = await this.settingService.getWallet(userId); 17 | const caption = 18 | `👋 欢迎使用 kheowzoo 交易 bot 👋\n` + 19 | `👤 用户: ${user.first_name}\n` + 20 | `💳 钱包: ${wallet.address}` + 21 | `💳 余额: ${await this.walletService.getBalance(wallet.address)} SOL\n` + 22 | `👤 邀请链接: https://t.me/khetzoo_bot?start=${user.referral_code}\n` + 23 | `现在邀请你的朋友加入可获取25%交易手续费返佣` + 24 | `发送合约即可开始交易`; 25 | return { 26 | caption, 27 | inline_keyboards: [ 28 | [{ text: '📝 钱包 📝', callback_data: 'wallet' }], 29 | [{ text: '⚙️ 设置 ⚙️', callback_data: 'setting' }], 30 | [{ text: '👥 邀请 👥', callback_data: 'invite' }], 31 | [{ text: '❌ 关闭 ❌', callback_data: 'delete_message' }], 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/telegram/screen/wallet.screen.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { WalletService } from '../../wallet/wallet.service'; 3 | import { Context, Markup } from 'telegraf'; 4 | import { SettingService } from '../../setting/setting.service'; 5 | 6 | @Injectable() 7 | export class WalletScreen { 8 | constructor( 9 | private readonly walletService: WalletService, 10 | private readonly settingService: SettingService, 11 | ) {} 12 | 13 | async buildWalletScreen(userId: number) { 14 | const currentWallet = await this.settingService.getWallet(userId); 15 | let caption = '当前机器人最多支持保存5个钱包'; 16 | caption += '请先生成钱包,再向钱包充值SOL后使用\n\n'; 17 | caption += '为保证钱包安全,当前只支持使用平台生成的钱包\n\n'; 18 | const wallets = await this.walletService.getWallets(userId); 19 | if (wallets && wallets.length > 0) { 20 | caption += '\n\n'; 21 | for (const wallet of wallets) { 22 | caption += `地址: ${wallet.address} ${wallet.address === currentWallet.address ? '(当前钱包)' : ''}\n`; 23 | caption += `余额: ${await this.walletService.getBalance(wallet.address)}\n\n`; 24 | } 25 | } 26 | return { 27 | caption, 28 | inlineKeyboards: [ 29 | [{ text: '📝 添加钱包 📝', callback_data: 'generate_wallet' }], 30 | [{ text: '⚙️ 导出私钥 ⚙️', callback_data: 'export_private_key' }], 31 | [{ text: '⚙️ 解除绑定 ⚙️', callback_data: 'remove_wallet' }], 32 | [{ text: '📝 切换钱包 📝', callback_data: 'switch_wallet' }], 33 | [{ text: '❌ 关闭 ❌', callback_data: 'delete_message' }], 34 | ], 35 | }; 36 | } 37 | 38 | async getWalletScreen(ctx: Context) { 39 | const walletScreen = await this.buildWalletScreen(ctx.from.id); 40 | return await ctx.replyWithHTML( 41 | walletScreen.caption, 42 | Markup.inlineKeyboard(walletScreen.inlineKeyboards), 43 | ); 44 | } 45 | 46 | async generateWallet(ctx: Context, userId: number) { 47 | const wallets = await this.walletService.getWallets(userId); 48 | if (wallets && wallets.length >= 5) { 49 | return await ctx.replyWithHTML( 50 | '当前机器人最多支持保存5个钱包\n\n请先解除绑定后再生成新的钱包', 51 | ); 52 | } 53 | const wallet = await this.walletService.generateWallet(userId); 54 | const walletScreen = await this.buildWalletScreen(userId); 55 | await ctx.editMessageText(walletScreen.caption, { 56 | parse_mode: 'HTML', 57 | reply_markup: { inline_keyboard: walletScreen.inlineKeyboards }, 58 | }); 59 | return await ctx.replyWithHTML( 60 | `生成钱包成功\n\n地址:${wallet.address}\n\n请向钱包充值SOL后使用\n 私钥如下:\n${wallet.private_key}`, 61 | Markup.inlineKeyboard([ 62 | [{ text: '❌ 关闭 ❌', callback_data: 'delete_message' }], 63 | ]), 64 | ); 65 | } 66 | 67 | async removeWalletScreen(ctx: Context, userId: number) { 68 | const wallets = await this.walletService.getWallets(userId); 69 | await ctx.replyWithHTML( 70 | `解除绑定将永久删除该钱包\n\n请在解除绑定前导出私钥,以免造成损失\n\n请选择要解除绑定的钱包`, 71 | Markup.inlineKeyboard( 72 | wallets.map((wallet) => [ 73 | { 74 | text: `${wallet.address}`, 75 | callback_data: `remove_wallet_${wallet.address}`, 76 | }, 77 | ]), 78 | ), 79 | ); 80 | } 81 | 82 | async removeWallet(ctx: Context, userId: number, address: string) { 83 | const currentWallet = await this.settingService.getWallet(userId); 84 | if (currentWallet.address == address) { 85 | return await ctx.replyWithHTML( 86 | '无法解绑当前使用中的钱包,请先切换钱包后重试', 87 | ); 88 | } 89 | const privateKey = await this.walletService.getPrivateKey(address); 90 | await this.walletService.removeWallet(userId, address); 91 | await ctx.deleteMessage(); 92 | return await ctx.replyWithHTML( 93 | `钱包解绑成功\n\n私钥如下\n\n${privateKey}`, 94 | Markup.inlineKeyboard([ 95 | [{ text: '❌ 关闭 ❌', callback_data: 'delete_message' }], 96 | ]), 97 | ); 98 | } 99 | 100 | async switchWalletScreen(ctx: Context, userId: number) { 101 | const wallets = await this.walletService.getWallets(userId); 102 | const currentWallet = await this.settingService.getWallet(userId); // current wallet 103 | return await ctx.replyWithHTML( 104 | `请选择要切换的钱包\n当前钱包:${currentWallet.address}`, 105 | Markup.inlineKeyboard( 106 | wallets.map((wallet) => [ 107 | { 108 | text: `${wallet.address}`, 109 | callback_data: `switch_wallet_${wallet.address}`, 110 | }, 111 | ]), 112 | ), 113 | ); 114 | } 115 | 116 | async switchWallet(ctx: Context) { 117 | const currentWallet = await this.settingService.getWallet(ctx.from.id); 118 | if ('match' in ctx) { 119 | if (currentWallet.address == ctx.match[1]) { 120 | return await ctx.replyWithHTML('当前钱包已经是' + ctx.match[1]); 121 | } 122 | try { 123 | await this.settingService.setWallet(ctx.from.id, ctx.match[1]); 124 | await ctx.deleteMessage(); 125 | await ctx.replyWithHTML( 126 | '切换成功\n' + `当前钱包:${ctx.match[1]}`, 127 | ); 128 | } catch (error) { 129 | console.log(error); 130 | } 131 | } 132 | } 133 | 134 | async exportScreen(ctx: Context, userId: number) { 135 | const caption = `请选择要导出的钱包`; 136 | const wallets = await this.walletService.getWallets(userId); 137 | return await ctx.replyWithHTML( 138 | caption, 139 | Markup.inlineKeyboard( 140 | wallets.map((wallet) => [ 141 | { 142 | text: `${wallet.address}`, 143 | callback_data: `export_wallet_${wallet.address}`, 144 | }, 145 | ]), 146 | ), 147 | ); 148 | } 149 | 150 | async exportWallet(ctx: Context) { 151 | if ('match' in ctx) { 152 | const privateKey = await this.walletService.getPrivateKey(ctx.match[1]); 153 | await ctx.deleteMessage(); 154 | await ctx.replyWithHTML( 155 | `${ctx.match[1]}的私钥如下\n\n${privateKey}`, 156 | Markup.inlineKeyboard([ 157 | [{ text: '❌ 关闭 ❌', callback_data: 'delete_message' }], 158 | ]), 159 | ); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/telegram/setting.update.ts: -------------------------------------------------------------------------------- 1 | import { Update } from 'nestjs-telegraf'; 2 | 3 | @Update() 4 | export class SettingUpdate {} 5 | -------------------------------------------------------------------------------- /src/telegram/telegram.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TelegramUpdate } from './telegram.update'; 3 | import { TelegramService } from './telegram.service'; 4 | import { UserModule } from '../user/user.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { User } from '../user/user.entity'; 7 | import { Message } from '../message/message.entity'; 8 | import { TokenInfoModule } from '../token-info/token-info.module'; 9 | import { TokenInfoService } from '../token-info/token-info.service'; 10 | import { TokenInfo } from '../token-info/token-info.entity'; 11 | import { PumpFunService } from '../token-info/pump-fun/pump-fun.service'; 12 | import { RedisCacheService } from '../redis-cache/redis-cache.service'; 13 | import { RedisCacheModule } from '../redis-cache/redis-cache.module'; 14 | import { MessageModule } from '../message/message.module'; 15 | import { MessageService } from '../message/message.service'; 16 | import { ScreenModule } from './screen/screen.module'; 17 | import { WalletModule } from '../wallet/wallet.module'; 18 | import { SettingModule } from '../setting/setting.module'; 19 | import { WalletUpdate } from './wallet.update'; 20 | import { InviteUpdate } from './invite.update'; 21 | import { SettingUpdate } from './setting.update'; 22 | import { TradeModule } from '../trade/trade.module'; 23 | 24 | @Module({ 25 | imports: [ 26 | UserModule, 27 | TypeOrmModule.forFeature([User, Message, TokenInfo]), 28 | TokenInfoModule, 29 | RedisCacheModule, 30 | MessageModule, 31 | ScreenModule, 32 | WalletModule, 33 | SettingModule, 34 | forwardRef(() => TradeModule), 35 | ], 36 | providers: [ 37 | TelegramUpdate, 38 | WalletUpdate, 39 | InviteUpdate, 40 | SettingUpdate, 41 | TelegramService, 42 | TokenInfoService, 43 | PumpFunService, 44 | RedisCacheService, 45 | MessageService, 46 | ], 47 | exports: [TelegramService], 48 | }) 49 | export class TelegramModule {} 50 | -------------------------------------------------------------------------------- /src/telegram/telegram.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TelegramService } from './telegram.service'; 3 | 4 | describe('TelegramService', () => { 5 | let service: TelegramService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [TelegramService], 10 | }).compile(); 11 | 12 | service = module.get(TelegramService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/telegram/telegram.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserService } from '../user/user.service'; 3 | import { Context, Markup, Telegraf } from 'telegraf'; 4 | import { TokenInfoService } from '../token-info/token-info.service'; 5 | import { MessageService } from '../message/message.service'; 6 | import { ScreenService } from './screen/screen.service'; 7 | import { WalletService } from '../wallet/wallet.service'; 8 | import { SettingService } from '../setting/setting.service'; 9 | import { TradeService } from '../trade/trade.service'; 10 | import { InjectBot } from 'nestjs-telegraf'; 11 | 12 | @Injectable() 13 | export class TelegramService { 14 | constructor( 15 | private readonly userService: UserService, 16 | private readonly tokenInfoService: TokenInfoService, 17 | private readonly messageService: MessageService, 18 | private readonly screenService: ScreenService, 19 | private readonly walletService: WalletService, 20 | private readonly settingService: SettingService, 21 | private readonly tradeService: TradeService, 22 | @InjectBot() private bot: Telegraf, 23 | ) {} 24 | 25 | private getStartPayload(messageText: string) { 26 | const args = messageText.split(' '); // 分割文本,以空格分隔命令和参数 27 | 28 | if (args.length > 1) { 29 | // 获取第二部分作为参数 30 | return args[1]; 31 | } else { 32 | return null; 33 | } 34 | } 35 | 36 | async sendMessage(userId: number, text: string) { 37 | await this.bot.telegram.sendMessage(userId, text, { parse_mode: 'HTML' }); 38 | } 39 | 40 | async start(ctx: Context) { 41 | const userId = ctx.from.id; 42 | // 若没有用户存在,则创建用户,并生成钱包 43 | if (!(await this.userService.findUser(userId))) { 44 | const referer_code = this.getStartPayload(ctx.text) || null; 45 | const firstName = ctx.from.first_name; 46 | const lastName = ctx.from.last_name || null; 47 | if (referer_code) { 48 | await this.userService.createUser( 49 | userId, 50 | firstName, 51 | lastName, 52 | referer_code, 53 | ); 54 | } else { 55 | await this.userService.createUser(userId, firstName, lastName); 56 | } 57 | 58 | // 生成并保存默认钱包 59 | const wallet = await this.walletService.generateWallet(userId); 60 | await this.settingService.setWallet(userId, wallet.address); 61 | } 62 | await this.userService.getOrGenerateReferralCode(userId); 63 | // 回复默认屏 64 | const startScreen = await this.screenService.getStartScreen(ctx.from.id); 65 | await ctx.replyWithHTML( 66 | startScreen.caption, 67 | Markup.inlineKeyboard(startScreen.inline_keyboards), 68 | ); 69 | } 70 | 71 | async getTokenInfo(ctx: Context) { 72 | // 获取token 73 | const data = await this.tokenInfoService.getFullTokenInfo(ctx.text); 74 | const screen = await this.screenService.getContractScreen( 75 | data, 76 | ctx.from.id, 77 | ); 78 | const msg = await ctx.replyWithHTML( 79 | screen.caption, 80 | Markup.inlineKeyboard(screen.inline_keyboards), 81 | ); 82 | await this.messageService.create(ctx.from.id, msg.message_id, ctx.text); 83 | } 84 | 85 | async contractRefresh(ctx: Context) { 86 | const msg = await this.messageService.getData(ctx.from.id, ctx.msgId); 87 | const data = await this.tokenInfoService.getFullTokenInfo(msg.mint); 88 | const screen = await this.screenService.getContractScreen( 89 | data, 90 | ctx.from.id, 91 | ); 92 | try { 93 | await ctx.editMessageText(screen.caption, { 94 | parse_mode: 'HTML', 95 | reply_markup: { inline_keyboard: screen.inline_keyboards }, 96 | }); 97 | } catch (error) { 98 | console.log(error); 99 | } 100 | } 101 | 102 | private async baseBuy(ctx, userId: number, mint: string, amount: number) { 103 | await ctx.reply(`购买${amount}SOL的代币${mint}`); 104 | await this.tradeService.trade(userId, amount, mint, true); 105 | } 106 | 107 | async buy(ctx: Context, amount: number) { 108 | const msg = await this.messageService.getData(ctx.from.id, ctx.msgId); 109 | await this.baseBuy(ctx, ctx.from.id, msg.mint, amount); 110 | } 111 | 112 | async buyCustom(ctx: Context) { 113 | const msg = await ctx.reply( 114 | '请输入你要购买的数量(SOL)', 115 | Markup.forceReply(), 116 | ); 117 | const currentMsg = await this.messageService.getData( 118 | ctx.from.id, 119 | ctx.msgId, 120 | ); 121 | await this.messageService.create( 122 | ctx.from.id, 123 | msg.message_id, 124 | currentMsg.mint, 125 | true, 126 | ); 127 | } 128 | 129 | async sell(ctx: Context, amount: any) { 130 | const msg = await this.messageService.getData(ctx.from.id, ctx.msgId); 131 | await this.baseSell(ctx, ctx.from.id, msg.mint, amount); 132 | } 133 | 134 | private async baseSell(ctx: Context, id: number, mint: string, amount: any) { 135 | await ctx.reply(`卖出${amount}%的${mint}`); 136 | await this.tradeService.trade(id, amount, mint, false); 137 | } 138 | 139 | async sellCustom(ctx: Context) { 140 | const msg = await ctx.reply('请输入你要卖出的比例(%)', Markup.forceReply()); 141 | const currentMsg = await this.messageService.getData( 142 | ctx.from.id, 143 | ctx.msgId, 144 | ); 145 | await this.messageService.create( 146 | ctx.from.id, 147 | msg.message_id, 148 | currentMsg.mint, 149 | false, 150 | ); 151 | } 152 | 153 | async handleAmount(ctx: Context) { 154 | if ('reply_to_message' in ctx.message) { 155 | const replyToMessage = ctx.message.reply_to_message; 156 | const currentMsg = await this.messageService.getData( 157 | ctx.from.id, 158 | replyToMessage.message_id, 159 | ); 160 | if (!currentMsg.isBuy === undefined) { 161 | console.log(undefined); 162 | return; 163 | } else if (currentMsg.isBuy) { 164 | await this.baseBuy(ctx, ctx.from.id, currentMsg.mint, Number(ctx.text)); 165 | } else { 166 | if (Number(ctx.text) > 100) { 167 | return await ctx.reply('比例不能超过100'); 168 | } 169 | await this.baseSell( 170 | ctx, 171 | ctx.from.id, 172 | currentMsg.mint, 173 | Number(ctx.text), 174 | ); 175 | } 176 | } 177 | } 178 | 179 | async changeWallet(ctx: Context) { 180 | if ('match' in ctx) { 181 | await this.settingService.setWallet(ctx.from.id, ctx.match[1]); 182 | await this.contractRefresh(ctx); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/telegram/telegram.update.ts: -------------------------------------------------------------------------------- 1 | import { Action, Ctx, Hears, Start, Update } from 'nestjs-telegraf'; 2 | import { Context } from 'telegraf'; 3 | import { TelegramService } from './telegram.service'; 4 | import { WalletScreen } from './screen/wallet.screen'; 5 | 6 | @Update() 7 | export class TelegramUpdate { 8 | constructor( 9 | private readonly telegramService: TelegramService, 10 | private readonly walletScreen: WalletScreen, 11 | ) {} 12 | 13 | @Start() 14 | async start(@Ctx() ctx: Context) { 15 | await this.telegramService.start(ctx); 16 | } 17 | 18 | @Hears(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/) 19 | async tokenInfo(@Ctx() ctx: Context) { 20 | console.log(ctx.msgId); 21 | await this.telegramService.getTokenInfo(ctx); 22 | } 23 | 24 | @Action('delete_message') 25 | async deleteMessage(@Ctx() ctx: Context) { 26 | await ctx.deleteMessage(); // 删除消息 27 | } 28 | 29 | @Action('contract_refresh') 30 | async contractRefresh(@Ctx() ctx: Context) { 31 | await this.telegramService.contractRefresh(ctx); 32 | } 33 | 34 | @Action(/^buy_(\d+(?:\.\d+)?)$/) 35 | async buy(@Ctx() ctx: Context) { 36 | // @ts-expect-error should be number 37 | const amount = ctx.match[1]; 38 | await this.telegramService.buy(ctx, amount); 39 | } 40 | 41 | @Action('buy_custom') 42 | async buyCustom(@Ctx() ctx: Context) { 43 | await this.telegramService.buyCustom(ctx); 44 | } 45 | 46 | @Hears(/^(\d+(?:\.\d+)?)$/) 47 | async handleAmount(@Ctx() ctx: Context) { 48 | await this.telegramService.handleAmount(ctx); 49 | } 50 | 51 | // Sell 52 | @Action(/^sell_(\d+(?:\.\d+)?)$/) 53 | async sell(@Ctx() ctx: Context) { 54 | // @ts-expect-error should be number 55 | const amount = ctx.match[1]; 56 | await this.telegramService.sell(ctx, amount); 57 | } 58 | 59 | @Action('sell_custom') 60 | async sellCustom(@Ctx() ctx: Context) { 61 | await this.telegramService.sellCustom(ctx); 62 | } 63 | 64 | @Action(/^change_wallet_([1-9A-HJ-NP-Za-km-z]{32,44})$/) 65 | async changeWallet(@Ctx() ctx: Context) { 66 | await this.telegramService.changeWallet(ctx); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/telegram/wallet.update.ts: -------------------------------------------------------------------------------- 1 | import { Action, Ctx, Update } from 'nestjs-telegraf'; 2 | import { Context } from 'telegraf'; 3 | import { WalletScreen } from './screen/wallet.screen'; 4 | 5 | @Update() 6 | export class WalletUpdate { 7 | constructor(private readonly walletScreen: WalletScreen) {} 8 | 9 | @Action('wallet') 10 | async wallet(@Ctx() ctx: Context) { 11 | await this.walletScreen.getWalletScreen(ctx); 12 | } 13 | 14 | @Action('generate_wallet') 15 | async generateWallet(@Ctx() ctx: Context) { 16 | await this.walletScreen.generateWallet(ctx, ctx.from.id); 17 | } 18 | 19 | @Action('remove_wallet') 20 | async removeWalletScreen(@Ctx() ctx: Context) { 21 | await this.walletScreen.removeWalletScreen(ctx, ctx.from.id); 22 | } 23 | 24 | @Action(/^remove_wallet_([1-9A-HJ-NP-Za-km-z]{32,44})$/) 25 | async removeWallet(@Ctx() ctx: Context) { 26 | if ('match' in ctx) { 27 | await this.walletScreen.removeWallet(ctx, ctx.from.id, ctx.match[1]); 28 | } 29 | } 30 | 31 | @Action('switch_wallet') 32 | async switchWalletScreen(@Ctx() ctx: Context) { 33 | await this.walletScreen.switchWalletScreen(ctx, ctx.from.id); 34 | } 35 | 36 | @Action(/^switch_wallet_([1-9A-HJ-NP-Za-km-z]{32,44})$/) 37 | async switchWallet(@Ctx() ctx: Context) { 38 | if ('match' in ctx) { 39 | await this.walletScreen.switchWallet(ctx); 40 | } 41 | } 42 | 43 | @Action('export_private_key') 44 | async exportScreen(@Ctx() ctx: Context) { 45 | await this.walletScreen.exportScreen(ctx, ctx.from.id); 46 | } 47 | 48 | @Action(/^export_wallet_([1-9A-HJ-NP-Za-km-z]{32,44})$/) 49 | async exportPrivateKey(@Ctx() ctx: Context) { 50 | if ('match' in ctx) { 51 | await this.walletScreen.exportWallet(ctx); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/data-item.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsObject } from 'class-validator'; 2 | import { ExtraInfoDto } from './extra-info.dto'; 3 | 4 | export class DataItemDto { 5 | @IsString() 6 | id: string; 7 | 8 | @IsString() 9 | type: string; 10 | 11 | @IsString() 12 | price: string; 13 | 14 | @IsObject() 15 | extraInfo: ExtraInfoDto; 16 | } 17 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/depth.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsOptional } from 'class-validator'; 2 | 3 | export class DepthDto { 4 | @IsOptional() 5 | @IsNumber() 6 | '10'?: number; 7 | 8 | @IsOptional() 9 | @IsNumber() 10 | '100'?: number; 11 | 12 | @IsOptional() 13 | @IsNumber() 14 | '1000'?: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/extra-info.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsObject, IsString } from 'class-validator'; 2 | import { LastSwappedPriceDto } from './last-swapped-price.dto'; 3 | import { QuotedPriceDto } from './quoted-price.dto'; 4 | import { BuyPriceImpactRatioDto, SellPriceImpactRatioDto } from './price-impact-ratio.dto'; 5 | 6 | export class ExtraInfoDto { 7 | @IsObject() 8 | lastSwappedPrice: LastSwappedPriceDto; 9 | 10 | @IsObject() 11 | quotedPrice: QuotedPriceDto; 12 | 13 | @IsString() 14 | confidenceLevel: string; 15 | 16 | @IsObject() 17 | depth: { 18 | buyPriceImpactRatio: BuyPriceImpactRatioDto; 19 | sellPriceImpactRatio: SellPriceImpactRatioDto; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/last-swapped-price.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsString } from 'class-validator'; 2 | 3 | export class LastSwappedPriceDto { 4 | @IsNumber() 5 | lastJupiterSellAt: number; 6 | 7 | @IsString() 8 | lastJupiterSellPrice: string; 9 | 10 | @IsNumber() 11 | lastJupiterBuyAt: number; 12 | 13 | @IsString() 14 | lastJupiterBuyPrice: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/price-impact-ratio.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsObject, IsNumber, IsOptional } from 'class-validator'; 2 | import { DepthDto } from './depth.dto'; 3 | 4 | export class BuyPriceImpactRatioDto { 5 | @IsObject() 6 | depth: DepthDto; 7 | 8 | @IsNumber() 9 | timestamp: number; 10 | } 11 | 12 | export class SellPriceImpactRatioDto { 13 | @IsObject() 14 | depth: DepthDto; 15 | 16 | @IsOptional() 17 | @IsNumber() 18 | timestamp?: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/quoted-price.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class QuotedPriceDto { 4 | @IsString() 5 | buyPrice: string; 6 | 7 | @IsNumber() 8 | buyAt: number; 9 | 10 | @IsOptional() 11 | @IsString() 12 | sellPrice?: string; 13 | 14 | @IsOptional() 15 | @IsNumber() 16 | sellAt?: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/token-info/jupiter-price-response-dto/response.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsObject } from 'class-validator'; 2 | import { DataItemDto } from './data-item.dto'; 3 | 4 | export class ResponseDto { 5 | @IsObject() 6 | data: { 7 | [key: string]: DataItemDto; 8 | }; 9 | 10 | @IsNumber() 11 | timeTaken: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/token-info/pump-fun/pump-fun-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsBoolean, 3 | IsNumber, 4 | IsOptional, 5 | IsString, 6 | IsUrl, 7 | } from 'class-validator'; 8 | import { Type } from 'class-transformer'; 9 | 10 | export class PumpFunResponseDto { 11 | @IsString() 12 | mint: string; 13 | 14 | @IsString() 15 | name: string; 16 | 17 | @IsString() 18 | symbol: string; 19 | 20 | @IsString() 21 | description: string; 22 | 23 | @IsUrl() 24 | image_uri: string; 25 | 26 | @IsUrl() 27 | metadata_uri: string; 28 | 29 | @IsUrl() 30 | twitter: string; 31 | 32 | @IsUrl() 33 | telegram: string; 34 | 35 | @IsString() 36 | bonding_curve: string; 37 | 38 | @IsString() 39 | associated_bonding_curve: string; 40 | 41 | @IsString() 42 | creator: string; 43 | 44 | @Type(() => Number) 45 | @IsNumber() 46 | created_timestamp: number; 47 | 48 | @IsString() 49 | raydium_pool: string; 50 | 51 | @IsBoolean() 52 | complete: boolean; 53 | 54 | @Type(() => Number) 55 | @IsNumber() 56 | virtual_sol_reserves: number; 57 | 58 | @Type(() => Number) 59 | @IsNumber() 60 | virtual_token_reserves: number; 61 | 62 | @Type(() => Number) 63 | @IsNumber() 64 | total_supply: number; 65 | 66 | @IsUrl() 67 | website: string; 68 | 69 | @IsBoolean() 70 | show_name: boolean; 71 | 72 | @Type(() => Number) 73 | @IsNumber() 74 | king_of_the_hill_timestamp: number; 75 | 76 | @Type(() => Number) 77 | @IsNumber() 78 | market_cap: number; 79 | 80 | @Type(() => Number) 81 | @IsNumber() 82 | reply_count: number; 83 | 84 | @Type(() => Number) 85 | @IsNumber() 86 | last_reply: number; 87 | 88 | @IsBoolean() 89 | nsfw: boolean; 90 | 91 | @IsString() 92 | market_id: string; 93 | 94 | @IsBoolean() 95 | inverted: boolean; 96 | 97 | @IsBoolean() 98 | is_currently_live: boolean; 99 | 100 | @IsString() 101 | username: string; 102 | 103 | @IsOptional() 104 | @IsString() 105 | profile_image: string | null; 106 | 107 | @Type(() => Number) 108 | @IsNumber() 109 | usd_market_cap: number; 110 | } 111 | -------------------------------------------------------------------------------- /src/token-info/pump-fun/pump-fun.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PumpFunService } from './pump-fun.service'; 3 | import { RedisCacheModule } from '../../redis-cache/redis-cache.module'; 4 | 5 | @Module({ 6 | providers: [PumpFunService], 7 | imports: [RedisCacheModule], 8 | }) 9 | export class PumpFunModule {} 10 | -------------------------------------------------------------------------------- /src/token-info/pump-fun/pump-fun.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PumpFunService } from './pump-fun.service'; 3 | import { RedisCacheService } from '../../redis-cache/redis-cache.service'; 4 | import { RedisCacheModule } from '../../redis-cache/redis-cache.module'; 5 | import { ConfigModule } from '@nestjs/config'; 6 | 7 | describe('PumpFunService', () => { 8 | let service: PumpFunService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [RedisCacheModule, ConfigModule.forRoot({})], 13 | providers: [PumpFunService, RedisCacheService], 14 | }).compile(); 15 | 16 | service = module.get(PumpFunService); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(service).toBeDefined(); 21 | }); 22 | 23 | it('isPumpFun', async () => { 24 | expect( 25 | await service.getPumpInfo('AiQcnL5gPjEXVH1E1FGUdN1WhPz4qXAZfQJxpGrJpump'), 26 | ).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/token-info/pump-fun/pump-fun.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PumpFunResponseDto } from './pump-fun-response.dto'; 3 | import { RedisCacheService } from '../../redis-cache/redis-cache.service'; 4 | 5 | @Injectable() 6 | export class PumpFunService { 7 | constructor(private readonly redisCacheService: RedisCacheService) {} 8 | 9 | async getPumpInfo(mintStr: string): Promise { 10 | const cacheKey = `pump_info_${mintStr}`; 11 | const cachedData = 12 | await this.redisCacheService.get(cacheKey); 13 | if (cachedData) { 14 | return cachedData; 15 | } 16 | const url = `https://frontend-api.pump.fun/coins/${mintStr}`; 17 | const response = await fetch(url, { 18 | headers: { 19 | 'User-Agent': 20 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0', 21 | Accept: '*/*', 22 | 'Accept-Language': 'en-US,en;q=0.5', 23 | 'Accept-Encoding': 'gzip, deflate, br', 24 | Referer: 'https://www.pump.fun/', 25 | Origin: 'https://www.pump.fun', 26 | Connection: 'keep-alive', 27 | 'Sec-Fetch-Dest': 'empty', 28 | 'Sec-Fetch-Mode': 'cors', 29 | 'Sec-Fetch-Site': 'cross-site', 30 | 'If-None-Match': 'W/"43a-tWaCcS4XujSi30IFlxDCJYxkMKg"', 31 | }, 32 | }); 33 | 34 | if (response.status === 200) { 35 | const data = await response.json(); 36 | await this.redisCacheService.set(cacheKey, data); 37 | return data; 38 | } else if (response.status === 500) { 39 | throw new Error('Not pump token'); 40 | } else { 41 | throw new Error('pump fun api error'); 42 | } 43 | } 44 | 45 | async isPump(mintStr: string): Promise { 46 | try { 47 | const data = await this.getPumpInfo(mintStr); 48 | return !data.complete; 49 | } catch (e: Error | any) { 50 | if (e.message === 'Not pump token') { 51 | return false; 52 | } 53 | throw e; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/token-info/token-info.dto.ts: -------------------------------------------------------------------------------- 1 | export class TokenInfoDto { 2 | success: boolean; 3 | message: string; 4 | result: TokenResultDto; 5 | } 6 | 7 | export class TokenResultDto { 8 | name: string; 9 | symbol: string; 10 | metadata_uri: string; 11 | description: string; 12 | image: string; 13 | decimals: number; 14 | address: string; 15 | mint_authority: string; 16 | freeze_authority: string; 17 | current_supply: number; 18 | extensions: any[]; // 如果有明确的扩展结构,可以进一步定义类型 19 | } 20 | -------------------------------------------------------------------------------- /src/token-info/token-info.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class TokenInfo { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column({ unique: true }) 9 | mint: string; 10 | 11 | @Column() 12 | name: string; 13 | 14 | @Column() 15 | symbol: string; 16 | 17 | @Column() 18 | supply: number; 19 | 20 | @Column() 21 | decimals: number; 22 | 23 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 24 | created_at: Date; 25 | } 26 | -------------------------------------------------------------------------------- /src/token-info/token-info.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PumpFunModule } from './pump-fun/pump-fun.module'; 3 | import { TokenInfoService } from './token-info.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TokenInfo } from './token-info.entity'; 6 | import { RedisCacheModule } from '../redis-cache/redis-cache.module'; 7 | import { PumpFunService } from './pump-fun/pump-fun.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | PumpFunModule, 12 | TypeOrmModule.forFeature([TokenInfo]), 13 | RedisCacheModule, 14 | ], 15 | providers: [TokenInfoService, PumpFunService], 16 | }) 17 | export class TokenInfoModule {} 18 | -------------------------------------------------------------------------------- /src/token-info/token-info.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TokenInfoService } from './token-info.service'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { RedisCacheService } from '../redis-cache/redis-cache.service'; 5 | import { PumpFunModule } from './pump-fun/pump-fun.module'; 6 | import { PumpFunService } from './pump-fun/pump-fun.service'; 7 | import { RedisCacheModule } from '../redis-cache/redis-cache.module'; 8 | import { TypeOrmModule } from '@nestjs/typeorm'; 9 | import { TokenInfo } from './token-info.entity'; 10 | import { typeOrmConfig } from '../config/typeorm.config'; 11 | 12 | describe('TokenInfoService', () => { 13 | let service: TokenInfoService; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | imports: [ 18 | ConfigModule.forRoot({}), 19 | PumpFunModule, 20 | RedisCacheModule, 21 | TypeOrmModule.forRootAsync({ 22 | imports: [ConfigModule], 23 | inject: [ConfigService], 24 | useFactory: typeOrmConfig, 25 | }), 26 | TypeOrmModule.forFeature([TokenInfo]), 27 | ], 28 | providers: [TokenInfoService, PumpFunService, RedisCacheService], 29 | }).compile(); 30 | 31 | service = module.get(TokenInfoService); 32 | }); 33 | 34 | it('should be defined', () => { 35 | expect(service).toBeDefined(); 36 | }); 37 | 38 | it('test getTokeninfo', async () => { 39 | const data = await service.getTokenInfo( 40 | 'SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s', 41 | ); 42 | console.log(data); 43 | }); 44 | 45 | it('test getTokenPrice', async () => { 46 | const data = await service.getTokenPrice( 47 | 'SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s', 48 | ); 49 | console.log(data); 50 | }); 51 | 52 | it('test getFullTokenInfo', async () => { 53 | const data = await service.getFullTokenInfo( 54 | 'AiQcnL5gPjEXVH1E1FGUdN1WhPz4qXAZfQJxpGrJpump', 55 | ); 56 | console.log(data); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/token-info/token-info.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PumpFunService } from './pump-fun/pump-fun.service'; 3 | import { ResponseDto } from './jupiter-price-response-dto/response.dto'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { TokenInfo } from './token-info.entity'; 7 | import { TokenInfoDto } from './token-info.dto'; 8 | import { Repository } from 'typeorm'; 9 | import { PumpFunResponseDto } from './pump-fun/pump-fun-response.dto'; 10 | import { RedisCacheService } from '../redis-cache/redis-cache.service'; 11 | 12 | @Injectable() 13 | export class TokenInfoService { 14 | constructor( 15 | private readonly pumpFunService: PumpFunService, 16 | private readonly configService: ConfigService, 17 | @InjectRepository(TokenInfo) 18 | private readonly tokenInfoRepository: Repository, 19 | private readonly redisCacheService: RedisCacheService, 20 | ) {} 21 | 22 | async getFullTokenInfo(mint: string) { 23 | const tokenInfo = this.getTokenInfo(mint); 24 | const tokenPrice = this.getTokenPrice(mint); 25 | const isPump = this.pumpFunService.isPump(mint); 26 | 27 | const [tokenInfoData, tokenPriceData, isPumpData] = await Promise.all([ 28 | tokenInfo, 29 | tokenPrice, 30 | isPump, 31 | ]); 32 | return { 33 | ...tokenInfoData, 34 | price: tokenPriceData.data[mint].price, 35 | isPump: isPumpData, 36 | }; 37 | } 38 | 39 | async getPumpInfo(mint: string): Promise { 40 | return await this.pumpFunService.getPumpInfo(mint); 41 | } 42 | 43 | async getTokenPrice(mint: string): Promise { 44 | const cacheKey = `token_price_${mint}`; 45 | const cachedData = await this.redisCacheService.get(cacheKey); 46 | if (cachedData) { 47 | return cachedData; 48 | } 49 | const response = await fetch( 50 | `https://api.jup.ag/price/v2?ids=${mint}&showExtraInfo=true`, 51 | ); 52 | 53 | if (response.status === 200) { 54 | const data: ResponseDto = await response.json(); 55 | // await this.cacheManager.set(cacheKey, data); 56 | await this.redisCacheService.set(cacheKey, data); 57 | return data; 58 | } else if (response.status === 500) { 59 | throw new Error('Not jupiter token'); 60 | } else { 61 | throw new Error('jupiter api error'); 62 | } 63 | } 64 | 65 | async getTokenInfo(mint: string) { 66 | const tokenInfo = await this.tokenInfoRepository.findOneBy({ mint }); 67 | if (tokenInfo) { 68 | return tokenInfo; 69 | } 70 | const response = await fetch( 71 | `https://api.shyft.to/sol/v1/token/get_info?network=mainnet-beta&token_address=${mint}`, 72 | { headers: { 'x-api-key': this.configService.get('SHYFT_API_KEY') } }, 73 | ); 74 | if (response.status === 200) { 75 | const data: TokenInfoDto = await response.json(); 76 | const newTokenData = { 77 | mint: data.result.address, 78 | name: data.result.name, 79 | symbol: data.result.symbol, 80 | supply: data.result.current_supply, 81 | decimals: data.result.decimals, 82 | }; 83 | const newTokenInfo = this.tokenInfoRepository.create(newTokenData); 84 | return this.tokenInfoRepository.save(newTokenInfo); 85 | } 86 | throw new Error('shyft api error'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/trade/jito.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import bs58 from 'bs58'; 3 | 4 | @Injectable() 5 | export class JitoService { 6 | get JitoEndpoints(): { 7 | tokyo: string; 8 | amsterdam: string; 9 | ny: string; 10 | mainnet: string; 11 | frankfurt: string; 12 | } { 13 | return this._JitoEndpoints; 14 | } 15 | 16 | constructor() {} 17 | 18 | private _JitoEndpoints = { 19 | mainnet: 'https://mainnet.block-engine.jito.wtf/api/v1/transactions', 20 | amsterdam: 21 | 'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/transactions', 22 | frankfurt: 23 | 'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/transactions', 24 | ny: 'https://ny.mainnet.block-engine.jito.wtf/api/v1/transactions', 25 | tokyo: 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/transactions', 26 | }; 27 | 28 | getRandomJitoEndpoint() { 29 | const keys = Object.keys(this.JitoEndpoints); 30 | const index = Math.floor(Math.random() * keys.length); 31 | return this.JitoEndpoints[keys[index]]; 32 | } 33 | 34 | async sendWithJito(serializedTx: Uint8Array | Buffer | number[]) { 35 | const endpoint = this.getRandomJitoEndpoint(); 36 | const encodedTx = bs58.encode(serializedTx); 37 | const payload = { 38 | jsonrpc: '2.0', 39 | id: 1, 40 | method: 'sendTransaction', 41 | params: [encodedTx], 42 | }; 43 | const res = await fetch(`${endpoint}?bundleOnly=true`, { 44 | method: 'POST', 45 | body: JSON.stringify(payload), 46 | headers: { 'Content-Type': 'application/json' }, 47 | }); 48 | const json = await res.json(); 49 | if (json.error) { 50 | throw new Error(json.error.message); 51 | } 52 | return json; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/trade/result.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Processor, WorkerHost } from '@nestjs/bullmq'; 2 | import { Job } from 'bullmq'; 3 | import { Connection } from '@solana/web3.js'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { TradeService } from './trade.service'; 6 | import { TelegramService } from '../telegram/telegram.service'; 7 | import { UserService } from '../user/user.service'; 8 | 9 | @Processor('trade_result') 10 | export class ResultConsumer extends WorkerHost { 11 | get connection(): Connection { 12 | return this._connection; 13 | } 14 | 15 | constructor( 16 | private readonly configService: ConfigService, 17 | private readonly tradeService: TradeService, 18 | private readonly telegramService: TelegramService, 19 | private readonly userService: UserService, 20 | ) { 21 | super(); 22 | } 23 | 24 | private _connection = new Connection( 25 | this.configService.get('SOLANA_ENDPOINT'), 26 | ); 27 | 28 | async process(job: Job) { 29 | const txid = job.data.txid; 30 | const userId = job.data.userId; 31 | const referralBalance = job.data.referralBalance; 32 | const result = await this.connection.getSignatureStatus(txid); 33 | if ( 34 | result && 35 | result.value && 36 | result.value.confirmationStatus === 'confirmed' 37 | ) { 38 | console.log(`Transaction ${txid} is confirmed.`); 39 | await this.tradeService.confirmTrade(txid); 40 | await this.telegramService.sendMessage( 41 | userId, 42 | `交易成功, 点击查看 点击查看区块浏览器\n`, 43 | ); 44 | await this.userService.addReferralBalance(userId, referralBalance); 45 | } else { 46 | console.log(`Transaction ${txid} is not confirmed yet.`); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/trade/solana.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Connection, PublicKey } from '@solana/web3.js'; 4 | import { 5 | getAccount, 6 | getAssociatedTokenAddress, 7 | getMint, 8 | } from '@solana/spl-token'; 9 | 10 | @Injectable() 11 | export class SolanaService { 12 | get connection(): Connection { 13 | return this._connection; 14 | } 15 | 16 | constructor(private readonly configService: ConfigService) {} 17 | 18 | private _connection = new Connection( 19 | this.configService.get('SOLANA_ENDPOINT'), 20 | ); 21 | 22 | async getTokenFmtBalance(address: string, mintStr: string) { 23 | const wallet = new PublicKey(address); 24 | const mintKey = new PublicKey(mintStr); 25 | const tokenAccount = await getAssociatedTokenAddress(mintKey, wallet); 26 | const info = await getAccount(this.connection, tokenAccount); 27 | const mint = await getMint(this.connection, info.mint); 28 | return Number(info.amount) / 10 ** mint.decimals; 29 | } 30 | 31 | async getTokenBalance(address: string, mintStr: string) { 32 | const wallet = new PublicKey(address); 33 | const mintKey = new PublicKey(mintStr); 34 | const tokenAccount = await getAssociatedTokenAddress(mintKey, wallet); 35 | const info = await getAccount(this.connection, tokenAccount); 36 | return Number(info.amount); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/trade/trade.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { User } from '../user/user.entity'; 3 | 4 | @Entity() 5 | export class Trade { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ nullable: true }) 10 | txid: string; 11 | 12 | @Column() 13 | tokenMint: string; 14 | 15 | @Column() 16 | is_buy: boolean; 17 | 18 | @Column({ default: false }) 19 | confirmed: boolean; 20 | 21 | @Column({ default: 0 }) 22 | amount: number; 23 | 24 | @Column({ default: 0 }) 25 | solAmount: number; 26 | 27 | @Column({ default: 0 }) 28 | price: number; 29 | 30 | @ManyToOne(() => User, (user) => user.trades) 31 | user: User; 32 | } 33 | -------------------------------------------------------------------------------- /src/trade/trade.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Trade } from './trade.entity'; 4 | import { UserModule } from '../user/user.module'; 5 | import { TradeService } from './trade.service'; 6 | import { SettingModule } from '../setting/setting.module'; 7 | import { SolanaService } from './solana.service'; 8 | import { JitoService } from './jito.service'; 9 | import { BullModule } from '@nestjs/bullmq'; 10 | import { ResultConsumer } from './result.consumer'; 11 | import { TelegramModule } from '../telegram/telegram.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([Trade]), 16 | UserModule, 17 | SettingModule, 18 | BullModule.registerQueue({ 19 | name: 'trade_result', 20 | }), 21 | TelegramModule, 22 | ], 23 | providers: [TradeService, SolanaService, JitoService, ResultConsumer], 24 | exports: [TradeService, SolanaService], 25 | }) 26 | export class TradeModule {} 27 | -------------------------------------------------------------------------------- /src/trade/trade.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TradeService } from './trade.service'; 3 | import { SettingModule } from '../setting/setting.module'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { ConfigModule, ConfigService } from '@nestjs/config'; 6 | import { typeOrmConfig } from '../config/typeorm.config'; 7 | import { Wallet } from '../wallet/wallet.entity'; 8 | import { User } from '../user/user.entity'; 9 | import { Setting } from '../setting/setting.entity'; 10 | import { SolanaService } from './solana.service'; 11 | import { JitoService } from './jito.service'; 12 | 13 | describe('TradeService', () => { 14 | let service: TradeService; 15 | let solanaService: SolanaService; 16 | 17 | beforeEach(async () => { 18 | const module: TestingModule = await Test.createTestingModule({ 19 | providers: [TradeService, SolanaService, JitoService], 20 | imports: [ 21 | ConfigModule.forRoot({}), 22 | TypeOrmModule.forRootAsync({ 23 | imports: [ConfigModule], 24 | inject: [ConfigService], 25 | useFactory: typeOrmConfig, 26 | }), 27 | TypeOrmModule.forFeature([Setting, User, Wallet]), 28 | SettingModule, 29 | ], 30 | }).compile(); 31 | 32 | service = module.get(TradeService); 33 | solanaService = module.get(SolanaService); 34 | }); 35 | 36 | it('should be defined', () => { 37 | expect(service).toBeDefined(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/trade/trade.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { createJupiterApiClient, QuoteResponse } from '@jup-ag/api'; 3 | import { SettingService } from '../setting/setting.service'; 4 | import { 5 | Keypair, 6 | PublicKey, 7 | Signer, 8 | SystemProgram, 9 | Transaction, 10 | TransactionInstruction, 11 | } from '@solana/web3.js'; 12 | import { ConfigService } from '@nestjs/config'; 13 | import bs58 from 'bs58'; 14 | import { SolanaService } from './solana.service'; 15 | import { JitoService } from './jito.service'; 16 | import { InjectQueue } from '@nestjs/bullmq'; 17 | import { Queue } from 'bullmq'; 18 | import { InjectRepository } from '@nestjs/typeorm'; 19 | import { Trade } from './trade.entity'; 20 | import { Repository } from 'typeorm'; 21 | import { UserService } from '../user/user.service'; 22 | import * as _ from 'lodash'; 23 | 24 | @Injectable() 25 | export class TradeService { 26 | get NativeSol(): string { 27 | return this._NativeSol; 28 | } 29 | 30 | set NativeSol(value: string) { 31 | this._NativeSol = value; 32 | } 33 | 34 | private jupiterQuoteApi = createJupiterApiClient(); 35 | private _NativeSol = 'So11111111111111111111111111111111111111112'; 36 | 37 | constructor( 38 | private readonly settingService: SettingService, 39 | private readonly configService: ConfigService, 40 | private readonly solanaService: SolanaService, 41 | private readonly jitoService: JitoService, 42 | private readonly userService: UserService, 43 | @InjectRepository(Trade) 44 | private readonly tradeRepository: Repository, 45 | @InjectQueue('trade_result') private readonly resultQueue: Queue, 46 | ) {} 47 | 48 | getFeeInstructions( 49 | address: string, 50 | feeAmount: number, 51 | ): TransactionInstruction[] { 52 | const feeReceiverAddress = this.configService.get('FEE_RECIPIENT_ADDRESS'); 53 | // Create a solana transfer instruction with web3.js 54 | const instruction = SystemProgram.transfer({ 55 | fromPubkey: new PublicKey(address), 56 | toPubkey: new PublicKey(feeReceiverAddress), 57 | lamports: feeAmount, 58 | }); 59 | return [instruction]; 60 | } 61 | 62 | async createTrade( 63 | userId: number, 64 | amount: number, 65 | solAmount: number, 66 | mint: string, 67 | isBuy: boolean, 68 | ) { 69 | const price = solAmount / amount; 70 | const trade = this.tradeRepository.create({ 71 | user: { id: userId }, 72 | amount, 73 | solAmount, 74 | tokenMint: mint, 75 | price, 76 | is_buy: isBuy, 77 | }); 78 | await this.tradeRepository.save(trade); 79 | return trade; 80 | } 81 | 82 | async confirmTrade(txid: string) { 83 | const trade = await this.tradeRepository.findOneBy({ txid }); 84 | if (trade) { 85 | trade.confirmed = true; 86 | await this.tradeRepository.save(trade); 87 | } 88 | } 89 | 90 | async getSwapTx( 91 | userId: number, 92 | quoteResponse: QuoteResponse, 93 | walletAddress: string, 94 | ) { 95 | const jitoFee = await this.settingService.getJitoFee(userId); 96 | const { swapTransaction } = await ( 97 | await fetch('https://quote-api.jup.ag/v6/swap', { 98 | method: 'POST', 99 | headers: { 100 | 'Content-Type': 'application/json', 101 | }, 102 | body: JSON.stringify({ 103 | // quoteResponse from /quote api 104 | quoteResponse, 105 | asLegacyTransaction: true, 106 | prioritizationFeeLamports: { 107 | jitoTipLamports: jitoFee, 108 | }, 109 | dynamicComputeUnitLimit: true, 110 | // user public key to be used for the swap 111 | userPublicKey: walletAddress, 112 | // auto wrap and unwrap SOL. default is true 113 | wrapAndUnwrapSol: true, 114 | skipUserAccountsRpcCalls: false, 115 | }), 116 | }) 117 | ).json(); 118 | return swapTransaction; 119 | } 120 | 121 | async getBuyInstructions(userId: number, amount: number, mint: string) { 122 | const wallet = await this.settingService.getWallet(userId); 123 | const privateKey = wallet.private_key; 124 | const signWallet = Keypair.fromSecretKey(bs58.decode(privateKey)); 125 | const signer: Signer = { 126 | publicKey: signWallet.publicKey, 127 | secretKey: signWallet.secretKey, 128 | }; 129 | const slippage = await this.settingService.getSlippage(userId); 130 | const lamports = Math.floor(amount * 10 ** 9); 131 | const quote = await this.jupiterQuoteApi.quoteGet({ 132 | inputMint: this.NativeSol, 133 | outputMint: mint, 134 | amount: lamports, 135 | slippageBps: slippage * 100, 136 | }); 137 | await this.createTrade( 138 | userId, 139 | Number(quote.outAmount), 140 | Number(quote.inAmount), 141 | mint, 142 | true, 143 | ); 144 | const swapTransaction = await this.getSwapTx(userId, quote, wallet.address); 145 | const txBuf = Buffer.from(swapTransaction, 'base64'); 146 | const transaction = Transaction.from(txBuf); 147 | const feeInstruction = this.getFeeInstructions( 148 | wallet.address, 149 | Math.floor(lamports / 100), 150 | ); 151 | transaction.add(...feeInstruction); 152 | transaction.sign(signer); 153 | return { 154 | transaction, 155 | referralBalance: Math.floor((lamports / 100) * 0.25), 156 | }; 157 | } 158 | 159 | async getSellInstructions(userId: number, rate: number, mint: string) { 160 | const wallet = await this.settingService.getWallet(userId); 161 | const privateKey = wallet.private_key; 162 | const signWallet = Keypair.fromSecretKey(bs58.decode(privateKey)); 163 | const signer: Signer = { 164 | publicKey: signWallet.publicKey, 165 | secretKey: signWallet.secretKey, 166 | }; 167 | const slippage = await this.settingService.getSlippage(userId); 168 | const totalAmount = await this.solanaService.getTokenBalance( 169 | wallet.address, 170 | mint, 171 | ); 172 | console.log(totalAmount); 173 | const amount = Math.floor((totalAmount * rate) / 100); 174 | console.log(amount); 175 | const quote = await this.jupiterQuoteApi.quoteGet({ 176 | inputMint: mint, 177 | outputMint: this.NativeSol, 178 | amount: amount, 179 | slippageBps: slippage * 100, 180 | }); 181 | const outAmount = parseInt(quote.outAmount); 182 | 183 | const swapTransaction = await this.getSwapTx(userId, quote, wallet.address); 184 | await this.createTrade(userId, amount, outAmount, mint, false); 185 | const txBuf = Buffer.from(swapTransaction, 'base64'); 186 | const transaction = Transaction.from(txBuf); 187 | const feeInstruction = this.getFeeInstructions( 188 | wallet.address, 189 | Math.floor(outAmount / 100), 190 | ); 191 | transaction.add(...feeInstruction); 192 | transaction.sign(signer); 193 | return { 194 | transaction, 195 | referralBalance: Math.floor((totalAmount / 100) * 0.25), 196 | }; 197 | } 198 | 199 | async trade(userId: number, rateOrSol: number, mint: string, isBuy: boolean) { 200 | let res: { transaction: Transaction; referralBalance: number }; 201 | if (isBuy) { 202 | res = await this.getBuyInstructions(userId, rateOrSol, mint); 203 | } else { 204 | res = await this.getSellInstructions(userId, rateOrSol, mint); 205 | } 206 | const serializedTx = res.transaction.serialize(); 207 | const result = await this.jitoService.sendWithJito(serializedTx); 208 | console.log(res.referralBalance); 209 | if (result.result) { 210 | this.resultQueue.add( 211 | 'checkTx', 212 | { txid: result.result, userId, referralBalance: res.referralBalance }, 213 | { 214 | delay: 2000, 215 | backoff: { 216 | type: 'fixed', 217 | delay: 2000, 218 | }, 219 | }, 220 | ); 221 | } 222 | } 223 | 224 | async getQuotePrice(mint: string, amount: number) { 225 | const quote = await this.jupiterQuoteApi.quoteGet({ 226 | inputMint: mint, 227 | outputMint: this.NativeSol, 228 | amount: amount, 229 | }); 230 | return quote.outAmount; 231 | } 232 | 233 | async calculateProfit(userId: number, mint: string) { 234 | const trades = await this.tradeRepository.find({ 235 | where: { tokenMint: mint, user: { id: userId } }, 236 | }); 237 | const buyTrades = _.filter(trades, { is_buy: true }); 238 | const solOut = _.sumBy(buyTrades, 'solAmount'); 239 | const sellTrades = _.filter(trades, { is_buy: false }); 240 | const solIn = _.sumBy(sellTrades, 'solAmount'); 241 | const wallet = await this.settingService.getWallet(userId); 242 | const currentPositionInToken = await this.solanaService.getTokenBalance( 243 | wallet.address, 244 | mint, 245 | ); 246 | const currentPositionInSol = await this.getQuotePrice( 247 | mint, 248 | currentPositionInToken, 249 | ); 250 | const profit: number = solIn - solOut + Number(currentPositionInSol); 251 | return Number(profit.toFixed(2)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | ManyToOne, 5 | OneToMany, 6 | OneToOne, 7 | PrimaryColumn, 8 | } from 'typeorm'; 9 | import { Wallet } from '../wallet/wallet.entity'; 10 | import { Trade } from '../trade/trade.entity'; 11 | import { Message } from '../message/message.entity'; 12 | import { Setting } from '../setting/setting.entity'; 13 | 14 | @Entity() 15 | export class User { 16 | @PrimaryColumn() 17 | id: number; 18 | 19 | @Column({ nullable: false }) 20 | first_name: string; 21 | 22 | @Column({ nullable: true }) 23 | last_name: string; 24 | 25 | @OneToMany(() => Wallet, (wallet) => wallet.user) 26 | wallets: Wallet[]; 27 | 28 | @ManyToOne(() => User, (user) => user.children) 29 | parent: User; 30 | 31 | @OneToMany(() => User, (user) => user.parent) 32 | children: User[]; 33 | 34 | @Column({ unique: true, nullable: true }) 35 | referral_code: string; 36 | 37 | @OneToMany(() => Trade, (trade) => trade.user) 38 | trades: Trade[]; 39 | 40 | @OneToMany(() => Message, (message) => message.user) 41 | messages: Message[]; 42 | 43 | @OneToOne(() => Setting, (setting) => setting.user) 44 | setting: Setting; 45 | 46 | @Column({ default: 0 }) 47 | referral_balance: number; 48 | 49 | @Column({ default: 0 }) 50 | withdraw_balance: number; 51 | } 52 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { User } from './user.entity'; 5 | 6 | @Module({ 7 | imports: [TypeOrmModule.forFeature([User])], 8 | providers: [UserService], 9 | exports: [UserService], 10 | }) 11 | export class UserModule {} 12 | -------------------------------------------------------------------------------- /src/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { typeOrmConfig } from '../config/typeorm.config'; 6 | import { User } from './user.entity'; 7 | 8 | describe('UserService', () => { 9 | let service: UserService; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [UserService], 14 | imports: [ 15 | ConfigModule.forRoot({}), 16 | TypeOrmModule.forRootAsync({ 17 | imports: [ConfigModule], 18 | inject: [ConfigService], 19 | useFactory: typeOrmConfig, 20 | }), 21 | TypeOrmModule.forFeature([User]), 22 | ], 23 | }).compile(); 24 | 25 | service = module.get(UserService); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Repository } from 'typeorm'; 3 | import { User } from './user.entity'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor( 9 | @InjectRepository(User) 10 | private readonly userRepository: Repository, 11 | ) {} 12 | 13 | async createUser( 14 | userId: number, 15 | firstName: string, 16 | lastName?: string, 17 | referer_code?: string, 18 | ) { 19 | const user = await this.userRepository.findOneBy({ id: userId }); 20 | let parent: User = null; 21 | if (referer_code) { 22 | parent = await this.userRepository.findOneBy({ 23 | referral_code: referer_code, 24 | }); 25 | } 26 | if (!user) { 27 | const newUser = this.userRepository.create({ 28 | id: userId, 29 | first_name: firstName, 30 | last_name: lastName ? lastName : null, 31 | parent: parent ? parent : null, 32 | }); 33 | await this.userRepository.save(newUser); 34 | } 35 | } 36 | 37 | async findUser(userId: number) { 38 | return await this.userRepository.findOneBy({ id: userId }); 39 | } 40 | 41 | async setReferParent(userId: number, referral_code: string) { 42 | const user = await this.userRepository.findOneBy({ id: userId }); 43 | if (user) { 44 | if (user.parent) return; 45 | const parent = await this.userRepository.findOneBy({ 46 | referral_code, 47 | }); 48 | if (parent) { 49 | user.parent = parent; 50 | await this.userRepository.save(user); 51 | } else { 52 | return; 53 | } 54 | } else { 55 | return; 56 | } 57 | } 58 | 59 | async getOrGenerateReferralCode(userId: number) { 60 | const user = await this.userRepository.findOneBy({ id: userId }); 61 | if (user) { 62 | if (user.referral_code) return user.referral_code; 63 | const newCode = this.generateCode(); 64 | user.referral_code = newCode; 65 | await this.userRepository.save(user); 66 | return newCode; 67 | } 68 | } 69 | 70 | // get children count 71 | async getChildrenCount(userId: number) { 72 | const user = await this.userRepository.findOneBy({ id: userId }); 73 | const children = await this.userRepository.find({ 74 | where: { parent: user }, 75 | }); 76 | if (children) { 77 | return children.length; 78 | } else return 0; 79 | } 80 | 81 | // It's for adding parent referral balance 82 | async addReferralBalance(userId: number, amount: number) { 83 | const user = await this.userRepository.findOne({ 84 | where: { id: userId }, 85 | relations: { parent: true }, 86 | }); 87 | const parent = await this.userRepository.findOneBy({ 88 | id: user.parent.id, 89 | }); 90 | console.log(parent); 91 | if (parent) { 92 | parent.referral_balance += amount; 93 | await this.userRepository.save(parent); 94 | } 95 | } 96 | 97 | async getReferralBalance(userId: number) { 98 | const user = await this.userRepository.findOneBy({ id: userId }); 99 | if (user) { 100 | return user.referral_balance; 101 | } 102 | } 103 | 104 | async addWithdrawBalance(userId: number, amount: number) { 105 | const user = await this.userRepository.findOneBy({ id: userId }); 106 | if (user) { 107 | user.withdraw_balance += amount; 108 | await this.userRepository.save(user); 109 | } 110 | } 111 | 112 | async getWithdrawBalance(userId: number) { 113 | const user = await this.userRepository.findOneBy({ id: userId }); 114 | if (user) { 115 | return user.withdraw_balance; 116 | } 117 | } 118 | 119 | private generateCode() { 120 | const str = 121 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 122 | let referral_code = ''; 123 | for (let i = 0; i < 10; i++) { 124 | referral_code += str.charAt(Math.floor(Math.random() * str.length)); 125 | } 126 | return referral_code; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/wallet/wallet.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | ManyToOne, 5 | OneToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import { User } from '../user/user.entity'; 9 | import { Setting } from '../setting/setting.entity'; 10 | 11 | @Entity() 12 | export class Wallet { 13 | @PrimaryGeneratedColumn() 14 | id: number; 15 | 16 | @Column({ unique: true }) 17 | address: string; 18 | 19 | @Column() 20 | balance: number; 21 | 22 | @Column() 23 | private_key: string; 24 | 25 | @ManyToOne(() => User, (user) => user.wallets) 26 | user: User; 27 | 28 | @OneToOne(() => Setting, (setting) => setting.wallet) 29 | setting: Setting; 30 | 31 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 32 | created_at: Date; 33 | } 34 | -------------------------------------------------------------------------------- /src/wallet/wallet.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WalletService } from './wallet.service'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Wallet } from './wallet.entity'; 5 | import { UserModule } from '../user/user.module'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Wallet]), UserModule, ConfigModule], 10 | providers: [WalletService], 11 | exports: [WalletService], 12 | }) 13 | export class WalletModule {} 14 | -------------------------------------------------------------------------------- /src/wallet/wallet.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WalletService } from './wallet.service'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { UserModule } from '../user/user.module'; 5 | import { UserService } from '../user/user.service'; 6 | import { Wallet } from './wallet.entity'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { typeOrmConfig } from '../config/typeorm.config'; 9 | 10 | describe('WalletService', () => { 11 | let service: WalletService; 12 | let userService: UserService; 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | ConfigModule.forRoot({}), 17 | UserModule, 18 | TypeOrmModule.forRootAsync({ 19 | imports: [ConfigModule], 20 | inject: [ConfigService], 21 | useFactory: typeOrmConfig, 22 | }), 23 | TypeOrmModule.forFeature([Wallet]), 24 | ], 25 | providers: [WalletService], 26 | }).compile(); 27 | 28 | service = module.get(WalletService); 29 | userService = module.get(UserService); 30 | }); 31 | 32 | it('should be defined', () => { 33 | expect(service).toBeDefined(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/wallet/wallet.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { 5 | Connection, 6 | Keypair, 7 | LAMPORTS_PER_SOL, 8 | PublicKey, 9 | } from '@solana/web3.js'; 10 | import bs58 from 'bs58'; 11 | import { Wallet } from './wallet.entity'; 12 | import { ConfigService } from '@nestjs/config'; 13 | import { UserService } from '../user/user.service'; 14 | 15 | @Injectable() 16 | export class WalletService { 17 | constructor( 18 | @InjectRepository(Wallet) 19 | private readonly walletRepository: Repository, 20 | private readonly userService: UserService, 21 | private readonly configService: ConfigService, 22 | ) {} 23 | 24 | async generateWallet(userId: number) { 25 | const user = await this.userService.findUser(userId); 26 | if (user) { 27 | if (user.wallets && user.wallets.length >= 5) return; 28 | const newKeypair = Keypair.generate(); 29 | const newWallet = this.walletRepository.create({ 30 | address: newKeypair.publicKey.toBase58(), 31 | private_key: bs58.encode(newKeypair.secretKey), 32 | balance: 0, 33 | user: user, 34 | }); 35 | return await this.walletRepository.save(newWallet); 36 | } 37 | } 38 | 39 | async findWalletByAddress(address: string) { 40 | return this.walletRepository.findOneBy({ address }); 41 | } 42 | 43 | async removeWallet(userId: number, address: string) { 44 | const user = await this.userService.findUser(userId); 45 | if (user) { 46 | await this.walletRepository.delete({ address, user }); 47 | } 48 | } 49 | 50 | async getPrivateKey(address: string) { 51 | const wallet = await this.walletRepository.findOneBy({ address }); 52 | if (wallet) { 53 | return wallet.private_key; 54 | } 55 | } 56 | 57 | async getWallets(userId: number) { 58 | const user = await this.userService.findUser(userId); 59 | if (user) { 60 | return this.walletRepository.find({ where: { user: user } }); 61 | } 62 | } 63 | 64 | async getBalance(address: string) { 65 | const connection = new Connection( 66 | this.configService.get('SOLANA_ENDPOINT'), 67 | 'confirmed', 68 | ); 69 | const balance = await connection.getBalance(new PublicKey(address)); 70 | return balance / LAMPORTS_PER_SOL; 71 | } 72 | 73 | async refreshUserWalletsBalance(userId: number) { 74 | const user = await this.userService.findUser(userId); 75 | if (user) { 76 | const wallets = await this.getWallets(userId); 77 | for (const wallet of wallets) { 78 | wallet.balance = await this.getBalance(wallet.address); 79 | await this.walletRepository.save(wallet); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6", 6 | "dom" 7 | ], 8 | "target": "ES2021", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "outDir": "./build", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "sourceMap": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------