@@ -0,0 +1,14 @@
+{
+ "plugins": [
+ ["module-resolver", {
+ "root": ["./"],
+ "alias": {}
+ }],
+ ["@babel/plugin-proposal-decorators", { "legacy": true }],
+ ["@babel/plugin-proposal-class-properties"]
+ ],
+ "presets": [
+ ["@babel/preset-env", { "targets": { "node": "current" } }],
+ "@babel/preset-typescript"
+ ]
+}
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+[*.md]
+max_line_length = 0
@@ -0,0 +1,8 @@
+DEBUG=1
+DB_HOST=127.0.0.1
+DB_NAME=wxmonitor
+DB_USER=root
+DB_PASS=
+PORT=8089
+SOLANA_RPC=
+DATA_SERVICR_BASE_URL=
@@ -0,0 +1,7 @@
+lib
+main.js
+server.js
+.eslintrc.js
+ref
+shellTool
+src/services/solswap.service/pumpfun.service/sdk/*
@@ -0,0 +1,36 @@
+// module.exports = {
+// parser: '@typescript-eslint/parser',
+// extends: [
+// 'standard-with-typescript',
+// 'eslint:recommended',
+// 'plugin:@typescript-eslint/recommended',
+// ],
+// plugins: ['prettier', '@typescript-eslint'],
+// rules: {
+// 'prettier/prettier': ['error', { singleQuote: true, semi: false }],
+// 'comma-dangle': 0,
+// '@typescript-eslint/space-before-function-paren': 0,
+// 'multiline-ternary': 0,
+// '@typescript-eslint/strict-boolean-expressions': 0,
+// },
+// }
+module.exports = {
+ // parser: '@typescript-eslint/parser',
+ extends: [
+ 'standard-with-typescript',
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ parserOptions: {
+ project: './tsconfig.json',
+ },
+ plugins: ['prettier'],
+ rules: {
+ 'prettier/prettier': ['error', { singleQuote: true, semi: false }],
+ 'comma-dangle': 0,
+ '@typescript-eslint/space-before-function-paren': 0,
+ 'multiline-ternary': 0,
+ '@typescript-eslint/strict-boolean-expressions': 0,
@@ -0,0 +1,24 @@
+node_modules
+output
+*.swp
+*.log
+.env
+upload
+memory
+profile
+public
+src/scripts/const.ts
+.vscode/*
+.prettierrc.js
+*.import.json
+*.import.csv
+temp.ts
+src/Monitor/startLocal.ts
+test.sh
+tempScripts
+staticData
+data
+### 2023/12/10 更新调整内容
+1. CampaignReward 模型新增 overwiteText 字段
+### 2023/12/8 更新调整内容
+1. campaigns 模型新增 needBindDiscord 字段
+2. campaignSteps 模型 新增 code 类型 joindiscord
+3. .env.example 新增 DISCORD 字段配置
+DISCORD_API_URL=https://discord.com/api
+DISCORD_CLIENT_ID=
+DISCORD_CLIENT_SECRET=
+DISCORD_REDIRECT_URI=http://localhost:3000/api/v1/discord/callback
+DISCORD_MY_SHELL_GUILD_ID=
@@ -0,0 +1,31 @@
+# SATEA VPN INFO MANAGE SYSTEM
+## 环境变量
+```shell
+DEBUG=1 # debug标识
+# 数据库信息
+DB_NAME=aklabs-merchant
+# 服务端口
+# Catalog结构文件目录,在 init.sh 中会创建一个,可以使用那个
+CATALOG_FILE_PATH=~/staticData/catalog.json
+# 支付系统信息
+PAYMENT_SYS_ENDPOINT=http://localhost:8090
+PAYMENT_SYS_APIKEY=local
+# 其他环境变量
+TEST_PK=0x000
+```
+## 初始化Catalog
+yarn run script -- lib/scripts/initCatalog.js
@@ -0,0 +1,11 @@
+echo "初始化"
+mkdir -p public/
+mkdir -p public/images
+mkdir -p public/audios
+mkdir -p staticData/
+mkdir -p data/
+cat "" >> data/.memory-card.json
+cp -rf pages public/
@@ -0,0 +1,142 @@
+ "name": "account-timeline-service",
+ "version": "1.0.1",
+ "main": "index.js",
+ "repository": "https://github.com/sekaiamber/account_timeline-service.git",
+ "author": "xiaomeng xu <a3824036@126.com>",
+ "license": "MIT",
+ "scripts": {
+ "postinstall": "./init.sh",
+ "babel": "babel src --out-dir lib --copy-files --extensions '.ts'",
+ "build": "npm run babel",
+ "lint": "eslint src",
+ "start": "npm run babel && node -r dotenv/config lib/start.js",
+ "start_monitor": "npm run babel && node -r dotenv/config lib/Monitor/start.js",
+ "start_monitor_local": "npm run babel && node -r dotenv/config lib/Monitor/startLocal.js",
+ "test": "npm run babel && node -r dotenv/config lib/test/image.js",
+ "upload_server": "http-server ./upload",
+ "script": "npm run babel && node -r dotenv/config ",
+ "script_no_build": "node -r dotenv/config ",
+ "pm2_start": "pm2 start --name banana npm -- run script -- lib/scripts/simpleStart.js",
+ "pm2_start2": "pm2 start --name banana2 npm -- run script -- lib/scripts/simpleStart2.js",
+ "pm2_start_monitor": "pm2 start --name gmgnlike_m npm -- run start_monitor"
+ "dependencies": {
+ "@bloxroute/solana-trader-client-ts": "^2.2.0",
+ "@cmdcode/buff-utils": "^2.0.0",
+ "@cmdcode/crypto-utils": "^2.4.6",
+ "@cmdcode/tapscript": "^1.4.3",
+ "@coral-xyz/anchor": "^0.30.1",
+ "@coral-xyz/borsh": "^0.30.1",
+ "@cosmjs/stargate": "^0.31.1",
+ "@goplus/sdk-node": "^1.0.9",
+ "@jup-ag/limit-order-sdk": "^0.1.10",
+ "@project-serum/anchor": "^0.26.0",
+ "@raydium-io/raydium-sdk-v2": "^0.1.73-alpha",
+ "@satea/vpn-info-monitor-utils": "0.0.19",
+ "@solana/spl-token": "^0.3.11",
+ "@solana/web3.js": "^1.95.2",
+ "@trpc/server": "10.45.0",
+ "@types/qrcode-terminal": "^0.12.2",
+ "@uniswap/v3-core": "^1.0.1",
+ "@uniswap/v3-sdk": "^3.9.0",
+ "alchemy-sdk": "^2.8.0",
+ "axios": "^0.24.0",
+ "bs58": "^5.0.0",
+ "cookie-parser": "^1.4.5",
+ "cron": "^2.3.1",
+ "crypto-js": "^4.2.0",
+ "csv-parse": "^5.5.3",
+ "decimal.js": "^10.3.1",
+ "decimal.js-light": "^2.5.1",
+ "dify-client": "^2.0.0",
+ "discord.js": "^14.14.1",
+ "dotenv": "^10.0.0",
+ "ejs": "^3.1.9",
+ "ethers": "^5.4.4",
+ "ethers6": "npm:ethers@^6.3.0",
+ "eventemitter3": "^5.0.1",
+ "express": "^4.17.1",
+ "express-http-proxy": "^2.0.0",
+ "express-winston": "^4.2.0",
+ "helmet": "^4.6.0",
+ "https-proxy-agent": "^5.0.0",
+ "i18next": "^22.4.10",
+ "js-yaml": "^4.1.0",
+ "jwt-simple": "^0.5.6",
+ "lodash": "^4.17.21",
+ "mariadb": "^2.5.5",
+ "markdown-escape": "^1.1.0",
+ "mathjs": "^11.6.0",
+ "memory-card": "1.1.2",
+ "merkletreejs": "^0.3.10",
+ "mkdirp": "^1.0.4",
+ "moment": "^2.29.1",
+ "moment-timezone": "^0.5.43",
+ "morgan": "^1.10.0",
+ "multer": "^1.4.5-lts.1",
+ "nanoid": "3",
+ "node-global-proxy": "^1.0.1",
+ "node-telegram-bot-api": "^0.63.0",
+ "qrcode-terminal": "^0.12.0",
+ "react-markdown": "^9.0.1",
+ "reflect-metadata": "^0.1.13",
+ "seedrandom": "^3.0.5",
+ "sequelize": "^6.9.0",
+ "sequelize-cursor-pagination": "^3.4.0",
+ "sequelize-nested-set": "^1.6.2",
+ "sequelize-typescript": "^2.1.1",
+ "siwe": "^2.1.4",
+ "socket.io": "^4.8.1",
+ "string-progressbar": "^1.0.4",
+ "string-similarity-js": "^2.1.4",
+ "telegraf": "^4.16.3",
+ "twitter-api-sdk": "^1.2.1",
+ "useless-helpers": "^0.0.8",
+ "uuid": "^10.0.0",
+ "wechaty": "^1.20.2",
+ "ws": "^8.18.0",
+ "zksync": "^0.13.1"
+ "devDependencies": {
+ "@babel/cli": "^7.14.8",
+ "@babel/core": "^7.15.0",
+ "@babel/plugin-proposal-class-properties": "^7.14.5",
+ "@babel/plugin-proposal-decorators": "^7.16.0",
+ "@babel/preset-env": "^7.15.0",
+ "@babel/preset-typescript": "^7.15.0",
+ "@types/bn.js": "^5.1.5",
+ "@types/cookie-parser": "^1.4.2",
+ "@types/cron": "^2.0.1",
+ "@types/crypto-js": "^4.1.2",
+ "@types/express": "^4.17.13",
+ "@types/express-http-proxy": "^1.6.6",
+ "@types/js-yaml": "^4.0.5",
+ "@types/lodash": "^4.14.191",
+ "@types/markdown-escape": "^1.1.0",
+ "@types/mkdirp": "^1.0.2",
+ "@types/morgan": "^1.9.3",
+ "@types/multer": "^1.4.7",
+ "@types/node": "^16.11.6",
+ "@types/node-telegram-bot-api": "^0.61.8",
+ "@types/seedrandom": "^3.0.5",
+ "@types/uuid": "^8.3.1",
+ "@types/validator": "^13.6.6",
+ "@types/web3-provider-engine": "^14.0.1",
+ "@typescript-eslint/eslint-plugin": "^5.20.0",
+ "@typescript-eslint/parser": "^5.54.0",
+ "babel-plugin-module-resolver": "^4.1.0",
+ "eslint": "7",
+ "eslint-config-standard-with-typescript": "^20.0.0",
+ "eslint-plugin-import": "2",
+ "eslint-plugin-node": "11",
+ "eslint-plugin-prettier": "^3.3.1",
+ "eslint-plugin-promise": "4",
+ "prettier": "^2.2.1",
+ "typescript": "^4.3.5",
+ "typescript-eslint": "^0.0.1-alpha.0"
+ "optionalDependencies": {
+ "bufferutil": "^4.0.8"
+ }
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ 11
+</body>
+</html>
@@ -0,0 +1,22 @@
+import path from 'path'
+export const PORT = process.env.PORT
+export const DB_HOST = process.env.DB_HOST
+export const DB_PORT = process.env.DB_PORT
+export const DB_NAME = process.env.DB_NAME
+export const DB_USER = process.env.DB_USER
+export const DB_PASS = process.env.DB_PASS
+export const TG_BOT_TOKEN = process.env.TG_BOT_TOKEN
+export const DATA_SERVICR_BASE_URL = process.env.DATA_SERVICR_BASE_URL!
+export const DATA_SERVICR_USERNAME = process.env.DATA_SERVICR_USERNAME!
+export const DATA_SERVICR_PASSWORD = process.env.DATA_SERVICR_PASSWORD!
+export const BANANA_ADDRESS = process.env.BANANA_ADDRESS!
+export const STATIC_FILEPATH = process.env.STATIC_FILEPATH!
+export const STATIC_IMAGES_FILEPATH = path.join(STATIC_FILEPATH, '/images')
+export const STATIC_AUDIOS_FILEPATH = path.join(STATIC_FILEPATH, '/audios')
+export const X_BOT_CLIENT_ID = process.env.X_BOT_CLIENT_ID!
+export const X_BOT_CLIENT_SECRET = process.env.X_BOT_CLIENT_SECRET!
+export const MODIFY_TOKEN = process.env.MODIFY_TOKEN!
@@ -0,0 +1,6 @@
+import PingpongController from './pingpong'
+import XController from './x'
+import TweetController from './tweet'
+export * from './types'
+export { PingpongController, XController, TweetController }
@@ -0,0 +1,40 @@
+import { Request, RequestHandler, NextFunction, Router } from 'express'
+import { Controller } from '../types'
+import NotFoundException from '../../exceptions/NotFoundException'
+import jsonResponseMiddleware, {
+ JsonResponse,
+} from '../../middleware/jsonResponse.middleware'
+// import apiKeyMiddleware from '../../middleware/apikey.middleware'
+export default class PingpongController implements Controller {
+ public path = '/api/v1/pingpong'
+ public router = Router()
+ constructor() {
+ this.initializeRoutes()
+ private initializeRoutes(): void {
+ this.router.get(
+ '/:type',
+ // apiKeyMiddleware(),
+ jsonResponseMiddleware,
+ this.ping as RequestHandler
+ )
+ private ping(
+ request: Request,
+ response: JsonResponse<{ type: string }>,
+ next: NextFunction
+ ): void {
+ const type = request.params.type
+ if (type === 'ping') {
+ response.jsonSuccess({ type })
+ } else if (type.startsWith('ping-')) {
+ } else {
+ next(new NotFoundException('Type'))
@@ -0,0 +1,110 @@
+import twitterService, { GenerateResult } from '../../services/twitterService'
+import { PaginationConnection } from 'sequelize-cursor-pagination'
+import { TweetData } from '../../db/models/Tweet'
+import { MODIFY_TOKEN } from '../../constants'
+interface ImportPayload {
+ token: string
+ urls: string[]
+interface TopPayload {
+ statusId: string
+ top?: boolean
+export default class TweetController implements Controller {
+ public path = '/api/v1/tweet'
+ '/list',
+ this.list as RequestHandler
+ this.router.post(
+ '/import',
+ this.import as RequestHandler
+ '/top',
+ this.top as RequestHandler
+ private list(
+ request: Request<any, any, any, { after?: string }>,
+ response: JsonResponse<PaginationConnection<TweetData>>,
+ const after = request.query.after
+ twitterService
+ .paginateTweetByOrder(after)
+ .then((tweets) => {
+ response.jsonSuccess(tweets)
+ })
+ .catch((e) => {
+ response.status(500).jsonError('Server Error', 1010)
+ private import(
+ request: Request<any, any, ImportPayload>,
+ response: JsonResponse<GenerateResult[]>,
+ const { urls, token } = request.body
+ if (token !== MODIFY_TOKEN) {
+ response.status(401).jsonError('Unauthorized', 1012)
+ return
+ .bulkGenerateTweetsByPublishAPI(urls)
+ response.status(500).jsonError('Server Error', 1011)
+ private top(
+ request: Request<any, any, TopPayload>,
+ response: JsonResponse<boolean>,
+ const { statusId, top, token } = request.body
+ .makeTweetTop(statusId, top)
+ .then(() => {
+ response.jsonSuccess(true)
+ response.status(500).jsonError('Server Error', 1014)
@@ -0,0 +1,18 @@
+import { Router } from 'express'
+export interface Controller {
+ path: string
+ router: Router
+export interface Pagination {
+ page: string
+ count: string
+export interface PaginationResponse<T> {
+ data: T[]
+ total: number
+ page: number
+ count: number
@@ -0,0 +1,71 @@
+import {
+ Request,
+ Response,
+ RequestHandler,
+ NextFunction,
+ Router,
+} from 'express'
+import twitterService from '../../services/twitterService'
+export default class XController implements Controller {
+ public path = '/api/v1/x'
+ this.router.get('/callback', this.callback as RequestHandler)
+ this.router.get('/login', this.login as RequestHandler)
+ private callback(
+ response: Response,
+ const { code, state } = request.query
+ .requestAccessToken(code as string, state as string)
+ response.status(200).send('ok')
+ .catch((e: any) => {
+ response
+ .status(500)
+ .send(
+ `Failed to get access token: ${
+ (e.message as string) ?? 'unknown error'
+ }`
+ private login(
+ .generateAuthURL()
+ .then((authUrl) => {
+ response.redirect(authUrl)
+ `Failed to generate auth URL: ${
@@ -0,0 +1,29 @@
+import { Sequelize } from 'sequelize-typescript'
+import * as Models from './models'
+import { RPC } from '../utils/EVMHelper/dbModels'
+import { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS } from '../constants'
+// import { promisify } from 'util'
+const { Constant, Application, Tweet } = Models
+const sequelize = new Sequelize({
+ host: DB_HOST,
+ port: DB_PORT ? Number.parseInt(DB_PORT) : 3306,
+ database: DB_NAME,
+ dialect: 'mariadb',
+ username: DB_USER,
+ password: DB_PASS,
+ dialectOptions: {
+ timezone: '+00:00',
+ allowPublicKeyRetrieval: true,
+ logging: false,
+ pool: {
+ max: 25,
+ models: [Constant, RPC, Application, Tweet],
+})
+export default sequelize
+export { Models }
@@ -0,0 +1,44 @@
+ Table,
+ Column,
+ AllowNull,
+ DataType,
+ Model,
+ Default,
+} from 'sequelize-typescript'
+import nanoid from '../../utils/uuid'
+export interface ApplicationUuidFormat {
+ alphabet: string
+ size: number
+@Table({
+ modelName: 'application',
+ indexes: [
+ {
+ fields: ['name'],
+ unique: true,
+export default class Application extends Model {
+ @AllowNull(false)
+ @Column(DataType.CHAR(30))
+ get name(): string {
+ return this.getDataValue('name')
+ @Column(DataType.CHAR(16))
+ get apikey(): string {
+ return this.getDataValue('apikey')
+ @Default(false)
+ @Column(DataType.BOOLEAN)
+ get disabled(): boolean {
+ return this.getDataValue('disabled')
@@ -0,0 +1,223 @@
+ Unique,
+import { ConstantData, ConstantKind, ConstantType } from './types'
+export type ConstantNames =
+ | 'singleTxBananaLimit'
+ | 'sleepTime'
+ | 'tradeLimitMax'
+ | 'tradeLimitMin'
+ | 'xBotToken'
+export type ConstantDataValue = string | number | boolean | object
+export interface BindedUser {
+ id: string
+ name: string
+/* eslint-disable */
+type NameMap<T> =
+ T extends 'singleTxBananaLimit' ? string :
+ // string like '[1,2,3]' for endpoint table id
+ T extends 'sleepTime' ? number :
+ T extends 'tradeLimitMax' ? number :
+ T extends 'tradeLimitMin' ? number :
+ T extends 'xBotToken' ? object :
+ never;
+/* eslint-enable */
+ modelName: 'constant',
+export default class Constant extends Model {
+ @Unique
+ @Column(DataType.STRING(50))
+ @Default('parameter')
+ @Column(DataType.ENUM('parameter', 'information', 'ui'))
+ get kind(): ConstantKind {
+ return this.getDataValue('kind')
+ @Default('string')
+ @Column(DataType.ENUM('decimal', 'string', 'boolean', 'json'))
+ get type(): ConstantType {
+ return this.getDataValue('type')
+ @Column(DataType.DECIMAL(32, 16))
+ get dataDecimal(): number | null {
+ return this.getDataValue('dataDecimal')
+ @Column(DataType.STRING(5000))
+ get dataString(): string | null {
+ return this.getDataValue('dataString')
+ get dataBoolean(): boolean | null {
+ return this.getDataValue('dataBoolean')
+ @Column(DataType.JSON)
+ get dataJson(): object | null {
+ return this.getDataValue('dataJson')
+ get memo(): string | null {
+ return this.getDataValue('memo')
+ get readOnly(): boolean {
+ return this.getDataValue('readOnly')
+ getConstantValue(): ConstantData {
+ let value: ConstantDataValue | null | undefined
+ switch (this.type) {
+ case 'decimal':
+ value = this.dataDecimal
+ break
+ case 'string':
+ value = this.dataString
+ case 'boolean':
+ value = this.dataBoolean
+ case 'json':
+ value = this.dataJson
+ default:
+ const ret: ConstantData = {
+ name: this.name,
+ kind: this.kind,
+ type: this.type,
+ memo: this.memo,
+ readOnly: this.readOnly,
+ value,
+ return ret
+ setConstantValue(value: ConstantDataValue | null): void {
+ this.setDataValue('dataDecimal', value)
+ this.setDataValue('dataString', value)
+ this.setDataValue('dataBoolean', value)
+ this.setDataValue('dataJson', value)
+ static async findByName(name: string): Promise<Constant | null> {
+ return await Constant.findOne({
+ where: { name },
+ static async findValueByName(
+ name: string,
+ defaults?: ConstantDataValue | null
+ ): Promise<ConstantDataValue | null | undefined> {
+ const ins = await Constant.findOne({
+ if (!ins) return defaults ?? null
+ const v = ins.getConstantValue()
+ return v.value
+ static async findOrCreatePyName(
+ defaultValue: ConstantDataValue,
+ kind: ConstantKind = 'parameter'
+ ): Promise<[Constant, boolean]> {
+ const payload: any = {
+ name,
+ kind,
+ switch (typeof defaultValue) {
+ case 'number':
+ payload.type = 'decimal'
+ payload.dataDecimal = defaultValue
+ payload.type = 'string'
+ payload.dataString = defaultValue
+ payload.type = 'boolean'
+ payload.dataBoolean = defaultValue
+ case 'object':
+ payload.type = 'json'
+ payload.dataJson = defaultValue
+ return await Constant.findOrCreate({
+ defaults: payload,
+ static async upsertPyName(
+ value: ConstantDataValue,
+ ): Promise<Constant> {
+ const [c, created] = await Constant.findOrCreatePyName(name, value, kind)
+ if (created) {
+ return c
+ c.setConstantValue(value)
+ await c.save()
+ await c.reload()
+ // eslint-disable-next-line prettier/prettier
+ static async get<T extends ConstantNames>(name: T): Promise<NameMap<T> | null> {
+ const value = await Constant.findValueByName(name)
+ return value as NameMap<T> | null
+ static async set<T extends ConstantNames>(name: T, value: NameMap<T>): Promise<Constant> {
+ const constant = await Constant.upsertPyName(name, value)
+ return constant
@@ -0,0 +1,83 @@
+export interface TweetData {
+ username: string
+ url: string
+ cleanHTML: string
+ body: object
+ order: number
+ modelName: 'tweet',
+ fields: ['statusId'],
+export default class Tweet extends Model {
+ @Column(DataType.CHAR(255))
+ get username(): string {
+ return this.getDataValue('username')
+ get statusId(): string {
+ return this.getDataValue('statusId')
+ @Column(DataType.TEXT)
+ get url(): string {
+ return this.getDataValue('url')
+ get cleanHTML(): string {
+ return this.getDataValue('cleanHTML')
+ get body(): object {
+ return this.getDataValue('body')
+ @Default(0)
+ @Column(DataType.INTEGER.UNSIGNED)
+ get order(): number {
+ return this.getDataValue('order')
+ getData(): TweetData {
+ return {
+ username: this.username,
+ statusId: this.statusId,
+ url: this.url,
+ cleanHTML: this.cleanHTML,
+ body: this.body,
+ order: this.order,
@@ -0,0 +1,5 @@
+import Constant from './Constant'
+import Application from './Application'
+import Tweet from './Tweet'
+export { Constant, Application, Tweet }
+export type ConstantKind = 'parameter' | 'information' | 'ui'
+export type ConstantType = 'decimal' | 'string' | 'boolean' | 'json'
+export interface ConstantData {
+ kind: ConstantKind
+ type: ConstantType
+ memo: string | null
+ readOnly: boolean
+ value: string | boolean | number | object | null | undefined
+export class NotImplementException extends Error {
+ super('not implement')
+import HttpException from './HttpException'
+export class DatabaseException extends HttpException {
+ constructor(message = 'database error') {
+ super(500, message)
+export class DatabaseCreateException extends DatabaseException {
+ constructor(id = 'Entity', extra?: string) {
+ super(`${id} create fail${extra ? ': ' + extra : ''}`)
+export class DatabaseModifyException extends DatabaseException {
+ super(`${id} modify fail${extra ? ': ' + extra : ''}`)
+export class DatabaseDeleteException extends DatabaseException {
+ super(`${id} delete fail${extra ? ': ' + extra : ''}`)
+export class DatabaseQueryException extends DatabaseException {
+ constructor(message = 'database query error') {
+ super(message)
@@ -0,0 +1,30 @@
+export class EndpointException extends HttpException {
+ constructor(public readonly code: number, message: string, status = 401) {
+ super(status, message)
+export class EndpointNotFoundException extends EndpointException {
+ super(1000, 'endpoint info not found')
+export class EndpointVersionExpiredFoundException extends EndpointException {
+ super(1001, 'endpoint version expired')
+export class EndpointCurrentVersionNotFoundException extends EndpointException {
+ super(1002, 'current version not found')
+export class EndpointVersionNotValidException extends EndpointException {
+ super(1003, 'endpoint version not valid')
+class HttpException extends Error {
+ status: number
+ message: string
+ constructor(status: number, message: string) {
+ this.status = status
+ this.message = message
+export default HttpException
@@ -0,0 +1,9 @@
+class NotFoundException extends HttpException {
+ constructor(id = 'Entity') {
+ super(404, `${id} not found`)
+export default NotFoundException
@@ -0,0 +1,19 @@
+export class UserException extends HttpException {
+ constructor(message = 'user error', code = 500) {
+ super(code, message)
+export class NeedUserException extends UserException {
+ super('need user', 401)
+export class UserVerifyFailException extends UserException {
+ constructor(account = 'Account', extra?: string) {
+ super(`${account} verify fail${extra ? ': ' + extra : ''}`, 401)
@@ -0,0 +1,155 @@
+import sequelize from './db'
+import express, { Request } from 'express'
+import cookieParser from 'cookie-parser'
+import logger from 'morgan'
+import helmet from 'helmet'
+import { Controller } from './controllers'
+import errorMiddleware from './middleware/error.middleware'
+import setHeaderMiddleware from './middleware/setHeaders.middleware'
+import { Server } from 'http'
+import 'moment-timezone'
+// import { Constant } from './db/models'
+// import { defaultSolanaHelper } from './utils/SolanaHelper'
+const deleteRequestBodySaver = function (
+ req: Request,
+ res: any,
+ buf: any,
+ encoding: any
+): void {
+ if (buf?.length && req.method.toLocaleUpperCase() === 'DELETE') {
+ try {
+ const str = buf.toString(encoding || 'utf8')
+ req.body = JSON.parse(str)
+ } catch (error) {}
+export interface APPOtions {
+ port: number
+ controllers: Controller[]
+ tgBotToken: string
+const defaultOptions: APPOtions = {
+ port: 9099,
+ controllers: [],
+ tgBotToken: process.env.TG_BOT_TOKEN ?? '',
+export default class APP {
+ private readonly option
+ public app: express.Application
+ private db?: Sequelize
+ private server?: Server
+ constructor(options: Partial<APPOtions>) {
+ this.option = {
+ ...defaultOptions,
+ ...options,
+ this.app = express()
+ process.once('SIGINT', () => {
+ this.destory().catch((e) => console.log(e))
+ process.once('SIGTERM', () => {
+ public async start(): Promise<void> {
+ this.listen()
+ // await this.bot.start()
+ private listen(): void {
+ this.server = this.app.listen(this.option.port, () => {
+ console.log(`App listening on the port ${this.option.port}`)
+ public async destory(): Promise<void> {
+ if (this.server) {
+ this.server.close()
+ if (this.db) {
+ await this.db.close()
+ process.exit(0)
+ async init(): Promise<void> {
+ this.initializeConfig()
+ this.initializeMiddlewares()
+ this.initializeControllers()
+ this.initializeErrorHandling()
+ await this.initializeDatabase()
+ await this.initializeConstants()
+ // await defaultSolanaHelper.getProvider()
+ private initializeConfig(): void {
+ // 设置密钥
+ this.app.set('jwtTokenSecret', process.env.JWT_SECRET ?? '666')
+ // view engine setup
+ this.app.set('views', path.join(__dirname, 'views'))
+ this.app.set('view engine', 'ejs')
+ private initializeMiddlewares(): void {
+ this.app.use(
+ helmet({
+ contentSecurityPolicy: false,
+ this.app.use(logger('dev'))
+ this.app.use(cookieParser(process.env.COOKIE_SECRET))
+ this.app.use(express.json({ verify: deleteRequestBodySaver }))
+ this.app.use(express.urlencoded({ extended: true }))
+ express.static(path.join(__dirname, '../public'), {
+ setHeaders(res) {
+ res.set('Access-Control-Allow-Origin', '*')
+ this.app.use(setHeaderMiddleware)
+ private initializeControllers(): void {
+ const controllers = this.option.controllers
+ controllers.forEach((controller) => {
+ this.app.use(controller.path, controller.router)
+ private initializeErrorHandling(): void {
+ this.app.use((_req, res) => {
+ res.status(404)
+ res.send('This api does not exist!')
+ this.app.use(errorMiddleware)
+ private async initializeDatabase(force = false): Promise<void> {
+ this.db = await sequelize.sync({ force })
+ private async initializeConstants(): Promise<void> {
+ // const SolFeeReceiver = await Constant.get('SolFeeReceiver')
+ // if (!SolFeeReceiver) {
+ // throw new Error('SolFeeReceiver not set')
+ // }
+ // const SolFeeBps = await Constant.get('SolFeeBps')
+ // if (!SolFeeBps) {
+ // throw new Error('SolFeeBps not set')
+ // const bootEndpoints = await Constant.get('BootEndpoints')
+ // if (!bootEndpoints) {
+ // throw new Error('BootEndpoints not set')
@@ -0,0 +1,57 @@
+import { NextFunction, Request, Response } from 'express'
+import { verfiyApikey } from '../services/application.service'
+import { Application } from '../db/models'
+export interface ApplicationRequest extends Request {
+ authApplication: Application
+interface ApiKeyMiddlewareOptions {
+ header: string
+ allowNull: boolean
+const defaultOptions: ApiKeyMiddlewareOptions = {
+ header: 'x-api-key',
+ allowNull: false,
+const apiKeyMiddleware = (
+ options: Partial<ApiKeyMiddlewareOptions> = {}
+): ((request: Request, response: Response, next: NextFunction) => void) => {
+ const useOptions = { ...defaultOptions, ...options }
+ return (request: Request, response: Response, next: NextFunction) => {
+ const token = request.headers[useOptions.header]
+ if (!token && useOptions.allowNull) {
+ next()
+ if (!token) {
+ response.status(401).end('API-KEY needed')
+ verfiyApikey(token as string)
+ .then((app) => {
+ if (!app) {
+ response.status(401).end('Application not exsit')
+ // eslint-disable-next-line @typescript-eslint/no-extra-semi
+ ;(request as ApplicationRequest).authApplication = app
+ .catch(() => {
+ response.status(401).end('Find application failed')
+ } catch (err) {
+ response.status(401).end('Access token parse failed')
+export default apiKeyMiddleware
@@ -0,0 +1,21 @@
+import HttpException from '../exceptions/HttpException'
+function errorMiddleware(
+ error: HttpException,
+ _request: Request,
+ _next: NextFunction
+ const status = error.status || 500
+ const message = error.message || 'Something went wrong'
+ response.status(status).send({
+ success: false,
+ error: {
+ message,
+ code: status,
+export default errorMiddleware
@@ -0,0 +1,54 @@
+export interface JsonResponse<T> extends Response {
+ jsonSuccess: (data: T, token?: string) => void
+ jsonError: (message: string, code: string | number, data?: any) => void
+export interface JsonResponseData<T> {
+ success: boolean
+ data?: T
+ error?: {
+ code: string | number
+ token?: string
+function jsonResponseMiddleware<T>(
+ const resp = response as JsonResponse<T>
+ resp.jsonSuccess = (data: T, token?: string): void => {
+ const ret: JsonResponseData<T> = {
+ success: true,
+ data,
+ token,
+ response.json(ret)
+ resp.jsonError = (
+ message: string,
+ code: string | number,
+ data?: any
+ ): void => {
+ code,
+ } catch (error) {
+ next(error)
+export default jsonResponseMiddleware
@@ -0,0 +1,80 @@
+import jwt from 'jwt-simple'
+import applicationServices from '../services/application.service'
+import User, { RoleType, UserData } from '../db/models/User'
+import { JWTContent, verfiyJWTToken } from '../utils/jwt'
+export interface JWTRequest extends Request {
+ jwtData: JWTContent
+ jwtUser: User
+ jwtRoles: RoleType[]
+ jwtUserData: UserData
+export interface jwtMiddlewareOptions {
+ needVip: boolean
+ needAdmin: boolean
+const defaultOptions: jwtMiddlewareOptions = {
+ needVip: false,
+ needAdmin: false,
+const jwtMiddleware = (
+ options: Partial<jwtMiddlewareOptions> = {}
+ verfiyJWTToken(request, request.get('x-access-token'))
+ .then(async (decoded) => {
+ const [data, user] = await applicationServices.getUserData(decoded.uuid)
+ if (useOptions.needAdmin && !data.roles?.includes('alice')) {
+ throw new Error('Access token has expired, 1')
+ ;(request as JWTRequest).jwtRoles = data.roles ?? []
+ ;(request as JWTRequest).jwtData = decoded
+ ;(request as JWTRequest).jwtUser = user
+ ;(request as JWTRequest).jwtUserData = data
+ response.status(401).end(e.message)
+const jwtAllowNullMiddleware = (
+): void => {
+ const token = request.get('x-access-token')
+ if (!token || token.length < 10) {
+ // 解析
+ const { app } = request
+ const decoded = jwt.decode(token, app.get('jwtTokenSecret')) as JWTContent
+ // todo handle token here
+ if (decoded.exp <= Date.now()) {
+jwtMiddleware.allowNull = jwtAllowNullMiddleware
+export default jwtMiddleware
+const { DATACENTER } = process.env
+function setHeaderMiddleware(
+ response.header('SB-DATACENTER', DATACENTER)
+export default setHeaderMiddleware
@@ -0,0 +1,45 @@
+import multer from 'multer'
+import fs from 'fs'
+import { v4 } from 'uuid'
+import moment from 'moment'
+import { Request } from 'express'
+const memoryUpload = multer()
+const { UPLOAD_DIST } = process.env
+function getToday(): string {
+ return moment(new Date()).format('yyyyMMDD')
+const diskStorage = multer.diskStorage({
+ destination(req, file, cb) {
+ const dir = UPLOAD_DIST ?? ''
+ cb(null, dir)
+ filename(req, file, cb) {
+ const today = getToday()
+ const dir = `${UPLOAD_DIST ?? ''}/${today}`
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir)
+ const { originalname } = file
+ const fix: string[] = originalname.split('.')
+ const newName = fix.length === 0 ? v4() : `${v4()}.${fix[fix.length - 1]}`
+ cb(null, `${today}/${newName}`)
+const diskUpload = multer({ storage: diskStorage })
+const uploadMiddleware = {
+ memory: memoryUpload,
+ disk: diskUpload,
+export default uploadMiddleware
+export interface FileRequest extends Request {
+ file: Express.Multer.File
@@ -0,0 +1,33 @@
+import parseFingerprint, { Fingerprint } from '../utils/fingerprint'
+import { stringifyIP } from '../utils/ip'
+export interface SacteTokenInfo {
+ jwt: string
+ fingerprint: Fingerprint | null
+ ip: string | null
+ userAgent: string | null
+export function getTobotoToken(request: Request): SacteTokenInfo | null {
+ // format: [x.y.z](jwt)[.0..0](base64 fingerprint)
+ if (!token || token.length < 10) return null
+ const arr = token.split('.')
+ if (arr.length < 3) return null
+ const jwt = arr.slice(0, 3).join('.')
+ const ret: SacteTokenInfo = {
+ jwt,
+ fingerprint: null,
+ ip: null,
+ userAgent: request.headers['user-agent'] ?? null,
+ if (arr[3]) {
+ ret.fingerprint = parseFingerprint(atob(arr[3]))
+ if (request.headers['x-forwarded-for']) {
+ // format 255.255.255.255
+ ret.ip = stringifyIP(request.headers['x-forwarded-for'] as string)
@@ -0,0 +1,13 @@
+import Decimal from 'decimal.js-light'
+import sequelize from '../db'
+Decimal.set({ toExpPos: 999, toExpNeg: -999, precision: 64 })
+export default async function env(task: () => Promise<void>): Promise<void> {
+ const db = await sequelize.sync()
+ await task()
+ console.log('done!')
+ await db.close()
@@ -0,0 +1,35 @@
+import { Client, auth } from 'twitter-api-sdk'
+import { X_BOT_CLIENT_ID, X_BOT_CLIENT_SECRET } from '../../constants'
+import { Constant } from '../../db/models'
+// Initialize auth client first
+let authClient: auth.OAuth2User
+export async function getAuthClient(): Promise<auth.OAuth2User> {
+ if (authClient) return authClient
+ const token = await Constant.get('xBotToken')
+ if (token) {
+ authClient = new auth.OAuth2User({
+ client_id: X_BOT_CLIENT_ID,
+ client_secret: X_BOT_CLIENT_SECRET,
+ callback: 'http://localhost:8089/api/v1/x/callback',
+ scopes: ['tweet.read', 'users.read', 'offline.access'],
+ return authClient
+export async function getTwitterClient(): Promise<Client> {
+ const authClient = await getAuthClient()
+ return new Client(authClient)
@@ -0,0 +1,198 @@
+import { Constant, Tweet } from '../../db/models'
+import axios from 'axios'
+import { getAuthClient, getTwitterClient } from './clients'
+ makePaginate,
+ PaginateOptions,
+ PaginationConnection,
+} from 'sequelize-cursor-pagination'
+const STATE = 'banana'
+async function generateAuthURL(): Promise<string> {
+ const authUrl = authClient.generateAuthURL({
+ state: STATE,
+ code_challenge_method: 's256',
+ return authUrl
+async function saveAccessToken(token: any): Promise<void> {
+ await Constant.set('xBotToken', token)
+async function requestAccessToken(code: string, state: string): Promise<void> {
+ if (state !== STATE) {
+ throw new Error("State isn't matching")
+ const token = await authClient.requestAccessToken(code)
+ await saveAccessToken(token.token)
+async function refreshAccessToken(): Promise<void> {
+ const token = await authClient.refreshAccessToken()
+const tweetUrlReg =
+ /^https:\/\/x\.com\/[a-zA-Z0-9_]+\/status\/([0-9]+)(\?=.*)?$/
+const removeScriptReg = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
+async function genenrateTweetByPublishAPI(url: string): Promise<Tweet> {
+ const match = url.match(tweetUrlReg)
+ if (!match) {
+ throw new Error('Invalid tweet URL')
+ const statusId = match[1]
+ const resp = await axios.get('https://publish.twitter.com/oembed', {
+ params: {
+ url,
+ partner: '',
+ hideConversation: 'on',
+ hide_thread: 'true',
+ omit_script: 'false',
+ const data = resp.data
+ if (!data.html || !data.author_name) {
+ throw new Error('Failed to fetch tweet HTML')
+ const cleanHTML = data.html.replace(removeScriptReg, '').trim()
+ const [tweet, created] = await Tweet.findOrCreate({
+ where: {
+ statusId,
+ defaults: {
+ username: data.author_name,
+ body: data,
+ cleanHTML,
+ if (!created) {
+ tweet.setDataValue('username', data.author_name)
+ tweet.setDataValue('url', url)
+ tweet.setDataValue('body', data)
+ tweet.setDataValue('cleanHTML', cleanHTML)
+ await tweet.save()
+ return tweet
+ } catch (error: any) {
+ if (error.response?.data?.error) {
+ throw new Error(error.response.data.error)
+ throw error
+async function updateTweet(statusId: string): Promise<Tweet> {
+ const tweet = await Tweet.findOne({ where: { statusId } })
+ if (!tweet) {
+ throw new Error('Tweet not found')
+ return await genenrateTweetByPublishAPI(tweet.url)
+// console.log(Tweet.rawAttributes)
+let paginate: (
+ this: unknown,
+ queryOptions: PaginateOptions<Tweet>
+) => Promise<PaginationConnection<Tweet>>
+async function paginateTweetByOrder(
+ after?: string,
+ limit = 30
+): Promise<PaginationConnection<TweetData>> {
+ if (!paginate) {
+ paginate = makePaginate(Tweet)
+ const secondResult = await paginate({
+ order: [
+ ['order', 'DESC'],
+ ['id', 'DESC'],
+ limit,
+ after,
+ disabled: false,
+ const ret: PaginationConnection<TweetData> = {
+ ...secondResult,
+ edges: secondResult.edges.map((edge) => ({
+ cursor: edge.node.id,
+ node: edge.node.getData(),
+ })),
+export interface GenerateResult {
+ error?: string
+async function bulkGenerateTweetsByPublishAPI(
+): Promise<GenerateResult[]> {
+ const results: GenerateResult[] = []
+ for (const url of urls) {
+ const tweet = await genenrateTweetByPublishAPI(url)
+ results.push({ url, success: true })
+ } catch (e) {
+ results.push({
+ error: (e as Error).message ?? 'unknown error',
+ return results
+async function makeTweetTop(statusId: string, top = true): Promise<void> {
+ if (top) {
+ const maxOrder: number = await Tweet.max('order')
+ tweet.setDataValue('order', maxOrder ? maxOrder + 1 : 1)
+ tweet.setDataValue('order', 0)
+const twitterService = {
+ getTwitterClient,
+ generateAuthURL,
+ STATE,
+ genenrateTweetByPublishAPI,
+ requestAccessToken,
+ refreshAccessToken,
+ paginateTweetByOrder,
+ updateTweet,
+ bulkGenerateTweetsByPublishAPI,
+ makeTweetTop,
+export default twitterService
@@ -0,0 +1,32 @@
+import APP from '.'
+import { PingpongController, XController, TweetController } from './controllers'
+import { PORT, TG_BOT_TOKEN } from './constants'
+const app = new APP({
+ port: PORT ? parseInt(PORT) : undefined,
+ controllers: [
+ new PingpongController(),
+ new XController(),
+ new TweetController(),
+ tgBotToken: TG_BOT_TOKEN,
+app
+ .init()
+ .then(async () => {
+ await app.start()
+ console.log('server inited')
+ .catch(async (e) => {
+ console.log(e)
+ return await app.destory()
@@ -0,0 +1,10 @@
+// import sequelize from '../db'
+// export default async function env(task: () => Promise<void>): Promise<void> {
+// const db = await sequelize.sync()
+// await task()
+// console.log('done!')
+// await db.close()
@@ -0,0 +1,147 @@
+ DefaultEVMHelperChainRPCs,
+ DefaultEVMHelperExplorerURLs,
+ DefaultEVMHelperScanAPIUrls,
+ SupportedEVMHelperChains,
+} from './types'
+import { RPC } from './dbModels'
+import { ethers, JsonRpcProvider, Provider, TransactionReceipt, Wallet } from 'ethers6'
+function isPrivateKey(pk: string): boolean {
+ return pk.startsWith('0x') && pk.length === 66
+export default class EVMHelper {
+ constructor(readonly chain: SupportedEVMHelperChains) {}
+ get explorer(): string {
+ return DefaultEVMHelperExplorerURLs[this.chain]
+ async getRandomRPC(): Promise<[string, number]> {
+ const rpcs = await RPC.getRPCList(this.chain)
+ if (rpcs.length === 0) {
+ return [DefaultEVMHelperChainRPCs[this.chain], -1]
+ const rpc = rpcs[Math.floor(Math.random() * rpcs.length)]
+ return [rpc.rpc, rpc.id]
+ async getRpcProvider(rpc?: string): Promise<JsonRpcProvider> {
+ let usingRpcIndex = rpc ? -2 : -1
+ let usingRpc = rpc ?? ''
+ if (!rpc) {
+ const [rpc, id] = await this.getRandomRPC()
+ usingRpc = rpc
+ usingRpcIndex = id
+ const provider = new JsonRpcProvider(usingRpc)
+ ;(provider as any).__usingRpcIndex = usingRpcIndex
+ ;(provider as any).__usingRpc = usingRpc
+ return provider
+ generateWallet(provider?: null | Provider): Wallet {
+ return Wallet.createRandom(provider) as unknown as Wallet
+ getWallet(
+ rpcProvider: JsonRpcProvider,
+ privateKeyOrMnemonic: string
+ ): Wallet {
+ let privateKey = privateKeyOrMnemonic
+ // console.log(`Using private key: ${privateKey}`)
+ // console.log(privateKey[privateKey.length - 2])
+ if (!isPrivateKey(privateKey)) {
+ const w = ethers.Wallet.fromPhrase(privateKeyOrMnemonic, rpcProvider)
+ privateKey = w.privateKey
+ const wallet = new ethers.Wallet(privateKey, rpcProvider)
+ return wallet
+ async getRandomProviderWallet(privateKeyOrMnemonic: string): Promise<Wallet> {
+ const rpcProvider = await this.getRpcProvider()
+ return this.getWallet(rpcProvider, privateKeyOrMnemonic)
+ link(sub: string, hash: string): string {
+ return `${this.explorer}/${sub}/${hash}`
+ txLink(hash: string): string {
+ return this.link('tx', hash)
+ // https://api.bscscan.com/api?module=account&action=txlist&address=0xb62bf8d41fe27622924971887d8f482b00e50660&startblock=0&endblock=99999999&page=1&offset=10&sort=desc
+ async getNormalTransactionsByAddress(
+ address: string,
+ apikey?: string,
+ limit = 10
+ ): Promise<TransactionReceipt[]> {
+ const scanAPI = DefaultEVMHelperScanAPIUrls[this.chain]
+ if (!scanAPI) {
+ throw new Error(`Scan API URL for ${this.chain} not found`)
+ const url = `${scanAPI}/api?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc${
+ apikey ? `&apikey=${apikey}` : ''
+ const res = await axios.get(url)
+ const data = res.data
+ if (!data.result) {
+ throw new Error(`Failed to get transactions for ${address}`)
+ const ret: TransactionReceipt[] = []
+ // {
+ // "blockNumber": "29985367",
+ // "blockHash": "0x910b9a716137b5cabdfa49b5934ea8f7a1867fb3ba1ec178a202d2fb89079a80",
+ // "timeStamp": "1689427357",
+ // "hash": "0x0a7c9fd6aea3347e67dbb51a2f24ca604a0498e0d2024691aee7a5fcd44ee699",
+ // "nonce": "12",
+ // "transactionIndex": "104",
+ // "from": "0xb62bf8d41fe27622924971887d8f482b00e50660",
+ // "to": "0xd10d9d9a9a9a8f872ea34608735827ca88f13b2d",
+ // "value": "8326109000000000",
+ // "gas": "21000",
+ // "gasPrice": "3000000000",
+ // "input": "0x",
+ // "methodId": "0x",
+ // "functionName": "",
+ // "contractAddress": "",
+ // "cumulativeGasUsed": "15084533",
+ // "txreceipt_status": "1",
+ // "gasUsed": "21000",
+ // "confirmations": "724",
+ // "isError": "0"
+ const provider = await this.getRpcProvider()
+ if (data.status !== '1') {
+ throw new Error(data.result)
+ data.result.forEach((scanTx: any) => {
+ const tx = new TransactionReceipt(
+ blockNumber: parseInt(scanTx.blockNumber),
+ blockHash: scanTx.blockHash,
+ hash: scanTx.hash,
+ to: scanTx.to,
+ from: scanTx.from,
+ index: parseInt(scanTx.transactionIndex),
+ gasUsed: scanTx.gasUsed,
+ gasPrice: scanTx.gasPrice,
+ cumulativeGasUsed: scanTx.cumulativeGasUsed,
+ status: parseInt(scanTx.txreceipt_status),
+ contractAddress: null,
+ logsBloom: '0x0',
+ logs: [],
+ type: 0,
+ root: null,
+ provider
+ ret.push(tx)
@@ -0,0 +1,181 @@
+[
+ "inputs": [
+ "internalType": "address",
+ "name": "token",
+ "type": "address"
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ "name": "asset",
+ "internalType": "uint256",
+ "name": "amount",
+ "type": "uint256"
+ "name": "interestRateMode",
+ "internalType": "uint16",
+ "name": "referralCode",
+ "type": "uint16"
+ "name": "onBehalfOf",
+ "name": "borrow",
+ "outputs": [],
+ "type": "function"
+ "name": "deposit",
+ "name": "addressIn",
+ "name": "getUserAccountData",
+ "outputs": [
+ "name": "totalCollateralETH",
+ "name": "totalDebtETH",
+ "name": "availableBorrowsETH",
+ "name": "currentLiquidationThreshold",
+ "name": "ltv",
+ "name": "healthFactor",
+ "stateMutability": "view",
+ "name": "rateMode",
+ "name": "repay",
+ "name": "",
+ "name": "to",
+ "name": "withdraw",
+]
@@ -0,0 +1,53 @@
+ JsonRpcSigner,
+ ContractTransactionResponse,
+ ContractTransactionReceipt,
+} from 'ethers'
+import { CONTRACT_CORECARD } from '../../../constants'
+import Erc721TokenContract from './erc721'
+import ABI from './MTCoreCardABI.json'
+export type MTCoreCardLevel = 0 | 1 | 2 | 3 | 4
+export default class MTCoreCardContract extends Erc721TokenContract {
+ constructor(provider: JsonRpcSigner) {
+ super(CONTRACT_CORECARD, provider)
+ this.ABI = ABI
+ async mint(
+ level: MTCoreCardLevel,
+ price: string,
+ address?: string
+ ): Promise<ContractTransactionReceipt> {
+ const useAddress = address ?? (await this.getCallerAddress())
+ const contract = this.getInstance()
+ const tx: ContractTransactionResponse = await contract.mint(
+ useAddress,
+ level,
+ value: price,
+ const txwait = await tx.wait()
+ if (!txwait) {
+ throw new Error('Transaction failed')
+ return txwait
+ async getMintPrice(level: MTCoreCardLevel): Promise<string> {
+ const bn = await contract.getMintPrice(level)
+ return bn.toString()
+ async getAllMintPrice(): Promise<[string, string, string, string, string]> {
+ const lv0 = await this.getMintPrice(0)
+ const lv1 = await this.getMintPrice(1)
+ const lv2 = await this.getMintPrice(2)
+ const lv3 = await this.getMintPrice(3)
+ const lv4 = await this.getMintPrice(4)
+ return [lv0, lv1, lv2, lv3, lv4]
@@ -0,0 +1,677 @@
+ "internalType": "string",
+ "name": "name_",
+ "type": "string"
+ "name": "symbol_",
+ "name": "baseURI_",
+ "anonymous": false,
+ "indexed": true,
+ "name": "owner",
+ "name": "approved",
+ "name": "tokenId",
+ "name": "Approval",
+ "type": "event"
+ "name": "operator",
+ "indexed": false,
+ "internalType": "bool",
+ "type": "bool"
+ "name": "ApprovalForAll",
+ "internalType": "enum MTPowerCard.Grade",
+ "name": "grade",
+ "type": "uint8"
+ "name": "PowerCardMinted",
+ "internalType": "bytes32",
+ "name": "role",
+ "type": "bytes32"
+ "name": "previousAdminRole",
+ "name": "newAdminRole",
+ "name": "RoleAdminChanged",
+ "name": "account",
+ "name": "sender",
+ "name": "RoleGranted",
+ "name": "RoleRevoked",
+ "name": "from",
+ "name": "Transfer",
+ "inputs": [],
+ "name": "BURNER_ROLE",
+ "name": "DEFAULT_ADMIN_ROLE",
+ "name": "approve",
+ "name": "balanceOf",
+ "name": "burn",
+ "name": "getApproved",
+ "name": "getRoleAdmin",
+ "name": "grantRole",
+ "name": "hasRole",
+ "name": "isApprovedForAll",
+ "name": "mint",
+ "stateMutability": "payable",
+ "name": "getMintPrice",
+ "name": "name",
+ "name": "ownerOf",
+ "name": "renounceRole",
+ "name": "revokeRole",
+ "name": "safeTransferFrom",
+ "internalType": "bytes",
+ "name": "data",
+ "type": "bytes"
+ "name": "setApprovalForAll",
+ "name": "setBaseURI",
+ "name": "burner",
+ "name": "setBurner",
+ "name": "price",
+ "name": "setPrice",
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4"
+ "name": "supportsInterface",
+ "name": "symbol",
+ "name": "tokenURI",
+ "name": "transferFrom",
+import TobotoOGFriendABI from './TobotoOGFriendABI.json'
+ ethers,
+import WrappedContract from './base'
+// import { tokenHumanAmount } from '../../../utils'
+// import dayjs from 'dayjs'
+export default class TobotoOGFriendContract extends WrappedContract {
+ constructor(address: string, provider: ethers.Signer) {
+ super(TobotoOGFriendABI, address, provider)
+ getInstance(): ethers.Contract {
+ return new ethers.Contract(this.address, this.ABI as any, this.provider)
+ async hasClaimed(address: string): Promise<boolean> {
+ const bn = await contract.hasClaimed(address)
+ return bn
+ async claim(proof: string[]): Promise<ContractTransactionReceipt> {
+ const tx: ContractTransactionResponse = await contract.claim(proof)
@@ -0,0 +1,747 @@
+ "name": "max_number_",
+ "name": "ERC721EnumerableForbiddenBatchMint",
+ "type": "error"
+ "name": "ERC721IncorrectOwner",
+ "name": "ERC721InsufficientApproval",
+ "name": "approver",
+ "name": "ERC721InvalidApprover",
+ "name": "ERC721InvalidOperator",
+ "name": "ERC721InvalidOwner",
+ "name": "receiver",
+ "name": "ERC721InvalidReceiver",
+ "name": "ERC721InvalidSender",
+ "name": "ERC721NonexistentToken",
+ "name": "index",
+ "name": "ERC721OutOfBoundsIndex",
+ "name": "OwnableInvalidOwner",
+ "name": "OwnableUnauthorizedAccount",
+ "name": "user",
+ "name": "Mint",
+ "name": "previousOwner",
+ "name": "newOwner",
+ "name": "OwnershipTransferred",
+ "name": "MAX_NUMBER",
+ "name": "baseURI",
+ "internalType": "bytes32[]",
+ "name": "merkleProof_",
+ "type": "bytes32[]"
+ "name": "claim",
+ "name": "to_",
+ "name": "claimTo",
+ "name": "hasClaimed",
+ "name": "merkleRoot",
+ "name": "renounceOwnership",
+ "name": "tokenByIndex",
+ "name": "tokenOfOwnerByIndex",
+ "name": "totalSupply",
+ "name": "transferOwnership",
+ "name": "updateBaseURI",
+ "name": "merkleRoot_",
+ "name": "updateMerkleRoot",
+import miniABI from './UniswapV2PairMiniABI.json'
+import { ethers } from 'ethers'
+export default class UniswapV2PairMiniContract extends WrappedContract {
+ super(miniABI, address, provider)
+ async getReserves(): Promise<[string, string, string]> {
+ const [reserve0, reserve1, blockTimestampLast] =
+ await contract.getReserves()
+ return [
+ reserve0.toString(),
+ reserve1.toString(),
+ blockTimestampLast.toString(),
@@ -0,0 +1,27 @@
+ "constant": true,
+ "name": "getReserves",
+ "internalType": "uint112",
+ "name": "reserve0",
+ "type": "uint112"
+ "name": "reserve1",
+ "internalType": "uint32",
+ "name": "blockTimestampLast",
+ "type": "uint32"
+ "payable": false,
@@ -0,0 +1,58 @@
+import { CONNECT_CHAIN } from '../../../constants'
+import miniABI from './UniswapV2Route02MiniABI.json'
+export default class UniswapV2Route02MiniContract extends WrappedContract {
+ constructor(provider: ethers.Signer, address: string = CONNECT_CHAIN.uniswapV2Router02Address!) {
+ async signSwapExactTokensForTokens(amountIn: any, amountOutMin: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapExactTokensForTokens.populateTransaction(amountIn, amountOutMin, path, to, deadline)
+ return tx.data
+ async signSwapTokensForExactTokens(amountOut: any, amountInMax: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapTokensForExactTokens.populateTransaction(amountOut, amountInMax, path, to, deadline)
+ async signSwapExactETHForTokens(amountOutMin: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapExactETHForTokens.populateTransaction(amountOutMin, path, to, deadline)
+ async signSwapTokensForExactETH(amountOut: any, amountInMax: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapTokensForExactETH.populateTransaction(amountOut, amountInMax, path, to, deadline)
+ async signSwapExactTokensForETH(amountIn: any, amountOutMin: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapExactTokensForETH.populateTransaction(amountIn, amountOutMin, path, to, deadline)
+ async signSwapETHForExactTokens(amountOut: any, path: any, to: any, deadline: any): Promise<string> {
+ const tx = await contract.signSwapETHForExactTokens.populateTransaction(amountOut, path, to, deadline)
@@ -0,0 +1,320 @@
+ "name": "amountIn",
+ "name": "amountOutMin",
+ "internalType": "address[]",
+ "name": "path",
+ "type": "address[]"
+ "name": "deadline",
+ "name": "swapExactTokensForTokens",
+ "internalType": "uint256[]",
+ "name": "amounts",
+ "type": "uint256[]"
+ "name": "amountOut",
+ "name": "amountInMax",
+ "name": "swapTokensForExactTokens",
+ "name": "swapExactETHForTokens",
+ "name": "swapTokensForExactETH",
+ "name": "swapExactTokensForETH",
+ "name": "swapETHForExactTokens",
+ "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens",
+ "name": "swapExactETHForTokensSupportingFeeOnTransferTokens",
+ "name": "swapExactTokensForETHSupportingFeeOnTransferTokens",
@@ -0,0 +1,67 @@
+import miniABI from './UniswapV3PoolMiniABI.json'
+export interface UniswapV3Slot0 {
+ sqrtPriceX96: string
+ tick: number
+ observationIndex: string
+ observationCardinality: string
+ observationCardinalityNext: string
+ feeProtocol: string
+ unlocked: boolean
+export default class UniswapV3PoolMiniContract extends WrappedContract {
+ async token0(): Promise<string> {
+ const token0 = await contract.token0()
+ return token0
+ async token1(): Promise<string> {
+ const token1 = await contract.token1()
+ return token1
+ async fee(): Promise<number> {
+ const fee: bigint = await contract.fee()
+ return parseInt(fee.toString())
+ async tickSpacing(): Promise<string> {
+ const tickSpacing: bigint = await contract.tickSpacing()
+ return tickSpacing.toString()
+ async liquidity(): Promise<string> {
+ const liquidity: bigint = await contract.liquidity()
+ return liquidity.toString()
+ async slot0(): Promise<UniswapV3Slot0> {
+ const slot0 = await contract.slot0()
+ sqrtPriceX96: slot0.sqrtPriceX96.toString(),
+ tick: parseInt(slot0.tick),
+ observationIndex: slot0.observationIndex.toString(),
+ observationCardinality: slot0.observationCardinality.toString(),
+ observationCardinalityNext: slot0.observationCardinalityNext.toString(),
+ feeProtocol: slot0.feeProtocol.toString(),
+ unlocked: slot0.unlocked,
@@ -0,0 +1,983 @@
+ "internalType": "int24",
+ "name": "tickLower",
+ "type": "int24"
+ "name": "tickUpper",
+ "internalType": "uint128",
+ "type": "uint128"
+ "name": "amount0",
+ "name": "amount1",
+ "name": "Burn",
+ "name": "recipient",
+ "name": "Collect",
+ "name": "CollectProtocol",
+ "name": "paid0",
+ "name": "paid1",
+ "name": "Flash",
+ "name": "observationCardinalityNextOld",
+ "name": "observationCardinalityNextNew",
+ "name": "IncreaseObservationCardinalityNext",
+ "internalType": "uint160",
+ "name": "sqrtPriceX96",
+ "type": "uint160"
+ "name": "tick",
+ "name": "Initialize",
+ "internalType": "uint8",
+ "name": "feeProtocol0Old",
+ "name": "feeProtocol1Old",
+ "name": "feeProtocol0New",
+ "name": "feeProtocol1New",
+ "name": "SetFeeProtocol",
+ "internalType": "int256",
+ "type": "int256"
+ "name": "liquidity",
+ "name": "Swap",
+ "name": "amount0Requested",
+ "name": "amount1Requested",
+ "name": "collect",
+ "name": "collectProtocol",
+ "name": "factory",
+ "name": "fee",
+ "internalType": "uint24",
+ "type": "uint24"
+ "name": "feeGrowthGlobal0X128",
+ "name": "feeGrowthGlobal1X128",
+ "name": "flash",
+ "name": "observationCardinalityNext",
+ "name": "increaseObservationCardinalityNext",
+ "name": "initialize",
+ "name": "maxLiquidityPerTick",
+ "name": "observations",
+ "name": "blockTimestamp",
+ "internalType": "int56",
+ "name": "tickCumulative",
+ "type": "int56"
+ "name": "secondsPerLiquidityCumulativeX128",
+ "name": "initialized",
+ "internalType": "uint32[]",
+ "name": "secondsAgos",
+ "type": "uint32[]"
+ "name": "observe",
+ "internalType": "int56[]",
+ "name": "tickCumulatives",
+ "type": "int56[]"
+ "internalType": "uint160[]",
+ "name": "secondsPerLiquidityCumulativeX128s",
+ "type": "uint160[]"
+ "name": "key",
+ "name": "positions",
+ "name": "_liquidity",
+ "name": "feeGrowthInside0LastX128",
+ "name": "feeGrowthInside1LastX128",
+ "name": "tokensOwed0",
+ "name": "tokensOwed1",
+ "name": "protocolFees",
+ "name": "token0",
+ "name": "token1",
+ "name": "feeProtocol0",
+ "name": "feeProtocol1",
+ "name": "setFeeProtocol",
+ "name": "slot0",
+ "name": "observationIndex",
+ "name": "observationCardinality",
+ "name": "feeProtocol",
+ "name": "unlocked",
+ "name": "snapshotCumulativesInside",
+ "name": "tickCumulativeInside",
+ "name": "secondsPerLiquidityInsideX128",
+ "name": "secondsInside",
+ "name": "zeroForOne",
+ "name": "amountSpecified",
+ "name": "sqrtPriceLimitX96",
+ "name": "swap",
+ "internalType": "int16",
+ "name": "wordPosition",
+ "type": "int16"
+ "name": "tickBitmap",
+ "name": "tickSpacing",
+ "name": "ticks",
+ "name": "liquidityGross",
+ "internalType": "int128",
+ "name": "liquidityNet",
+ "type": "int128"
+ "name": "feeGrowthOutside0X128",
+ "name": "feeGrowthOutside1X128",
+ "name": "tickCumulativeOutside",
+ "name": "secondsPerLiquidityOutsideX128",
+ "name": "secondsOutside",
@@ -0,0 +1,49 @@
+import ABI from './UniswapV3QuoterABI.json'
+export default class UniswapV3QuoterMiniContract extends WrappedContract {
+ super(ABI, address, provider)
+ async callQuoteExactInputSingle(
+ tokenIn: string,
+ tokenOut: string,
+ fee: number,
+ amountIn: string,
+ sqrtPriceLimitX96?: string
+ ): Promise<string> {
+ const bn = await contract.quoteExactInputSingle.staticCall(
+ tokenIn,
+ tokenOut,
+ fee,
+ amountIn,
+ sqrtPriceLimitX96 ?? 0
+ async callQuoteExactOutputSingle(
+ amountOut: string,
+ const bn = await contract.quoteExactOutputSingle.staticCall(
+ amountOut,
@@ -0,0 +1,193 @@
+ "name": "_factory",
+ "name": "_WETH9",
+ "name": "WETH9",
+ "name": "quoteExactInput",
+ "name": "tokenIn",
+ "name": "tokenOut",
+ "name": "quoteExactInputSingle",
+ "name": "quoteExactOutput",
+ "name": "quoteExactOutputSingle",
+ "name": "amount0Delta",
+ "name": "amount1Delta",
+ "name": "uniswapV3SwapCallback",
@@ -0,0 +1,43 @@
+import { BigNumber, ethers } from 'ethers'
+import AaveABI from './AaveABI.json'
+import { CONTRACT_AAVEMOCK } from '../../../constants'
+interface AaveAccountData {
+ totalCollateralETH: BigNumber
+ totalDebtETH: BigNumber
+ availableBorrowsETH: BigNumber
+ currentLiquidationThreshold: BigNumber
+ ltv: BigNumber
+ healthFactor: BigNumber
+export default class AaveContract extends WrappedContract {
+ constructor(provider: ethers.Signer) {
+ super(AaveABI, CONTRACT_AAVEMOCK, provider)
+ async getUserAccountData(efPoolAddress: string): Promise<AaveAccountData> {
+ const [
+ totalCollateralETH,
+ totalDebtETH,
+ availableBorrowsETH,
+ currentLiquidationThreshold,
+ ltv,
+ healthFactor,
+ ] = (await contract.getUserAccountData(efPoolAddress)) as BigNumber[]
@@ -0,0 +1,17 @@
+export default class WrappedContract {
+ protected ABI: any[]
+ protected address: string
+ protected provider: ethers.Signer
+ constructor(abi: any[], address: string, provider: ethers.Signer) {
+ this.ABI = abi
+ this.address = address
+ this.provider = provider
+ async getCallerAddress(): Promise<string> {
+ return await this.provider.getAddress()
@@ -0,0 +1,190 @@
+import erc20ABI from './erc20ABI.json'
+import { MAX_ETH_NUM } from '../../../constants'
+import ABI from './degenSwapABI.json'
+import { ChainId, Token, WETH9, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
+import { SWAP_ADDRESS, CONNECT_CHAIN, TOKEN_ADDRESS, WETH_ADDRESS } from '../../../constants'
+import UniswapV2PairMiniABI from './UniswapV2PairMiniABI.json'
+import UniswapV2Route02MiniABI from './UniswapV2Route02MiniABI.json'
+import { Pair, Route, Trade } from '@uniswap/v2-sdk'
+import { BigNumber } from '@ethersproject/bignumber'
+import DegenSwapTokenContract from './degenSwapToken'
+import { timeNumber } from '../../../utils/time'
+const DegenToken = new Token(CONNECT_CHAIN.chainId, TOKEN_ADDRESS, 18, 'DegenToken', 'DEGEN', false, BigNumber.from(30), BigNumber.from(30))
+const Weth = new Token(CONNECT_CHAIN.chainId, WETH_ADDRESS, 18, 'WETH', 'WETH', false, BigNumber.from(30), BigNumber.from(30))
+async function createPair(provider: ethers.Provider): Promise<Pair> {
+ // TODO: 不知道为啥出错
+ // const pairAddress = Pair.getAddress(Weth, DegenToken)
+ const pairAddress = (window as any).CONSTS.PAIR_ADDRESS
+ // Setup provider, import necessary ABI ...
+ const pairContract = new ethers.Contract(pairAddress, UniswapV2PairMiniABI, provider)
+ const reserves = await pairContract["getReserves"]()
+ const [reserve0, reserve1] = reserves
+ const tokens = [DegenToken, Weth]
+ const [token0, token1] = tokens[0].sortsBefore(tokens[1]) ? tokens : [tokens[1], tokens[0]]
+ const pair = new Pair(CurrencyAmount.fromRawAmount(token0, reserve0.toString()), CurrencyAmount.fromRawAmount(token1, reserve1.toString()))
+ return pair
+export interface PriceInfluence {
+ input: string
+ midPrice: string
+ executionPrice: string
+ influence: number
+export interface DegenBuyResult {
+ tx: ContractTransactionReceipt
+ critical: boolean
+export default class DegenSwapContract extends WrappedContract {
+ super(ABI, SWAP_ADDRESS, provider)
+ getUniInstance(): ethers.Contract {
+ return new ethers.Contract((window as any).CONSTS.V2_ROUTER_ADDRESS, UniswapV2Route02MiniABI, this.provider)
+ async getPoolETHBalance(): Promise<string> {
+ const balance = await this.provider.provider!.getBalance(this.address)
+ return balance.toString()
+ async buy(ethAmount: string): Promise<DegenBuyResult> {
+ const tx: ContractTransactionResponse = await contract.buy(
+ TOKEN_ADDRESS,
+ value: ethAmount,
+ const logs = txwait.logs.filter((log) => log.address.toLowerCase() === this.address.toLowerCase())
+ const event = logs.map((log) => contract.interface.parseLog(log)).filter((event) => event?.name === 'Critical')
+ let critical = false
+ if (event[0]) {
+ critical = event[0].args[0]
+ tx: txwait,
+ critical,
+ async sell(tokenAmount: string): Promise<ContractTransactionReceipt> {
+ const contract = this.getUniInstance()
+ const owner = await this.provider.getAddress()
+ const ts = Math.floor((new Date().getTime() + timeNumber.day * 30) / 1000)
+ const ttt = await contract.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(tokenAmount, '1', [DegenToken.address, Weth.address], owner, ts)
+ const price = await this.getExecutionSellPrice(tokenAmount)
+ const slippage = new Decimal(tokenAmount).mul(price).mul(0.7).toFixed(0)
+ const tx: ContractTransactionResponse = await contract.swapExactTokensForETHSupportingFeeOnTransferTokens(tokenAmount, slippage, [DegenToken.address, Weth.address], owner, ts)
+ async sellAllowance(): Promise<string> {
+ const uniContract = this.getUniInstance()
+ const tokenContract = new DegenSwapTokenContract(this.provider)
+ const spender = await uniContract.getAddress()
+ const bn = await tokenContract.allowance(owner, spender)
+ async sellApprove(): Promise<ContractTransactionReceipt> {
+ return await tokenContract.approve(spender, MAX_ETH_NUM)
+ async getBuyPrice(): Promise<string> {
+ const pair = await createPair(this.provider.provider!)
+ const route = new Route([pair], Weth, DegenToken)
+ return route.midPrice.toSignificant(10)
+ async getSellPrice(): Promise<string> {
+ const route = new Route([pair], DegenToken, Weth)
+ async getExecutionBuyPrice(ethAmount: string): Promise<string> {
+ const trade = new Trade(route, CurrencyAmount.fromRawAmount(Weth, ethAmount), TradeType.EXACT_INPUT)
+ return trade.executionPrice.toSignificant(10)
+ async getExecutionSellPrice(tokenAmount: string): Promise<string> {
+ const trade = new Trade(route, CurrencyAmount.fromRawAmount(DegenToken, tokenAmount), TradeType.EXACT_INPUT)
+ async getExecutionBuyPriceInfluence(ethAmount: string): Promise<PriceInfluence> {
+ const midPrice = await this.getBuyPrice()
+ const executionPrice = await this.getExecutionBuyPrice(ethAmount)
+ const influence = new Decimal(midPrice).div(new Decimal(executionPrice)).minus(1).toNumber()
+ input: ethAmount,
+ midPrice,
+ executionPrice,
+ influence,
+ async getExecutionSellPriceInfluence(ethAmount: string): Promise<PriceInfluence> {
+ const midPrice = await this.getSellPrice()
+ const executionPrice = await this.getExecutionSellPrice(ethAmount)
@@ -0,0 +1,164 @@
+ "name": "_token",
+ "name": "buy",
+ "name": "_uniswapRouter",
+ "name": "critical",
+ "name": "Critical",
+ "name": "_pair",
+ "name": "setTokenPairs",
+ "name": "tkn",
+ "name": "withdrawStuckTokens",
+ "type": "receive"
+ "name": "tokenPairs",
+ "name": "uniswapV2Router",
+ "internalType": "contract IUniswapV2Router02",
@@ -0,0 +1,76 @@
+import Erc20TokenContract from './erc20'
+import { TOKEN_ADDRESS } from '../../../constants'
+import { ContractTransactionReceipt, ContractTransactionResponse, ethers } from 'ethers'
+import ABI from './degenSwapTokenABI.json'
+export type DegenSwapTokenContractStatus = 'waiting' | 'minting' | 'swaping'
+export default class DegenSwapTokenContract extends Erc20TokenContract {
+ super(TOKEN_ADDRESS, provider)
+ async mint(): Promise<ContractTransactionReceipt> {
+ const value = await this.calculateMintPrice()
+ const tx: ContractTransactionResponse = await contract.mint({
+ async mintCount(address: string): Promise<number> {
+ const bn = await contract.mintCount(address)
+ return parseInt(bn)
+ async mintLimit(): Promise<number> {
+ const bn = await contract.mintLimit()
+ async amountPerMint(): Promise<string> {
+ const bn = await contract.amountPerMint() as bigint
+ async calculateMintPrice(): Promise<string> {
+ const bn = await contract.calculateMintPrice() as bigint
+ async tradingOpen(): Promise<boolean> {
+ const bn = await contract.tradingOpen() as boolean
+ async presaleOpen(): Promise<boolean> {
+ const bn = await contract.presaleOpen() as boolean
+ async status(): Promise<DegenSwapTokenContractStatus> {
+ const presaleOpen = await this.presaleOpen()
+ const tradingOpen = await this.tradingOpen()
+ if (tradingOpen) return 'swaping'
+ if (presaleOpen) return 'minting'
+ return 'waiting'
+ static getSymbol(): string {
+ return (window as any).CONSTS.SYMBOL
@@ -0,0 +1,686 @@
+ "name": "_degenswapRouter",
+ "name": "_name",
+ "name": "_symbol",
+ "internalType": "UD60x18",
+ "name": "x",
+ "name": "PRBMath_UD60x18_Exp2_InputTooBig",
+ "name": "ReentrancyGuardReentrantCall",
+ "name": "spender",
+ "name": "value",
+ "name": "allowance",
+ "name": "amountPerMint",
+ "name": "automatedMarketMakerPairs",
+ "name": "buyTax",
+ "name": "calculateMintPrice",
+ "name": "result",
+ "name": "decimals",
+ "name": "subtractedValue",
+ "name": "decreaseAllowance",
+ "name": "degenSwapRouter",
+ "name": "excluded",
+ "name": "excludeFromFees",
+ "name": "inSwap",
+ "name": "addedValue",
+ "name": "increaseAllowance",
+ "name": "initPrice",
+ "name": "isExcludedFromFees",
+ "name": "manualSwap",
+ "name": "mintCount",
+ "name": "mintLimit",
+ "name": "minted",
+ "name": "openTrading",
+ "name": "presaleOpen",
+ "name": "sellTax",
+ "name": "pair",
+ "name": "setAutomatedMarketMakerPair",
+ "name": "startSale",
+ "name": "targetCount",
+ "name": "tradingOpen",
+ "name": "transfer",
+ "name": "uniswapV2Pair",
+import { TOKEN_ADDRESS, ZERO_ADDRESS } from '../../../constants'
+import { TransactionReceipt, ContractTransactionResponse, ethers, solidityPackedKeccak256 } from 'ethers'
+import ABI from './degenSwapTokenV2ABI.json'
+import { awaitConfirmations } from '../../../utils/evm'
+export type DepositTimeState = 'notStarted' | 'ongoing' | 'ended'
+// 邀请是1,ido是2,退款是3
+export enum ClaimCheckType {
+ referral = 1,
+ claim = 2,
+ refund = 3,
+// export interface ClaimState {
+// fairLaunchRoot: string | null
+// referralRoot: string | null
+// refundRoot: string | null
+// const zeroByte32 = '0x0000000000000000000000000000000000000000000000000000000000000000'
+let uniswapV2PairCache: string | null = null
+export default class DegenSwapTokenV2Contract extends Erc20TokenContract {
+ async ticketPrice(): Promise<string> {
+ const bn = await contract.ticketPrice()
+ async uniswapV2Pair(): Promise<string> {
+ if (uniswapV2PairCache) return uniswapV2PairCache
+ const addr = await contract.uniswapV2Pair()
+ uniswapV2PairCache = addr
+ return addr
+ async targetCap(): Promise<string> {
+ const bn = await contract.cap()
+ async currentCap(): Promise<string> {
+ const bn = await this.provider.provider!.getBalance(this.address)
+ async deposit(tickets: number): Promise<TransactionReceipt> {
+ const price = await this.ticketPrice()
+ const value = new Decimal(price).mul(tickets).toFixed(0)
+ const tx: ContractTransactionResponse = await contract.deposit(tickets, {
+ const txwait = await awaitConfirmations(tx)
+ async claim(amount: string, proof: string[]): Promise<TransactionReceipt> {
+ const tx: ContractTransactionResponse = await contract.claim(amount, proof)
+ async claimReferral(amount: string, proof: string[]): Promise<TransactionReceipt> {
+ const tx: ContractTransactionResponse = await contract.claimReferral(amount, proof)
+ async refund(amount: string, proof: string[]): Promise<TransactionReceipt> {
+ const tx: ContractTransactionResponse = await contract.refund(amount, proof)
+ // 邀请是1,ido是2,退款是3
+ async isClaimedOrigin(address: string, amount: string, type: ClaimCheckType): Promise<boolean> {
+ const hash = solidityPackedKeccak256(['address', 'uint256', 'uint'], [address, amount, type])
+ const result = await contract.isClaimed(hash)
+ return result
+ async isClaimed(address: string, amount: string): Promise<boolean> {
+ return await this.isClaimedOrigin(address, amount, ClaimCheckType.claim)
+ async isRefunded(address: string, amount: string): Promise<boolean> {
+ return await this.isClaimedOrigin(address, amount, ClaimCheckType.refund)
+ async isReferralClaimed(address: string, amount: string): Promise<boolean> {
+ return await this.isClaimedOrigin(address, amount, ClaimCheckType.referral)
+ const result = await contract.tradingOpen()
+ async claimOpen(): Promise<boolean> {
+ const result = await contract.claimOpen()
+ async claimReferralOpen(): Promise<boolean> {
+ const result = await contract.claimReferralOpen()
+ async addressDeposits(address: string): Promise<string> {
+ const bn = await contract.deposits(address)
+ async depositTimeState(): Promise<DepositTimeState> {
+ const endDateBn = await contract.depositEndDate()
+ const endDate = parseInt(endDateBn) * 1000
+ if (endDate === 0) return 'notStarted'
+ if (Date.now() < endDate) return 'ongoing'
+ return 'ended'
+ // async claimState(): Promise<ClaimState> {
+ // const contract = this.getInstance()
+ // const fairLaunchRoot = await contract.fairLaunchRoot()
+ // const referralRoot = await contract.referralRoot()
+ // const refundRoot = await contract.refundRoot()
+ // return {
+ // fairLaunchRoot: fairLaunchRoot === zeroByte32 ? null : fairLaunchRoot.toString(),
+ // referralRoot: referralRoot === zeroByte32 ? null : referralRoot.toString(),
+ // refundRoot: refundRoot === zeroByte32 ? null : refundRoot.toString(),
@@ -0,0 +1,1095 @@
+ "name": "_betSwapRouter",
+ "name": "_marketMaker",
+ "name": "_amount",
+ "name": "_account",
+ "name": "ClaimedReferralWithProof",
+ "name": "ClaimedWithProof",
+ "name": "depositor",
+ "name": "amountETHdeposited",
+ "name": "totalSubscription",
+ "name": "Deposited",
+ "name": "RefundWithProof",
+ "name": "betSwapRouter",
+ "name": "cap",
+ "name": "_merkleProof",
+ "name": "claimOpen",
+ "name": "claimReferral",
+ "name": "claimReferralOpen",
+ "name": "tickets",
+ "name": "depositEndDate",
+ "name": "deposits",
+ "name": "ethRefunded",
+ "name": "fairLaunch",
+ "name": "fairLaunchAllocation",
+ "name": "fairLaunchRoot",
+ "name": "getParticipants",
+ "name": "addrs",
+ "name": "start",
+ "name": "length",
+ "name": "getStuckTokens",
+ "name": "idoTokensClaimed",
+ "name": "_hash",
+ "name": "isClaimed",
+ "name": "liquidityAllocation",
+ "name": "marketMaker",
+ "name": "openClaim",
+ "name": "openClaimReferral",
+ "name": "overEth",
+ "name": "participants",
+ "name": "referralAllocation",
+ "name": "referralRoot",
+ "name": "referralTokensClaimed",
+ "name": "refund",
+ "name": "refundRoot",
+ "name": "_root",
+ "name": "setFairLaunchRoot",
+ "name": "setReferralRoot",
+ "name": "setRefundRoot",
+ "name": "stopSale",
+ "name": "subscription",
+ "name": "ticketPrice",
+ "name": "timeout",
@@ -0,0 +1,200 @@
+ TransactionReceipt,
+import ABI from './degenSwapV2ABI.json'
+import DegenSwapTokenV2Contract from './degenSwapTokenV2'
+async function createPair(pairAddress: string, provider: ethers.Provider): Promise<Pair> {
+ // const pairAddress = (window as any).CONSTS.PAIR_ADDRESS
+ tx: TransactionReceipt
+export default class DegenSwapV2Contract extends WrappedContract {
+ async buy(ethAmount: string, criticalRatio: number): Promise<DegenBuyResult> {
+ criticalRatio,
+ gasLimit: 500000
+ // console.log(logs)
+ // console.log(logs.map((log) => contract.interface.parseLog(log)))
+ // console.log(event)
+ async sell(tokenAmount: string): Promise<TransactionReceipt> {
+ const tokenContract = new DegenSwapTokenV2Contract(this.provider)
+ async sellApprove(): Promise<TransactionReceipt> {
+ const pairAddress = await new DegenSwapTokenV2Contract(this.provider).uniswapV2Pair()
+ const pair = await createPair(pairAddress, this.provider.provider!)
@@ -0,0 +1,187 @@
+ "name": "_criticalRatio",
+ "name": "_uniswapV2Router",
+ "name": "_uniswapV2Factory",
+ "name": "uniswapV2Factory",
+ "internalType": "contract IUniswapV2Factory",
@@ -0,0 +1,88 @@
+export default class Erc20TokenContract extends WrappedContract {
+ super(erc20ABI, address, provider)
+ async balanceOf(address: string): Promise<string> {
+ const bn = await contract.balanceOf(address)
+ async approve(
+ amount: string = MAX_ETH_NUM
+ const tx: ContractTransactionResponse = await contract.approve(
+ address,
+ amount
+ async transfer(
+ amount: string
+ const tx: ContractTransactionResponse = await contract.transfer(
+ async allowance(owner: string, spender: string): Promise<string> {
+ const bn = await contract.allowance(owner, spender)
+ async symbol(): Promise<string> {
+ const bn = await contract.symbol()
+ async name(): Promise<string> {
+ const bn = await contract.name()
+ async decimals(): Promise<number> {
+ const bn = await contract.decimals()
+ async totalSupply(): Promise<string> {
+ const bn = await contract.totalSupply()
@@ -0,0 +1,222 @@
+ "constant": false,
+ "name": "_spender",
+ "name": "_value",
+ "name": "_from",
+ "name": "_to",
+ "name": "_owner",
+ "name": "balance",
+ "payable": true,
+ "type": "fallback"
+ "name": "whale",
+ "name": "whaleToFragment",
+ "name": "fragmentToWhale",
@@ -0,0 +1,102 @@
+import erc721ABI from './erc721ABI.json'
+interface NFTTokenURI {
+ id: number
+ uri: string
+export default class Erc72TokenContract extends WrappedContract {
+ super(erc721ABI, address, provider)
+ async balanceOf(address: string): Promise<number> {
+ async tokenOfOwnerByIndex(address: string, index: number): Promise<number> {
+ const bn = await contract.tokenOfOwnerByIndex(address, index)
+ async tokenURI(tokenId: number): Promise<string> {
+ return await contract.tokenURI(tokenId)
+ async safeTransferFrom(
+ addressFrom: string,
+ addressTo: string,
+ tokenId: number
+ const method = contract['safeTransferFrom(address,address,uint256)']
+ const tx: ContractTransactionResponse = await method(
+ addressFrom,
+ addressTo,
+ tokenId
+ async getApproved(tokenId: number): Promise<string> {
+ return await contract.getApproved(tokenId)
+ // other
+ async tokenIdsOf(address: string): Promise<number[]> {
+ const count = await this.balanceOf(address)
+ const list: number[] = []
+ for (let i = 0; i < count; i++) {
+ list[i] = await this.tokenOfOwnerByIndex(address, i)
+ return list
+ async tokenURIListOf(address: string): Promise<NFTTokenURI[]> {
+ const ids = await this.tokenIdsOf(address)
+ const list: NFTTokenURI[] = []
+ for (let i = 0; i < ids.length; i++) {
+ const uri = await this.tokenURI(ids[i])
+ list[i] = {
+ id: ids[i],
+ uri,
@@ -0,0 +1,861 @@
+ "name": "baseTokenURI",
+ "name": "Paused",
+ "name": "Unpaused",
+ "name": "DOMAIN_SEPARATOR",
+ "name": "MINTER_ROLE",
+ "name": "PAUSER_ROLE",
+ "name": "getMetadata",
+ "name": "metadata",
+ "name": "getRoleMember",
+ "name": "getRoleMemberCount",
+ "name": "v",
+ "name": "r",
+ "name": "s",
+ "name": "mintPermit",
+ "name": "nonces",
+ "name": "pause",
+ "name": "paused",
+ "name": "_data",
+ "name": "setMetadata",
+ "name": "unpause",
@@ -0,0 +1,41 @@
+import erc721MockABI from './erc721MockABI.json'
+import { CONTRACT_ERC721 } from '../../../constants'
+export default class Erc721MockContract extends WrappedContract {
+ constructor(provider: ethers.Signer | ethers.providers.Provider) {
+ super(erc721MockABI, CONTRACT_ERC721, provider)
+ async getPrice(): Promise<string> {
+ const price: BigNumber = await contract.getPrice()
+ return price.toString()
+ async mint(amount = 1, txOptions = {}): Promise<ethers.ContractReceipt> {
+ const price = await this.getPrice()
+ const value = new Decimal(price).mul(amount).toString()
+ const status = await contract.mint(amount, {
+ // gasPrice: ethers.utils.parseUnits('1', 'gwei'),
+ gasLimit: 800000,
+ ...txOptions,
+ const tx = await status.wait()
+ return tx
+ async balanceOf(address: string): Promise<BigNumber> {
@@ -0,0 +1,229 @@
+ "name": "_ef_token",
+ "name": "_aave",
+ "name": "_weth",
+ "name": "_vault",
+ "name": "aave",
+ "name": "balancer",
+ "name": "canWithdraw",
+ "name": "ef_token",
+ "name": "getDebt",
+ "name": "getVolume",
+ "internalType": "contract IERC20[]",
+ "name": "tokens",
+ "name": "feeAmounts",
+ "name": "userData",
+ "name": "receiveFlashLoan",
+ "name": "recovery",
+ "name": "sandbagsAddress",
+ "name": "setAddress",
+ "name": "setQueueAddress",
+ "name": "setSandbagAddress",
+ "name": "weth",
+import filterABI from './filterABI.json'
+import { CONTRACT_FILTER } from '../../../constants'
+export default class FilterContract extends WrappedContract {
+ super(filterABI, CONTRACT_FILTER, provider)
@@ -0,0 +1,235 @@
+ "name": "_filter",
+ "name": "InQueue",
+ "name": "_index",
+ "name": "execute",
+ "name": "_toAddress",
+ "name": "filterMint",
+ "name": "getExecuteAddress",
+ "name": "getFilterRuleAddress",
+ "name": "getIndex",
+ "name": "getIsFilterWorking",
+ "name": "getOwner",
+ "name": "isFilterWorking",
+ "name": "revert",
+ "name": "_execute",
+ "name": "setExecuteAddress",
+ "name": "setFilterRuleAddress",
+ "name": "_isWorking",
+ "name": "setIsFilterWorking",
@@ -0,0 +1,20 @@
+import filterABI from './filterRuleABI.json'
+import { ethers, BigNumber } from 'ethers'
+import { CONTRACT_FILTER_RULE } from '../../../constants'
+export default class FilterRuleContract extends WrappedContract {
+ super(filterABI, CONTRACT_FILTER_RULE, provider)
+ async getMaxGasPrice(): Promise<string> {
+ const bn: BigNumber = await contract.getMaxGasPrice()
@@ -0,0 +1,124 @@
+ "name": "caller",
+ "name": "orgin",
+ "name": "EOAFail",
+ "name": "gasPrice",
+ "name": "gasTooHigh",
+ "name": "getMaxGasPrice",
+ "name": "_maxGasPrice",
+ "name": "setMaxGasPrice",
+ "name": "_caller",
+ "name": "test",
@@ -0,0 +1,51 @@
+import FoxABI from './foxABI.json'
+export class FoxReadOnlyContract {
+ protected ABI: any[] = FoxABI
+ protected provider: ethers.Provider
+ constructor(provider: ethers.Provider) {
+ this.address = (window as any).CONSTS.CONTRACT_ADDRESS
+ async mintTimes(address: string): Promise<number> {
+ const bn = await contract.mintTimes(address)
+export default class FoxContract extends WrappedContract {
+ super(FoxABI, (window as any).CONSTS.CONTRACT_ADDRESS, provider)
+ async remainMintTimes(): Promise<number> {
+ const address = await this.provider.getAddress()
+ const max: bigint = await contract.maxMintTimes()
+ const bn: bigint = await contract.mintTimes(address)
+ const remain = max - bn
+ return parseInt(remain.toString())
+ async currentTokens(): Promise<string> {
+ const bn: bigint = await contract.currentTokens()
@@ -0,0 +1,607 @@
+ "name": "blackHole",
+ "name": "boostCount",
+ "name": "currentTokens",
+ "name": "launchTime",
+ "name": "magicNumber",
+ "name": "maxMintTimes",
+ "name": "mintTimes",
+ "name": "saleCount",
+ "name": "_sellTax",
+ "name": "updateSellTax",
@@ -0,0 +1,70 @@
+import { ethers, ContractTransaction } from 'ethers'
+import hackerABI from './hackerABI.json'
+ CONTRACT_EF,
+ CONTRACT_EFTOKEN,
+ CONTRACT_EF_SANDBAGS,
+ CONTRACT_HACKER,
+ CONTRACT_SAFE_EFTOKEN,
+ CONTRACT_FLASHLOANMOCK,
+ CONTRACT_AAVEMOCK,
+ CONTRACT_WETHTOKEN,
+} from '../../../constants'
+import { EFTokenErc20TokenContract } from './erc20'
+export default class HackerContract extends WrappedContract {
+ private poolAddress: string = ''
+ private efTokenAddress: string = ''
+ constructor(provider: ethers.Signer, isProtected: boolean = false) {
+ super(hackerABI, CONTRACT_HACKER, provider)
+ this.protected = isProtected
+ get protected(): boolean {
+ return this.poolAddress === CONTRACT_EF_SANDBAGS
+ set protected(value: boolean) {
+ this.poolAddress = value ? CONTRACT_EF_SANDBAGS : CONTRACT_EF
+ this.efTokenAddress = value ? CONTRACT_SAFE_EFTOKEN : CONTRACT_EFTOKEN
+ async checkAllowanceOfEf(account: string): Promise<boolean> {
+ const token = new EFTokenErc20TokenContract(this.provider, this.protected)
+ const allow = await token.allowance(account, this.address)
+ const userAmount = await token.balanceOf(account)
+ return new Decimal(allow).eq(userAmount)
+ async approveEf(): Promise<ethers.ContractReceipt> {
+ const userAddress = await token.getCallerAddress()
+ const userAmount = await token.balanceOf(userAddress)
+ return await token.approve(this.address, userAmount)
+ async takeMoney(): Promise<ethers.ContractReceipt> {
+ return contract
+ .takeMoney(
+ this.poolAddress,
+ this.efTokenAddress,
+ gasLimit: 1000000,
+ .then(async (tx: ContractTransaction) => {
+ return await tx.wait()
@@ -0,0 +1,59 @@
+ "name": "_target",
+ "name": "ef",
+ "name": "efToken",
+ "name": "vault",
+ "name": "takeMoney",
+import { CONTRACT_PAIR } from '../../../constants'
+export default class LPTokenErc20TokenContract extends Erc20TokenContract {
+ super(CONTRACT_PAIR, provider)
@@ -0,0 +1,1112 @@
+ "name": "Claim",
+ "name": "Fees",
+ "name": "amount0In",
+ "name": "amount1In",
+ "name": "amount0Out",
+ "name": "amount1Out",
+ "name": "Sync",
+ "name": "claimFees",
+ "name": "claimed0",
+ "name": "claimed1",
+ "name": "claimable0",
+ "name": "claimable1",
+ "name": "current",
+ "name": "currentCumulativePrices",
+ "name": "reserve0Cumulative",
+ "name": "reserve1Cumulative",
+ "name": "fees",
+ "name": "getAmountOut",
+ "name": "_reserve0",
+ "name": "_reserve1",
+ "name": "_blockTimestampLast",
+ "name": "index0",
+ "name": "index1",
+ "name": "lastObservation",
+ "components": [
+ "name": "timestamp",
+ "internalType": "struct Pair.Observation",
+ "type": "tuple"
+ "name": "dec0",
+ "name": "dec1",
+ "name": "r0",
+ "name": "r1",
+ "name": "st",
+ "name": "t0",
+ "name": "t1",
+ "name": "observationLength",
+ "name": "permit",
+ "name": "points",
+ "name": "prices",
+ "name": "granularity",
+ "name": "quote",
+ "name": "reserve0CumulativeLast",
+ "name": "reserve1CumulativeLast",
+ "name": "window",
+ "name": "sample",
+ "name": "skim",
+ "name": "stable",
+ "name": "supplyIndex0",
+ "name": "supplyIndex1",
+ "name": "sync",
+ "name": "dst",
+ "name": "src",
@@ -0,0 +1,50 @@
+import pancakeABI from './pancakeABI.json'
+import { ContractTransaction, ethers } from 'ethers'
+ CONTRACT_PANCAKE,
+ CONTRACT_SBT,
+ CONTRACT_USDT,
+import dayjs from 'dayjs'
+export default class PancakeContract extends WrappedContract {
+ super(pancakeABI, CONTRACT_PANCAKE, provider)
+ async swapExactTokensForTokens(
+ path: string[],
+ to: string,
+ deadline = dayjs().add(15, 'minute').unix()
+ ): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.swapExactTokensForTokens(
+ 0,
+ path,
+ to,
+ deadline,
+ gasLimit: 300000,
+ async swapExactSBTForUSDT(
+ to: string
+ return await this.swapExactTokensForTokens(
+ [CONTRACT_SBT, CONTRACT_USDT],
+ to
@@ -0,0 +1,973 @@
+ "name": "_WETH",
+ "name": "WETH",
+ "name": "tokenA",
+ "name": "tokenB",
+ "name": "amountADesired",
+ "name": "amountBDesired",
+ "name": "amountAMin",
+ "name": "amountBMin",
+ "name": "addLiquidity",
+ "name": "amountA",
+ "name": "amountB",
+ "name": "amountTokenDesired",
+ "name": "amountTokenMin",
+ "name": "amountETHMin",
+ "name": "addLiquidityETH",
+ "name": "amountToken",
+ "name": "amountETH",
+ "name": "reserveIn",
+ "name": "reserveOut",
+ "name": "getAmountIn",
+ "stateMutability": "pure",
+ "name": "getAmountsIn",
+ "name": "getAmountsOut",
+ "name": "reserveA",
+ "name": "reserveB",
+ "name": "removeLiquidity",
+ "name": "removeLiquidityETH",
+ "name": "removeLiquidityETHSupportingFeeOnTransferTokens",
+ "name": "approveMax",
+ "name": "removeLiquidityETHWithPermit",
+ "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens",
+ "name": "removeLiquidityWithPermit",
@@ -0,0 +1,153 @@
+import poolABI from './poolABI.json'
+import { CONTRACT_POOL } from '../../../constants'
+import { WHALETokenErc20TokenContract } from './erc20'
+export enum WhalePresaleStatus {
+ upcoming = 'upcoming',
+ living = 'living',
+ ended = 'ended',
+export interface PoolUserInfo {
+ rewardDebt: string
+ lockEndedTimestamp: string
+export default class PoolContract extends WrappedContract {
+ super(poolABI, CONTRACT_POOL, provider)
+ async deposit(mode: 0 | 1, value: string): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.deposit(mode, value)
+ async depositToken(value: string): Promise<ethers.ContractReceipt> {
+ return await this.deposit(1, value)
+ async depositLP(value: string): Promise<ethers.ContractReceipt> {
+ return await this.deposit(0, value)
+ async depositMax(mode: 0 | 1): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.depositMax(mode)
+ async depositMaxToken(): Promise<ethers.ContractReceipt> {
+ return await this.depositMax(1)
+ async depositMaxLP(): Promise<ethers.ContractReceipt> {
+ return await this.depositMax(0)
+ async getUserInfo(mode: 0 | 1, address: string): Promise<PoolUserInfo> {
+ const ui = await contract.userInfo(mode, address)
+ amount: ui.amount.toString(),
+ rewardDebt: ui.rewardDebt.toString(),
+ lockEndedTimestamp: ui.lockEndedTimestamp.toString(),
+ async getTokenUserInfo(address: string): Promise<PoolUserInfo> {
+ const userInfo = await this.getUserInfo(1, address)
+ const tokenContract = new WHALETokenErc20TokenContract(this.provider)
+ const toWhale = await tokenContract.whaleToFragment(userInfo.amount)
+ userInfo.amount = toWhale.toString()
+ return userInfo
+ async getLPUserInfo(address: string): Promise<PoolUserInfo> {
+ return await this.getUserInfo(0, address)
+ async getDepositBalance(mode: 0 | 1, address: string): Promise<string> {
+ const { amount } = await this.getUserInfo(mode, address)
+ return amount.toString()
+ async getTokenDepositBalance(address: string): Promise<string> {
+ const whaleAmount = await this.getDepositBalance(1, address)
+ const toWhale = await tokenContract.whaleToFragment(whaleAmount)
+ return toWhale.toString()
+ async getLPDepositBalance(address: string): Promise<string> {
+ return await this.getDepositBalance(0, address)
+ async withdraw(mode: 0 | 1, value: string): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.withdraw(mode, value)
+ async withdrawToken(value: string): Promise<ethers.ContractReceipt> {
+ const amount = await tokenContract.fragmentToWhale(value)
+ return await this.withdraw(1, amount)
+ async withdrawLP(value: string): Promise<ethers.ContractReceipt> {
+ return await this.withdraw(0, value)
+ async withdrawMax(mode: 0 | 1): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.withdrawMax(mode)
+ async withdrawMaxToken(): Promise<ethers.ContractReceipt> {
+ return await this.withdrawMax(1)
+ async withdrawMaxLP(): Promise<ethers.ContractReceipt> {
+ return await this.withdrawMax(0)
+ async claim(mode: 0 | 1, address: string): Promise<ethers.ContractReceipt> {
+ const tx: ContractTransaction = await contract.claim(mode, address)
+ async claimToken(address: string): Promise<ethers.ContractReceipt> {
+ return await this.claim(1, address)
+ async claimLP(address: string): Promise<ethers.ContractReceipt> {
+ return await this.claim(0, address)
+ async pendingReward(mode: 0 | 1, address: string): Promise<string> {
+ const bn = await contract.pendingReward(mode, address)
+ async pendingRewardToken(address: string): Promise<string> {
+ return await this.pendingReward(1, address)
+ async pendingRewardLP(address: string): Promise<string> {
+ return await this.pendingReward(0, address)
@@ -0,0 +1,738 @@
+ "internalType": "contract IWhaleToken",
+ "name": "_whale",
+ "internalType": "contract IUniswapPair",
+ "name": "_whaleLp",
+ "internalType": "contract IOVM_L1BlockNumber",
+ "name": "_l1BlockNumber",
+ "name": "_rewardPerBlock",
+ "name": "_startBlock",
+ "name": "pid",
+ "name": "Deposit",
+ "name": "allocPoint",
+ "internalType": "contract IERC20",
+ "name": "lpToken",
+ "name": "LogPoolAddition",
+ "name": "LogRewardPerBlock",
+ "name": "lockDuration",
+ "name": "LogSetLockDuration",
+ "name": "LogSetPool",
+ "name": "lastRewardBlock",
+ "name": "lpSupply",
+ "name": "accRewardPerShare",
+ "name": "LogUpdatePool",
+ "name": "RewardPaid",
+ "name": "Withdraw",
+ "name": "_allocPoint",
+ "name": "_lpToken",
+ "name": "_withUpdate",
+ "name": "add",
+ "name": "_pid",
+ "name": "principal",
+ "name": "ratio",
+ "name": "n",
+ "name": "compound",
+ "name": "compoundRatio",
+ "name": "depositMax",
+ "name": "l1BlockNumber",
+ "name": "lastBlock",
+ "name": "lockDurations",
+ "name": "massUpdatePools",
+ "name": "_user",
+ "name": "pendingReward",
+ "name": "poolInfo",
+ "name": "poolLength",
+ "name": "pow",
+ "name": "rewardPerBlock",
+ "name": "set",
+ "name": "_lockDuration",
+ "name": "setLockDuration",
+ "name": "startBlock",
+ "name": "totalAllocPoint",
+ "name": "totalLocked",
+ "name": "updatePool",
+ "name": "updateRewardPerBlock",
+ "name": "userInfo",
+ "name": "rewardDebt",
+ "name": "lockEndedTimestamp",
+ "name": "whaleLp",
+ "name": "withdrawMax",
@@ -0,0 +1,290 @@
+import { Op } from 'sequelize'
+ ForeignKey,
+ BelongsTo,
+ HasOne,
+ BeforeUpdate,
+ BeforeCreate,
+ Is,
+ BelongsToMany,
+ BeforeFind,
+ HasMany,
+import { SupportedEVMHelperChains } from '../types'
+ modelName: 'evmh_rpc',
+ fields: ['chainName'],
+ fields: ['chainName', 'rpc'],
+export default class RPC extends Model {
+ @Column(DataType.CHAR(32))
+ get chainName(): SupportedEVMHelperChains {
+ return this.getDataValue('chainName')
+ @Column(DataType.CHAR(128).BINARY)
+ get rpc(): string {
+ return this.getDataValue('rpc')
+ static async getRPCList(chainName: SupportedEVMHelperChains): Promise<RPC[]> {
+ if (!this.sequelize) {
+ return []
+ const r = await RPC.findAll({
+ chainName,
+ return r
@@ -0,0 +1,3 @@
+import RPC from './RPC'
+export { RPC }
@@ -0,0 +1,2 @@
+import EVMHelper from './EVMHelper'
+export default EVMHelper
@@ -0,0 +1,64 @@
+export enum SupportedEVMHelperChains {
+ mainnet = 'mainnet',
+ goerli = 'goerli',
+ scrollAlpha = 'scrollAlpha',
+ bsc = 'bsc',
+ bscTestnet = 'bscTestnet',
+ arbitrum = 'arbitrum',
+ optimism = 'optimism',
+ avax = 'avax',
+ polygon = 'polygon',
+ zkSyncMainnet = 'zkSyncMainnet',
+ blastSepoliaTestnet = 'blastSepoliaTestnet',
+// eslint-disable-next-line prettier/prettier
+export const DefaultEVMHelperChainRPCs: Record<SupportedEVMHelperChains, string> = {
+ mainnet: 'https://eth.llamarpc.com',
+ goerli: 'https://eth-goerli.public.blastapi.io',
+ scrollAlpha: 'https://alpha-rpc.scroll.io/l2',
+ bsc: 'https://bsc-dataseed.binance.org',
+ bscTestnet: 'https://data-seed-prebsc-1-s3.binance.org:8545',
+ arbitrum: 'https://endpoints.omniatech.io/v1/arbitrum/one/public',
+ optimism: 'https://endpoints.omniatech.io/v1/op/mainnet/public',
+ avax: 'https://api.avax.network/ext/bc/C/rpc',
+ polygon: 'https://polygon.blockpi.network/v1/rpc/public',
+ zkSyncMainnet: 'https://mainnet.era.zksync.io',
+ blastSepoliaTestnet: 'https://sepolia.blast.io',
+export const DefaultEVMHelperExplorerURLs: Record<SupportedEVMHelperChains, string> = {
+ mainnet: 'https://etherscan.io',
+ goerli: 'https://goerli.etherscan.io',
+ scrollAlpha: 'https://blockscout.scroll.io',
+ bsc: 'https://bscscan.com',
+ bscTestnet: 'https://testnet.bscscan.com',
+ arbitrum: 'https://arbiscan.io',
+ optimism: 'https://optimistic.etherscan.io',
+ avax: 'https://snowtrace.io',
+ polygon: 'https://polygonscan.com',
+ zkSyncMainnet: 'https://explorer.zksync.io',
+ blastSepoliaTestnet: 'https://testnet.blastscan.io',
+export const MAX_ETH_NUM =
+ '115792089237316195423570985008687907853269984665640564039457584007913129639935'
+export const MAX_UINT128 = '340282366920938463463374607431768211455'
+export const DefaultEVMHelperScanAPIUrls: Record<SupportedEVMHelperChains, string> = {
+ mainnet: '',
+ goerli: '',
+ scrollAlpha: '',
+ bsc: 'https://api.bscscan.com',
+ bscTestnet: '',
+ arbitrum: '',
+ optimism: '',
+ avax: '',
+ polygon: '',
+ zkSyncMainnet: '',
+ blastSepoliaTestnet:
+ 'https://api.routescan.io/v2/network/testnet/evm/168587773/etherscan',
@@ -0,0 +1,108 @@
+ Commitment,
+ Keypair,
+ PublicKey,
+ SystemProgram,
+ TransactionInstruction,
+ Transaction,
+} from '@solana/web3.js'
+import axios, { AxiosInstance } from 'axios'
+import { lamports } from '..'
+import SolanaHelper, { PriorityFee } from './SolanaHelper'
+import { addMemoToSerializedTxn } from '@bloxroute/solana-trader-client-ts'
+export enum BloxrouteSolanaRpc {
+ England = 'https://uk.solana.dex.blxrbdn.com',
+ NewYork = 'https://ny.solana.dex.blxrbdn.com',
+const TRADER_API_TIP_WALLET = 'HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY'
+export default class BloxrouteSolanaHelper {
+ private readonly caller: AxiosInstance
+ constructor(
+ private readonly solanaHelper: SolanaHelper,
+ private readonly token = process.env.BLOXROUTE_TOKEN!,
+ private readonly rpc: BloxrouteSolanaRpc = BloxrouteSolanaRpc.NewYork
+ ) {
+ this.caller = axios.create({
+ baseURL: rpc,
+ headers: {
+ Authorization: token,
+ async getRecentPriorityFee(
+ project: 'P_JUPITER' | 'P_RAYDIUM' = 'P_JUPITER'
+ ): Promise<bigint> {
+ const response = await this.caller.get('/api/v2/system/priority-fee', {
+ params: { project },
+ return BigInt(response.data.feeAtPercentile)
+ CreateTraderAPITipInstruction(
+ payer: PublicKey,
+ tipAmount: number = parseInt(lamports('0.001'))
+ ): TransactionInstruction {
+ const tipAddress = new PublicKey(TRADER_API_TIP_WALLET)
+ return SystemProgram.transfer({
+ fromPubkey: payer,
+ toPubkey: tipAddress,
+ lamports: tipAmount,
+ async sendTx(
+ tx: Transaction,
+ signers: Keypair[],
+ priorityFees?: PriorityFee,
+ tipAmount: number = parseInt(lamports('0.01')),
+ commitment: Commitment = 'confirmed'
+ const connection = await this.solanaHelper.getProvider()
+ const blockHash = (await connection.getLatestBlockhash(commitment))
+ .blockhash
+ const feeedTx = this.solanaHelper.makePriorityFeesTx(tx, priorityFees)
+ console.log('tip amount:', tipAmount)
+ const tipTx = this.CreateTraderAPITipInstruction(payer, tipAmount)
+ feeedTx.add(tipTx)
+ console.log('Transaction to be sent:', feeedTx)
+ const versionedTx = await this.solanaHelper.buildVersionedTx(
+ payer,
+ feeedTx,
+ blockHash
+ versionedTx.sign(signers)
+ const serializedTransaztionBytes = versionedTx.serialize()
+ const buff = Buffer.from(serializedTransaztionBytes)
+ const b64Content = buff.toString('base64')
+ const memoedB64Content = addMemoToSerializedTxn(b64Content)
+ console.log('Base64 transaction:', memoedB64Content)
+ // throw new Error('Failed to send transaction')
+ const response = await this.caller.post('/api/v2/submit', {
+ transaction: { content: memoedB64Content },
+ frontRunningProtection: false,
+ useStakedRPCs: true,
+ console.log('Transaction sent:', response.data)
+ return '111'
+ async getTxStatus(signature: string): Promise<void> {
+ const response = await this.caller.get('/api/v2/transaction', {
+ params: { signature },
+ console.log(response.data)
@@ -0,0 +1,219 @@
+import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'
+import bs58 from 'bs58'
+import SolanaHelper from './SolanaHelper'
+import { timeNumber } from '../time'
+import { sleep } from '..'
+// export interface JitoHelperBlockEngineConfig {
+// BLOCK_ENGINE_URL: string
+// SHRED_RECEIVER_ADDR: string
+// RELAYER_URL: string
+export interface JitoHelperOptions {
+ // blockEngineConfig: JitoHelperBlockEngineConfig
+ rpc: string
+export interface WaitForLandedOptions {
+ throwOnInvalid: boolean
+ maxTry: number
+ interval: number
+ verbose: boolean
+const defaultWaitForLandedOptions: WaitForLandedOptions = {
+ throwOnInvalid: true,
+ maxTry: 99,
+ interval: timeNumber.second * 5,
+ verbose: false,
+export interface JitoJRPCResponse<T> {
+ jsonrpc: '2.0'
+ result: {
+ context: {
+ slot: number
+ value: T
+export interface JitoJRPCInflightBundleStatus {
+ bundle_id: string
+ status: 'Invalid' | 'Pending' | 'Failed' | 'Landed'
+ landed_slot: number | null
+export type JitoJRPCInflightBundleStatusesResponse = JitoJRPCResponse<JitoJRPCInflightBundleStatus[]>
+export interface JitoJRPCBundleStatus {
+ transactions: string[]
+ confirmation_status: 'processed' | 'confirmed' | 'finalized'
+ err: {
+ Ok: null
+export type JitoJRPCBundleStatusesResponse = JitoJRPCResponse<JitoJRPCBundleStatus[]>
+export default class JitoJRPCHelper {
+ readonly options: JitoHelperOptions,
+ private readonly solanaHelper: SolanaHelper
+ ) {}
+ async getInflightBundleStatuses(
+ id1: string,
+ id2?: string,
+ id3?: string,
+ id4?: string,
+ id5?: string
+ ): Promise<JitoJRPCInflightBundleStatusesResponse> {
+ const url = `${this.options.rpc}/api/v1/bundles`
+ const data = {
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'getInflightBundleStatuses',
+ params: [[id1]],
+ if (id2) data.params[0].push(id2)
+ if (id3) data.params[0].push(id3)
+ if (id4) data.params[0].push(id4)
+ if (id5) data.params[0].push(id5)
+ const response = await axios.post(url, data)
+ return response.data
+ async getBundleStatuses(
+ ): Promise<JitoJRPCBundleStatusesResponse> {
+ method: 'getBundleStatuses',
+ async getTipAccounts(): Promise<string[]> {
+ method: 'getTipAccounts',
+ params: [],
+ interface Resp {
+ result: string[]
+ const response = await axios.post<Resp>(url, data)
+ return response.data.result
+ async sendBundle(
+ tx1: VersionedTransaction,
+ tx2?: VersionedTransaction,
+ tx3?: VersionedTransaction,
+ tx4?: VersionedTransaction,
+ tx5?: VersionedTransaction
+ // make tx1 to fully-signed transactions, as base-58 encoded strings
+ method: 'sendBundle',
+ params: [[bs58.encode(tx1.serialize())]],
+ // console.log(url)
+ if (tx2) data.params[0].push(bs58.encode(tx2.serialize()))
+ if (tx3) data.params[0].push(bs58.encode(tx3.serialize()))
+ if (tx4) data.params[0].push(bs58.encode(tx4.serialize()))
+ if (tx5) data.params[0].push(bs58.encode(tx5.serialize()))
+ // return 'test'
+ async addTipTransactionToTx(
+ tipLamports: bigint
+ ): Promise<void> {
+ const tipAccounts = await this.getTipAccounts()
+ if (tipAccounts.length === 0) {
+ throw new Error('No tip accounts available')
+ const tipAccount = tipAccounts[0]
+ const transferTx = this.solanaHelper.makeTransferTx(
+ new PublicKey(tipAccount),
+ tipLamports
+ tx.add(transferTx)
+ async waitForLanded(
+ bundleId: string,
+ options: Partial<WaitForLandedOptions> = {}
+ ): Promise<JitoJRPCBundleStatus> {
+ const { throwOnInvalid, maxTry, interval, verbose } = {
+ ...defaultWaitForLandedOptions,
+ let tryCount = 0
+ while (true) {
+ if (tryCount >= maxTry) {
+ throw new Error('Bundle not landed after max tries')
+ const resp = await this.getInflightBundleStatuses(bundleId)
+ if (verbose) {
+ console.log(resp.result)
+ const inflight = resp.result.value[0]
+ if (!inflight) {
+ await sleep(interval)
+ continue
+ switch (inflight.status) {
+ case 'Landed': {
+ const status = await this.getBundleStatuses(bundleId)
+ return status.result.value[0]
+ case 'Failed':
+ throw new Error('Bundle failed')
+ case 'Invalid': {
+ if (throwOnInvalid) {
+ throw new Error('Bundle is invalid')
+ case 'Pending':
+ tryCount++
@@ -0,0 +1,426 @@
+ ComputeBudgetProgram,
+ Connection,
+ LAMPORTS_PER_SOL,
+ Finality,
+ TransactionMessage,
+ VersionedTransaction,
+ VersionedTransactionResponse,
+ SendTransactionError,
+ sendAndConfirmTransaction,
+ VersionedBlockResponse,
+import JitoJRPCHelper, { JitoHelperOptions } from './JitoHelper'
+import BloxrouteSolanaHelper from './BloxrouteSolanaHelper'
+export interface SolanaHelperOptions {
+ jitoHelperOptions: JitoHelperOptions
+export interface PriorityFee {
+ unitLimit?: number
+ unitPrice?: number
+export interface PriorityFeeInfo {
+ median: number
+ average: number
+ max: number
+ min: number
+ numSamples: number
+export interface TransactionResult {
+ signature?: string
+ error?: unknown
+ results?: VersionedTransactionResponse
+const defaultSolanaHelperOptions: SolanaHelperOptions = {
+ rpc: 'https://rpc.ankr.com/solana',
+ jitoHelperOptions: {
+ rpc: 'https://mainnet.block-engine.jito.wtf',
+export default class SolanaHelper {
+ solDecimals = 9
+ lamportsPerSol = LAMPORTS_PER_SOL
+ private readonly options: SolanaHelperOptions
+ private provider: Connection | null = null
+ readonly jitoHelper: JitoJRPCHelper
+ readonly bloxrouteSolanaHelper
+ constructor(options: Partial<SolanaHelperOptions> = {}) {
+ this.options = {
+ ...defaultSolanaHelperOptions,
+ this.jitoHelper = new JitoJRPCHelper(this.options.jitoHelperOptions, this)
+ this.bloxrouteSolanaHelper = new BloxrouteSolanaHelper(this)
+ async getProvider(): Promise<Connection> {
+ if (this.provider) return this.provider
+ this.provider = new Connection(this.options.rpc, 'confirmed')
+ return this.provider
+ get connection(): Connection {
+ if (!this.provider) throw new Error('Provider not initialized')
+ async getLatestBlockhash(
+ const provider = await this.getProvider()
+ const blockhash = await provider.getLatestBlockhash(commitment)
+ return blockhash.blockhash
+ getAccount(pk: string): Keypair {
+ return Keypair.fromSecretKey(bs58.decode(pk))
+ getPrivateKey(account: Keypair): string {
+ return bs58.encode(account.secretKey)
+ async getBalance(accountPublicKey: string): Promise<string> {
+ const balance = await provider.getBalance(new PublicKey(accountPublicKey))
+ async getSPLBalance(
+ walletPublicKey: string,
+ tokenPublicKey: string
+ const token = await provider.getTokenAccountsByOwner(
+ new PublicKey(walletPublicKey),
+ mint: new PublicKey(tokenPublicKey),
+ if (token.value.length === 0) {
+ return '0'
+ const balance = await provider.getTokenAccountBalance(token.value[0].pubkey)
+ if (!balance) {
+ return balance.value.amount.toString()
+ generateAccount(): Keypair {
+ const account = Keypair.generate()
+ return account
+ combineTransactions(...transactions: Transaction[]): Transaction {
+ const totalTxs = new Transaction()
+ for (const tx of transactions) {
+ totalTxs.add(tx)
+ return totalTxs
+ makePriorityFeesTx(tx: Transaction, priorityFees?: PriorityFee): Transaction {
+ if (!priorityFees) return tx
+ const newTx = new Transaction()
+ if (priorityFees.unitLimit) {
+ const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
+ units: priorityFees.unitLimit,
+ newTx.add(modifyComputeUnits)
+ if (priorityFees.unitPrice) {
+ const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
+ microLamports: priorityFees.unitPrice,
+ newTx.add(addPriorityFee)
+ newTx.add(tx)
+ return newTx
+ makeTransferTx(
+ recipient: PublicKey,
+ lamports: bigint
+ ): Transaction {
+ newTx.add(
+ SystemProgram.transfer({
+ toPubkey: recipient,
+ lamports,
+ async signTx(
+ blockHash: string,
+ priorityFees?: PriorityFee
+ ): Promise<VersionedTransaction> {
+ const newTx = this.makePriorityFeesTx(tx, priorityFees)
+ const versionedTx = await this.buildVersionedTx(payer, newTx, blockHash)
+ return versionedTx
+ commitment: Commitment = 'confirmed',
+ finality: Finality = 'confirmed'
+ ): Promise<TransactionResult> {
+ const connection = await this.getProvider()
+ console.log('Blockhash:', blockHash)
+ // const versionedTx = await this.signTx(
+ // tx,
+ // payer,
+ // signers,
+ // blockHash,
+ // priorityFees
+ // )
+ // throw new Error('Transaction failed to sign')
+ const feeedTx = this.makePriorityFeesTx(tx, priorityFees)
+ const sig = await sendAndConfirmTransaction(
+ connection,
+ signers
+ // skipPreflight: false,
+ // commitment: 'confirmed',
+ console.log('sig:', `https://solscan.io/tx/${sig}`)
+ // const sig = await connection.sendTransaction(versionedTx, {
+ // })
+ // console.log('sig:', `https://solscan.io/tx/${sig}`)
+ // const txResult = await this.getTxDetails(
+ // connection,
+ // sig,
+ // commitment,
+ // finality
+ // if (!txResult) {
+ // success: false,
+ // error: 'Transaction failed',
+ signature: sig,
+ // results: txResult,
+ if (e instanceof SendTransactionError) {
+ const ste = e
+ console.log(await ste.getLogs(connection))
+ console.error(e)
+ error: e,
+ async buildVersionedTx(
+ blockHash: string
+ const messageV0 = new TransactionMessage({
+ payerKey: payer,
+ recentBlockhash: blockHash,
+ instructions: tx.instructions,
+ }).compileToV0Message()
+ return new VersionedTransaction(messageV0)
+ async getTxDetails(
+ connection: Connection,
+ sig: string,
+ ): Promise<VersionedTransactionResponse | null> {
+ const latestBlockHash = await connection.getLatestBlockhash()
+ await connection.confirmTransaction(
+ blockhash: latestBlockHash.blockhash,
+ lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
+ commitment
+ return await connection.getTransaction(sig, {
+ maxSupportedTransactionVersion: 0,
+ commitment: finality,
+ async getRecentPriorityFees(
+ programId?: string,
+ numBlocks = 10
+ ): Promise<PriorityFeeInfo> {
+ const latestBlockheight = await connection.getSlot('finalized')
+ // 获取最近的区块生产时间数据
+ const blocks = await connection.getBlocks(latestBlockheight - numBlocks)
+ const priorityFees: Array<number[] | null> = []
+ const bss: number[][] = []
+ for (let i = 0; i < Math.ceil(numBlocks / 10); i++) {
+ const bs = blocks.slice(i * 10, (i + 1) * 10)
+ bss.push(bs)
+ let doneCount = 0
+ const checkDone = (): boolean => {
+ return doneCount === bss.length
+ const getFees = async (bs: number[]): Promise<void> => {
+ // 获取这些区块的优先费用数据
+ const fees = await Promise.all(
+ bs.map(async (block) => {
+ // const blockProdTime = await connection.getBlockTime(block)
+ // if (!blockProdTime) return null
+ const blockDetails = await connection.getBlock(block, {
+ if (!blockDetails?.transactions) return null
+ const txs = blockDetails.transactions.flat()
+ const programedTxs = txs.filter((tx) => {
+ if (!programId) return true
+ // 检查交易中的程序ID
+ // if (!tx.transaction.message.accountKeys) return false
+ const accountKeys = ((tx.transaction.message as any)
+ .accountKeys ?? []) as PublicKey[]
+ const accountKeyArr = accountKeys.map((key) => key.toBase58())
+ const containsTargetProgram = accountKeyArr.some(
+ (key) => key === programId
+ return containsTargetProgram
+ // 提取所有交易的优先费用
+ return programedTxs
+ .map((tx) => {
+ const priorityFeesSol = tx.meta?.fee
+ ? (tx.meta.fee - 5000) / LAMPORTS_PER_SOL // 减去基础费用 5000 lamports
+ : 0
+ return priorityFeesSol > 0 ? priorityFeesSol : 0
+ .filter((fee) => fee > 0)
+ console.error(`Error processing block ${block}:`, error)
+ return null
+ priorityFees.push(...fees)
+ console.log('done += 1', doneCount)
+ doneCount += 1
+ for (const bs of bss) {
+ getFees(bs).catch((error) => console.error(error.message))
+ await sleep(200)
+ while (!checkDone()) {
+ console.log(doneCount)
+ console.log('length:', priorityFees)
+ // 将所有费用合并到一个数组
+ const allFees = priorityFees
+ .filter((fees): fees is number[] => fees !== null)
+ .flat()
+ if (allFees.length === 0) {
+ median: 0,
+ average: 0,
+ max: 0,
+ min: 0,
+ numSamples: 0,
+ // 计算统计数据
+ allFees.sort((a, b) => a - b)
+ const median = allFees[Math.floor(allFees.length / 2)]
+ const average = allFees.reduce((a, b) => a + b) / allFees.length
+ const max = Math.max(...allFees)
+ const min = Math.min(...allFees)
+ median,
+ average,
+ max,
+ min,
+ numSamples: allFees.length,
+ console.error('Error fetching priority fees:', error)