├── .dockerignore ├── .editorconfig ├── .env_local ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── Dockerfile ├── README.md ├── cmd ├── client.js └── server.js ├── docker-compose.yml ├── docker-entrypoint-initdb.d └── init.sql ├── index.ts ├── package.json ├── patches └── web3-core-helpers+1.7.1.patch ├── pm2.json_bak ├── src ├── abi │ └── spv.json ├── config │ └── config.ts ├── context.ts ├── db.ts ├── maker.ts ├── models │ ├── MakerTransaction.ts │ ├── Transactions.ts │ └── index.ts ├── server.ts ├── server │ ├── controllers │ │ └── index.ts │ └── router.ts ├── service │ ├── MatchService.ts │ ├── spv.ts │ ├── subgraphs.ts │ ├── tcpInject.ts │ ├── transaction.ts │ └── watch.ts ├── types │ ├── const.ts │ └── index.ts └── utils │ ├── StraknetStatusConfirm.ts │ ├── cache.ts │ ├── index.ts │ ├── maker.ts │ └── oldUtils.ts ├── test ├── isEmpty.test.ts └── pushDydx.test.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs/* 3 | cache/* 4 | runtime/* 5 | dist/* 6 | .git 7 | .vscode 8 | .env -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Shared editor configurations that track the way prettier formats source. 2 | # 3 | # To use this in vscode: 4 | # 5 | # ext install EditorConfig 6 | 7 | root = true 8 | 9 | # Source files look unixy by default 10 | [*] 11 | end_of_line = lf 12 | insert_final_newline = true 13 | 14 | # Javascript and Typescript look like Google-style 15 | [*.{js,json,ts}] 16 | charset = utf-8 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # Not currently supported by vscode, but is supported others, e.g. vim. 21 | max_line_length = 80 -------------------------------------------------------------------------------- /.env_local: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .yalc 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | node: true, 5 | }, 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: "tsconfig.json", 9 | sourceType: "module", 10 | }, 11 | plugins: [ 12 | "unused-imports", 13 | "@typescript-eslint", 14 | "eslint-plugin-import", 15 | "simple-import-sort", 16 | ], 17 | extends: [ 18 | "plugin:@typescript-eslint/recommended", 19 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 20 | "prettier", 21 | ], 22 | rules: { 23 | "no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", 24 | "unused-imports/no-unused-imports": "error", 25 | "unused-imports/no-unused-vars": [ 26 | "warn", 27 | { 28 | vars: "all", 29 | varsIgnorePattern: "^_", 30 | args: "after-used", 31 | argsIgnorePattern: "^_", 32 | }, 33 | ], 34 | "@typescript-eslint/no-unsafe-member-access": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-unsafe-call": "off", 37 | "@typescript-eslint/no-unused-vars": "off", 38 | "@typescript-eslint/require-await": "off", 39 | "@typescript-eslint/no-unsafe-return": "off", 40 | "@typescript-eslint/await-thenable": "off", 41 | "@typescript-eslint/no-unsafe-assignment": "off", 42 | "@typescript-eslint/no-unsafe-argument": "off", 43 | "@typescript-eslint/ban-types": "off", 44 | "@typescript-eslint/restrict-template-expressions": "off", 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | coverage/ 6 | .idea/ 7 | run/ 8 | .DS_Store 9 | *.sw* 10 | *.un~ 11 | .nyc_output/ 12 | dist/ 13 | mysql*/ 14 | local.* 15 | .vscode/ 16 | cache/ 17 | runtime/ 18 | src/maker.ts 19 | src/config/chains.json 20 | src/config/testnet.json 21 | pm2.json 22 | .yalc 23 | yalc.lock 24 | .env 25 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | npm run prettier 4 | npm run lint 5 | npm run build 6 | git add -A -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github 2 | .dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "bracketSameLine": false, 6 | "jsxSingleQuote": false, 7 | "printWidth": 80, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "semi": true, 11 | "singleQuote": false, 12 | "tabWidth": 2, 13 | "trailingComma": "all", 14 | "useTabs": false 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.15 2 | RUN mkdir -p /home/makerTransationData 3 | WORKDIR /home/makerTransationData 4 | COPY ./ . 5 | RUN apt-get update 6 | RUN apt-get install -y vim 7 | RUN yarn config set ignore-engines true 8 | RUN yarn global add pm2 9 | RUN yarn install --network-timeout 600000 10 | RUN yarn run build 11 | EXPOSE 8001 12 | CMD ["node","./dist/index.js"] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # makerTransationData 2 | -------------------------------------------------------------------------------- /cmd/client.js: -------------------------------------------------------------------------------- 1 | const net = require("net"); 2 | const prompt = require("prompt"); 3 | const client = new net.Socket(); 4 | client.connect(8001, "127.0.0.1", function () { 5 | console.log( 6 | `Successfully connected to the ${client.remoteAddress}:${client.remotePort} server\n`, 7 | ); 8 | prompt.start(); 9 | prompt.get(["DydxApiKey"], function (err, result) { 10 | if (err) { 11 | console.error(err.message); 12 | return; 13 | } 14 | client.write( 15 | JSON.stringify({ 16 | op: "inject", 17 | data: { key: "11", value: result["DydxApiKey"] }, 18 | }), 19 | ); 20 | }); 21 | }); 22 | client.on("data", function (data) { 23 | console.log(`\nServer Response:` + data.toString()); 24 | }); 25 | client.on("end", function () { 26 | console.log("Send Data end"); 27 | }); 28 | -------------------------------------------------------------------------------- /cmd/server.js: -------------------------------------------------------------------------------- 1 | const net = require("net"); 2 | const subscribes = []; 3 | const pushData = {}; 4 | const server = net.createServer(function (socket) { 5 | socket.name = "client:" + Date.now(); 6 | socket.on("data", function (data) { 7 | // const readSize = socket.bytesRead; 8 | let body = data.toString(); 9 | try { 10 | body = JSON.parse(data.toString()); 11 | } catch (error) {} 12 | if (body && body.op === "subscribe") { 13 | subscribes.push(socket); 14 | socket.write( 15 | JSON.stringify({ op: "message", data: "Subscribe Success" }), 16 | ); 17 | } 18 | if (typeof body === "object" && body.op === "inject") { 19 | Object.assign(pushData, body.data); 20 | } 21 | socket.write( 22 | JSON.stringify({ op: "message", data: "Receive Inject Success" }), 23 | ); 24 | }); 25 | socket.on("close", function () { 26 | const index = subscribes.find(client => client.name === socket.name); 27 | subscribes.splice(index, 1); 28 | console.log("server closed!"); 29 | }); 30 | socket.on("error", function (err) { 31 | console.error("error", err); 32 | }); 33 | }); 34 | 35 | server.listen(8001, function () { 36 | console.log( 37 | `Start Server Success ${server.address().address}:${server.address().port}`, 38 | ); 39 | setInterval(() => { 40 | if (subscribes.length > 0 && Object.keys(pushData).length > 0) { 41 | subscribes.forEach(client => { 42 | console.debug("Send Inject Config To:", client.name); 43 | client.write(JSON.stringify({ op: "inject", data: pushData })); 44 | }); 45 | } 46 | }, 2000); 47 | }); 48 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | redis: 4 | image: redis:latest 5 | container_name: data-redis 6 | restart: always 7 | volumes: 8 | - ./runtime/redis:/data:rw 9 | ports: 10 | - "6389:6379" 11 | data-server: 12 | build: . 13 | container_name: data-server 14 | restart: always 15 | command: pm2-runtime start pm2.json 16 | ports: 17 | - '8001:8001' 18 | - '3000:3000' 19 | depends_on: 20 | - redis 21 | links: 22 | - redis 23 | volumes: 24 | - './runtime:/home/makerTransationData/runtime' 25 | - './pm2.json:/home/makerTransationData/pm2.json' 26 | # rabbitmq: 27 | # image: rabbitmq 28 | # ports: 29 | # - "5672:5672" 30 | # - "15672:15672" 31 | # hostname: rabbit 32 | # environment: 33 | # RABBITMQ_DEFAULT_VHOST: / 34 | # RABBITMQ_DEFAULT_USER: admin 35 | # RABBITMQ_DEFAULT_PASS: admin 36 | -------------------------------------------------------------------------------- /docker-entrypoint-initdb.d/init.sql: -------------------------------------------------------------------------------- 1 | -- create database if not exists orbiter_data default character set UTF8mb4 collate utf8mb4_unicode_ci; 2 | -- USE orbiter_data; 3 | SET NAMES utf8mb4; 4 | SET FOREIGN_KEY_CHECKS = 0; 5 | -- ---------------------------- 6 | -- Table structure for maker_transaction 7 | -- ---------------------------- 8 | DROP TABLE IF EXISTS `maker_transaction`; 9 | CREATE TABLE `maker_transaction` ( 10 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', 11 | `transcationId` varchar(100) DEFAULT NULL COMMENT 'transcationId', 12 | `inId` int(11) DEFAULT NULL COMMENT 'inId', 13 | `outId` int(11) DEFAULT NULL COMMENT 'outId', 14 | `fromChain` int(11) DEFAULT NULL COMMENT 'from Chain', 15 | `toChain` int(11) DEFAULT NULL COMMENT 'to Chain', 16 | `toAmount` varchar(255) DEFAULT NULL COMMENT 'toAmount', 17 | `replySender` varchar(255) DEFAULT NULL COMMENT 'maker Sender Address', 18 | `replyAccount` varchar(255) DEFAULT NULL COMMENT 'reply user Recipient', 19 | `createdAt` datetime NOT NULL, 20 | `updatedAt` datetime NOT NULL, 21 | PRIMARY KEY (`id`), 22 | UNIQUE KEY `trxid` (`transcationId`) USING BTREE, 23 | UNIQUE KEY `maker_transaction_ibfk_1` (`inId`) USING BTREE, 24 | UNIQUE KEY `maker_transaction_ibfk_2` (`outId`) USING BTREE, 25 | KEY `replySender` (`replySender`) USING BTREE 26 | ) ENGINE=InnoDB AUTO_INCREMENT=1998278 DEFAULT CHARSET=utf8; 27 | 28 | -- ---------------------------- 29 | -- Table structure for transaction 30 | -- ---------------------------- 31 | DROP TABLE IF EXISTS `transaction`; 32 | CREATE TABLE `transaction` ( 33 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', 34 | `hash` varchar(255) NOT NULL COMMENT 'Hash', 35 | `nonce` bigint(20) NOT NULL COMMENT 'Nonce', 36 | `blockHash` varchar(255) DEFAULT NULL COMMENT 'blockHash', 37 | `blockNumber` bigint(20) DEFAULT NULL COMMENT 'blockNumber', 38 | `transactionIndex` int(11) DEFAULT NULL COMMENT 'transactionIndex', 39 | `from` varchar(255) NOT NULL COMMENT 'from', 40 | `to` varchar(255) NOT NULL COMMENT 'to', 41 | `value` varchar(32) NOT NULL COMMENT 'value', 42 | `symbol` varchar(20) NOT NULL COMMENT 'symbol', 43 | `gasPrice` bigint(20) DEFAULT NULL COMMENT 'gasPrice', 44 | `gas` bigint(20) DEFAULT NULL COMMENT 'gas', 45 | `input` text COMMENT 'input', 46 | `status` tinyint(1) NOT NULL COMMENT 'status:0=PENDING,1=COMPLETE,2=REJECT,3=MatchFailed,4=refund,99= MatchSuccess,98=makerDelayTransfer', 47 | `tokenAddress` varchar(255) NOT NULL COMMENT 'tokenAddress', 48 | `timestamp` datetime NOT NULL COMMENT 'timestamp', 49 | `side` tinyint(1) DEFAULT NULL COMMENT 'side:0=user,1=maker', 50 | `fee` varchar(20) DEFAULT NULL COMMENT 'fee', 51 | `feeToken` varchar(20) DEFAULT NULL COMMENT 'feeToken', 52 | `chainId` int(11) NOT NULL COMMENT 'chainId', 53 | `source` varchar(20) DEFAULT NULL COMMENT 'source', 54 | `memo` varchar(50) DEFAULT NULL COMMENT 'memo', 55 | `extra` json DEFAULT NULL COMMENT 'extra', 56 | `replyAccount` varchar(255) DEFAULT NULL COMMENT 'replyUser', 57 | `replySender` varchar(255) DEFAULT NULL COMMENT 'replyMaker', 58 | `createdAt` datetime NOT NULL, 59 | `updatedAt` datetime NOT NULL, 60 | PRIMARY KEY (`id`,`timestamp`) USING BTREE, 61 | KEY `symbol` (`replySender`,`chainId`,`symbol`) USING BTREE 62 | ) ENGINE=InnoDB AUTO_INCREMENT=1091205 DEFAULT CHARSET=utf8; 63 | 64 | -- ---------------------------- 65 | -- View structure for data_size 66 | -- ---------------------------- 67 | DROP VIEW IF EXISTS `data_size`; 68 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `data_size` AS select concat(round(sum(((`information_schema`.`tables`.`DATA_LENGTH` / 1024) / 1024)),2),'M') AS `dataSize`,concat(round(sum(((`information_schema`.`tables`.`INDEX_LENGTH` / 1024) / 1024)),2),'M') AS `indexSize` from `information_schema`.`tables` where (`information_schema`.`tables`.`TABLE_SCHEMA` = 'orbiterTransaction'); 69 | 70 | -- ---------------------------- 71 | -- View structure for day_trx_statistics 72 | -- ---------------------------- 73 | DROP VIEW IF EXISTS `day_trx_statistics`; 74 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `day_trx_statistics` AS select count(1) AS `count(1)`,date_format(`t`.`timestamp`,'%Y-%m-%d') AS `ym` from `transaction` `t` where ((`t`.`chainId` = 3) and ((`t`.`from` = '0x80C67432656d59144cEFf962E8fAF8926599bCF8') or (`t`.`to` = '0x80C67432656d59144cEFf962E8fAF8926599bCF8')) and (`t`.`timestamp` >= '2022-06-01 00:00')) group by `ym` order by `ym`; 75 | 76 | -- ---------------------------- 77 | -- View structure for groupchain_trx_statistics 78 | -- ---------------------------- 79 | DROP VIEW IF EXISTS `groupchain_trx_statistics`; 80 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `groupchain_trx_statistics` AS select `transaction`.`chainId` AS `chainId`,count(1) AS `totalTrx` from `transaction` group by `transaction`.`chainId`; 81 | 82 | -- ---------------------------- 83 | -- View structure for matched_num 84 | -- ---------------------------- 85 | DROP VIEW IF EXISTS `matched_num`; 86 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `matched_num` AS select count(1) AS `count(1)` from `maker_transaction` where ((`maker_transaction`.`inId` is not null) and (`maker_transaction`.`outId` is not null)); 87 | 88 | -- ---------------------------- 89 | -- View structure for mismatch_num 90 | -- ---------------------------- 91 | DROP VIEW IF EXISTS `mismatch_num`; 92 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `mismatch_num` AS select sum((case when isnull(`maker_transaction`.`inId`) then 0 else 1 end)) AS `inTotal`,sum((case when isnull(`maker_transaction`.`outId`) then 0 else 1 end)) AS `outTotal`,count(1) AS `total` from `maker_transaction` where (isnull(`maker_transaction`.`inId`) or isnull(`maker_transaction`.`outId`)); 93 | 94 | -- ---------------------------- 95 | -- View structure for month_count 96 | -- ---------------------------- 97 | DROP VIEW IF EXISTS `month_count`; 98 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `month_count` AS select count(1) AS `trxCount`,date_format(`t`.`timestamp`,'%Y-%m') AS `ym` from `transaction` `t` group by `ym` order by `ym`; 99 | 100 | -- ---------------------------- 101 | -- View structure for query_match 102 | -- ---------------------------- 103 | DROP VIEW IF EXISTS `query_match`; 104 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `query_match` AS select `t1`.`hash` AS `fromHash`,`t2`.`hash` AS `toHash`,`t1`.`timestamp` AS `fromTime`,`t2`.`timestamp` AS `toTime`,`mt`.`id` AS `id`,`mt`.`transcationId` AS `transcationId`,`mt`.`inId` AS `inId`,`mt`.`outId` AS `outId`,`mt`.`fromChain` AS `fromChain`,`mt`.`toChain` AS `toChain`,`mt`.`toAmount` AS `toAmount`,`mt`.`replySender` AS `replySender`,`mt`.`replyAccount` AS `replyAccount`,`mt`.`createdAt` AS `createdAt`,`mt`.`updatedAt` AS `updatedAt` from ((`maker_transaction` `mt` join `transaction` `t1` on((`t1`.`id` = `mt`.`inId`))) join `transaction` `t2` on((`t2`.`id` = `mt`.`outId`))) where ((`mt`.`inId` is not null) and (`mt`.`outId` is not null)) order by `t1`.`timestamp` desc; 105 | 106 | -- ---------------------------- 107 | -- View structure for revenue_statistics 108 | -- ---------------------------- 109 | DROP VIEW IF EXISTS `revenue_statistics`; 110 | CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `revenue_statistics` AS select `inTx`.`timestamp` AS `timestamp`,`inTx`.`value` AS `inValue`,`inTx`.`symbol` AS `inSymbol`,`inTx`.`replySender` AS `replySender`,`inTx`.`replyAccount` AS `replyAccount`,`inTx`.`chainId` AS `fromChain`,`inTx`.`memo` AS `toChain`,`outTx`.`value` AS `outValue`,`outTx`.`symbol` AS `outSymbol`,`outTx`.`fee` AS `outFee`,`outTx`.`feeToken` AS `outFeeToken` from ((`transaction` `inTx` join `maker_transaction` `mt` on((`inTx`.`id` = `mt`.`inId`))) join `transaction` `outTx` on(((`mt`.`outId` = `outTx`.`id`) and (`inTx`.`status` in (99,98))))); 111 | 112 | -- ---------------------------- 113 | -- Procedure structure for auto_create_partition 114 | -- ---------------------------- 115 | DROP PROCEDURE IF EXISTS `auto_create_partition`; 116 | delimiter ;; 117 | CREATE PROCEDURE `auto_create_partition`(IN `table_name` varchar(64)) 118 | BEGIN 119 | SET @next_month:=CONCAT(date_format(date_add(now(),interval 2 month),'%Y%m'),'01'); 120 | SET @SQL = CONCAT( 'ALTER TABLE `', table_name, '`', 121 | ' ADD PARTITION (PARTITION p', @next_month, " VALUES LESS THAN (TO_DAYS(", 122 | @next_month ,")) );" ); 123 | PREPARE STMT FROM @SQL; 124 | EXECUTE STMT; 125 | DEALLOCATE PREPARE STMT; 126 | END 127 | ;; 128 | delimiter ; 129 | 130 | -- ---------------------------- 131 | -- Procedure structure for sp_add_partitio_transaction 132 | -- ---------------------------- 133 | DROP PROCEDURE IF EXISTS `sp_add_partitio_transaction`; 134 | delimiter ;; 135 | CREATE PROCEDURE `sp_add_partitio_transaction`() 136 | BEGIN 137 | DECLARE i,j INT UNSIGNED DEFAULT 1; 138 | DECLARE v_tmp_date DATE; 139 | SET @stmt = ''; 140 | SET @stmt_begin = 'ALTER TABLE transaction PARTITION BY RANGE COLUMNS (timestamp)('; 141 | SET i = 2021; 142 | WHILE i <= 2023 DO 143 | SET j = 1; 144 | WHILE j <= 12 DO 145 | SET v_tmp_date = CONCAT(i,'-01-01'); 146 | SET @stmt = CONCAT(@stmt,'PARTITION p',i,'_',LPAD(j,2,"0"),' VALUES LESS THAN (''',DATE_ADD(v_tmp_date,INTERVAL j MONTH),'''),'); 147 | SET j = j + 1; 148 | END WHILE; 149 | SET i = i + 1; 150 | END WHILE; 151 | SET @stmt_end = 'PARTITION p_max VALUES LESS THAN (maxvalue))'; 152 | SET @stmt = CONCAT(@stmt_begin,@stmt,@stmt_end); 153 | PREPARE s1 FROM @stmt; 154 | EXECUTE s1; 155 | DROP PREPARE s1; 156 | 157 | SET @stmt = NULL; 158 | SET @stmt_begin = NULL; 159 | SET @stmt_end = NULL; 160 | END 161 | ;; 162 | delimiter ; 163 | SET FOREIGN_KEY_CHECKS = 1; 164 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Watch } from "./src/service/watch"; 2 | import "dotenv/config"; 3 | import { SPV } from "./src/service/spv"; 4 | import { Context } from "./src/context"; 5 | import { createServer } from "./src/server"; 6 | import utc from "dayjs/plugin/utc"; 7 | import dayjs from "dayjs"; 8 | // import { StarknetStatusConfirm } from "./src/utils/StraknetStatusConfirm"; 9 | dayjs.extend(utc); 10 | export class Application { 11 | public ctx: Context = new Context(); 12 | async bootstrap() { 13 | await this.ctx.init(); 14 | createServer(this.ctx); 15 | const watch = new Watch(this.ctx); 16 | await watch.start(); 17 | // watch.readDBMatch("2022-10-18 00:47:33", "2022-10-19 00:47:33") 18 | // .then(result => { 19 | // this.ctx.logger.info(`readDBMatch end`, result); 20 | // }) 21 | // .catch((error: any) => { 22 | // this.ctx.logger.error(`readDBMatch error`, error); 23 | // }); 24 | if (this.ctx.isSpv) { 25 | const spvService = new SPV(this.ctx, Number(process.env["SPV_CHAIN"])); 26 | spvService.start().catch(error => { 27 | this.ctx.logger.error("SPV init tree error:", error); 28 | }); 29 | } 30 | // void new StarknetStatusConfirm(this.ctx, "4").start(); 31 | } 32 | } 33 | const app = new Application(); 34 | app.bootstrap().catch(error => { 35 | console.error("start app error", error); 36 | }); 37 | process.on("uncaughtException", (err: Error) => { 38 | console.error("Global Uncaught exception:", err); 39 | }); 40 | process.on("unhandledRejection", (err: Error) => { 41 | console.error( 42 | "There are failed functions where promise is not captured:", 43 | err, 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orbiter-spv", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "seqauto": "sequelize-auto -o ./src/models -d orbiter-new -h localhost -u root -p 3306 -x 123456 -e mysql -l ts", 8 | "dev": "ts-node ./index.ts", 9 | "pushServer": "ts-node ./cmd/server.js", 10 | "inject": "ts-node ./cmd/client.js", 11 | "start": "node ./dist/index.js", 12 | "build": "npm run clean && tsc && ncp package.json dist/package.json", 13 | "clean": "rimraf coverage output dist", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "lint": "eslint 'src/**/*.{js,ts}' 'index.ts' --fix", 16 | "prettier": "prettier --check --write --config .prettierrc.json '**/*.{ts,tsc}'", 17 | "prepare": "husky install", 18 | "postinstall": "patch-package" 19 | }, 20 | "bin": { 21 | "inject": "./dist/inject/client.js" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "@types/figlet": "^1.5.4", 27 | "@types/fs-extra": "^9.0.13", 28 | "@types/inquirer": "^8.2.1", 29 | "@types/lodash": "^4.14.182", 30 | "@types/node": "^18.0.0", 31 | "@types/sequelize": "^4.28.13", 32 | "@typescript-eslint/eslint-plugin": "^5.30.5", 33 | "@typescript-eslint/parser": "^5.30.5", 34 | "eslint": "^8.19.0", 35 | "eslint-config-prettier": "^8.5.0", 36 | "eslint-plugin-import": "^2.26.0", 37 | "eslint-plugin-simple-import-sort": "^7.0.0", 38 | "eslint-plugin-unused-imports": "^2.0.0", 39 | "husky": "^8.0.1", 40 | "ncp": "^2.0.0", 41 | "prettier": "^2.7.1", 42 | "rimraf": "^3.0.2", 43 | "ts-node": "^10.8.1", 44 | "typescript": "^4.7.4" 45 | }, 46 | "dependencies": { 47 | "@types/koa": "^2.13.5", 48 | "@types/koa-router": "^7.4.4", 49 | "bignumber.js": "^9.0.2", 50 | "cache-manager": "^5.1.3", 51 | "chalk": "^4.1.2", 52 | "dayjs": "^1.11.4", 53 | "dotenv": "^16.0.1", 54 | "ethers": "^5.6.9", 55 | "figlet": "^1.5.2", 56 | "fs-extra": "^10.1.0", 57 | "inquirer": "^8.2.4", 58 | "ioredis": "^5.2.0", 59 | "keccak256": "^1.0.6", 60 | "koa": "^2.13.4", 61 | "koa-body": "^5.0.0", 62 | "koa-router": "^12.0.0", 63 | "lodash": "^4.17.21", 64 | "merkletreejs": "^0.2.32", 65 | "mysql2": "^2.3.3", 66 | "orbiter-chaincore": "^1.3.5-alpha.21", 67 | "patch-package": "^6.4.7", 68 | "postinstall-postinstall": "^2.1.0", 69 | "prompt": "^1.3.0", 70 | "sequelize": "^6.21.0", 71 | "sequelize-auto": "^0.8.8", 72 | "sequelize-typescript": "^2.1.3", 73 | "web3": "^1.7.5" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /patches/web3-core-helpers+1.7.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/web3-core-helpers/lib/formatters.js b/node_modules/web3-core-helpers/lib/formatters.js 2 | index d5f30bc..7723383 100644 3 | --- a/node_modules/web3-core-helpers/lib/formatters.js 4 | +++ b/node_modules/web3-core-helpers/lib/formatters.js 5 | @@ -203,7 +203,7 @@ var outputTransactionFormatter = function (tx) { 6 | if (tx.transactionIndex !== null) 7 | tx.transactionIndex = utils.hexToNumber(tx.transactionIndex); 8 | tx.nonce = utils.hexToNumber(tx.nonce); 9 | - tx.gas = utils.hexToNumber(tx.gas); 10 | +if (tx.gas) tx.gas = outputBigNumberFormatter(tx.gas); 11 | if (tx.gasPrice) 12 | tx.gasPrice = outputBigNumberFormatter(tx.gasPrice); 13 | if (tx.maxFeePerGas) 14 | -------------------------------------------------------------------------------- /pm2.json_bak: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "inject_server", 5 | "exec_mode": "fork", 6 | "script": "server.js", 7 | "cwd": "./cmd/", 8 | "env": { 9 | "NODE_ENV": "test" 10 | } 11 | }, 12 | { 13 | "name": "makerTpransationData", 14 | "instances": 4, 15 | "exec_mode": "cluster", 16 | "script": "index.js", 17 | "cwd": "./dist/", 18 | "env": { 19 | "RUNTIME_DIR": "/home/makerTransationData", 20 | "MYSQL_DB_HOST": "xxxxx", 21 | "MYSQL_DB_NAME": "orbiter", 22 | "MYSQL_DB_USERNAME": "orbiter", 23 | "MYSQL_DB_PASSWORD": "xxxxx", 24 | "NODE_ENV": "production", 25 | "INSTANCES": 4, 26 | "REDIS_HOST": "redis", 27 | "REDIS_PORT": 6379 28 | 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/abi/spv.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "uint8", 8 | "name": "version", 9 | "type": "uint8" 10 | } 11 | ], 12 | "name": "Initialized", 13 | "type": "event" 14 | }, 15 | { 16 | "anonymous": false, 17 | "inputs": [ 18 | { 19 | "indexed": true, 20 | "internalType": "address", 21 | "name": "previousOwner", 22 | "type": "address" 23 | }, 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "newOwner", 28 | "type": "address" 29 | } 30 | ], 31 | "name": "OwnershipTransferred", 32 | "type": "event" 33 | }, 34 | { 35 | "inputs": [], 36 | "name": "initialize", 37 | "outputs": [], 38 | "stateMutability": "nonpayable", 39 | "type": "function" 40 | }, 41 | { 42 | "inputs": [ 43 | { 44 | "internalType": "uint256", 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "name": "makerTxTree", 50 | "outputs": [ 51 | { 52 | "internalType": "bytes32", 53 | "name": "", 54 | "type": "bytes32" 55 | } 56 | ], 57 | "stateMutability": "view", 58 | "type": "function" 59 | }, 60 | { 61 | "inputs": [], 62 | "name": "owner", 63 | "outputs": [ 64 | { 65 | "internalType": "address", 66 | "name": "", 67 | "type": "address" 68 | } 69 | ], 70 | "stateMutability": "view", 71 | "type": "function" 72 | }, 73 | { 74 | "inputs": [], 75 | "name": "renounceOwnership", 76 | "outputs": [], 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "inputs": [ 82 | { 83 | "internalType": "uint256", 84 | "name": "chain", 85 | "type": "uint256" 86 | }, 87 | { 88 | "internalType": "bytes32", 89 | "name": "root", 90 | "type": "bytes32" 91 | } 92 | ], 93 | "name": "setMakerTxTreeRoot", 94 | "outputs": [], 95 | "stateMutability": "nonpayable", 96 | "type": "function" 97 | }, 98 | { 99 | "inputs": [ 100 | { 101 | "internalType": "uint256", 102 | "name": "chain", 103 | "type": "uint256" 104 | }, 105 | { 106 | "internalType": "bytes32", 107 | "name": "root", 108 | "type": "bytes32" 109 | } 110 | ], 111 | "name": "setUserTxTreeRoot", 112 | "outputs": [], 113 | "stateMutability": "nonpayable", 114 | "type": "function" 115 | }, 116 | { 117 | "inputs": [ 118 | { 119 | "internalType": "address", 120 | "name": "newOwner", 121 | "type": "address" 122 | } 123 | ], 124 | "name": "transferOwnership", 125 | "outputs": [], 126 | "stateMutability": "nonpayable", 127 | "type": "function" 128 | }, 129 | { 130 | "inputs": [ 131 | { 132 | "internalType": "uint256", 133 | "name": "", 134 | "type": "uint256" 135 | } 136 | ], 137 | "name": "userTxTree", 138 | "outputs": [ 139 | { 140 | "internalType": "bytes32", 141 | "name": "", 142 | "type": "bytes32" 143 | } 144 | ], 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "inputs": [ 150 | { 151 | "components": [ 152 | { 153 | "internalType": "uint256", 154 | "name": "chainID", 155 | "type": "uint256" 156 | }, 157 | { 158 | "internalType": "bytes32", 159 | "name": "txHash", 160 | "type": "bytes32" 161 | }, 162 | { 163 | "internalType": "bytes32", 164 | "name": "lpid", 165 | "type": "bytes32" 166 | }, 167 | { 168 | "internalType": "address", 169 | "name": "sourceAddress", 170 | "type": "address" 171 | }, 172 | { 173 | "internalType": "address", 174 | "name": "destAddress", 175 | "type": "address" 176 | }, 177 | { 178 | "internalType": "address", 179 | "name": "tokenAddress", 180 | "type": "address" 181 | }, 182 | { 183 | "internalType": "uint256", 184 | "name": "amount", 185 | "type": "uint256" 186 | }, 187 | { 188 | "internalType": "uint256", 189 | "name": "nonce", 190 | "type": "uint256" 191 | }, 192 | { 193 | "internalType": "uint256", 194 | "name": "timestamp", 195 | "type": "uint256" 196 | }, 197 | { 198 | "internalType": "uint256", 199 | "name": "responseAmount", 200 | "type": "uint256" 201 | }, 202 | { 203 | "internalType": "uint256", 204 | "name": "ebcid", 205 | "type": "uint256" 206 | } 207 | ], 208 | "internalType": "struct OperationsLib.txInfo", 209 | "name": "_txInfo", 210 | "type": "tuple" 211 | }, 212 | { 213 | "internalType": "bytes32[]", 214 | "name": "_proof", 215 | "type": "bytes32[]" 216 | } 217 | ], 218 | "name": "verifyMakerTxProof", 219 | "outputs": [ 220 | { 221 | "internalType": "bool", 222 | "name": "", 223 | "type": "bool" 224 | } 225 | ], 226 | "stateMutability": "view", 227 | "type": "function" 228 | }, 229 | { 230 | "inputs": [ 231 | { 232 | "components": [ 233 | { 234 | "internalType": "uint256", 235 | "name": "chainID", 236 | "type": "uint256" 237 | }, 238 | { 239 | "internalType": "bytes32", 240 | "name": "txHash", 241 | "type": "bytes32" 242 | }, 243 | { 244 | "internalType": "bytes32", 245 | "name": "lpid", 246 | "type": "bytes32" 247 | }, 248 | { 249 | "internalType": "address", 250 | "name": "sourceAddress", 251 | "type": "address" 252 | }, 253 | { 254 | "internalType": "address", 255 | "name": "destAddress", 256 | "type": "address" 257 | }, 258 | { 259 | "internalType": "address", 260 | "name": "tokenAddress", 261 | "type": "address" 262 | }, 263 | { 264 | "internalType": "uint256", 265 | "name": "amount", 266 | "type": "uint256" 267 | }, 268 | { 269 | "internalType": "uint256", 270 | "name": "nonce", 271 | "type": "uint256" 272 | }, 273 | { 274 | "internalType": "uint256", 275 | "name": "timestamp", 276 | "type": "uint256" 277 | }, 278 | { 279 | "internalType": "uint256", 280 | "name": "responseAmount", 281 | "type": "uint256" 282 | }, 283 | { 284 | "internalType": "uint256", 285 | "name": "ebcid", 286 | "type": "uint256" 287 | } 288 | ], 289 | "internalType": "struct OperationsLib.txInfo", 290 | "name": "_txInfo", 291 | "type": "tuple" 292 | }, 293 | { 294 | "internalType": "bytes32[]", 295 | "name": "_proof", 296 | "type": "bytes32[]" 297 | } 298 | ], 299 | "name": "verifyUserTxProof", 300 | "outputs": [ 301 | { 302 | "internalType": "bool", 303 | "name": "", 304 | "type": "bool" 305 | } 306 | ], 307 | "stateMutability": "view", 308 | "type": "function" 309 | } 310 | ] 311 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | development: { 3 | dialect: "mysql", 4 | database: process.env.MYSQL_DB_NAME || "ob", 5 | username: process.env.MYSQL_DB_USERNAME || "root", 6 | password: process.env.MYSQL_DB_PASSWORD || "root", 7 | host: process.env.MYSQL_DB_HOST || "localhost", 8 | port: Number(process.env.MYSQL_DB_PORT || "3306"), 9 | logging: false, 10 | timezone: "+00:00", 11 | }, 12 | test: { 13 | dialect: "mysql", 14 | database: process.env.MYSQL_DB_NAME || "ob", 15 | username: process.env.MYSQL_DB_USERNAME || "root", 16 | password: process.env.MYSQL_DB_PASSWORD || "root", 17 | host: process.env.MYSQL_DB_HOST || "localhost", 18 | logging: false, 19 | port: Number(process.env.MYSQL_DB_PORT || "3306"), 20 | timezone: "+00:00", 21 | }, 22 | production: { 23 | dialect: "mysql", 24 | database: process.env.MYSQL_DB_NAME, 25 | username: process.env.MYSQL_DB_USERNAME, 26 | password: process.env.MYSQL_DB_PASSWORD, 27 | host: process.env.MYSQL_DB_HOST, 28 | port: Number(process.env.MYSQL_DB_PORT), 29 | logging: false, 30 | timezone: "+00:00", 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | import { readFile } from "fs/promises"; 3 | import { initModels } from "./models"; 4 | import { Config, IMarket } from "./types"; 5 | import { LoggerService } from "orbiter-chaincore/src/utils"; 6 | import { Logger } from "winston"; 7 | import { convertMarketListToFile } from "./utils"; 8 | import { TCPInject } from "./service/tcpInject"; 9 | import { chains } from "orbiter-chaincore"; 10 | import { makerList, makerListHistory } from "./maker"; 11 | import Subgraphs from "./service/subgraphs"; 12 | import db from "./db"; 13 | export class Context { 14 | public models = initModels(db); 15 | public logger!: Logger; 16 | public redis!: Redis; 17 | public instanceId: number; 18 | public instanceCount: number; 19 | public makerConfigs: Array = []; 20 | public NODE_ENV: "development" | "production" | "test" = ( 21 | (process.env["NODE_ENV"] || "development") 22 | ); 23 | public isSpv: boolean; 24 | public config: Config = { 25 | chains: [], 26 | chainsTokens: [], 27 | subgraphEndpoint: "", 28 | crossAddressTransferMap: { 29 | "0x07c57808b9cea7130c44aab2f8ca6147b04408943b48c6d8c3c83eb8cfdd8c0b": 30 | "0x06d1d401ae235ba01e5d8a6ade82a0f17aba7db4f8780194b4d65315071be10b", // eth 31 | "0x001709eA381e87D4c9ba5e4A67Adc9868C05e82556A53FD1b3A8b1F21e098143": 32 | "0x01a316c2a9eece495df038a074781ce3983b4dbda665b951cc52a3025690a448", // dai 33 | }, 34 | L1L2Mapping: { 35 | "0x80c67432656d59144ceff962e8faf8926599bcf8": 36 | "0x07c57808b9cea7130c44aab2f8ca6147b04408943b48c6d8c3c83eb8cfdd8c0b", 37 | "0x095d2918b03b2e86d68551dcf11302121fb626c9": 38 | "0x001709ea381e87d4c9ba5e4a67adc9868c05e82556a53fd1b3a8b1f21e098143", 39 | "0x0043d60e87c5dd08c86c3123340705a1556c4719": 40 | "0x001709ea381e87d4c9ba5e4a67adc9868c05e82556a53fd1b3a8b1f21e098143", 41 | }, 42 | }; 43 | private async initChainConfigs() { 44 | const file = `${ 45 | this.NODE_ENV === "production" ? "chains" : "testnet" 46 | }.json`; 47 | const result = await readFile(`./src/config/${file}`); 48 | const configs = JSON.parse(result.toString()); 49 | chains.fill(configs); 50 | this.config.chains = chains.getAllChains(); 51 | return configs; 52 | } 53 | private initLogger() { 54 | this.logger = LoggerService.createLogger({ 55 | dir: `${process.env.RUNTIME_DIR || ""}/logs${this.instanceId}`, 56 | }); 57 | } 58 | private initRedis() { 59 | const { REDIS_PORT, REDIS_HOST, REDIS_DB, REDIS_PASS } = process.env; 60 | this.redis = new Redis({ 61 | port: Number(REDIS_PORT || 6379), // Redis port 62 | host: REDIS_HOST || "127.0.0.1", // Redis host 63 | password: REDIS_PASS, 64 | db: Number(REDIS_DB || this.instanceId), // Defaults to 0 65 | }); 66 | } 67 | async init() { 68 | await this.initChainConfigs(); 69 | chains.fill(this.config.chains); 70 | const subApi = new Subgraphs(this.config.subgraphEndpoint); 71 | // Update LP regularly 72 | if (this.isSpv) { 73 | try { 74 | this.makerConfigs = await subApi.getAllLp(); 75 | } catch (error) { 76 | this.logger.error("init LP error", error); 77 | } 78 | this.config.chainsTokens = await subApi.getChains(); 79 | setInterval(() => { 80 | try { 81 | subApi 82 | .getAllLp() 83 | .then(result => { 84 | if (result && result.length > 0) { 85 | this.makerConfigs = result; 86 | } 87 | }) 88 | .catch(error => { 89 | this.logger.error("setInterval getAllLp error:", error); 90 | }); 91 | if (Date.now() % 6 === 0) { 92 | subApi 93 | .getChains() 94 | .then(chainsTokens => { 95 | if (chainsTokens) { 96 | this.config.chainsTokens = chainsTokens; 97 | } 98 | }) 99 | .catch(error => { 100 | this.logger.error("setInterval getChains error:", error); 101 | }); 102 | } 103 | } catch (error) { 104 | this.logger.error("setInterval error:", error); 105 | } 106 | }, 1000 * 10); 107 | } else { 108 | await fetchFileMakerList(this); 109 | } 110 | } 111 | constructor() { 112 | this.isSpv = process.env["IS_SPV"] === "1"; 113 | this.config.subgraphEndpoint = process.env["SUBGRAPHS"] || ""; 114 | this.instanceId = Number(process.env.NODE_APP_INSTANCE || 0); 115 | this.instanceCount = Number(process.env.INSTANCES || 1); 116 | this.initLogger(); 117 | this.initRedis(); 118 | new TCPInject(this); 119 | } 120 | } 121 | export async function fetchFileMakerList(ctx: Context) { 122 | // ------------- 123 | ctx.makerConfigs = await convertMarketListToFile(makerList, ctx); 124 | const makerConfigsHistory = await convertMarketListToFile( 125 | makerListHistory, 126 | ctx, 127 | ); 128 | ctx.makerConfigs.push(...makerConfigsHistory); 129 | } 130 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Options } from "sequelize"; 2 | import configs from "./config/config"; 3 | 4 | const env = process.env.NODE_ENV || "development"; 5 | 6 | const config = (configs as { [key: string]: Options })[env]; 7 | const db: Sequelize = new Sequelize({ 8 | ...config, 9 | define: { 10 | underscored: false, 11 | }, 12 | }); 13 | 14 | db.sync({}).catch(error => { 15 | console.error("sequelize sync error:", error); 16 | }); 17 | export default db; 18 | -------------------------------------------------------------------------------- /src/models/MakerTransaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreationOptional, 3 | DataTypes, 4 | InferCreationAttributes, 5 | InferAttributes, 6 | Model, 7 | Sequelize, 8 | } from "sequelize"; 9 | export class MakerTransaction extends Model< 10 | InferAttributes, 11 | InferCreationAttributes 12 | > { 13 | declare id: CreationOptional; 14 | declare transcationId: string; 15 | declare inId: number | null; 16 | declare outId: number | null; 17 | declare fromChain: number | null; 18 | declare toChain: number | null; 19 | declare toAmount: string | null; 20 | declare replySender: string | null; 21 | declare replyAccount: string | null; 22 | declare createdAt: CreationOptional; 23 | declare updatedAt: CreationOptional; 24 | 25 | static initModel(sequelize: Sequelize): typeof MakerTransaction { 26 | return MakerTransaction.init( 27 | { 28 | id: { 29 | autoIncrement: true, 30 | type: DataTypes.BIGINT, 31 | allowNull: false, 32 | primaryKey: true, 33 | comment: "ID", 34 | }, 35 | transcationId: { 36 | type: DataTypes.STRING(100), 37 | allowNull: true, 38 | comment: "transcationId", 39 | unique: "trxid", 40 | }, 41 | inId: { 42 | type: DataTypes.BIGINT, 43 | allowNull: true, 44 | comment: "inId", 45 | unique: "maker_transaction_inId", 46 | }, 47 | outId: { 48 | type: DataTypes.BIGINT, 49 | allowNull: true, 50 | comment: "outId", 51 | unique: "maker_transaction_outId", 52 | }, 53 | fromChain: { 54 | type: DataTypes.INTEGER, 55 | allowNull: true, 56 | comment: "from Chain", 57 | }, 58 | toChain: { 59 | type: DataTypes.INTEGER, 60 | allowNull: true, 61 | comment: "to Chain", 62 | }, 63 | toAmount: { 64 | type: DataTypes.STRING(255), 65 | allowNull: true, 66 | comment: "toAmount", 67 | }, 68 | replySender: { 69 | type: DataTypes.STRING(255), 70 | allowNull: true, 71 | comment: "maker Sender Address", 72 | }, 73 | replyAccount: { 74 | type: DataTypes.STRING(255), 75 | allowNull: true, 76 | comment: "reply user Recipient", 77 | }, 78 | createdAt: { 79 | type: DataTypes.DATE, 80 | }, 81 | updatedAt: { 82 | type: DataTypes.DATE, 83 | }, 84 | }, 85 | { 86 | sequelize, 87 | tableName: "maker_transaction", 88 | timestamps: true, 89 | indexes: [ 90 | { 91 | name: "PRIMARY", 92 | unique: true, 93 | using: "BTREE", 94 | fields: [{ name: "id" }], 95 | }, 96 | // { 97 | // name: "trxid", 98 | // unique: true, 99 | // using: "BTREE", 100 | // fields: [{ name: "transcationId" }], 101 | // }, 102 | { 103 | name: "maker_transaction_inId", 104 | unique: true, 105 | using: "BTREE", 106 | fields: [{ name: "inId" }], 107 | }, 108 | // { 109 | // name: "maker_transaction_outId", 110 | // unique: true, 111 | // using: "BTREE", 112 | // fields: [{ name: "outId" }], 113 | // }, 114 | { 115 | name: "replySender", 116 | using: "BTREE", 117 | fields: [{ name: "replySender" }], 118 | }, 119 | ], 120 | }, 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/models/Transactions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreationOptional, 3 | DataTypes, 4 | InferCreationAttributes, 5 | InferAttributes, 6 | Model, 7 | Sequelize, 8 | } from "sequelize"; 9 | import { Json } from "../types"; 10 | 11 | export class Transaction extends Model< 12 | InferAttributes, 13 | InferCreationAttributes 14 | > { 15 | declare id: CreationOptional; 16 | declare hash: string; 17 | declare nonce: string; 18 | declare blockHash: string | null; 19 | declare blockNumber: number | null; 20 | declare transactionIndex: number | null; 21 | declare from: string; 22 | declare to: string; 23 | declare value: string; 24 | declare symbol: string; 25 | declare gasPrice: number | null; 26 | declare gas: number | null; 27 | declare input: string | null; 28 | declare status: number; 29 | declare tokenAddress: string | null; 30 | declare timestamp: Date; 31 | declare fee: string | null; 32 | declare feeToken: string | null; 33 | declare chainId: number; 34 | declare source: string | null; 35 | declare memo: string | null; 36 | declare side: number; 37 | declare makerId: string | null; 38 | declare transferId: string; 39 | declare expectValue: string | null; 40 | declare lpId: string | null; 41 | declare extra: Json | null; 42 | declare replyAccount: string | null; 43 | declare replySender: string | null; 44 | declare createdAt: CreationOptional; 45 | declare updatedAt: CreationOptional; 46 | 47 | static initModel(sequelize: Sequelize): typeof Transaction { 48 | return Transaction.init( 49 | { 50 | id: { 51 | autoIncrement: true, 52 | type: DataTypes.BIGINT, 53 | allowNull: false, 54 | primaryKey: true, 55 | comment: "ID", 56 | }, 57 | hash: { 58 | type: DataTypes.STRING(255), 59 | allowNull: false, 60 | comment: "Hash", 61 | }, 62 | nonce: { 63 | type: DataTypes.STRING(20), 64 | allowNull: false, 65 | comment: "Nonce", 66 | }, 67 | blockHash: { 68 | type: DataTypes.STRING(255), 69 | allowNull: true, 70 | comment: "blockHash", 71 | }, 72 | blockNumber: { 73 | type: DataTypes.BIGINT, 74 | allowNull: true, 75 | comment: "blockNumber", 76 | }, 77 | transactionIndex: { 78 | type: DataTypes.INTEGER, 79 | allowNull: true, 80 | comment: "transactionIndex", 81 | }, 82 | from: { 83 | type: DataTypes.STRING(255), 84 | allowNull: false, 85 | comment: "from", 86 | }, 87 | to: { 88 | type: DataTypes.STRING(255), 89 | allowNull: false, 90 | comment: "to", 91 | }, 92 | value: { 93 | type: DataTypes.STRING(32), 94 | allowNull: false, 95 | comment: "value", 96 | }, 97 | symbol: { 98 | type: DataTypes.STRING(20), 99 | allowNull: false, 100 | comment: "symbol", 101 | }, 102 | gasPrice: { 103 | type: DataTypes.BIGINT, 104 | allowNull: true, 105 | comment: "gasPrice", 106 | }, 107 | gas: { 108 | type: DataTypes.BIGINT, 109 | allowNull: true, 110 | comment: "gas", 111 | }, 112 | input: { 113 | type: DataTypes.TEXT, 114 | allowNull: true, 115 | comment: "input", 116 | }, 117 | status: { 118 | type: DataTypes.BOOLEAN, 119 | allowNull: false, 120 | comment: 121 | "status:0=PENDING,1=COMPLETE,2=REJECT,3=MatchFailed,4=refund,5=Timers Not Match,99=MatchSuccess,98=MakerDelayTransfer", 122 | }, 123 | tokenAddress: { 124 | type: DataTypes.STRING(255), 125 | allowNull: false, 126 | comment: "tokenAddress", 127 | }, 128 | timestamp: { 129 | type: DataTypes.DATE, 130 | allowNull: false, 131 | comment: "timestamp", 132 | }, 133 | fee: { 134 | type: DataTypes.STRING(20), 135 | allowNull: true, 136 | comment: "fee", 137 | }, 138 | feeToken: { 139 | type: DataTypes.STRING(20), 140 | allowNull: true, 141 | comment: "feeToken", 142 | }, 143 | chainId: { 144 | type: DataTypes.INTEGER, 145 | allowNull: false, 146 | comment: "chainId", 147 | }, 148 | source: { 149 | type: DataTypes.STRING(20), 150 | allowNull: true, 151 | comment: "source", 152 | }, 153 | memo: { 154 | type: DataTypes.STRING(50), 155 | allowNull: true, 156 | comment: "memo", 157 | }, 158 | side: { 159 | type: DataTypes.TINYINT, 160 | allowNull: false, 161 | comment: "side:0=user,1=maker", 162 | }, 163 | makerId: { 164 | type: DataTypes.STRING, 165 | allowNull: true, 166 | comment: "maker id", 167 | }, 168 | transferId: { 169 | type: DataTypes.STRING(32), 170 | allowNull: true, 171 | comment: "transferId", 172 | }, 173 | expectValue: { 174 | type: DataTypes.STRING(32), 175 | allowNull: true, 176 | comment: "expectValue", 177 | }, 178 | lpId: { 179 | type: DataTypes.STRING, 180 | allowNull: true, 181 | comment: "lp id", 182 | }, 183 | extra: { 184 | type: DataTypes.JSON, 185 | allowNull: true, 186 | comment: "extra", 187 | }, 188 | replyAccount: { 189 | type: DataTypes.STRING(255), 190 | allowNull: true, 191 | }, 192 | replySender: { 193 | type: DataTypes.STRING(255), 194 | allowNull: true, 195 | }, 196 | createdAt: { 197 | type: DataTypes.DATE, 198 | }, 199 | updatedAt: { 200 | type: DataTypes.DATE, 201 | }, 202 | }, 203 | { 204 | sequelize, 205 | tableName: "transaction", 206 | timestamps: true, 207 | indexes: [ 208 | { 209 | name: "PRIMARY", 210 | unique: true, 211 | using: "BTREE", 212 | fields: [{ name: "id" }], 213 | // fields: [{ name: "id" }, { name: "timestamp" }], 214 | }, 215 | { 216 | name: "hash", 217 | unique: true, 218 | using: "BTREE", 219 | fields: [{ name: "hash" }], 220 | // fields: [{ name: "chainId" }, { name: "hash" }], 221 | }, 222 | { 223 | name: "symbol", 224 | using: "BTREE", 225 | fields: [ 226 | { name: "replySender" }, 227 | { name: "chainId" }, 228 | { name: "symbol" }, 229 | ], 230 | }, 231 | ], 232 | }, 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import type { Sequelize } from "sequelize"; 2 | import { Transaction } from "./Transactions"; 3 | import { MakerTransaction } from "./MakerTransaction"; 4 | export { Transaction, MakerTransaction }; 5 | export function initModels(sequelize: Sequelize) { 6 | MakerTransaction.initModel(sequelize); 7 | Transaction.initModel(sequelize); 8 | MakerTransaction.belongsTo(Transaction, { as: "in", foreignKey: "inId" }); 9 | Transaction.hasOne(MakerTransaction, { 10 | as: "maker_transaction", 11 | foreignKey: "inId", 12 | }); 13 | MakerTransaction.belongsTo(Transaction, { as: "out", foreignKey: "outId" }); 14 | Transaction.hasOne(MakerTransaction, { 15 | as: "out_maker_transaction", 16 | foreignKey: "outId", 17 | }); 18 | return { 19 | sequelize, 20 | Transaction, 21 | MakerTransaction, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "./context"; 2 | import Koa2 from "koa"; 3 | import router from "./server/router"; 4 | export function createServer(spvCtx: Context) { 5 | const app = new Koa2(); 6 | app.use(async (ctx, next) => { 7 | ctx.state.spvCtx = spvCtx; 8 | try { 9 | await next(); 10 | } catch (error: any) { 11 | console.error(error); 12 | return (ctx.body = { 13 | errno: 1000, 14 | errmsg: error["message"], 15 | }); 16 | } 17 | }); 18 | app.use(router.routes()); 19 | const port = process.env["PORT"] || 3000; 20 | app.listen(port, () => { 21 | console.log(`Api Service Start: http://127.0.0.1:${port}`); 22 | }); 23 | return app; 24 | } 25 | -------------------------------------------------------------------------------- /src/server/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { SPV } from "./../../service/spv"; 2 | import { isEmpty } from "orbiter-chaincore/src/utils/core"; 3 | import { Context } from "../../context"; 4 | import Router from "koa-router"; 5 | import dayjs from "dayjs"; 6 | import { Op } from "sequelize"; 7 | 8 | export async function getMakerTransactionsCount(ctx: Router.RouterContext) { 9 | const spvCtx = ctx.state["spvCtx"] as Context; 10 | const query = ctx.request.query; 11 | const makerId = query["makerId"]; 12 | if (!makerId) { 13 | return (ctx.body = { 14 | errno: 1000, 15 | errmsg: "Missing parameter", 16 | }); 17 | } 18 | const where: any = { 19 | makerId, status: { 20 | [Op.or]: [0, 1], 21 | }, 22 | }; 23 | const count: number = 24 | await spvCtx.models.Transaction.count({ 25 | where, 26 | }) || 0; 27 | ctx.body = { 28 | errno: 0, 29 | data: { count }, 30 | }; 31 | } 32 | export async function getTransferTransactions(ctx: Router.RouterContext) { 33 | const queryType = ctx.params["type"] || "all"; 34 | const spvCtx = ctx.state["spvCtx"] as Context; 35 | const query = ctx.request.query; 36 | const page = Number(query["page"]) || 1; 37 | const pageSize = Number(query["pageSize"]) || 10; 38 | const filterAddress = query["replyAccount"]; 39 | const where: any = { 40 | replyAccount: filterAddress, 41 | }; 42 | if (isEmpty(query) || isEmpty(query["replyAccount"])) { 43 | return (ctx.body = { 44 | errno: 1000, 45 | errmsg: "Missing parameter", 46 | }); 47 | } 48 | if (query["status"]) { 49 | where["status"] = Number(query["status"]); 50 | } 51 | switch (queryType) { 52 | case "in": 53 | where["to"] = filterAddress; 54 | break; 55 | case "out": 56 | where["from"] = filterAddress; 57 | break; 58 | case "appealable": 59 | where["from"] = filterAddress; 60 | where["side"] = 0; 61 | where["status"] = 1; 62 | // where["timestamp"] = { 63 | // [Op.lte]: dayjs() 64 | // .subtract(1, "s") 65 | // .subtract(spvCtx.config.makerTransferTimeout, "m") 66 | // .toDate(), 67 | // }; 68 | break; 69 | } 70 | const result: any = 71 | (await spvCtx.models.Transaction.findAndCountAll({ 72 | raw: true, 73 | attributes: [ 74 | "hash", 75 | "from", 76 | "to", 77 | "chainId", 78 | "symbol", 79 | "value", 80 | "side", 81 | "status", 82 | "memo", 83 | "nonce", 84 | "timestamp", 85 | "makerId", 86 | "lpId", 87 | "expectValue", 88 | "tokenAddress", 89 | "extra", 90 | ], 91 | limit: pageSize, 92 | offset: pageSize * (page - 1), 93 | where, 94 | })) || {}; 95 | for (const row of result.rows) { 96 | row.ebcId = row.extra.ebcId; 97 | row.expectSafetyCode = 0; 98 | row.timestamp = dayjs(row.timestamp).utc().unix(); 99 | if (row.side === 0) { 100 | row.expectSafetyCode = row.nonce; 101 | } 102 | delete row.extra; 103 | } 104 | result["page"] = page; 105 | result["pageSize"] = pageSize; 106 | ctx.body = { 107 | errno: 0, 108 | data: result, 109 | }; 110 | } 111 | export async function getDelayTransferProof(ctx: Router.RouterContext) { 112 | const query = ctx.request.query; 113 | const fromChain = query["fromChain"]; 114 | const fromTxId = query["fromTxId"]; 115 | const toTxId = query["toTxId"]; 116 | if ( 117 | isEmpty(query) || 118 | isEmpty(fromChain) || 119 | isEmpty(fromTxId) || 120 | isEmpty(toTxId) 121 | ) { 122 | return (ctx.body = { 123 | errno: 1000, 124 | errmsg: "Missing parameter chainId or txid", 125 | }); 126 | } 127 | const spvCtx = ctx.state["spvCtx"] as Context; 128 | // valid is exists 129 | const fromTx = await spvCtx.models.Transaction.findOne({ 130 | raw: true, 131 | where: { 132 | chainId: Number(fromChain), 133 | side: 0, 134 | status: 98, 135 | hash: fromTxId, 136 | }, 137 | }); 138 | if (isEmpty(fromTx) || !fromTx) { 139 | return (ctx.body = { 140 | errno: 1000, 141 | errmsg: "From Transaction does not exist", 142 | }); 143 | } 144 | 145 | if (fromTx.status != 98) { 146 | return (ctx.body = { 147 | errno: 1000, 148 | errmsg: "Incorrect transaction status", 149 | }); 150 | } 151 | 152 | const toChain = Number(fromTx?.memo); 153 | const toTx = await spvCtx.models.Transaction.findOne({ 154 | raw: true, 155 | where: { 156 | chainId: Number(toChain), 157 | side: 1, 158 | hash: toTxId, 159 | }, 160 | }); 161 | if (isEmpty(toTx) || !toTx) { 162 | return (ctx.body = { 163 | errno: 1000, 164 | data: toTx, 165 | errmsg: "To Transaction does not exist", 166 | }); 167 | } 168 | 169 | // get 170 | const mtTx = await spvCtx.models.MakerTransaction.findOne({ 171 | attributes: ["id"], 172 | where: { 173 | inId: fromTx.id, 174 | outId: toTx.id, 175 | }, 176 | }); 177 | if (!mtTx || isEmpty(mtTx)) { 178 | return (ctx.body = { 179 | errno: 1000, 180 | data: null, 181 | errmsg: "Collection records do not match", 182 | }); 183 | } 184 | const { hex, leaf } = await SPV.calculateLeaf(toTx); 185 | const delayedPayment = SPV.tree[String(toChain)].delayedPayment; 186 | if (!delayedPayment) { 187 | return (ctx.body = { 188 | errno: 0, 189 | data: [], 190 | errmsg: "proof non-existent", 191 | }); 192 | } 193 | const proof = delayedPayment.getHexProof(hex); 194 | // const extra:any = txTx.extra || {}; 195 | ctx.body = { 196 | errno: 0, 197 | data: { 198 | tx: leaf, 199 | proof, 200 | }, 201 | errmsg: "", 202 | }; 203 | } 204 | 205 | export async function getUncollectedPaymentProof(ctx: Router.RouterContext) { 206 | const query = ctx.request.query; 207 | if (isEmpty(query) || isEmpty(query["chainId"]) || isEmpty(query["txid"])) { 208 | return (ctx.body = { 209 | errno: 1000, 210 | errmsg: "Missing parameter chainId or txid", 211 | }); 212 | } 213 | const spvCtx = ctx.state["spvCtx"] as Context; 214 | const tx = await spvCtx.models.Transaction.findOne({ 215 | raw: true, 216 | where: { 217 | chainId: Number(query["chainId"]), 218 | side: 0, 219 | hash: query["txid"], 220 | }, 221 | }); 222 | if (isEmpty(tx) || !tx) { 223 | return (ctx.body = { 224 | errno: 1000, 225 | data: tx, 226 | errmsg: `${query["txid"]} Tx Not Found`, 227 | }); 228 | } 229 | const { hex } = await SPV.calculateLeaf(tx); 230 | if (!SPV.tree[String(query["chainId"])]) { 231 | return (ctx.body = { 232 | errno: 1000, 233 | data: [], 234 | errmsg: "proof non-existent", 235 | }); 236 | } 237 | const uncollectedPayment = 238 | SPV.tree[String(query["chainId"])].uncollectedPayment; 239 | if (!uncollectedPayment) { 240 | return (ctx.body = { 241 | errno: 1000, 242 | data: [], 243 | errmsg: "non-existent", 244 | }); 245 | } 246 | const proof = uncollectedPayment.getHexProof(hex); 247 | ctx.body = { 248 | errno: 0, 249 | data: proof, 250 | errmsg: "", 251 | }; 252 | } 253 | export async function getLps(ctx: Router.RouterContext) { 254 | const spvCtx = ctx.state["spvCtx"] as Context; 255 | ctx.body = { 256 | errno: 0, 257 | data: spvCtx.makerConfigs, 258 | errmsg: "", 259 | }; 260 | } 261 | -------------------------------------------------------------------------------- /src/server/router.ts: -------------------------------------------------------------------------------- 1 | // import { isEmpty } from 'orbiter-chaincore/src/utils/core'; 2 | // import { Context } from './../context'; 3 | import * as controllers from "./controllers/index"; 4 | import Router from "koa-router"; 5 | const router = new Router(); 6 | router.get("/", ctx => { 7 | ctx.body = "welcome"; 8 | }); 9 | 10 | router.get( 11 | "/wallet/getTransferTransactions/:type", 12 | controllers.getTransferTransactions, 13 | ); 14 | router.get("/wallet/getDelayTransferProof", controllers.getDelayTransferProof); 15 | router.get( 16 | "/wallet/getUncollectedPaymentProof", 17 | controllers.getUncollectedPaymentProof, 18 | ); 19 | router.get("/lps", controllers.getLps); 20 | router.get( 21 | "/wallet/makerTransactions/count", 22 | controllers.getMakerTransactionsCount, 23 | ); 24 | // router.get('/getUncollectedPaymentProof', (ctx) => { 25 | // // const spvCtx = ctx.state['spvCtx'] as Context; 26 | // const query = ctx.request.query; 27 | // if (isEmpty(query) || isEmpty(query['chainId'] || isEmpty(query['txid']))) { 28 | // return ctx.body = { errno: 1000, errmsg: "Missing parameter chainId or txid" }; 29 | // } 30 | // ctx.body = 'welcome'; 31 | // }); 32 | export default router; 33 | -------------------------------------------------------------------------------- /src/service/MatchService.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orbiter-Finance/makerTransationData/ada6a439c551ecd411e3900e10452d03abddbf1f/src/service/MatchService.ts -------------------------------------------------------------------------------- /src/service/spv.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "orbiter-chaincore/src/utils/core"; 2 | import { Transaction as transactionAttributes } from "../models/Transactions"; 3 | import { chains } from "orbiter-chaincore"; 4 | import dayjs from "dayjs"; 5 | import { Contract, ethers, providers, utils } from "ethers"; 6 | import keccak256 from "keccak256"; 7 | import MerkleTree from "merkletreejs"; 8 | import { Op } from "sequelize"; 9 | import { Context } from "../context"; 10 | import SPVAbi from "../abi/spv.json"; 11 | import { orderBy } from "lodash"; 12 | import { groupWatchAddressByChain } from "../utils"; 13 | export class SPV { 14 | private rpcPovider!: providers.JsonRpcProvider; 15 | public static tree: { 16 | [key: string]: { 17 | uncollectedPayment: MerkleTree; 18 | delayedPayment: MerkleTree; 19 | }; 20 | } = {}; 21 | constructor( 22 | private readonly ctx: Context, 23 | private readonly contractChainId: number, 24 | ) { 25 | const chain = chains.getChainByInternalId(String(this.contractChainId)); 26 | if (chain) { 27 | this.rpcPovider = new providers.JsonRpcProvider(chain.rpc[0]); 28 | } 29 | } 30 | 31 | public static async calculateLeaf(tx: transactionAttributes) { 32 | let expectValue; 33 | let expectSafetyCode = 0; 34 | const extra: any = tx.extra || {}; 35 | if (tx.side === 0 && extra) { 36 | // user 37 | expectValue = tx.expectValue || "0"; 38 | expectSafetyCode = Number(tx.nonce); 39 | } else if (tx.side === 1) { 40 | expectValue = tx.value || "0"; 41 | expectSafetyCode = Number(tx.memo); 42 | } 43 | const ebcid = extra.ebcId || 0; 44 | const leaf = { 45 | lpId: tx.lpId, 46 | chain: tx.chainId, 47 | id: tx.hash, 48 | from: tx.from.toLowerCase(), 49 | to: tx.to.toLowerCase(), 50 | nonce: tx.nonce, 51 | value: tx.value, 52 | token: tx.tokenAddress, 53 | timestamp: dayjs(tx.timestamp).utc().unix(), 54 | expectValue, 55 | expectSafetyCode, 56 | ebcId: ebcid, 57 | }; 58 | const args = [ 59 | String(leaf.lpId), 60 | leaf.chain, 61 | leaf.id, 62 | leaf.from, 63 | leaf.to, 64 | Number(leaf.nonce), 65 | leaf.value, 66 | leaf.token, 67 | leaf.timestamp, 68 | leaf.expectValue, 69 | leaf.expectSafetyCode, 70 | Number(leaf.ebcId), 71 | ]; 72 | if (isEmpty(args[0])) { 73 | throw new Error("Missing parameter LPID"); 74 | } 75 | const hex = utils.solidityKeccak256( 76 | [ 77 | "bytes32", 78 | "uint256", 79 | "bytes32", 80 | "address", 81 | "address", 82 | "uint256", 83 | "uint256", 84 | "address", 85 | "uint256", 86 | "uint256", 87 | "uint256", 88 | "uint256", 89 | ], 90 | args, 91 | ); 92 | return { hex, leaf }; 93 | } 94 | public async start() { 95 | const chainGroup = groupWatchAddressByChain(this.ctx.makerConfigs); 96 | console.log("chainGroup:", chainGroup); 97 | for (const chainId in chainGroup) { 98 | const defaultLeafs = [ 99 | Buffer.from( 100 | "0000000000000000000000000000000000000000000000000000000000000000", 101 | "hex", 102 | ), 103 | ]; 104 | const tree = { 105 | uncollectedPayment: new MerkleTree(defaultLeafs, keccak256, { 106 | sort: true, 107 | }), 108 | delayedPayment: new MerkleTree(defaultLeafs, keccak256, { 109 | sort: true, 110 | }), 111 | }; 112 | SPV.tree[chainId] = tree; 113 | const spvByChain = new ChainSPVTree( 114 | this.ctx, 115 | Number(chainId), 116 | this.rpcPovider, 117 | tree, 118 | ); 119 | spvByChain.initTree().catch(error => { 120 | this.ctx.logger.error(`${chainId} initTree error:`, error); 121 | }); 122 | } 123 | } 124 | } 125 | 126 | export class ChainSPVTree { 127 | public tree: { 128 | userTxTree: MerkleTree; 129 | makerTxTree: MerkleTree; 130 | }; 131 | private maxTxId = { 132 | user: 0, 133 | maker: 0, 134 | }; 135 | constructor( 136 | private readonly ctx: Context, 137 | private readonly chainId: number, 138 | private readonly rpcPovider: providers.JsonRpcProvider, 139 | tree: any, 140 | ) { 141 | this.tree = { 142 | makerTxTree: tree["delayedPayment"], 143 | userTxTree: tree["uncollectedPayment"], 144 | }; 145 | } 146 | public async initTree() { 147 | // Timer refresh 148 | const refresh = () => { 149 | this.getUserNotRefundedTransactionList() 150 | .then(txList => { 151 | txList.length > 0 && this.updateUserTxTree(txList); 152 | }) 153 | .catch(error => { 154 | this.ctx.logger.error(`checkTree User error:`, error); 155 | }); 156 | this.getMakerDelayTransactionList() 157 | .then(txList => { 158 | txList.length > 0 && this.updateMakerTxTree(txList); 159 | }) 160 | .catch(error => { 161 | this.ctx.logger.error(`checkTree Maker error:`, error); 162 | }); 163 | }; 164 | refresh(); 165 | setInterval(refresh, 1000 * 60); 166 | return true; 167 | } 168 | 169 | public async updateMakerTxTree(txList: Array) { 170 | txList = orderBy(txList, ["id"], ["asc"]); 171 | for (const tx of txList) { 172 | const { hex } = await SPV.calculateLeaf(tx); 173 | if (this.tree.makerTxTree.getLeafIndex(Buffer.from(hex)) < 0) { 174 | if (tx.id > this.maxTxId.maker) { 175 | this.maxTxId.maker = tx.id; 176 | } 177 | this.tree.makerTxTree.addLeaf(hex); 178 | // this.tree.makerTxTree.addLeaf(Buffer.from(hex)); 179 | } 180 | } 181 | // 182 | // if (txList.length > 0) { 183 | const nowRoot = this.tree.makerTxTree.getHexRoot(); 184 | const onChainRoot = await this.getMakerTreeRoot(); 185 | console.debug( 186 | "makerTxTree:\n", 187 | this.tree.makerTxTree.toString(), 188 | `\ndiff:${onChainRoot}/${nowRoot}`, 189 | ); 190 | if (onChainRoot != nowRoot) { 191 | await this.setMakerTxTreeRoot(nowRoot); 192 | } 193 | // } 194 | } 195 | public async updateUserTxTree(txList: Array) { 196 | txList = orderBy(txList, ["id"], ["asc"]); 197 | for (const tx of txList) { 198 | const { hex } = await SPV.calculateLeaf(tx); 199 | if (this.tree.userTxTree.getLeafIndex(Buffer.from(hex)) < 0) { 200 | if (tx.id > this.maxTxId.user) { 201 | this.maxTxId.user = tx.id; 202 | } 203 | this.tree.userTxTree.addLeaf(hex as any); 204 | } 205 | } 206 | // 207 | // if (txList.length > 0) { 208 | const nowRoot = this.tree.userTxTree.getHexRoot(); 209 | const onChainRoot = await this.getUserTreeRoot(); 210 | console.debug( 211 | "userTxTree:\n", 212 | this.tree.userTxTree.toString(), 213 | `\ndiff:${onChainRoot}/${nowRoot}`, 214 | ); 215 | if (onChainRoot != nowRoot) { 216 | await this.setUserTxTreeRoot(nowRoot); 217 | } 218 | // } 219 | } 220 | public async getUserNotRefundedTransactionList(): Promise< 221 | Array 222 | > { 223 | const chainData = this.ctx.config.chainsTokens.find( 224 | row => row.id === this.chainId, 225 | ); 226 | if (!chainData) { 227 | this.ctx.logger.error( 228 | "getUserNotRefundedTransactionList getChain Not Found", 229 | ); 230 | return []; 231 | } 232 | const maxReceiptTime = chainData.maxReceiptTime; 233 | const where = { 234 | chainId: this.chainId, 235 | status: 1, 236 | side: 0, 237 | id: { 238 | [Op.gt]: this.maxTxId.user, 239 | }, 240 | timestamp: { 241 | [Op.lte]: dayjs().subtract(maxReceiptTime, "s").toDate(), 242 | }, 243 | }; 244 | const txList = await this.ctx.models.Transaction.findAll({ 245 | attributes: [ 246 | "id", 247 | "hash", 248 | "from", 249 | "to", 250 | "value", 251 | "nonce", 252 | "memo", 253 | "side", 254 | "tokenAddress", 255 | "symbol", 256 | "expectValue", 257 | "lpId", 258 | "makerId", 259 | "chainId", 260 | "timestamp", 261 | "extra", 262 | ], 263 | raw: true, 264 | where, 265 | }); 266 | return txList; 267 | } 268 | 269 | public async getMakerDelayTransactionList(): Promise< 270 | Array 271 | > { 272 | // TODO: 273 | const where = { 274 | chainId: this.chainId, 275 | status: 98, 276 | side: 1, 277 | id: { 278 | [Op.gt]: this.maxTxId.maker, 279 | }, 280 | }; 281 | const txList = await this.ctx.models.Transaction.findAll({ 282 | attributes: [ 283 | "id", 284 | "hash", 285 | "from", 286 | "to", 287 | "value", 288 | "nonce", 289 | "memo", 290 | "side", 291 | "tokenAddress", 292 | "symbol", 293 | "expectValue", 294 | "lpId", 295 | "makerId", 296 | "chainId", 297 | "timestamp", 298 | "extra", 299 | ], 300 | raw: true, 301 | where, 302 | }); 303 | return txList; 304 | } 305 | public async setUserTxTreeRoot(root: string) { 306 | const { SPV_CONTRACT, SPV_WALLET } = process.env; 307 | if (!SPV_CONTRACT) { 308 | throw new Error("SPV_CONTRACT Not Found"); 309 | } 310 | if (!SPV_WALLET) { 311 | throw new Error("SPV_WALLET Not Found"); 312 | } 313 | const wallet = new ethers.Wallet(SPV_WALLET, this.rpcPovider); 314 | const spvContract = new Contract(SPV_CONTRACT, SPVAbi, wallet); 315 | try { 316 | const params: any = {}; 317 | if (process.env["GAS_LIMIT"]) 318 | params["gasLimit"] = Number(process.env["GAS_LIMIT"]); 319 | const tx = await spvContract.setUserTxTreeRoot( 320 | this.chainId, 321 | root, 322 | params, 323 | ); 324 | await tx.wait(); 325 | this.ctx.logger.info( 326 | `${this.chainId} setUserTxTreeRoot success:${tx.hash}`, 327 | ); 328 | return tx; 329 | } catch (error) { 330 | this.ctx.logger.error(`${this.chainId} setUserTxTreeRoot error:`, error); 331 | } 332 | } 333 | public async setMakerTxTreeRoot(root: string) { 334 | const { SPV_CONTRACT, SPV_WALLET } = process.env; 335 | if (!SPV_CONTRACT) { 336 | throw new Error("SPV_CONTRACT Not Found"); 337 | } 338 | if (!SPV_WALLET) { 339 | throw new Error("SPV_WALLET Not Found"); 340 | } 341 | const wallet = new ethers.Wallet(SPV_WALLET, this.rpcPovider); 342 | const spvContract = new Contract(SPV_CONTRACT, SPVAbi, wallet); 343 | try { 344 | const params: any = {}; 345 | if (process.env["GAS_LIMIT"]) 346 | params["gasLimit"] = Number(process.env["GAS_LIMIT"]); 347 | const tx = await spvContract.setMakerTxTreeRoot( 348 | this.chainId, 349 | root, 350 | params, 351 | ); 352 | await tx.wait(); 353 | this.ctx.logger.info( 354 | `${this.chainId} setMakerTxTreeRoot success:${tx.hash}`, 355 | ); 356 | return tx; 357 | } catch (error) { 358 | this.ctx.logger.error(`${this.chainId} setMakerTxTreeRoot error:`, error); 359 | } 360 | } 361 | public async getUserTreeRoot() { 362 | const { SPV_CONTRACT, SPV_WALLET } = process.env; 363 | if (!SPV_CONTRACT) { 364 | throw new Error("SPV_CONTRACT Not Found"); 365 | } 366 | if (!SPV_WALLET) { 367 | throw new Error("SPV_WALLET Not Found"); 368 | } 369 | const wallet = new ethers.Wallet(SPV_WALLET, this.rpcPovider); 370 | const spvContract = new Contract(SPV_CONTRACT, SPVAbi, wallet); 371 | const root = await spvContract.userTxTree(this.chainId); 372 | return root; 373 | } 374 | public async getMakerTreeRoot() { 375 | const { SPV_CONTRACT, SPV_WALLET } = process.env; 376 | if (!SPV_CONTRACT) { 377 | throw new Error("SPV_CONTRACT Not Found"); 378 | } 379 | if (!SPV_WALLET) { 380 | throw new Error("SPV_WALLET Not Found"); 381 | } 382 | const wallet = new ethers.Wallet(SPV_WALLET, this.rpcPovider); 383 | const spvContract = new Contract(SPV_CONTRACT, SPVAbi, wallet); 384 | const root = await spvContract.makerTxTree(this.chainId); 385 | return root; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/service/subgraphs.ts: -------------------------------------------------------------------------------- 1 | import { convertChainLPToOldLP } from "../utils"; 2 | 3 | export default class Subgraphs { 4 | constructor(private readonly endpoint: string) {} 5 | 6 | public async getAllLp() { 7 | const headers = { 8 | "content-type": "application/json", 9 | }; 10 | const graphqlQuery = { 11 | operationName: "fetchLpList", 12 | query: `query fetchLpList { 13 | lpEntities(where: {status_in:[1,2]}){ 14 | id 15 | createdAt 16 | maxPrice 17 | minPrice 18 | sourcePresion 19 | destPresion 20 | tradingFee 21 | gasFee 22 | startTime 23 | stopTime 24 | maker { 25 | id 26 | owner 27 | } 28 | pair { 29 | id 30 | sourceChain 31 | destChain 32 | sourceToken 33 | destToken 34 | ebcId 35 | } 36 | } 37 | }`, 38 | variables: {}, 39 | }; 40 | 41 | const options = { 42 | method: "POST", 43 | headers: headers, 44 | body: JSON.stringify(graphqlQuery), 45 | }; 46 | 47 | const response = await fetch(this.endpoint, options); 48 | const data = await response.json(); 49 | // 50 | const lpEntities = data.data["lpEntities"]; 51 | if (!(lpEntities && Array.isArray(lpEntities))) { 52 | throw new Error("Get LP List Fail"); 53 | } 54 | const convertData = convertChainLPToOldLP(lpEntities); 55 | return convertData; 56 | } 57 | 58 | public async getChains() { 59 | const headers = { 60 | "content-type": "application/json", 61 | }; 62 | const graphqlQuery = { 63 | operationName: "getChains", 64 | query: `query { 65 | chainEntities { 66 | id 67 | maxDisputeTime 68 | maxReceiptTime 69 | batchLimit 70 | tokenList { 71 | id 72 | decimals 73 | } 74 | } 75 | }`, 76 | variables: {}, 77 | }; 78 | 79 | const options = { 80 | method: "POST", 81 | headers: headers, 82 | body: JSON.stringify(graphqlQuery), 83 | }; 84 | 85 | const response = await fetch(this.endpoint, options); 86 | const { data } = await response.json(); 87 | if (!data) { 88 | console.error("getChains fail", data); 89 | return; 90 | } 91 | return data.chainEntities.map((row: any) => { 92 | row.maxDisputeTime = Number(row.maxDisputeTime); 93 | row.maxReceiptTime = Number(row.maxReceiptTime); 94 | row.batchLimit = Number(row.batchLimit); 95 | row.id = Number(row.id); 96 | return row; 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/service/tcpInject.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import net from "net"; 3 | import { equals } from "orbiter-chaincore/src/utils/core"; 4 | import { sleep } from "../utils"; 5 | import { chains } from "orbiter-chaincore"; 6 | export class TCPInject { 7 | public client: net.Socket; 8 | constructor(public readonly ctx: Context) { 9 | this.client = new net.Socket(); 10 | this.client.connect(8001, "127.0.0.1", () => { 11 | console.log("[Inject-Service] Successfully connected to the server\n"); 12 | this.client.write( 13 | JSON.stringify({ 14 | op: "subscribe", 15 | data: "", 16 | }), 17 | ); 18 | }); 19 | this.client.on("data", (str: string) => { 20 | let body: any = {}; 21 | try { 22 | body = JSON.parse(str); 23 | } catch (err) {} 24 | if (body && body.op === "inject") { 25 | const chain = chains 26 | .getAllChains() 27 | .find(row => equals(row.internalId, body.data.key)); 28 | if (!chain) { 29 | return ctx.logger.error( 30 | `Inject Key Not Find Chain Config ${body.data.key}`, 31 | ); 32 | } 33 | chain.api.key = body.data.value; 34 | } 35 | }); 36 | // client.on("end", () => { 37 | // console.log("Send Data end"); 38 | // }); 39 | this.client.on("error", error => { 40 | if ((Date.now() / 1000) * 10 === 0) { 41 | ctx.logger.error("sub error:", error); 42 | } 43 | sleep(1000 * 10) 44 | .then(() => { 45 | // subscribeInject(ctx); 46 | }) 47 | .catch(error => { 48 | ctx.logger.error("sleep error:", error); 49 | }); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/service/transaction.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from "./../models/Transactions"; 2 | import { MakerTransaction } from "../models/MakerTransaction"; 3 | import dayjs from "dayjs"; 4 | import { chains } from "orbiter-chaincore"; 5 | import { ITransaction, TransactionStatus } from "orbiter-chaincore/src/types"; 6 | import { dydx } from "orbiter-chaincore/src/utils"; 7 | import BigNumber from "bignumber.js"; 8 | import { 9 | equals, 10 | fix0xPadStartAddress, 11 | isEmpty, 12 | } from "orbiter-chaincore/src/utils/core"; 13 | import { InferAttributes, InferCreationAttributes, Op } from "sequelize"; 14 | import { Context } from "../context"; 15 | import { TranferId, TransactionID } from "../utils"; 16 | import { getAmountFlag, getAmountToSend } from "../utils/oldUtils"; 17 | import { IMarket } from "../types"; 18 | import { Transaction as transactionAttributes } from "../models/Transactions"; 19 | 20 | export async function findByHashTxMatch( 21 | ctx: Context, 22 | hashOrId: number | string, 23 | ) { 24 | const where: any = {}; 25 | if (typeof hashOrId == "string") { 26 | where["hash"] = String(hashOrId); 27 | } else { 28 | where["id"] = Number(hashOrId); 29 | } 30 | const tx = await ctx.models.Transaction.findOne({ 31 | raw: true, 32 | attributes: { exclude: ["input", "blockHash", "transactionIndex"] }, 33 | where, 34 | }); 35 | if (!tx || !tx.id) { 36 | throw new Error(` hash ${hashOrId} Tx Not Found`); 37 | } 38 | if (![1, 99].includes(tx.status)) { 39 | ctx.logger.error(`Tx ${tx.hash} Incorrect transaction status`); 40 | return { 41 | inId: null, 42 | outId: null, 43 | }; 44 | } 45 | 46 | if ( 47 | isEmpty(tx.from) || 48 | isEmpty(tx.to) || 49 | isEmpty(tx.value) || 50 | isEmpty(String(tx.nonce)) || 51 | isEmpty(tx.symbol) 52 | ) { 53 | ctx.logger.error(`Tx ${tx.hash} Missing required parameters`, { 54 | from: tx.from, 55 | to: tx.to, 56 | value: tx.value, 57 | nonce: tx.nonce, 58 | symbol: tx.symbol, 59 | }); 60 | return { inId: null, outId: null }; 61 | } 62 | const isMakerSend = 63 | ctx.makerConfigs.findIndex((row: IMarket) => 64 | equals(row.sender, tx.from), 65 | ) !== -1; 66 | const isUserSend = 67 | ctx.makerConfigs.findIndex((row: IMarket) => 68 | equals(row.recipient, tx.to), 69 | ) !== -1; 70 | const mtTx = await ctx.models.MakerTransaction.findOne({ 71 | attributes: ["id", "inId", "outId"], 72 | raw: true, 73 | where: { 74 | [Op.or]: { 75 | inId: tx.id, 76 | outId: tx.id, 77 | }, 78 | }, 79 | }); 80 | if (mtTx && mtTx.inId && mtTx.outId) { 81 | await ctx.models.Transaction.update( 82 | { 83 | status: 99, 84 | }, 85 | { 86 | where: { 87 | id: { 88 | [Op.in]: [mtTx.inId, mtTx.outId], 89 | }, 90 | }, 91 | }, 92 | ); 93 | return { 94 | inId: mtTx.inId, 95 | outId: mtTx.outId, 96 | }; 97 | } 98 | if (isMakerSend) { 99 | try { 100 | return await processMakerSendUserTx(ctx, tx); 101 | } catch (error: any) { 102 | ctx.logger.error(`processMakerSendUserTx error: `, { 103 | error, 104 | tx, 105 | }); 106 | } 107 | } else if (isUserSend) { 108 | try { 109 | return await processUserSendMakerTx(ctx, tx); 110 | } catch (error: any) { 111 | ctx.logger.error(`processUserSendMakerTx error: `, { 112 | error, 113 | tx, 114 | }); 115 | } 116 | } else { 117 | ctx.logger.error( 118 | `findByHashTxMatch matchSourceData This transaction is not matched to the merchant address: ${tx.hash}`, 119 | tx, 120 | ); 121 | } 122 | } 123 | export async function bulkCreateTransaction( 124 | ctx: Context, 125 | txlist: Array, 126 | ): Promise>> { 127 | const upsertList: Array> = []; 128 | for (const tx of txlist) { 129 | // ctx.logger.info(`processSubTx:${tx.hash}`); 130 | const chainConfig = chains.getChainByChainId(tx.chainId); 131 | if (!chainConfig) { 132 | ctx.logger.error(`getChainByInternalId chainId ${tx.chainId} not found`); 133 | continue; 134 | } 135 | if ( 136 | chainConfig.tokens.findIndex(row => 137 | equals(row.address, String(tx.tokenAddress)), 138 | ) < 0 139 | ) { 140 | ctx.logger.error(`${tx.hash} Tx ${tx.tokenAddress} Token Not Found`); 141 | continue; 142 | } 143 | // ctx.logger.info( 144 | // `[${chainConfig.name}] chain:${chainConfig.internalId}, hash:${tx.hash}` 145 | // ); 146 | let memo = getAmountFlag(Number(chainConfig.internalId), String(tx.value)); 147 | const txExtra = tx.extra || {}; 148 | if (["9", "99"].includes(chainConfig.internalId) && txExtra) { 149 | memo = String(txExtra.memo % 9000); 150 | } else if ( 151 | ["11", "511"].includes(chainConfig.internalId) && 152 | txExtra["type"] === "TRANSFER_OUT" 153 | ) { 154 | if (!tx.to) { 155 | tx.to = dydx.getEthereumAddressFromClientId(txExtra["clientId"]); 156 | } 157 | // makerAddress 158 | if (!tx.from) { 159 | const makerItem = await ctx.makerConfigs.find( 160 | (row: { toChain: { id: number } }) => 161 | equals(row.toChain.id, Number(chainConfig.internalId)), 162 | ); 163 | tx.from = (makerItem && makerItem.sender) || ""; 164 | } 165 | } 166 | const txData: Partial = { 167 | hash: tx.hash.toLowerCase(), 168 | nonce: String(tx.nonce), 169 | blockHash: tx.blockHash, 170 | blockNumber: tx.blockNumber, 171 | transactionIndex: tx.transactionIndex, 172 | from: tx.from, 173 | to: tx.to, 174 | value: new BigNumber(String(tx.value)).toFixed(), 175 | symbol: tx.symbol, 176 | gasPrice: tx.gasPrice, 177 | gas: tx.gas, 178 | input: tx.input != "0x" ? tx.input : undefined, 179 | status: tx.status, 180 | tokenAddress: tx.tokenAddress || "", 181 | timestamp: dayjs(tx.timestamp * 1000) 182 | .utc() 183 | .toDate(), 184 | fee: String(tx.fee), 185 | feeToken: tx.feeToken, 186 | chainId: Number(chainConfig.internalId), 187 | source: tx.source, 188 | extra: {}, 189 | memo, 190 | replyAccount: undefined, 191 | replySender: undefined, 192 | side: 0, 193 | makerId: undefined, 194 | lpId: undefined, 195 | expectValue: undefined, 196 | }; 197 | const saveExtra: any = { 198 | ebcId: "", 199 | }; 200 | const isMakerSend = 201 | ctx.makerConfigs.findIndex((row: { sender: any }) => 202 | equals(row.sender, tx.from), 203 | ) !== -1; 204 | const isUserSend = 205 | ctx.makerConfigs.findIndex((row: { recipient: any }) => 206 | equals(row.recipient, tx.to), 207 | ) !== -1; 208 | if (isMakerSend) { 209 | txData.side = 1; 210 | // maker send 211 | txData.replyAccount = txData.to; 212 | txData.replySender = txData.from; 213 | txData.transferId = TranferId( 214 | String(txData.chainId), 215 | String(txData.replySender), 216 | String(txData.replyAccount), 217 | String(txData.memo), 218 | String(txData.symbol), 219 | txData.value, 220 | ); 221 | } else if (isUserSend) { 222 | txData.side = 0; 223 | const fromChainId = Number(txData.chainId); 224 | const toChainId = Number(txData.memo); 225 | // user send 226 | // Calculation collection ID 227 | txData.replyAccount = txData.from; 228 | if ([44, 4, 11, 511].includes(fromChainId)) { 229 | // dydx contract send 230 | // starknet contract send 231 | txData.replyAccount = txExtra["ext"] || ""; 232 | } else if ([44, 4, 11, 511].includes(toChainId)) { 233 | const ext = txExtra["ext"] || ""; 234 | saveExtra["ext"] = ext; 235 | // 11,511 0x02 first 236 | // 4, 44 0x03 first 237 | txData.replyAccount = `0x${ext.substring(4)}`; 238 | if ([44, 4].includes(toChainId)) { 239 | txData.replyAccount = fix0xPadStartAddress(txData.replyAccount, 66); 240 | } 241 | } 242 | const market = ctx.makerConfigs.find( 243 | m => 244 | equals(m.fromChain.id, fromChainId) && 245 | equals(m.toChain.id, toChainId) && 246 | equals(m.recipient, String(txData.to)) && 247 | equals(m.fromChain.symbol, String(txData.symbol)) && 248 | equals(m.fromChain.tokenAddress, String(txData.tokenAddress)) && 249 | dayjs(txData.timestamp).unix() >= m.times[0] && 250 | dayjs(txData.timestamp).unix() <= m.times[1], 251 | ); 252 | if (!market) { 253 | // market not found 254 | txData.status = 3; 255 | } else { 256 | // valid timestamp 257 | txData.lpId = market.id || null; 258 | txData.makerId = market.makerId || null; 259 | // ebc 260 | saveExtra["ebcId"] = market.ebcId; 261 | txData.replySender = market.sender; 262 | // calc response amount 263 | try { 264 | txData.expectValue = String( 265 | await calcMakerSendAmount(ctx.makerConfigs, txData as any), 266 | ); 267 | txData.transferId = TranferId( 268 | toChainId, 269 | txData.replySender, 270 | String(txData.replyAccount), 271 | String(txData.nonce), 272 | String(txData.symbol), 273 | txData.expectValue, 274 | ); 275 | 276 | // if (new BigNumber(txData.expectValue).lt(new BigNumber(market.fromChain.minPrice)) || new BigNumber(txData.expectValue).gt(new BigNumber(market.fromChain.maxPrice))) { 277 | // // overflow 278 | // txData.status = 5; 279 | // } 280 | // TODO: valid maxPrice and minPrice 281 | // const minPrice = new BigNumber(market.pool.minPrice) 282 | // .plus(new BigNumber(market.pool.tradingFee)) 283 | // .multipliedBy(new BigNumber(10 ** market.fromChain.decimals)); 284 | // const maxPrice = new BigNumber(market.pool.maxPrice) 285 | // .plus(new BigNumber(market.pool.tradingFee)) 286 | // .multipliedBy(new BigNumber(10 ** market.fromChain.decimals)); 287 | // if () { 288 | // // txData.status = 5; 289 | // } 290 | } catch (error) { 291 | ctx.logger.error( 292 | "bulkCreateTransaction calcMakerSendAmount error:", 293 | error, 294 | ); 295 | } 296 | } 297 | } 298 | 299 | if ( 300 | [3, 33, 8, 88, 12, 512, 9, 99].includes(Number(txData.chainId)) && 301 | txData.status === TransactionStatus.PENDING 302 | ) { 303 | txData.status = TransactionStatus.COMPLETE; 304 | } 305 | txData.extra = saveExtra; 306 | upsertList.push(txData); 307 | } 308 | // calc response amount 309 | // const updateOnDuplicate:string[] = [ 310 | // // "hash", 311 | // "from", 312 | // "to", 313 | // "source", 314 | // "value", 315 | // "fee", 316 | // "feeToken", 317 | // "symbol", 318 | // "input", 319 | // "extra", 320 | // "timestamp", 321 | // "tokenAddress", 322 | // "nonce", 323 | // "memo", 324 | // "replyAccount", 325 | // "replySender", 326 | // "side", 327 | // "expectValue", 328 | // "lpId", 329 | // "makerId", 330 | // ]; 331 | for (const row of upsertList) { 332 | try { 333 | const [newTx, created] = await ctx.models.Transaction.findOrCreate({ 334 | defaults: row, 335 | attributes: ["id", "hash", "status", "expectValue"], 336 | where: { 337 | hash: row.hash, 338 | }, 339 | }); 340 | if (!created) { 341 | if (![0, 1].includes(row.status)) { 342 | newTx.status = row.status; 343 | await newTx.save(); 344 | } 345 | } 346 | row.id = newTx.id; 347 | } catch (error: any) { 348 | console.log(row); 349 | ctx.logger.error("processSubTx error:", error); 350 | throw error; 351 | } 352 | } 353 | // const upsertResult = await ctx.models.Transaction.bulkCreate(upsertList, { 354 | // updateOnDuplicate:updateOnDuplicate as any, 355 | // }); 356 | // for (const tx of upsertList) { 357 | // const item = await upsertResult.find(row => equals(row.hash, tx.hash)); 358 | // if (equals(item?.hash, tx.hash)) { 359 | // if (!item?.id) { 360 | // ctx.logger.error("空ID1", item?.id) 361 | // ctx.logger.error("空ID2", item?.getDataValue("id")) 362 | // ctx.logger.error("空ID3", item?.isNewRecord,'===', item?.get("id")) 363 | // } 364 | // tx.id = item?.id; 365 | // if (!item?.id) { 366 | // } 367 | // } else { 368 | // process.exit(1); 369 | // } 370 | // } 371 | return upsertList; 372 | } 373 | export async function calcMakerSendAmount( 374 | makerConfigs: Array, 375 | trx: transactionAttributes, 376 | ) { 377 | if ( 378 | isEmpty(trx.chainId) || 379 | isEmpty(trx.memo) || 380 | isEmpty(trx.symbol) || 381 | isEmpty(trx.tokenAddress) || 382 | isEmpty(trx.timestamp) 383 | ) { 384 | throw new Error("Missing parameter"); 385 | } 386 | const fromChainId = Number(trx.chainId); 387 | const toChainId = Number(trx.memo); 388 | const market = makerConfigs.find( 389 | m => 390 | equals(m.fromChain.id, String(fromChainId)) && 391 | equals(m.toChain.id, String(toChainId)) && 392 | equals(m.fromChain.symbol, trx.symbol) && 393 | equals(m.fromChain.tokenAddress, trx.tokenAddress) && 394 | dayjs(trx.timestamp).unix() >= m.times[0] && 395 | dayjs(trx.timestamp).unix() <= m.times[1], 396 | ); 397 | if (!market) { 398 | return 0; 399 | } 400 | return ( 401 | getAmountToSend( 402 | Number(fromChainId), 403 | Number(toChainId), 404 | trx.value.toString(), 405 | market.pool, 406 | trx.nonce, 407 | )?.tAmount || 0 408 | ); 409 | } 410 | export async function processUserSendMakerTx( 411 | ctx: Context, 412 | userTx: Transaction, 413 | ) { 414 | const t = await ctx.models.sequelize.transaction(); 415 | try { 416 | // const userTx = await ctx.models.Transaction.findOne({ 417 | // attributes: [ 418 | // "id", 419 | // "hash", 420 | // "transferId", 421 | // "chainId", 422 | // "from", 423 | // "to", 424 | // "tokenAddress", 425 | // "nonce", 426 | // "status", 427 | // "timestamp", 428 | // "value", 429 | // "expectValue", 430 | // "memo", 431 | // "symbol", 432 | // "makerId", 433 | // "lpId", 434 | // "replySender", 435 | // "replyAccount", 436 | // ], 437 | // where: { 438 | // hash, 439 | // // status: 1, 440 | // side: 0, 441 | // }, 442 | // include: [ 443 | // { 444 | // required: false, 445 | // attributes: ["id", "inId", "outId"], 446 | // model: ctx.models.MakerTransaction, 447 | // as: "maker_transaction", 448 | // }, 449 | // ], 450 | // transaction: t, 451 | // }); 452 | if (!userTx || isEmpty(userTx.id)) { 453 | throw new Error("Missing Id Or Transaction does not exist"); 454 | } 455 | if (!userTx || isEmpty(userTx.transferId)) { 456 | throw new Error("Missing transferId Or Transaction does not exist"); 457 | } 458 | 459 | const relInOut = (userTx)["maker_transaction"]; 460 | if (relInOut && relInOut.inId && relInOut.outId) { 461 | ctx.logger.error(`UserTx %s Already matched`, userTx.hash); 462 | return { 463 | inId: relInOut.inId, 464 | outId: relInOut.outId, 465 | errmsg: "UserTx Already matched", 466 | }; 467 | } 468 | if (userTx.status != 1 && userTx.status != 0) { 469 | throw new Error(`${userTx.hash} Current status cannot match`); 470 | } 471 | // user send to Maker 472 | const fromChainId = Number(userTx.chainId); 473 | const toChainId = Number(userTx.memo); 474 | const transcationId = TransactionID( 475 | String(userTx.from), 476 | userTx.chainId, 477 | userTx.nonce, 478 | userTx.symbol, 479 | dayjs(userTx.timestamp).valueOf(), 480 | ); 481 | 482 | const where = { 483 | transferId: userTx.transferId, 484 | status: [0, 1], 485 | side: 1, 486 | timestamp: { 487 | [Op.gte]: dayjs(userTx.timestamp).subtract(5, "m").toDate(), 488 | }, 489 | }; 490 | // Because of the delay of starknet network, the time will be longer if it is starknet 491 | if ([4, 44].includes(fromChainId)) { 492 | where.timestamp = { 493 | [Op.gte]: dayjs(userTx.timestamp).subtract(180, "m").toDate(), 494 | }; 495 | } 496 | const makerSendTx = await ctx.models.Transaction.findOne({ 497 | attributes: ["id", "timestamp"], 498 | where, 499 | order: [["timestamp", "asc"]], 500 | transaction: t, 501 | // include: [{ 502 | // required: false, 503 | // attributes: ['id', 'inId', 'outId'], 504 | // model: ctx.models.MakerTransaction, 505 | // as: 'out_maker_transaction' 506 | // }] 507 | }); 508 | const upsertData: Partial> = { 509 | transcationId, 510 | inId: userTx.id, 511 | fromChain: userTx.chainId, 512 | toChain: toChainId, 513 | toAmount: String(userTx.expectValue), 514 | replySender: userTx.replySender, 515 | replyAccount: userTx.replyAccount, 516 | }; 517 | 518 | if (makerSendTx && makerSendTx.id) { 519 | let maxReceiptTime = 1 * 60 * 60 * 24; 520 | if (ctx.isSpv) { 521 | const chainData = ctx.config.chainsTokens.find((row: any) => 522 | equals(row.id, userTx.chainId), 523 | ); 524 | if (!chainData) { 525 | ctx.logger.error("processUserSendMakerTx getChain Not Found"); 526 | return; 527 | } 528 | maxReceiptTime = chainData.maxReceiptTime; 529 | } 530 | upsertData.outId = makerSendTx.id; 531 | let upStatus = 99; 532 | const delayMin = dayjs(makerSendTx.timestamp).diff(userTx.timestamp, "s"); 533 | if (delayMin > maxReceiptTime) { 534 | upStatus = 98; // 535 | } 536 | makerSendTx.status = upStatus; 537 | makerSendTx.lpId = userTx.lpId; 538 | makerSendTx.makerId = userTx.makerId; 539 | await makerSendTx.save({ 540 | transaction: t, 541 | }); 542 | await ctx.models.Transaction.update( 543 | { 544 | status: upStatus, 545 | }, 546 | { 547 | where: { 548 | id: userTx.id, 549 | }, 550 | transaction: t, 551 | }, 552 | ); 553 | } 554 | await ctx.models.MakerTransaction.upsert(upsertData, { 555 | transaction: t, 556 | }); 557 | await t.commit(); 558 | return { inId: userTx.id, outId: makerSendTx?.id }; 559 | } catch (error) { 560 | await t.rollback(); 561 | ctx.logger.error("processUserSendMakerTx error", error); 562 | } 563 | } 564 | export async function quickMatchSuccess( 565 | ctx: Context, 566 | inId: number, 567 | outId: number, 568 | transferId: string, 569 | ) { 570 | const outTx = await ctx.models.Transaction.findOne({ 571 | attributes: ["id", "status"], 572 | where: { 573 | status: [0, 1], 574 | id: outId, 575 | }, 576 | }); 577 | if (!outTx) { 578 | return { 579 | inId, 580 | outId: null, 581 | errmsg: `No quick matching transactions found ${outId}`, 582 | }; 583 | } 584 | const rows = await ctx.models.MakerTransaction.update( 585 | { 586 | outId: outId, 587 | }, 588 | { 589 | where: { 590 | inId: inId, 591 | outId: null, 592 | }, 593 | }, 594 | ); 595 | if (rows.length == 1) { 596 | return { 597 | inId, 598 | outId, 599 | errmsg: "ok", 600 | }; 601 | } else { 602 | return { 603 | inId, 604 | outId, 605 | errmsg: "fail", 606 | }; 607 | } 608 | } 609 | 610 | export async function processMakerSendUserTx( 611 | ctx: Context, 612 | makerTx: Transaction, 613 | isCross?: boolean, 614 | ) { 615 | const t = await ctx.models.sequelize.transaction(); 616 | try { 617 | // const makerTx = await ctx.models.Transaction.findOne({ 618 | // attributes: [ 619 | // "id", 620 | // "transferId", 621 | // "chainId", 622 | // "status", 623 | // "timestamp", 624 | // "value", 625 | // "memo", 626 | // "symbol", 627 | // "replySender", 628 | // "replyAccount", 629 | // ], 630 | // where: { 631 | // hash, 632 | // // status: 1, 633 | // side: 1, 634 | // }, 635 | // transaction: t, 636 | // include: [ 637 | // { 638 | // required: false, 639 | // attributes: ["id", "inId", "outId"], 640 | // model: ctx.models.MakerTransaction, 641 | // as: "out_maker_transaction", 642 | // }, 643 | // ], 644 | // }); 645 | if (!makerTx || isEmpty(makerTx.id)) { 646 | throw new Error("Missing Id Or Transaction does not exist"); 647 | } 648 | if (!makerTx || isEmpty(makerTx.transferId)) { 649 | throw new Error("Missing transferId Or Transaction does not exist"); 650 | } 651 | const relInOut = (makerTx)["out_maker_transaction"]; 652 | if (relInOut && relInOut.inId && relInOut.outId) { 653 | ctx.logger.error(`MakerTx %s Already matched`, relInOut.hash); 654 | return { 655 | inId: relInOut.inId, 656 | outId: relInOut.outId, 657 | errmsg: "MakerTx Already matched", 658 | }; 659 | } 660 | if (makerTx.status != 1) { 661 | throw new Error(`${makerTx.hash} Current status cannot match`); 662 | } 663 | const models = ctx.models; 664 | let where: any = { 665 | transferId: makerTx.transferId, 666 | status: 1, 667 | side: 0, 668 | timestamp: { 669 | [Op.lte]: dayjs(makerTx.timestamp).add(5, "m").toDate(), 670 | }, 671 | }; 672 | if (isCross) { 673 | where = { 674 | memo: makerTx.chainId, 675 | // nonce: trx.memo, 676 | symbol: makerTx.symbol, 677 | replyAccount: makerTx.replyAccount, 678 | replySender: "", 679 | expectValue: { 680 | [Op.gte]: makerTx.value, 681 | [Op.lte]: new BigNumber(makerTx.value).plus(9000).toFixed(), 682 | }, 683 | status: 1, 684 | side: 0, 685 | timestamp: { 686 | [Op.lte]: dayjs(makerTx.timestamp).add(5, "m").toDate(), 687 | }, 688 | }; 689 | for (const addr1 in ctx.config.crossAddressTransferMap) { 690 | if ( 691 | equals( 692 | ctx.config.crossAddressTransferMap[addr1], 693 | String(makerTx.replySender), 694 | ) 695 | ) { 696 | where.replySender = addr1; 697 | break; 698 | } 699 | } 700 | if (equals(where.replySender, makerTx.replySender)) { 701 | throw new Error("Multi address collection mapping is not configured"); 702 | } 703 | } 704 | const userSendTx = await models.Transaction.findOne({ 705 | attributes: [ 706 | "id", 707 | "from", 708 | "to", 709 | "chainId", 710 | "symbol", 711 | "nonce", 712 | "timestamp", 713 | "lpId", 714 | "makerId", 715 | "replyAccount", 716 | "replySender", 717 | ], 718 | where, 719 | }); 720 | 721 | if (isEmpty(userSendTx) || !userSendTx) { 722 | return { 723 | outId: makerTx.id, 724 | inId: null, 725 | errmsg: "User transaction not found", 726 | }; 727 | } 728 | 729 | const upsertData: Partial> = { 730 | inId: userSendTx.id, 731 | outId: makerTx.id, 732 | toChain: makerTx.chainId, 733 | toAmount: String(makerTx.value), 734 | replySender: makerTx.replySender, 735 | replyAccount: makerTx.replyAccount, 736 | fromChain: userSendTx.chainId, 737 | }; 738 | upsertData.transcationId = TransactionID( 739 | String(userSendTx.from), 740 | userSendTx.chainId, 741 | userSendTx.nonce, 742 | userSendTx.symbol, 743 | dayjs(userSendTx.timestamp).valueOf(), 744 | ); 745 | let upStatus = 99; 746 | let maxReceiptTime = 1 * 60 * 60 * 24; 747 | if (ctx.isSpv) { 748 | const chainData = ctx.config.chainsTokens.find((row: any) => 749 | equals(row.id, userSendTx.chainId), 750 | ); 751 | if (!chainData) { 752 | ctx.logger.error("processMakerSendUserTx getChain Not Found"); 753 | return; 754 | } 755 | maxReceiptTime = chainData.maxReceiptTime; 756 | } 757 | // Check whether the payment is delayed in minutes 758 | const delayMin = dayjs(makerTx.timestamp).diff(userSendTx.timestamp, "s"); 759 | if (delayMin > maxReceiptTime) { 760 | upStatus = 98; // 761 | } 762 | userSendTx.status = upStatus; 763 | await userSendTx.save({ 764 | transaction: t, 765 | }); 766 | await ctx.models.Transaction.update( 767 | { 768 | status: upStatus, 769 | lpId: userSendTx.lpId, 770 | makerId: userSendTx.makerId, 771 | }, 772 | { 773 | where: { 774 | id: userSendTx.id, 775 | }, 776 | transaction: t, 777 | }, 778 | ); 779 | await models.MakerTransaction.upsert(upsertData, { 780 | transaction: t, 781 | }); 782 | await t.commit(); 783 | return { inId: userSendTx.id, outId: makerTx.id, errmsg: "ok" }; 784 | } catch (error) { 785 | t && (await t.rollback()); 786 | ctx.logger.error("processMakerTxCrossAddress error", error); 787 | } 788 | } 789 | -------------------------------------------------------------------------------- /src/service/watch.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "orbiter-chaincore/src/utils/core"; 2 | import { pubSub, ScanChainMain } from "orbiter-chaincore"; 3 | import { Transaction } from "orbiter-chaincore/src/types"; 4 | import { groupWatchAddressByChain } from "../utils"; 5 | import { Context } from "../context"; 6 | import { 7 | bulkCreateTransaction, 8 | findByHashTxMatch, 9 | processMakerSendUserTx, 10 | processUserSendMakerTx, 11 | quickMatchSuccess, 12 | } from "./transaction"; 13 | import dayjs from "dayjs"; 14 | import { 15 | TRANSACTION_RAW, 16 | MATCH_SUCCESS, 17 | USERTX_WAIT_MATCH, 18 | MAKERTX_WAIT_MATCH, 19 | MAKERTX_TRANSFERID, 20 | } from "../types/const"; 21 | import { Op } from "sequelize"; 22 | 23 | export class Watch { 24 | constructor(public readonly ctx: Context) {} 25 | public isMultiAddressPaymentCollection(makerAddress: string): boolean { 26 | return Object.values(this.ctx.config.crossAddressTransferMap).includes( 27 | makerAddress.toLowerCase(), 28 | ); 29 | } 30 | public async processSubTxList(txlist: Array) { 31 | const saveTxList = await bulkCreateTransaction(this.ctx, txlist); 32 | for (const tx of saveTxList) { 33 | // save log 34 | // if (tx.status != 1) { 35 | // continue; 36 | // } 37 | if (!tx.id) { 38 | this.ctx.logger.error(`Id non-existent`, tx); 39 | continue; 40 | } 41 | const reidsT = await this.ctx.redis.multi(); 42 | // const reidsT = await this.ctx.redis.multi() 43 | // .hset(TRANSACTION_RAW, tx.id, JSON.stringify(tx)); 44 | if (tx.side === 0) { 45 | const result = await processUserSendMakerTx(this.ctx, tx as any); 46 | if (result?.inId && result.outId) { 47 | // success 48 | await reidsT 49 | .hset(MATCH_SUCCESS, result.outId, result.inId) 50 | .hdel(MAKERTX_TRANSFERID, result.outId) 51 | .zrem(MAKERTX_WAIT_MATCH, result.outId); 52 | } else { 53 | await reidsT.hset(USERTX_WAIT_MATCH, tx.transferId, tx.id); 54 | } 55 | } 56 | if (tx.side === 1 && tx.replySender) { 57 | if (this.isMultiAddressPaymentCollection(tx.from)) { 58 | // Multi address payment collection 59 | await reidsT 60 | .hset(TRANSACTION_RAW, tx.id, JSON.stringify(tx)) 61 | .hset(MAKERTX_TRANSFERID, tx.id, `${tx.transferId}_cross`) 62 | .zadd(MAKERTX_WAIT_MATCH, dayjs(tx.timestamp).unix(), tx.id); 63 | } else { 64 | await reidsT 65 | .hset(MAKERTX_TRANSFERID, tx.id, tx.transferId) 66 | .zadd(MAKERTX_WAIT_MATCH, dayjs(tx.timestamp).unix(), tx.id); 67 | } 68 | } 69 | await reidsT.exec(); 70 | } 71 | return saveTxList; 72 | } 73 | public async start() { 74 | const ctx = this.ctx; 75 | try { 76 | const chainGroup = groupWatchAddressByChain(ctx.makerConfigs); 77 | const scanChain = new ScanChainMain(ctx.config.chains); 78 | for (const id in chainGroup) { 79 | if (Number(id) % this.ctx.instanceCount !== this.ctx.instanceId) { 80 | continue; 81 | } 82 | ctx.logger.info( 83 | `Start Subscribe ChainId: ${id}, instanceId:${this.ctx.instanceId}, instances:${this.ctx.instanceCount}`, 84 | ); 85 | pubSub.subscribe(`${id}:txlist`, (result: Transaction[]) => { 86 | this.processSubTxList(result) 87 | .then(result => { 88 | this.ctx.logger.info( 89 | `Received subscription transaction,instanceId:${this.ctx.instanceId}, instances:${this.ctx.instanceCount}`, 90 | result.map(tx => tx.hash), 91 | ); 92 | }) 93 | .catch(error => { 94 | ctx.logger.error(`${id} processSubTxList error:`, error); 95 | }); 96 | }); 97 | scanChain.startScanChain(id, chainGroup[id]).catch(error => { 98 | ctx.logger.error(`${id} startScanChain error:`, error); 99 | }); 100 | } 101 | process.on("SIGINT", () => { 102 | scanChain.pause().catch(error => { 103 | ctx.logger.error("chaincore pause error:", error); 104 | }); 105 | process.exit(0); 106 | }); 107 | } catch (error: any) { 108 | ctx.logger.error("startSub error:", error); 109 | } 110 | if (this.ctx.instanceId === 0) { 111 | this.readUserSendReMatch().catch(error => { 112 | this.ctx.logger.error("readUserSendReMatch error:", error); 113 | }); 114 | // processMakerTxCrossAddress(this.ctx, "0x64282d53de4947ce61c44e702d44afde73d5135c8a7c5d3a5e56262e8af8913").then(result => { 115 | // console.log(result, '==result'); 116 | // }).catch(error => { 117 | // console.log(error, '======error'); 118 | // }) 119 | // setInterval(() => { 120 | 121 | // }, 1000 * 60); 122 | // this.readQueneMatch().catch(error => { 123 | // this.ctx.logger.error("readQueneMatch error:", error); 124 | // }); 125 | // this.readMakerTxCacheReMatch().catch(error => { 126 | // this.ctx.logger.error("readMakerTxCacheReMatch error:", error); 127 | // }); 128 | } 129 | } 130 | // read db 131 | public async readUserSendReMatch(): Promise { 132 | const startAt = dayjs().subtract(7, "d").startOf("d").toDate(); 133 | let endAt = dayjs().subtract(1, "minute").toDate(); 134 | try { 135 | // read 136 | const txList = await this.ctx.models.Transaction.findAll({ 137 | raw: true, 138 | attributes: { exclude: ["input", "blockHash", "transactionIndex"] }, 139 | order: [["timestamp", "desc"]], 140 | limit: 500, 141 | where: { 142 | side: 0, 143 | status: [0, 1], 144 | timestamp: { 145 | [Op.gte]: startAt, 146 | [Op.lte]: endAt, 147 | }, 148 | }, 149 | }); 150 | for (const tx of txList) { 151 | await processUserSendMakerTx(this.ctx, tx).catch(error => { 152 | this.ctx.logger.error( 153 | `readDBMatch process total:${txList.length}, id:${tx.id},hash:${tx.hash}`, 154 | error, 155 | ); 156 | }); 157 | endAt = tx.timestamp; 158 | } 159 | // if (txList.length <= 0 || dayjs(endAt).isBefore(startAt)) { 160 | // return { startAt, endAt, count: txList.length }; 161 | // } 162 | } catch (error) { 163 | } finally { 164 | await sleep(1000 * 10); 165 | return await this.readUserSendReMatch(); 166 | } 167 | } 168 | public async readMakerTxCacheReMatch() { 169 | const outIdList: Array = await this.ctx.redis.zrangebyscore( 170 | MAKERTX_WAIT_MATCH, 171 | dayjs() 172 | .subtract(60 * 24 * 7, "minute") 173 | .unix(), 174 | dayjs().unix(), 175 | ); 176 | if (outIdList.length > 0) { 177 | for (const outTxId of outIdList) { 178 | const transferId = await this.ctx.redis.hget( 179 | MAKERTX_TRANSFERID, 180 | outTxId, 181 | ); 182 | if (!transferId) { 183 | continue; 184 | } 185 | let matchRes: any = { 186 | inId: null, 187 | outId: null, 188 | }; 189 | try { 190 | if (transferId.includes("_cross")) { 191 | // tx 192 | const txItem = await this.ctx.redis 193 | .hget(TRANSACTION_RAW, outTxId) 194 | .then(res => res && JSON.parse(res)); 195 | matchRes = await processMakerSendUserTx(this.ctx, txItem, true); 196 | } else { 197 | const inTxId = await this.ctx.redis.hget( 198 | USERTX_WAIT_MATCH, 199 | transferId, 200 | ); 201 | if (inTxId) { 202 | matchRes = await quickMatchSuccess( 203 | this.ctx, 204 | Number(inTxId), 205 | Number(outTxId), 206 | transferId, 207 | ); 208 | } else { 209 | const res = await findByHashTxMatch( 210 | this.ctx, 211 | Number(outTxId), 212 | ).catch(error => { 213 | this.ctx.logger.info( 214 | `readMakerTxCacheReMatch findByHashTxMatch error:`, 215 | error, 216 | ); 217 | }); 218 | } 219 | } 220 | if (matchRes.inId && matchRes.outId) { 221 | this.ctx.logger.info(`quickMatchSuccess result:`, matchRes); 222 | await this.ctx.redis 223 | .multi() 224 | .zrem(MAKERTX_WAIT_MATCH, outTxId) 225 | .hdel(MAKERTX_TRANSFERID, outTxId) 226 | .hdel(USERTX_WAIT_MATCH, transferId) 227 | .hset(MATCH_SUCCESS, matchRes.outId, matchRes.inId) 228 | .exec(); 229 | } 230 | } catch (error) { 231 | this.ctx.logger.error(`readUserCacheSendReMatch error:`, error); 232 | } 233 | } 234 | } 235 | await sleep(1000 * 10); 236 | 237 | return await this.readUserSendReMatch(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/types/const.ts: -------------------------------------------------------------------------------- 1 | // export const USER_MATCH_TX = `MatchTx:0`; 2 | export const TRANSACTION_RAW = `TRANSACTION_RAW`; 3 | export const MATCH_SUCCESS = `MATCH_SUCCESS`; 4 | export const USERTX_WAIT_MATCH = `MatchTx:0`; 5 | export const USERTX_ID_TRANSFERID = `USERTX_ID_TRANSFERID`; 6 | export const MAKERTX_WAIT_MATCH = `MatchTx:1`; 7 | export const MAKERTX_TRANSFERID = `MAKERTX_TRANSFERID`; 8 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IMarket { 2 | id: string; 3 | makerId: string; 4 | ebcId: string; 5 | recipient: string; 6 | sender: string; 7 | fromChain: { 8 | id: number; 9 | name: string; 10 | tokenAddress: string; 11 | symbol: string; 12 | decimals: number; 13 | maxPrice: number; 14 | minPrice: number; 15 | }; 16 | toChain: { 17 | id: number; 18 | name: string; 19 | tokenAddress: string; 20 | symbol: string; 21 | decimals: number; 22 | }; 23 | times: Number[]; 24 | pool?: any; 25 | } 26 | export interface Config { 27 | L1L2Mapping: { 28 | [key: string]: string; 29 | }; 30 | crossAddressTransferMap: { 31 | [key: string]: string; 32 | }; 33 | chainsTokens: Array; 34 | subgraphEndpoint: string; 35 | chains: Array; 36 | } 37 | 38 | export interface JsonMap { 39 | [member: string]: string | number | boolean | null | JsonArray | JsonMap; 40 | } 41 | 42 | export type JsonArray = Array< 43 | string | number | boolean | null | JsonArray | JsonMap 44 | >; 45 | 46 | export type Json = JsonMap | JsonArray | string | number | boolean | null; 47 | -------------------------------------------------------------------------------- /src/utils/StraknetStatusConfirm.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import * as fs from "fs-extra"; 3 | import { chains, chainService } from "orbiter-chaincore"; 4 | import { Op } from "sequelize"; 5 | import { Context } from "./../context"; 6 | 7 | export class StarknetStatusConfirm { 8 | constructor( 9 | private readonly ctx: Context, 10 | private readonly chainId: string, 11 | ) {} 12 | async start(): Promise { 13 | const { count, rows } = await this.readTxs(); 14 | await fs.outputJSONSync( 15 | "./runtime/starknet_status/starknet_receive.json", 16 | rows, 17 | ); 18 | console.log(`\x1B[31m total:${count}, page:1, size:${rows.length}\x1B[0m`); 19 | for (let i = 0; i < rows.length; i++) { 20 | const tx = rows[i]; 21 | const status = await this.getStatusByTxid(tx.hash); 22 | const content = `Process:${i + 1}/${rows.length}/${count},Hash:${ 23 | tx.hash 24 | },Status:${status}`; 25 | status === "REJECTED" 26 | ? console.log(`\x1B[31m${content}\x1B[0m`) 27 | : console.log(content); 28 | await fs.appendFileSync( 29 | `./runtime/starknet_status/${status}`, 30 | tx.hash + `\n`, 31 | ); 32 | } 33 | } 34 | async readTxs() { 35 | // query 36 | const result = await this.ctx.models.Transaction.findAndCountAll({ 37 | raw: true, 38 | attributes: ["id", "hash", "from", "value", "memo", "symbol"], 39 | where: { 40 | chainId: 4, 41 | timestamp: { 42 | [Op.gte]: dayjs().subtract(7, "d").startOf("d").toDate(), 43 | [Op.lte]: dayjs().endOf("d").toDate(), 44 | }, 45 | status: { 46 | // [Op.not]: 2 47 | [Op.in]: [99, 98], 48 | }, 49 | to: "0x07c57808b9cea7130c44aab2f8ca6147b04408943b48c6d8c3c83eb8cfdd8c0b", 50 | }, 51 | order: [["id", "desc"]], 52 | limit: 5000, 53 | }); 54 | return result; 55 | } 56 | async getStatusByTxid(txid: string) { 57 | const service = new chainService.Starknet( 58 | chains.getChainByInternalId(this.chainId), 59 | ); 60 | const { tx_status } = await service.provider.getTransactionStatus(txid); 61 | return tx_status; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import { caching } from "cache-manager"; 2 | export default async function getCache(options?: any) { 3 | const caheStore = await caching("memory", options); 4 | return caheStore; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import { padStart } from "orbiter-chaincore/src/utils/core"; 3 | import crypto from "crypto"; 4 | export * from "./maker"; 5 | export async function sleep(ms: number): Promise { 6 | return new Promise(resolve => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, ms); 10 | }); 11 | } 12 | export function MD5(value: string) { 13 | const md5 = crypto.createHash("md5"); 14 | return md5.update(value).digest("hex"); 15 | } 16 | 17 | export function TransactionID( 18 | fromAddress: string, 19 | fromChainId: number | string, 20 | fromTxNonce: string | number, 21 | symbol: string | undefined, 22 | timestamp?: number, 23 | ) { 24 | let ext = ""; 25 | if ([8, 88].includes(Number(fromChainId))) { 26 | ext = timestamp ? `_${dayjs(timestamp).unix()}` : ""; 27 | } 28 | return `${fromAddress}${padStart(String(fromChainId), 4, "0")}${ 29 | symbol || "NULL" 30 | }${fromTxNonce}${ext}`.toLowerCase(); 31 | } 32 | 33 | export function TranferId( 34 | toChainId: number | string, 35 | replySender: string, 36 | replyAccount: string, 37 | userNonce: number | string, 38 | toSymbol: string, 39 | toValue?: string, 40 | ) { 41 | return MD5( 42 | `${toChainId}_${replySender}_${replyAccount}_${userNonce}_${toSymbol}_${toValue}`.toLowerCase(), 43 | ).toString(); 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/maker.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "./../context"; 2 | import { BigNumber } from "bignumber.js"; 3 | import { equals, isEmpty } from "orbiter-chaincore/src/utils/core"; 4 | import { IMarket } from "../types"; 5 | import { uniq, flatten, clone } from "lodash"; 6 | import { chains } from "orbiter-chaincore"; 7 | export async function convertMarketListToFile( 8 | makerList: Array, 9 | ctx: Context, 10 | ): Promise> { 11 | const crossAddressTransferMap = ctx.config.crossAddressTransferMap; 12 | const crossAddressMakers: any[] = []; 13 | const configs = flatten( 14 | makerList.map(row => { 15 | return convertPool(row); 16 | }), 17 | ).map(row => { 18 | if ([4, 44].includes(row.toChain.id)) { 19 | row.sender = ctx.config.L1L2Mapping[row.sender.toLowerCase()]; 20 | } 21 | if ([4, 44].includes(row.fromChain.id)) { 22 | // starknet mapping 23 | row.recipient = ctx.config.L1L2Mapping[row.recipient.toLowerCase()]; 24 | } 25 | // after 26 | const item = clone(row); 27 | for (const addr1 in crossAddressTransferMap) { 28 | if (equals(row.sender, addr1)) { 29 | item.sender = crossAddressTransferMap[addr1]; 30 | crossAddressMakers.push(item); 31 | } 32 | } 33 | return row; 34 | }); 35 | return [...configs, ...crossAddressMakers]; 36 | } 37 | export function convertChainLPToOldLP(oldLpList: Array): Array { 38 | const marketList: Array = oldLpList.map(row => { 39 | try { 40 | const pair = row["pair"]; 41 | const maker = row["maker"]; 42 | const fromChain = chains.getChainByInternalId(pair.sourceChain); 43 | const fromToken = fromChain.tokens.find(row => 44 | equals(row.address, pair.sourceToken), 45 | ); 46 | const toChain = chains.getChainByInternalId(pair.destChain); 47 | const toToken = toChain.tokens.find(row => 48 | equals(row.address, pair.destToken), 49 | ); 50 | const recipientAddress = maker["owner"]; 51 | const senderAddress = maker["owner"]; 52 | const fromChainId = pair.sourceChain; 53 | const toChainId = pair.destChain; 54 | const minPrice = new BigNumber( 55 | Number(row["minPrice"]) / Math.pow(10, Number(row["sourcePresion"])), 56 | ).toNumber(); 57 | const maxPrice = new BigNumber( 58 | Number(row["maxPrice"]) / Math.pow(10, Number(row["sourcePresion"])), 59 | ).toNumber(); 60 | const times = [ 61 | Number(row["startTime"]), 62 | Number(row["stopTime"] || 9999999999), 63 | ]; 64 | 65 | const lpConfig: IMarket = { 66 | id: row["id"], 67 | recipient: recipientAddress, 68 | sender: senderAddress, 69 | makerId: maker.id, 70 | ebcId: pair["ebcId"], 71 | fromChain: { 72 | id: Number(fromChainId), 73 | name: fromChain.name, 74 | tokenAddress: pair.sourceToken, 75 | symbol: fromToken?.symbol || "", 76 | decimals: Number(row["sourcePresion"]), 77 | maxPrice: maxPrice, 78 | minPrice: minPrice, 79 | }, 80 | toChain: { 81 | id: Number(toChainId), 82 | name: toChain.name, 83 | tokenAddress: pair.destToken, 84 | symbol: toToken?.symbol || "", 85 | decimals: Number(row["destPresion"]), 86 | }, 87 | times, 88 | pool: { 89 | //Subsequent versions will modify the structure 90 | makerAddress: recipientAddress, 91 | c1ID: fromChainId, 92 | c2ID: toChainId, 93 | c1Name: fromChain.name, 94 | c2Name: toChain.name, 95 | t1Address: pair.sourceToken, 96 | t2Address: pair.destToken, 97 | tName: fromToken?.symbol, 98 | minPrice, 99 | maxPrice, 100 | precision: Number(row["sourcePresion"]), 101 | avalibleDeposit: 1000, 102 | tradingFee: new BigNumber( 103 | Number(row["tradingFee"]) / 104 | Math.pow(10, Number(row["destPresion"])), 105 | ).toNumber(), 106 | gasFee: new BigNumber( 107 | Number(row["gasFee"]) / Math.pow(10, Number(row["destPresion"])), 108 | ).toNumber(), 109 | avalibleTimes: times, 110 | }, 111 | }; 112 | 113 | return lpConfig; 114 | } catch (error) { 115 | console.error(`convertChainLPToOldLP error:`, row, error); 116 | return null; 117 | } 118 | }); 119 | return marketList.filter(row => !isEmpty(row)) as any; 120 | } 121 | export function groupWatchAddressByChain(makerList: Array): { 122 | [key: string]: Array; 123 | } { 124 | const chainIds = uniq( 125 | flatten(makerList.map(row => [row.fromChain.id, row.toChain.id])), 126 | ); 127 | const chain: any = {}; 128 | for (const id of chainIds) { 129 | const recipientAddress = uniq( 130 | makerList.filter(m => m.fromChain.id === id).map(m => m.recipient), 131 | ); 132 | const senderAddress = uniq( 133 | makerList.filter(m => m.toChain.id === id).map(m => m.sender), 134 | ); 135 | chain[id] = uniq([...senderAddress, ...recipientAddress]); 136 | } 137 | return chain; 138 | } 139 | // getNewMarketList().then((result) => { 140 | // console.log(groupWatchAddressByChain(result), '===result') 141 | // }) 142 | export function convertPool(pool: any): Array { 143 | return [ 144 | { 145 | id: "", 146 | makerId: "", 147 | ebcId: "", 148 | recipient: pool.makerAddress, 149 | sender: pool.makerAddress, 150 | fromChain: { 151 | id: Number(pool.c1ID), 152 | name: pool.c1Name, 153 | tokenAddress: pool.t1Address, 154 | symbol: pool.tName, 155 | decimals: pool.precision, 156 | minPrice: pool.c1MinPrice * Math.pow(10, 18), 157 | maxPrice: pool.c1MaxPrice * Math.pow(10, 18), 158 | }, 159 | toChain: { 160 | id: Number(pool.c2ID), 161 | name: pool.c2Name, 162 | tokenAddress: pool.t2Address, 163 | symbol: pool.tName, 164 | decimals: pool.precision, 165 | }, 166 | times: [ 167 | pool["c1AvalibleTimes"][0].startTime, 168 | pool["c1AvalibleTimes"][0].endTime, 169 | ], 170 | pool: { 171 | //Subsequent versions will modify the structure 172 | makerAddress: pool.makerAddress, 173 | c1ID: pool.c1ID, 174 | c2ID: pool.c2ID, 175 | c1Name: pool.c1Name, 176 | c2Name: pool.c2Name, 177 | t1Address: pool.t1Address, 178 | t2Address: pool.t2Address, 179 | tName: pool.tName, 180 | minPrice: pool.c1MinPrice, 181 | maxPrice: pool.c1MaxPrice, 182 | precision: pool.precision, 183 | avalibleDeposit: pool.c1AvalibleDeposit, 184 | tradingFee: pool.c1TradingFee, 185 | gasFee: pool.c1GasFee, 186 | avalibleTimes: pool.c1AvalibleTimes, 187 | }, 188 | }, 189 | { 190 | id: "", 191 | makerId: "", 192 | ebcId: "", 193 | recipient: pool.makerAddress, 194 | sender: pool.makerAddress, 195 | fromChain: { 196 | id: Number(pool.c2ID), 197 | name: pool.c2Name, 198 | tokenAddress: pool.t2Address, 199 | symbol: pool.tName, 200 | decimals: pool.precision, 201 | minPrice: pool.c1MinPrice * Math.pow(10, 18), 202 | maxPrice: pool.c1MaxPrice * Math.pow(10, 18), 203 | }, 204 | toChain: { 205 | id: Number(pool.c1ID), 206 | name: pool.c1Name, 207 | tokenAddress: pool.t1Address, 208 | symbol: pool.tName, 209 | decimals: pool.precision, 210 | }, 211 | // minPrice: pool.c2MinPrice, 212 | // maxPrice: pool.c2MaxPrice, 213 | // precision: pool.precision, 214 | // avalibleDeposit: pool.c2AvalibleDeposit, 215 | // tradingFee: pool.c2TradingFee, 216 | // gasFee: pool.c2GasFee, 217 | // avalibleTimes: pool.c2AvalibleTimes, 218 | times: [ 219 | pool["c2AvalibleTimes"][0].startTime, 220 | pool["c2AvalibleTimes"][0].endTime, 221 | ], 222 | pool: { 223 | //Subsequent versions will modify the structure 224 | makerAddress: pool.makerAddress, 225 | c1ID: pool.c1ID, 226 | c2ID: pool.c2ID, 227 | c1Name: pool.c1Name, 228 | c2Name: pool.c2Name, 229 | t1Address: pool.t1Address, 230 | t2Address: pool.t2Address, 231 | tName: pool.tName, 232 | minPrice: pool.c2MinPrice, 233 | maxPrice: pool.c2MaxPrice, 234 | precision: pool.precision, 235 | avalibleDeposit: pool.c2AvalibleDeposit, 236 | tradingFee: pool.c2TradingFee, 237 | gasFee: pool.c2GasFee, 238 | avalibleTimes: pool.c2AvalibleTimes, 239 | }, 240 | }, 241 | ]; 242 | } 243 | -------------------------------------------------------------------------------- /src/utils/oldUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { BigNumber } from "bignumber.js"; 4 | 5 | const MAX_BITS: any = { 6 | eth: 256, 7 | arbitrum: 256, 8 | zksync: 35, 9 | zksync2: 256, 10 | starknet: 256, 11 | polygon: 256, 12 | optimism: 256, 13 | immutablex: 28, 14 | loopring: 256, 15 | metis: 256, 16 | dydx: 28, 17 | boba: 256, 18 | zkspace: 35, 19 | bnbchain: 256, 20 | arbitrum_nova: 256, 21 | polygon_zkevm: 256, 22 | orbiter: 256, 23 | }; 24 | const precisionResolverMap: any = { 25 | // pay attention: the type of field "userAmount" in the following methods is not BigNumber 26 | // but string in decimal!!! 27 | "18": (userAmount: any) => userAmount.slice(0, 6), 28 | default: (userAmount: any) => userAmount, 29 | }; 30 | export const CHAIN_INDEX: any = { 31 | 1: "eth", 32 | 2: "arbitrum", 33 | 22: "arbitrum", 34 | 3: "zksync", 35 | 33: "zksync", 36 | 4: "starknet", 37 | 44: "starknet", 38 | 5: "eth", 39 | 6: "polygon", 40 | 66: "polygon", 41 | 7: "optimism", 42 | 77: "optimism", 43 | 8: "immutablex", 44 | 88: "immutablex", 45 | 9: "loopring", 46 | 99: "loopring", 47 | 10: "metis", 48 | 510: "metis", 49 | 11: "dydx", 50 | 511: "dydx", 51 | 12: "zkspace", 52 | 512: "zkspace", 53 | 13: "boba", 54 | 513: "boba", 55 | 514: "zksync2", 56 | 15: "bnbchain", 57 | 515: "bnbchain", 58 | 16: "arbitrum_nova", 59 | 516: "arbitrum_nova", 60 | 517: "polygon_zkevm", 61 | 599: "orbiter", 62 | }; 63 | 64 | export const SIZE_OP = { 65 | P_NUMBER: 4, 66 | }; 67 | 68 | function isLimitNumber(chain: string | number) { 69 | if (chain === 3 || chain === 33 || chain === "zksync") { 70 | return true; 71 | } 72 | if (chain === 8 || chain === 88 || chain === "immutablex") { 73 | return true; 74 | } 75 | if (chain === 11 || chain === 511 || chain === "dydx") { 76 | return true; 77 | } 78 | if (chain === 12 || chain === 512 || chain === "zkspace") { 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | function isLPChain(chain: string | number) { 85 | if (chain === 9 || chain === 99 || chain === "loopring") { 86 | return true; 87 | } 88 | return false; 89 | } 90 | 91 | /** 92 | * @description { 93 | * This method is to confirm the legitimacy of the amount 94 | * if the amount u passed is legal, it will return it intact 95 | * otherwise the data we processed will be returned 96 | * } 97 | * @param userAmount the amount user given 98 | * @param chain config of the current chain 99 | */ 100 | const performUserAmountLegality = (userAmount: BigNumber, chain: any) => { 101 | const { precision } = chain; 102 | const decimalData = userAmount.toFormat(); // convert BigNumber instance to decimal 103 | // if the precision that current chain support equals 18, the maximum precision of userAmount u passed is 6 104 | const matchResolver = 105 | precisionResolverMap[precision] || precisionResolverMap["default"]; 106 | // eg: precision equals 18, but the value of userAmount is 0.3333333333 107 | // covert result after matchResolver processed was 0.333333 108 | const convertResult = matchResolver(decimalData, chain); 109 | return new BigNumber(convertResult); 110 | }; 111 | function getToAmountFromUserAmount( 112 | userAmount: any, 113 | selectMakerInfo: any, 114 | isWei: any, 115 | ) { 116 | let toAmount_tradingFee = new BigNumber(userAmount).minus( 117 | new BigNumber(selectMakerInfo.tradingFee), 118 | ); 119 | let gasFee = toAmount_tradingFee 120 | .multipliedBy(new BigNumber(selectMakerInfo.gasFee)) 121 | .dividedBy(new BigNumber(1000)); 122 | let digit = selectMakerInfo.precision === 18 ? 5 : 2; 123 | // accessLogger.info('digit =', digit) 124 | let gasFee_fix = gasFee.decimalPlaces(digit, BigNumber.ROUND_UP); 125 | // accessLogger.info('gasFee_fix =', gasFee_fix.toString()) 126 | let toAmount_fee = toAmount_tradingFee.minus(gasFee_fix); 127 | // accessLogger.info('toAmount_fee =', toAmount_fee.toString()) 128 | if (!toAmount_fee || isNaN(Number(toAmount_fee))) { 129 | return 0; 130 | } 131 | if (isWei) { 132 | return toAmount_fee.multipliedBy( 133 | new BigNumber(10 ** selectMakerInfo.precision), 134 | ); 135 | } else { 136 | return toAmount_fee; 137 | } 138 | } 139 | function getTAmountFromRAmount( 140 | chain: number, 141 | amount: string, 142 | pText: string | any[], 143 | ) { 144 | if (!isChainSupport(chain)) { 145 | return { 146 | state: false, 147 | error: "The chain did not support", 148 | }; 149 | } 150 | if (Number(amount) < 1) { 151 | return { 152 | state: false, 153 | error: "the token doesn't support that many decimal digits", 154 | }; 155 | } 156 | if (pText.length > SIZE_OP.P_NUMBER) { 157 | return { 158 | state: false, 159 | error: "the pText size invalid", 160 | }; 161 | } 162 | 163 | const validDigit = AmountValidDigits(chain, amount); // 10 11 164 | const amountLength = amount.toString().length; 165 | if (amountLength < SIZE_OP.P_NUMBER) { 166 | return { 167 | state: false, 168 | error: "Amount size must be greater than pNumberSize", 169 | }; 170 | } 171 | if (isLimitNumber(chain) && amountLength > validDigit) { 172 | const tAmount = 173 | amount.toString().slice(0, validDigit - pText.length) + 174 | pText + 175 | amount.toString().slice(validDigit); 176 | return { 177 | state: true, 178 | tAmount: tAmount, 179 | }; 180 | } else if (isLPChain(chain)) { 181 | return { 182 | state: true, 183 | tAmount: amount + "", 184 | }; 185 | } else { 186 | const tAmount = 187 | amount.toString().slice(0, amountLength - pText.length) + pText; 188 | return { 189 | state: true, 190 | tAmount: tAmount, 191 | }; 192 | } 193 | } 194 | 195 | function getPTextFromTAmount(chain: number, amount: string) { 196 | if (!isChainSupport(chain)) { 197 | return { 198 | state: false, 199 | error: "The chain did not support", 200 | }; 201 | } 202 | if (Number(amount) < 1) { 203 | return { 204 | state: false, 205 | error: "the token doesn't support that many decimal digits", 206 | }; 207 | } 208 | amount = new BigNumber(String(amount)).toFixed(); 209 | //Get the effective number of digits 210 | const validDigit = AmountValidDigits(chain, amount); // 10 11 211 | const amountLength = amount.toString().length; 212 | if (amountLength < SIZE_OP.P_NUMBER) { 213 | return { 214 | state: false, 215 | error: "Amount size must be greater than pNumberSize", 216 | }; 217 | } 218 | if (isLimitNumber(chain) && amountLength > validDigit) { 219 | const zkAmount = amount.toString().slice(0, validDigit); 220 | const op_text = zkAmount.slice(-SIZE_OP.P_NUMBER); 221 | return { 222 | state: true, 223 | pText: op_text, 224 | }; 225 | } else { 226 | const op_text = amount.toString().slice(-SIZE_OP.P_NUMBER); 227 | return { 228 | state: true, 229 | pText: op_text, 230 | }; 231 | } 232 | } 233 | function getRAmountFromTAmount(chain: number, amount: string) { 234 | let pText = ""; 235 | for (let index = 0; index < SIZE_OP.P_NUMBER; index++) { 236 | pText = pText + "0"; 237 | } 238 | if (!isChainSupport(chain)) { 239 | return { 240 | state: false, 241 | error: "The chain did not support", 242 | }; 243 | } 244 | if (Number(amount) < 1) { 245 | return { 246 | state: false, 247 | error: "the token doesn't support that many decimal digits", 248 | }; 249 | } 250 | 251 | const validDigit = AmountValidDigits(chain, amount); // 10 11 252 | const amountLength = amount.toString().length; 253 | if (amountLength < SIZE_OP.P_NUMBER) { 254 | return { 255 | state: false, 256 | error: "Amount size must be greater than pNumberSize", 257 | }; 258 | } 259 | if (isLimitNumber(chain) && amountLength > validDigit) { 260 | const rAmount = 261 | amount.toString().slice(0, validDigit - SIZE_OP.P_NUMBER) + 262 | pText + 263 | amount.toString().slice(validDigit); 264 | return { 265 | state: true, 266 | rAmount: rAmount, 267 | }; 268 | } else { 269 | const rAmount = 270 | amount.toString().slice(0, amountLength - SIZE_OP.P_NUMBER) + pText; 271 | return { 272 | state: true, 273 | rAmount: rAmount, 274 | }; 275 | } 276 | } 277 | 278 | function isChainSupport(chain: string | number) { 279 | if (CHAIN_INDEX[chain] && MAX_BITS[CHAIN_INDEX[chain]]) { 280 | return true; 281 | } 282 | return false; 283 | } 284 | 285 | /** 286 | * 0 ~ (2 ** N - 1) 287 | * @param { any } chain 288 | * @returns { any } 289 | */ 290 | function AmountRegion(chain: number): any { 291 | if (!isChainSupport(chain)) { 292 | return { 293 | error: "The chain did not support", 294 | }; 295 | } 296 | if (typeof chain === "number") { 297 | const max = new BigNumber(2 ** MAX_BITS[CHAIN_INDEX[chain]] - 1); 298 | return { 299 | min: new BigNumber(0), 300 | max: max, 301 | }; 302 | } else if (typeof chain === "string") { 303 | const n = MAX_BITS[String(chain).toLowerCase()]; 304 | const max = new BigNumber(2 ** n - 1); 305 | return { 306 | min: new BigNumber(0), 307 | max: max, 308 | }; 309 | } 310 | } 311 | 312 | function AmountMaxDigits(chain: number) { 313 | const amountRegion = AmountRegion(chain); 314 | if (amountRegion?.error) { 315 | return amountRegion; 316 | } 317 | return amountRegion.max.toFixed().length; 318 | } 319 | 320 | function AmountValidDigits(chain: number, amount: string) { 321 | const amountMaxDigits = AmountMaxDigits(chain); 322 | if (amountMaxDigits.error) { 323 | return amountMaxDigits.error; 324 | } 325 | const amountRegion = AmountRegion(chain); 326 | 327 | const ramount = removeSidesZero(amount.toString()); 328 | if (ramount.length > amountMaxDigits) { 329 | return "amount is inValid"; 330 | } 331 | //note:the compare is one by one,not all by all 332 | if (ramount > amountRegion.max.toFixed()) { 333 | return amountMaxDigits - 1; 334 | } else { 335 | return amountMaxDigits; 336 | } 337 | } 338 | 339 | function removeSidesZero(param: string) { 340 | if (typeof param !== "string") { 341 | return "param must be string"; 342 | } 343 | return param.replace(/^0+(\d)|(\d)0+$/gm, "$1$2"); 344 | } 345 | 346 | function isAmountInRegion(amount: BigNumber.Value, chain: number) { 347 | if (!isChainSupport(chain)) { 348 | return { 349 | state: false, 350 | error: "The chain did not support", 351 | }; 352 | } 353 | const amountRegion = AmountRegion(chain); 354 | if (amountRegion.error) { 355 | return false; 356 | } 357 | if ( 358 | new BigNumber(amount).gte(amountRegion.min) && 359 | new BigNumber(amount).lte(amountRegion.max) 360 | ) { 361 | return true; 362 | } 363 | return false; 364 | } 365 | 366 | function pTextFormatZero(num: string) { 367 | if (String(num).length > SIZE_OP.P_NUMBER) return num; 368 | return (Array(SIZE_OP.P_NUMBER).join("0") + num).slice(-SIZE_OP.P_NUMBER); 369 | } 370 | 371 | /** 372 | * Get return amount 373 | * @param fromChainID 374 | * @param toChainID 375 | * @param amountStr 376 | * @param pool 377 | * @param nonce 378 | * @returns 379 | */ 380 | export function getAmountToSend( 381 | fromChainID: number, 382 | toChainID: number, 383 | amountStr: string, 384 | pool: { precision: number; tradingFee: number; gasFee: number }, 385 | nonce: string | number, 386 | ) { 387 | const realAmount = getRAmountFromTAmount(fromChainID, amountStr); 388 | if (!realAmount.state) { 389 | console.error(realAmount.error); 390 | return; 391 | } 392 | const rAmount = realAmount.rAmount; 393 | if (nonce > 8999) { 394 | console.error("nonce too high, not allowed"); 395 | return; 396 | } 397 | const nonceStr = pTextFormatZero(String(nonce)); 398 | const readyAmount = getToAmountFromUserAmount( 399 | new BigNumber(rAmount).dividedBy(new BigNumber(10 ** pool.precision)), 400 | pool, 401 | true, 402 | ); 403 | 404 | return getTAmountFromRAmount(toChainID, readyAmount.toString(), nonceStr); 405 | } 406 | /** 407 | * @param chainId 408 | * @param amount 409 | * @returns 410 | */ 411 | export function getAmountFlag(chainId: number, amount: string): string { 412 | const rst = getPTextFromTAmount(chainId, amount); 413 | if (!rst.state) { 414 | return "0"; 415 | } 416 | return (Number(rst.pText) % 9000) + ""; 417 | } 418 | 419 | export { 420 | getTAmountFromRAmount, 421 | getRAmountFromTAmount, 422 | getPTextFromTAmount, 423 | pTextFormatZero, 424 | isLimitNumber, 425 | getToAmountFromUserAmount, 426 | }; 427 | -------------------------------------------------------------------------------- /test/isEmpty.test.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "orbiter-chaincore/src/utils/core"; 2 | const tx = { 3 | from: "0xd7aa9ba6caac7b0436c91396f22ca5a7f31664fc", 4 | to: "0x6a745cf3283fd2a048229f079869b4e88e16ce16", 5 | value: "10000000000", 6 | nonce: 1213, 7 | symbol: "USDT", 8 | }; 9 | console.log(isEmpty(tx.from)); 10 | console.log(isEmpty(tx.to)); 11 | console.log(isEmpty(tx.value)); 12 | console.log(isEmpty(1)); 13 | console.log(isEmpty(tx.symbol)); 14 | -------------------------------------------------------------------------------- /test/pushDydx.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orbiter-Finance/makerTransationData/ada6a439c551ecd411e3900e10452d03abddbf1f/test/pushDydx.test.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Projects */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 23 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | "resolveJsonModule": true, /* Enable importing .json files */ 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | 40 | /* JavaScript Support */ 41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 44 | 45 | /* Emit */ 46 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 47 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 48 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 49 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 51 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 52 | // "removeComments": true, /* Disable emitting comments. */ 53 | // "noEmit": true, /* Disable emitting files from a compilation. */ 54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 61 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 62 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 63 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 64 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 65 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 66 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 67 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 68 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 69 | 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 73 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 74 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 75 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 76 | 77 | /* Type Checking */ 78 | "strict": true, /* Enable all strict type-checking options. */ 79 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 80 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 82 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 84 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 85 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 87 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 92 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 97 | 98 | /* Completeness */ 99 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 100 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 101 | }, 102 | "include": ["index.ts","src/**/*", "src/**/*.json"] 103 | } 104 | --------------------------------------------------------------------------------