From 9ed97aa76c1178cf020023e1d5ad7c0fb0b159ad Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Tue, 8 Jul 2025 00:41:36 +1000 Subject: [PATCH 01/45] review of master branch, please use develop moving forward. --- .env.sample | 1 + LICENSE | 21 +++++++++++++++++++++ README.md | 31 ++++++++++++++++++++++++++----- bun.lock | 6 ++---- config/index.ts | 3 +++ helpers/index.ts | 7 +++++++ index.ts | 15 +++++++++++++++ package.json | 9 +++++++-- proto/fietCexNode/CexService.ts | 1 + proto/node.proto | 23 ++++++++++++++++++++++- tsconfig.json | 1 + types.ts | 4 ++++ 12 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 .env.sample create mode 100644 LICENSE diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..06ca597 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +# TODO: Always include an .env.sample when using dotenv... just good practice. It should always be up to date. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ab8802 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2024 Usher Labs Pty Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 524d3c8..51132d1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# FietCexBroker +# CEX Broker A high-performance gRPC-based cryptocurrency exchange broker service that provides unified access to multiple centralized exchanges (CEX) including Binance and Bybit. Built with TypeScript, Bun, and CCXT for reliable trading operations. ## Features -- **Multi-Exchange Support**: Unified API for Binance and Bybit +- **Multi-Exchange Support**: Unified API to any CEX supported by [CCXT](https://github.com/ccxt/ccxt) - **gRPC Interface**: High-performance RPC communication - **Real-time Pricing**: Optimal price discovery across exchanges - **Balance Management**: Real-time balance checking @@ -23,17 +23,20 @@ A high-performance gRPC-based cryptocurrency exchange broker service that provid ## Installation 1. Clone the repository: + ```bash git clone -cd fietCexBroker +cd cex-broker ``` -2. Install dependencies: +1. Install dependencies: + ```bash bun install ``` -3. Generate protobuf types: +1. Generate protobuf types: + ```bash bun run proto-gen ``` @@ -151,6 +154,7 @@ The service exposes a gRPC interface with the following methods: Get optimal buy/sell prices across supported exchanges. **Request:** + ```protobuf message OptimalPriceRequest { string symbol = 1; // Trading pair symbol, e.g. "ARB/USDT" @@ -160,6 +164,7 @@ message OptimalPriceRequest { ``` **Response:** + ```protobuf message OptimalPriceResponse { map results = 1; @@ -172,6 +177,7 @@ message PriceInfo { ``` **Example:** + ```typescript const request = { symbol: "ARB/USDT", @@ -185,6 +191,7 @@ const request = { Get available balance for a specific currency on a specific exchange. **Request:** + ```protobuf message BalanceRequest { string cex = 1; // CEX identifier (e.g., "BINANCE", "BYBIT") @@ -193,6 +200,7 @@ message BalanceRequest { ``` **Response:** + ```protobuf message BalanceResponse { double balance = 1; // Available balance for the token @@ -201,6 +209,7 @@ message BalanceResponse { ``` **Example:** + ```typescript const request = { cex: "BINANCE", @@ -213,6 +222,7 @@ const request = { Confirm a deposit transaction. **Request:** + ```protobuf message DepositConfirmationRequest { string chain = 1; @@ -223,6 +233,7 @@ message DepositConfirmationRequest { ``` **Response:** + ```protobuf message DepositConfirmationResponse { double newBalance = 1; @@ -234,6 +245,7 @@ message DepositConfirmationResponse { Execute a transfer/withdrawal to an external address. **Request:** + ```protobuf message TransferRequest { string chain = 1; // Network chain (e.g., "ARBITRUM", "BEP20") @@ -245,6 +257,7 @@ message TransferRequest { ``` **Response:** + ```protobuf message TransferResponse { bool success = 1; @@ -257,6 +270,7 @@ message TransferResponse { Convert between different tokens using limit orders. **Request:** + ```protobuf message ConvertRequest { string from_token = 1; // Source token @@ -268,6 +282,7 @@ message ConvertRequest { ``` **Response:** + ```protobuf message ConvertResponse { string order_id = 3; @@ -279,6 +294,7 @@ message ConvertResponse { Get details of a specific order. **Request:** + ```protobuf message OrderDetailsRequest { string order_id = 1; // Unique order identifier @@ -287,6 +303,7 @@ message OrderDetailsRequest { ``` **Response:** + ```protobuf message OrderDetailsResponse { string order_id = 1; // Unique order identifier @@ -304,6 +321,7 @@ message OrderDetailsResponse { Cancel an existing order. **Request:** + ```protobuf message CancelOrderRequest { string order_id = 1; // Unique order identifier @@ -312,6 +330,7 @@ message CancelOrderRequest { ``` **Response:** + ```protobuf message CancelOrderResponse { bool success = 1; // Whether cancellation was successful @@ -399,6 +418,7 @@ bun test --coverage ## Dependencies ### Core Dependencies + - `@grpc/grpc-js`: gRPC server implementation - `@grpc/proto-loader`: Protocol buffer loading - `ccxt`: Cryptocurrency exchange library @@ -406,6 +426,7 @@ bun test --coverage - `joi`: Configuration validation ### Development Dependencies + - `@biomejs/biome`: Code formatting and linting - `@types/bun`: Bun type definitions - `bun-types`: Additional Bun types diff --git a/bun.lock b/bun.lock index 9297db8..c894875 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "devDependencies": { "@biomejs/biome": "2.0.6", - "@types/bun": "latest", + "@types/bun": "^1.2.18", "bun-types": "latest", "husky": "^9.1.7", }, @@ -76,7 +76,7 @@ "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], - "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], "@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="], @@ -137,7 +137,5 @@ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "@types/bun/bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], } } diff --git a/config/index.ts b/config/index.ts index 50eee08..8635028 100644 --- a/config/index.ts +++ b/config/index.ts @@ -6,10 +6,13 @@ dotenv.config(); const baseConfig = { port: process.env.PORT_NUM, + // TODO: We can make these environment variables more dynamic... eg. CEX_API_KEY_[EXCHANGE_NAMESPACE]_[NUMBER] - in case many exchanges for same exchange. bybitApiKey: process.env.BYBIT_API_KEY, bybitApiSecret: process.env.BYBIT_API_SECRET, binanceApiKey: process.env.BINANCE_API_KEY, binanceApiSecret: process.env.BINANCE_API_SECRET, + // TODO: Terrible naming convention? What is Rooch doing here... + // TODO: This "brokers" should be called "exchanges".... right? brokers: (process.env.ROOCH_CHAIN_ID ? process.env.ROOCH_CHAIN_ID.split(",") : BrokerList) as string[], diff --git a/helpers/index.ts b/helpers/index.ts index 818ec0e..aff6d21 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -10,11 +10,14 @@ const ALLOWED_IPS = [ "127.0.0.1", // localhost "::1", // IPv6 localhost // Add your allowed IP addresses here + // TODO: Allowed IPs should be loaded via .env. Local ips are fine to keep hardcoded. ]; export function isIpAllowed(ip: string): boolean { return ALLOWED_IPS.includes(ip); } + +// TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." export async function buyAtOptimalPrice( exchange: Exchange, symbol: string, @@ -61,6 +64,7 @@ export async function buyAtOptimalPrice( * Fetches the order book, computes the worst‐case fill price on the ask side for `size`, * and submits a single limit‐sell at that price. */ +// TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." export async function sellAtOptimalPrice( exchange: Exchange, symbol: string, @@ -119,6 +123,8 @@ export function loadPolicy(): PolicyConfig { try { const fs = require("bun:fs"); const path = require("bun:path"); + // TODO: Load the policy that we want from a hidden file in the root directory. We want to open source this Broker. + // TODO: Users can then copy the example policy.json file to root and run with it. const policyPath = path.join(__dirname, "../policy/policy.json"); const policyData = fs.readFileSync(policyPath, "utf8"); return JSON.parse(policyData) as PolicyConfig; @@ -131,6 +137,7 @@ export function loadPolicy(): PolicyConfig { /** * Validates withdraw request against policy rules */ +// TODO: Nice work on the policy engine, however, we'll need incorporate a mapping between how the CEX Broker recognises networks, and how different CEXs recognise networks - eg. Binance might have "BSC", but another chain will have BNB" export function validateWithdraw( policy: PolicyConfig, network: string, diff --git a/index.ts b/index.ts index 72e7011..2d8bad7 100644 --- a/index.ts +++ b/index.ts @@ -47,6 +47,7 @@ async function main() { const server = getServer(policy); + // TODO: Remove this... console.log( `BINANCE: Broker Balance ,${JSON.stringify(await brokers.BINANCE.fetchFreeBalance())}`, ); @@ -81,6 +82,9 @@ function authenticateRequest(call: grpc.ServerUnaryCall): boolean { function getServer(policy: PolicyConfig) { const server = new grpc.Server(); server.addService(fietCexNode.CexService.service, { + // TODO: Consolidate all of these calls into "ExecuteAction", "SubscribeToStream"... + + // TODO: Getting optimal price is for the MM tech to decide... GetOptimalPrice: async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, @@ -196,6 +200,9 @@ function getServer(policy: PolicyConfig) { } // Validate against policy + + // TODO: I recognise that deposit/withdraw, etc. will need additional considerations as we validate against the policy... + // TODO: Therefore, either we can keep the standalone Deposit/Withdraw Methods, or we check the "ExecuteAction" method for the "deposit"/"withdraw"/"convert"/"cancelOrder"/"getOrderDetails"/"getBalance" actions... to determine extra validation. const validation = validateDeposit(policy, chain, Number(amount)); if (!validation.valid) { return callback( @@ -207,6 +214,7 @@ function getServer(policy: PolicyConfig) { ); } + // TODO: Where is CCXT used here? try { console.log( `[${new Date().toISOString()}] ` + @@ -299,6 +307,8 @@ function getServer(policy: PolicyConfig) { null, ); } + + // TODO: My point is why can this not be agnostic to the CEX... const transaction = await broker.withdraw( token, Number(amount), @@ -319,6 +329,11 @@ function getServer(policy: PolicyConfig) { ); } }, + + // TODO: "Convert" and "createLimitOrder" are too extremely different things... + // TODO: "Convert" is a generic action that can be used to convert any token to any other token... + // TODO: "createLimitOrder" is a specific action that can be used to create a limit order on a specific CEX... + // TODO: "Convert" could be "createMarketOrder"... a differnt thing to "createLimitOrder"... Convert: async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData, diff --git a/package.json b/package.json index 877eeb9..1bcf90e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { - "name": "fietCexBroker", + "name": "cex-broker", + "description": "Unified gRPC API to CEXs by Usher Labs", + "repository": "git@gitlab.com:usherlabs/cex-broker.git", + "homepage": "https://usher.so/", + "author": "Oki Ayobami ", "module": "index.ts", "type": "module", "private": true, + "license": "MIT", "devDependencies": { "@biomejs/biome": "2.0.6", - "@types/bun": "latest", + "@types/bun": "^1.2.18", "bun-types": "latest", "husky": "^9.1.7" }, diff --git a/proto/fietCexNode/CexService.ts b/proto/fietCexNode/CexService.ts index 6443e27..499321c 100644 --- a/proto/fietCexNode/CexService.ts +++ b/proto/fietCexNode/CexService.ts @@ -17,6 +17,7 @@ import type { OrderDetailsResponse as _fietCexNode_OrderDetailsResponse, OrderDe import type { TransferRequest as _fietCexNode_TransferRequest, TransferRequest__Output as _fietCexNode_TransferRequest__Output } from '../fietCexNode/TransferRequest'; import type { TransferResponse as _fietCexNode_TransferResponse, TransferResponse__Output as _fietCexNode_TransferResponse__Output } from '../fietCexNode/TransferResponse'; +// TODO: Is this file auto-generated? If so, can we gitignore it, and add protogen to the post-install script? Unless there's a reason not too.. export interface CexServiceClient extends grpc.Client { CancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; CancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; diff --git a/proto/node.proto b/proto/node.proto index 042667e..31d4f40 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -1,5 +1,26 @@ syntax = "proto3"; -package fietCexNode; +package cex-broker; // TODO: I've renamed... + +// TODO: This whole file can be a simple message... +// TODO: A single generic CCXT action/message to executing any CCXT function... This way the broker is dynamic ... +// TODO: Maybe a second action to trigger a websocket/streaming response... +// message CcxtActionRequest { +// string action = 1; // The CCXT method to call (e.g., "fetchBalance", "createOrder") +// map parameters = 2; // Parameters to pass to the CCXT method +// string cex = 3; // CEX identifier (e.g., "binance", "bybit") +// string symbol = 4; // Optional: trading pair symbol if needed +} + +// message CcxtActionResponse { +// bool success = 1; // Whether the action was successful +// string result = 2; // JSON string of the result data +// string error = 3; // Error message if success is false +// } +// service CexService { +// rpc ExecuteCcxtAction(CcxtActionRequest) returns (CcxtActionResponse); +// } + + // Mode enum for price direction enum OrderMode { BUY = 0; diff --git a/tsconfig.json b/tsconfig.json index 146fe4e..3e4ca63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, + "types": ["@types/bun"], // Bundler mode "moduleResolution": "bundler", diff --git a/types.ts b/types.ts index df0995c..c1ad735 100644 --- a/types.ts +++ b/types.ts @@ -51,6 +51,10 @@ export type Policies = { [apiKey: string]: Policy; // key is an Ethereum-style address like '0x...' }; +// TODO: Why are Bybit and Binance so tightly integrated into this? Should we not simply have a dynamic conversion between CCXT interface and this node? +// TODO: Otheriwse, we'll need to setup every integration supported by CCXT indidivually inside of this node? +// TODO: Rename "broker" to "cexs" or "exchanges"... +// ? The Node itself is a broker. The destination of requests is the CEX... export const BrokerList = ["BINANCE", "BYBIT"] as const; export type ISupportedBroker = (typeof BrokerList)[number]; From c5a10979bffc66f152f8ace0a3dd2e91b82674df Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 00:46:08 +0100 Subject: [PATCH 02/45] Refactor broker config and update dependencies --- build.ts | 12 ++ bun.lock | 13 +- client.ts | 29 ++-- config/broker.ts | 246 +++++++++++++++++++++++++++++----- config/index.ts | 8 +- helpers/index.test.ts | 2 +- helpers/index.ts | 9 +- index.ts | 298 +++++++++++++++++++++++++----------------- integration.test.ts | 6 +- package.json | 12 +- types.ts | 115 +++++++++++++++- 11 files changed, 553 insertions(+), 197 deletions(-) create mode 100644 build.ts diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..efa0d93 --- /dev/null +++ b/build.ts @@ -0,0 +1,12 @@ +import dts from 'bun-plugin-dts' + +await Bun.build({ + entrypoints: ['./index.ts'], + outdir: './dist', + target:"node", + plugins: [ + dts() + ], +}) + +// Generates `dist/index.d.ts` and `dist/other/foo.d.ts` \ No newline at end of file diff --git a/bun.lock b/bun.lock index c894875..1828bc9 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,8 @@ }, "devDependencies": { "@biomejs/biome": "2.0.6", - "@types/bun": "^1.2.18", + "@types/bun": "latest", + "bun-plugin-dts": "^0.3.0", "bun-types": "latest", "husky": "^9.1.7", }, @@ -86,6 +87,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "bun-plugin-dts": ["bun-plugin-dts@0.3.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.8.1" } }, "sha512-QpiAOKfPcdOToxySOqRY8FwL+brTvyXEHWzrSCRKt4Pv7Z4pnUrhK9tFtM7Ndm7ED09B/0cGXnHJKqmekr/ERw=="], + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], "ccxt": ["ccxt@4.4.91", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-IP7wZc1KfAojMfKyMeGwC/aJoXvAUFFUMtu3x9Wp5BAOISftcrcl/yt/gjxaPddrMT2/BcU3wHZzvqzr1FBFBw=="], @@ -96,16 +99,22 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "dotenv": ["dotenv@17.0.0", "", {}, "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ=="], + "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -120,6 +129,8 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/client.ts b/client.ts index 768d9f8..db381c6 100644 --- a/client.ts +++ b/client.ts @@ -1,4 +1,4 @@ -import path from "bun:path"; +import path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "./proto/node"; @@ -27,18 +27,7 @@ client.waitForReady(deadline, (err) => { }); function onClientReady() { - client.getOptimalPrice( - { mode: 0, symbol: "ARB/USDT", quantity: 200 }, - (err, result) => { - if (err) { - console.error({ err }); - return; - } - console.log({ x: result?.results }); - }, - ); - - client.getBalance({ cex: "BYBIT", token: "USDT" }, (err, result) => { + client.getBalance({ cex: "bybit", token: "USDT" }, (err, result) => { if (err) { console.error({ err }); return; @@ -46,11 +35,11 @@ function onClientReady() { console.log({ x: result }); }); - client.Transfer({cex:"BINANCE",amount:1,token:"USDT",chain:"BEP20",recipientAddress:"0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"},(err,result)=>{ - if (err) { - console.error({ err }); - return; - } - console.log({ x: result }); - }) +// client.Transfer({cex:"binance",amount:1,token:"USDT",chain:"BEP20",recipientAddress:"0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"},(err,result)=>{ +// if (err) { +// console.error({ err }); +// return; +// } +// console.log({ x: result }); +// }) } diff --git a/config/broker.ts b/config/broker.ts index 22db1f1..33db2ed 100644 --- a/config/broker.ts +++ b/config/broker.ts @@ -1,50 +1,228 @@ -import ccxt, { type bybit, type binance } from "ccxt"; +import ccxt from "ccxt"; +import type { alpaca, + apex, + ascendex, + bequant, + bigone, + binance, + binancecoinm, + binanceus, + binanceusdm, + bingx, + bit2c, + bitbank, + bitbns, + bitfinex, + bitflyer, + bitget, + bithumb, + bitmart, + bitmex, bitopro, + bitrue, bitso,bitstamp, + bitteam, + bittrade, + bitvavo, + blockchaincom, + blofin, + btcalpha, + btcbox, + btcmarkets, + btcturk, + bybit, + cex, + coinbase, + coinbaseadvanced, + coinbaseexchange, + coinbaseinternational, + coincatch, + coincheck, + coinex, + coinmate, + coinmetro, + coinone, + coinsph, + coinspot, + cryptocom, + cryptomus, + defx, + delta, + deribit, + derive, + digifinex, + ellipx, + exmo, + fmfwio, + gate, + gateio, + gemini, + hashkey, + hitbtc, + hollaex, + htx, + huobi, + hyperliquid, + independentreserve, + indodax, + kraken, + krakenfutures, + kucoin, + kucoinfutures, + latoken, + lbank, + luno, + mercado, + mexc, + modetrade, + myokx, + ndax, + novadax, + oceanex, + okcoin, + okx, + okxus, + onetrading, + oxfun, + p2b, + paradex, + paymium, + phemex, + poloniex, + probit, + timex, + tokocrypto, + tradeogre, + upbit, + vertex, + wavesexchange, + whitebit, + woo, + woofipro, + xt, + yobit, + zaif, + zonda, +} from "ccxt" import { SupportedBroker } from "../types"; import type { ISupportedBroker } from "../types"; import config from "./index"; // Map each broker key to its specific CCXT class type BrokerInstanceMap = { - [SupportedBroker.BYBIT]: bybit; - [SupportedBroker.BINANCE]: binance; -}; - + [SupportedBroker.alpaca]: alpaca; + [SupportedBroker.apex]: apex; + [SupportedBroker.ascendex]: ascendex; + [SupportedBroker.bequant]: bequant; + [SupportedBroker.bigone]: bigone; + [SupportedBroker.binance]: binance; + [SupportedBroker.binancecoinm]: binancecoinm; + [SupportedBroker.binanceus]: binanceus; + [SupportedBroker.binanceusdm]: binanceusdm; + [SupportedBroker.bingx]: bingx; + [SupportedBroker.bit2c]: bit2c; + [SupportedBroker.bitbank]: bitbank; + [SupportedBroker.bitbns]: bitbns; + [SupportedBroker.bitfinex]: bitfinex; + [SupportedBroker.bitflyer]: bitflyer; + [SupportedBroker.bitget]: bitget; + [SupportedBroker.bithumb]: bithumb; + [SupportedBroker.bitmart]: bitmart; + [SupportedBroker.bitmex]: bitmex; + [SupportedBroker.bitopro]: bitopro; + [SupportedBroker.bitrue]: bitrue; + [SupportedBroker.bitso]: bitso; + [SupportedBroker.bitstamp]: bitstamp; + [SupportedBroker.bitteam]: bitteam; + [SupportedBroker.bittrade]: bittrade; + [SupportedBroker.bitvavo]: bitvavo; + [SupportedBroker.blockchaincom]: blockchaincom; + [SupportedBroker.blofin]: blofin; + [SupportedBroker.btcalpha]: btcalpha; + [SupportedBroker.btcbox]: btcbox; + [SupportedBroker.btcmarkets]: btcmarkets; + [SupportedBroker.btcturk]: btcturk; + [SupportedBroker.bybit]: bybit; + [SupportedBroker.cex]: cex; + [SupportedBroker.coinbase]: coinbase; + [SupportedBroker.coinbaseadvanced]: coinbaseadvanced; + [SupportedBroker.coinbaseexchange]: coinbaseexchange; + [SupportedBroker.coinbaseinternational]: coinbaseinternational; + [SupportedBroker.coincatch]: coincatch; + [SupportedBroker.coincheck]: coincheck; + [SupportedBroker.coinex]: coinex; + [SupportedBroker.coinmate]: coinmate; + [SupportedBroker.coinmetro]: coinmetro; + [SupportedBroker.coinone]: coinone; + [SupportedBroker.coinsph]: coinsph; + [SupportedBroker.coinspot]: coinspot; + [SupportedBroker.cryptocom]: cryptocom; + [SupportedBroker.cryptomus]: cryptomus; + [SupportedBroker.defx]: defx; + [SupportedBroker.delta]: delta; + [SupportedBroker.deribit]: deribit; + [SupportedBroker.derive]: derive; + [SupportedBroker.digifinex]: digifinex; + [SupportedBroker.ellipx]: ellipx; + [SupportedBroker.exmo]: exmo; + [SupportedBroker.fmfwio]: fmfwio; + [SupportedBroker.gate]: gate; + [SupportedBroker.gateio]: gateio; + [SupportedBroker.gemini]: gemini; + [SupportedBroker.hashkey]: hashkey; + [SupportedBroker.hitbtc]: hitbtc; + [SupportedBroker.hollaex]: hollaex; + [SupportedBroker.htx]: htx; + [SupportedBroker.huobi]: huobi; + [SupportedBroker.hyperliquid]: hyperliquid; + [SupportedBroker.independentreserve]: independentreserve; + [SupportedBroker.indodax]: indodax; + [SupportedBroker.kraken]: kraken; + [SupportedBroker.krakenfutures]: krakenfutures; + [SupportedBroker.kucoin]: kucoin; + [SupportedBroker.kucoinfutures]: kucoinfutures; + [SupportedBroker.latoken]: latoken; + [SupportedBroker.lbank]: lbank; + [SupportedBroker.luno]: luno; + [SupportedBroker.mercado]: mercado; + [SupportedBroker.mexc]: mexc; + [SupportedBroker.modetrade]: modetrade; + [SupportedBroker.myokx]: myokx; + [SupportedBroker.ndax]: ndax; + [SupportedBroker.novadax]: novadax; + [SupportedBroker.oceanex]: oceanex; + [SupportedBroker.okcoin]: okcoin; + [SupportedBroker.okx]: okx; + [SupportedBroker.okxus]: okxus; + [SupportedBroker.onetrading]: onetrading; + [SupportedBroker.oxfun]: oxfun; + [SupportedBroker.p2b]: p2b; + [SupportedBroker.paradex]: paradex; + [SupportedBroker.paymium]: paymium; + [SupportedBroker.phemex]: phemex; + [SupportedBroker.poloniex]: poloniex; + [SupportedBroker.probit]: probit; + [SupportedBroker.timex]: timex; + [SupportedBroker.tokocrypto]: tokocrypto; + [SupportedBroker.tradeogre]: tradeogre; + [SupportedBroker.upbit]: upbit; + [SupportedBroker.vertex]: vertex; + [SupportedBroker.wavesexchange]: wavesexchange; + [SupportedBroker.whitebit]: whitebit; + [SupportedBroker.woo]: woo; + [SupportedBroker.woofipro]: woofipro; + [SupportedBroker.xt]: xt; + [SupportedBroker.yobit]: yobit; + [SupportedBroker.zaif]: zaif; + [SupportedBroker.zonda]: zonda; + }; + // Dynamic BrokerMap: each key maps to the correct broker type export type BrokerMap = Partial<{ [K in ISupportedBroker]: BrokerInstanceMap[K]; }>; + // Initialize brokers map const brokers: BrokerMap = {}; -// Conditionally initialize Bybit broker -if (config.brokers.includes(SupportedBroker.BYBIT as ISupportedBroker)) { - const bybitBroker = new ccxt.bybit({ - apiKey: config.bybitApiKey, - secret: config.bybitApiSecret, - defaultType: "spot", - }); - // Override Bybit API hostname - bybitBroker.options = { - ...bybitBroker.options, - hostname: "bytick.com", - }; - brokers[SupportedBroker.BYBIT as ISupportedBroker] = bybitBroker; -} - -// Conditionally initialize Binance broker -if (config.brokers.includes(SupportedBroker.BINANCE as ISupportedBroker)) { - const binanceBroker = new ccxt.binance({ - apiKey: config.binanceApiKey, - secret: config.binanceApiSecret, - defaultType: "spot", - }); - // Override Binance API hostname - binanceBroker.options = { - ...binanceBroker.options, - hostname: "binance.me", - }; - brokers[SupportedBroker.BINANCE as ISupportedBroker] = binanceBroker; -} export default brokers as Required; diff --git a/config/index.ts b/config/index.ts index 8635028..b7c2ab0 100644 --- a/config/index.ts +++ b/config/index.ts @@ -31,18 +31,18 @@ const isRequiredWhenBrokerInclude = ( const envVarsSchema = Joi.object({ port: Joi.number().default(8082), - bybitApiKey: isRequiredWhenBrokerInclude(Joi.string(), SupportedBroker.BYBIT), + bybitApiKey: isRequiredWhenBrokerInclude(Joi.string(), SupportedBroker.bybit), bybitApiSecret: isRequiredWhenBrokerInclude( Joi.string(), - SupportedBroker.BYBIT, + SupportedBroker.bybit, ), binanceApiKey: isRequiredWhenBrokerInclude( Joi.string(), - SupportedBroker.BINANCE, + SupportedBroker.binance, ), binanceApiSecret: isRequiredWhenBrokerInclude( Joi.string(), - SupportedBroker.BINANCE, + SupportedBroker.binance, ), }).unknown(); diff --git a/helpers/index.test.ts b/helpers/index.test.ts index 2d329c8..8a3071d 100644 --- a/helpers/index.test.ts +++ b/helpers/index.test.ts @@ -150,7 +150,7 @@ describe("Helper Functions", () => { test("should load policy successfully", () => { // This test will use the actual policy file const { loadPolicy } = require("./index"); - const policy = loadPolicy(); + const policy = loadPolicy("./policy/policy.json"); expect(policy).toBeDefined(); expect(policy.withdraw.rule.networks).toContain("ARBITRUM"); diff --git a/helpers/index.ts b/helpers/index.ts index aff6d21..984e946 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -1,5 +1,7 @@ import type { Exchange } from "ccxt"; import type { PolicyConfig } from "../types"; +import fs from "fs"; + /** * Fetches the order book, computes the worst‐case fill price for `size`, * and submits a single limit‐buy at that price. @@ -119,13 +121,8 @@ export async function sellAtOptimalPrice( /** * Loads and validates policy configuration */ -export function loadPolicy(): PolicyConfig { +export function loadPolicy(policyPath: string): PolicyConfig { try { - const fs = require("bun:fs"); - const path = require("bun:path"); - // TODO: Load the policy that we want from a hidden file in the root directory. We want to open source this Broker. - // TODO: Users can then copy the example policy.json file to root and run with it. - const policyPath = path.join(__dirname, "../policy/policy.json"); const policyData = fs.readFileSync(policyPath, "utf8"); return JSON.parse(policyData) as PolicyConfig; } catch (error) { diff --git a/index.ts b/index.ts index 2d8bad7..95f5c34 100644 --- a/index.ts +++ b/index.ts @@ -1,21 +1,16 @@ -import ccxt from "ccxt"; -import path from "bun:path"; +import ccxt, { type Exchange } from "ccxt"; +import path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "./proto/node"; import config from "./config"; -import brokers from "./config/broker"; -import type { OptimalPriceRequest } from "./proto/fietCexNode/OptimalPriceRequest"; import { - buyAtOptimalPrice, - sellAtOptimalPrice, loadPolicy, validateOrder, validateWithdraw, validateDeposit, isIpAllowed, } from "./helpers"; -import type { OptimalPriceResponse } from "./proto/fietCexNode/OptimalPriceResponse"; import type { BalanceRequest } from "./proto/fietCexNode/BalanceRequest"; import type { BalanceResponse } from "./proto/fietCexNode/BalanceResponse"; import type { PolicyConfig } from "./types"; @@ -29,6 +24,7 @@ import type { OrderDetailsRequest } from "./proto/fietCexNode/OrderDetailsReques import type { OrderDetailsResponse } from "./proto/fietCexNode/OrderDetailsResponse"; import type { CancelOrderRequest } from "./proto/fietCexNode/CancelOrderRequest"; import type { CancelOrderResponse } from "./proto/fietCexNode/CancelOrderResponse"; +import { watchFile, unwatchFile } from "fs"; const PROTO_FILE = "./proto/node.proto"; @@ -40,32 +36,158 @@ const fietCexNode = grpcObj.fietCexNode; console.log("CCXT Version:", ccxt.version); -async function main() { - // Load policy configuration - const policy = loadPolicy(); - console.log("Policy loaded successfully"); - - const server = getServer(policy); - - // TODO: Remove this... - console.log( - `BINANCE: Broker Balance ,${JSON.stringify(await brokers.BINANCE.fetchFreeBalance())}`, - ); - console.log( - `BYBIT: Broker Balance ,${JSON.stringify(await brokers.BYBIT.fetchFreeBalance())}`, - ); - - server.bindAsync( - `0.0.0.0:${config.port}`, - grpc.ServerCredentials.createInsecure(), - (err, port) => { - if (err) { - console.error(err); - return; +type BrokerCredentials = { + apiKey: string; + apiSecret: string; +}; + +export default class CEXBroker { + private brokerConfig: Record = {}; + #policyFilePath?: string; + private policy: PolicyConfig; + private brokers: Record = {}; + private server: grpc.Server | null = null; + + /** + * Loads environment variables prefixed with CEX_BROKER_ + * Expected format: + * CEX_BROKER__API_KEY + * CEX_BROKER__API_SECRET + */ + private loadEnvConfig(): void { + console.log("🔧 Loading CEX_BROKER_ environment variables:"); + const configMap: Record> = {}; + + for (const [key, value] of Object.entries(process.env)) { + if (!key.startsWith("CEX_BROKER_")) continue; + + const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); + if (!match) { + console.warn(`⚠️ Skipping unrecognized env var: ${key}`); + continue; } - console.log(`Your server as started on port ${port}`); - }, - ); + + const broker = match[1].toLowerCase(); // normalize to lowercase + const type = match[2].toLowerCase(); // 'key' or 'secret' + + if (!configMap[broker]) { + configMap[broker] = {}; + } + + if (type === "key") { + configMap[broker].apiKey = value || ""; + } else if (type === "secret") { + configMap[broker].apiSecret = value || ""; + } + } + + if (Object.keys(configMap).length === 0) { + console.error(`❌ NO CEX Broker Key Found`); + } + + // Finalize config and print result per broker + for (const [broker, creds] of Object.entries(configMap)) { + const hasKey = !!creds.apiKey; + const hasSecret = !!creds.apiSecret; + + if (hasKey && hasSecret) { + this.brokerConfig[broker] = { + apiKey: creds.apiKey ?? "", + apiSecret: creds.apiSecret ?? "", + }; + console.log( + `✅ Loaded credentials for broker "${broker.toUpperCase()}"`, + ); + const ExchangeClass = (ccxt as any)[broker]; + const client = new ExchangeClass({ + apiKey: creds.apiKey, + secret: creds.apiSecret, + enableRateLimit: true, + defaultType: "spot", + }); + this.brokers[broker] = client; + } else { + const missing = []; + if (!hasKey) missing.push("API_KEY"); + if (!hasSecret) missing.push("API_SECRET"); + console.warn( + `❌ Missing ${missing.join(" and ")} for broker "${broker.toUpperCase()}"`, + ); + } + } + } + + constructor(policies: string | PolicyConfig) { + if (typeof policies === "string") { + this.#policyFilePath = policies; + this.policy = loadPolicy(policies); + } else { + this.policy = policies; + } + + // If monitoring a file, start watcher + if (this.#policyFilePath) { + this.watchPolicyFile(this.#policyFilePath); + } + + this.loadEnvConfig(); + } + + /** + * Watches the policy JSON file for changes, reloads policies, and reruns broker. + * @param filePath + */ + private watchPolicyFile(filePath: string): void { + watchFile(filePath, { interval: 1000 }, (curr, prev) => { + if (curr.mtime > prev.mtime) { + try { + const updated = loadPolicy(filePath); + this.policy = updated; + console.log( + `Policies reloaded from ${filePath} at ${new Date().toISOString()}`, + ); + // Rerun broker with updated policies + this.run(); + } catch (err) { + console.error(`Error reloading policies: ${err}`); + } + } + }); + } + + /** + * Stops watching the policy file, if applicable. + */ + public stopWatchingPolicies(): void { + if (this.#policyFilePath) { + unwatchFile(this.#policyFilePath); + console.log(`Stopped watching policy file: ${this.#policyFilePath}`); + } + } + + /** + * Starts the broker, applying policies then running appropriate tasks. + */ + public async run(): Promise { + if (this.server) { + await this.server.forceShutdown(); + } + console.log(`Running CEXBroker at ${new Date().toISOString()}`); + this.server = getServer(this.policy, this.brokers); + + this.server.bindAsync( + `0.0.0.0:${config.port}`, + grpc.ServerCredentials.createInsecure(), + (err, port) => { + if (err) { + console.error(err); + return; + } + console.log(`Your server as started on port ${port}`); + }, + ); + return this.server; + } } function authenticateRequest(call: grpc.ServerUnaryCall): boolean { @@ -79,93 +201,10 @@ function authenticateRequest(call: grpc.ServerUnaryCall): boolean { return true; } -function getServer(policy: PolicyConfig) { +function getServer(policy: PolicyConfig, brokers: Record) { const server = new grpc.Server(); server.addService(fietCexNode.CexService.service, { // TODO: Consolidate all of these calls into "ExecuteAction", "SubscribeToStream"... - - // TODO: Getting optimal price is for the MM tech to decide... - GetOptimalPrice: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { mode, symbol, quantity } = - call.request as Required; - - // Validate required fields - if (mode === undefined || mode === null) { - // Return a gRPC error - const error = { - code: grpc.status.INVALID_ARGUMENT, - message: "Mode is required and must be BUY or SELL", - }; - return callback(error, null); - } - - if (!symbol) { - const error = { - code: grpc.status.INVALID_ARGUMENT, - message: "Symbol is required", - }; - return callback(error, null); - } - - if (!quantity || Number(quantity) <= 0) { - const error = { - code: grpc.status.INVALID_ARGUMENT, - message: "Quantity must be a positive number", - }; - return callback(error, null); - } - // Extract tokens from symbol (e.g., "ARB/USDT" -> fromToken: "ARB", toToken: "USDT") - const tokens = symbol.split("/"); - if (tokens.length !== 2) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "Invalid symbol format. Expected format: TOKEN1/TOKEN2", - }, - null, - ); - } - - if (mode === 1) { - const BINANCE = await sellAtOptimalPrice( - brokers.BINANCE, - symbol, - Number(quantity), - ); - const BYBIT = await sellAtOptimalPrice( - brokers.BYBIT, - symbol, - Number(quantity), - ); - return callback(null, { results: { BINANCE, BYBIT } }); - } else { - const BINANCE = await buyAtOptimalPrice( - brokers.BINANCE, - symbol, - Number(quantity), - ); - const BYBIT = await buyAtOptimalPrice( - brokers.BYBIT, - symbol, - Number(quantity), - ); - return callback(null, { results: { BINANCE, BYBIT } }); - } - }, Deposit: async ( call: grpc.ServerUnaryCall< DepositConfirmationRequest, @@ -293,6 +332,16 @@ function getServer(policy: PolicyConfig) { // Validate CEX key const broker = brokers[cex as keyof typeof brokers]; + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + const data = await broker.fetchCurrencies("USDT"); const networks = Object.keys( (data[token] ?? { networks: [] }).networks, @@ -392,6 +441,16 @@ function getServer(policy: PolicyConfig) { const symbol = market?.split(":")[1] ?? ""; const [from, _to] = symbol.split("/"); + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + const order = await broker.createLimitOrder( symbol, from === fromToken ? "sell" : "buy", @@ -587,7 +646,7 @@ function getServer(policy: PolicyConfig) { ); } - const cancelledOrder = await broker.cancelOrder(orderId); + const cancelledOrder: any = await broker.cancelOrder(orderId); callback(null, { success: cancelledOrder.status === "canceled", @@ -607,5 +666,6 @@ function getServer(policy: PolicyConfig) { }); return server; } - -main(); +// const policyPath = "./policy/policy.json" +// const broker = new CEXBroker(policyPath); +// broker.run().then(e=>console.log({e})) diff --git a/integration.test.ts b/integration.test.ts index 1bb4ae5..913fc79 100644 --- a/integration.test.ts +++ b/integration.test.ts @@ -44,7 +44,7 @@ describe("Integration Tests", () => { const { validateWithdraw } = require("./helpers"); const { loadPolicy } = require("./helpers"); - const policy = loadPolicy(); + const policy = loadPolicy("./policy/policy.json"); // Test valid withdrawal const validResult = validateWithdraw( @@ -73,7 +73,7 @@ describe("Integration Tests", () => { const { validateOrder } = require("./helpers"); const { loadPolicy } = require("./helpers"); - const policy = loadPolicy(); + const policy = loadPolicy("./policy/policy.json"); // Test valid order const validResult = validateOrder(policy, "USDT", "ETH", 1, "BINANCE"); @@ -142,7 +142,7 @@ describe("Integration Tests", () => { const { validateOrder } = require("./helpers"); const { loadPolicy } = require("./helpers"); - const policy = loadPolicy(); + const policy = loadPolicy("./policy/policy.json"); // Test with invalid symbol format const result = validateOrder( diff --git a/package.json b/package.json index 1bcf90e..bc3de0e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "license": "MIT", "devDependencies": { "@biomejs/biome": "2.0.6", - "@types/bun": "^1.2.18", + "@types/bun": "latest", + "bun-plugin-dts": "^0.3.0", "bun-types": "latest", "husky": "^9.1.7" }, @@ -33,5 +34,12 @@ "ccxt": "^4.4.91", "dotenv": "^17.0.0", "joi": "^17.13.3" - } + }, + "homepage": "https://github.com/usherlabs/cex-broker#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "contributors": [ + "Oki Ayobami (https://github.com/xlassix)" + ] } diff --git a/types.ts b/types.ts index c1ad735..70eb699 100644 --- a/types.ts +++ b/types.ts @@ -50,13 +50,114 @@ export type Policy = { export type Policies = { [apiKey: string]: Policy; // key is an Ethereum-style address like '0x...' }; - -// TODO: Why are Bybit and Binance so tightly integrated into this? Should we not simply have a dynamic conversion between CCXT interface and this node? -// TODO: Otheriwse, we'll need to setup every integration supported by CCXT indidivually inside of this node? -// TODO: Rename "broker" to "cexs" or "exchanges"... -// ? The Node itself is a broker. The destination of requests is the CEX... -export const BrokerList = ["BINANCE", "BYBIT"] as const; - +export const BrokerList = [ + "alpaca", + "apex", + "ascendex", + "bequant", + "bigone", + "binance", + "binancecoinm", + "binanceus", + "binanceusdm", + "bingx", + "bit2c", + "bitbank", + "bitbns", + "bitfinex", + "bitflyer", + "bitget", + "bithumb", + "bitmart", + "bitmex", + "bitopro", + "bitrue", + "bitso", + "bitstamp", + "bitteam", + "bittrade", + "bitvavo", + "blockchaincom", + "blofin", + "btcalpha", + "btcbox", + "btcmarkets", + "btcturk", + "bybit", + "cex", + "coinbase", + "coinbaseadvanced", + "coinbaseexchange", + "coinbaseinternational", + "coincatch", + "coincheck", + "coinex", + "coinmate", + "coinmetro", + "coinone", + "coinsph", + "coinspot", + "cryptocom", + "cryptomus", + "defx", + "delta", + "deribit", + "derive", + "digifinex", + "ellipx", + "exmo", + "fmfwio", + "gate", + "gateio", + "gemini", + "hashkey", + "hitbtc", + "hollaex", + "htx", + "huobi", + "hyperliquid", + "independentreserve", + "indodax", + "kraken", + "krakenfutures", + "kucoin", + "kucoinfutures", + "latoken", + "lbank", + "luno", + "mercado", + "mexc", + "modetrade", + "myokx", + "ndax", + "novadax", + "oceanex", + "okcoin", + "okx", + "okxus", + "onetrading", + "oxfun", + "p2b", + "paradex", + "paymium", + "phemex", + "poloniex", + "probit", + "timex", + "tokocrypto", + "tradeogre", + "upbit", + "vertex", + "wavesexchange", + "whitebit", + "woo", + "woofipro", + "xt", + "yobit", + "zaif", + "zonda" + ] as const; + export type ISupportedBroker = (typeof BrokerList)[number]; export const SupportedBroker = BrokerList.reduce( From cd5b8e0c5895df9020a174e54e0bccf54fbe6255 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 00:48:00 +0100 Subject: [PATCH 03/45] Add build:ts script and remove homepage field --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc3de0e..61b5a4a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "proto-gen": "./proto-gen.sh", "start": "bun run index.ts", "build": "bun build ./index.ts --outdir ./build --target bun", + "build:ts": "bun run ./build.ts", "test": "bun test", "format": "bunx biome format --write", "lint": "bunx biome lint --write", @@ -35,7 +36,6 @@ "dotenv": "^17.0.0", "joi": "^17.13.3" }, - "homepage": "https://github.com/usherlabs/cex-broker#readme", "publishConfig": { "registry": "https://registry.npmjs.org" }, From 71fda9efef2f25252a55931159953b165b4a23c1 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 00:51:31 +0100 Subject: [PATCH 04/45] Refactor to use cexBroker package and client --- client.ts | 2 +- index.ts | 26 ++-- proto/fietCexNode/BalanceRequest.ts | 12 -- proto/fietCexNode/BalanceResponse.ts | 12 -- proto/fietCexNode/CancelOrderRequest.ts | 12 -- proto/fietCexNode/CancelOrderResponse.ts | 12 -- proto/fietCexNode/CexService.ts | 112 ------------------ proto/fietCexNode/ConvertRequest.ts | 18 --- proto/fietCexNode/ConvertResponse.ts | 10 -- .../fietCexNode/DepositConfirmationRequest.ts | 16 --- .../DepositConfirmationResponse.ts | 10 -- proto/fietCexNode/OptimalPriceRequest.ts | 15 --- proto/fietCexNode/OptimalPriceResponse.ts | 11 -- proto/fietCexNode/OrderDetailsRequest.ts | 12 -- proto/fietCexNode/OrderDetailsResponse.ts | 22 ---- proto/fietCexNode/OrderMode.ts | 14 --- proto/fietCexNode/PriceInfo.ts | 12 -- proto/fietCexNode/TransferRequest.ts | 18 --- proto/fietCexNode/TransferResponse.ts | 12 -- proto/node.proto | 5 +- proto/node.ts | 6 +- 21 files changed, 19 insertions(+), 350 deletions(-) delete mode 100644 proto/fietCexNode/BalanceRequest.ts delete mode 100644 proto/fietCexNode/BalanceResponse.ts delete mode 100644 proto/fietCexNode/CancelOrderRequest.ts delete mode 100644 proto/fietCexNode/CancelOrderResponse.ts delete mode 100644 proto/fietCexNode/CexService.ts delete mode 100644 proto/fietCexNode/ConvertRequest.ts delete mode 100644 proto/fietCexNode/ConvertResponse.ts delete mode 100644 proto/fietCexNode/DepositConfirmationRequest.ts delete mode 100644 proto/fietCexNode/DepositConfirmationResponse.ts delete mode 100644 proto/fietCexNode/OptimalPriceRequest.ts delete mode 100644 proto/fietCexNode/OptimalPriceResponse.ts delete mode 100644 proto/fietCexNode/OrderDetailsRequest.ts delete mode 100644 proto/fietCexNode/OrderDetailsResponse.ts delete mode 100644 proto/fietCexNode/OrderMode.ts delete mode 100644 proto/fietCexNode/PriceInfo.ts delete mode 100644 proto/fietCexNode/TransferRequest.ts delete mode 100644 proto/fietCexNode/TransferResponse.ts diff --git a/client.ts b/client.ts index db381c6..61491ec 100644 --- a/client.ts +++ b/client.ts @@ -11,7 +11,7 @@ const grpcObj = grpc.loadPackageDefinition( packageDef, ) as unknown as ProtoGrpcType; -const client = new grpcObj.fietCexNode.CexService( +const client = new grpcObj.cexBroker.CexService( `0.0.0.0:${config.port}`, grpc.credentials.createInsecure(), ); diff --git a/index.ts b/index.ts index 95f5c34..7429301 100644 --- a/index.ts +++ b/index.ts @@ -11,19 +11,19 @@ import { validateDeposit, isIpAllowed, } from "./helpers"; -import type { BalanceRequest } from "./proto/fietCexNode/BalanceRequest"; -import type { BalanceResponse } from "./proto/fietCexNode/BalanceResponse"; +import type { BalanceRequest } from "./proto/cexBroker/BalanceRequest"; +import type { BalanceResponse } from "./proto/cexBroker/BalanceResponse"; import type { PolicyConfig } from "./types"; -import type { TransferRequest } from "./proto/fietCexNode/TransferRequest"; -import type { TransferResponse } from "./proto/fietCexNode/TransferResponse"; -import type { DepositConfirmationRequest } from "./proto/fietCexNode/DepositConfirmationRequest"; -import type { DepositConfirmationResponse } from "./proto/fietCexNode/DepositConfirmationResponse"; -import type { ConvertRequest } from "./proto/fietCexNode/ConvertRequest"; -import type { ConvertResponse } from "./proto/fietCexNode/ConvertResponse"; -import type { OrderDetailsRequest } from "./proto/fietCexNode/OrderDetailsRequest"; -import type { OrderDetailsResponse } from "./proto/fietCexNode/OrderDetailsResponse"; -import type { CancelOrderRequest } from "./proto/fietCexNode/CancelOrderRequest"; -import type { CancelOrderResponse } from "./proto/fietCexNode/CancelOrderResponse"; +import type { TransferRequest } from "./proto/cexBroker/TransferRequest"; +import type { TransferResponse } from "./proto/cexBroker/TransferResponse"; +import type { DepositConfirmationRequest } from "./proto/cexBroker/DepositConfirmationRequest"; +import type { DepositConfirmationResponse } from "./proto/cexBroker/DepositConfirmationResponse"; +import type { ConvertRequest } from "./proto/cexBroker/ConvertRequest"; +import type { ConvertResponse } from "./proto/cexBroker/ConvertResponse"; +import type { OrderDetailsRequest } from "./proto/cexBroker/OrderDetailsRequest"; +import type { OrderDetailsResponse } from "./proto/cexBroker/OrderDetailsResponse"; +import type { CancelOrderRequest } from "./proto/cexBroker/CancelOrderRequest"; +import type { CancelOrderResponse } from "./proto/cexBroker/CancelOrderResponse"; import { watchFile, unwatchFile } from "fs"; const PROTO_FILE = "./proto/node.proto"; @@ -32,7 +32,7 @@ const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); const grpcObj = grpc.loadPackageDefinition( packageDef, ) as unknown as ProtoGrpcType; -const fietCexNode = grpcObj.fietCexNode; +const fietCexNode = grpcObj.cexBroker; console.log("CCXT Version:", ccxt.version); diff --git a/proto/fietCexNode/BalanceRequest.ts b/proto/fietCexNode/BalanceRequest.ts deleted file mode 100644 index b4a08c9..0000000 --- a/proto/fietCexNode/BalanceRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface BalanceRequest { - 'cex'?: (string); - 'token'?: (string); -} - -export interface BalanceRequest__Output { - 'cex'?: (string); - 'token'?: (string); -} diff --git a/proto/fietCexNode/BalanceResponse.ts b/proto/fietCexNode/BalanceResponse.ts deleted file mode 100644 index 0c81f55..0000000 --- a/proto/fietCexNode/BalanceResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface BalanceResponse { - 'balance'?: (number | string); - 'currency'?: (string); -} - -export interface BalanceResponse__Output { - 'balance'?: (number); - 'currency'?: (string); -} diff --git a/proto/fietCexNode/CancelOrderRequest.ts b/proto/fietCexNode/CancelOrderRequest.ts deleted file mode 100644 index f8d5c1c..0000000 --- a/proto/fietCexNode/CancelOrderRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface CancelOrderRequest { - 'orderId'?: (string); - 'cex'?: (string); -} - -export interface CancelOrderRequest__Output { - 'orderId'?: (string); - 'cex'?: (string); -} diff --git a/proto/fietCexNode/CancelOrderResponse.ts b/proto/fietCexNode/CancelOrderResponse.ts deleted file mode 100644 index 72837e7..0000000 --- a/proto/fietCexNode/CancelOrderResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface CancelOrderResponse { - 'success'?: (boolean); - 'finalStatus'?: (string); -} - -export interface CancelOrderResponse__Output { - 'success'?: (boolean); - 'finalStatus'?: (string); -} diff --git a/proto/fietCexNode/CexService.ts b/proto/fietCexNode/CexService.ts deleted file mode 100644 index 499321c..0000000 --- a/proto/fietCexNode/CexService.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Original file: proto/node.proto - -import type * as grpc from '@grpc/grpc-js' -import type { MethodDefinition } from '@grpc/proto-loader' -import type { BalanceRequest as _fietCexNode_BalanceRequest, BalanceRequest__Output as _fietCexNode_BalanceRequest__Output } from '../fietCexNode/BalanceRequest'; -import type { BalanceResponse as _fietCexNode_BalanceResponse, BalanceResponse__Output as _fietCexNode_BalanceResponse__Output } from '../fietCexNode/BalanceResponse'; -import type { CancelOrderRequest as _fietCexNode_CancelOrderRequest, CancelOrderRequest__Output as _fietCexNode_CancelOrderRequest__Output } from '../fietCexNode/CancelOrderRequest'; -import type { CancelOrderResponse as _fietCexNode_CancelOrderResponse, CancelOrderResponse__Output as _fietCexNode_CancelOrderResponse__Output } from '../fietCexNode/CancelOrderResponse'; -import type { ConvertRequest as _fietCexNode_ConvertRequest, ConvertRequest__Output as _fietCexNode_ConvertRequest__Output } from '../fietCexNode/ConvertRequest'; -import type { ConvertResponse as _fietCexNode_ConvertResponse, ConvertResponse__Output as _fietCexNode_ConvertResponse__Output } from '../fietCexNode/ConvertResponse'; -import type { DepositConfirmationRequest as _fietCexNode_DepositConfirmationRequest, DepositConfirmationRequest__Output as _fietCexNode_DepositConfirmationRequest__Output } from '../fietCexNode/DepositConfirmationRequest'; -import type { DepositConfirmationResponse as _fietCexNode_DepositConfirmationResponse, DepositConfirmationResponse__Output as _fietCexNode_DepositConfirmationResponse__Output } from '../fietCexNode/DepositConfirmationResponse'; -import type { OptimalPriceRequest as _fietCexNode_OptimalPriceRequest, OptimalPriceRequest__Output as _fietCexNode_OptimalPriceRequest__Output } from '../fietCexNode/OptimalPriceRequest'; -import type { OptimalPriceResponse as _fietCexNode_OptimalPriceResponse, OptimalPriceResponse__Output as _fietCexNode_OptimalPriceResponse__Output } from '../fietCexNode/OptimalPriceResponse'; -import type { OrderDetailsRequest as _fietCexNode_OrderDetailsRequest, OrderDetailsRequest__Output as _fietCexNode_OrderDetailsRequest__Output } from '../fietCexNode/OrderDetailsRequest'; -import type { OrderDetailsResponse as _fietCexNode_OrderDetailsResponse, OrderDetailsResponse__Output as _fietCexNode_OrderDetailsResponse__Output } from '../fietCexNode/OrderDetailsResponse'; -import type { TransferRequest as _fietCexNode_TransferRequest, TransferRequest__Output as _fietCexNode_TransferRequest__Output } from '../fietCexNode/TransferRequest'; -import type { TransferResponse as _fietCexNode_TransferResponse, TransferResponse__Output as _fietCexNode_TransferResponse__Output } from '../fietCexNode/TransferResponse'; - -// TODO: Is this file auto-generated? If so, can we gitignore it, and add protogen to the post-install script? Unless there's a reason not too.. -export interface CexServiceClient extends grpc.Client { - CancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _fietCexNode_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _fietCexNode_CancelOrderRequest, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _fietCexNode_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _fietCexNode_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _fietCexNode_CancelOrderRequest, callback: grpc.requestCallback<_fietCexNode_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - - Convert(argument: _fietCexNode_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _fietCexNode_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _fietCexNode_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _fietCexNode_ConvertRequest, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _fietCexNode_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _fietCexNode_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _fietCexNode_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _fietCexNode_ConvertRequest, callback: grpc.requestCallback<_fietCexNode_ConvertResponse__Output>): grpc.ClientUnaryCall; - - Deposit(argument: _fietCexNode_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _fietCexNode_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _fietCexNode_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _fietCexNode_DepositConfirmationRequest, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _fietCexNode_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _fietCexNode_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _fietCexNode_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _fietCexNode_DepositConfirmationRequest, callback: grpc.requestCallback<_fietCexNode_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - - GetBalance(argument: _fietCexNode_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _fietCexNode_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _fietCexNode_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _fietCexNode_BalanceRequest, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _fietCexNode_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _fietCexNode_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _fietCexNode_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _fietCexNode_BalanceRequest, callback: grpc.requestCallback<_fietCexNode_BalanceResponse__Output>): grpc.ClientUnaryCall; - - GetOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - GetOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - GetOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - GetOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - getOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - getOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - getOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - getOptimalPrice(argument: _fietCexNode_OptimalPriceRequest, callback: grpc.requestCallback<_fietCexNode_OptimalPriceResponse__Output>): grpc.ClientUnaryCall; - - GetOrderDetails(argument: _fietCexNode_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _fietCexNode_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _fietCexNode_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _fietCexNode_OrderDetailsRequest, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _fietCexNode_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _fietCexNode_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _fietCexNode_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _fietCexNode_OrderDetailsRequest, callback: grpc.requestCallback<_fietCexNode_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - - Transfer(argument: _fietCexNode_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _fietCexNode_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _fietCexNode_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _fietCexNode_TransferRequest, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _fietCexNode_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _fietCexNode_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _fietCexNode_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _fietCexNode_TransferRequest, callback: grpc.requestCallback<_fietCexNode_TransferResponse__Output>): grpc.ClientUnaryCall; - -} - -export interface CexServiceHandlers extends grpc.UntypedServiceImplementation { - CancelOrder: grpc.handleUnaryCall<_fietCexNode_CancelOrderRequest__Output, _fietCexNode_CancelOrderResponse>; - - Convert: grpc.handleUnaryCall<_fietCexNode_ConvertRequest__Output, _fietCexNode_ConvertResponse>; - - Deposit: grpc.handleUnaryCall<_fietCexNode_DepositConfirmationRequest__Output, _fietCexNode_DepositConfirmationResponse>; - - GetBalance: grpc.handleUnaryCall<_fietCexNode_BalanceRequest__Output, _fietCexNode_BalanceResponse>; - - GetOptimalPrice: grpc.handleUnaryCall<_fietCexNode_OptimalPriceRequest__Output, _fietCexNode_OptimalPriceResponse>; - - GetOrderDetails: grpc.handleUnaryCall<_fietCexNode_OrderDetailsRequest__Output, _fietCexNode_OrderDetailsResponse>; - - Transfer: grpc.handleUnaryCall<_fietCexNode_TransferRequest__Output, _fietCexNode_TransferResponse>; - -} - -export interface CexServiceDefinition extends grpc.ServiceDefinition { - CancelOrder: MethodDefinition<_fietCexNode_CancelOrderRequest, _fietCexNode_CancelOrderResponse, _fietCexNode_CancelOrderRequest__Output, _fietCexNode_CancelOrderResponse__Output> - Convert: MethodDefinition<_fietCexNode_ConvertRequest, _fietCexNode_ConvertResponse, _fietCexNode_ConvertRequest__Output, _fietCexNode_ConvertResponse__Output> - Deposit: MethodDefinition<_fietCexNode_DepositConfirmationRequest, _fietCexNode_DepositConfirmationResponse, _fietCexNode_DepositConfirmationRequest__Output, _fietCexNode_DepositConfirmationResponse__Output> - GetBalance: MethodDefinition<_fietCexNode_BalanceRequest, _fietCexNode_BalanceResponse, _fietCexNode_BalanceRequest__Output, _fietCexNode_BalanceResponse__Output> - GetOptimalPrice: MethodDefinition<_fietCexNode_OptimalPriceRequest, _fietCexNode_OptimalPriceResponse, _fietCexNode_OptimalPriceRequest__Output, _fietCexNode_OptimalPriceResponse__Output> - GetOrderDetails: MethodDefinition<_fietCexNode_OrderDetailsRequest, _fietCexNode_OrderDetailsResponse, _fietCexNode_OrderDetailsRequest__Output, _fietCexNode_OrderDetailsResponse__Output> - Transfer: MethodDefinition<_fietCexNode_TransferRequest, _fietCexNode_TransferResponse, _fietCexNode_TransferRequest__Output, _fietCexNode_TransferResponse__Output> -} diff --git a/proto/fietCexNode/ConvertRequest.ts b/proto/fietCexNode/ConvertRequest.ts deleted file mode 100644 index 832385f..0000000 --- a/proto/fietCexNode/ConvertRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Original file: proto/node.proto - - -export interface ConvertRequest { - 'fromToken'?: (string); - 'toToken'?: (string); - 'amount'?: (number | string); - 'price'?: (number | string); - 'cex'?: (string); -} - -export interface ConvertRequest__Output { - 'fromToken'?: (string); - 'toToken'?: (string); - 'amount'?: (number); - 'price'?: (number); - 'cex'?: (string); -} diff --git a/proto/fietCexNode/ConvertResponse.ts b/proto/fietCexNode/ConvertResponse.ts deleted file mode 100644 index 44b2b9e..0000000 --- a/proto/fietCexNode/ConvertResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Original file: proto/node.proto - - -export interface ConvertResponse { - 'orderId'?: (string); -} - -export interface ConvertResponse__Output { - 'orderId'?: (string); -} diff --git a/proto/fietCexNode/DepositConfirmationRequest.ts b/proto/fietCexNode/DepositConfirmationRequest.ts deleted file mode 100644 index b5b3149..0000000 --- a/proto/fietCexNode/DepositConfirmationRequest.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Original file: proto/node.proto - - -export interface DepositConfirmationRequest { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number | string); - 'transactionHash'?: (string); -} - -export interface DepositConfirmationRequest__Output { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number); - 'transactionHash'?: (string); -} diff --git a/proto/fietCexNode/DepositConfirmationResponse.ts b/proto/fietCexNode/DepositConfirmationResponse.ts deleted file mode 100644 index b690b56..0000000 --- a/proto/fietCexNode/DepositConfirmationResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Original file: proto/node.proto - - -export interface DepositConfirmationResponse { - 'newBalance'?: (number | string); -} - -export interface DepositConfirmationResponse__Output { - 'newBalance'?: (number); -} diff --git a/proto/fietCexNode/OptimalPriceRequest.ts b/proto/fietCexNode/OptimalPriceRequest.ts deleted file mode 100644 index 5f12b6a..0000000 --- a/proto/fietCexNode/OptimalPriceRequest.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Original file: proto/node.proto - -import type { OrderMode as _fietCexNode_OrderMode, OrderMode__Output as _fietCexNode_OrderMode__Output } from '../fietCexNode/OrderMode'; - -export interface OptimalPriceRequest { - 'symbol'?: (string); - 'quantity'?: (number | string); - 'mode'?: (_fietCexNode_OrderMode); -} - -export interface OptimalPriceRequest__Output { - 'symbol'?: (string); - 'quantity'?: (number); - 'mode'?: (_fietCexNode_OrderMode__Output); -} diff --git a/proto/fietCexNode/OptimalPriceResponse.ts b/proto/fietCexNode/OptimalPriceResponse.ts deleted file mode 100644 index 4b2b97a..0000000 --- a/proto/fietCexNode/OptimalPriceResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Original file: proto/node.proto - -import type { PriceInfo as _fietCexNode_PriceInfo, PriceInfo__Output as _fietCexNode_PriceInfo__Output } from '../fietCexNode/PriceInfo'; - -export interface OptimalPriceResponse { - 'results'?: ({[key: string]: _fietCexNode_PriceInfo}); -} - -export interface OptimalPriceResponse__Output { - 'results'?: ({[key: string]: _fietCexNode_PriceInfo__Output}); -} diff --git a/proto/fietCexNode/OrderDetailsRequest.ts b/proto/fietCexNode/OrderDetailsRequest.ts deleted file mode 100644 index ffd65d9..0000000 --- a/proto/fietCexNode/OrderDetailsRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface OrderDetailsRequest { - 'orderId'?: (string); - 'cex'?: (string); -} - -export interface OrderDetailsRequest__Output { - 'orderId'?: (string); - 'cex'?: (string); -} diff --git a/proto/fietCexNode/OrderDetailsResponse.ts b/proto/fietCexNode/OrderDetailsResponse.ts deleted file mode 100644 index e80ad1a..0000000 --- a/proto/fietCexNode/OrderDetailsResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Original file: proto/node.proto - - -export interface OrderDetailsResponse { - 'orderId'?: (string); - 'status'?: (string); - 'originalAmount'?: (number | string); - 'filledAmount'?: (number | string); - 'symbol'?: (string); - 'mode'?: (string); - 'price'?: (number | string); -} - -export interface OrderDetailsResponse__Output { - 'orderId'?: (string); - 'status'?: (string); - 'originalAmount'?: (number); - 'filledAmount'?: (number); - 'symbol'?: (string); - 'mode'?: (string); - 'price'?: (number); -} diff --git a/proto/fietCexNode/OrderMode.ts b/proto/fietCexNode/OrderMode.ts deleted file mode 100644 index 721639e..0000000 --- a/proto/fietCexNode/OrderMode.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Original file: proto/node.proto - -export const OrderMode = { - BUY: 0, - SELL: 1, -} as const; - -export type OrderMode = - | 'BUY' - | 0 - | 'SELL' - | 1 - -export type OrderMode__Output = typeof OrderMode[keyof typeof OrderMode] diff --git a/proto/fietCexNode/PriceInfo.ts b/proto/fietCexNode/PriceInfo.ts deleted file mode 100644 index 1215ad7..0000000 --- a/proto/fietCexNode/PriceInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface PriceInfo { - 'avgPrice'?: (number | string); - 'fillPrice'?: (number | string); -} - -export interface PriceInfo__Output { - 'avgPrice'?: (number); - 'fillPrice'?: (number); -} diff --git a/proto/fietCexNode/TransferRequest.ts b/proto/fietCexNode/TransferRequest.ts deleted file mode 100644 index faf16c7..0000000 --- a/proto/fietCexNode/TransferRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Original file: proto/node.proto - - -export interface TransferRequest { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number | string); - 'cex'?: (string); - 'token'?: (string); -} - -export interface TransferRequest__Output { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number); - 'cex'?: (string); - 'token'?: (string); -} diff --git a/proto/fietCexNode/TransferResponse.ts b/proto/fietCexNode/TransferResponse.ts deleted file mode 100644 index 5ab2b7c..0000000 --- a/proto/fietCexNode/TransferResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface TransferResponse { - 'success'?: (boolean); - 'transactionId'?: (string); -} - -export interface TransferResponse__Output { - 'success'?: (boolean); - 'transactionId'?: (string); -} diff --git a/proto/node.proto b/proto/node.proto index 31d4f40..698fbdb 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package cex-broker; // TODO: I've renamed... +package cexBroker; // TODO: I've renamed... // TODO: This whole file can be a simple message... // TODO: A single generic CCXT action/message to executing any CCXT function... This way the broker is dynamic ... @@ -9,7 +9,7 @@ package cex-broker; // TODO: I've renamed... // map parameters = 2; // Parameters to pass to the CCXT method // string cex = 3; // CEX identifier (e.g., "binance", "bybit") // string symbol = 4; // Optional: trading pair symbol if needed -} +// } // message CcxtActionResponse { // bool success = 1; // Whether the action was successful @@ -111,7 +111,6 @@ service CexService { rpc Deposit(DepositConfirmationRequest) returns (DepositConfirmationResponse); rpc Transfer(TransferRequest) returns (TransferResponse); rpc Convert(ConvertRequest) returns (ConvertResponse); - rpc GetOptimalPrice(OptimalPriceRequest) returns (OptimalPriceResponse); rpc GetBalance(BalanceRequest) returns (BalanceResponse); rpc GetOrderDetails(OrderDetailsRequest) returns (OrderDetailsResponse); rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse); diff --git a/proto/node.ts b/proto/node.ts index 4284ee8..2eecc1d 100644 --- a/proto/node.ts +++ b/proto/node.ts @@ -1,19 +1,19 @@ import type * as grpc from '@grpc/grpc-js'; import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; -import type { CexServiceClient as _fietCexNode_CexServiceClient, CexServiceDefinition as _fietCexNode_CexServiceDefinition } from './fietCexNode/CexService'; +import type { CexServiceClient as _cexBroker_CexServiceClient, CexServiceDefinition as _cexBroker_CexServiceDefinition } from './cexBroker/CexService'; type SubtypeConstructor any, Subtype> = { new(...args: ConstructorParameters): Subtype; }; export interface ProtoGrpcType { - fietCexNode: { + cexBroker: { BalanceRequest: MessageTypeDefinition BalanceResponse: MessageTypeDefinition CancelOrderRequest: MessageTypeDefinition CancelOrderResponse: MessageTypeDefinition - CexService: SubtypeConstructor & { service: _fietCexNode_CexServiceDefinition } + CexService: SubtypeConstructor & { service: _cexBroker_CexServiceDefinition } ConvertRequest: MessageTypeDefinition ConvertResponse: MessageTypeDefinition DepositConfirmationRequest: MessageTypeDefinition From 99b747476fa3a422e2ab33b1d85c60bf3d9f3a06 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 00:55:21 +0100 Subject: [PATCH 05/45] Add cexBroker proto files for service implementation --- proto/cexBroker/BalanceRequest.ts | 12 +++ proto/cexBroker/BalanceResponse.ts | 12 +++ proto/cexBroker/CancelOrderRequest.ts | 12 +++ proto/cexBroker/CancelOrderResponse.ts | 12 +++ proto/cexBroker/CexService.ts | 97 +++++++++++++++++++ proto/cexBroker/ConvertRequest.ts | 18 ++++ proto/cexBroker/ConvertResponse.ts | 10 ++ proto/cexBroker/DepositConfirmationRequest.ts | 16 +++ .../cexBroker/DepositConfirmationResponse.ts | 10 ++ proto/cexBroker/OptimalPriceRequest.ts | 15 +++ proto/cexBroker/OptimalPriceResponse.ts | 11 +++ proto/cexBroker/OrderDetailsRequest.ts | 12 +++ proto/cexBroker/OrderDetailsResponse.ts | 22 +++++ proto/cexBroker/OrderMode.ts | 14 +++ proto/cexBroker/PriceInfo.ts | 12 +++ proto/cexBroker/TransferRequest.ts | 18 ++++ proto/cexBroker/TransferResponse.ts | 12 +++ proto/node.proto | 15 +-- proto/node.ts | 3 - 19 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 proto/cexBroker/BalanceRequest.ts create mode 100644 proto/cexBroker/BalanceResponse.ts create mode 100644 proto/cexBroker/CancelOrderRequest.ts create mode 100644 proto/cexBroker/CancelOrderResponse.ts create mode 100644 proto/cexBroker/CexService.ts create mode 100644 proto/cexBroker/ConvertRequest.ts create mode 100644 proto/cexBroker/ConvertResponse.ts create mode 100644 proto/cexBroker/DepositConfirmationRequest.ts create mode 100644 proto/cexBroker/DepositConfirmationResponse.ts create mode 100644 proto/cexBroker/OptimalPriceRequest.ts create mode 100644 proto/cexBroker/OptimalPriceResponse.ts create mode 100644 proto/cexBroker/OrderDetailsRequest.ts create mode 100644 proto/cexBroker/OrderDetailsResponse.ts create mode 100644 proto/cexBroker/OrderMode.ts create mode 100644 proto/cexBroker/PriceInfo.ts create mode 100644 proto/cexBroker/TransferRequest.ts create mode 100644 proto/cexBroker/TransferResponse.ts diff --git a/proto/cexBroker/BalanceRequest.ts b/proto/cexBroker/BalanceRequest.ts new file mode 100644 index 0000000..b4a08c9 --- /dev/null +++ b/proto/cexBroker/BalanceRequest.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface BalanceRequest { + 'cex'?: (string); + 'token'?: (string); +} + +export interface BalanceRequest__Output { + 'cex'?: (string); + 'token'?: (string); +} diff --git a/proto/cexBroker/BalanceResponse.ts b/proto/cexBroker/BalanceResponse.ts new file mode 100644 index 0000000..0c81f55 --- /dev/null +++ b/proto/cexBroker/BalanceResponse.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface BalanceResponse { + 'balance'?: (number | string); + 'currency'?: (string); +} + +export interface BalanceResponse__Output { + 'balance'?: (number); + 'currency'?: (string); +} diff --git a/proto/cexBroker/CancelOrderRequest.ts b/proto/cexBroker/CancelOrderRequest.ts new file mode 100644 index 0000000..f8d5c1c --- /dev/null +++ b/proto/cexBroker/CancelOrderRequest.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface CancelOrderRequest { + 'orderId'?: (string); + 'cex'?: (string); +} + +export interface CancelOrderRequest__Output { + 'orderId'?: (string); + 'cex'?: (string); +} diff --git a/proto/cexBroker/CancelOrderResponse.ts b/proto/cexBroker/CancelOrderResponse.ts new file mode 100644 index 0000000..72837e7 --- /dev/null +++ b/proto/cexBroker/CancelOrderResponse.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface CancelOrderResponse { + 'success'?: (boolean); + 'finalStatus'?: (string); +} + +export interface CancelOrderResponse__Output { + 'success'?: (boolean); + 'finalStatus'?: (string); +} diff --git a/proto/cexBroker/CexService.ts b/proto/cexBroker/CexService.ts new file mode 100644 index 0000000..9d9af0a --- /dev/null +++ b/proto/cexBroker/CexService.ts @@ -0,0 +1,97 @@ +// Original file: proto/node.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { BalanceRequest as _cexBroker_BalanceRequest, BalanceRequest__Output as _cexBroker_BalanceRequest__Output } from '../cexBroker/BalanceRequest'; +import type { BalanceResponse as _cexBroker_BalanceResponse, BalanceResponse__Output as _cexBroker_BalanceResponse__Output } from '../cexBroker/BalanceResponse'; +import type { CancelOrderRequest as _cexBroker_CancelOrderRequest, CancelOrderRequest__Output as _cexBroker_CancelOrderRequest__Output } from '../cexBroker/CancelOrderRequest'; +import type { CancelOrderResponse as _cexBroker_CancelOrderResponse, CancelOrderResponse__Output as _cexBroker_CancelOrderResponse__Output } from '../cexBroker/CancelOrderResponse'; +import type { ConvertRequest as _cexBroker_ConvertRequest, ConvertRequest__Output as _cexBroker_ConvertRequest__Output } from '../cexBroker/ConvertRequest'; +import type { ConvertResponse as _cexBroker_ConvertResponse, ConvertResponse__Output as _cexBroker_ConvertResponse__Output } from '../cexBroker/ConvertResponse'; +import type { DepositConfirmationRequest as _cexBroker_DepositConfirmationRequest, DepositConfirmationRequest__Output as _cexBroker_DepositConfirmationRequest__Output } from '../cexBroker/DepositConfirmationRequest'; +import type { DepositConfirmationResponse as _cexBroker_DepositConfirmationResponse, DepositConfirmationResponse__Output as _cexBroker_DepositConfirmationResponse__Output } from '../cexBroker/DepositConfirmationResponse'; +import type { OrderDetailsRequest as _cexBroker_OrderDetailsRequest, OrderDetailsRequest__Output as _cexBroker_OrderDetailsRequest__Output } from '../cexBroker/OrderDetailsRequest'; +import type { OrderDetailsResponse as _cexBroker_OrderDetailsResponse, OrderDetailsResponse__Output as _cexBroker_OrderDetailsResponse__Output } from '../cexBroker/OrderDetailsResponse'; +import type { TransferRequest as _cexBroker_TransferRequest, TransferRequest__Output as _cexBroker_TransferRequest__Output } from '../cexBroker/TransferRequest'; +import type { TransferResponse as _cexBroker_TransferResponse, TransferResponse__Output as _cexBroker_TransferResponse__Output } from '../cexBroker/TransferResponse'; + +export interface CexServiceClient extends grpc.Client { + CancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + CancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + CancelOrder(argument: _cexBroker_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + CancelOrder(argument: _cexBroker_CancelOrderRequest, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + cancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + cancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + cancelOrder(argument: _cexBroker_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + cancelOrder(argument: _cexBroker_CancelOrderRequest, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; + + Convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + Convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + Convert(argument: _cexBroker_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + Convert(argument: _cexBroker_ConvertRequest, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + convert(argument: _cexBroker_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + convert(argument: _cexBroker_ConvertRequest, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; + + Deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + Deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + Deposit(argument: _cexBroker_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + Deposit(argument: _cexBroker_DepositConfirmationRequest, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + deposit(argument: _cexBroker_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + deposit(argument: _cexBroker_DepositConfirmationRequest, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; + + GetBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + GetBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + GetBalance(argument: _cexBroker_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + GetBalance(argument: _cexBroker_BalanceRequest, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + getBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + getBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + getBalance(argument: _cexBroker_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + getBalance(argument: _cexBroker_BalanceRequest, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; + + GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + getOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + getOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + getOrderDetails(argument: _cexBroker_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + getOrderDetails(argument: _cexBroker_OrderDetailsRequest, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; + + Transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + Transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + Transfer(argument: _cexBroker_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + Transfer(argument: _cexBroker_TransferRequest, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + transfer(argument: _cexBroker_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + transfer(argument: _cexBroker_TransferRequest, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface CexServiceHandlers extends grpc.UntypedServiceImplementation { + CancelOrder: grpc.handleUnaryCall<_cexBroker_CancelOrderRequest__Output, _cexBroker_CancelOrderResponse>; + + Convert: grpc.handleUnaryCall<_cexBroker_ConvertRequest__Output, _cexBroker_ConvertResponse>; + + Deposit: grpc.handleUnaryCall<_cexBroker_DepositConfirmationRequest__Output, _cexBroker_DepositConfirmationResponse>; + + GetBalance: grpc.handleUnaryCall<_cexBroker_BalanceRequest__Output, _cexBroker_BalanceResponse>; + + GetOrderDetails: grpc.handleUnaryCall<_cexBroker_OrderDetailsRequest__Output, _cexBroker_OrderDetailsResponse>; + + Transfer: grpc.handleUnaryCall<_cexBroker_TransferRequest__Output, _cexBroker_TransferResponse>; + +} + +export interface CexServiceDefinition extends grpc.ServiceDefinition { + CancelOrder: MethodDefinition<_cexBroker_CancelOrderRequest, _cexBroker_CancelOrderResponse, _cexBroker_CancelOrderRequest__Output, _cexBroker_CancelOrderResponse__Output> + Convert: MethodDefinition<_cexBroker_ConvertRequest, _cexBroker_ConvertResponse, _cexBroker_ConvertRequest__Output, _cexBroker_ConvertResponse__Output> + Deposit: MethodDefinition<_cexBroker_DepositConfirmationRequest, _cexBroker_DepositConfirmationResponse, _cexBroker_DepositConfirmationRequest__Output, _cexBroker_DepositConfirmationResponse__Output> + GetBalance: MethodDefinition<_cexBroker_BalanceRequest, _cexBroker_BalanceResponse, _cexBroker_BalanceRequest__Output, _cexBroker_BalanceResponse__Output> + GetOrderDetails: MethodDefinition<_cexBroker_OrderDetailsRequest, _cexBroker_OrderDetailsResponse, _cexBroker_OrderDetailsRequest__Output, _cexBroker_OrderDetailsResponse__Output> + Transfer: MethodDefinition<_cexBroker_TransferRequest, _cexBroker_TransferResponse, _cexBroker_TransferRequest__Output, _cexBroker_TransferResponse__Output> +} diff --git a/proto/cexBroker/ConvertRequest.ts b/proto/cexBroker/ConvertRequest.ts new file mode 100644 index 0000000..832385f --- /dev/null +++ b/proto/cexBroker/ConvertRequest.ts @@ -0,0 +1,18 @@ +// Original file: proto/node.proto + + +export interface ConvertRequest { + 'fromToken'?: (string); + 'toToken'?: (string); + 'amount'?: (number | string); + 'price'?: (number | string); + 'cex'?: (string); +} + +export interface ConvertRequest__Output { + 'fromToken'?: (string); + 'toToken'?: (string); + 'amount'?: (number); + 'price'?: (number); + 'cex'?: (string); +} diff --git a/proto/cexBroker/ConvertResponse.ts b/proto/cexBroker/ConvertResponse.ts new file mode 100644 index 0000000..44b2b9e --- /dev/null +++ b/proto/cexBroker/ConvertResponse.ts @@ -0,0 +1,10 @@ +// Original file: proto/node.proto + + +export interface ConvertResponse { + 'orderId'?: (string); +} + +export interface ConvertResponse__Output { + 'orderId'?: (string); +} diff --git a/proto/cexBroker/DepositConfirmationRequest.ts b/proto/cexBroker/DepositConfirmationRequest.ts new file mode 100644 index 0000000..b5b3149 --- /dev/null +++ b/proto/cexBroker/DepositConfirmationRequest.ts @@ -0,0 +1,16 @@ +// Original file: proto/node.proto + + +export interface DepositConfirmationRequest { + 'chain'?: (string); + 'recipientAddress'?: (string); + 'amount'?: (number | string); + 'transactionHash'?: (string); +} + +export interface DepositConfirmationRequest__Output { + 'chain'?: (string); + 'recipientAddress'?: (string); + 'amount'?: (number); + 'transactionHash'?: (string); +} diff --git a/proto/cexBroker/DepositConfirmationResponse.ts b/proto/cexBroker/DepositConfirmationResponse.ts new file mode 100644 index 0000000..b690b56 --- /dev/null +++ b/proto/cexBroker/DepositConfirmationResponse.ts @@ -0,0 +1,10 @@ +// Original file: proto/node.proto + + +export interface DepositConfirmationResponse { + 'newBalance'?: (number | string); +} + +export interface DepositConfirmationResponse__Output { + 'newBalance'?: (number); +} diff --git a/proto/cexBroker/OptimalPriceRequest.ts b/proto/cexBroker/OptimalPriceRequest.ts new file mode 100644 index 0000000..6b7046d --- /dev/null +++ b/proto/cexBroker/OptimalPriceRequest.ts @@ -0,0 +1,15 @@ +// Original file: proto/node.proto + +import type { OrderMode as _cexBroker_OrderMode, OrderMode__Output as _cexBroker_OrderMode__Output } from '../cexBroker/OrderMode'; + +export interface OptimalPriceRequest { + 'symbol'?: (string); + 'quantity'?: (number | string); + 'mode'?: (_cexBroker_OrderMode); +} + +export interface OptimalPriceRequest__Output { + 'symbol'?: (string); + 'quantity'?: (number); + 'mode'?: (_cexBroker_OrderMode__Output); +} diff --git a/proto/cexBroker/OptimalPriceResponse.ts b/proto/cexBroker/OptimalPriceResponse.ts new file mode 100644 index 0000000..808c320 --- /dev/null +++ b/proto/cexBroker/OptimalPriceResponse.ts @@ -0,0 +1,11 @@ +// Original file: proto/node.proto + +import type { PriceInfo as _cexBroker_PriceInfo, PriceInfo__Output as _cexBroker_PriceInfo__Output } from '../cexBroker/PriceInfo'; + +export interface OptimalPriceResponse { + 'results'?: ({[key: string]: _cexBroker_PriceInfo}); +} + +export interface OptimalPriceResponse__Output { + 'results'?: ({[key: string]: _cexBroker_PriceInfo__Output}); +} diff --git a/proto/cexBroker/OrderDetailsRequest.ts b/proto/cexBroker/OrderDetailsRequest.ts new file mode 100644 index 0000000..ffd65d9 --- /dev/null +++ b/proto/cexBroker/OrderDetailsRequest.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface OrderDetailsRequest { + 'orderId'?: (string); + 'cex'?: (string); +} + +export interface OrderDetailsRequest__Output { + 'orderId'?: (string); + 'cex'?: (string); +} diff --git a/proto/cexBroker/OrderDetailsResponse.ts b/proto/cexBroker/OrderDetailsResponse.ts new file mode 100644 index 0000000..e80ad1a --- /dev/null +++ b/proto/cexBroker/OrderDetailsResponse.ts @@ -0,0 +1,22 @@ +// Original file: proto/node.proto + + +export interface OrderDetailsResponse { + 'orderId'?: (string); + 'status'?: (string); + 'originalAmount'?: (number | string); + 'filledAmount'?: (number | string); + 'symbol'?: (string); + 'mode'?: (string); + 'price'?: (number | string); +} + +export interface OrderDetailsResponse__Output { + 'orderId'?: (string); + 'status'?: (string); + 'originalAmount'?: (number); + 'filledAmount'?: (number); + 'symbol'?: (string); + 'mode'?: (string); + 'price'?: (number); +} diff --git a/proto/cexBroker/OrderMode.ts b/proto/cexBroker/OrderMode.ts new file mode 100644 index 0000000..721639e --- /dev/null +++ b/proto/cexBroker/OrderMode.ts @@ -0,0 +1,14 @@ +// Original file: proto/node.proto + +export const OrderMode = { + BUY: 0, + SELL: 1, +} as const; + +export type OrderMode = + | 'BUY' + | 0 + | 'SELL' + | 1 + +export type OrderMode__Output = typeof OrderMode[keyof typeof OrderMode] diff --git a/proto/cexBroker/PriceInfo.ts b/proto/cexBroker/PriceInfo.ts new file mode 100644 index 0000000..1215ad7 --- /dev/null +++ b/proto/cexBroker/PriceInfo.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface PriceInfo { + 'avgPrice'?: (number | string); + 'fillPrice'?: (number | string); +} + +export interface PriceInfo__Output { + 'avgPrice'?: (number); + 'fillPrice'?: (number); +} diff --git a/proto/cexBroker/TransferRequest.ts b/proto/cexBroker/TransferRequest.ts new file mode 100644 index 0000000..faf16c7 --- /dev/null +++ b/proto/cexBroker/TransferRequest.ts @@ -0,0 +1,18 @@ +// Original file: proto/node.proto + + +export interface TransferRequest { + 'chain'?: (string); + 'recipientAddress'?: (string); + 'amount'?: (number | string); + 'cex'?: (string); + 'token'?: (string); +} + +export interface TransferRequest__Output { + 'chain'?: (string); + 'recipientAddress'?: (string); + 'amount'?: (number); + 'cex'?: (string); + 'token'?: (string); +} diff --git a/proto/cexBroker/TransferResponse.ts b/proto/cexBroker/TransferResponse.ts new file mode 100644 index 0000000..5ab2b7c --- /dev/null +++ b/proto/cexBroker/TransferResponse.ts @@ -0,0 +1,12 @@ +// Original file: proto/node.proto + + +export interface TransferResponse { + 'success'?: (boolean); + 'transactionId'?: (string); +} + +export interface TransferResponse__Output { + 'success'?: (boolean); + 'transactionId'?: (string); +} diff --git a/proto/node.proto b/proto/node.proto index 698fbdb..c10d168 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -27,20 +27,7 @@ enum OrderMode { SELL = 1; } // Optimal price query -message OptimalPriceRequest { - string symbol = 1; // Trading pair symbol, e.g. "ARB/USDT" - double quantity = 2; // Quantity to buy or sell - OrderMode mode = 3; // Buy or Sell mode -} -// Single‐symbol price info -message PriceInfo { - double avgPrice = 1; // Volume‑weighted average price - double fillPrice = 2; // Worst‑case fill price -} -// The new response: a map of symbol→PriceInfo -message OptimalPriceResponse { - map results = 1; -} + // Order details query message OrderDetailsRequest { string order_id = 1; // Unique order identifier diff --git a/proto/node.ts b/proto/node.ts index 2eecc1d..d6ff3d6 100644 --- a/proto/node.ts +++ b/proto/node.ts @@ -18,12 +18,9 @@ export interface ProtoGrpcType { ConvertResponse: MessageTypeDefinition DepositConfirmationRequest: MessageTypeDefinition DepositConfirmationResponse: MessageTypeDefinition - OptimalPriceRequest: MessageTypeDefinition - OptimalPriceResponse: MessageTypeDefinition OrderDetailsRequest: MessageTypeDefinition OrderDetailsResponse: MessageTypeDefinition OrderMode: EnumTypeDefinition - PriceInfo: MessageTypeDefinition TransferRequest: MessageTypeDefinition TransferResponse: MessageTypeDefinition } From 99a80579a4dfd7175c076482b6a510697750fa18 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 00:57:05 +0100 Subject: [PATCH 06/45] Rename client.ts to client.dev.ts --- client.ts => client.dev.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client.ts => client.dev.ts (100%) diff --git a/client.ts b/client.dev.ts similarity index 100% rename from client.ts rename to client.dev.ts From e284b5149966bf66d8996fe987cb3cfa334e62d2 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 10:28:01 +0100 Subject: [PATCH 07/45] Add server shutdown in stop method --- index.ts | 7 +++++-- package.json | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 7429301..b4c315e 100644 --- a/index.ts +++ b/index.ts @@ -156,13 +156,16 @@ export default class CEXBroker { } /** - * Stops watching the policy file, if applicable. + * Stops Server and Stop watching the policy file, if applicable. */ - public stopWatchingPolicies(): void { + public stop(): void { if (this.#policyFilePath) { unwatchFile(this.#policyFilePath); console.log(`Stopped watching policy file: ${this.#policyFilePath}`); } + if (this.server){ + this.server?.forceShutdown() + } } /** diff --git a/package.json b/package.json index 61b5a4a..718b7f6 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "type": "module", "private": true, "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", "devDependencies": { "@biomejs/biome": "2.0.6", "@types/bun": "latest", @@ -15,6 +17,9 @@ "bun-types": "latest", "husky": "^9.1.7" }, + "files": [ + "dist" + ], "scripts": { "proto-gen": "./proto-gen.sh", "start": "bun run index.ts", From 7364e19a880470b719b31bcdd3d971ca8954dcc0 Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 21:17:46 +0100 Subject: [PATCH 08/45] Add CLI and enhance broker configuration handling --- build.ts | 10 +++ bun.lock | 15 ++--- cli.ts | 25 ++++++++ client.dev.ts | 11 +--- commands/start-broker.ts | 10 +++ config/broker.ts | 1 - config/index.ts | 57 ----------------- helpers/index.ts | 57 ++++++++++++++++- index.ts | 75 ++++++++++++++++------- package.json | 19 ++++-- patches/@protobufjs%2Finquire@1.1.0.patch | 16 +++++ policy/policy.json | 3 +- tsconfig.json | 2 +- types.ts | 11 ++++ 14 files changed, 205 insertions(+), 107 deletions(-) create mode 100755 cli.ts create mode 100644 commands/start-broker.ts delete mode 100644 config/index.ts create mode 100644 patches/@protobufjs%2Finquire@1.1.0.patch diff --git a/build.ts b/build.ts index efa0d93..6d98127 100644 --- a/build.ts +++ b/build.ts @@ -9,4 +9,14 @@ await Bun.build({ ], }) +await Bun.build({ + entrypoints: ["./cli.ts"], + outdir: './dist/commands', + target:"node", + plugins: [ + dts() + ], +}) + + // Generates `dist/index.d.ts` and `dist/other/foo.d.ts` \ No newline at end of file diff --git a/bun.lock b/bun.lock index 1828bc9..7fe8ccf 100644 --- a/bun.lock +++ b/bun.lock @@ -7,13 +7,13 @@ "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", "ccxt": "^4.4.91", + "commander": "^14.0.0", "dotenv": "^17.0.0", "joi": "^17.13.3", }, "devDependencies": { "@biomejs/biome": "2.0.6", "@types/bun": "latest", - "bun-plugin-dts": "^0.3.0", "bun-types": "latest", "husky": "^9.1.7", }, @@ -22,6 +22,9 @@ }, }, }, + "patchedDependencies": { + "@protobufjs/inquire@1.1.0": "patches/@protobufjs%2Finquire@1.1.0.patch", + }, "packages": { "@biomejs/biome": ["@biomejs/biome@2.0.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.6", "@biomejs/cli-darwin-x64": "2.0.6", "@biomejs/cli-linux-arm64": "2.0.6", "@biomejs/cli-linux-arm64-musl": "2.0.6", "@biomejs/cli-linux-x64": "2.0.6", "@biomejs/cli-linux-x64-musl": "2.0.6", "@biomejs/cli-win32-arm64": "2.0.6", "@biomejs/cli-win32-x64": "2.0.6" }, "bin": { "biome": "bin/biome" } }, "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA=="], @@ -87,8 +90,6 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "bun-plugin-dts": ["bun-plugin-dts@0.3.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.8.1" } }, "sha512-QpiAOKfPcdOToxySOqRY8FwL+brTvyXEHWzrSCRKt4Pv7Z4pnUrhK9tFtM7Ndm7ED09B/0cGXnHJKqmekr/ERw=="], - "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], "ccxt": ["ccxt@4.4.91", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-IP7wZc1KfAojMfKyMeGwC/aJoXvAUFFUMtu3x9Wp5BAOISftcrcl/yt/gjxaPddrMT2/BcU3wHZzvqzr1FBFBw=="], @@ -99,22 +100,18 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "dotenv": ["dotenv@17.0.0", "", {}, "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ=="], - "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], - "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -129,8 +126,6 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/cli.ts b/cli.ts new file mode 100755 index 0000000..51274bc --- /dev/null +++ b/cli.ts @@ -0,0 +1,25 @@ +#!/usr/bin/env bun + +import { Command } from 'commander'; +import { startBrokerCommand } from './commands/start-broker'; + +const program = new Command(); + +program + .name('cex-broker') + .description('CLI to start the CEXBroker service') + .requiredOption('-p, --policy ', 'Policy JSON file') + .option('--port ', 'Port number (default: 8086)', '8086') + .action(async (options) => { + try { + await startBrokerCommand( + options.policy, + parseInt(options.port, 10) + ); + } catch (err) { + console.error('❌ Failed to start broker:', err); + process.exit(1); + } + }); + +program.parse(process.argv); diff --git a/client.dev.ts b/client.dev.ts index 61491ec..e7b4468 100644 --- a/client.dev.ts +++ b/client.dev.ts @@ -2,9 +2,9 @@ import path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "./proto/node"; -import config from "./config"; const PROTO_FILE = "./proto/node.proto"; +const port= 8086 const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); const grpcObj = grpc.loadPackageDefinition( @@ -12,7 +12,7 @@ const grpcObj = grpc.loadPackageDefinition( ) as unknown as ProtoGrpcType; const client = new grpcObj.cexBroker.CexService( - `0.0.0.0:${config.port}`, + `0.0.0.0:${port}`, grpc.credentials.createInsecure(), ); @@ -35,11 +35,4 @@ function onClientReady() { console.log({ x: result }); }); -// client.Transfer({cex:"binance",amount:1,token:"USDT",chain:"BEP20",recipientAddress:"0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"},(err,result)=>{ -// if (err) { -// console.error({ err }); -// return; -// } -// console.log({ x: result }); -// }) } diff --git a/commands/start-broker.ts b/commands/start-broker.ts new file mode 100644 index 0000000..e94458c --- /dev/null +++ b/commands/start-broker.ts @@ -0,0 +1,10 @@ +import CEXBroker from '../index'; + +/** + * CLI Command wrapper to start the CEXBroker + */ +export async function startBrokerCommand(policyPath: string, port: number) { + const broker = new CEXBroker({}, policyPath, { port }); + broker.loadEnvConfig(); + await broker.run(); +} diff --git a/config/broker.ts b/config/broker.ts index 33db2ed..74c20c5 100644 --- a/config/broker.ts +++ b/config/broker.ts @@ -104,7 +104,6 @@ import type { alpaca, } from "ccxt" import { SupportedBroker } from "../types"; import type { ISupportedBroker } from "../types"; -import config from "./index"; // Map each broker key to its specific CCXT class type BrokerInstanceMap = { diff --git a/config/index.ts b/config/index.ts deleted file mode 100644 index b7c2ab0..0000000 --- a/config/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import dotenv from "dotenv"; -import Joi from "joi"; -import { BrokerList, SupportedBroker } from "../types"; - -dotenv.config(); - -const baseConfig = { - port: process.env.PORT_NUM, - // TODO: We can make these environment variables more dynamic... eg. CEX_API_KEY_[EXCHANGE_NAMESPACE]_[NUMBER] - in case many exchanges for same exchange. - bybitApiKey: process.env.BYBIT_API_KEY, - bybitApiSecret: process.env.BYBIT_API_SECRET, - binanceApiKey: process.env.BINANCE_API_KEY, - binanceApiSecret: process.env.BINANCE_API_SECRET, - // TODO: Terrible naming convention? What is Rooch doing here... - // TODO: This "brokers" should be called "exchanges".... right? - brokers: (process.env.ROOCH_CHAIN_ID - ? process.env.ROOCH_CHAIN_ID.split(",") - : BrokerList) as string[], -}; - -const isRequiredWhenBrokerInclude = ( - schema: Joi.StringSchema, - value: string, -) => - Joi.string().when("brokers", { - is: Joi.array().items(Joi.string().valid(value)).has(value), - // biome-ignore lint/suspicious/noThenProperty: Dynamic check - then: schema.required(), // 'details' is required if 'status' is 'active' - otherwise: Joi.string().optional().allow("", null).default(""), // 'details' is optional otherwise - }); - -const envVarsSchema = Joi.object({ - port: Joi.number().default(8082), - bybitApiKey: isRequiredWhenBrokerInclude(Joi.string(), SupportedBroker.bybit), - bybitApiSecret: isRequiredWhenBrokerInclude( - Joi.string(), - SupportedBroker.bybit, - ), - binanceApiKey: isRequiredWhenBrokerInclude( - Joi.string(), - SupportedBroker.binance, - ), - binanceApiSecret: isRequiredWhenBrokerInclude( - Joi.string(), - SupportedBroker.binance, - ), -}).unknown(); - -const { value: envVars, error } = envVarsSchema.validate({ - ...baseConfig, -}); - -if (error) { - throw new Error(`Config validation error: ${error.message}`); -} - -export default envVars as typeof baseConfig; diff --git a/helpers/index.ts b/helpers/index.ts index 984e946..b7592eb 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -1,6 +1,7 @@ import type { Exchange } from "ccxt"; import type { PolicyConfig } from "../types"; import fs from "fs"; +import Joi from "joi"; /** * Fetches the order book, computes the worst‐case fill price for `size`, @@ -124,7 +125,61 @@ export async function sellAtOptimalPrice( export function loadPolicy(policyPath: string): PolicyConfig { try { const policyData = fs.readFileSync(policyPath, "utf8"); - return JSON.parse(policyData) as PolicyConfig; + + // Joi schema for WithdrawRule + const withdrawRuleSchema = Joi.object({ + networks: Joi.array().items(Joi.string()).required(), + whitelist: Joi.array().items(Joi.string()).required(), + amounts: Joi.array() + .items( + Joi.object({ + ticker: Joi.string().required(), + max: Joi.number().required(), + min: Joi.number().required(), + }) + ) + .required(), + }); + + // Joi schema for OrderRule + const orderRuleSchema = Joi.object({ + markets: Joi.array().items(Joi.string()).required(), + limits: Joi.array() + .items( + Joi.object({ + from: Joi.string().required(), + to: Joi.string().required(), + min: Joi.number().required(), + max: Joi.number().required(), + }) + ) + .required(), + }); + + // Full PolicyConfig schema + const policyConfigSchema = Joi.object({ + withdraw: Joi.object({ + rule: withdrawRuleSchema.required(), + }).required(), + + deposit: Joi.object() + .pattern(Joi.string(), Joi.valid(null)) // Record + .required(), + + order: Joi.object({ + rule: orderRuleSchema.required(), + }).required(), + }); + + const { error, value } = policyConfigSchema.validate(JSON.parse(policyData)); + + if (error) { + console.error('Validation failed:', error.details); + } + + return value as PolicyConfig; + + } catch (error) { console.error("Failed to load policy:", error); throw new Error("Policy configuration could not be loaded"); diff --git a/index.ts b/index.ts index b4c315e..ddfbdbd 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,6 @@ import path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "./proto/node"; -import config from "./config"; import { loadPolicy, validateOrder, @@ -13,7 +12,7 @@ import { } from "./helpers"; import type { BalanceRequest } from "./proto/cexBroker/BalanceRequest"; import type { BalanceResponse } from "./proto/cexBroker/BalanceResponse"; -import type { PolicyConfig } from "./types"; +import { BrokerList, type BrokerCredentials, type ExchangeCredentials, type PolicyConfig } from "./types"; import type { TransferRequest } from "./proto/cexBroker/TransferRequest"; import type { TransferResponse } from "./proto/cexBroker/TransferResponse"; import type { DepositConfirmationRequest } from "./proto/cexBroker/DepositConfirmationRequest"; @@ -25,6 +24,7 @@ import type { OrderDetailsResponse } from "./proto/cexBroker/OrderDetailsRespons import type { CancelOrderRequest } from "./proto/cexBroker/CancelOrderRequest"; import type { CancelOrderResponse } from "./proto/cexBroker/CancelOrderResponse"; import { watchFile, unwatchFile } from "fs"; +import Joi from "joi"; const PROTO_FILE = "./proto/node.proto"; @@ -36,14 +36,11 @@ const fietCexNode = grpcObj.cexBroker; console.log("CCXT Version:", ccxt.version); -type BrokerCredentials = { - apiKey: string; - apiSecret: string; -}; export default class CEXBroker { - private brokerConfig: Record = {}; + #brokerConfig: Record = {}; #policyFilePath?: string; + port = 8086; private policy: PolicyConfig; private brokers: Record = {}; private server: grpc.Server | null = null; @@ -54,7 +51,7 @@ export default class CEXBroker { * CEX_BROKER__API_KEY * CEX_BROKER__API_SECRET */ - private loadEnvConfig(): void { + public loadEnvConfig(): void { console.log("🔧 Loading CEX_BROKER_ environment variables:"); const configMap: Record> = {}; @@ -91,12 +88,12 @@ export default class CEXBroker { const hasSecret = !!creds.apiSecret; if (hasKey && hasSecret) { - this.brokerConfig[broker] = { + this.#brokerConfig[broker] = { apiKey: creds.apiKey ?? "", apiSecret: creds.apiSecret ?? "", }; console.log( - `✅ Loaded credentials for broker "${broker.toUpperCase()}"`, + `✅ Loaded credentials for broker "${broker}"`, ); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ @@ -111,16 +108,52 @@ export default class CEXBroker { if (!hasKey) missing.push("API_KEY"); if (!hasSecret) missing.push("API_SECRET"); console.warn( - `❌ Missing ${missing.join(" and ")} for broker "${broker.toUpperCase()}"`, + `❌ Missing ${missing.join(" and ")} for broker "${broker}"`, ); } } } - constructor(policies: string | PolicyConfig) { + /** + * Validates an exc hange credential object structure. + */ + public loadExchangeCredentials(creds: unknown): asserts creds is ExchangeCredentials { + const schema = Joi.object>() + .pattern( + Joi.string().allow(...BrokerList).required(), + Joi.object({ + apiKey: Joi.string().required(), + apiSecret: Joi.string().required(), + }) + ) + .required(); + + const { value, error } = schema.validate(creds); + if (error) { + throw new Error(`Invalid credentials format: ${error.message}`); + } + + // Finalize config and print result per broker + for (const [broker, creds] of Object.entries(value)) { + console.log( + `✅ Loaded credentials for broker "${broker}"`, + ); + const ExchangeClass = (ccxt as any)[broker]; + const client = new ExchangeClass({ + apiKey: creds.apiKey, + secret: creds.apiSecret, + enableRateLimit: true, + defaultType: "spot", + }); + this.brokers[broker] = client; + } + } + + constructor(apiCredentials: ExchangeCredentials, policies: string | PolicyConfig,config?:{port:number}) { if (typeof policies === "string") { this.#policyFilePath = policies; this.policy = loadPolicy(policies); + this.port= config?.port??8086 } else { this.policy = policies; } @@ -130,7 +163,7 @@ export default class CEXBroker { this.watchPolicyFile(this.#policyFilePath); } - this.loadEnvConfig(); + this.loadExchangeCredentials(apiCredentials); } /** @@ -163,15 +196,15 @@ export default class CEXBroker { unwatchFile(this.#policyFilePath); console.log(`Stopped watching policy file: ${this.#policyFilePath}`); } - if (this.server){ - this.server?.forceShutdown() + if (this.server) { + this.server.forceShutdown(); } } /** * Starts the broker, applying policies then running appropriate tasks. */ - public async run(): Promise { + public async run(): Promise { if (this.server) { await this.server.forceShutdown(); } @@ -179,7 +212,7 @@ export default class CEXBroker { this.server = getServer(this.policy, this.brokers); this.server.bindAsync( - `0.0.0.0:${config.port}`, + `0.0.0.0:${this.port}`, grpc.ServerCredentials.createInsecure(), (err, port) => { if (err) { @@ -189,7 +222,7 @@ export default class CEXBroker { console.log(`Your server as started on port ${port}`); }, ); - return this.server; + return this; } } @@ -260,7 +293,7 @@ function getServer(policy: PolicyConfig, brokers: Record) { try { console.log( `[${new Date().toISOString()}] ` + - `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, + `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, ); callback(null, { newBalance: 0 }); } catch (error) { @@ -670,5 +703,5 @@ function getServer(policy: PolicyConfig, brokers: Record) { return server; } // const policyPath = "./policy/policy.json" -// const broker = new CEXBroker(policyPath); -// broker.run().then(e=>console.log({e})) +// const broker = new CEXBroker({},policyPath,{port:8096}); +// broker.run().then(e => console.log({ e })) diff --git a/package.json b/package.json index 718b7f6..8df71da 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,38 @@ { "name": "cex-broker", + "version": "0.0.1", "description": "Unified gRPC API to CEXs by Usher Labs", "repository": "git@gitlab.com:usherlabs/cex-broker.git", "homepage": "https://usher.so/", "author": "Oki Ayobami ", "module": "index.ts", "type": "module", - "private": true, "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "devDependencies": { "@biomejs/biome": "2.0.6", "@types/bun": "latest", - "bun-plugin-dts": "^0.3.0", + "bun-plugin-dts":"latest", "bun-types": "latest", "husky": "^9.1.7" }, "files": [ "dist" - ], + ], + "bin": { + "cex-broker": "./cli.ts" + }, "scripts": { "proto-gen": "./proto-gen.sh", "start": "bun run index.ts", - "build": "bun build ./index.ts --outdir ./build --target bun", + "build": "bun build ./index.ts --outdir ./build --target node", "build:ts": "bun run ./build.ts", "test": "bun test", "format": "bunx biome format --write", "lint": "bunx biome lint --write", "check": "bunx biome check --write", - "prepare": "husky" + "prepare": "bunx husky" }, "peerDependencies": { "typescript": "^5" @@ -38,6 +41,7 @@ "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", "ccxt": "^4.4.91", + "commander": "^14.0.0", "dotenv": "^17.0.0", "joi": "^17.13.3" }, @@ -46,5 +50,8 @@ }, "contributors": [ "Oki Ayobami (https://github.com/xlassix)" - ] + ], + "patchedDependencies": { + "@protobufjs/inquire@1.1.0": "patches/@protobufjs%2Finquire@1.1.0.patch" + } } diff --git a/patches/@protobufjs%2Finquire@1.1.0.patch b/patches/@protobufjs%2Finquire@1.1.0.patch new file mode 100644 index 0000000..337d86e --- /dev/null +++ b/patches/@protobufjs%2Finquire@1.1.0.patch @@ -0,0 +1,16 @@ +diff --git a/index.js b/index.js +index 33778b5539b7fcd7a1e99474a4ecb1745fdfe508..4cc0a71670abd106fb29bc7f3d185c438039d91e 100644 +--- a/index.js ++++ b/index.js +@@ -9,7 +9,10 @@ module.exports = inquire; + */ + function inquire(moduleName) { + try { +- var mod = eval("quire".replace(/^/,"re"))(moduleName); // eslint-disable-line no-eval ++ // 注释掉下面的代码 ++ // var mod = eval("quire".replace(/^/,"re"))(moduleName); // eslint-disable-line no-eval ++ // 新的代码 ++ var mod = require(moduleName); + if (mod && (mod.length || Object.keys(mod).length)) + return mod; + } catch (e) {} // eslint-disable-line no-empty diff --git a/policy/policy.json b/policy/policy.json index 230aec2..f66efa6 100644 --- a/policy/policy.json +++ b/policy/policy.json @@ -25,7 +25,8 @@ "BYBIT:ARB/USDC", "UPBIT:ETH/USDC", "BINANCE:ETH/USDT", - "BINANCE:BTC/ETH" + "BINANCE:BTC/ETH", + "BINANCE:BTC/USDC" ], "limits": [ { "from": "USDT", "to": "ETH", "min": 1, "max": 100000 }, diff --git a/tsconfig.json b/tsconfig.json index 3e4ca63..bc093e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, - "types": ["@types/bun"], + "types": ["@types/bun", "bun-types" ], // Bundler mode "moduleResolution": "bundler", diff --git a/types.ts b/types.ts index 70eb699..11e1f89 100644 --- a/types.ts +++ b/types.ts @@ -159,6 +159,7 @@ export const BrokerList = [ ] as const; export type ISupportedBroker = (typeof BrokerList)[number]; +export type SupportedBrokers = typeof BrokerList[number] export const SupportedBroker = BrokerList.reduce( (acc, value) => { @@ -167,3 +168,13 @@ export const SupportedBroker = BrokerList.reduce( }, {} as Record<(typeof BrokerList)[number], string>, ); + + +export type BrokerCredentials = { + apiKey: string; + apiSecret: string; +}; + +export interface ExchangeCredentials { + [exchange: string]: BrokerCredentials +} From ead6b954ebed678ed57aa7ca109dce8a14ddf1ca Mon Sep 17 00:00:00 2001 From: xlassix Date: Thu, 10 Jul 2025 21:24:22 +0100 Subject: [PATCH 09/45] Refactor import and method formatting --- index.ts | 40 ++++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/index.ts b/index.ts index ddfbdbd..bb00fae 100644 --- a/index.ts +++ b/index.ts @@ -12,7 +12,12 @@ import { } from "./helpers"; import type { BalanceRequest } from "./proto/cexBroker/BalanceRequest"; import type { BalanceResponse } from "./proto/cexBroker/BalanceResponse"; -import { BrokerList, type BrokerCredentials, type ExchangeCredentials, type PolicyConfig } from "./types"; +import { + BrokerList, + type BrokerCredentials, + type ExchangeCredentials, + type PolicyConfig, +} from "./types"; import type { TransferRequest } from "./proto/cexBroker/TransferRequest"; import type { TransferResponse } from "./proto/cexBroker/TransferResponse"; import type { DepositConfirmationRequest } from "./proto/cexBroker/DepositConfirmationRequest"; @@ -36,7 +41,6 @@ const fietCexNode = grpcObj.cexBroker; console.log("CCXT Version:", ccxt.version); - export default class CEXBroker { #brokerConfig: Record = {}; #policyFilePath?: string; @@ -92,9 +96,7 @@ export default class CEXBroker { apiKey: creds.apiKey ?? "", apiSecret: creds.apiSecret ?? "", }; - console.log( - `✅ Loaded credentials for broker "${broker}"`, - ); + console.log(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -115,16 +117,20 @@ export default class CEXBroker { } /** - * Validates an exc hange credential object structure. - */ - public loadExchangeCredentials(creds: unknown): asserts creds is ExchangeCredentials { + * Validates an exc hange credential object structure. + */ + public loadExchangeCredentials( + creds: unknown, + ): asserts creds is ExchangeCredentials { const schema = Joi.object>() .pattern( - Joi.string().allow(...BrokerList).required(), + Joi.string() + .allow(...BrokerList) + .required(), Joi.object({ apiKey: Joi.string().required(), apiSecret: Joi.string().required(), - }) + }), ) .required(); @@ -135,9 +141,7 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - console.log( - `✅ Loaded credentials for broker "${broker}"`, - ); + console.log(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -149,11 +153,15 @@ export default class CEXBroker { } } - constructor(apiCredentials: ExchangeCredentials, policies: string | PolicyConfig,config?:{port:number}) { + constructor( + apiCredentials: ExchangeCredentials, + policies: string | PolicyConfig, + config?: { port: number }, + ) { if (typeof policies === "string") { this.#policyFilePath = policies; this.policy = loadPolicy(policies); - this.port= config?.port??8086 + this.port = config?.port ?? 8086; } else { this.policy = policies; } @@ -293,7 +301,7 @@ function getServer(policy: PolicyConfig, brokers: Record) { try { console.log( `[${new Date().toISOString()}] ` + - `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, + `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, ); callback(null, { newBalance: 0 }); } catch (error) { diff --git a/package.json b/package.json index 8df71da..e3309dc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dist" ], "bin": { - "cex-broker": "./cli.ts" + "cex-broker": "dist/commands/cli.js" }, "scripts": { "proto-gen": "./proto-gen.sh", From 560340475ac927dc339857111d00584153f682bd Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 07:53:58 +0100 Subject: [PATCH 10/45] Remove broker.ts and update types.ts mapping --- bun.lock | 12 ++- config/broker.ts | 227 ----------------------------------------------- index.ts | 5 +- package.json | 3 +- types.ts | 16 +++- 5 files changed, 27 insertions(+), 236 deletions(-) delete mode 100644 config/broker.ts diff --git a/bun.lock b/bun.lock index 7fe8ccf..79dc80b 100644 --- a/bun.lock +++ b/bun.lock @@ -8,12 +8,12 @@ "@grpc/proto-loader": "^0.7.15", "ccxt": "^4.4.91", "commander": "^14.0.0", - "dotenv": "^17.0.0", "joi": "^17.13.3", }, "devDependencies": { "@biomejs/biome": "2.0.6", "@types/bun": "latest", + "bun-plugin-dts": "latest", "bun-types": "latest", "husky": "^9.1.7", }, @@ -90,6 +90,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "bun-plugin-dts": ["bun-plugin-dts@0.3.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.8.1" } }, "sha512-QpiAOKfPcdOToxySOqRY8FwL+brTvyXEHWzrSCRKt4Pv7Z4pnUrhK9tFtM7Ndm7ED09B/0cGXnHJKqmekr/ERw=="], + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], "ccxt": ["ccxt@4.4.91", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-IP7wZc1KfAojMfKyMeGwC/aJoXvAUFFUMtu3x9Wp5BAOISftcrcl/yt/gjxaPddrMT2/BcU3wHZzvqzr1FBFBw=="], @@ -102,9 +104,11 @@ "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "dotenv": ["dotenv@17.0.0", "", {}, "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ=="], + "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -112,6 +116,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -126,6 +132,8 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/config/broker.ts b/config/broker.ts deleted file mode 100644 index 74c20c5..0000000 --- a/config/broker.ts +++ /dev/null @@ -1,227 +0,0 @@ -import ccxt from "ccxt"; -import type { alpaca, - apex, - ascendex, - bequant, - bigone, - binance, - binancecoinm, - binanceus, - binanceusdm, - bingx, - bit2c, - bitbank, - bitbns, - bitfinex, - bitflyer, - bitget, - bithumb, - bitmart, - bitmex, bitopro, - bitrue, bitso,bitstamp, - bitteam, - bittrade, - bitvavo, - blockchaincom, - blofin, - btcalpha, - btcbox, - btcmarkets, - btcturk, - bybit, - cex, - coinbase, - coinbaseadvanced, - coinbaseexchange, - coinbaseinternational, - coincatch, - coincheck, - coinex, - coinmate, - coinmetro, - coinone, - coinsph, - coinspot, - cryptocom, - cryptomus, - defx, - delta, - deribit, - derive, - digifinex, - ellipx, - exmo, - fmfwio, - gate, - gateio, - gemini, - hashkey, - hitbtc, - hollaex, - htx, - huobi, - hyperliquid, - independentreserve, - indodax, - kraken, - krakenfutures, - kucoin, - kucoinfutures, - latoken, - lbank, - luno, - mercado, - mexc, - modetrade, - myokx, - ndax, - novadax, - oceanex, - okcoin, - okx, - okxus, - onetrading, - oxfun, - p2b, - paradex, - paymium, - phemex, - poloniex, - probit, - timex, - tokocrypto, - tradeogre, - upbit, - vertex, - wavesexchange, - whitebit, - woo, - woofipro, - xt, - yobit, - zaif, - zonda, -} from "ccxt" -import { SupportedBroker } from "../types"; -import type { ISupportedBroker } from "../types"; - -// Map each broker key to its specific CCXT class -type BrokerInstanceMap = { - [SupportedBroker.alpaca]: alpaca; - [SupportedBroker.apex]: apex; - [SupportedBroker.ascendex]: ascendex; - [SupportedBroker.bequant]: bequant; - [SupportedBroker.bigone]: bigone; - [SupportedBroker.binance]: binance; - [SupportedBroker.binancecoinm]: binancecoinm; - [SupportedBroker.binanceus]: binanceus; - [SupportedBroker.binanceusdm]: binanceusdm; - [SupportedBroker.bingx]: bingx; - [SupportedBroker.bit2c]: bit2c; - [SupportedBroker.bitbank]: bitbank; - [SupportedBroker.bitbns]: bitbns; - [SupportedBroker.bitfinex]: bitfinex; - [SupportedBroker.bitflyer]: bitflyer; - [SupportedBroker.bitget]: bitget; - [SupportedBroker.bithumb]: bithumb; - [SupportedBroker.bitmart]: bitmart; - [SupportedBroker.bitmex]: bitmex; - [SupportedBroker.bitopro]: bitopro; - [SupportedBroker.bitrue]: bitrue; - [SupportedBroker.bitso]: bitso; - [SupportedBroker.bitstamp]: bitstamp; - [SupportedBroker.bitteam]: bitteam; - [SupportedBroker.bittrade]: bittrade; - [SupportedBroker.bitvavo]: bitvavo; - [SupportedBroker.blockchaincom]: blockchaincom; - [SupportedBroker.blofin]: blofin; - [SupportedBroker.btcalpha]: btcalpha; - [SupportedBroker.btcbox]: btcbox; - [SupportedBroker.btcmarkets]: btcmarkets; - [SupportedBroker.btcturk]: btcturk; - [SupportedBroker.bybit]: bybit; - [SupportedBroker.cex]: cex; - [SupportedBroker.coinbase]: coinbase; - [SupportedBroker.coinbaseadvanced]: coinbaseadvanced; - [SupportedBroker.coinbaseexchange]: coinbaseexchange; - [SupportedBroker.coinbaseinternational]: coinbaseinternational; - [SupportedBroker.coincatch]: coincatch; - [SupportedBroker.coincheck]: coincheck; - [SupportedBroker.coinex]: coinex; - [SupportedBroker.coinmate]: coinmate; - [SupportedBroker.coinmetro]: coinmetro; - [SupportedBroker.coinone]: coinone; - [SupportedBroker.coinsph]: coinsph; - [SupportedBroker.coinspot]: coinspot; - [SupportedBroker.cryptocom]: cryptocom; - [SupportedBroker.cryptomus]: cryptomus; - [SupportedBroker.defx]: defx; - [SupportedBroker.delta]: delta; - [SupportedBroker.deribit]: deribit; - [SupportedBroker.derive]: derive; - [SupportedBroker.digifinex]: digifinex; - [SupportedBroker.ellipx]: ellipx; - [SupportedBroker.exmo]: exmo; - [SupportedBroker.fmfwio]: fmfwio; - [SupportedBroker.gate]: gate; - [SupportedBroker.gateio]: gateio; - [SupportedBroker.gemini]: gemini; - [SupportedBroker.hashkey]: hashkey; - [SupportedBroker.hitbtc]: hitbtc; - [SupportedBroker.hollaex]: hollaex; - [SupportedBroker.htx]: htx; - [SupportedBroker.huobi]: huobi; - [SupportedBroker.hyperliquid]: hyperliquid; - [SupportedBroker.independentreserve]: independentreserve; - [SupportedBroker.indodax]: indodax; - [SupportedBroker.kraken]: kraken; - [SupportedBroker.krakenfutures]: krakenfutures; - [SupportedBroker.kucoin]: kucoin; - [SupportedBroker.kucoinfutures]: kucoinfutures; - [SupportedBroker.latoken]: latoken; - [SupportedBroker.lbank]: lbank; - [SupportedBroker.luno]: luno; - [SupportedBroker.mercado]: mercado; - [SupportedBroker.mexc]: mexc; - [SupportedBroker.modetrade]: modetrade; - [SupportedBroker.myokx]: myokx; - [SupportedBroker.ndax]: ndax; - [SupportedBroker.novadax]: novadax; - [SupportedBroker.oceanex]: oceanex; - [SupportedBroker.okcoin]: okcoin; - [SupportedBroker.okx]: okx; - [SupportedBroker.okxus]: okxus; - [SupportedBroker.onetrading]: onetrading; - [SupportedBroker.oxfun]: oxfun; - [SupportedBroker.p2b]: p2b; - [SupportedBroker.paradex]: paradex; - [SupportedBroker.paymium]: paymium; - [SupportedBroker.phemex]: phemex; - [SupportedBroker.poloniex]: poloniex; - [SupportedBroker.probit]: probit; - [SupportedBroker.timex]: timex; - [SupportedBroker.tokocrypto]: tokocrypto; - [SupportedBroker.tradeogre]: tradeogre; - [SupportedBroker.upbit]: upbit; - [SupportedBroker.vertex]: vertex; - [SupportedBroker.wavesexchange]: wavesexchange; - [SupportedBroker.whitebit]: whitebit; - [SupportedBroker.woo]: woo; - [SupportedBroker.woofipro]: woofipro; - [SupportedBroker.xt]: xt; - [SupportedBroker.yobit]: yobit; - [SupportedBroker.zaif]: zaif; - [SupportedBroker.zonda]: zonda; - }; - -// Dynamic BrokerMap: each key maps to the correct broker type -export type BrokerMap = Partial<{ - [K in ISupportedBroker]: BrokerInstanceMap[K]; -}>; - - -// Initialize brokers map -const brokers: BrokerMap = {}; - - -export default brokers as Required; diff --git a/index.ts b/index.ts index bb00fae..9ae782f 100644 --- a/index.ts +++ b/index.ts @@ -710,6 +710,5 @@ function getServer(policy: PolicyConfig, brokers: Record) { }); return server; } -// const policyPath = "./policy/policy.json" -// const broker = new CEXBroker({},policyPath,{port:8096}); -// broker.run().then(e => console.log({ e })) + +console.log({x:Object.keys(ccxt)}) \ No newline at end of file diff --git a/package.json b/package.json index e3309dc..dd5844c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@biomejs/biome": "2.0.6", "@types/bun": "latest", - "bun-plugin-dts":"latest", + "bun-plugin-dts": "latest", "bun-types": "latest", "husky": "^9.1.7" }, @@ -42,7 +42,6 @@ "@grpc/proto-loader": "^0.7.15", "ccxt": "^4.4.91", "commander": "^14.0.0", - "dotenv": "^17.0.0", "joi": "^17.13.3" }, "publishConfig": { diff --git a/types.ts b/types.ts index 11e1f89..f4f409c 100644 --- a/types.ts +++ b/types.ts @@ -1,3 +1,5 @@ +import ccxt from "ccxt"; + // Policy types based on the policy.json structure export type WithdrawRule = { networks: string[]; @@ -47,9 +49,17 @@ export type Policy = { }>; }; -export type Policies = { - [apiKey: string]: Policy; // key is an Ethereum-style address like '0x...' +// Dynamic type mapping using CCXT's exchange classes +type BrokerInstanceMap = { + [K in ISupportedBroker]: InstanceType; }; + + +// Dynamic BrokerMap: each key maps to the correct broker type +export type BrokerMap = Partial<{ + [K in ISupportedBroker]: BrokerInstanceMap[K]; +}>; + export const BrokerList = [ "alpaca", "apex", @@ -158,6 +168,8 @@ export const BrokerList = [ "zonda" ] as const; +export type brokers = Required; + export type ISupportedBroker = (typeof BrokerList)[number]; export type SupportedBrokers = typeof BrokerList[number] From b897245641fcd1b4bf28321b05a530fac70702f1 Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 07:55:33 +0100 Subject: [PATCH 11/45] Add postinstall script to package.json --- index.ts | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 9ae782f..436183f 100644 --- a/index.ts +++ b/index.ts @@ -711,4 +711,4 @@ function getServer(policy: PolicyConfig, brokers: Record) { return server; } -console.log({x:Object.keys(ccxt)}) \ No newline at end of file +console.log({ x: Object.keys(ccxt) }); diff --git a/package.json b/package.json index dd5844c..4e0d8ba 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,13 @@ "scripts": { "proto-gen": "./proto-gen.sh", "start": "bun run index.ts", - "build": "bun build ./index.ts --outdir ./build --target node", "build:ts": "bun run ./build.ts", "test": "bun test", "format": "bunx biome format --write", "lint": "bunx biome lint --write", "check": "bunx biome check --write", - "prepare": "bunx husky" + "prepare": "bunx husky", + "postinstall": "./proto-gen.sh" }, "peerDependencies": { "typescript": "^5" From f53f9ffc20a9f9c87ad814d5361df68ed5d844ec Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 09:35:55 +0100 Subject: [PATCH 12/45] Refactor tests and remove console.log debug line --- index.ts | 2 -- index.test.ts => test/index.test.ts | 2 +- .../integration.test.ts | 20 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) rename index.test.ts => test/index.test.ts (98%) rename integration.test.ts => test/integration.test.ts (87%) diff --git a/index.ts b/index.ts index 436183f..55a28d7 100644 --- a/index.ts +++ b/index.ts @@ -710,5 +710,3 @@ function getServer(policy: PolicyConfig, brokers: Record) { }); return server; } - -console.log({ x: Object.keys(ccxt) }); diff --git a/index.test.ts b/test/index.test.ts similarity index 98% rename from index.test.ts rename to test/index.test.ts index b2b1ca0..35e0e07 100644 --- a/index.test.ts +++ b/test/index.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { isIpAllowed } from "./helpers"; +import { isIpAllowed } from "../helpers"; describe("RPC Server Logic Tests", () => { describe("IP Authentication", () => { diff --git a/integration.test.ts b/test/integration.test.ts similarity index 87% rename from integration.test.ts rename to test/integration.test.ts index 913fc79..67d4f5c 100644 --- a/integration.test.ts +++ b/test/integration.test.ts @@ -6,7 +6,7 @@ describe("Integration Tests", () => { // Test that the policy file can be loaded const fs = require("bun:fs"); const path = require("bun:path"); - const policyPath = path.join(__dirname, "./policy/policy.json"); + const policyPath = path.join(__dirname, "../policy/policy.json"); expect(() => { const policyData = fs.readFileSync(policyPath, "utf8"); @@ -18,7 +18,7 @@ describe("Integration Tests", () => { test("should have correct policy structure", () => { const fs = require("bun:fs"); const path = require("bun:path"); - const policyPath = path.join(__dirname, "./policy/policy.json"); + const policyPath = path.join(__dirname, "../policy/policy.json"); const policyData = fs.readFileSync(policyPath, "utf8"); const policy = JSON.parse(policyData); @@ -41,8 +41,8 @@ describe("Integration Tests", () => { describe("Helper Functions Integration", () => { test("should validate withdraw policy correctly", () => { - const { validateWithdraw } = require("./helpers"); - const { loadPolicy } = require("./helpers"); + const { validateWithdraw } = require("../helpers"); + const { loadPolicy } = require("../helpers"); const policy = loadPolicy("./policy/policy.json"); @@ -70,8 +70,8 @@ describe("Integration Tests", () => { }); test("should validate order policy correctly", () => { - const { validateOrder } = require("./helpers"); - const { loadPolicy } = require("./helpers"); + const { validateOrder } = require("../helpers"); + const { loadPolicy } = require("../helpers"); const policy = loadPolicy("./policy/policy.json"); @@ -89,7 +89,7 @@ describe("Integration Tests", () => { describe("Price Calculation Integration", () => { test("should calculate optimal prices correctly", async () => { - const { buyAtOptimalPrice, sellAtOptimalPrice } = require("./helpers"); + const { buyAtOptimalPrice, sellAtOptimalPrice } = require("../helpers"); // Create a mock exchange with realistic order book data const mockExchange = { @@ -125,7 +125,7 @@ describe("Integration Tests", () => { describe("Error Handling Integration", () => { test("should handle insufficient depth correctly", async () => { - const { buyAtOptimalPrice } = require("./helpers"); + const { buyAtOptimalPrice } = require("../helpers"); const insufficientExchange = { fetchOrderBook: async () => ({ @@ -139,8 +139,8 @@ describe("Integration Tests", () => { }); test("should handle invalid symbol format", () => { - const { validateOrder } = require("./helpers"); - const { loadPolicy } = require("./helpers"); + const { validateOrder } = require("../helpers"); + const { loadPolicy } = require("../helpers"); const policy = loadPolicy("./policy/policy.json"); From 24da97da75c9359f6290f6d23788e456fc655fcb Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 15:03:15 +0100 Subject: [PATCH 13/45] Refactor paths to src directory --- biome.json | 2 +- build.ts | 4 +- cli.ts | 25 -- client.dev.ts | 38 --- index.ts | 712 -------------------------------------------------- package.json | 6 +- 6 files changed, 6 insertions(+), 781 deletions(-) delete mode 100755 cli.ts delete mode 100644 client.dev.ts delete mode 100644 index.ts diff --git a/biome.json b/biome.json index c74a9f3..dcb6f5f 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "includes": ["proto/", "index.ts", "helpers/", "config/", "policy/"] + "includes": ["proto/", "src/index.ts", "helpers/", "config/", "policy/"] }, "formatter": { "enabled": true, diff --git a/build.ts b/build.ts index 6d98127..b04f7d2 100644 --- a/build.ts +++ b/build.ts @@ -1,7 +1,7 @@ import dts from 'bun-plugin-dts' await Bun.build({ - entrypoints: ['./index.ts'], + entrypoints: ['./src/index.ts'], outdir: './dist', target:"node", plugins: [ @@ -10,7 +10,7 @@ await Bun.build({ }) await Bun.build({ - entrypoints: ["./cli.ts"], + entrypoints: ["./src/cli.ts"], outdir: './dist/commands', target:"node", plugins: [ diff --git a/cli.ts b/cli.ts deleted file mode 100755 index 51274bc..0000000 --- a/cli.ts +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bun - -import { Command } from 'commander'; -import { startBrokerCommand } from './commands/start-broker'; - -const program = new Command(); - -program - .name('cex-broker') - .description('CLI to start the CEXBroker service') - .requiredOption('-p, --policy ', 'Policy JSON file') - .option('--port ', 'Port number (default: 8086)', '8086') - .action(async (options) => { - try { - await startBrokerCommand( - options.policy, - parseInt(options.port, 10) - ); - } catch (err) { - console.error('❌ Failed to start broker:', err); - process.exit(1); - } - }); - -program.parse(process.argv); diff --git a/client.dev.ts b/client.dev.ts deleted file mode 100644 index e7b4468..0000000 --- a/client.dev.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "path"; -import * as grpc from "@grpc/grpc-js"; -import * as protoLoader from "@grpc/proto-loader"; -import type { ProtoGrpcType } from "./proto/node"; - -const PROTO_FILE = "./proto/node.proto"; -const port= 8086 - -const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); -const grpcObj = grpc.loadPackageDefinition( - packageDef, -) as unknown as ProtoGrpcType; - -const client = new grpcObj.cexBroker.CexService( - `0.0.0.0:${port}`, - grpc.credentials.createInsecure(), -); - -const deadline = new Date(); -deadline.setSeconds(deadline.getSeconds() + 5); -client.waitForReady(deadline, (err) => { - if (err) { - console.error(err); - return; - } - onClientReady(); -}); - -function onClientReady() { - client.getBalance({ cex: "bybit", token: "USDT" }, (err, result) => { - if (err) { - console.error({ err }); - return; - } - console.log({ x: result }); - }); - -} diff --git a/index.ts b/index.ts deleted file mode 100644 index 55a28d7..0000000 --- a/index.ts +++ /dev/null @@ -1,712 +0,0 @@ -import ccxt, { type Exchange } from "ccxt"; -import path from "path"; -import * as grpc from "@grpc/grpc-js"; -import * as protoLoader from "@grpc/proto-loader"; -import type { ProtoGrpcType } from "./proto/node"; -import { - loadPolicy, - validateOrder, - validateWithdraw, - validateDeposit, - isIpAllowed, -} from "./helpers"; -import type { BalanceRequest } from "./proto/cexBroker/BalanceRequest"; -import type { BalanceResponse } from "./proto/cexBroker/BalanceResponse"; -import { - BrokerList, - type BrokerCredentials, - type ExchangeCredentials, - type PolicyConfig, -} from "./types"; -import type { TransferRequest } from "./proto/cexBroker/TransferRequest"; -import type { TransferResponse } from "./proto/cexBroker/TransferResponse"; -import type { DepositConfirmationRequest } from "./proto/cexBroker/DepositConfirmationRequest"; -import type { DepositConfirmationResponse } from "./proto/cexBroker/DepositConfirmationResponse"; -import type { ConvertRequest } from "./proto/cexBroker/ConvertRequest"; -import type { ConvertResponse } from "./proto/cexBroker/ConvertResponse"; -import type { OrderDetailsRequest } from "./proto/cexBroker/OrderDetailsRequest"; -import type { OrderDetailsResponse } from "./proto/cexBroker/OrderDetailsResponse"; -import type { CancelOrderRequest } from "./proto/cexBroker/CancelOrderRequest"; -import type { CancelOrderResponse } from "./proto/cexBroker/CancelOrderResponse"; -import { watchFile, unwatchFile } from "fs"; -import Joi from "joi"; - -const PROTO_FILE = "./proto/node.proto"; - -const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); -const grpcObj = grpc.loadPackageDefinition( - packageDef, -) as unknown as ProtoGrpcType; -const fietCexNode = grpcObj.cexBroker; - -console.log("CCXT Version:", ccxt.version); - -export default class CEXBroker { - #brokerConfig: Record = {}; - #policyFilePath?: string; - port = 8086; - private policy: PolicyConfig; - private brokers: Record = {}; - private server: grpc.Server | null = null; - - /** - * Loads environment variables prefixed with CEX_BROKER_ - * Expected format: - * CEX_BROKER__API_KEY - * CEX_BROKER__API_SECRET - */ - public loadEnvConfig(): void { - console.log("🔧 Loading CEX_BROKER_ environment variables:"); - const configMap: Record> = {}; - - for (const [key, value] of Object.entries(process.env)) { - if (!key.startsWith("CEX_BROKER_")) continue; - - const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); - if (!match) { - console.warn(`⚠️ Skipping unrecognized env var: ${key}`); - continue; - } - - const broker = match[1].toLowerCase(); // normalize to lowercase - const type = match[2].toLowerCase(); // 'key' or 'secret' - - if (!configMap[broker]) { - configMap[broker] = {}; - } - - if (type === "key") { - configMap[broker].apiKey = value || ""; - } else if (type === "secret") { - configMap[broker].apiSecret = value || ""; - } - } - - if (Object.keys(configMap).length === 0) { - console.error(`❌ NO CEX Broker Key Found`); - } - - // Finalize config and print result per broker - for (const [broker, creds] of Object.entries(configMap)) { - const hasKey = !!creds.apiKey; - const hasSecret = !!creds.apiSecret; - - if (hasKey && hasSecret) { - this.#brokerConfig[broker] = { - apiKey: creds.apiKey ?? "", - apiSecret: creds.apiSecret ?? "", - }; - console.log(`✅ Loaded credentials for broker "${broker}"`); - const ExchangeClass = (ccxt as any)[broker]; - const client = new ExchangeClass({ - apiKey: creds.apiKey, - secret: creds.apiSecret, - enableRateLimit: true, - defaultType: "spot", - }); - this.brokers[broker] = client; - } else { - const missing = []; - if (!hasKey) missing.push("API_KEY"); - if (!hasSecret) missing.push("API_SECRET"); - console.warn( - `❌ Missing ${missing.join(" and ")} for broker "${broker}"`, - ); - } - } - } - - /** - * Validates an exc hange credential object structure. - */ - public loadExchangeCredentials( - creds: unknown, - ): asserts creds is ExchangeCredentials { - const schema = Joi.object>() - .pattern( - Joi.string() - .allow(...BrokerList) - .required(), - Joi.object({ - apiKey: Joi.string().required(), - apiSecret: Joi.string().required(), - }), - ) - .required(); - - const { value, error } = schema.validate(creds); - if (error) { - throw new Error(`Invalid credentials format: ${error.message}`); - } - - // Finalize config and print result per broker - for (const [broker, creds] of Object.entries(value)) { - console.log(`✅ Loaded credentials for broker "${broker}"`); - const ExchangeClass = (ccxt as any)[broker]; - const client = new ExchangeClass({ - apiKey: creds.apiKey, - secret: creds.apiSecret, - enableRateLimit: true, - defaultType: "spot", - }); - this.brokers[broker] = client; - } - } - - constructor( - apiCredentials: ExchangeCredentials, - policies: string | PolicyConfig, - config?: { port: number }, - ) { - if (typeof policies === "string") { - this.#policyFilePath = policies; - this.policy = loadPolicy(policies); - this.port = config?.port ?? 8086; - } else { - this.policy = policies; - } - - // If monitoring a file, start watcher - if (this.#policyFilePath) { - this.watchPolicyFile(this.#policyFilePath); - } - - this.loadExchangeCredentials(apiCredentials); - } - - /** - * Watches the policy JSON file for changes, reloads policies, and reruns broker. - * @param filePath - */ - private watchPolicyFile(filePath: string): void { - watchFile(filePath, { interval: 1000 }, (curr, prev) => { - if (curr.mtime > prev.mtime) { - try { - const updated = loadPolicy(filePath); - this.policy = updated; - console.log( - `Policies reloaded from ${filePath} at ${new Date().toISOString()}`, - ); - // Rerun broker with updated policies - this.run(); - } catch (err) { - console.error(`Error reloading policies: ${err}`); - } - } - }); - } - - /** - * Stops Server and Stop watching the policy file, if applicable. - */ - public stop(): void { - if (this.#policyFilePath) { - unwatchFile(this.#policyFilePath); - console.log(`Stopped watching policy file: ${this.#policyFilePath}`); - } - if (this.server) { - this.server.forceShutdown(); - } - } - - /** - * Starts the broker, applying policies then running appropriate tasks. - */ - public async run(): Promise { - if (this.server) { - await this.server.forceShutdown(); - } - console.log(`Running CEXBroker at ${new Date().toISOString()}`); - this.server = getServer(this.policy, this.brokers); - - this.server.bindAsync( - `0.0.0.0:${this.port}`, - grpc.ServerCredentials.createInsecure(), - (err, port) => { - if (err) { - console.error(err); - return; - } - console.log(`Your server as started on port ${port}`); - }, - ); - return this; - } -} - -function authenticateRequest(call: grpc.ServerUnaryCall): boolean { - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !isIpAllowed(clientIp)) { - console.warn( - `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, - ); - return false; - } - return true; -} - -function getServer(policy: PolicyConfig, brokers: Record) { - const server = new grpc.Server(); - server.addService(fietCexNode.CexService.service, { - // TODO: Consolidate all of these calls into "ExecuteAction", "SubscribeToStream"... - Deposit: async ( - call: grpc.ServerUnaryCall< - DepositConfirmationRequest, - DepositConfirmationResponse - >, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement deposit logic - const { chain, recipientAddress, amount, transactionHash } = call.request; - - // Validate required fields - if (!chain || !amount || !recipientAddress || !transactionHash) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "chain, transactionHash, recipientAddress and amount are required", - }, - null, - ); - } - - // Validate against policy - - // TODO: I recognise that deposit/withdraw, etc. will need additional considerations as we validate against the policy... - // TODO: Therefore, either we can keep the standalone Deposit/Withdraw Methods, or we check the "ExecuteAction" method for the "deposit"/"withdraw"/"convert"/"cancelOrder"/"getOrderDetails"/"getBalance" actions... to determine extra validation. - const validation = validateDeposit(policy, chain, Number(amount)); - if (!validation.valid) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: validation.error, - }, - null, - ); - } - - // TODO: Where is CCXT used here? - try { - console.log( - `[${new Date().toISOString()}] ` + - `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, - ); - callback(null, { newBalance: 0 }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Deposit confirmation failed", - }, - null, - ); - } - }, - Transfer: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement transfer logic - const { chain, cex, amount, recipientAddress, token } = call.request; - - // Validate required fields - if (!chain || !recipientAddress || !amount || !cex || !token) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "chain, recipient_address, amount, and ticker are required", - }, - null, - ); - } - - // Validate against policy - const validation = validateWithdraw( - policy, - chain, - recipientAddress, - Number(amount), - token, - ); - if (!validation.valid) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: validation.error, - }, - null, - ); - } - - try { - if (!Object.keys(brokers).includes(cex)) { - return callback( - { - code: grpc.status.INTERNAL, - message: `Broker ${cex} is not active. Allowed Broker: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const data = await broker.fetchCurrencies("USDT"); - const networks = Object.keys( - (data[token] ?? { networks: [] }).networks, - ); - - if (!networks.includes(chain)) { - return callback( - { - code: grpc.status.INTERNAL, - message: `Broker ${cex} doesnt support this ${chain} for token ${token}`, - }, - null, - ); - } - - // TODO: My point is why can this not be agnostic to the CEX... - const transaction = await broker.withdraw( - token, - Number(amount), - recipientAddress, - undefined, - { network: chain }, - ); - - callback(null, { success: true, transactionId: transaction.id }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Transfer failed", - }, - null, - ); - } - }, - - // TODO: "Convert" and "createLimitOrder" are too extremely different things... - // TODO: "Convert" is a generic action that can be used to convert any token to any other token... - // TODO: "createLimitOrder" is a specific action that can be used to create a limit order on a specific CEX... - // TODO: "Convert" could be "createMarketOrder"... a differnt thing to "createLimitOrder"... - Convert: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement convert logic - const { fromToken, toToken, amount, cex, price } = call.request; - - // Validate required fields - if (!fromToken || !toToken || !amount || !cex || !price) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "toToken, fromToken, amount, cex, and price are required", - }, - null, - ); - } - - const validation = validateOrder( - policy, - fromToken, - toToken, - Number(amount), - cex, - ); - if (!validation.valid) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: validation.error, - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - - const market = policy.order.rule.markets.find( - (market) => - market.includes(`${fromToken}/${toToken}`) || - market.includes(`${toToken}/${fromToken}`), - ); - const symbol = market?.split(":")[1] ?? ""; - const [from, _to] = symbol.split("/"); - - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const order = await broker.createLimitOrder( - symbol, - from === fromToken ? "sell" : "buy", - Number(amount), - Number(price), - ); - - callback(null, { - orderId: order.id, - }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Conversion failed", - }, - null, - ); - } - }, - GetBalance: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { cex, token } = call.request as Required; - - // Validate required fields - if (!cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "cex_key is required", - }, - null, - ); - } - - if (!token) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "token is required", - }, - null, - ); - } - - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - try { - // Fetch balance from the specified CEX - const balance = (await broker.fetchFreeBalance()) as any; - const currencyBalance = balance[token]; - - callback(null, { - balance: currencyBalance || 0, - currency: token, - }); - } catch (error) { - console.error(`Error fetching balance from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch balance from ${cex}`, - }, - null, - ); - } - }, - GetOrderDetails: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { orderId, cex } = call.request; - - // Validate required fields - if (!orderId || !cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "order_id and cex are required", - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const orderDetails = await broker.fetchOrder(orderId); - - callback(null, { - orderId: orderDetails.id, - status: orderDetails.status, - originalAmount: orderDetails.amount, - filledAmount: orderDetails.filled, - symbol: orderDetails.symbol, - mode: orderDetails.side, - price: orderDetails.price, - }); - } catch (error) { - console.error(`Error fetching order details from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch order details from ${cex}`, - }, - null, - ); - } - }, - CancelOrder: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { orderId, cex } = call.request; - - // Validate required fields - if (!orderId || !cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "order_id and cex are required", - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const cancelledOrder: any = await broker.cancelOrder(orderId); - - callback(null, { - success: cancelledOrder.status === "canceled", - finalStatus: cancelledOrder.status, - }); - } catch (error) { - console.error(`Error cancelling order from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to cancel order from ${cex}`, - }, - null, - ); - } - }, - }); - return server; -} diff --git a/package.json b/package.json index 4e0d8ba..a5f1cc5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "repository": "git@gitlab.com:usherlabs/cex-broker.git", "homepage": "https://usher.so/", "author": "Oki Ayobami ", - "module": "index.ts", + "module": "src/index.ts", "type": "module", "license": "MIT", "main": "dist/index.js", @@ -25,8 +25,8 @@ }, "scripts": { "proto-gen": "./proto-gen.sh", - "start": "bun run index.ts", - "build:ts": "bun run ./build.ts", + "start": "bun run ./src/index.ts", + "build:ts": "bun run ./src/build.ts", "test": "bun test", "format": "bunx biome format --write", "lint": "bunx biome lint --write", From 366e4311b13fba19580bd363adaba8dc60c58bd0 Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 15:03:27 +0100 Subject: [PATCH 14/45] Add CLI and client files for CEXBroker service --- src/cli.ts | 25 ++ src/client.dev.ts | 38 +++ src/index.ts | 712 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 775 insertions(+) create mode 100755 src/cli.ts create mode 100644 src/client.dev.ts create mode 100644 src/index.ts diff --git a/src/cli.ts b/src/cli.ts new file mode 100755 index 0000000..0ff1ba6 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,25 @@ +#!/usr/bin/env bun + +import { Command } from 'commander'; +import { startBrokerCommand } from '../commands/start-broker'; + +const program = new Command(); + +program + .name('cex-broker') + .description('CLI to start the CEXBroker service') + .requiredOption('-p, --policy ', 'Policy JSON file') + .option('--port ', 'Port number (default: 8086)', '8086') + .action(async (options) => { + try { + await startBrokerCommand( + options.policy, + parseInt(options.port, 10) + ); + } catch (err) { + console.error('❌ Failed to start broker:', err); + process.exit(1); + } + }); + +program.parse(process.argv); diff --git a/src/client.dev.ts b/src/client.dev.ts new file mode 100644 index 0000000..026d7a6 --- /dev/null +++ b/src/client.dev.ts @@ -0,0 +1,38 @@ +import path from "path"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import type { ProtoGrpcType } from "../proto/node"; + +const PROTO_FILE = "../proto/node.proto"; +const port= 8086 + +const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); +const grpcObj = grpc.loadPackageDefinition( + packageDef, +) as unknown as ProtoGrpcType; + +const client = new grpcObj.cexBroker.CexService( + `0.0.0.0:${port}`, + grpc.credentials.createInsecure(), +); + +const deadline = new Date(); +deadline.setSeconds(deadline.getSeconds() + 5); +client.waitForReady(deadline, (err) => { + if (err) { + console.error(err); + return; + } + onClientReady(); +}); + +function onClientReady() { + client.getBalance({ cex: "bybit", token: "USDT" }, (err, result) => { + if (err) { + console.error({ err }); + return; + } + console.log({ x: result }); + }); + +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..874f05a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,712 @@ +import ccxt, { type Exchange } from "ccxt"; +import path from "path"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import type { ProtoGrpcType } from "../proto/node"; +import { + loadPolicy, + validateOrder, + validateWithdraw, + validateDeposit, + isIpAllowed, +} from "../helpers"; +import type { BalanceRequest } from "../proto/cexBroker/BalanceRequest"; +import type { BalanceResponse } from "../proto/cexBroker/BalanceResponse"; +import { + BrokerList, + type BrokerCredentials, + type ExchangeCredentials, + type PolicyConfig, +} from "../types"; +import type { TransferRequest } from "../proto/cexBroker/TransferRequest"; +import type { TransferResponse } from "../proto/cexBroker/TransferResponse"; +import type { DepositConfirmationRequest } from "../proto/cexBroker/DepositConfirmationRequest"; +import type { DepositConfirmationResponse } from "../proto/cexBroker/DepositConfirmationResponse"; +import type { ConvertRequest } from "../proto/cexBroker/ConvertRequest"; +import type { ConvertResponse } from "../proto/cexBroker/ConvertResponse"; +import type { OrderDetailsRequest } from "../proto/cexBroker/OrderDetailsRequest"; +import type { OrderDetailsResponse } from "../proto/cexBroker/OrderDetailsResponse"; +import type { CancelOrderRequest } from "../proto/cexBroker/CancelOrderRequest"; +import type { CancelOrderResponse } from "../proto/cexBroker/CancelOrderResponse"; +import { watchFile, unwatchFile } from "fs"; +import Joi from "joi"; + +const PROTO_FILE = "../proto/node.proto"; + +const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); +const grpcObj = grpc.loadPackageDefinition( + packageDef, +) as unknown as ProtoGrpcType; +const fietCexNode = grpcObj.cexBroker; + +console.log("CCXT Version:", ccxt.version); + +export default class CEXBroker { + #brokerConfig: Record = {}; + #policyFilePath?: string; + port = 8086; + private policy: PolicyConfig; + private brokers: Record = {}; + private server: grpc.Server | null = null; + + /** + * Loads environment variables prefixed with CEX_BROKER_ + * Expected format: + * CEX_BROKER__API_KEY + * CEX_BROKER__API_SECRET + */ + public loadEnvConfig(): void { + console.log("🔧 Loading CEX_BROKER_ environment variables:"); + const configMap: Record> = {}; + + for (const [key, value] of Object.entries(process.env)) { + if (!key.startsWith("CEX_BROKER_")) continue; + + const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); + if (!match) { + console.warn(`⚠️ Skipping unrecognized env var: ${key}`); + continue; + } + + const broker = match[1].toLowerCase(); // normalize to lowercase + const type = match[2].toLowerCase(); // 'key' or 'secret' + + if (!configMap[broker]) { + configMap[broker] = {}; + } + + if (type === "key") { + configMap[broker].apiKey = value || ""; + } else if (type === "secret") { + configMap[broker].apiSecret = value || ""; + } + } + + if (Object.keys(configMap).length === 0) { + console.error(`❌ NO CEX Broker Key Found`); + } + + // Finalize config and print result per broker + for (const [broker, creds] of Object.entries(configMap)) { + const hasKey = !!creds.apiKey; + const hasSecret = !!creds.apiSecret; + + if (hasKey && hasSecret) { + this.#brokerConfig[broker] = { + apiKey: creds.apiKey ?? "", + apiSecret: creds.apiSecret ?? "", + }; + console.log(`✅ Loaded credentials for broker "${broker}"`); + const ExchangeClass = (ccxt as any)[broker]; + const client = new ExchangeClass({ + apiKey: creds.apiKey, + secret: creds.apiSecret, + enableRateLimit: true, + defaultType: "spot", + }); + this.brokers[broker] = client; + } else { + const missing = []; + if (!hasKey) missing.push("API_KEY"); + if (!hasSecret) missing.push("API_SECRET"); + console.warn( + `❌ Missing ${missing.join(" and ")} for broker "${broker}"`, + ); + } + } + } + + /** + * Validates an exc hange credential object structure. + */ + public loadExchangeCredentials( + creds: unknown, + ): asserts creds is ExchangeCredentials { + const schema = Joi.object>() + .pattern( + Joi.string() + .allow(...BrokerList) + .required(), + Joi.object({ + apiKey: Joi.string().required(), + apiSecret: Joi.string().required(), + }), + ) + .required(); + + const { value, error } = schema.validate(creds); + if (error) { + throw new Error(`Invalid credentials format: ${error.message}`); + } + + // Finalize config and print result per broker + for (const [broker, creds] of Object.entries(value)) { + console.log(`✅ Loaded credentials for broker "${broker}"`); + const ExchangeClass = (ccxt as any)[broker]; + const client = new ExchangeClass({ + apiKey: creds.apiKey, + secret: creds.apiSecret, + enableRateLimit: true, + defaultType: "spot", + }); + this.brokers[broker] = client; + } + } + + constructor( + apiCredentials: ExchangeCredentials, + policies: string | PolicyConfig, + config?: { port: number }, + ) { + if (typeof policies === "string") { + this.#policyFilePath = policies; + this.policy = loadPolicy(policies); + this.port = config?.port ?? 8086; + } else { + this.policy = policies; + } + + // If monitoring a file, start watcher + if (this.#policyFilePath) { + this.watchPolicyFile(this.#policyFilePath); + } + + this.loadExchangeCredentials(apiCredentials); + } + + /** + * Watches the policy JSON file for changes, reloads policies, and reruns broker. + * @param filePath + */ + private watchPolicyFile(filePath: string): void { + watchFile(filePath, { interval: 1000 }, (curr, prev) => { + if (curr.mtime > prev.mtime) { + try { + const updated = loadPolicy(filePath); + this.policy = updated; + console.log( + `Policies reloaded from ${filePath} at ${new Date().toISOString()}`, + ); + // Rerun broker with updated policies + this.run(); + } catch (err) { + console.error(`Error reloading policies: ${err}`); + } + } + }); + } + + /** + * Stops Server and Stop watching the policy file, if applicable. + */ + public stop(): void { + if (this.#policyFilePath) { + unwatchFile(this.#policyFilePath); + console.log(`Stopped watching policy file: ${this.#policyFilePath}`); + } + if (this.server) { + this.server.forceShutdown(); + } + } + + /** + * Starts the broker, applying policies then running appropriate tasks. + */ + public async run(): Promise { + if (this.server) { + await this.server.forceShutdown(); + } + console.log(`Running CEXBroker at ${new Date().toISOString()}`); + this.server = getServer(this.policy, this.brokers); + + this.server.bindAsync( + `0.0.0.0:${this.port}`, + grpc.ServerCredentials.createInsecure(), + (err, port) => { + if (err) { + console.error(err); + return; + } + console.log(`Your server as started on port ${port}`); + }, + ); + return this; + } +} + +function authenticateRequest(call: grpc.ServerUnaryCall): boolean { + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || !isIpAllowed(clientIp)) { + console.warn( + `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, + ); + return false; + } + return true; +} + +function getServer(policy: PolicyConfig, brokers: Record) { + const server = new grpc.Server(); + server.addService(fietCexNode.CexService.service, { + // TODO: Consolidate all of these calls into "ExecuteAction", "SubscribeToStream"... + Deposit: async ( + call: grpc.ServerUnaryCall< + DepositConfirmationRequest, + DepositConfirmationResponse + >, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + // Implement deposit logic + const { chain, recipientAddress, amount, transactionHash } = call.request; + + // Validate required fields + if (!chain || !amount || !recipientAddress || !transactionHash) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "chain, transactionHash, recipientAddress and amount are required", + }, + null, + ); + } + + // Validate against policy + + // TODO: I recognise that deposit/withdraw, etc. will need additional considerations as we validate against the policy... + // TODO: Therefore, either we can keep the standalone Deposit/Withdraw Methods, or we check the "ExecuteAction" method for the "deposit"/"withdraw"/"convert"/"cancelOrder"/"getOrderDetails"/"getBalance" actions... to determine extra validation. + const validation = validateDeposit(policy, chain, Number(amount)); + if (!validation.valid) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: validation.error, + }, + null, + ); + } + + // TODO: Where is CCXT used here? + try { + console.log( + `[${new Date().toISOString()}] ` + + `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, + ); + callback(null, { newBalance: 0 }); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } + }, + Transfer: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + // Implement transfer logic + const { chain, cex, amount, recipientAddress, token } = call.request; + + // Validate required fields + if (!chain || !recipientAddress || !amount || !cex || !token) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "chain, recipient_address, amount, and ticker are required", + }, + null, + ); + } + + // Validate against policy + const validation = validateWithdraw( + policy, + chain, + recipientAddress, + Number(amount), + token, + ); + if (!validation.valid) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: validation.error, + }, + null, + ); + } + + try { + if (!Object.keys(brokers).includes(cex)) { + return callback( + { + code: grpc.status.INTERNAL, + message: `Broker ${cex} is not active. Allowed Broker: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + // Validate CEX key + const broker = brokers[cex as keyof typeof brokers]; + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const data = await broker.fetchCurrencies("USDT"); + const networks = Object.keys( + (data[token] ?? { networks: [] }).networks, + ); + + if (!networks.includes(chain)) { + return callback( + { + code: grpc.status.INTERNAL, + message: `Broker ${cex} doesnt support this ${chain} for token ${token}`, + }, + null, + ); + } + + // TODO: My point is why can this not be agnostic to the CEX... + const transaction = await broker.withdraw( + token, + Number(amount), + recipientAddress, + undefined, + { network: chain }, + ); + + callback(null, { success: true, transactionId: transaction.id }); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Transfer failed", + }, + null, + ); + } + }, + + // TODO: "Convert" and "createLimitOrder" are too extremely different things... + // TODO: "Convert" is a generic action that can be used to convert any token to any other token... + // TODO: "createLimitOrder" is a specific action that can be used to create a limit order on a specific CEX... + // TODO: "Convert" could be "createMarketOrder"... a differnt thing to "createLimitOrder"... + Convert: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + // Implement convert logic + const { fromToken, toToken, amount, cex, price } = call.request; + + // Validate required fields + if (!fromToken || !toToken || !amount || !cex || !price) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "toToken, fromToken, amount, cex, and price are required", + }, + null, + ); + } + + const validation = validateOrder( + policy, + fromToken, + toToken, + Number(amount), + cex, + ); + if (!validation.valid) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: validation.error, + }, + null, + ); + } + + try { + // Validate CEX key + const broker = brokers[cex as keyof typeof brokers]; + + const market = policy.order.rule.markets.find( + (market) => + market.includes(`${fromToken}/${toToken}`) || + market.includes(`${toToken}/${fromToken}`), + ); + const symbol = market?.split(":")[1] ?? ""; + const [from, _to] = symbol.split("/"); + + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const order = await broker.createLimitOrder( + symbol, + from === fromToken ? "sell" : "buy", + Number(amount), + Number(price), + ); + + callback(null, { + orderId: order.id, + }); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Conversion failed", + }, + null, + ); + } + }, + GetBalance: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + const { cex, token } = call.request as Required; + + // Validate required fields + if (!cex) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "cex_key is required", + }, + null, + ); + } + + if (!token) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "token is required", + }, + null, + ); + } + + // Validate CEX key + const broker = brokers[cex as keyof typeof brokers]; + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + try { + // Fetch balance from the specified CEX + const balance = (await broker.fetchFreeBalance()) as any; + const currencyBalance = balance[token]; + + callback(null, { + balance: currencyBalance || 0, + currency: token, + }); + } catch (error) { + console.error(`Error fetching balance from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch balance from ${cex}`, + }, + null, + ); + } + }, + GetOrderDetails: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + const { orderId, cex } = call.request; + + // Validate required fields + if (!orderId || !cex) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "order_id and cex are required", + }, + null, + ); + } + + try { + // Validate CEX key + const broker = brokers[cex as keyof typeof brokers]; + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const orderDetails = await broker.fetchOrder(orderId); + + callback(null, { + orderId: orderDetails.id, + status: orderDetails.status, + originalAmount: orderDetails.amount, + filledAmount: orderDetails.filled, + symbol: orderDetails.symbol, + mode: orderDetails.side, + price: orderDetails.price, + }); + } catch (error) { + console.error(`Error fetching order details from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch order details from ${cex}`, + }, + null, + ); + } + }, + CancelOrder: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + + const { orderId, cex } = call.request; + + // Validate required fields + if (!orderId || !cex) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "order_id and cex are required", + }, + null, + ); + } + + try { + // Validate CEX key + const broker = brokers[cex as keyof typeof brokers]; + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const cancelledOrder: any = await broker.cancelOrder(orderId); + + callback(null, { + success: cancelledOrder.status === "canceled", + finalStatus: cancelledOrder.status, + }); + } catch (error) { + console.error(`Error cancelling order from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to cancel order from ${cex}`, + }, + null, + ); + } + }, + }); + return server; +} From 9be1e6526b8510b4dd08422c678b285c7adf9c68 Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 18:48:27 +0100 Subject: [PATCH 15/45] Refactor CEX service with unified CCXT actions --- build.ts | 10 +- proto/cexBroker/Action.ts | 29 + proto/cexBroker/BalanceRequest.ts | 12 - proto/cexBroker/BalanceResponse.ts | 12 - proto/cexBroker/CancelOrderRequest.ts | 12 - proto/cexBroker/CancelOrderResponse.ts | 12 - proto/cexBroker/CcxtActionRequest.ts | 17 + proto/cexBroker/CcxtActionResponse.ts | 10 + proto/cexBroker/CexService.ts | 94 +--- proto/cexBroker/ConvertRequest.ts | 18 - proto/cexBroker/ConvertResponse.ts | 10 - proto/cexBroker/DepositConfirmationRequest.ts | 16 - .../cexBroker/DepositConfirmationResponse.ts | 10 - proto/cexBroker/OptimalPriceRequest.ts | 15 - proto/cexBroker/OptimalPriceResponse.ts | 11 - proto/cexBroker/OrderDetailsRequest.ts | 12 - proto/cexBroker/OrderDetailsResponse.ts | 22 - proto/cexBroker/OrderMode.ts | 14 - proto/cexBroker/PriceInfo.ts | 12 - proto/cexBroker/TransferRequest.ts | 18 - proto/cexBroker/TransferResponse.ts | 12 - proto/node.proto | 113 +--- proto/node.ts | 16 +- src/cli.ts | 22 +- src/client.dev.ts | 6 +- {commands => src/commands}/start-broker.ts | 4 +- {helpers => src/helpers}/index.test.ts | 0 {helpers => src/helpers}/index.ts | 19 +- src/index.ts | 526 +----------------- src/server.ts | 404 ++++++++++++++ test/index.test.ts | 27 - test/integration.test.ts | 16 +- 32 files changed, 550 insertions(+), 981 deletions(-) create mode 100644 proto/cexBroker/Action.ts delete mode 100644 proto/cexBroker/BalanceRequest.ts delete mode 100644 proto/cexBroker/BalanceResponse.ts delete mode 100644 proto/cexBroker/CancelOrderRequest.ts delete mode 100644 proto/cexBroker/CancelOrderResponse.ts create mode 100644 proto/cexBroker/CcxtActionRequest.ts create mode 100644 proto/cexBroker/CcxtActionResponse.ts delete mode 100644 proto/cexBroker/ConvertRequest.ts delete mode 100644 proto/cexBroker/ConvertResponse.ts delete mode 100644 proto/cexBroker/DepositConfirmationRequest.ts delete mode 100644 proto/cexBroker/DepositConfirmationResponse.ts delete mode 100644 proto/cexBroker/OptimalPriceRequest.ts delete mode 100644 proto/cexBroker/OptimalPriceResponse.ts delete mode 100644 proto/cexBroker/OrderDetailsRequest.ts delete mode 100644 proto/cexBroker/OrderDetailsResponse.ts delete mode 100644 proto/cexBroker/OrderMode.ts delete mode 100644 proto/cexBroker/PriceInfo.ts delete mode 100644 proto/cexBroker/TransferRequest.ts delete mode 100644 proto/cexBroker/TransferResponse.ts rename {commands => src/commands}/start-broker.ts (65%) rename {helpers => src/helpers}/index.test.ts (100%) rename {helpers => src/helpers}/index.ts (93%) create mode 100644 src/server.ts diff --git a/build.ts b/build.ts index b04f7d2..6df730d 100644 --- a/build.ts +++ b/build.ts @@ -1,8 +1,9 @@ import dts from 'bun-plugin-dts' + await Bun.build({ - entrypoints: ['./src/index.ts'], - outdir: './dist', + entrypoints: ["./src/cli.ts"], + outdir: './dist/commands', target:"node", plugins: [ dts() @@ -10,13 +11,12 @@ await Bun.build({ }) await Bun.build({ - entrypoints: ["./src/cli.ts"], - outdir: './dist/commands', + entrypoints: ['./src/index.ts'], + outdir: './dist', target:"node", plugins: [ dts() ], }) - // Generates `dist/index.d.ts` and `dist/other/foo.d.ts` \ No newline at end of file diff --git a/proto/cexBroker/Action.ts b/proto/cexBroker/Action.ts new file mode 100644 index 0000000..7ff5682 --- /dev/null +++ b/proto/cexBroker/Action.ts @@ -0,0 +1,29 @@ +// Original file: proto/node.proto + +export const Action = { + NoAction: 0, + Deposit: 1, + Transfer: 2, + CreateOrder: 3, + GetOrderDetails: 4, + CancelOrder: 5, + FetchBalance: 6, +} as const; + +export type Action = + | 'NoAction' + | 0 + | 'Deposit' + | 1 + | 'Transfer' + | 2 + | 'CreateOrder' + | 3 + | 'GetOrderDetails' + | 4 + | 'CancelOrder' + | 5 + | 'FetchBalance' + | 6 + +export type Action__Output = typeof Action[keyof typeof Action] diff --git a/proto/cexBroker/BalanceRequest.ts b/proto/cexBroker/BalanceRequest.ts deleted file mode 100644 index b4a08c9..0000000 --- a/proto/cexBroker/BalanceRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface BalanceRequest { - 'cex'?: (string); - 'token'?: (string); -} - -export interface BalanceRequest__Output { - 'cex'?: (string); - 'token'?: (string); -} diff --git a/proto/cexBroker/BalanceResponse.ts b/proto/cexBroker/BalanceResponse.ts deleted file mode 100644 index 0c81f55..0000000 --- a/proto/cexBroker/BalanceResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface BalanceResponse { - 'balance'?: (number | string); - 'currency'?: (string); -} - -export interface BalanceResponse__Output { - 'balance'?: (number); - 'currency'?: (string); -} diff --git a/proto/cexBroker/CancelOrderRequest.ts b/proto/cexBroker/CancelOrderRequest.ts deleted file mode 100644 index f8d5c1c..0000000 --- a/proto/cexBroker/CancelOrderRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface CancelOrderRequest { - 'orderId'?: (string); - 'cex'?: (string); -} - -export interface CancelOrderRequest__Output { - 'orderId'?: (string); - 'cex'?: (string); -} diff --git a/proto/cexBroker/CancelOrderResponse.ts b/proto/cexBroker/CancelOrderResponse.ts deleted file mode 100644 index 72837e7..0000000 --- a/proto/cexBroker/CancelOrderResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface CancelOrderResponse { - 'success'?: (boolean); - 'finalStatus'?: (string); -} - -export interface CancelOrderResponse__Output { - 'success'?: (boolean); - 'finalStatus'?: (string); -} diff --git a/proto/cexBroker/CcxtActionRequest.ts b/proto/cexBroker/CcxtActionRequest.ts new file mode 100644 index 0000000..aba0132 --- /dev/null +++ b/proto/cexBroker/CcxtActionRequest.ts @@ -0,0 +1,17 @@ +// Original file: proto/node.proto + +import type { Action as _cexBroker_Action, Action__Output as _cexBroker_Action__Output } from '../cexBroker/Action'; + +export interface CcxtActionRequest { + 'action'?: (_cexBroker_Action); + 'payload'?: ({[key: string]: string}); + 'cex'?: (string); + 'symbol'?: (string); +} + +export interface CcxtActionRequest__Output { + 'action'?: (_cexBroker_Action__Output); + 'payload'?: ({[key: string]: string}); + 'cex'?: (string); + 'symbol'?: (string); +} diff --git a/proto/cexBroker/CcxtActionResponse.ts b/proto/cexBroker/CcxtActionResponse.ts new file mode 100644 index 0000000..fb0915d --- /dev/null +++ b/proto/cexBroker/CcxtActionResponse.ts @@ -0,0 +1,10 @@ +// Original file: proto/node.proto + + +export interface CcxtActionResponse { + 'result'?: (string); +} + +export interface CcxtActionResponse__Output { + 'result'?: (string); +} diff --git a/proto/cexBroker/CexService.ts b/proto/cexBroker/CexService.ts index 9d9af0a..13ec422 100644 --- a/proto/cexBroker/CexService.ts +++ b/proto/cexBroker/CexService.ts @@ -2,96 +2,26 @@ import type * as grpc from '@grpc/grpc-js' import type { MethodDefinition } from '@grpc/proto-loader' -import type { BalanceRequest as _cexBroker_BalanceRequest, BalanceRequest__Output as _cexBroker_BalanceRequest__Output } from '../cexBroker/BalanceRequest'; -import type { BalanceResponse as _cexBroker_BalanceResponse, BalanceResponse__Output as _cexBroker_BalanceResponse__Output } from '../cexBroker/BalanceResponse'; -import type { CancelOrderRequest as _cexBroker_CancelOrderRequest, CancelOrderRequest__Output as _cexBroker_CancelOrderRequest__Output } from '../cexBroker/CancelOrderRequest'; -import type { CancelOrderResponse as _cexBroker_CancelOrderResponse, CancelOrderResponse__Output as _cexBroker_CancelOrderResponse__Output } from '../cexBroker/CancelOrderResponse'; -import type { ConvertRequest as _cexBroker_ConvertRequest, ConvertRequest__Output as _cexBroker_ConvertRequest__Output } from '../cexBroker/ConvertRequest'; -import type { ConvertResponse as _cexBroker_ConvertResponse, ConvertResponse__Output as _cexBroker_ConvertResponse__Output } from '../cexBroker/ConvertResponse'; -import type { DepositConfirmationRequest as _cexBroker_DepositConfirmationRequest, DepositConfirmationRequest__Output as _cexBroker_DepositConfirmationRequest__Output } from '../cexBroker/DepositConfirmationRequest'; -import type { DepositConfirmationResponse as _cexBroker_DepositConfirmationResponse, DepositConfirmationResponse__Output as _cexBroker_DepositConfirmationResponse__Output } from '../cexBroker/DepositConfirmationResponse'; -import type { OrderDetailsRequest as _cexBroker_OrderDetailsRequest, OrderDetailsRequest__Output as _cexBroker_OrderDetailsRequest__Output } from '../cexBroker/OrderDetailsRequest'; -import type { OrderDetailsResponse as _cexBroker_OrderDetailsResponse, OrderDetailsResponse__Output as _cexBroker_OrderDetailsResponse__Output } from '../cexBroker/OrderDetailsResponse'; -import type { TransferRequest as _cexBroker_TransferRequest, TransferRequest__Output as _cexBroker_TransferRequest__Output } from '../cexBroker/TransferRequest'; -import type { TransferResponse as _cexBroker_TransferResponse, TransferResponse__Output as _cexBroker_TransferResponse__Output } from '../cexBroker/TransferResponse'; +import type { CcxtActionRequest as _cexBroker_CcxtActionRequest, CcxtActionRequest__Output as _cexBroker_CcxtActionRequest__Output } from '../cexBroker/CcxtActionRequest'; +import type { CcxtActionResponse as _cexBroker_CcxtActionResponse, CcxtActionResponse__Output as _cexBroker_CcxtActionResponse__Output } from '../cexBroker/CcxtActionResponse'; export interface CexServiceClient extends grpc.Client { - CancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _cexBroker_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - CancelOrder(argument: _cexBroker_CancelOrderRequest, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _cexBroker_CancelOrderRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _cexBroker_CancelOrderRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - cancelOrder(argument: _cexBroker_CancelOrderRequest, callback: grpc.requestCallback<_cexBroker_CancelOrderResponse__Output>): grpc.ClientUnaryCall; - - Convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _cexBroker_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - Convert(argument: _cexBroker_ConvertRequest, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _cexBroker_ConvertRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _cexBroker_ConvertRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - convert(argument: _cexBroker_ConvertRequest, callback: grpc.requestCallback<_cexBroker_ConvertResponse__Output>): grpc.ClientUnaryCall; - - Deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _cexBroker_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - Deposit(argument: _cexBroker_DepositConfirmationRequest, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _cexBroker_DepositConfirmationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _cexBroker_DepositConfirmationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - deposit(argument: _cexBroker_DepositConfirmationRequest, callback: grpc.requestCallback<_cexBroker_DepositConfirmationResponse__Output>): grpc.ClientUnaryCall; - - GetBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _cexBroker_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - GetBalance(argument: _cexBroker_BalanceRequest, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _cexBroker_BalanceRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _cexBroker_BalanceRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - getBalance(argument: _cexBroker_BalanceRequest, callback: grpc.requestCallback<_cexBroker_BalanceResponse__Output>): grpc.ClientUnaryCall; - - GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - GetOrderDetails(argument: _cexBroker_OrderDetailsRequest, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _cexBroker_OrderDetailsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _cexBroker_OrderDetailsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - getOrderDetails(argument: _cexBroker_OrderDetailsRequest, callback: grpc.requestCallback<_cexBroker_OrderDetailsResponse__Output>): grpc.ClientUnaryCall; - - Transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _cexBroker_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - Transfer(argument: _cexBroker_TransferRequest, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _cexBroker_TransferRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _cexBroker_TransferRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; - transfer(argument: _cexBroker_TransferRequest, callback: grpc.requestCallback<_cexBroker_TransferResponse__Output>): grpc.ClientUnaryCall; + ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + executeCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + executeCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + executeCcxtAction(argument: _cexBroker_CcxtActionRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; + executeCcxtAction(argument: _cexBroker_CcxtActionRequest, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; } export interface CexServiceHandlers extends grpc.UntypedServiceImplementation { - CancelOrder: grpc.handleUnaryCall<_cexBroker_CancelOrderRequest__Output, _cexBroker_CancelOrderResponse>; - - Convert: grpc.handleUnaryCall<_cexBroker_ConvertRequest__Output, _cexBroker_ConvertResponse>; - - Deposit: grpc.handleUnaryCall<_cexBroker_DepositConfirmationRequest__Output, _cexBroker_DepositConfirmationResponse>; - - GetBalance: grpc.handleUnaryCall<_cexBroker_BalanceRequest__Output, _cexBroker_BalanceResponse>; - - GetOrderDetails: grpc.handleUnaryCall<_cexBroker_OrderDetailsRequest__Output, _cexBroker_OrderDetailsResponse>; - - Transfer: grpc.handleUnaryCall<_cexBroker_TransferRequest__Output, _cexBroker_TransferResponse>; + ExecuteCcxtAction: grpc.handleUnaryCall<_cexBroker_CcxtActionRequest__Output, _cexBroker_CcxtActionResponse>; } export interface CexServiceDefinition extends grpc.ServiceDefinition { - CancelOrder: MethodDefinition<_cexBroker_CancelOrderRequest, _cexBroker_CancelOrderResponse, _cexBroker_CancelOrderRequest__Output, _cexBroker_CancelOrderResponse__Output> - Convert: MethodDefinition<_cexBroker_ConvertRequest, _cexBroker_ConvertResponse, _cexBroker_ConvertRequest__Output, _cexBroker_ConvertResponse__Output> - Deposit: MethodDefinition<_cexBroker_DepositConfirmationRequest, _cexBroker_DepositConfirmationResponse, _cexBroker_DepositConfirmationRequest__Output, _cexBroker_DepositConfirmationResponse__Output> - GetBalance: MethodDefinition<_cexBroker_BalanceRequest, _cexBroker_BalanceResponse, _cexBroker_BalanceRequest__Output, _cexBroker_BalanceResponse__Output> - GetOrderDetails: MethodDefinition<_cexBroker_OrderDetailsRequest, _cexBroker_OrderDetailsResponse, _cexBroker_OrderDetailsRequest__Output, _cexBroker_OrderDetailsResponse__Output> - Transfer: MethodDefinition<_cexBroker_TransferRequest, _cexBroker_TransferResponse, _cexBroker_TransferRequest__Output, _cexBroker_TransferResponse__Output> + ExecuteCcxtAction: MethodDefinition<_cexBroker_CcxtActionRequest, _cexBroker_CcxtActionResponse, _cexBroker_CcxtActionRequest__Output, _cexBroker_CcxtActionResponse__Output> } diff --git a/proto/cexBroker/ConvertRequest.ts b/proto/cexBroker/ConvertRequest.ts deleted file mode 100644 index 832385f..0000000 --- a/proto/cexBroker/ConvertRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Original file: proto/node.proto - - -export interface ConvertRequest { - 'fromToken'?: (string); - 'toToken'?: (string); - 'amount'?: (number | string); - 'price'?: (number | string); - 'cex'?: (string); -} - -export interface ConvertRequest__Output { - 'fromToken'?: (string); - 'toToken'?: (string); - 'amount'?: (number); - 'price'?: (number); - 'cex'?: (string); -} diff --git a/proto/cexBroker/ConvertResponse.ts b/proto/cexBroker/ConvertResponse.ts deleted file mode 100644 index 44b2b9e..0000000 --- a/proto/cexBroker/ConvertResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Original file: proto/node.proto - - -export interface ConvertResponse { - 'orderId'?: (string); -} - -export interface ConvertResponse__Output { - 'orderId'?: (string); -} diff --git a/proto/cexBroker/DepositConfirmationRequest.ts b/proto/cexBroker/DepositConfirmationRequest.ts deleted file mode 100644 index b5b3149..0000000 --- a/proto/cexBroker/DepositConfirmationRequest.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Original file: proto/node.proto - - -export interface DepositConfirmationRequest { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number | string); - 'transactionHash'?: (string); -} - -export interface DepositConfirmationRequest__Output { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number); - 'transactionHash'?: (string); -} diff --git a/proto/cexBroker/DepositConfirmationResponse.ts b/proto/cexBroker/DepositConfirmationResponse.ts deleted file mode 100644 index b690b56..0000000 --- a/proto/cexBroker/DepositConfirmationResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Original file: proto/node.proto - - -export interface DepositConfirmationResponse { - 'newBalance'?: (number | string); -} - -export interface DepositConfirmationResponse__Output { - 'newBalance'?: (number); -} diff --git a/proto/cexBroker/OptimalPriceRequest.ts b/proto/cexBroker/OptimalPriceRequest.ts deleted file mode 100644 index 6b7046d..0000000 --- a/proto/cexBroker/OptimalPriceRequest.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Original file: proto/node.proto - -import type { OrderMode as _cexBroker_OrderMode, OrderMode__Output as _cexBroker_OrderMode__Output } from '../cexBroker/OrderMode'; - -export interface OptimalPriceRequest { - 'symbol'?: (string); - 'quantity'?: (number | string); - 'mode'?: (_cexBroker_OrderMode); -} - -export interface OptimalPriceRequest__Output { - 'symbol'?: (string); - 'quantity'?: (number); - 'mode'?: (_cexBroker_OrderMode__Output); -} diff --git a/proto/cexBroker/OptimalPriceResponse.ts b/proto/cexBroker/OptimalPriceResponse.ts deleted file mode 100644 index 808c320..0000000 --- a/proto/cexBroker/OptimalPriceResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Original file: proto/node.proto - -import type { PriceInfo as _cexBroker_PriceInfo, PriceInfo__Output as _cexBroker_PriceInfo__Output } from '../cexBroker/PriceInfo'; - -export interface OptimalPriceResponse { - 'results'?: ({[key: string]: _cexBroker_PriceInfo}); -} - -export interface OptimalPriceResponse__Output { - 'results'?: ({[key: string]: _cexBroker_PriceInfo__Output}); -} diff --git a/proto/cexBroker/OrderDetailsRequest.ts b/proto/cexBroker/OrderDetailsRequest.ts deleted file mode 100644 index ffd65d9..0000000 --- a/proto/cexBroker/OrderDetailsRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface OrderDetailsRequest { - 'orderId'?: (string); - 'cex'?: (string); -} - -export interface OrderDetailsRequest__Output { - 'orderId'?: (string); - 'cex'?: (string); -} diff --git a/proto/cexBroker/OrderDetailsResponse.ts b/proto/cexBroker/OrderDetailsResponse.ts deleted file mode 100644 index e80ad1a..0000000 --- a/proto/cexBroker/OrderDetailsResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Original file: proto/node.proto - - -export interface OrderDetailsResponse { - 'orderId'?: (string); - 'status'?: (string); - 'originalAmount'?: (number | string); - 'filledAmount'?: (number | string); - 'symbol'?: (string); - 'mode'?: (string); - 'price'?: (number | string); -} - -export interface OrderDetailsResponse__Output { - 'orderId'?: (string); - 'status'?: (string); - 'originalAmount'?: (number); - 'filledAmount'?: (number); - 'symbol'?: (string); - 'mode'?: (string); - 'price'?: (number); -} diff --git a/proto/cexBroker/OrderMode.ts b/proto/cexBroker/OrderMode.ts deleted file mode 100644 index 721639e..0000000 --- a/proto/cexBroker/OrderMode.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Original file: proto/node.proto - -export const OrderMode = { - BUY: 0, - SELL: 1, -} as const; - -export type OrderMode = - | 'BUY' - | 0 - | 'SELL' - | 1 - -export type OrderMode__Output = typeof OrderMode[keyof typeof OrderMode] diff --git a/proto/cexBroker/PriceInfo.ts b/proto/cexBroker/PriceInfo.ts deleted file mode 100644 index 1215ad7..0000000 --- a/proto/cexBroker/PriceInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface PriceInfo { - 'avgPrice'?: (number | string); - 'fillPrice'?: (number | string); -} - -export interface PriceInfo__Output { - 'avgPrice'?: (number); - 'fillPrice'?: (number); -} diff --git a/proto/cexBroker/TransferRequest.ts b/proto/cexBroker/TransferRequest.ts deleted file mode 100644 index faf16c7..0000000 --- a/proto/cexBroker/TransferRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Original file: proto/node.proto - - -export interface TransferRequest { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number | string); - 'cex'?: (string); - 'token'?: (string); -} - -export interface TransferRequest__Output { - 'chain'?: (string); - 'recipientAddress'?: (string); - 'amount'?: (number); - 'cex'?: (string); - 'token'?: (string); -} diff --git a/proto/cexBroker/TransferResponse.ts b/proto/cexBroker/TransferResponse.ts deleted file mode 100644 index 5ab2b7c..0000000 --- a/proto/cexBroker/TransferResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Original file: proto/node.proto - - -export interface TransferResponse { - 'success'?: (boolean); - 'transactionId'?: (string); -} - -export interface TransferResponse__Output { - 'success'?: (boolean); - 'transactionId'?: (string); -} diff --git a/proto/node.proto b/proto/node.proto index c10d168..e40c84f 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -4,101 +4,28 @@ package cexBroker; // TODO: I've renamed... // TODO: This whole file can be a simple message... // TODO: A single generic CCXT action/message to executing any CCXT function... This way the broker is dynamic ... // TODO: Maybe a second action to trigger a websocket/streaming response... -// message CcxtActionRequest { -// string action = 1; // The CCXT method to call (e.g., "fetchBalance", "createOrder") -// map parameters = 2; // Parameters to pass to the CCXT method -// string cex = 3; // CEX identifier (e.g., "binance", "bybit") -// string symbol = 4; // Optional: trading pair symbol if needed -// } - -// message CcxtActionResponse { -// bool success = 1; // Whether the action was successful -// string result = 2; // JSON string of the result data -// string error = 3; // Error message if success is false -// } -// service CexService { -// rpc ExecuteCcxtAction(CcxtActionRequest) returns (CcxtActionResponse); -// } - - -// Mode enum for price direction -enum OrderMode { - BUY = 0; - SELL = 1; +message CcxtActionRequest { + Action action = 1; // The CCXT method to call (e.g., "fetchBalance", "createOrder") + map payload = 2; // Parameters to pass to the CCXT method + string cex = 3; // CEX identifier (e.g., "binance", "bybit") + string symbol = 4; // Optional: trading pair symbol if needed } -// Optimal price query -// Order details query -message OrderDetailsRequest { - string order_id = 1; // Unique order identifier - string cex = 2; // CEX identifier -} -message OrderDetailsResponse { - string order_id = 1; // Unique order identifier - string status = 2; // Current order status - double original_amount = 3; // Original order amount - double filled_amount = 4; // Amount that has been filled - string symbol = 5; // Trading pair symbol - string mode = 6; // Buy or Sell mode - double price = 7; // Order price -} -// Cancel order request -message CancelOrderRequest { - string order_id = 1; // Unique order identifier - string cex = 2; // CEX identifier -} -message CancelOrderResponse { - bool success = 1; // Whether cancellation was successful - string final_status = 2; // Final status of the order -} -// Withdraw -message DepositConfirmationRequest { - string chain = 1; - string recipient_address = 2; - double amount = 3; - string transaction_hash = 4; -} -message DepositConfirmationResponse { - double new_balance = 1; +message CcxtActionResponse { + string result = 2; // JSON string of the result data } -// Transfer -message TransferRequest { - string chain = 1; - string recipient_address = 2; - double amount = 3; - string cex=4; - string token = 5; -} -message TransferResponse { - bool success = 1; - string transaction_id= 2; -} -// Convert -message ConvertRequest { - string from_token = 1; - string to_token = 2; - double amount = 3; - double price= 4; - string cex=5; -} -message ConvertResponse { - string order_id= 3; -} -// Balance -message BalanceRequest { - string cex = 1; // CEX identifier (e.g., "BINANCE", "BYBIT") - string token = 2; // Trading pair symbol, e.g. "USDT" -} -message BalanceResponse { - double balance = 1; // Available balance for the symbol - string currency = 2; // Currency of the balance -} -// CEX service definition service CexService { - rpc Deposit(DepositConfirmationRequest) returns (DepositConfirmationResponse); - rpc Transfer(TransferRequest) returns (TransferResponse); - rpc Convert(ConvertRequest) returns (ConvertResponse); - rpc GetBalance(BalanceRequest) returns (BalanceResponse); - rpc GetOrderDetails(OrderDetailsRequest) returns (OrderDetailsResponse); - rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse); + rpc ExecuteCcxtAction(CcxtActionRequest) returns (CcxtActionResponse); } + + +// Mode enum for price direction +enum Action { + NoAction=0; + Deposit = 1; + Transfer = 2; + CreateOrder= 3; + GetOrderDetails=4; + CancelOrder=5; + FetchBalance=6; +} \ No newline at end of file diff --git a/proto/node.ts b/proto/node.ts index d6ff3d6..866098b 100644 --- a/proto/node.ts +++ b/proto/node.ts @@ -9,20 +9,10 @@ type SubtypeConstructor any, Subtype> export interface ProtoGrpcType { cexBroker: { - BalanceRequest: MessageTypeDefinition - BalanceResponse: MessageTypeDefinition - CancelOrderRequest: MessageTypeDefinition - CancelOrderResponse: MessageTypeDefinition + Action: EnumTypeDefinition + CcxtActionRequest: MessageTypeDefinition + CcxtActionResponse: MessageTypeDefinition CexService: SubtypeConstructor & { service: _cexBroker_CexServiceDefinition } - ConvertRequest: MessageTypeDefinition - ConvertResponse: MessageTypeDefinition - DepositConfirmationRequest: MessageTypeDefinition - DepositConfirmationResponse: MessageTypeDefinition - OrderDetailsRequest: MessageTypeDefinition - OrderDetailsResponse: MessageTypeDefinition - OrderMode: EnumTypeDefinition - TransferRequest: MessageTypeDefinition - TransferResponse: MessageTypeDefinition } } diff --git a/src/cli.ts b/src/cli.ts index 0ff1ba6..02823d3 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { Command } from 'commander'; -import { startBrokerCommand } from '../commands/start-broker'; +import { startBrokerCommand } from './commands/start-broker'; const program = new Command(); @@ -10,12 +10,28 @@ program .description('CLI to start the CEXBroker service') .requiredOption('-p, --policy ', 'Policy JSON file') .option('--port ', 'Port number (default: 8086)', '8086') + .option('--whitelist ', 'IPv4 address whitelist (space-separated list)') .action(async (options) => { try { + // Optional: Validate IPv4 addresses + if (options.whitelist) { + const isValidIPv4 = (ip: string) => + /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && + ip.split('.').every(part => Number(part) >= 0 && Number(part) <= 255); + + for (const ip of options.whitelist) { + if (!isValidIPv4(ip)) { + console.error(`❌ Invalid IPv4 address: ${ip}`); + process.exit(1); + } + } + } + await startBrokerCommand( options.policy, - parseInt(options.port, 10) - ); + parseInt(options.port, 10), + options.whitelist ?? [] // Pass whitelist to your command + ); } catch (err) { console.error('❌ Failed to start broker:', err); process.exit(1); diff --git a/src/client.dev.ts b/src/client.dev.ts index 026d7a6..1fae9fb 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -16,6 +16,10 @@ const client = new grpcObj.cexBroker.CexService( grpc.credentials.createInsecure(), ); +const metadata = new grpc.Metadata(); +metadata.add('authorization', 'Bearer your_token_here'); // Example header +metadata.add('custom-header', 'custom_value'); + const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 5); client.waitForReady(deadline, (err) => { @@ -27,7 +31,7 @@ client.waitForReady(deadline, (err) => { }); function onClientReady() { - client.getBalance({ cex: "bybit", token: "USDT" }, (err, result) => { + client.executeCcxtAction({ cex: "bybit", token: "USDT" },metadata, (err, result) => { if (err) { console.error({ err }); return; diff --git a/commands/start-broker.ts b/src/commands/start-broker.ts similarity index 65% rename from commands/start-broker.ts rename to src/commands/start-broker.ts index e94458c..9c3d858 100644 --- a/commands/start-broker.ts +++ b/src/commands/start-broker.ts @@ -3,8 +3,8 @@ import CEXBroker from '../index'; /** * CLI Command wrapper to start the CEXBroker */ -export async function startBrokerCommand(policyPath: string, port: number) { - const broker = new CEXBroker({}, policyPath, { port }); +export async function startBrokerCommand(policyPath: string, port: number,whitelistIps: string[]) { + const broker = new CEXBroker({}, policyPath, { port,whitelistIps }); broker.loadEnvConfig(); await broker.run(); } diff --git a/helpers/index.test.ts b/src/helpers/index.test.ts similarity index 100% rename from helpers/index.test.ts rename to src/helpers/index.test.ts diff --git a/helpers/index.ts b/src/helpers/index.ts similarity index 93% rename from helpers/index.ts rename to src/helpers/index.ts index b7592eb..7cb35b8 100644 --- a/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,25 +1,8 @@ import type { Exchange } from "ccxt"; -import type { PolicyConfig } from "../types"; +import type { PolicyConfig } from "../../types"; import fs from "fs"; import Joi from "joi"; -/** - * Fetches the order book, computes the worst‐case fill price for `size`, - * and submits a single limit‐buy at that price. - */ - -// IP Whitelist configuration -const ALLOWED_IPS = [ - "127.0.0.1", // localhost - "::1", // IPv6 localhost - // Add your allowed IP addresses here - // TODO: Allowed IPs should be loaded via .env. Local ips are fine to keep hardcoded. -]; - -export function isIpAllowed(ip: string): boolean { - return ALLOWED_IPS.includes(ip); -} - // TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." export async function buyAtOptimalPrice( exchange: Exchange, diff --git a/src/index.ts b/src/index.ts index 874f05a..e8de68a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,15 @@ import ccxt, { type Exchange } from "ccxt"; -import path from "path"; import * as grpc from "@grpc/grpc-js"; -import * as protoLoader from "@grpc/proto-loader"; -import type { ProtoGrpcType } from "../proto/node"; -import { - loadPolicy, - validateOrder, - validateWithdraw, - validateDeposit, - isIpAllowed, -} from "../helpers"; -import type { BalanceRequest } from "../proto/cexBroker/BalanceRequest"; -import type { BalanceResponse } from "../proto/cexBroker/BalanceResponse"; +import { watchFile, unwatchFile } from "fs"; +import Joi from "joi"; +import { loadPolicy } from "./helpers"; import { BrokerList, type BrokerCredentials, type ExchangeCredentials, type PolicyConfig, } from "../types"; -import type { TransferRequest } from "../proto/cexBroker/TransferRequest"; -import type { TransferResponse } from "../proto/cexBroker/TransferResponse"; -import type { DepositConfirmationRequest } from "../proto/cexBroker/DepositConfirmationRequest"; -import type { DepositConfirmationResponse } from "../proto/cexBroker/DepositConfirmationResponse"; -import type { ConvertRequest } from "../proto/cexBroker/ConvertRequest"; -import type { ConvertResponse } from "../proto/cexBroker/ConvertResponse"; -import type { OrderDetailsRequest } from "../proto/cexBroker/OrderDetailsRequest"; -import type { OrderDetailsResponse } from "../proto/cexBroker/OrderDetailsResponse"; -import type { CancelOrderRequest } from "../proto/cexBroker/CancelOrderRequest"; -import type { CancelOrderResponse } from "../proto/cexBroker/CancelOrderResponse"; -import { watchFile, unwatchFile } from "fs"; -import Joi from "joi"; - -const PROTO_FILE = "../proto/node.proto"; - -const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); -const grpcObj = grpc.loadPackageDefinition( - packageDef, -) as unknown as ProtoGrpcType; -const fietCexNode = grpcObj.cexBroker; +import { getServer } from "./server"; console.log("CCXT Version:", ccxt.version); @@ -47,6 +19,11 @@ export default class CEXBroker { port = 8086; private policy: PolicyConfig; private brokers: Record = {}; + private whitelistIps: string[] = [ + "127.0.0.1", // localhost + "::1", // IPv6 localhost + ]; + private server: grpc.Server | null = null; /** @@ -156,7 +133,7 @@ export default class CEXBroker { constructor( apiCredentials: ExchangeCredentials, policies: string | PolicyConfig, - config?: { port: number }, + config?: { port: number; whitelistIps: string[] }, ) { if (typeof policies === "string") { this.#policyFilePath = policies; @@ -172,6 +149,10 @@ export default class CEXBroker { } this.loadExchangeCredentials(apiCredentials); + this.whitelistIps = [ + ...this.whitelistIps, + ...(config ?? { whitelistIps: [] }).whitelistIps, + ]; } /** @@ -217,7 +198,7 @@ export default class CEXBroker { await this.server.forceShutdown(); } console.log(`Running CEXBroker at ${new Date().toISOString()}`); - this.server = getServer(this.policy, this.brokers); + this.server = getServer(this.policy, this.brokers, this.whitelistIps); this.server.bindAsync( `0.0.0.0:${this.port}`, @@ -233,480 +214,3 @@ export default class CEXBroker { return this; } } - -function authenticateRequest(call: grpc.ServerUnaryCall): boolean { - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !isIpAllowed(clientIp)) { - console.warn( - `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, - ); - return false; - } - return true; -} - -function getServer(policy: PolicyConfig, brokers: Record) { - const server = new grpc.Server(); - server.addService(fietCexNode.CexService.service, { - // TODO: Consolidate all of these calls into "ExecuteAction", "SubscribeToStream"... - Deposit: async ( - call: grpc.ServerUnaryCall< - DepositConfirmationRequest, - DepositConfirmationResponse - >, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement deposit logic - const { chain, recipientAddress, amount, transactionHash } = call.request; - - // Validate required fields - if (!chain || !amount || !recipientAddress || !transactionHash) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "chain, transactionHash, recipientAddress and amount are required", - }, - null, - ); - } - - // Validate against policy - - // TODO: I recognise that deposit/withdraw, etc. will need additional considerations as we validate against the policy... - // TODO: Therefore, either we can keep the standalone Deposit/Withdraw Methods, or we check the "ExecuteAction" method for the "deposit"/"withdraw"/"convert"/"cancelOrder"/"getOrderDetails"/"getBalance" actions... to determine extra validation. - const validation = validateDeposit(policy, chain, Number(amount)); - if (!validation.valid) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: validation.error, - }, - null, - ); - } - - // TODO: Where is CCXT used here? - try { - console.log( - `[${new Date().toISOString()}] ` + - `Amount ${amount} at ${transactionHash} on chain ${chain}. Paid to ${recipientAddress}`, - ); - callback(null, { newBalance: 0 }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Deposit confirmation failed", - }, - null, - ); - } - }, - Transfer: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement transfer logic - const { chain, cex, amount, recipientAddress, token } = call.request; - - // Validate required fields - if (!chain || !recipientAddress || !amount || !cex || !token) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "chain, recipient_address, amount, and ticker are required", - }, - null, - ); - } - - // Validate against policy - const validation = validateWithdraw( - policy, - chain, - recipientAddress, - Number(amount), - token, - ); - if (!validation.valid) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: validation.error, - }, - null, - ); - } - - try { - if (!Object.keys(brokers).includes(cex)) { - return callback( - { - code: grpc.status.INTERNAL, - message: `Broker ${cex} is not active. Allowed Broker: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const data = await broker.fetchCurrencies("USDT"); - const networks = Object.keys( - (data[token] ?? { networks: [] }).networks, - ); - - if (!networks.includes(chain)) { - return callback( - { - code: grpc.status.INTERNAL, - message: `Broker ${cex} doesnt support this ${chain} for token ${token}`, - }, - null, - ); - } - - // TODO: My point is why can this not be agnostic to the CEX... - const transaction = await broker.withdraw( - token, - Number(amount), - recipientAddress, - undefined, - { network: chain }, - ); - - callback(null, { success: true, transactionId: transaction.id }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Transfer failed", - }, - null, - ); - } - }, - - // TODO: "Convert" and "createLimitOrder" are too extremely different things... - // TODO: "Convert" is a generic action that can be used to convert any token to any other token... - // TODO: "createLimitOrder" is a specific action that can be used to create a limit order on a specific CEX... - // TODO: "Convert" could be "createMarketOrder"... a differnt thing to "createLimitOrder"... - Convert: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - // Implement convert logic - const { fromToken, toToken, amount, cex, price } = call.request; - - // Validate required fields - if (!fromToken || !toToken || !amount || !cex || !price) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "toToken, fromToken, amount, cex, and price are required", - }, - null, - ); - } - - const validation = validateOrder( - policy, - fromToken, - toToken, - Number(amount), - cex, - ); - if (!validation.valid) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: validation.error, - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - - const market = policy.order.rule.markets.find( - (market) => - market.includes(`${fromToken}/${toToken}`) || - market.includes(`${toToken}/${fromToken}`), - ); - const symbol = market?.split(":")[1] ?? ""; - const [from, _to] = symbol.split("/"); - - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const order = await broker.createLimitOrder( - symbol, - from === fromToken ? "sell" : "buy", - Number(amount), - Number(price), - ); - - callback(null, { - orderId: order.id, - }); - } catch (error) { - console.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Conversion failed", - }, - null, - ); - } - }, - GetBalance: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { cex, token } = call.request as Required; - - // Validate required fields - if (!cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "cex_key is required", - }, - null, - ); - } - - if (!token) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "token is required", - }, - null, - ); - } - - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - try { - // Fetch balance from the specified CEX - const balance = (await broker.fetchFreeBalance()) as any; - const currencyBalance = balance[token]; - - callback(null, { - balance: currencyBalance || 0, - currency: token, - }); - } catch (error) { - console.error(`Error fetching balance from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch balance from ${cex}`, - }, - null, - ); - } - }, - GetOrderDetails: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { orderId, cex } = call.request; - - // Validate required fields - if (!orderId || !cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "order_id and cex are required", - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const orderDetails = await broker.fetchOrder(orderId); - - callback(null, { - orderId: orderDetails.id, - status: orderDetails.status, - originalAmount: orderDetails.amount, - filledAmount: orderDetails.filled, - symbol: orderDetails.symbol, - mode: orderDetails.side, - price: orderDetails.price, - }); - } catch (error) { - console.error(`Error fetching order details from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch order details from ${cex}`, - }, - null, - ); - } - }, - CancelOrder: async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - - const { orderId, cex } = call.request; - - // Validate required fields - if (!orderId || !cex) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: "order_id and cex are required", - }, - null, - ); - } - - try { - // Validate CEX key - const broker = brokers[cex as keyof typeof brokers]; - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const cancelledOrder: any = await broker.cancelOrder(orderId); - - callback(null, { - success: cancelledOrder.status === "canceled", - finalStatus: cancelledOrder.status, - }); - } catch (error) { - console.error(`Error cancelling order from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to cancel order from ${cex}`, - }, - null, - ); - } - }, - }); - return server; -} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..cbb34b6 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,404 @@ +import { + validateDeposit, + validateOrder, + validateWithdraw, +} from "./helpers"; +import type { PolicyConfig } from "../types"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import type { ProtoGrpcType } from "../proto/node"; +import path from "path"; +import type { Exchange } from "ccxt"; +import type { CcxtActionRequest, CcxtActionRequest__Output } from "../proto/cexBroker/CcxtActionRequest"; +import type { CcxtActionResponse } from "../proto/cexBroker/CcxtActionResponse"; +import { Action } from "../proto/cexBroker/Action"; +import Joi from "joi"; +import ccxt from "ccxt"; + + +const PROTO_FILE = "../proto/node.proto"; + +const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); +const grpcObj = grpc.loadPackageDefinition( + packageDef, +) as unknown as ProtoGrpcType; +const cexNode = grpcObj.cexBroker; + +function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIps: string[]): boolean { + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || whitelistIps.includes(clientIp)) { + console.warn( + `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, + ); + return false; + } + return true; +} + +function createBroker(cex: string, metadata: grpc.Metadata): Exchange { + const api_key = metadata.get('api-key'); + const api_secret = metadata.get('api-secret'); + const ExchangeClass = (ccxt as any)[cex]; + + return new ExchangeClass({ + apiKey: api_key[0], + secret: api_secret[0], + enableRateLimit: true, + defaultType: "spot", + }); +} + + +export function getServer(policy: PolicyConfig, brokers: Record, whitelistIps: string[]) { + const server = new grpc.Server(); + server.addService(cexNode.CexService.service, { + ExecuteCcxtAction: async ( + call: grpc.ServerUnaryCall< + CcxtActionRequest, + CcxtActionResponse + >, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call, whitelistIps)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + // Read incoming metadata + const metadata = call.metadata; + const { action, payload, cex, symbol } = call.request + // Validate required fields + if (!action || !cex || !symbol || !cex || !payload) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "action, cex, symbol, and cex are required", + }, + null, + ); + } + + + + const broker = brokers[cex as keyof typeof brokers] + ?? createBroker(cex, metadata); + + switch (action) { + case Action.Deposit: + const transactionSchema = Joi.object({ + recipientAddress: Joi.string() + .required(), + amount: Joi.number().positive().required(), // Must be a positive number + transactionHash: Joi.string().required() + }); + const { value, error } = transactionSchema.validate(call.request.payload) + if (error) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "ValidationError:" + error.message, + }, + null, + ); + } + // TODO: Where is CCXT used here? + try { + const deposits = await broker.fetchDeposits(symbol, 50) + const deposit = deposits.find(deposit => deposit.id == value.transactionHash || deposit.txid == value.transactionHash) + + if (deposit) { + console.log( + `[${new Date().toISOString()}] ` + + `Amount ${value.amount} at ${value.transactionHash} . Paid to ${value.recipientAddress}`, + ); + deposit.network + return callback(null, { result: JSON.stringify({ ...deposit }) }); + } + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } + break; + + case Action.Transfer: + const transferSchema = Joi.object({ + recipientAddress: Joi.string() + .required(), + amount: Joi.number().positive().required(), // Must be a positive number + chain: Joi.string().required() + }); + const { value: transferValue, error: transferError } = transferSchema.validate(call.request.payload) + if (transferError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "ValidationError:" + transferError.message, + }, + null, + ); + } + // Validate against policy + const transferValidation = validateWithdraw( + policy, + transferValue.chain, + transferValue.recipientAddress, + Number(transferValue.amount), + symbol, + ); + if (!transferValidation.valid) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: transferValidation.error, + }, + null, + ); + } + try { + const data = await broker.fetchCurrencies("USDT"); + const networks = Object.keys( + (data[symbol] ?? { networks: [] }).networks, + ); + + if (!networks.includes(transferValue.chain)) { + return callback( + { + code: grpc.status.INTERNAL, + message: `Broker ${cex} doesnt support this ${transferValue.chain} for token ${symbol}`, + }, + null, + ); + } + + // TODO: My point is why can this not be agnostic to the CEX... + const transaction = await broker.withdraw( + symbol, + Number(transferValue.amount), + transferValue.recipientAddress, + undefined, + { network: transferValue.chain }, + ); + + callback(null, { result: JSON.stringify({ ...transaction }) }); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Transfer failed", + }, + null, + ); + } + break; + + case Action.CreateOrder: + const createOrderSchema = Joi.object({ + amount: Joi.number().positive().required(), // Must be a positive number + fromToken: Joi.string().required(), + toToken: Joi.string().required(), + price: Joi.number().positive().required() + }); + const { value: orderValue, error: orderError } = createOrderSchema.validate(call.request.payload) + if (orderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "ValidationError:" + orderError.message, + }, + null, + ); + } + const validation = validateOrder( + policy, + orderValue.fromToken, + orderValue.toToken, + Number(orderValue.amount), + cex, + ); + if (!validation.valid) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: validation.error, + }, + null, + ); + } + + try { + + const market = policy.order.rule.markets.find( + (market) => + market.includes(`${orderValue.fromToken}/${orderValue.toToken}`) || + market.includes(`${orderValue.toToken}/${orderValue.fromToken}`), + ); + const symbol = market?.split(":")[1] ?? ""; + const [from, _to] = symbol.split("/"); + + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const order = await broker.createLimitOrder( + symbol, + from === orderValue.fromToken ? "sell" : "buy", + Number(orderValue.amount), + Number(orderValue.price), + ); + + callback(null, { result: JSON.stringify({ ...order }) }); + } catch (error) { + console.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Order Creation failed", + }, + null, + ); + } + + break; + + case Action.GetOrderDetails: + const getOrderSchema = Joi.object({ + orderId: Joi.string().required(), + }); + const { value: getOrderValue, error: getOrderError } = getOrderSchema.validate(call.request.payload) + // Validate required fields + if (getOrderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "ValidationError:" + getOrderError.message, + }, + null, + ); + } + + try { + // Validate CEX key + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const orderDetails = await broker.fetchOrder(getOrderValue.orderId); + + callback(null, { + result: JSON.stringify({ + orderId: orderDetails.id, + status: orderDetails.status, + originalAmount: orderDetails.amount, + filledAmount: orderDetails.filled, + symbol: orderDetails.symbol, + mode: orderDetails.side, + price: orderDetails.price, + }) + }); + } catch (error) { + console.error(`Error fetching order details from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch order details from ${cex}`, + }, + null, + ); + } + break; + case Action.CancelOrder: + const cancelOrderSchema = Joi.object({ + orderId: Joi.string().required(), + }); + const { value: cancelOrderValue, error: cancelOrderError } = cancelOrderSchema.validate(call.request.payload) + // Validate required fields + if (cancelOrderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "ValidationError:" + cancelOrderError.message, + }, + null, + ); + } + + const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId); + + callback(null, { + result:JSON.stringify({...cancelledOrder}) + }); + break; + case Action.FetchBalance: + try { + // Fetch balance from the specified CEX + const balance = (await broker.fetchFreeBalance()) as any; + const currencyBalance = balance[symbol]; + + callback(null, { + result: JSON.stringify({ + balance: currencyBalance || 0, + currency: symbol + }) + }); + } catch (error) { + console.error(`Error fetching balance from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch balance from ${cex}`, + }, + null, + ); + } + break; + + default: + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: + "Invalid Action", + }, + ) + } + }, + }); + return server; +} diff --git a/test/index.test.ts b/test/index.test.ts index 35e0e07..da8aa32 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,33 +1,6 @@ import { describe, test, expect } from "bun:test"; -import { isIpAllowed } from "../helpers"; describe("RPC Server Logic Tests", () => { - describe("IP Authentication", () => { - test("should allow requests from whitelisted IPs", () => { - const allowedIPs = ["127.0.0.1"]; - - allowedIPs.forEach((ip) => { - const clientIp = ip; - const isAllowed = allowedIPs.includes(clientIp); - - expect(isAllowed).toBe(true); - }); - }); - - test("should block requests from unauthorized IPs", () => { - const clientIp = "192.168.1.100"; - const isAllowed = isIpAllowed(clientIp); - - expect(isAllowed).toBe(false); - }); - - test("should handle undefined peer information", () => { - const clientIp = ""; - const isAllowed = isIpAllowed(clientIp); - - expect(isAllowed).toBe(false); - }); - }); describe("GetOptimalPrice Validation", () => { test("should validate required fields correctly", () => { diff --git a/test/integration.test.ts b/test/integration.test.ts index 67d4f5c..e4d8030 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -41,8 +41,8 @@ describe("Integration Tests", () => { describe("Helper Functions Integration", () => { test("should validate withdraw policy correctly", () => { - const { validateWithdraw } = require("../helpers"); - const { loadPolicy } = require("../helpers"); + const { validateWithdraw } = require("../src/helpers"); + const { loadPolicy } = require("../src/helpers"); const policy = loadPolicy("./policy/policy.json"); @@ -70,8 +70,8 @@ describe("Integration Tests", () => { }); test("should validate order policy correctly", () => { - const { validateOrder } = require("../helpers"); - const { loadPolicy } = require("../helpers"); + const { validateOrder } = require("../src/helpers"); + const { loadPolicy } = require("../src/helpers"); const policy = loadPolicy("./policy/policy.json"); @@ -89,7 +89,7 @@ describe("Integration Tests", () => { describe("Price Calculation Integration", () => { test("should calculate optimal prices correctly", async () => { - const { buyAtOptimalPrice, sellAtOptimalPrice } = require("../helpers"); + const { buyAtOptimalPrice, sellAtOptimalPrice } = require("../src/helpers"); // Create a mock exchange with realistic order book data const mockExchange = { @@ -125,7 +125,7 @@ describe("Integration Tests", () => { describe("Error Handling Integration", () => { test("should handle insufficient depth correctly", async () => { - const { buyAtOptimalPrice } = require("../helpers"); + const { buyAtOptimalPrice } = require("../src/helpers"); const insufficientExchange = { fetchOrderBook: async () => ({ @@ -139,8 +139,8 @@ describe("Integration Tests", () => { }); test("should handle invalid symbol format", () => { - const { validateOrder } = require("../helpers"); - const { loadPolicy } = require("../helpers"); + const { validateOrder } = require("../src/helpers"); + const { loadPolicy } = require("../src/helpers"); const policy = loadPolicy("./policy/policy.json"); From a98dbbfd7ad830b62a62279ce749a9bec4a4e7a5 Mon Sep 17 00:00:00 2001 From: xlassix Date: Fri, 11 Jul 2025 22:26:06 +0100 Subject: [PATCH 16/45] Add dotenv for env vars & improve API key handling --- bun.lock | 3 +++ package.json | 1 + src/client.dev.ts | 10 +++++++--- src/index.ts | 2 +- src/server.ts | 21 ++++++++++++++++++--- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 79dc80b..9d05311 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@types/bun": "latest", "bun-plugin-dts": "latest", "bun-types": "latest", + "dotenv": "^17.2.0", "husky": "^9.1.7", }, "peerDependencies": { @@ -108,6 +109,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "dotenv": ["dotenv@17.2.0", "", {}, "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ=="], + "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/package.json b/package.json index a5f1cc5..3b7c8dd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/bun": "latest", "bun-plugin-dts": "latest", "bun-types": "latest", + "dotenv": "^17.2.0", "husky": "^9.1.7" }, "files": [ diff --git a/src/client.dev.ts b/src/client.dev.ts index 1fae9fb..0d2fb35 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -2,6 +2,8 @@ import path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; +import { Action } from "../proto/cexBroker/Action"; +import {config} from "dotenv" const PROTO_FILE = "../proto/node.proto"; const port= 8086 @@ -16,9 +18,11 @@ const client = new grpcObj.cexBroker.CexService( grpc.credentials.createInsecure(), ); +config() + const metadata = new grpc.Metadata(); -metadata.add('authorization', 'Bearer your_token_here'); // Example header -metadata.add('custom-header', 'custom_value'); +metadata.add('api-key', process.env.BYBIT_API_KEY??""); // Example header +metadata.add('api-secret', process.env.BYBIT_API_SECRET??""); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 5); @@ -31,7 +35,7 @@ client.waitForReady(deadline, (err) => { }); function onClientReady() { - client.executeCcxtAction({ cex: "bybit", token: "USDT" },metadata, (err, result) => { + client.executeCcxtAction({ cex: "bybit", symbol: "USDT",payload:{},action: Action.FetchBalance },metadata, (err, result) => { if (err) { console.error({ err }); return; diff --git a/src/index.ts b/src/index.ts index e8de68a..e1aad0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -150,8 +150,8 @@ export default class CEXBroker { this.loadExchangeCredentials(apiCredentials); this.whitelistIps = [ - ...this.whitelistIps, ...(config ?? { whitelistIps: [] }).whitelistIps, + ...this.whitelistIps, ]; } diff --git a/src/server.ts b/src/server.ts index cbb34b6..a3b1ebd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -26,7 +26,7 @@ const cexNode = grpcObj.cexBroker; function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIps: string[]): boolean { const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || whitelistIps.includes(clientIp)) { + if (!clientIp || !whitelistIps.includes(clientIp)) { console.warn( `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, ); @@ -35,11 +35,16 @@ function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIp return true; } -function createBroker(cex: string, metadata: grpc.Metadata): Exchange { +function createBroker(cex: string, metadata: grpc.Metadata): Exchange |null{ const api_key = metadata.get('api-key'); const api_secret = metadata.get('api-secret'); const ExchangeClass = (ccxt as any)[cex]; + metadata.remove('api-key'); + metadata.remove('api-secret'); + if (api_secret.length==0 || api_key.length==0){ + return null + } return new ExchangeClass({ apiKey: api_key[0], secret: api_secret[0], @@ -73,7 +78,7 @@ export function getServer(policy: PolicyConfig, brokers: Record Date: Fri, 11 Jul 2025 22:39:43 +0100 Subject: [PATCH 17/45] Add tslog for structured logging --- bun.lock | 3 +++ package.json | 3 ++- src/client.dev.ts | 7 ++++--- src/helpers/index.ts | 5 +++-- src/helpers/logger.ts | 8 ++++++++ src/index.ts | 27 ++++++++++++++------------- src/server.ts | 5 +++-- 7 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 src/helpers/logger.ts diff --git a/bun.lock b/bun.lock index 9d05311..dc290fc 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "ccxt": "^4.4.91", "commander": "^14.0.0", "joi": "^17.13.3", + "tslog": "^4.9.3", }, "devDependencies": { "@biomejs/biome": "2.0.6", @@ -141,6 +142,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "tslog": ["tslog@4.9.3", "", {}, "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], diff --git a/package.json b/package.json index 3b7c8dd..5f6e93b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "@grpc/proto-loader": "^0.7.15", "ccxt": "^4.4.91", "commander": "^14.0.0", - "joi": "^17.13.3" + "joi": "^17.13.3", + "tslog": "^4.9.3" }, "publishConfig": { "registry": "https://registry.npmjs.org" diff --git a/src/client.dev.ts b/src/client.dev.ts index 0d2fb35..bcfc990 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -4,6 +4,7 @@ import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import { Action } from "../proto/cexBroker/Action"; import {config} from "dotenv" +import { log } from "./helpers/logger"; const PROTO_FILE = "../proto/node.proto"; const port= 8086 @@ -28,7 +29,7 @@ const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 5); client.waitForReady(deadline, (err) => { if (err) { - console.error(err); + log.error(err); return; } onClientReady(); @@ -37,10 +38,10 @@ client.waitForReady(deadline, (err) => { function onClientReady() { client.executeCcxtAction({ cex: "bybit", symbol: "USDT",payload:{},action: Action.FetchBalance },metadata, (err, result) => { if (err) { - console.error({ err }); + log.error({ err }); return; } - console.log({ x: result }); + log.info({ x: result }); }); } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 7cb35b8..998740a 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -2,6 +2,7 @@ import type { Exchange } from "ccxt"; import type { PolicyConfig } from "../../types"; import fs from "fs"; import Joi from "joi"; +import { log } from "./logger"; // TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." export async function buyAtOptimalPrice( @@ -38,7 +39,7 @@ export async function buyAtOptimalPrice( } const avgPrice = cumCost / size; - console.log( + log.info( `[${new Date().toISOString()}] ` + `Will buy ${size} ${symbol.split("/")[0]} at limit ${fillPrice.toFixed(6)} ` + `(VWAP ≃ ${avgPrice.toFixed(6)})`, @@ -93,7 +94,7 @@ export async function sellAtOptimalPrice( } const avgPrice = cumProceeds / size; - console.log( + log.info( `[${new Date().toISOString()}] ` + `Will sell ${size} ${symbol.split("/")[0]} at limit ${fillPrice.toFixed(6)} ` + `(VWAP ≃ ${avgPrice.toFixed(6)})`, diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts new file mode 100644 index 0000000..b4a1996 --- /dev/null +++ b/src/helpers/logger.ts @@ -0,0 +1,8 @@ +import { Logger } from "tslog"; + +const log = new Logger({ + type: process.env.NODE_ENV === "production" ? "json" : "pretty", + stylePrettyLogs: false, +}); + +export { log }; diff --git a/src/index.ts b/src/index.ts index e1aad0a..94df1b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,8 +10,9 @@ import { type PolicyConfig, } from "../types"; import { getServer } from "./server"; +import { log } from "./helpers/logger"; -console.log("CCXT Version:", ccxt.version); +log("CCXT Version:", ccxt.version); export default class CEXBroker { #brokerConfig: Record = {}; @@ -33,7 +34,7 @@ export default class CEXBroker { * CEX_BROKER__API_SECRET */ public loadEnvConfig(): void { - console.log("🔧 Loading CEX_BROKER_ environment variables:"); + log("🔧 Loading CEX_BROKER_ environment variables:"); const configMap: Record> = {}; for (const [key, value] of Object.entries(process.env)) { @@ -41,7 +42,7 @@ export default class CEXBroker { const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); if (!match) { - console.warn(`⚠️ Skipping unrecognized env var: ${key}`); + warn(`⚠️ Skipping unrecognized env var: ${key}`); continue; } @@ -60,7 +61,7 @@ export default class CEXBroker { } if (Object.keys(configMap).length === 0) { - console.error(`❌ NO CEX Broker Key Found`); + error(`❌ NO CEX Broker Key Found`); } // Finalize config and print result per broker @@ -73,7 +74,7 @@ export default class CEXBroker { apiKey: creds.apiKey ?? "", apiSecret: creds.apiSecret ?? "", }; - console.log(`✅ Loaded credentials for broker "${broker}"`); + log(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -86,7 +87,7 @@ export default class CEXBroker { const missing = []; if (!hasKey) missing.push("API_KEY"); if (!hasSecret) missing.push("API_SECRET"); - console.warn( + warn( `❌ Missing ${missing.join(" and ")} for broker "${broker}"`, ); } @@ -118,7 +119,7 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - console.log(`✅ Loaded credentials for broker "${broker}"`); + log(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -165,13 +166,13 @@ export default class CEXBroker { try { const updated = loadPolicy(filePath); this.policy = updated; - console.log( + log.info( `Policies reloaded from ${filePath} at ${new Date().toISOString()}`, ); // Rerun broker with updated policies this.run(); } catch (err) { - console.error(`Error reloading policies: ${err}`); + log.error(`Error reloading policies: ${err}`); } } }); @@ -183,7 +184,7 @@ export default class CEXBroker { public stop(): void { if (this.#policyFilePath) { unwatchFile(this.#policyFilePath); - console.log(`Stopped watching policy file: ${this.#policyFilePath}`); + log.info(`Stopped watching policy file: ${this.#policyFilePath}`); } if (this.server) { this.server.forceShutdown(); @@ -197,7 +198,7 @@ export default class CEXBroker { if (this.server) { await this.server.forceShutdown(); } - console.log(`Running CEXBroker at ${new Date().toISOString()}`); + log.info(`Running CEXBroker at ${new Date().toISOString()}`); this.server = getServer(this.policy, this.brokers, this.whitelistIps); this.server.bindAsync( @@ -205,10 +206,10 @@ export default class CEXBroker { grpc.ServerCredentials.createInsecure(), (err, port) => { if (err) { - console.error(err); + log.error(err); return; } - console.log(`Your server as started on port ${port}`); + log.info(`Your server as started on port ${port}`); }, ); return this; diff --git a/src/server.ts b/src/server.ts index a3b1ebd..86c335d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ import type { CcxtActionResponse } from "../proto/cexBroker/CcxtActionResponse"; import { Action } from "../proto/cexBroker/Action"; import Joi from "joi"; import ccxt from "ccxt"; +import { log } from "./helpers/logger"; const PROTO_FILE = "../proto/node.proto"; @@ -129,7 +130,7 @@ export function getServer(policy: PolicyConfig, brokers: Record deposit.id == value.transactionHash || deposit.txid == value.transactionHash) if (deposit) { - console.log( + log.info( `[${new Date().toISOString()}] ` + `Amount ${value.amount} at ${value.transactionHash} . Paid to ${value.recipientAddress}`, ); @@ -144,7 +145,7 @@ export function getServer(policy: PolicyConfig, brokers: Record Date: Fri, 11 Jul 2025 22:44:14 +0100 Subject: [PATCH 18/45] Refactor JSON includes and format code --- biome.json | 9 +++++- src/helpers/index.ts | 70 +++++++++++++++++++++---------------------- src/helpers/logger.ts | 4 +-- src/index.ts | 4 +-- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/biome.json b/biome.json index dcb6f5f..afa7be1 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,14 @@ }, "files": { "ignoreUnknown": true, - "includes": ["proto/", "src/index.ts", "helpers/", "config/", "policy/"] + "includes": [ + "proto/", + "src/index.ts", + "src/helpers/*.ts", + "config/", + "./src/commands", + "policy/" + ] }, "formatter": { "enabled": true, diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 998740a..3cb5f20 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -112,58 +112,58 @@ export function loadPolicy(policyPath: string): PolicyConfig { // Joi schema for WithdrawRule const withdrawRuleSchema = Joi.object({ - networks: Joi.array().items(Joi.string()).required(), - whitelist: Joi.array().items(Joi.string()).required(), - amounts: Joi.array() - .items( - Joi.object({ - ticker: Joi.string().required(), - max: Joi.number().required(), - min: Joi.number().required(), - }) - ) - .required(), + networks: Joi.array().items(Joi.string()).required(), + whitelist: Joi.array().items(Joi.string()).required(), + amounts: Joi.array() + .items( + Joi.object({ + ticker: Joi.string().required(), + max: Joi.number().required(), + min: Joi.number().required(), + }), + ) + .required(), }); // Joi schema for OrderRule const orderRuleSchema = Joi.object({ - markets: Joi.array().items(Joi.string()).required(), - limits: Joi.array() - .items( - Joi.object({ - from: Joi.string().required(), - to: Joi.string().required(), - min: Joi.number().required(), - max: Joi.number().required(), - }) - ) - .required(), + markets: Joi.array().items(Joi.string()).required(), + limits: Joi.array() + .items( + Joi.object({ + from: Joi.string().required(), + to: Joi.string().required(), + min: Joi.number().required(), + max: Joi.number().required(), + }), + ) + .required(), }); // Full PolicyConfig schema const policyConfigSchema = Joi.object({ - withdraw: Joi.object({ - rule: withdrawRuleSchema.required(), - }).required(), + withdraw: Joi.object({ + rule: withdrawRuleSchema.required(), + }).required(), - deposit: Joi.object() - .pattern(Joi.string(), Joi.valid(null)) // Record - .required(), + deposit: Joi.object() + .pattern(Joi.string(), Joi.valid(null)) // Record + .required(), - order: Joi.object({ - rule: orderRuleSchema.required(), - }).required(), + order: Joi.object({ + rule: orderRuleSchema.required(), + }).required(), }); - const { error, value } = policyConfigSchema.validate(JSON.parse(policyData)); + const { error, value } = policyConfigSchema.validate( + JSON.parse(policyData), + ); if (error) { - console.error('Validation failed:', error.details); + console.error("Validation failed:", error.details); } return value as PolicyConfig; - - } catch (error) { console.error("Failed to load policy:", error); throw new Error("Policy configuration could not be loaded"); diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index b4a1996..8429e66 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -1,8 +1,8 @@ import { Logger } from "tslog"; const log = new Logger({ - type: process.env.NODE_ENV === "production" ? "json" : "pretty", - stylePrettyLogs: false, + type: process.env.NODE_ENV === "production" ? "json" : "pretty", + stylePrettyLogs: false, }); export { log }; diff --git a/src/index.ts b/src/index.ts index 94df1b4..e2ec27d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,9 +87,7 @@ export default class CEXBroker { const missing = []; if (!hasKey) missing.push("API_KEY"); if (!hasSecret) missing.push("API_SECRET"); - warn( - `❌ Missing ${missing.join(" and ")} for broker "${broker}"`, - ); + warn(`❌ Missing ${missing.join(" and ")} for broker "${broker}"`); } } } From 332649259ec31ccc0bb4ad9dae72fb7aeaaa3ba7 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sat, 12 Jul 2025 09:26:09 +0100 Subject: [PATCH 19/45] Remove proto files and update .gitignore --- .gitignore | 3 ++- proto/cexBroker/Action.ts | 29 --------------------------- proto/cexBroker/CcxtActionRequest.ts | 17 ---------------- proto/cexBroker/CcxtActionResponse.ts | 10 --------- proto/cexBroker/CexService.ts | 27 ------------------------- 5 files changed, 2 insertions(+), 84 deletions(-) delete mode 100644 proto/cexBroker/Action.ts delete mode 100644 proto/cexBroker/CcxtActionRequest.ts delete mode 100644 proto/cexBroker/CcxtActionResponse.ts delete mode 100644 proto/cexBroker/CexService.ts diff --git a/.gitignore b/.gitignore index 9e539af..f6f5265 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -build \ No newline at end of file +build +proto/**git \ No newline at end of file diff --git a/proto/cexBroker/Action.ts b/proto/cexBroker/Action.ts deleted file mode 100644 index 7ff5682..0000000 --- a/proto/cexBroker/Action.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Original file: proto/node.proto - -export const Action = { - NoAction: 0, - Deposit: 1, - Transfer: 2, - CreateOrder: 3, - GetOrderDetails: 4, - CancelOrder: 5, - FetchBalance: 6, -} as const; - -export type Action = - | 'NoAction' - | 0 - | 'Deposit' - | 1 - | 'Transfer' - | 2 - | 'CreateOrder' - | 3 - | 'GetOrderDetails' - | 4 - | 'CancelOrder' - | 5 - | 'FetchBalance' - | 6 - -export type Action__Output = typeof Action[keyof typeof Action] diff --git a/proto/cexBroker/CcxtActionRequest.ts b/proto/cexBroker/CcxtActionRequest.ts deleted file mode 100644 index aba0132..0000000 --- a/proto/cexBroker/CcxtActionRequest.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Original file: proto/node.proto - -import type { Action as _cexBroker_Action, Action__Output as _cexBroker_Action__Output } from '../cexBroker/Action'; - -export interface CcxtActionRequest { - 'action'?: (_cexBroker_Action); - 'payload'?: ({[key: string]: string}); - 'cex'?: (string); - 'symbol'?: (string); -} - -export interface CcxtActionRequest__Output { - 'action'?: (_cexBroker_Action__Output); - 'payload'?: ({[key: string]: string}); - 'cex'?: (string); - 'symbol'?: (string); -} diff --git a/proto/cexBroker/CcxtActionResponse.ts b/proto/cexBroker/CcxtActionResponse.ts deleted file mode 100644 index fb0915d..0000000 --- a/proto/cexBroker/CcxtActionResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Original file: proto/node.proto - - -export interface CcxtActionResponse { - 'result'?: (string); -} - -export interface CcxtActionResponse__Output { - 'result'?: (string); -} diff --git a/proto/cexBroker/CexService.ts b/proto/cexBroker/CexService.ts deleted file mode 100644 index 13ec422..0000000 --- a/proto/cexBroker/CexService.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Original file: proto/node.proto - -import type * as grpc from '@grpc/grpc-js' -import type { MethodDefinition } from '@grpc/proto-loader' -import type { CcxtActionRequest as _cexBroker_CcxtActionRequest, CcxtActionRequest__Output as _cexBroker_CcxtActionRequest__Output } from '../cexBroker/CcxtActionRequest'; -import type { CcxtActionResponse as _cexBroker_CcxtActionResponse, CcxtActionResponse__Output as _cexBroker_CcxtActionResponse__Output } from '../cexBroker/CcxtActionResponse'; - -export interface CexServiceClient extends grpc.Client { - ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - ExecuteCcxtAction(argument: _cexBroker_CcxtActionRequest, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - executeCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - executeCcxtAction(argument: _cexBroker_CcxtActionRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - executeCcxtAction(argument: _cexBroker_CcxtActionRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - executeCcxtAction(argument: _cexBroker_CcxtActionRequest, callback: grpc.requestCallback<_cexBroker_CcxtActionResponse__Output>): grpc.ClientUnaryCall; - -} - -export interface CexServiceHandlers extends grpc.UntypedServiceImplementation { - ExecuteCcxtAction: grpc.handleUnaryCall<_cexBroker_CcxtActionRequest__Output, _cexBroker_CcxtActionResponse>; - -} - -export interface CexServiceDefinition extends grpc.ServiceDefinition { - ExecuteCcxtAction: MethodDefinition<_cexBroker_CcxtActionRequest, _cexBroker_CcxtActionResponse, _cexBroker_CcxtActionRequest__Output, _cexBroker_CcxtActionResponse__Output> -} From 03000bf3c505da5b6ebd48f22544842eac3d2370 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 13 Jul 2025 09:19:17 +0100 Subject: [PATCH 20/45] Add exchange API keys and refactor README.md --- .env.sample | 9 +- README.md | 496 ++++++++++++++++++-------------------- proto/node.proto | 5 +- src/helpers/index.test.ts | 82 +------ src/helpers/index.ts | 101 -------- src/server.ts | 5 +- test/integration.test.ts | 70 ------ 7 files changed, 241 insertions(+), 527 deletions(-) diff --git a/.env.sample b/.env.sample index 06ca597..6504b24 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,8 @@ -# TODO: Always include an .env.sample when using dotenv... just good practice. It should always be up to date. \ No newline at end of file + +# Required for CLI and Optional for the Broker Library + +CEX_BROKER_BYBIT_API_KEY=*********************** +CEX_BROKER_BINANCE_API_KEY=**************************************** +CEX_BROKER_BYBIT_API_SECRET=*********************************************** +CEX_BROKER_BINANCE_API_SECRET=************************************************************ +PORT_NUM=8086 \ No newline at end of file diff --git a/README.md b/README.md index 51132d1..e56bab1 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,61 @@ # CEX Broker -A high-performance gRPC-based cryptocurrency exchange broker service that provides unified access to multiple centralized exchanges (CEX) including Binance and Bybit. Built with TypeScript, Bun, and CCXT for reliable trading operations. - -## Features - -- **Multi-Exchange Support**: Unified API to any CEX supported by [CCXT](https://github.com/ccxt/ccxt) -- **gRPC Interface**: High-performance RPC communication -- **Real-time Pricing**: Optimal price discovery across exchanges -- **Balance Management**: Real-time balance checking -- **Order Management**: Create, track, and cancel orders -- **Transfer Operations**: Withdraw funds to external addresses -- **Token Conversion**: Convert between different tokens -- **Policy Enforcement**: Configurable trading and withdrawal limits +A high-performance gRPC-based cryptocurrency exchange broker service that provides unified access to multiple centralized exchanges (CEX) through the CCXT library. Built with TypeScript, Bun, and designed for reliable trading operations with policy enforcement. + +## 🚀 Features + +- **Multi-Exchange Support**: Unified API to any CEX supported by [CCXT](https://github.com/ccxt/ccxt) (100+ exchanges) +- **gRPC Interface**: High-performance RPC communication with type safety +- **Policy Enforcement**: Configurable trading and withdrawal limits with real-time policy updates - **IP Authentication**: Security through IP whitelisting +- **Real-time Policy Updates**: Hot-reload policy changes without server restart - **Type Safety**: Full TypeScript support with generated protobuf types +- **Comprehensive Logging**: Built-in logging with tslog +- **CLI Support**: Command-line interface for easy management -## Prerequisites +## 📋 Prerequisites - [Bun](https://bun.sh) (v1.2.17 or higher) -- API keys for supported exchanges (Binance, Bybit) +- API keys for supported exchanges (e.g., Binance, Bybit, etc.) -## Installation +## 🛠️ Installation -1. Clone the repository: - -```bash -git clone -cd cex-broker -``` +1. **Clone the repository:** + ```bash + git clone + cd fietCexBroker + ``` -1. Install dependencies: - -```bash -bun install -``` +2. **Install dependencies:** + ```bash + bun install + ``` -1. Generate protobuf types: - -```bash -bun run proto-gen -``` +3. **Generate protobuf types:** + ```bash + bun run proto-gen + ``` -## Configuration +## ⚙️ Configuration ### Environment Variables -Create a `.env` file in the root directory with the following variables: +The broker loads configuration from environment variables with the `CEX_BROKER_` prefix: ```env # Server Configuration -PORT_NUM=8082 - -# Exchange API Keys -BINANCE_API_KEY=your_binance_api_key -BINANCE_API_SECRET=your_binance_api_secret -BYBIT_API_KEY=your_bybit_api_key -BYBIT_API_SECRET=your_bybit_api_secret - -# Supported Brokers (optional, defaults to BINANCE,BYBIT) -ROOCH_CHAIN_ID=BINANCE,BYBIT +PORT_NUM=8086 + +# Exchange API Keys (format: CEX_BROKER__API_KEY/SECRET) +CEX_BROKER_BINANCE_API_KEY=your_binance_api_key +CEX_BROKER_BINANCE_API_SECRET=your_binance_api_secret +CEX_BROKER_BYBIT_API_KEY=your_bybit_api_key +CEX_BROKER_BYBIT_API_SECRET=your_bybit_api_secret +CEX_BROKER_KRAKEN_API_KEY=your_kraken_api_key +CEX_BROKER_KRAKEN_API_SECRET=your_kraken_api_secret ``` -**Note**: API keys are only required for the exchanges you plan to use. The system will validate that required keys are provided based on the `ROOCH_CHAIN_ID` configuration. +**Note**: Only configure API keys for exchanges you plan to use. The system will automatically detect and initialize configured exchanges. ### Policy Configuration @@ -71,7 +65,7 @@ Configure trading policies in `policy/policy.json`: { "withdraw": { "rule": { - "networks": ["BEP20", "ARBITRUM"], + "networks": ["BEP20", "ARBITRUM", "ETHEREUM"], "whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"], "amounts": [ { @@ -107,16 +101,16 @@ Configure trading policies in `policy/policy.json`: } ``` -## Usage +## 🚀 Usage ### Starting the Server ```bash -# Development +# Development mode bun run start # Production build -bun run build +bun run build:ts bun run ./build/index.js ``` @@ -127,7 +121,7 @@ bun run ./build/index.js bun run start # Build for production -bun run build +bun run build:ts # Run tests bun test @@ -145,263 +139,202 @@ bun run lint bun run check ``` -## API Reference +## 📡 API Reference -The service exposes a gRPC interface with the following methods: +The service exposes a gRPC interface with the following method: -### GetOptimalPrice +### ExecuteCcxtAction -Get optimal buy/sell prices across supported exchanges. +Execute any CCXT method on supported exchanges. **Request:** - ```protobuf -message OptimalPriceRequest { - string symbol = 1; // Trading pair symbol, e.g. "ARB/USDT" - double quantity = 2; // Quantity to buy or sell - OrderMode mode = 3; // Buy (0) or Sell (1) mode +message CcxtActionRequest { + Action action = 1; // The CCXT method to call + map payload = 2; // Parameters to pass to the CCXT method + string cex = 3; // CEX identifier (e.g., "binance", "bybit") + string symbol = 4; // Optional: trading pair symbol if needed } ``` **Response:** - ```protobuf -message OptimalPriceResponse { - map results = 1; -} - -message PriceInfo { - double avgPrice = 1; // Volume-weighted average price - double fillPrice = 2; // Worst-case fill price +message CcxtActionResponse { + string result = 2; // JSON string of the result data } ``` -**Example:** +**Available Actions:** +- `NoAction` (0): No operation +- `Deposit` (1): Deposit funds +- `Transfer` (2): Transfer/withdraw funds +- `CreateOrder` (3): Create a new order +- `GetOrderDetails` (4): Get order information +- `CancelOrder` (5): Cancel an existing order +- `FetchBalance` (6): Get account balance + +**Example Usage:** ```typescript -const request = { - symbol: "ARB/USDT", - quantity: 100, - mode: 0 // BUY +// Fetch balance +const balanceRequest = { + action: 6, // FetchBalance + payload: {}, + cex: "binance", + symbol: "" }; -``` - -### GetBalance - -Get available balance for a specific currency on a specific exchange. - -**Request:** -```protobuf -message BalanceRequest { - string cex = 1; // CEX identifier (e.g., "BINANCE", "BYBIT") - string token = 2; // Token symbol, e.g. "USDT" -} +// Create order +const orderRequest = { + action: 3, // CreateOrder + payload: { + symbol: "BTC/USDT", + type: "limit", + side: "buy", + amount: "0.001", + price: "50000" + }, + cex: "binance", + symbol: "BTC/USDT" +}; ``` -**Response:** +## 🔒 Security -```protobuf -message BalanceResponse { - double balance = 1; // Available balance for the token - string currency = 2; // Currency of the balance -} -``` +### IP Authentication -**Example:** +All API calls require IP authentication. Configure allowed IPs in the broker initialization: ```typescript -const request = { - cex: "BINANCE", - token: "USDT" +const config = { + port: 8086, + whitelistIps: [ + "127.0.0.1", // localhost + "::1", // IPv6 localhost + "192.168.1.100", // Your allowed IP + ] }; ``` -### Deposit - -Confirm a deposit transaction. - -**Request:** - -```protobuf -message DepositConfirmationRequest { - string chain = 1; - string recipient_address = 2; - double amount = 3; - string transaction_hash = 4; -} -``` - -**Response:** - -```protobuf -message DepositConfirmationResponse { - double newBalance = 1; -} -``` +### API Key Management -### Transfer +- Store API keys securely in environment variables +- Use read-only API keys when possible +- Regularly rotate API keys +- Monitor API usage and set appropriate rate limits -Execute a transfer/withdrawal to an external address. +## 🏗️ Architecture -**Request:** +### Project Structure -```protobuf -message TransferRequest { - string chain = 1; // Network chain (e.g., "ARBITRUM", "BEP20") - string recipient_address = 2; // Destination address - double amount = 3; // Amount to transfer - string cex = 4; // CEX identifier - string token = 5; // Token symbol -} ``` - -**Response:** - -```protobuf -message TransferResponse { - bool success = 1; - string transaction_id = 2; -} +fietCexBroker/ +├── src/ # Source code +│ ├── commands/ # CLI commands +│ ├── helpers/ # Utility functions +│ ├── index.ts # Main broker class +│ ├── server.ts # gRPC server implementation +│ └── cli.ts # CLI entry point +├── proto/ # Protocol buffer definitions +│ ├── node.proto # Service definition +│ └── node.ts # Type exports +├── policy/ # Policy configuration +│ └── policy.json # Trading and withdrawal rules +├── scripts/ # Build scripts +├── test/ # Test files +├── types.ts # TypeScript type definitions +├── build.ts # Build configuration +└── package.json # Dependencies and scripts ``` -### Convert - -Convert between different tokens using limit orders. +### Core Components -**Request:** +- **CEXBroker**: Main broker class that manages exchange connections and policy enforcement +- **Policy System**: Real-time policy validation and enforcement +- **gRPC Server**: High-performance RPC interface +- **CCXT Integration**: Unified access to 100+ cryptocurrency exchanges -```protobuf -message ConvertRequest { - string from_token = 1; // Source token - string to_token = 2; // Destination token - double amount = 3; // Amount to convert - double price = 4; // Limit price - string cex = 5; // CEX identifier -} -``` +## 🧪 Development -**Response:** +### Adding New Exchanges -```protobuf -message ConvertResponse { - string order_id = 3; -} -``` +The broker automatically supports all exchanges available in CCXT. To add a new exchange: -### GetOrderDetails +1. Add your API credentials to environment variables: + ```env + CEX_BROKER__API_KEY=your_api_key + CEX_BROKER__API_SECRET=your_api_secret + ``` -Get details of a specific order. +2. Update policy configuration if needed for the new exchange -**Request:** +3. The broker will automatically detect and initialize the exchange -```protobuf -message OrderDetailsRequest { - string order_id = 1; // Unique order identifier - string cex = 2; // CEX identifier -} -``` +### Querying Supported Networks -**Response:** +To understand which networks each exchange supports for deposits and withdrawals, you can query the exchange's currency information: -```protobuf -message OrderDetailsResponse { - string order_id = 1; // Unique order identifier - string status = 2; // Current order status - double original_amount = 3; // Original order amount - double filled_amount = 4; // Amount that has been filled - string symbol = 5; // Trading pair symbol - string mode = 6; // Buy or Sell mode - double price = 7; // Order price +```typescript +import ccxt from 'ccxt'; + +// Initialize the exchange (no API keys needed for public data) +const exchange = new ccxt.binance(); // or any other exchange like ccxt.bybit() + +// Fetch all currencies and their network information +const currencies = await exchange.fetchCurrencies(); + +// Example: Check USDT networks on Binance +const usdtInfo = currencies['USDT']; +console.log("USDT Networks on Binance:"); +console.log(usdtInfo?.networks); + +// Example output: +// { +// 'BEP20': {id: 'BSC', network: 'BSC', active: true, deposit: true, withdraw: true, fee: 1.0}, +// 'ETH': {id: 'ETH', network: 'ETH', active: true, deposit: true, withdraw: true, fee: 15.0}, +// 'TRC20': {id: 'TRX', network: 'TRX', active: true, deposit: true, withdraw: true, fee: 1.0} +// } + +// Check all available currencies +for (const [currency, info] of Object.entries(currencies)) { + if ('networks' in info) { + console.log(`\n${currency} networks:`); + for (const [network, networkInfo] of Object.entries(info.networks)) { + console.log(` ${network}:`, networkInfo); + } + } } ``` -### CancelOrder - -Cancel an existing order. - -**Request:** - -```protobuf -message CancelOrderRequest { - string order_id = 1; // Unique order identifier - string cex = 2; // CEX identifier -} -``` +**Common Network Identifiers:** +- `BEP20` / `BSC`: Binance Smart Chain +- `ETH` / `ERC20`: Ethereum +- `TRC20`: Tron +- `ARBITRUM`: Arbitrum One +- `POLYGON`: Polygon +- `AVALANCHE`: Avalanche C-Chain +- `OPTIMISM`: Optimism -**Response:** +**Using this information in your policy:** -```protobuf -message CancelOrderResponse { - bool success = 1; // Whether cancellation was successful - string final_status = 2; // Final status of the order +```json +{ + "withdraw": { + "rule": { + "networks": ["BEP20", "ARBITRUM", "ETH"], // Networks supported by your exchanges + "whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"], + "amounts": [ + { + "ticker": "USDT", + "max": 100000, + "min": 1 + } + ] + } + } } ``` -## Security - -### IP Authentication - -All API calls require IP authentication. Configure allowed IPs in `helpers/index.ts`: - -```typescript -const ALLOWED_IPS = [ - "127.0.0.1", // localhost - "::1", // IPv6 localhost - // Add your allowed IP addresses here -]; -``` - -### API Key Management - -- Store API keys securely in environment variables -- Use read-only API keys when possible -- Regularly rotate API keys -- Monitor API usage and set appropriate rate limits - -## Error Handling - -The service returns appropriate gRPC status codes: - -- `INVALID_ARGUMENT`: Missing or invalid parameters -- `PERMISSION_DENIED`: IP not allowed or policy violation -- `NOT_FOUND`: Resource not found (e.g., currency balance) -- `INTERNAL`: Server error - -## Development - -### Project Structure - -``` -fietCexBroker/ -├── config/ # Configuration files -│ ├── broker.ts # Exchange broker setup -│ └── index.ts # Environment configuration -├── helpers/ # Utility functions -│ └── index.ts # Core helper functions -├── policy/ # Policy configuration -│ └── policy.json # Trading and withdrawal rules -├── proto/ # Protocol buffer definitions -│ ├── fietCexNode/ # Generated TypeScript types -│ ├── node.proto # Service definition -│ └── node.ts # Type exports -├── scripts/ # Build scripts -│ └── patch-protobufjs.js -├── index.ts # Main server file -├── types.ts # TypeScript type definitions -├── proto-gen.sh # Protobuf generation script -├── biome.json # Code formatting/linting config -├── bunfig.toml # Bun configuration -└── package.json # Dependencies and scripts -``` - -### Adding New Exchanges - -1. Add the exchange to `types.ts` in the `BrokerList` -2. Configure API keys in `config/index.ts` -3. Initialize the broker in `config/broker.ts` -4. Update policy configuration if needed - ### Testing ```bash @@ -415,37 +348,68 @@ bun test --watch bun test --coverage ``` -## Dependencies +### Code Quality + +```bash +# Format code +bun run format + +# Lint code +bun run lint + +# Check code (format + lint) +bun run check +``` + +## 📦 Dependencies ### Core Dependencies - `@grpc/grpc-js`: gRPC server implementation - `@grpc/proto-loader`: Protocol buffer loading -- `ccxt`: Cryptocurrency exchange library -- `dotenv`: Environment variable management +- `ccxt`: Cryptocurrency exchange library (100+ exchanges) +- `commander`: CLI framework - `joi`: Configuration validation +- `tslog`: TypeScript logging ### Development Dependencies - `@biomejs/biome`: Code formatting and linting - `@types/bun`: Bun type definitions +- `bun-plugin-dts`: TypeScript declaration generation - `bun-types`: Additional Bun types - `husky`: Git hooks -## Contributing +## 🤝 Contributing 1. Fork the repository -2. Create a feature branch +2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Make your changes 4. Add tests for new functionality -5. Ensure all tests pass -6. Run `bun run check` to format and lint code -7. Submit a pull request +5. Ensure all tests pass (`bun test`) +6. Run code quality checks (`bun run check`) +7. Commit your changes (`git commit -m 'Add amazing feature'`) +8. Push to the branch (`git push origin feature/amazing-feature`) +9. Open a Pull Request + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🆘 Support + +For issues and questions: + +- Open an issue on the repository +- Contact the development team +- Check the [CCXT documentation](https://docs.ccxt.com/) for exchange-specific information -## License +## 🙏 Acknowledgments -[Add your license information here] +- [CCXT](https://github.com/ccxt/ccxt) for providing unified access to cryptocurrency exchanges +- [Bun](https://bun.sh) for the fast JavaScript runtime +- [gRPC](https://grpc.io/) for high-performance RPC communication -## Support +--- -For issues and questions, please open an issue on the repository or contact the development team. +**Built with ❤️ by Usher Labs** diff --git a/proto/node.proto b/proto/node.proto index e40c84f..1cb3316 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -1,9 +1,6 @@ syntax = "proto3"; -package cexBroker; // TODO: I've renamed... +package cexBroker; -// TODO: This whole file can be a simple message... -// TODO: A single generic CCXT action/message to executing any CCXT function... This way the broker is dynamic ... -// TODO: Maybe a second action to trigger a websocket/streaming response... message CcxtActionRequest { Action action = 1; // The CCXT method to call (e.g., "fetchBalance", "createOrder") map payload = 2; // Parameters to pass to the CCXT method diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index 8a3071d..8895818 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -1,13 +1,11 @@ import { describe, test, expect, beforeEach, mock } from "bun:test"; import { - buyAtOptimalPrice, - sellAtOptimalPrice, validateWithdraw, validateOrder, validateDeposit, } from "./index"; import type { Exchange } from "ccxt"; -import type { PolicyConfig } from "../types"; +import type { PolicyConfig } from "../../types"; describe("Helper Functions", () => { let mockExchange: Exchange; @@ -68,84 +66,6 @@ describe("Helper Functions", () => { }; }); - describe("buyAtOptimalPrice", () => { - test("should calculate optimal buy price correctly", async () => { - const result = await buyAtOptimalPrice(mockExchange, "ARB/USDT", 25); - - expect(result).toBeDefined(); - expect(result.avgPrice).toBeGreaterThan(0); - expect(result.fillPrice).toBeGreaterThan(0); - expect(result.size).toBe(25); - expect(result.symbol).toBe("ARB/USDT"); - expect(mockExchange.fetchOrderBook).toHaveBeenCalledWith("ARB/USDT", 500); - }); - - test("should handle insufficient depth", async () => { - // Mock exchange with insufficient depth - const insufficientExchange = { - fetchOrderBook: mock(async () => ({ - bids: [[100, 5]], // Only 5 volume available - })), - } as any; - - await expect( - buyAtOptimalPrice(insufficientExchange, "ARB/USDT", 10), - ).rejects.toThrow("Insufficient depth"); - }); - - test("should handle edge case with exact volume match", async () => { - const exactExchange = { - fetchOrderBook: mock(async () => ({ - bids: [[100, 25]], // Exact volume needed - })), - } as any; - - const result = await buyAtOptimalPrice(exactExchange, "ARB/USDT", 25); - expect(result.avgPrice).toBe(100); - expect(result.fillPrice).toBe(100); - }); - }); - - describe("sellAtOptimalPrice", () => { - test("should calculate optimal sell price correctly", async () => { - const result = await sellAtOptimalPrice(mockExchange, "ARB/USDT", 25); - - expect(result).toBeDefined(); - expect(result.avgPrice).toBeGreaterThan(0); - expect(result.fillPrice).toBeGreaterThan(0); - expect(result.size).toBe(25); - expect(result.symbol).toBe("ARB/USDT"); - expect(mockExchange.fetchOrderBook).toHaveBeenCalledWith("ARB/USDT"); - }); - - test("should handle insufficient depth for selling", async () => { - const insufficientExchange = { - fetchOrderBook: mock(async () => ({ - asks: [[101, 5]], // Only 5 volume available - })), - } as any; - - await expect( - sellAtOptimalPrice(insufficientExchange, "ARB/USDT", 10), - ).rejects.toThrow("Insufficient depth"); - }); - - test("should handle undefined orderbook entries", async () => { - const badExchange = { - fetchOrderBook: mock(async () => ({ - asks: [ - [undefined, 10], - [102, undefined], - ], - })), - } as any; - - await expect( - sellAtOptimalPrice(badExchange, "ARB/USDT", 5), - ).rejects.toThrow("Orderbook entry had undefined price or volume"); - }); - }); - describe("loadPolicy", () => { test("should load policy successfully", () => { // This test will use the actual policy file diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 3cb5f20..be51132 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,107 +1,6 @@ -import type { Exchange } from "ccxt"; import type { PolicyConfig } from "../../types"; import fs from "fs"; import Joi from "joi"; -import { log } from "./logger"; - -// TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." -export async function buyAtOptimalPrice( - exchange: Exchange, - symbol: string, - size: number, -) { - // 1) fetch the order book - const book = await exchange.fetchOrderBook(symbol, 500); - const bids = book.bids; - - // 2) walk bids until cumulative >= size - let remaining = size; - let cumCost = 0; - let fillPrice = 0; - - for (const [priceRaw, volumeRaw] of bids) { - const price = Number(priceRaw); - const volume = Number(volumeRaw); - const take = Math.min(volume, remaining); - cumCost += take * price; - remaining -= take; - - if (remaining <= 0) { - fillPrice = price; - break; - } - } - - if (remaining > 0) { - throw new Error( - `Insufficient depth: only filled ${size - remaining} of ${size}`, - ); - } - - const avgPrice = cumCost / size; - log.info( - `[${new Date().toISOString()}] ` + - `Will buy ${size} ${symbol.split("/")[0]} at limit ${fillPrice.toFixed(6)} ` + - `(VWAP ≃ ${avgPrice.toFixed(6)})`, - ); - return { avgPrice, fillPrice, size, symbol }; -} - -/** - * Fetches the order book, computes the worst‐case fill price on the ask side for `size`, - * and submits a single limit‐sell at that price. - */ -// TODO: This is still arb functionality... literally this node is "Package A to be sent to Binance, Package B sent to Kucoin, Package C sent to Coinjar." -export async function sellAtOptimalPrice( - exchange: Exchange, - symbol: string, - size: number, -) { - // 1) fetch the order book - const book = await exchange.fetchOrderBook(symbol); - const asks = book.asks; - - // 2) walk asks until cumulative >= size - let remaining = size; - let cumProceeds = 0; - let fillPrice = 0; - - for (const entry of asks) { - const priceRaw = entry[0]; - const volumeRaw = entry[1]; - - if (priceRaw === undefined || volumeRaw === undefined) { - throw new Error("Orderbook entry had undefined price or volume"); - } - - const price = Number(priceRaw); - const volume = Number(volumeRaw); - - const take = Math.min(volume, remaining); - cumProceeds += take * price; - remaining -= take; - - if (remaining <= 0) { - fillPrice = price; - break; - } - } - - if (remaining > 0) { - throw new Error( - `Insufficient depth: only sold ${size - remaining} of ${size}`, - ); - } - - const avgPrice = cumProceeds / size; - log.info( - `[${new Date().toISOString()}] ` + - `Will sell ${size} ${symbol.split("/")[0]} at limit ${fillPrice.toFixed(6)} ` + - `(VWAP ≃ ${avgPrice.toFixed(6)})`, - ); - - return { avgPrice, fillPrice, size, symbol }; -} /** * Loads and validates policy configuration diff --git a/src/server.ts b/src/server.ts index 86c335d..41b3bc8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -96,7 +96,7 @@ export function getServer(policy: PolicyConfig, brokers: Record deposit.id == value.transactionHash || deposit.txid == value.transactionHash) @@ -206,8 +205,6 @@ export function getServer(policy: PolicyConfig, brokers: Record { }); }); - describe("Price Calculation Integration", () => { - test("should calculate optimal prices correctly", async () => { - const { buyAtOptimalPrice, sellAtOptimalPrice } = require("../src/helpers"); - - // Create a mock exchange with realistic order book data - const mockExchange = { - fetchOrderBook: async (_symbol: string) => ({ - bids: [ - [100, 10], - [99, 20], - [98, 30], - ], - asks: [ - [101, 10], - [102, 20], - [103, 30], - ], - }), - }; - - // Test buy calculation - const buyResult = await buyAtOptimalPrice(mockExchange, "ARB/USDT", 25); - expect(buyResult.avgPrice).toBeGreaterThan(0); - expect(buyResult.fillPrice).toBeGreaterThan(0); - expect(buyResult.size).toBe(25); - expect(buyResult.symbol).toBe("ARB/USDT"); - - // Test sell calculation - const sellResult = await sellAtOptimalPrice(mockExchange, "ARB/USDT", 25); - expect(sellResult.avgPrice).toBeGreaterThan(0); - expect(sellResult.fillPrice).toBeGreaterThan(0); - expect(sellResult.size).toBe(25); - expect(sellResult.symbol).toBe("ARB/USDT"); - }); - }); - - describe("Error Handling Integration", () => { - test("should handle insufficient depth correctly", async () => { - const { buyAtOptimalPrice } = require("../src/helpers"); - - const insufficientExchange = { - fetchOrderBook: async () => ({ - bids: [[100, 5]], // Only 5 volume available - }), - }; - - await expect( - buyAtOptimalPrice(insufficientExchange, "ARB/USDT", 10), - ).rejects.toThrow("Insufficient depth"); - }); - - test("should handle invalid symbol format", () => { - const { validateOrder } = require("../src/helpers"); - const { loadPolicy } = require("../src/helpers"); - - const policy = loadPolicy("./policy/policy.json"); - - // Test with invalid symbol format - const result = validateOrder( - policy, - "USDT", - "ETH", - 0.5, - "BINANCE", - "ARB", // Invalid format - missing '/' - ); - - expect(result.valid).toBe(false); - }); - }); }); From cc3848f54ae310b24676221771199a9f495a3d71 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 13 Jul 2025 09:19:48 +0100 Subject: [PATCH 21/45] Refactor import statements in index.test.ts --- src/helpers/index.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index 8895818..ab1edc7 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -1,9 +1,5 @@ import { describe, test, expect, beforeEach, mock } from "bun:test"; -import { - validateWithdraw, - validateOrder, - validateDeposit, -} from "./index"; +import { validateWithdraw, validateOrder, validateDeposit } from "./index"; import type { Exchange } from "ccxt"; import type { PolicyConfig } from "../../types"; From ebfcc57cc5217511ea1404ea956f70f295801188 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 13 Jul 2025 09:29:54 +0100 Subject: [PATCH 22/45] Add orderType in order creation logic --- src/server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 41b3bc8..3672464 100644 --- a/src/server.ts +++ b/src/server.ts @@ -228,6 +228,7 @@ export function getServer(policy: PolicyConfig, brokers: Record Date: Sun, 13 Jul 2025 09:34:51 +0100 Subject: [PATCH 23/45] Update .gitignore to exclude all proto files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f6f5265..180bb1e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store build -proto/**git \ No newline at end of file +proto/** \ No newline at end of file From 1cf9361a417b116bbbad64a6b893767e643ea297 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 13 Jul 2025 09:39:52 +0100 Subject: [PATCH 24/45] Remove PORT_NUM from .env.sample file --- .env.sample | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 6504b24..d50369a 100644 --- a/.env.sample +++ b/.env.sample @@ -4,5 +4,4 @@ CEX_BROKER_BYBIT_API_KEY=*********************** CEX_BROKER_BINANCE_API_KEY=**************************************** CEX_BROKER_BYBIT_API_SECRET=*********************************************** -CEX_BROKER_BINANCE_API_SECRET=************************************************************ -PORT_NUM=8086 \ No newline at end of file +CEX_BROKER_BINANCE_API_SECRET=************************************************************ \ No newline at end of file From b485436c67cd234546fd0dbc764d9d0d5603a09d Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 13 Jul 2025 10:21:57 +0100 Subject: [PATCH 25/45] Add subscription feature to CexService --- proto/node.proto | 32 ++++++++++++++++++++++++++++---- proto/node.ts | 7 +++++-- src/server.ts | 12 ++++++------ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/proto/node.proto b/proto/node.proto index 1cb3316..599ad77 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -1,20 +1,44 @@ syntax = "proto3"; package cexBroker; -message CcxtActionRequest { +message ActionRequest { Action action = 1; // The CCXT method to call (e.g., "fetchBalance", "createOrder") map payload = 2; // Parameters to pass to the CCXT method string cex = 3; // CEX identifier (e.g., "binance", "bybit") string symbol = 4; // Optional: trading pair symbol if needed } -message CcxtActionResponse { +message ActionResponse { string result = 2; // JSON string of the result data } -service CexService { - rpc ExecuteCcxtAction(CcxtActionRequest) returns (CcxtActionResponse); + +message SubscribeRequest { + string cex = 1; // CEX identifier (e.g., "binance", "bybit") + string symbol = 2; // Trading pair symbol (e.g., "BTC/USDT") + SubscriptionType type = 3; // Type of subscription (orderbook, trades, etc.) + map options = 4; // Additional subscription options +} + +message SubscribeResponse { + string data = 1; // JSON string of the streaming data + int64 timestamp = 2; // Unix timestamp of the data + string symbol = 3; // Trading pair symbol + SubscriptionType type = 4; // Type of subscription } +enum SubscriptionType { + ORDERBOOK = 0; // Order book updates + TRADES = 1; // Recent trades + TICKER = 2; // Ticker information + OHLCV = 3; // OHLCV candlestick data + BALANCE = 4; // Balance updates + ORDERS = 5; // Order updates +} + +service CexService { + rpc ExecuteAction(ActionRequest) returns (ActionResponse); + rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse); +} // Mode enum for price direction enum Action { diff --git a/proto/node.ts b/proto/node.ts index 866098b..c07f8cd 100644 --- a/proto/node.ts +++ b/proto/node.ts @@ -10,9 +10,12 @@ type SubtypeConstructor any, Subtype> export interface ProtoGrpcType { cexBroker: { Action: EnumTypeDefinition - CcxtActionRequest: MessageTypeDefinition - CcxtActionResponse: MessageTypeDefinition + ActionRequest: MessageTypeDefinition + ActionResponse: MessageTypeDefinition CexService: SubtypeConstructor & { service: _cexBroker_CexServiceDefinition } + SubscribeRequest: MessageTypeDefinition + SubscribeResponse: MessageTypeDefinition + SubscriptionType: EnumTypeDefinition } } diff --git a/src/server.ts b/src/server.ts index 3672464..2329ae7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,8 +9,8 @@ import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import path from "path"; import type { Exchange } from "ccxt"; -import type { CcxtActionRequest, CcxtActionRequest__Output } from "../proto/cexBroker/CcxtActionRequest"; -import type { CcxtActionResponse } from "../proto/cexBroker/CcxtActionResponse"; +import type { ActionRequest, ActionRequest__Output } from "../proto/cexBroker/ActionRequest"; +import type { ActionResponse } from "../proto/cexBroker/ActionResponse"; import { Action } from "../proto/cexBroker/Action"; import Joi from "joi"; import ccxt from "ccxt"; @@ -58,12 +58,12 @@ function createBroker(cex: string, metadata: grpc.Metadata): Exchange |null{ export function getServer(policy: PolicyConfig, brokers: Record, whitelistIps: string[]) { const server = new grpc.Server(); server.addService(cexNode.CexService.service, { - ExecuteCcxtAction: async ( + ExecuteAction: async ( call: grpc.ServerUnaryCall< - CcxtActionRequest, - CcxtActionResponse + ActionRequest, + ActionResponse >, - callback: grpc.sendUnaryData, + callback: grpc.sendUnaryData, ) => { // IP Authentication if (!authenticateRequest(call, whitelistIps)) { From 7748d467ed09f538a8786ef1818712a84158bcbd Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 14 Jul 2025 08:05:11 +0100 Subject: [PATCH 26/45] Add balance subscription and logging improvements --- src/client.dev.ts | 49 ++++++++-- src/index.ts | 18 ++-- src/server.ts | 237 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 274 insertions(+), 30 deletions(-) diff --git a/src/client.dev.ts b/src/client.dev.ts index bcfc990..48b5af8 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -3,6 +3,7 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import { Action } from "../proto/cexBroker/Action"; +import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; import {config} from "dotenv" import { log } from "./helpers/logger"; @@ -36,12 +37,48 @@ client.waitForReady(deadline, (err) => { }); function onClientReady() { - client.executeCcxtAction({ cex: "bybit", symbol: "USDT",payload:{},action: Action.FetchBalance },metadata, (err, result) => { - if (err) { - log.error({ err }); - return; - } - log.info({ x: result }); + // // Test ExecuteAction for balance + // client.executeAction({ cex: "bybit", symbol: "USDT",payload:{},action: Action.FetchBalance },metadata, (err, result) => { + // if (err) { + // log.error({ err }); + // return; + // } + // log.info("ExecuteAction Balance Result:", { result }); + // }); + + // Test Subscribe for balance streaming + log.info("Starting balance subscription test..."); + const subscribeCall = client.subscribe({ + cex: "bybit", + symbol: "USDT", + type: SubscriptionType.BALANCE, + options: {} + }, metadata); + + // Handle incoming stream data + subscribeCall.on('data', (response) => { + log.info("Balance Subscription Update:", { + timestamp: new Date(response.timestamp).toISOString(), + symbol: response.symbol, + type: response.type, + data: JSON.parse(response.data) + }); + }); + + // Handle stream end + subscribeCall.on('end', () => { + log.info("Balance subscription stream ended"); + }); + + // Handle stream errors + subscribeCall.on('error', (error) => { + log.error("Balance subscription stream error:", error); }); + // Keep the subscription alive for 30 seconds + setTimeout(() => { + log.info("Closing balance subscription after 30 seconds"); + // For server-side streaming, we don't need to call end() on the client + // The server will handle the stream lifecycle + }, 30000); } diff --git a/src/index.ts b/src/index.ts index e2ec27d..f032213 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { import { getServer } from "./server"; import { log } from "./helpers/logger"; -log("CCXT Version:", ccxt.version); +log.info("CCXT Version:", ccxt.version); export default class CEXBroker { #brokerConfig: Record = {}; @@ -34,7 +34,7 @@ export default class CEXBroker { * CEX_BROKER__API_SECRET */ public loadEnvConfig(): void { - log("🔧 Loading CEX_BROKER_ environment variables:"); + log.info("🔧 Loading CEX_BROKER_ environment variables:"); const configMap: Record> = {}; for (const [key, value] of Object.entries(process.env)) { @@ -42,7 +42,7 @@ export default class CEXBroker { const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); if (!match) { - warn(`⚠️ Skipping unrecognized env var: ${key}`); + log.warn(`⚠️ Skipping unrecognized env var: ${key}`); continue; } @@ -61,7 +61,7 @@ export default class CEXBroker { } if (Object.keys(configMap).length === 0) { - error(`❌ NO CEX Broker Key Found`); + log.error(`❌ NO CEX Broker Key Found`); } // Finalize config and print result per broker @@ -74,7 +74,7 @@ export default class CEXBroker { apiKey: creds.apiKey ?? "", apiSecret: creds.apiSecret ?? "", }; - log(`✅ Loaded credentials for broker "${broker}"`); + log.info(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -87,7 +87,7 @@ export default class CEXBroker { const missing = []; if (!hasKey) missing.push("API_KEY"); if (!hasSecret) missing.push("API_SECRET"); - warn(`❌ Missing ${missing.join(" and ")} for broker "${broker}"`); + log.warn(`❌ Missing ${missing.join(" and ")} for broker "${broker}"`); } } } @@ -117,7 +117,7 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - log(`✅ Loaded credentials for broker "${broker}"`); + log.info(`✅ Loaded credentials for broker "${broker}"`); const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, @@ -213,3 +213,7 @@ export default class CEXBroker { return this; } } + +const policy = loadPolicy("./policy/policy.json"); +const broker =new CEXBroker({},policy) +broker.run() \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 2329ae7..1fe69e6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,8 +12,12 @@ import type { Exchange } from "ccxt"; import type { ActionRequest, ActionRequest__Output } from "../proto/cexBroker/ActionRequest"; import type { ActionResponse } from "../proto/cexBroker/ActionResponse"; import { Action } from "../proto/cexBroker/Action"; +import type { SubscribeRequest } from "../proto/cexBroker/SubscribeRequest"; +import type { SubscribeResponse } from "../proto/cexBroker/SubscribeResponse"; +import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; import Joi from "joi"; import ccxt from "ccxt"; +import from "ws" import { log } from "./helpers/logger"; @@ -28,7 +32,7 @@ const cexNode = grpcObj.cexBroker; function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIps: string[]): boolean { const clientIp = call.getPeer().split(":")[0]; if (!clientIp || !whitelistIps.includes(clientIp)) { - console.warn( + log.warn( `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, ); return false; @@ -36,14 +40,14 @@ function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIp return true; } -function createBroker(cex: string, metadata: grpc.Metadata): Exchange |null{ +function createBroker(cex: string, metadata: grpc.Metadata): Exchange | null { const api_key = metadata.get('api-key'); const api_secret = metadata.get('api-secret'); - const ExchangeClass = (ccxt as any)[cex]; + const ExchangeClass = (ccxt.pro as any)[cex]; metadata.remove('api-key'); metadata.remove('api-secret'); - if (api_secret.length==0 || api_key.length==0){ + if (api_secret.length == 0 || api_key.length == 0) { return null } return new ExchangeClass({ @@ -79,7 +83,7 @@ export function getServer(policy: PolicyConfig, brokers: Record, + ) => { + // IP Authentication + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || !whitelistIps.includes(clientIp)) { + log.warn( + `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, + ); + call.destroy(new Error("Access denied: Unauthorized IP")); + return; + } + + // Read incoming metadata + const metadata = call.metadata; + let broker: Exchange | null = null; + + try { + // For ServerWritableStream, we need to get the request from the call + // The request should be available in the call object + const request = call.request as SubscribeRequest; + const { cex, symbol, type, options } = request; + + // Validate required fields + if (!cex || !symbol || type === undefined) { + call.write({ + data: JSON.stringify({ error: "cex, symbol, and type are required" }), + timestamp: Date.now(), + symbol: symbol || "", + type: type || SubscriptionType.ORDERBOOK, + }); + call.end(); + return; + } + + // Get or create broker + broker = brokers[cex as keyof typeof brokers] ?? createBroker(cex, metadata); + + if (!broker) { + call.write({ + data: JSON.stringify({ error: "Exchange not registered and no API metadata found" }), + timestamp: Date.now(), + symbol, + type, + }); + call.end(); + return; + } + + // Handle different subscription types + switch (type) { + case SubscriptionType.ORDERBOOK: + try { + const orderbook = await broker.watchOrderBook(symbol); + call.write({ + data: JSON.stringify(orderbook), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching orderbook for ${symbol} on ${cex}:`, error); + call.write({ + data: JSON.stringify({ error: `Failed to fetch orderbook: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.TRADES: + try { + const trades = await broker.watchTrades(symbol); + call.write({ + data: JSON.stringify(trades), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching trades for ${symbol} on ${cex}:`, error.message); + call.write({ + data: JSON.stringify({ error: `Failed to fetch trades: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.TICKER: + try { + const ticker = await broker.watchTicker(symbol); + call.write({ + data: JSON.stringify(ticker), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching ticker for ${symbol} on ${cex}:`, error.message); + call.write({ + data: JSON.stringify({ error: `Failed to fetch ticker: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.OHLCV: + try { + const timeframe = options?.timeframe || '1m'; + const ohlcv = await broker.watchOHLCV(symbol, timeframe); + call.write({ + data: JSON.stringify(ohlcv), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching OHLCV for ${symbol} on ${cex}:`, error.message); + call.write({ + data: JSON.stringify({ error: `Failed to fetch OHLCV: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.BALANCE: + try { + const balance = await broker.watchBalance(); + call.write({ + data: JSON.stringify(balance), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching balance for ${cex}:`, error.message); + call.write({ + data: JSON.stringify({ error: `Failed to fetch balance: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.ORDERS: + try { + const orders = await broker.watchOrders(symbol); + call.write({ + data: JSON.stringify(orders), + timestamp: Date.now(), + symbol, + type, + }); + } catch (error:any) { + log.error(`Error fetching orders for ${symbol} on ${cex}:`, error); + call.write({ + data: JSON.stringify({ error: `Failed to fetch orders: ${error.message}` }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + default: + call.write({ + data: JSON.stringify({ error: "Invalid subscription type" }), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error) { + log.error("Error in Subscribe stream:", error); + call.write({ + data: JSON.stringify({ error: `Internal server error: ${error}` }), + timestamp: Date.now(), + symbol: "", + type: SubscriptionType.ORDERBOOK, + }); + } + + call.on('end', () => { + log.info("Subscribe stream ended"); + }); + + call.on('error', (error) => { + log.error("Subscribe stream error:", error); + }); + }, }); return server; } From 46afd551f1e65b5f5e148aee09565a501ef747cf Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 14 Jul 2025 10:29:57 +0100 Subject: [PATCH 27/45] Remove unused policy loading and broker run --- src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index f032213..5361d7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -213,7 +213,3 @@ export default class CEXBroker { return this; } } - -const policy = loadPolicy("./policy/policy.json"); -const broker =new CEXBroker({},policy) -broker.run() \ No newline at end of file From 780d63e0e889aacc2d46baf786969f37c595d901 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 20 Jul 2025 20:13:13 +0100 Subject: [PATCH 28/45] Add Verity Prover URL and secondary keys support --- build.ts | 2 +- bun.lock | 58 +- package.json | 2 +- proto/node.proto | 1 + src/cli.ts | 72 +- src/client.dev.ts | 92 ++- src/commands/start-broker.ts | 4 +- src/helpers/index.test.ts | 4 +- src/helpers/index.ts | 2 +- src/index.ts | 174 ++++- src/server.ts | 1302 ++++++++++++++++++---------------- types.ts => src/types.ts | 7 +- tsconfig.json | 2 +- 13 files changed, 1028 insertions(+), 694 deletions(-) rename types.ts => src/types.ts (94%) diff --git a/build.ts b/build.ts index 6df730d..48eeeb4 100644 --- a/build.ts +++ b/build.ts @@ -15,7 +15,7 @@ await Bun.build({ outdir: './dist', target:"node", plugins: [ - dts() + // dts() ], }) diff --git a/bun.lock b/bun.lock index dc290fc..9b1748c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", - "ccxt": "^4.4.91", + "@usherlabs/ccxt": "^0.0.4", "commander": "^14.0.0", "joi": "^17.13.3", "tslog": "^4.9.3", @@ -84,19 +84,29 @@ "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/eventsource": ["@types/eventsource@1.1.15", "", {}, "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA=="], + "@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@usherlabs/ccxt": ["@usherlabs/ccxt@0.0.4", "", { "dependencies": { "@usherlabs/verity-client": "^0.0.24", "axios": "^1.10.0", "ws": "^8.8.1" } }, "sha512-lmBieByW4BgNOJjBRicOSUooMqS0SH3bOkKRPrJTZd/vOS2liFF7zamk2WxKhrZVeO2Azk4lOhrL0MH7H7l4sw=="], + + "@usherlabs/verity-client": ["@usherlabs/verity-client@0.0.24", "", { "dependencies": { "@types/eventsource": "^1.1.15", "axios": "^1.9.0", "eventsource": "^2.0.2", "uuid": "^11.1.0" } }, "sha512-TtKQUeIaKtWDTFIbFWfe/GKA3YixsOwko4vS8rjFuxQdg+FA6XUMUQolyZaAyqDr6QSzNtSu7FTYIluJZewFyQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], + "bun-plugin-dts": ["bun-plugin-dts@0.3.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.8.1" } }, "sha512-QpiAOKfPcdOToxySOqRY8FwL+brTvyXEHWzrSCRKt4Pv7Z4pnUrhK9tFtM7Ndm7ED09B/0cGXnHJKqmekr/ERw=="], "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], - "ccxt": ["ccxt@4.4.91", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-IP7wZc1KfAojMfKyMeGwC/aJoXvAUFFUMtu3x9Wp5BAOISftcrcl/yt/gjxaPddrMT2/BcU3wHZzvqzr1FBFBw=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -104,24 +114,58 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "dotenv": ["dotenv@17.2.0", "", {}, "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ=="], "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], + + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -132,8 +176,16 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -148,6 +200,8 @@ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], diff --git a/package.json b/package.json index 5f6e93b..b3e61e2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", - "ccxt": "^4.4.91", + "@usherlabs/ccxt": "^0.0.4", "commander": "^14.0.0", "joi": "^17.13.3", "tslog": "^4.9.3" diff --git a/proto/node.proto b/proto/node.proto index 599ad77..a378a68 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -49,4 +49,5 @@ enum Action { GetOrderDetails=4; CancelOrder=5; FetchBalance=6; + FetchDepositAddresses=7; } \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 02823d3..7fdd40d 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,41 +1,51 @@ #!/usr/bin/env bun -import { Command } from 'commander'; -import { startBrokerCommand } from './commands/start-broker'; +import { Command } from "commander"; +import { startBrokerCommand } from "./commands/start-broker"; const program = new Command(); program - .name('cex-broker') - .description('CLI to start the CEXBroker service') - .requiredOption('-p, --policy ', 'Policy JSON file') - .option('--port ', 'Port number (default: 8086)', '8086') - .option('--whitelist ', 'IPv4 address whitelist (space-separated list)') - .action(async (options) => { - try { - // Optional: Validate IPv4 addresses - if (options.whitelist) { - const isValidIPv4 = (ip: string) => - /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && - ip.split('.').every(part => Number(part) >= 0 && Number(part) <= 255); + .name("cex-broker") + .description("CLI to start the CEXBroker service") + .requiredOption("-p, --policy ", "Policy JSON file") + .option("--port ", "Port number (default: 8086)", "8086") + .option( + "-w","--whitelist ", + "IPv4 address whitelist (space-separated list)", + ) + .option( + "-vu","--verityProverUrl ", + "Verity Prover Url", + ) + .action(async (options) => { + try { + // Optional: Validate IPv4 addresses + if (options.whitelist) { + const isValidIPv4 = (ip: string) => + /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && + ip + .split(".") + .every((part) => Number(part) >= 0 && Number(part) <= 255); - for (const ip of options.whitelist) { - if (!isValidIPv4(ip)) { - console.error(`❌ Invalid IPv4 address: ${ip}`); - process.exit(1); - } - } - } + for (const ip of options.whitelist) { + if (!isValidIPv4(ip)) { + console.error(`❌ Invalid IPv4 address: ${ip}`); + process.exit(1); + } + } + } - await startBrokerCommand( - options.policy, - parseInt(options.port, 10), - options.whitelist ?? [] // Pass whitelist to your command - ); - } catch (err) { - console.error('❌ Failed to start broker:', err); - process.exit(1); - } - }); + await startBrokerCommand( + options.policy, + parseInt(options.port, 10), + options.whitelist ?? [], // Pass whitelist to your command, + options.verityProverUrl + ); + } catch (err) { + console.error("❌ Failed to start broker:", err); + process.exit(1); + } + }); program.parse(process.argv); diff --git a/src/client.dev.ts b/src/client.dev.ts index 48b5af8..54c9293 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -4,11 +4,13 @@ import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import { Action } from "../proto/cexBroker/Action"; import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; -import {config} from "dotenv" +import { config } from "dotenv"; import { log } from "./helpers/logger"; +import CEXBroker from "."; +import { loadPolicy } from "./helpers"; const PROTO_FILE = "../proto/node.proto"; -const port= 8086 +const port = 8086; const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); const grpcObj = grpc.loadPackageDefinition( @@ -20,11 +22,18 @@ const client = new grpcObj.cexBroker.CexService( grpc.credentials.createInsecure(), ); -config() +config(); + +const broker = new CEXBroker({}, loadPolicy("./policy/policy.json"), { + useVerity: true, +}); +broker.loadEnvConfig(); +broker.run(); + const metadata = new grpc.Metadata(); -metadata.add('api-key', process.env.BYBIT_API_KEY??""); // Example header -metadata.add('api-secret', process.env.BYBIT_API_SECRET??""); +metadata.add("api-key", process.env.BYBIT_API_KEY ?? ""); // Example header +metadata.add("api-secret", process.env.BYBIT_API_SECRET ?? ""); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 5); @@ -46,39 +55,50 @@ function onClientReady() { // log.info("ExecuteAction Balance Result:", { result }); // }); - // Test Subscribe for balance streaming - log.info("Starting balance subscription test..."); - const subscribeCall = client.subscribe({ - cex: "bybit", - symbol: "USDT", - type: SubscriptionType.BALANCE, - options: {} - }, metadata); - - // Handle incoming stream data - subscribeCall.on('data', (response) => { - log.info("Balance Subscription Update:", { - timestamp: new Date(response.timestamp).toISOString(), - symbol: response.symbol, - type: response.type, - data: JSON.parse(response.data) - }); + // Test ExecuteAction for balance + client.executeAction({ cex: "bybit", symbol: "USDT",payload:{chain:"TRC20"},action: Action.FetchDepositAddresses },metadata, (err, result) => { + if (err) { + log.error({ err }); + return; + } + log.info("ExecuteAction Balance Result:", { result }); }); - // Handle stream end - subscribeCall.on('end', () => { - log.info("Balance subscription stream ended"); - }); + // // Test Subscribe for balance streaming + // log.info("Starting balance subscription test..."); + // const subscribeCall = client.subscribe( + // { + // cex: "bybit", + // symbol: "BTC/USDT", + // type: SubscriptionType.TICKER, + // options: {}, + // }, + // metadata, + // ); - // Handle stream errors - subscribeCall.on('error', (error) => { - log.error("Balance subscription stream error:", error); - }); + // // Handle incoming stream data + // subscribeCall.on("data", (response) => { + // log.info("Balance Subscription Update:", { + // symbol: response.symbol, + // type: response.type, + // data: JSON.parse(response.data), + // }); + // }); + + // // Handle stream end + // subscribeCall.on("end", () => { + // log.info("Balance subscription stream ended"); + // }); + + // // Handle stream errors + // subscribeCall.on("error", (error) => { + // log.error("Balance subscription stream error:", error); + // }); - // Keep the subscription alive for 30 seconds - setTimeout(() => { - log.info("Closing balance subscription after 30 seconds"); - // For server-side streaming, we don't need to call end() on the client - // The server will handle the stream lifecycle - }, 30000); + // // Keep the subscription alive for 30 seconds + // setTimeout(() => { + // log.info("Closing balance subscription after 30 seconds"); + // // For server-side streaming, we don't need to call end() on the client + // // The server will handle the stream lifecycle + // }, 3000000); } diff --git a/src/commands/start-broker.ts b/src/commands/start-broker.ts index 9c3d858..4769e51 100644 --- a/src/commands/start-broker.ts +++ b/src/commands/start-broker.ts @@ -3,8 +3,8 @@ import CEXBroker from '../index'; /** * CLI Command wrapper to start the CEXBroker */ -export async function startBrokerCommand(policyPath: string, port: number,whitelistIps: string[]) { - const broker = new CEXBroker({}, policyPath, { port,whitelistIps }); +export async function startBrokerCommand(policyPath: string, port: number,whitelistIps: string[],url: string) { + const broker = new CEXBroker({}, policyPath, { port,whitelistIps,verityProverUrl: url }); broker.loadEnvConfig(); await broker.run(); } diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index ab1edc7..307d268 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeEach, mock } from "bun:test"; import { validateWithdraw, validateOrder, validateDeposit } from "./index"; -import type { Exchange } from "ccxt"; -import type { PolicyConfig } from "../../types"; +import type { Exchange } from "@usherlabs/ccxt"; +import type { PolicyConfig } from "../types"; describe("Helper Functions", () => { let mockExchange: Exchange; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index be51132..dc5c5a5 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,4 +1,4 @@ -import type { PolicyConfig } from "../../types"; +import type { PolicyConfig } from "../types"; import fs from "fs"; import Joi from "joi"; diff --git a/src/index.ts b/src/index.ts index 5361d7c..c590b13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import ccxt, { type Exchange } from "ccxt"; +import ccxt, { type Exchange } from "@usherlabs/ccxt"; import * as grpc from "@grpc/grpc-js"; import { watchFile, unwatchFile } from "fs"; import Joi from "joi"; @@ -8,24 +8,30 @@ import { type BrokerCredentials, type ExchangeCredentials, type PolicyConfig, -} from "../types"; +} from "./types"; import { getServer } from "./server"; import { log } from "./helpers/logger"; log.info("CCXT Version:", ccxt.version); + export default class CEXBroker { - #brokerConfig: Record = {}; + #brokerConfig: ExchangeCredentials = {}; #policyFilePath?: string; + #verityProverUrl: string ="http://localhost:8080"; port = 8086; private policy: PolicyConfig; - private brokers: Record = {}; + private brokers: Record< + string, + { primary: Exchange; secondaryBrokers: Exchange[] } + > = {}; private whitelistIps: string[] = [ "127.0.0.1", // localhost "::1", // IPv6 localhost ]; private server: grpc.Server | null = null; + private useVerity: boolean = false; /** * Loads environment variables prefixed with CEX_BROKER_ @@ -35,12 +41,38 @@ export default class CEXBroker { */ public loadEnvConfig(): void { log.info("🔧 Loading CEX_BROKER_ environment variables:"); - const configMap: Record> = {}; + const configMap: Record< + string, + Partial & { + _secondaryMap?: Record; + } + > = {}; for (const [key, value] of Object.entries(process.env)) { if (!key.startsWith("CEX_BROKER_")) continue; - const match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); + // Match secondary keys like API_KEY_1, API_SECRET_1 + let match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)_(\d+)$/); + if (match) { + const broker = match[1].toLowerCase(); + const type = match[2].toLowerCase(); + const index = +match[3]; + + if (!configMap[broker]) configMap[broker] = {}; + if (!configMap[broker]._secondaryMap) + configMap[broker]._secondaryMap = {}; + if (!configMap[broker]._secondaryMap[index]) + configMap[broker]._secondaryMap[index] = {}; + + if (type === "key") { + configMap[broker]._secondaryMap[index].apiKey = value || ""; + } else if (type === "secret") { + configMap[broker]._secondaryMap[index].apiSecret = value || ""; + } + continue; + } + + match = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)$/); if (!match) { log.warn(`⚠️ Skipping unrecognized env var: ${key}`); continue; @@ -68,21 +100,64 @@ export default class CEXBroker { for (const [broker, creds] of Object.entries(configMap)) { const hasKey = !!creds.apiKey; const hasSecret = !!creds.apiSecret; + const ExchangeClass = (ccxt.pro as any)[broker]; if (hasKey && hasSecret) { + const secondaryKeys: { apiKey: string; apiSecret: string }[] = []; + const secondaryBrokers: Exchange[] = []; + + if (creds._secondaryMap) { + for (const index of Object.keys(creds._secondaryMap)) { + const sec = creds._secondaryMap[+index]; + if (!!sec?.apiKey && !!sec?.apiSecret) { + secondaryKeys[+index] = { + apiKey: sec.apiKey, + apiSecret: sec.apiSecret, + }; + secondaryBrokers[+index] = new ExchangeClass({ + apiKey: sec.apiKey, + secret: sec.apiSecret, + enableRateLimit: true, + defaultType: "spot", + useVerity: this.useVerity, + verityProverUrl: this.#verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000, + }, + }); + } else { + log.warn( + `⚠️ Incomplete secondary credentials for broker "${broker}" at index ${index}`, + ); + } + } + } + this.#brokerConfig[broker] = { apiKey: creds.apiKey ?? "", apiSecret: creds.apiSecret ?? "", + secondaryKeys: secondaryKeys, }; log.info(`✅ Loaded credentials for broker "${broker}"`); - const ExchangeClass = (ccxt as any)[broker]; const client = new ExchangeClass({ apiKey: creds.apiKey, secret: creds.apiSecret, enableRateLimit: true, defaultType: "spot", + useVerity: this.useVerity, + verityProverUrl: this.#verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000, + }, }); - this.brokers[broker] = client; + this.brokers[broker] = { + primary: client, + secondaryBrokers: secondaryBrokers, + }; } else { const missing = []; if (!hasKey) missing.push("API_KEY"); @@ -93,12 +168,14 @@ export default class CEXBroker { } /** - * Validates an exc hange credential object structure. + * Validates an exchange credential object structure. */ public loadExchangeCredentials( creds: unknown, ): asserts creds is ExchangeCredentials { - const schema = Joi.object>() + const schema = Joi.object< + Record + >() .pattern( Joi.string() .allow(...BrokerList) @@ -106,6 +183,14 @@ export default class CEXBroker { Joi.object({ apiKey: Joi.string().required(), apiSecret: Joi.string().required(), + secondaryKeys: Joi.array() + .items( + Joi.object({ + apiKey: Joi.string().required(), + apiSecret: Joi.string().required(), + }), + ) + .default([]), }), ) .required(); @@ -117,23 +202,71 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - log.info(`✅ Loaded credentials for broker "${broker}"`); - const ExchangeClass = (ccxt as any)[broker]; + const ExchangeClass = (ccxt.pro as any)[broker]; + + log.info( + `✅ Loaded credentials for broker "${broker}" (${1 + (creds.secondaryKeys?.length || 0)} key sets)`, + ); + const secondaryBroker: Exchange[] = []; + + for (const index of Object.keys(creds.secondaryKeys)) { + const sec = creds.secondaryKeys[+index]; + if (!!sec?.apiKey && !!sec?.apiSecret) { + secondaryBroker[+index] = new ExchangeClass({ + apiKey: sec.apiKey, + secret: sec.apiSecret, + enableRateLimit: true, + defaultType: "spot", + useVerity: this.useVerity, + verityProverUrl: this.#verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000, + }, + }); + } else { + log.warn( + `⚠️ Incomplete secondary credentials for broker "${broker}" at index ${index}`, + ); + } + } + + // Store full config, including secondary keys + this.#brokerConfig[broker] = { + apiKey: creds.apiKey, + apiSecret: creds.apiSecret, + secondaryKeys: creds.secondaryKeys ?? [], + }; + const client = new ExchangeClass({ apiKey: creds.apiKey, secret: creds.apiSecret, enableRateLimit: true, defaultType: "spot", + useVerity: this.useVerity, + verityProverUrl: this.#verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000, + }, }); - this.brokers[broker] = client; + + this.brokers[broker] = { + primary: client, + secondaryBrokers: secondaryBroker, + }; } } constructor( apiCredentials: ExchangeCredentials, policies: string | PolicyConfig, - config?: { port: number; whitelistIps: string[] }, + config?: { port?: number; whitelistIps?: string[]; useVerity?: boolean, verityProverUrl?: string }, ) { + this.useVerity = config?.useVerity || false; + if (typeof policies === "string") { this.#policyFilePath = policies; this.policy = loadPolicy(policies); @@ -146,10 +279,11 @@ export default class CEXBroker { if (this.#policyFilePath) { this.watchPolicyFile(this.#policyFilePath); } + this.#verityProverUrl = config?.verityProverUrl || "http://localhost:8080" this.loadExchangeCredentials(apiCredentials); this.whitelistIps = [ - ...(config ?? { whitelistIps: [] }).whitelistIps, + ...((config ?? { whitelistIps: [] }).whitelistIps ?? []), ...this.whitelistIps, ]; } @@ -197,7 +331,14 @@ export default class CEXBroker { await this.server.forceShutdown(); } log.info(`Running CEXBroker at ${new Date().toISOString()}`); - this.server = getServer(this.policy, this.brokers, this.whitelistIps); + this.server = getServer( + this.policy, + this.brokers, + this.whitelistIps, + this.useVerity, + this.#verityProverUrl, + + ); this.server.bindAsync( `0.0.0.0:${this.port}`, @@ -213,3 +354,4 @@ export default class CEXBroker { return this; } } + diff --git a/src/server.ts b/src/server.ts index 1fe69e6..fec2b2e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,622 +1,726 @@ -import { - validateDeposit, - validateOrder, - validateWithdraw, -} from "./helpers"; -import type { PolicyConfig } from "../types"; +import { validateDeposit, validateOrder, validateWithdraw } from "./helpers"; +import type { PolicyConfig } from "./types"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import path from "path"; -import type { Exchange } from "ccxt"; -import type { ActionRequest, ActionRequest__Output } from "../proto/cexBroker/ActionRequest"; +import type { Exchange } from "@usherlabs/ccxt"; +import type { + ActionRequest, + ActionRequest__Output, +} from "../proto/cexBroker/ActionRequest"; import type { ActionResponse } from "../proto/cexBroker/ActionResponse"; import { Action } from "../proto/cexBroker/Action"; import type { SubscribeRequest } from "../proto/cexBroker/SubscribeRequest"; import type { SubscribeResponse } from "../proto/cexBroker/SubscribeResponse"; import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; -import Joi from "joi"; -import ccxt from "ccxt"; -import from "ws" +import Joi, { options } from "joi"; +import ccxt from "@usherlabs/ccxt"; import { log } from "./helpers/logger"; - +import { Network } from "inspector/promises"; const PROTO_FILE = "../proto/node.proto"; const packageDef = protoLoader.loadSync(path.resolve(__dirname, PROTO_FILE)); const grpcObj = grpc.loadPackageDefinition( - packageDef, + packageDef, ) as unknown as ProtoGrpcType; const cexNode = grpcObj.cexBroker; -function authenticateRequest(call: grpc.ServerUnaryCall, whitelistIps: string[]): boolean { - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !whitelistIps.includes(clientIp)) { - log.warn( - `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, - ); - return false; - } - return true; +function authenticateRequest( + call: grpc.ServerUnaryCall, + whitelistIps: string[], +): boolean { + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || !whitelistIps.includes(clientIp)) { + log.warn(`Blocked access from unauthorized IP: ${clientIp || "unknown"}`); + return false; + } + return true; } -function createBroker(cex: string, metadata: grpc.Metadata): Exchange | null { - const api_key = metadata.get('api-key'); - const api_secret = metadata.get('api-secret'); - const ExchangeClass = (ccxt.pro as any)[cex]; - - metadata.remove('api-key'); - metadata.remove('api-secret'); - if (api_secret.length == 0 || api_key.length == 0) { - return null - } - return new ExchangeClass({ - apiKey: api_key[0], - secret: api_secret[0], - enableRateLimit: true, - defaultType: "spot", - }); -} -export function getServer(policy: PolicyConfig, brokers: Record, whitelistIps: string[]) { - const server = new grpc.Server(); - server.addService(cexNode.CexService.service, { - ExecuteAction: async ( - call: grpc.ServerUnaryCall< - ActionRequest, - ActionResponse - >, - callback: grpc.sendUnaryData, - ) => { - // IP Authentication - if (!authenticateRequest(call, whitelistIps)) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: "Access denied: Unauthorized IP", - }, - null, - ); - } - // Read incoming metadata - const metadata = call.metadata; - const { action, payload, cex, symbol } = call.request - // Validate required fields - if (!action || !cex || !symbol) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "action, cex, symbol, and cex are required", - }, - null, - ); - } - - - - const broker = brokers[cex as keyof typeof brokers] - ?? createBroker(cex, metadata); - - if (!broker) { - return callback( - { - code: grpc.status.UNAUTHENTICATED, - message: `This Exchange is not registered and No API metadata ws found`, - }, - null, - ); - } - - switch (action) { - case Action.Deposit: - const transactionSchema = Joi.object({ - recipientAddress: Joi.string() - .required(), - amount: Joi.number().positive().required(), // Must be a positive number - transactionHash: Joi.string().required() - }); - const { value, error } = transactionSchema.validate(call.request.payload) - if (error) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError:" + error.message, - }, - null, - ); - } - try { - const deposits = await broker.fetchDeposits(symbol, 50) - const deposit = deposits.find(deposit => deposit.id == value.transactionHash || deposit.txid == value.transactionHash) - - if (deposit) { - log.info( - `[${new Date().toISOString()}] ` + - `Amount ${value.amount} at ${value.transactionHash} . Paid to ${value.recipientAddress}`, - ); - deposit.network - return callback(null, { result: JSON.stringify({ ...deposit }) }); - } - callback( - { - code: grpc.status.INTERNAL, - message: "Deposit confirmation failed", - }, - null, - ); - } catch (error) { - log.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Deposit confirmation failed", - }, - null, - ); - } - break; - - case Action.Transfer: - const transferSchema = Joi.object({ - recipientAddress: Joi.string() - .required(), - amount: Joi.number().positive().required(), // Must be a positive number - chain: Joi.string().required() - }); - const { value: transferValue, error: transferError } = transferSchema.validate(call.request.payload) - if (transferError) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError:" + transferError.message, - }, - null, - ); - } - // Validate against policy - const transferValidation = validateWithdraw( - policy, - transferValue.chain, - transferValue.recipientAddress, - Number(transferValue.amount), - symbol, - ); - if (!transferValidation.valid) { - return callback( - { - code: grpc.status.PERMISSION_DENIED, - message: transferValidation.error, - }, - null, - ); - } - try { - const data = await broker.fetchCurrencies("USDT"); - const networks = Object.keys( - (data[symbol] ?? { networks: [] }).networks, - ); - - if (!networks.includes(transferValue.chain)) { - return callback( - { - code: grpc.status.INTERNAL, - message: `Broker ${cex} doesnt support this ${transferValue.chain} for token ${symbol}`, - }, - null, - ); - } - const transaction = await broker.withdraw( - symbol, - Number(transferValue.amount), - transferValue.recipientAddress, - undefined, - { network: transferValue.chain }, - ); - - callback(null, { result: JSON.stringify({ ...transaction }) }); - } catch (error) { - log.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Transfer failed", - }, - null, - ); - } - break; - - case Action.CreateOrder: - const createOrderSchema = Joi.object({ - orderType: Joi.string().valid("market", "limit").default("limit"), - amount: Joi.number().positive().required(), // Must be a positive number - fromToken: Joi.string().required(), - toToken: Joi.string().required(), - price: Joi.number().positive().required() - }); - const { value: orderValue, error: orderError } = createOrderSchema.validate(call.request.payload) - if (orderError) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError:" + orderError.message, - }, - null, - ); - } - const validation = validateOrder( - policy, - orderValue.fromToken, - orderValue.toToken, - Number(orderValue.amount), - cex, - ); - if (!validation.valid) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: validation.error, - }, - null, - ); - } - - try { - - const market = policy.order.rule.markets.find( - (market) => - market.includes(`${orderValue.fromToken}/${orderValue.toToken}`) || - market.includes(`${orderValue.toToken}/${orderValue.fromToken}`), - ); - const symbol = market?.split(":")[1] ?? ""; - const [from, _to] = symbol.split("/"); - - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const order = await broker.createOrder( - symbol, - orderValue.orderType, - from === orderValue.fromToken ? "sell" : "buy", - Number(orderValue.amount), - Number(orderValue.price), - ); - - callback(null, { result: JSON.stringify({ ...order }) }); - } catch (error) { - log.error({ error }); - callback( - { - code: grpc.status.INTERNAL, - message: "Order Creation failed", - }, - null, - ); - } - - break; - - case Action.GetOrderDetails: - const getOrderSchema = Joi.object({ - orderId: Joi.string().required(), - }); - const { value: getOrderValue, error: getOrderError } = getOrderSchema.validate(call.request.payload) - // Validate required fields - if (getOrderError) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError:" + getOrderError.message, - }, - null, - ); - } - - try { - // Validate CEX key - if (!broker) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, - }, - null, - ); - } - - const orderDetails = await broker.fetchOrder(getOrderValue.orderId); - - callback(null, { - result: JSON.stringify({ - orderId: orderDetails.id, - status: orderDetails.status, - originalAmount: orderDetails.amount, - filledAmount: orderDetails.filled, - symbol: orderDetails.symbol, - mode: orderDetails.side, - price: orderDetails.price, - }) - }); - } catch (error) { - log.error(`Error fetching order details from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch order details from ${cex}`, - }, - null, - ); - } - break; - case Action.CancelOrder: - const cancelOrderSchema = Joi.object({ - orderId: Joi.string().required(), - }); - const { value: cancelOrderValue, error: cancelOrderError } = cancelOrderSchema.validate(call.request.payload) - // Validate required fields - if (cancelOrderError) { - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError:" + cancelOrderError.message, - }, - null, - ); - } - - const cancelledOrder = await broker.cancelOrder(cancelOrderValue.orderId); - - callback(null, { - result: JSON.stringify({ ...cancelledOrder }) - }); - break; - case Action.FetchBalance: - try { - // Fetch balance from the specified CEX - const balance = (await broker.fetchFreeBalance()) as any; - const currencyBalance = balance[symbol]; - - callback(null, { - result: JSON.stringify({ - balance: currencyBalance || 0, - currency: symbol - }) - }); - } catch (error) { - log.error(`Error fetching balance from ${cex}:`, error); - callback( - { - code: grpc.status.INTERNAL, - message: `Failed to fetch balance from ${cex}`, - }, - null, - ); - } - break; - - default: - return callback( - { - code: grpc.status.INVALID_ARGUMENT, - message: - "Invalid Action", - }, - ) - } - }, - - Subscribe: async ( - call: grpc.ServerWritableStream, - ) => { - // IP Authentication - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !whitelistIps.includes(clientIp)) { - log.warn( - `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, - ); - call.destroy(new Error("Access denied: Unauthorized IP")); - return; - } - - // Read incoming metadata - const metadata = call.metadata; - let broker: Exchange | null = null; - - try { - // For ServerWritableStream, we need to get the request from the call - // The request should be available in the call object - const request = call.request as SubscribeRequest; - const { cex, symbol, type, options } = request; - - // Validate required fields - if (!cex || !symbol || type === undefined) { - call.write({ - data: JSON.stringify({ error: "cex, symbol, and type are required" }), - timestamp: Date.now(), - symbol: symbol || "", - type: type || SubscriptionType.ORDERBOOK, - }); - call.end(); - return; - } - - // Get or create broker - broker = brokers[cex as keyof typeof brokers] ?? createBroker(cex, metadata); - - if (!broker) { - call.write({ - data: JSON.stringify({ error: "Exchange not registered and no API metadata found" }), - timestamp: Date.now(), - symbol, - type, - }); - call.end(); - return; - } - - // Handle different subscription types - switch (type) { - case SubscriptionType.ORDERBOOK: - try { - const orderbook = await broker.watchOrderBook(symbol); - call.write({ - data: JSON.stringify(orderbook), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching orderbook for ${symbol} on ${cex}:`, error); - call.write({ - data: JSON.stringify({ error: `Failed to fetch orderbook: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - case SubscriptionType.TRADES: - try { - const trades = await broker.watchTrades(symbol); - call.write({ - data: JSON.stringify(trades), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching trades for ${symbol} on ${cex}:`, error.message); - call.write({ - data: JSON.stringify({ error: `Failed to fetch trades: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - case SubscriptionType.TICKER: - try { - const ticker = await broker.watchTicker(symbol); - call.write({ - data: JSON.stringify(ticker), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching ticker for ${symbol} on ${cex}:`, error.message); - call.write({ - data: JSON.stringify({ error: `Failed to fetch ticker: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - case SubscriptionType.OHLCV: - try { - const timeframe = options?.timeframe || '1m'; - const ohlcv = await broker.watchOHLCV(symbol, timeframe); - call.write({ - data: JSON.stringify(ohlcv), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching OHLCV for ${symbol} on ${cex}:`, error.message); - call.write({ - data: JSON.stringify({ error: `Failed to fetch OHLCV: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - case SubscriptionType.BALANCE: - try { - const balance = await broker.watchBalance(); - call.write({ - data: JSON.stringify(balance), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching balance for ${cex}:`, error.message); - call.write({ - data: JSON.stringify({ error: `Failed to fetch balance: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - case SubscriptionType.ORDERS: - try { - const orders = await broker.watchOrders(symbol); - call.write({ - data: JSON.stringify(orders), - timestamp: Date.now(), - symbol, - type, - }); - } catch (error:any) { - log.error(`Error fetching orders for ${symbol} on ${cex}:`, error); - call.write({ - data: JSON.stringify({ error: `Failed to fetch orders: ${error.message}` }), - timestamp: Date.now(), - symbol, - type, - }); - } - break; - - default: - call.write({ - data: JSON.stringify({ error: "Invalid subscription type" }), - timestamp: Date.now(), - symbol, - type, - }); - } - } catch (error) { - log.error("Error in Subscribe stream:", error); - call.write({ - data: JSON.stringify({ error: `Internal server error: ${error}` }), - timestamp: Date.now(), - symbol: "", - type: SubscriptionType.ORDERBOOK, - }); - } - - call.on('end', () => { - log.info("Subscribe stream ended"); - }); - - call.on('error', (error) => { - log.error("Subscribe stream error:", error); - }); - }, - }); - return server; +export function getServer( + policy: PolicyConfig, + brokers: Record, + whitelistIps: string[], + useVerity: boolean, + verityProverUrl: string, +) { + const server = new grpc.Server(); + function createBroker(cex: string, metadata: grpc.Metadata, secondaryBrokers: Exchange[]): Exchange | null { + const api_key = metadata.get("api-key"); + const api_secret = metadata.get("api-secret"); + const use_secondary_key = metadata.get("use-secondary-key"); + if (use_secondary_key.length > 0) { + return secondaryBrokers[use_secondary_key.length - 1] ?? null + } + + const ExchangeClass = (ccxt.pro as any)[cex]; + + metadata.remove("api-key"); + metadata.remove("api-secret"); + if (api_secret.length == 0 || api_key.length == 0) { + return null; + } + const exchange = new ExchangeClass({ + apiKey: api_key[0], + secret: api_secret[0], + enableRateLimit: true, + defaultType: "spot", + useVerity: useVerity, + verityProverUrl: verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000 + } + }); + exchange.options['recvWindow'] = 60000; + return exchange; + } + server.addService(cexNode.CexService.service, { + ExecuteAction: async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ) => { + // IP Authentication + if (!authenticateRequest(call, whitelistIps)) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, + ); + } + // Read incoming metadata + const metadata = call.metadata; + const { action, payload, cex, symbol } = call.request; + // Validate required fields + if (!action || !cex || !symbol) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "action, cex, symbol, and cex are required", + }, + null, + ); + } + + const broker = + brokers[cex as keyof typeof brokers]?.primary ?? createBroker(cex, metadata, brokers[cex as keyof typeof brokers]?.secondaryBrokers ?? []); + + + if (!broker) { + return callback( + { + code: grpc.status.UNAUTHENTICATED, + message: `This Exchange is not registered and No API metadata ws found`, + }, + null, + ); + } + + switch (action) { + case Action.Deposit: + const transactionSchema = Joi.object({ + recipientAddress: Joi.string().required(), + amount: Joi.number().positive().required(), // Must be a positive number + transactionHash: Joi.string().required(), + }); + const { value, error } = transactionSchema.validate( + call.request.payload??{}, + ); + if (error) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError:" + error.message, + }, + null, + ); + } + try { + const deposits = await broker.fetchDeposits(symbol, 50); + const deposit = deposits.find( + (deposit) => + deposit.id == value.transactionHash || + deposit.txid == value.transactionHash, + ); + + if (deposit) { + log.info( + `Amount ${value.amount} at ${value.transactionHash} . Paid to ${value.recipientAddress}`, + ); + return callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...deposit }) }); + } + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } catch (error) { + log.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } + break; + + case Action.FetchDepositAddresses: + const fetchDepositAddressesSchema = Joi.object({ + chain: Joi.string().required(), + }) + const { value: fetchDepositAddresses, error: errorFetchDepositAddresses } = fetchDepositAddressesSchema.validate( + call.request.payload??{}, + ); + if (errorFetchDepositAddresses) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError: " + errorFetchDepositAddresses?.message, + }, + null, + ); + } + try { + const depositAddresses = broker.has.fetchDepositAddress == true ? await broker.fetchDepositAddress(symbol, { network: fetchDepositAddresses.chain }) : await broker.fetchDepositAddressesByNetwork(symbol, { network: fetchDepositAddresses.chain }); + + if (depositAddresses) { + return callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...depositAddresses }) }); + } + callback( + { + code: grpc.status.INTERNAL, + message: "Deposit confirmation failed", + }, + null, + ); + } catch (error: any) { + log.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Fetch Deposit Addresses confirmation failed: " + error.message, + }, + null, + ); + } + break; + case Action.Transfer: + const transferSchema = Joi.object({ + recipientAddress: Joi.string().required(), + amount: Joi.number().positive().required(), // Must be a positive number + chain: Joi.string().required(), + }); + const { value: transferValue, error: transferError } = + transferSchema.validate(call.request.payload??{}) + if (transferError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError:" + transferError?.message, + }, + null, + ); + } + // Validate against policy + const transferValidation = validateWithdraw( + policy, + transferValue.chain, + transferValue.recipientAddress, + Number(transferValue.amount), + symbol, + ); + if (!transferValidation.valid) { + return callback( + { + code: grpc.status.PERMISSION_DENIED, + message: transferValidation.error, + }, + null, + ); + } + try { + const data = await broker.fetchCurrencies("USDT"); + const networks = Object.keys( + (data[symbol] ?? { networks: [] }).networks, + ); + + if (!networks.includes(transferValue.chain)) { + return callback( + { + code: grpc.status.INTERNAL, + message: `Broker ${cex} doesnt support this ${transferValue.chain} for token ${symbol}`, + }, + null, + ); + } + const transaction = await broker.withdraw( + symbol, + Number(transferValue.amount), + transferValue.recipientAddress, + undefined, + { network: transferValue.chain }, + ); + log.info("Transfer Transfer" + JSON.stringify(transaction) + ); + + callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...transaction }) }); + } catch (error) { + log.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Transfer failed", + }, + null, + ); + } + break; + + case Action.CreateOrder: + const createOrderSchema = Joi.object({ + orderType: Joi.string().valid("market", "limit").default("limit"), + amount: Joi.number().positive().required(), // Must be a positive number + fromToken: Joi.string().required(), + toToken: Joi.string().required(), + price: Joi.number().positive().required(), + }); + const { value: orderValue, error: orderError } = + createOrderSchema.validate(call.request.payload??{}); + if (orderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError:" + orderError.message, + }, + null, + ); + } + const validation = validateOrder( + policy, + orderValue.fromToken, + orderValue.toToken, + Number(orderValue.amount), + cex, + ); + if (!validation.valid) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: validation.error, + }, + null, + ); + } + + try { + const market = policy.order.rule.markets.find( + (market) => + market.includes( + `${orderValue.fromToken}/${orderValue.toToken}`, + ) || + market.includes( + `${orderValue.toToken}/${orderValue.fromToken}`, + ), + ); + const symbol = market?.split(":")[1] ?? ""; + const [from, _to] = symbol.split("/"); + + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const order = await broker.createOrder( + symbol, + orderValue.orderType, + from === orderValue.fromToken ? "sell" : "buy", + Number(orderValue.amount), + Number(orderValue.price), + ); + + callback(null, { result: JSON.stringify({ ...order }) }); + } catch (error) { + log.error({ error }); + callback( + { + code: grpc.status.INTERNAL, + message: "Order Creation failed", + }, + null, + ); + } + + break; + + case Action.GetOrderDetails: + const getOrderSchema = Joi.object({ + orderId: Joi.string().required(), + }); + const { value: getOrderValue, error: getOrderError } = + getOrderSchema.validate(call.request.payload??{}) + // Validate required fields + if (getOrderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError:" + getOrderError.message, + }, + null, + ); + } + + try { + // Validate CEX key + if (!broker) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: `Invalid CEX key: ${cex}. Supported keys: ${Object.keys(brokers).join(", ")}`, + }, + null, + ); + } + + const orderDetails = await broker.fetchOrder(getOrderValue.orderId); + + callback(null, { + result: JSON.stringify({ + orderId: orderDetails.id, + status: orderDetails.status, + originalAmount: orderDetails.amount, + filledAmount: orderDetails.filled, + symbol: orderDetails.symbol, + mode: orderDetails.side, + price: orderDetails.price, + }), + }); + } catch (error) { + log.error(`Error fetching order details from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch order details from ${cex}`, + }, + null, + ); + } + break; + case Action.CancelOrder: + const cancelOrderSchema = Joi.object({ + orderId: Joi.string().required(), + }); + const { value: cancelOrderValue, error: cancelOrderError } = + cancelOrderSchema.validate(call.request.payload??{}); + // Validate required fields + if (cancelOrderError) { + return callback( + { + code: grpc.status.INVALID_ARGUMENT, + message: "ValidationError:" + cancelOrderError.message, + }, + null, + ); + } + + const cancelledOrder = await broker.cancelOrder( + cancelOrderValue.orderId, + ); + + callback(null, { + result: JSON.stringify({ ...cancelledOrder }), + }); + break; + case Action.FetchBalance: + try { + // Fetch balance from the specified CEX + const balance = (await broker.fetchFreeBalance()) as any; + const currencyBalance = balance[symbol]; + + callback(null, { + result: useVerity ? broker.last_proof : JSON.stringify({ + balance: currencyBalance || 0, + currency: symbol, + }), + }); + } catch (error) { + log.error(`Error fetching balance from ${cex}:`, error); + callback( + { + code: grpc.status.INTERNAL, + message: `Failed to fetch balance from ${cex}`, + }, + null, + ); + } + break; + + default: + return callback({ + code: grpc.status.INVALID_ARGUMENT, + message: "Invalid Action", + }); + } + }, + + Subscribe: async ( + call: grpc.ServerWritableStream, + ) => { + // IP Authentication + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || !whitelistIps.includes(clientIp)) { + log.warn( + `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, + ); + call.destroy(new Error("Access denied: Unauthorized IP")); + return; + } + + // Read incoming metadata + const metadata = call.metadata; + let broker: Exchange | null = null; + + try { + // For ServerWritableStream, we need to get the request from the call + // The request should be available in the call object + const request = call.request as SubscribeRequest; + const { cex, symbol, type, options } = request; + + // Validate required fields + if (!cex || !symbol || type === undefined) { + call.write({ + data: JSON.stringify({ + error: "cex, symbol, and type are required", + }), + timestamp: Date.now(), + symbol: symbol || "", + type: type || SubscriptionType.ORDERBOOK, + }); + call.end(); + return; + } + + // Get or create broker + broker = + brokers[cex as keyof typeof brokers]?.primary ?? createBroker(cex, metadata, brokers[cex as keyof typeof brokers]?.secondaryBrokers ?? []); + + if (!broker) { + call.write({ + data: JSON.stringify({ + error: "Exchange not registered and no API metadata found", + }), + timestamp: Date.now(), + symbol, + type, + }); + call.end(); + return; + } + + // Handle different subscription types + switch (type) { + case SubscriptionType.ORDERBOOK: + try { + while (true) { + const orderbook = await broker.watchOrderBook(symbol); + call.write({ + data: JSON.stringify(orderbook), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error( + `Error fetching orderbook for ${symbol} on ${cex}:`, + error, + ); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch orderbook: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.TRADES: + try { + while (true) { + + const trades = await broker.watchTrades(symbol); + call.write({ + data: JSON.stringify(trades), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error( + `Error fetching trades for ${symbol} on ${cex}:`, + error.message, + ); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch trades: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.TICKER: + try { + while (true) { + const ticker = await broker.watchTicker(symbol); + call.write({ + data: JSON.stringify(ticker), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error( + `Error fetching ticker for ${symbol} on ${cex}:`, + error.message, + ); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch ticker: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.OHLCV: + try { + while (true) { + const timeframe = options?.timeframe || "1m"; + const ohlcv = await broker.fetchOHLCVWs(symbol, timeframe); + call.write({ + data: JSON.stringify(ohlcv), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error( + `Error fetching OHLCV for ${symbol} on ${cex}:`, + error.message, + ); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch OHLCV: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.BALANCE: + try { + while (true) { + const balance = await broker.watchBalance(); + call.write({ + data: JSON.stringify(balance), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error(`Error fetching balance for ${cex}:`, error.message); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch balance: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + case SubscriptionType.ORDERS: + try { + while (true) { + const orders = await broker.watchOrders(symbol); + call.write({ + data: JSON.stringify(orders), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error: any) { + log.error( + `Error fetching orders for ${symbol} on ${cex}:`, + error, + ); + call.write({ + data: JSON.stringify({ + error: `Failed to fetch orders: ${error.message}`, + }), + timestamp: Date.now(), + symbol, + type, + }); + } + break; + + default: + call.write({ + data: JSON.stringify({ error: "Invalid subscription type" }), + timestamp: Date.now(), + symbol, + type, + }); + } + } catch (error) { + log.error("Error in Subscribe stream:", error); + call.write({ + data: JSON.stringify({ error: `Internal server error: ${error}` }), + timestamp: Date.now(), + symbol: "", + type: SubscriptionType.ORDERBOOK, + }); + } + + call.on("end", () => { + log.info("Subscribe stream ended"); + }); + + call.on("error", (error) => { + log.error("Subscribe stream error:", error); + }); + }, + }); + return server; } diff --git a/types.ts b/src/types.ts similarity index 94% rename from types.ts rename to src/types.ts index f4f409c..54ca574 100644 --- a/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import ccxt from "ccxt"; +import ccxt from "@usherlabs/ccxt"; // Policy types based on the policy.json structure export type WithdrawRule = { @@ -186,7 +186,10 @@ export type BrokerCredentials = { apiKey: string; apiSecret: string; }; +export type SecondaryKeys={ + secondaryKeys: Array +} export interface ExchangeCredentials { - [exchange: string]: BrokerCredentials + [exchange: string]: BrokerCredentials & SecondaryKeys } diff --git a/tsconfig.json b/tsconfig.json index bc093e4..d2346de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,9 +5,9 @@ "target": "ESNext", "module": "Preserve", "moduleDetection": "force", - "jsx": "react-jsx", "allowJs": true, "types": ["@types/bun", "bun-types" ], + // Bundler mode "moduleResolution": "bundler", From 2fdde36dd315a7cc8ff31c0e4c489af7c254c1a9 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 20 Jul 2025 20:27:12 +0100 Subject: [PATCH 29/45] Enhance README with streaming and ZK proof details --- README.md | 205 +++++++++++++++++++++++++++++++++++++++++++++------ src/index.ts | 14 ++-- 2 files changed, 191 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index e56bab1..ee8bcf1 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,28 @@ # CEX Broker -A high-performance gRPC-based cryptocurrency exchange broker service that provides unified access to multiple centralized exchanges (CEX) through the CCXT library. Built with TypeScript, Bun, and designed for reliable trading operations with policy enforcement. +A high-performance gRPC-based cryptocurrency exchange broker service that provides unified access to multiple centralized exchanges (CEX) through the CCXT library. Built with TypeScript, Bun, and designed for reliable trading operations with policy enforcement, real-time streaming, and zero-knowledge proof integration. ## 🚀 Features - **Multi-Exchange Support**: Unified API to any CEX supported by [CCXT](https://github.com/ccxt/ccxt) (100+ exchanges) - **gRPC Interface**: High-performance RPC communication with type safety +- **Real-time Streaming**: Live orderbook, trades, ticker, OHLCV, balance, and order updates - **Policy Enforcement**: Configurable trading and withdrawal limits with real-time policy updates - **IP Authentication**: Security through IP whitelisting +- **Zero-Knowledge Proofs**: Optional Verity integration for privacy-preserving operations +- **Secondary Broker Support**: Multiple API keys per exchange for load balancing and redundancy - **Real-time Policy Updates**: Hot-reload policy changes without server restart - **Type Safety**: Full TypeScript support with generated protobuf types - **Comprehensive Logging**: Built-in logging with tslog - **CLI Support**: Command-line interface for easy management +- **Deposit Address Management**: Fetch deposit addresses for supported networks +- **Advanced Order Management**: Create, fetch, and cancel orders with full details ## 📋 Prerequisites - [Bun](https://bun.sh) (v1.2.17 or higher) - API keys for supported exchanges (e.g., Binance, Bybit, etc.) +- Optional: Verity prover URL for zero-knowledge proof integration ## 🛠️ Installation @@ -46,13 +52,19 @@ The broker loads configuration from environment variables with the `CEX_BROKER_` # Server Configuration PORT_NUM=8086 -# Exchange API Keys (format: CEX_BROKER__API_KEY/SECRET) +# Primary Exchange API Keys (format: CEX_BROKER__API_KEY/SECRET) CEX_BROKER_BINANCE_API_KEY=your_binance_api_key CEX_BROKER_BINANCE_API_SECRET=your_binance_api_secret CEX_BROKER_BYBIT_API_KEY=your_bybit_api_key CEX_BROKER_BYBIT_API_SECRET=your_bybit_api_secret CEX_BROKER_KRAKEN_API_KEY=your_kraken_api_key CEX_BROKER_KRAKEN_API_SECRET=your_kraken_api_secret + +# Secondary Exchange API Keys (for load balancing and redundancy) +CEX_BROKER_BINANCE_API_KEY_1=your_secondary_binance_api_key +CEX_BROKER_BINANCE_API_SECRET_1=your_secondary_binance_api_secret +CEX_BROKER_BINANCE_API_KEY_2=your_tertiary_binance_api_key +CEX_BROKER_BINANCE_API_SECRET_2=your_tertiary_binance_api_secret ``` **Note**: Only configure API keys for exchanges you plan to use. The system will automatically detect and initialize configured exchanges. @@ -106,6 +118,9 @@ Configure trading policies in `policy/policy.json`: ### Starting the Server ```bash +# Using the CLI (recommended) +bun run start-broker --policy policy/policy.json --port 8086 --whitelist 127.0.0.1 192.168.1.100 --verityProverUrl http://localhost:8080 + # Development mode bun run start @@ -114,6 +129,18 @@ bun run build:ts bun run ./build/index.js ``` +### CLI Options + +```bash +cex-broker --help + +Options: + -p, --policy Policy JSON file (required) + --port Port number (default: 8086) + -w, --whitelist IPv4 address whitelist (space-separated list) + -vu, --verityProverUrl Verity Prover URL for zero-knowledge proofs +``` + ### Available Scripts ```bash @@ -141,37 +168,38 @@ bun run check ## 📡 API Reference -The service exposes a gRPC interface with the following method: +The service exposes a gRPC interface with two main methods: -### ExecuteCcxtAction +### ExecuteAction -Execute any CCXT method on supported exchanges. +Execute trading operations on supported exchanges. **Request:** ```protobuf -message CcxtActionRequest { - Action action = 1; // The CCXT method to call - map payload = 2; // Parameters to pass to the CCXT method +message ActionRequest { + Action action = 1; // The action to perform + map payload = 2; // Parameters for the action string cex = 3; // CEX identifier (e.g., "binance", "bybit") - string symbol = 4; // Optional: trading pair symbol if needed + string symbol = 4; // Trading pair symbol if needed } ``` **Response:** ```protobuf -message CcxtActionResponse { - string result = 2; // JSON string of the result data +message ActionResponse { + string result = 2; // JSON string of the result data or ZK proof } ``` **Available Actions:** - `NoAction` (0): No operation -- `Deposit` (1): Deposit funds +- `Deposit` (1): Confirm deposit transaction - `Transfer` (2): Transfer/withdraw funds - `CreateOrder` (3): Create a new order - `GetOrderDetails` (4): Get order information - `CancelOrder` (5): Cancel an existing order - `FetchBalance` (6): Get account balance +- `FetchDepositAddresses` (7): Get deposit addresses for a token/network **Example Usage:** @@ -181,31 +209,99 @@ const balanceRequest = { action: 6, // FetchBalance payload: {}, cex: "binance", - symbol: "" + symbol: "USDT" }; // Create order const orderRequest = { action: 3, // CreateOrder payload: { - symbol: "BTC/USDT", - type: "limit", - side: "buy", + orderType: "limit", amount: "0.001", + fromToken: "BTC", + toToken: "USDT", price: "50000" }, cex: "binance", symbol: "BTC/USDT" }; + +// Fetch deposit addresses +const depositAddressRequest = { + action: 7, // FetchDepositAddresses + payload: { + chain: "BEP20" + }, + cex: "binance", + symbol: "USDT" +}; +``` + +### Subscribe (Streaming) + +Real-time streaming of market data and account updates. + +**Request:** +```protobuf +message SubscribeRequest { + string cex = 1; // CEX identifier + string symbol = 2; // Trading pair symbol + SubscriptionType type = 3; // Type of subscription + map options = 4; // Additional options (e.g., timeframe) +} +``` + +**Response Stream:** +```protobuf +message SubscribeResponse { + string data = 1; // JSON string of the streaming data + int64 timestamp = 2; // Unix timestamp + string symbol = 3; // Trading pair symbol + SubscriptionType type = 4; // Type of subscription +} +``` + +**Available Subscription Types:** +- `ORDERBOOK` (0): Real-time order book updates +- `TRADES` (1): Live trade feed +- `TICKER` (2): Ticker information updates +- `OHLCV` (3): Candlestick data (configurable timeframe) +- `BALANCE` (4): Account balance updates +- `ORDERS` (5): Order status updates + +**Example Usage:** + +```typescript +// Subscribe to orderbook updates +const orderbookRequest = { + cex: "binance", + symbol: "BTC/USDT", + type: 0, // ORDERBOOK + options: {} +}; + +// Subscribe to OHLCV with custom timeframe +const ohlcvRequest = { + cex: "binance", + symbol: "BTC/USDT", + type: 3, // OHLCV + options: { + timeframe: "1h" + } +}; ``` ## 🔒 Security ### IP Authentication -All API calls require IP authentication. Configure allowed IPs in the broker initialization: +All API calls require IP authentication. Configure allowed IPs via CLI or broker initialization: -```typescript +```bash +# Via CLI +cex-broker --policy policy.json --whitelist 127.0.0.1 192.168.1.100 + +# Via code const config = { port: 8086, whitelistIps: [ @@ -216,12 +312,54 @@ const config = { }; ``` +### Secondary Broker Support + +For high-availability and load balancing, you can configure multiple API keys per exchange: + +```env +# Primary keys +CEX_BROKER_BINANCE_API_KEY=primary_key +CEX_BROKER_BINANCE_API_SECRET=primary_secret + +# Secondary keys (numbered) +CEX_BROKER_BINANCE_API_KEY_1=secondary_key_1 +CEX_BROKER_BINANCE_API_SECRET_1=secondary_secret_1 +CEX_BROKER_BINANCE_API_KEY_2=secondary_key_2 +CEX_BROKER_BINANCE_API_SECRET_2=secondary_secret_2 +``` + +To use secondary brokers, include the `use-secondary-key` metadata in your gRPC calls: + +```typescript +const metadata = new grpc.Metadata(); +metadata.set('use-secondary-key', '1'); // Use secondary broker 1 +metadata.set('use-secondary-key', '2'); // Use secondary broker 2 +``` + +### Zero-Knowledge Proof Integration + +Enable privacy-preserving operations with Verity integration: + +```bash +# Start with Verity integration +cex-broker --policy policy.json --verityProverUrl http://localhost:8080 +``` + +When Verity is enabled, responses include zero-knowledge proofs instead of raw data: + +```typescript +// With Verity enabled +const response = await client.ExecuteAction(request, metadata); +// response.result contains ZK proof instead of raw data +``` + ### API Key Management - Store API keys securely in environment variables - Use read-only API keys when possible - Regularly rotate API keys - Monitor API usage and set appropriate rate limits +- Use secondary brokers for redundancy and load distribution ## 🏗️ Architecture @@ -231,10 +369,14 @@ const config = { fietCexBroker/ ├── src/ # Source code │ ├── commands/ # CLI commands +│ │ └── start-broker.ts # Broker startup command │ ├── helpers/ # Utility functions +│ │ ├── index.ts # Policy validation helpers +│ │ └── logger.ts # Logging configuration │ ├── index.ts # Main broker class │ ├── server.ts # gRPC server implementation -│ └── cli.ts # CLI entry point +│ ├── cli.ts # CLI entry point +│ └── types.ts # TypeScript type definitions ├── proto/ # Protocol buffer definitions │ ├── node.proto # Service definition │ └── node.ts # Type exports @@ -242,7 +384,7 @@ fietCexBroker/ │ └── policy.json # Trading and withdrawal rules ├── scripts/ # Build scripts ├── test/ # Test files -├── types.ts # TypeScript type definitions +├── patches/ # Dependency patches ├── build.ts # Build configuration └── package.json # Dependencies and scripts ``` @@ -251,8 +393,10 @@ fietCexBroker/ - **CEXBroker**: Main broker class that manages exchange connections and policy enforcement - **Policy System**: Real-time policy validation and enforcement -- **gRPC Server**: High-performance RPC interface +- **gRPC Server**: High-performance RPC interface with streaming support - **CCXT Integration**: Unified access to 100+ cryptocurrency exchanges +- **Verity Integration**: Zero-knowledge proof generation for privacy +- **Secondary Broker Management**: Load balancing and redundancy support ## 🧪 Development @@ -270,6 +414,22 @@ The broker automatically supports all exchanges available in CCXT. To add a new 3. The broker will automatically detect and initialize the exchange +### Using Secondary Brokers + +Secondary brokers provide redundancy and load balancing: + +1. Configure secondary API keys: + ```env + CEX_BROKER_BINANCE_API_KEY_1=secondary_key_1 + CEX_BROKER_BINANCE_API_SECRET_1=secondary_secret_1 + ``` + +2. Use secondary brokers in your gRPC calls: + ```typescript + const metadata = new grpc.Metadata(); + metadata.set('use-secondary-key', '1'); // Use secondary broker + ``` + ### Querying Supported Networks To understand which networks each exchange supports for deposits and withdrawals, you can query the exchange's currency information: @@ -367,7 +527,7 @@ bun run check - `@grpc/grpc-js`: gRPC server implementation - `@grpc/proto-loader`: Protocol buffer loading -- `ccxt`: Cryptocurrency exchange library (100+ exchanges) +- `@usherlabs/ccxt`: Enhanced CCXT library with Verity support - `commander`: CLI framework - `joi`: Configuration validation - `tslog`: TypeScript logging @@ -409,6 +569,7 @@ For issues and questions: - [CCXT](https://github.com/ccxt/ccxt) for providing unified access to cryptocurrency exchanges - [Bun](https://bun.sh) for the fast JavaScript runtime - [gRPC](https://grpc.io/) for high-performance RPC communication +- [Verity](https://usher.so/) for zero-knowledge proof integration --- diff --git a/src/index.ts b/src/index.ts index c590b13..7d76aec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,11 +14,10 @@ import { log } from "./helpers/logger"; log.info("CCXT Version:", ccxt.version); - export default class CEXBroker { #brokerConfig: ExchangeCredentials = {}; #policyFilePath?: string; - #verityProverUrl: string ="http://localhost:8080"; + #verityProverUrl: string = "http://localhost:8080"; port = 8086; private policy: PolicyConfig; private brokers: Record< @@ -263,7 +262,12 @@ export default class CEXBroker { constructor( apiCredentials: ExchangeCredentials, policies: string | PolicyConfig, - config?: { port?: number; whitelistIps?: string[]; useVerity?: boolean, verityProverUrl?: string }, + config?: { + port?: number; + whitelistIps?: string[]; + useVerity?: boolean; + verityProverUrl?: string; + }, ) { this.useVerity = config?.useVerity || false; @@ -279,7 +283,7 @@ export default class CEXBroker { if (this.#policyFilePath) { this.watchPolicyFile(this.#policyFilePath); } - this.#verityProverUrl = config?.verityProverUrl || "http://localhost:8080" + this.#verityProverUrl = config?.verityProverUrl || "http://localhost:8080"; this.loadExchangeCredentials(apiCredentials); this.whitelistIps = [ @@ -337,7 +341,6 @@ export default class CEXBroker { this.whitelistIps, this.useVerity, this.#verityProverUrl, - ); this.server.bindAsync( @@ -354,4 +357,3 @@ export default class CEXBroker { return this; } } - From 61e038b35cc761dec6ffe6866dfce9a60a45f6ba Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 20 Jul 2025 20:50:56 +0100 Subject: [PATCH 30/45] Add tests for CEXBroker, gRPC server, start command --- test/cex-broker.test.ts | 369 ++++++++++++++++++++++++++ test/server.test.ts | 535 ++++++++++++++++++++++++++++++++++++++ test/start-broker.test.ts | 133 ++++++++++ 3 files changed, 1037 insertions(+) create mode 100644 test/cex-broker.test.ts create mode 100644 test/server.test.ts create mode 100644 test/start-broker.test.ts diff --git a/test/cex-broker.test.ts b/test/cex-broker.test.ts new file mode 100644 index 0000000..2e0a708 --- /dev/null +++ b/test/cex-broker.test.ts @@ -0,0 +1,369 @@ +import { describe, test, expect, beforeEach, mock, afterEach } from "bun:test"; +import CEXBroker from "../src/index"; +import type { PolicyConfig } from "../src/types"; +import * as grpc from "@grpc/grpc-js"; + +describe("CEXBroker", () => { + let broker: CEXBroker; + let testPolicy: PolicyConfig; + + beforeEach(() => { + // Test policy configuration + testPolicy = { + withdraw: { + rule: { + networks: ["BEP20", "ETH"], + whitelist: ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"], + amounts: [ + { + ticker: "USDT", + max: 100000, + min: 1, + }, + ], + }, + }, + deposit: {}, + order: { + rule: { + markets: [ + "BINANCE:BTC/USDT", + "BINANCE:ETH/USDT", + ], + limits: [ + { from: "USDT", to: "BTC", min: 1, max: 100000 }, + { from: "BTC", to: "USDT", min: 0.001, max: 1 }, + ], + }, + }, + }; + + // Clear environment variables before each test + delete process.env.CEX_BROKER_BINANCE_API_KEY; + delete process.env.CEX_BROKER_BINANCE_API_SECRET; + delete process.env.CEX_BROKER_BINANCE_API_KEY_1; + delete process.env.CEX_BROKER_BINANCE_API_SECRET_1; + }); + + afterEach(() => { + if (broker) { + broker.stop(); + } + }); + + describe("Environment Configuration", () => { + test("should load primary API keys from environment", () => { + process.env.CEX_BROKER_BINANCE_API_KEY = "test_key"; + process.env.CEX_BROKER_BINANCE_API_SECRET = "test_secret"; + + broker = new CEXBroker({}, testPolicy); + broker.loadEnvConfig(); + + // Test that environment variables are loaded + expect(process.env.CEX_BROKER_BINANCE_API_KEY).toBe("test_key"); + expect(process.env.CEX_BROKER_BINANCE_API_SECRET).toBe("test_secret"); + }); + + test("should load secondary API keys from environment", () => { + process.env.CEX_BROKER_BINANCE_API_KEY_1 = "secondary_key_1"; + process.env.CEX_BROKER_BINANCE_API_SECRET_1 = "secondary_secret_1"; + process.env.CEX_BROKER_BINANCE_API_KEY_2 = "secondary_key_2"; + process.env.CEX_BROKER_BINANCE_API_SECRET_2 = "secondary_secret_2"; + + broker = new CEXBroker({}, testPolicy); + broker.loadEnvConfig(); + + // Test that secondary environment variables are loaded + expect(process.env.CEX_BROKER_BINANCE_API_KEY_1).toBe("secondary_key_1"); + expect(process.env.CEX_BROKER_BINANCE_API_SECRET_1).toBe("secondary_secret_1"); + expect(process.env.CEX_BROKER_BINANCE_API_KEY_2).toBe("secondary_key_2"); + expect(process.env.CEX_BROKER_BINANCE_API_SECRET_2).toBe("secondary_secret_2"); + }); + + test("should handle case-insensitive broker names", () => { + process.env.CEX_BROKER_BINANCE_API_KEY = "test_key"; + process.env.CEX_BROKER_BINANCE_API_SECRET = "test_secret"; + + broker = new CEXBroker({}, testPolicy); + broker.loadEnvConfig(); + + // Test that broker names are normalized to lowercase + expect(process.env.CEX_BROKER_BINANCE_API_KEY).toBe("test_key"); + }); + + test("should skip unrecognized environment variables", () => { + process.env.CEX_BROKER_INVALID_VAR = "invalid_value"; + process.env.CEX_BROKER_BINANCE_API_KEY = "test_key"; + + broker = new CEXBroker({}, testPolicy); + broker.loadEnvConfig(); + + // Test that only valid variables are processed + expect(process.env.CEX_BROKER_BINANCE_API_KEY).toBe("test_key"); + }); + + test("should handle empty API keys", () => { + process.env.CEX_BROKER_BINANCE_API_KEY = ""; + process.env.CEX_BROKER_BINANCE_API_SECRET = ""; + + broker = new CEXBroker({}, testPolicy); + broker.loadEnvConfig(); + + // Test that empty keys are handled + expect(process.env.CEX_BROKER_BINANCE_API_KEY).toBe(""); + }); + }); + + describe("Broker Initialization", () => { + test("should initialize with empty credentials", () => { + broker = new CEXBroker({}, testPolicy); + expect(broker).toBeDefined(); + }); + + test("should initialize with custom port", () => { + broker = new CEXBroker({}, testPolicy, { port: 9090 }); + expect(broker).toBeDefined(); + // Note: The port property might not be directly accessible due to private implementation + }); + + test("should initialize with custom whitelist IPs", () => { + const whitelistIps = ["192.168.1.100", "10.0.0.1"]; + broker = new CEXBroker({}, testPolicy, { whitelistIps }); + expect(broker).toBeDefined(); + }); + + test("should initialize with Verity integration", () => { + broker = new CEXBroker({}, testPolicy, { + useVerity: true, + verityProverUrl: "http://localhost:8080", + }); + expect(broker).toBeDefined(); + }); + + test("should use default port when not specified", () => { + broker = new CEXBroker({}, testPolicy); + expect(broker.port).toBe(8086); + }); + + test("should use default whitelist IPs when not specified", () => { + broker = new CEXBroker({}, testPolicy); + expect(broker).toBeDefined(); + }); + }); + + describe("Policy Management", () => { + test("should load policy from file path", () => { + broker = new CEXBroker({}, "./policy/policy.json"); + expect(broker).toBeDefined(); + }); + + test("should load policy from object", () => { + broker = new CEXBroker({}, testPolicy); + expect(broker).toBeDefined(); + }); + + test("should validate policy structure", () => { + const invalidPolicy = { + withdraw: {}, + // Missing order policy + }; + + // Test that invalid policy is handled + expect(invalidPolicy.order).toBeUndefined(); + }); + + test("should handle policy file watching", () => { + broker = new CEXBroker({}, "./policy/policy.json"); + expect(broker).toBeDefined(); + }); + }); + + describe("Exchange Credentials", () => { + test("should validate exchange credentials structure", () => { + const validCredentials = { + binance: { + apiKey: "test_key", + apiSecret: "test_secret", + }, + }; + + const invalidCredentials = { + binance: { + apiKey: "test_key", + // Missing apiSecret + }, + }; + + // Test validation logic + expect(validCredentials.binance.apiSecret).toBeDefined(); + expect(invalidCredentials.binance.apiSecret).toBeUndefined(); + }); + + test("should handle multiple exchanges", () => { + const credentials = { + binance: { + apiKey: "binance_key", + apiSecret: "binance_secret", + }, + bybit: { + apiKey: "bybit_key", + apiSecret: "bybit_secret", + }, + }; + + broker = new CEXBroker(credentials, testPolicy); + expect(broker).toBeDefined(); + }); + + test("should handle secondary broker credentials", () => { + const credentials = { + binance: { + apiKey: "primary_key", + apiSecret: "primary_secret", + }, + }; + + broker = new CEXBroker(credentials, testPolicy); + expect(broker).toBeDefined(); + }); + }); + + describe("Server Management", () => { + test("should start server successfully", async () => { + broker = new CEXBroker({}, testPolicy, { port: 0 }); // Use random port + const startedBroker = await broker.run(); + expect(startedBroker).toBe(broker); + }); + + test("should stop server successfully", () => { + broker = new CEXBroker({}, testPolicy); + broker.stop(); + expect(broker).toBeDefined(); + }); + + test("should handle server startup errors", async () => { + // Test with invalid port + broker = new CEXBroker({}, testPolicy, { port: -1 }); + + try { + await broker.run(); + expect(false).toBe(true); // Should not reach here + } catch (error) { + expect(error).toBeDefined(); + } + }); + }); + + describe("Configuration Validation", () => { + test("should validate port number", () => { + const validPort = 8086; + const invalidPort = -1; + + expect(validPort > 0 && validPort <= 65535).toBe(true); + expect(invalidPort > 0 && invalidPort <= 65535).toBe(false); + }); + + test("should validate IP addresses", () => { + const validIPs = ["127.0.0.1", "192.168.1.100"]; + const invalidIPs = ["invalid_ip", "256.256.256.256"]; + + const isValidIPv4 = (ip: string) => + /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && + ip.split(".").every((part) => Number(part) >= 0 && Number(part) <= 255); + + validIPs.forEach(ip => expect(isValidIPv4(ip)).toBe(true)); + invalidIPs.forEach(ip => expect(isValidIPv4(ip)).toBe(false)); + }); + + test("should validate Verity URL", () => { + const validURL = "http://localhost:8080"; + const invalidURL = "not_a_url"; + + const isValidURL = (url: string) => { + try { + new URL(url); + return true; + } catch { + return false; + } + }; + + expect(isValidURL(validURL)).toBe(true); + expect(isValidURL(invalidURL)).toBe(false); + }); + }); + + describe("Error Handling", () => { + test("should handle missing policy file", () => { + try { + broker = new CEXBroker({}, "./nonexistent/policy.json"); + expect(false).toBe(true); // Should not reach here + } catch (error) { + expect(error).toBeDefined(); + } + }); + + test("should handle invalid policy JSON", () => { + const invalidPolicy = "invalid json"; + + try { + JSON.parse(invalidPolicy); + expect(false).toBe(true); // Should not reach here + } catch (error) { + expect(error).toBeDefined(); + } + }); + + test("should handle network errors", () => { + const networkError = new Error("Network timeout"); + expect(networkError.message).toBe("Network timeout"); + }); + + test("should handle exchange initialization errors", () => { + const invalidCredentials = { + nonexistent: { + apiKey: "invalid_key", + apiSecret: "invalid_secret", + }, + }; + + // Test that invalid exchange is handled + expect(invalidCredentials.nonexistent).toBeDefined(); + }); + }); + + describe("Integration Tests", () => { + test("should initialize with all components", () => { + process.env.CEX_BROKER_BINANCE_API_KEY = "test_key"; + process.env.CEX_BROKER_BINANCE_API_SECRET = "test_secret"; + + broker = new CEXBroker({}, testPolicy, { + port: 8086, + whitelistIps: ["127.0.0.1"], + useVerity: false, + verityProverUrl: "http://localhost:8080", + }); + + expect(broker).toBeDefined(); + expect(broker.port).toBe(8086); + }); + + test("should handle complex configuration", () => { + const credentials = { + binance: { + apiKey: "primary_key", + apiSecret: "primary_secret", + }, + }; + + broker = new CEXBroker(credentials, testPolicy, { + port: 9090, + whitelistIps: ["192.168.1.100", "10.0.0.1"], + useVerity: true, + verityProverUrl: "http://verity:8080", + }); + + expect(broker).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/test/server.test.ts b/test/server.test.ts new file mode 100644 index 0000000..89763c4 --- /dev/null +++ b/test/server.test.ts @@ -0,0 +1,535 @@ +import { describe, test, expect, beforeEach, mock, afterEach } from "bun:test"; +import * as grpc from "@grpc/grpc-js"; +import { getServer } from "../src/server"; +import type { PolicyConfig } from "../src/types"; +import type { Exchange } from "@usherlabs/ccxt"; +import { Action } from "../proto/cexBroker/Action"; +import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; + +describe("gRPC Server", () => { + let mockExchange: Exchange; + let testPolicy: PolicyConfig; + let server: grpc.Server; + let brokers: Record; + + beforeEach(() => { + // Create comprehensive mock exchange + mockExchange = { + fetchDeposits: mock(async (symbol: string, limit: number) => [ + { + id: "tx123", + txid: "tx123", + amount: 100, + currency: symbol, + status: "ok", + timestamp: Date.now(), + }, + ]), + fetchDepositAddress: mock(async (symbol: string, params: any) => ({ + address: "0x1234567890123456789012345678901234567890", + tag: null, + network: params.network, + })), + fetchDepositAddressesByNetwork: mock(async (symbol: string, params: any) => ({ + address: "0x1234567890123456789012345678901234567890", + tag: null, + network: params.network, + })), + has: { + fetchDepositAddress: true, + }, + fetchCurrencies: mock(async (symbol: string) => ({ + [symbol]: { + networks: { + BEP20: { id: "BSC", network: "BSC", active: true, deposit: true, withdraw: true, fee: 1.0 }, + ETH: { id: "ETH", network: "ETH", active: true, deposit: true, withdraw: true, fee: 15.0 }, + }, + }, + })), + withdraw: mock(async (symbol: string, amount: number, address: string, tag: string, params: any) => ({ + id: "withdraw123", + amount, + address, + currency: symbol, + status: "ok", + timestamp: Date.now(), + })), + createOrder: mock(async (symbol: string, type: string, side: string, amount: number, price: number) => ({ + id: "order123", + symbol, + type, + side, + amount, + price, + status: "open", + timestamp: Date.now(), + })), + fetchOrder: mock(async (orderId: string) => ({ + id: orderId, + symbol: "BTC/USDT", + status: "closed", + amount: 0.001, + filled: 0.001, + side: "buy", + price: 50000, + })), + cancelOrder: mock(async (orderId: string) => ({ + id: orderId, + status: "canceled", + symbol: "BTC/USDT", + })), + fetchFreeBalance: mock(async () => ({ + USDT: 1000, + BTC: 0.1, + })), + watchOrderBook: mock(async (symbol: string) => ({ + symbol, + bids: [[50000, 1]], + asks: [[50001, 1]], + timestamp: Date.now(), + })), + watchTrades: mock(async (symbol: string) => [ + { + id: "trade123", + symbol, + amount: 0.001, + price: 50000, + side: "buy", + timestamp: Date.now(), + }, + ]), + watchTicker: mock(async (symbol: string) => ({ + symbol, + last: 50000, + bid: 49999, + ask: 50001, + volume: 100, + timestamp: Date.now(), + })), + fetchOHLCVWs: mock(async (symbol: string, timeframe: string) => [ + [Date.now(), 50000, 50001, 49999, 50000, 100], + ]), + watchBalance: mock(async () => ({ + free: { USDT: 1000, BTC: 0.1 }, + total: { USDT: 1000, BTC: 0.1 }, + })), + watchOrders: mock(async (symbol: string) => [ + { + id: "order123", + symbol, + status: "open", + amount: 0.001, + filled: 0, + side: "buy", + price: 50000, + }, + ]), + last_proof: "zk_proof_123", + } as any; + + // Test policy configuration + testPolicy = { + withdraw: { + rule: { + networks: ["BEP20", "ETH"], + whitelist: ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"], + amounts: [ + { + ticker: "USDT", + max: 100000, + min: 1, + }, + ], + }, + }, + deposit: {}, + order: { + rule: { + markets: [ + "BINANCE:BTC/USDT", + "BINANCE:ETH/USDT", + ], + limits: [ + { from: "USDT", to: "BTC", min: 1, max: 100000 }, + { from: "BTC", to: "USDT", min: 0.001, max: 1 }, + ], + }, + }, + }; + + brokers = { + binance: { + primary: mockExchange, + secondaryBrokers: [mockExchange, mockExchange], + }, + }; + + server = getServer(testPolicy, brokers, ["127.0.0.1"], false, "http://localhost:8080"); + }); + + afterEach(() => { + if (server) { + server.tryShutdown(() => {}); + } + }); + + describe("ExecuteAction", () => { + test("should authenticate IP correctly", () => { + const call = { + getPeer: () => "127.0.0.1:12345", + metadata: new grpc.Metadata(), + request: { + action: Action.FetchBalance, + payload: {}, + cex: "binance", + symbol: "USDT", + }, + } as any; + + const callback = mock((error: any, response: any) => {}); + + // This would require more complex mocking of the gRPC service + // For now, we test the authentication logic separately + expect(true).toBe(true); + }); + + test("should reject unauthorized IP", () => { + const call = { + getPeer: () => "192.168.1.100:12345", + metadata: new grpc.Metadata(), + request: { + action: Action.FetchBalance, + payload: {}, + cex: "binance", + symbol: "USDT", + }, + } as any; + + const callback = mock((error: any, response: any) => {}); + + // This would require more complex mocking of the gRPC service + // For now, we test the authentication logic separately + expect(true).toBe(true); + }); + + test("should validate required fields", () => { + const call = { + getPeer: () => "127.0.0.1:12345", + metadata: new grpc.Metadata(), + request: { + action: Action.FetchBalance, + payload: {}, + cex: "", // Missing cex + symbol: "USDT", + }, + } as any; + + const callback = mock((error: any, response: any) => {}); + + // This would require more complex mocking of the gRPC service + // For now, we test the validation logic separately + expect(true).toBe(true); + }); + }); + + describe("Action Handlers", () => { + describe("Deposit Action", () => { + test("should validate deposit payload correctly", () => { + const validPayload = { + recipientAddress: "0x1234567890123456789012345678901234567890", + amount: 100, + transactionHash: "tx123", + }; + + const invalidPayload = { + recipientAddress: "0x1234567890123456789012345678901234567890", + amount: -100, // Invalid amount + transactionHash: "tx123", + }; + + // Test validation logic + expect(validPayload.amount > 0).toBe(true); + expect(invalidPayload.amount > 0).toBe(false); + }); + + test("should find deposit by transaction hash", async () => { + const deposits = await mockExchange.fetchDeposits("USDT", 50); + const deposit = deposits.find((d: any) => d.id === "tx123" || d.txid === "tx123"); + + expect(deposit).toBeDefined(); + expect(deposit.id).toBe("tx123"); + }); + }); + + describe("FetchDepositAddresses Action", () => { + test("should validate chain parameter", () => { + const validPayload = { chain: "BEP20" }; + const invalidPayload = { chain: "" }; + + expect(validPayload.chain).toBeTruthy(); + expect(invalidPayload.chain).toBeFalsy(); + }); + + test("should fetch deposit address with network parameter", async () => { + const address = await mockExchange.fetchDepositAddress("USDT", { network: "BEP20" }); + expect(address.address).toBe("0x1234567890123456789012345678901234567890"); + expect(address.network).toBe("BEP20"); + }); + }); + + describe("Transfer Action", () => { + test("should validate transfer payload", () => { + const validPayload = { + recipientAddress: "0x9d467fa9062b6e9b1a46e26007ad82db116c67cb", + amount: 100, + chain: "BEP20", + }; + + const invalidPayload = { + recipientAddress: "0x1234567890123456789012345678901234567890", // Not whitelisted + amount: 100, + chain: "BEP20", + }; + + // Test validation logic + expect(validPayload.amount > 0).toBe(true); + expect(validPayload.chain).toBeTruthy(); + expect(validPayload.recipientAddress).toBeTruthy(); + }); + + test("should validate network support", async () => { + const currencies = await mockExchange.fetchCurrencies("USDT"); + const networks = Object.keys(currencies["USDT"].networks); + + expect(networks).toContain("BEP20"); + expect(networks).toContain("ETH"); + }); + }); + + describe("CreateOrder Action", () => { + test("should validate order payload", () => { + const validPayload = { + orderType: "limit", + amount: 0.001, + fromToken: "BTC", + toToken: "USDT", + price: 50000, + }; + + const invalidPayload = { + orderType: "invalid", + amount: -0.001, + fromToken: "BTC", + toToken: "USDT", + price: -50000, + }; + + // Test validation logic + expect(["market", "limit"].includes(validPayload.orderType)).toBe(true); + expect(validPayload.amount > 0).toBe(true); + expect(validPayload.price > 0).toBe(true); + }); + + test("should determine correct order side", () => { + const symbol = "BTC/USDT"; + const [from, to] = symbol.split("/"); + const fromToken = "BTC"; + const side = from === fromToken ? "sell" : "buy"; + + expect(side).toBe("sell"); + }); + }); + + describe("GetOrderDetails Action", () => { + test("should validate order ID", () => { + const validPayload = { orderId: "order123" }; + const invalidPayload = { orderId: "" }; + + expect(validPayload.orderId).toBeTruthy(); + expect(invalidPayload.orderId).toBeFalsy(); + }); + + test("should format order details correctly", async () => { + const orderDetails = await mockExchange.fetchOrder("order123"); + const formatted = { + orderId: orderDetails.id, + status: orderDetails.status, + originalAmount: orderDetails.amount, + filledAmount: orderDetails.filled, + symbol: orderDetails.symbol, + mode: orderDetails.side, + price: orderDetails.price, + }; + + expect(formatted.orderId).toBe("order123"); + expect(formatted.status).toBe("closed"); + expect(formatted.symbol).toBe("BTC/USDT"); + }); + }); + + describe("CancelOrder Action", () => { + test("should validate order ID for cancellation", () => { + const validPayload = { orderId: "order123" }; + const invalidPayload = { orderId: "" }; + + expect(validPayload.orderId).toBeTruthy(); + expect(invalidPayload.orderId).toBeFalsy(); + }); + + test("should cancel order successfully", async () => { + const cancelledOrder = await mockExchange.cancelOrder("order123"); + expect(cancelledOrder.status).toBe("canceled"); + expect(cancelledOrder.id).toBe("order123"); + }); + }); + + describe("FetchBalance Action", () => { + test("should fetch balance for specific symbol", async () => { + const balance = await mockExchange.fetchFreeBalance(); + const currencyBalance = balance["USDT"]; + + expect(currencyBalance).toBe(1000); + }); + + test("should handle missing currency balance", async () => { + const balance = await mockExchange.fetchFreeBalance(); + const currencyBalance = balance["INVALID"]; + + expect(currencyBalance).toBeUndefined(); + }); + }); + }); + + describe("Subscribe Stream", () => { + describe("Orderbook Subscription", () => { + test("should stream orderbook data", async () => { + const orderbook = await mockExchange.watchOrderBook("BTC/USDT"); + expect(orderbook.symbol).toBe("BTC/USDT"); + expect(orderbook.bids).toBeDefined(); + expect(orderbook.asks).toBeDefined(); + }); + }); + + describe("Trades Subscription", () => { + test("should stream trades data", async () => { + const trades = await mockExchange.watchTrades("BTC/USDT"); + expect(Array.isArray(trades)).toBe(true); + expect(trades.length).toBeGreaterThan(0); + expect(trades[0].symbol).toBe("BTC/USDT"); + }); + }); + + describe("Ticker Subscription", () => { + test("should stream ticker data", async () => { + const ticker = await mockExchange.watchTicker("BTC/USDT"); + expect(ticker.symbol).toBe("BTC/USDT"); + expect(ticker.last).toBeDefined(); + expect(ticker.bid).toBeDefined(); + expect(ticker.ask).toBeDefined(); + }); + }); + + describe("OHLCV Subscription", () => { + test("should stream OHLCV data with default timeframe", async () => { + const ohlcv = await mockExchange.fetchOHLCVWs("BTC/USDT", "1m"); + expect(Array.isArray(ohlcv)).toBe(true); + expect(ohlcv.length).toBeGreaterThan(0); + }); + + test("should stream OHLCV data with custom timeframe", async () => { + const ohlcv = await mockExchange.fetchOHLCVWs("BTC/USDT", "1h"); + expect(Array.isArray(ohlcv)).toBe(true); + expect(ohlcv.length).toBeGreaterThan(0); + }); + }); + + describe("Balance Subscription", () => { + test("should stream balance updates", async () => { + const balance = await mockExchange.watchBalance(); + expect(balance.free).toBeDefined(); + expect(balance.total).toBeDefined(); + expect(balance.free.USDT).toBe(1000); + }); + }); + + describe("Orders Subscription", () => { + test("should stream order updates", async () => { + const orders = await mockExchange.watchOrders("BTC/USDT"); + expect(Array.isArray(orders)).toBe(true); + expect(orders.length).toBeGreaterThan(0); + expect(orders[0].symbol).toBe("BTC/USDT"); + }); + }); + }); + + describe("Secondary Broker Support", () => { + test("should create broker with secondary keys", () => { + const metadata = new grpc.Metadata(); + metadata.set("api-key", "secondary_key"); + metadata.set("api-secret", "secondary_secret"); + metadata.set("use-secondary-key", "1"); + + // Test that secondary broker selection works + expect(metadata.get("use-secondary-key").length).toBe(1); + }); + + test("should fallback to primary broker when secondary not available", () => { + const metadata = new grpc.Metadata(); + metadata.set("use-secondary-key", "999"); // Non-existent secondary + + // Test fallback logic + expect(metadata.get("use-secondary-key").length).toBe(1); + }); + }); + + describe("Verity Integration", () => { + test("should return ZK proof when Verity is enabled", () => { + const serverWithVerity = getServer(testPolicy, brokers, ["127.0.0.1"], true, "http://localhost:8080"); + + // Test that Verity integration is configured + expect(serverWithVerity).toBeDefined(); + }); + + test("should return raw data when Verity is disabled", () => { + const serverWithoutVerity = getServer(testPolicy, brokers, ["127.0.0.1"], false, "http://localhost:8080"); + + // Test that Verity integration is not configured + expect(serverWithoutVerity).toBeDefined(); + }); + }); + + describe("Error Handling", () => { + test("should handle exchange errors gracefully", async () => { + // Mock exchange with error + const errorExchange = { + ...mockExchange, + fetchBalance: mock(async () => { + throw new Error("Exchange error"); + }), + } as any; + + // Test error handling + expect(errorExchange).toBeDefined(); + }); + + test("should handle network errors", async () => { + // Mock network error + const networkError = new Error("Network timeout"); + expect(networkError.message).toBe("Network timeout"); + }); + + test("should handle validation errors", () => { + const invalidRequest = { + action: Action.CreateOrder, + payload: { + amount: -1, // Invalid amount + }, + cex: "binance", + symbol: "BTC/USDT", + }; + + // Test validation error handling + expect(invalidRequest.payload.amount <= 0).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/test/start-broker.test.ts b/test/start-broker.test.ts new file mode 100644 index 0000000..1f86f2e --- /dev/null +++ b/test/start-broker.test.ts @@ -0,0 +1,133 @@ +import { describe, test, expect, beforeEach } from "bun:test"; +import { startBrokerCommand } from "../src/commands/start-broker"; + +describe("Start Broker Command", () => { + describe("Function Signature", () => { + test("should have correct function signature", () => { + expect(typeof startBrokerCommand).toBe("function"); + }); + }); + + describe("Parameter Validation", () => { + test("should validate policy path", () => { + const policyPath = "./policy/policy.json"; + expect(policyPath).toBeTruthy(); + expect(typeof policyPath).toBe("string"); + }); + + test("should validate port number", () => { + const port = 8086; + expect(port > 0 && port <= 65535).toBe(true); + }); + + test("should validate whitelist IPs", () => { + const whitelistIps = ["127.0.0.1", "192.168.1.100"]; + expect(Array.isArray(whitelistIps)).toBe(true); + whitelistIps.forEach(ip => { + expect(typeof ip).toBe("string"); + }); + }); + + test("should validate Verity prover URL", () => { + const verityProverUrl = "http://localhost:8080"; + if (verityProverUrl) { + expect(() => new URL(verityProverUrl)).not.toThrow(); + } + }); + }); + + describe("Configuration Options", () => { + test("should handle custom port configuration", () => { + const port = 9090; + expect(port > 0 && port <= 65535).toBe(true); + }); + + test("should handle multiple whitelist IPs", () => { + const whitelistIps = ["127.0.0.1", "192.168.1.100", "10.0.0.1"]; + expect(Array.isArray(whitelistIps)).toBe(true); + expect(whitelistIps.length).toBe(3); + }); + + test("should handle Verity integration configuration", () => { + const verityProverUrl = "https://verity.usher.so"; + expect(() => new URL(verityProverUrl)).not.toThrow(); + }); + + test("should handle empty whitelist", () => { + const whitelistIps: string[] = []; + expect(Array.isArray(whitelistIps)).toBe(true); + expect(whitelistIps.length).toBe(0); + }); + }); + + describe("Integration Tests", () => { + test("should validate complete configuration", () => { + const policyPath = "./policy/policy.json"; + const port = 8086; + const whitelistIps = ["127.0.0.1"]; + const verityProverUrl = "http://localhost:8080"; + + // Validate all parameters + expect(policyPath).toBeTruthy(); + expect(port > 0 && port <= 65535).toBe(true); + expect(Array.isArray(whitelistIps)).toBe(true); + expect(() => new URL(verityProverUrl)).not.toThrow(); + }); + + test("should handle complex configuration", () => { + const policyPath = "./policy/policy.json"; + const port = 9090; + const whitelistIps = [ + "127.0.0.1", + "192.168.1.100", + "10.0.0.1", + "172.16.0.1", + ]; + const verityProverUrl = "https://verity.usher.so/api/v1"; + + // Validate all parameters + expect(policyPath).toBeTruthy(); + expect(port > 0 && port <= 65535).toBe(true); + expect(Array.isArray(whitelistIps)).toBe(true); + expect(whitelistIps.length).toBe(4); + expect(() => new URL(verityProverUrl)).not.toThrow(); + }); + + test("should handle production-like configuration", () => { + const policyPath = "./policy/policy.json"; + const port = 443; + const whitelistIps = ["192.168.1.100", "10.0.0.1"]; + const verityProverUrl = "https://verity.production.usher.so"; + + // Validate all parameters + expect(policyPath).toBeTruthy(); + expect(port > 0 && port <= 65535).toBe(true); + expect(Array.isArray(whitelistIps)).toBe(true); + expect(whitelistIps.length).toBe(2); + expect(() => new URL(verityProverUrl)).not.toThrow(); + }); + }); + + describe("Edge Cases", () => { + test("should handle maximum port number", () => { + const port = 65535; + expect(port > 0 && port <= 65535).toBe(true); + }); + + test("should handle minimum port number", () => { + const port = 1; + expect(port > 0 && port <= 65535).toBe(true); + }); + + test("should handle large number of whitelist IPs", () => { + const whitelistIps = Array.from({ length: 100 }, (_, i) => `192.168.1.${i + 1}`); + expect(Array.isArray(whitelistIps)).toBe(true); + expect(whitelistIps.length).toBe(100); + }); + + test("should handle long Verity URL", () => { + const verityProverUrl = "https://very-long-verity-url.example.com/api/v1/prover/endpoint"; + expect(() => new URL(verityProverUrl)).not.toThrow(); + }); + }); +}); \ No newline at end of file From 2844d8f9045452f0449e8959df35b52f7badcd22 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 20 Jul 2025 20:54:46 +0100 Subject: [PATCH 31/45] Rename package to @usherlabs/cex-broker --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b3e61e2..2688094 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "cex-broker", + "name": "@usherlabs/cex-broker", "version": "0.0.1", "description": "Unified gRPC API to CEXs by Usher Labs", "repository": "git@gitlab.com:usherlabs/cex-broker.git", From 4390e06a526671e61564082af79f9a485b652650 Mon Sep 17 00:00:00 2001 From: xlassix Date: Sun, 20 Jul 2025 23:12:33 +0100 Subject: [PATCH 32/45] Fix spacing issues in server.ts file --- src/server.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/server.ts b/src/server.ts index fec2b2e..ea9ba9b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -49,12 +49,13 @@ export function getServer( verityProverUrl: string, ) { const server = new grpc.Server(); - function createBroker(cex: string, metadata: grpc.Metadata, secondaryBrokers: Exchange[]): Exchange | null { + function createBroker(cex: string, metadata: grpc.Metadata, secondaryBrokers: Exchange[]): Exchange | null { const api_key = metadata.get("api-key"); const api_secret = metadata.get("api-secret"); const use_secondary_key = metadata.get("use-secondary-key"); if (use_secondary_key.length > 0) { - return secondaryBrokers[use_secondary_key.length - 1] ?? null + const keyIndex = Number.isInteger(+(use_secondary_key[use_secondary_key.length - 1] ?? "0")) + return secondaryBrokers[+keyIndex] ?? null } const ExchangeClass = (ccxt.pro as any)[cex]; @@ -131,7 +132,7 @@ export function getServer( transactionHash: Joi.string().required(), }); const { value, error } = transactionSchema.validate( - call.request.payload??{}, + call.request.payload ?? {}, ); if (error) { return callback( @@ -180,13 +181,13 @@ export function getServer( chain: Joi.string().required(), }) const { value: fetchDepositAddresses, error: errorFetchDepositAddresses } = fetchDepositAddressesSchema.validate( - call.request.payload??{}, + call.request.payload ?? {}, ); if (errorFetchDepositAddresses) { return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError: " + errorFetchDepositAddresses?.message, + message: "ValidationError: " + errorFetchDepositAddresses?.message, }, null, ); @@ -222,7 +223,7 @@ export function getServer( chain: Joi.string().required(), }); const { value: transferValue, error: transferError } = - transferSchema.validate(call.request.payload??{}) + transferSchema.validate(call.request.payload ?? {}) if (transferError) { return callback( { @@ -296,7 +297,7 @@ export function getServer( price: Joi.number().positive().required(), }); const { value: orderValue, error: orderError } = - createOrderSchema.validate(call.request.payload??{}); + createOrderSchema.validate(call.request.payload ?? {}); if (orderError) { return callback( { @@ -373,7 +374,7 @@ export function getServer( orderId: Joi.string().required(), }); const { value: getOrderValue, error: getOrderError } = - getOrderSchema.validate(call.request.payload??{}) + getOrderSchema.validate(call.request.payload ?? {}) // Validate required fields if (getOrderError) { return callback( @@ -426,7 +427,7 @@ export function getServer( orderId: Joi.string().required(), }); const { value: cancelOrderValue, error: cancelOrderError } = - cancelOrderSchema.validate(call.request.payload??{}); + cancelOrderSchema.validate(call.request.payload ?? {}); // Validate required fields if (cancelOrderError) { return callback( From 4251ded787c7737d3b35d4849dcf0fbb75625201 Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Thu, 24 Jul 2025 16:59:24 +1000 Subject: [PATCH 33/45] prevent nodejs imports on lint --- biome.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index afa7be1..0a09e18 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "useNodejsImportProtocol": "off" + } } }, "javascript": { From 5c7fd0ace96a6ff35db0104c8208dd7b8e1b0191 Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Thu, 24 Jul 2025 17:02:19 +1000 Subject: [PATCH 34/45] adjust the gh actions to publish on release and use lints --- .github/workflows/{bun-ci.yml => ci.yml} | 12 +++++-- .github/workflows/publish.yml | 41 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) rename .github/workflows/{bun-ci.yml => ci.yml} (65%) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/ci.yml similarity index 65% rename from .github/workflows/bun-ci.yml rename to .github/workflows/ci.yml index df0c57d..715c2a3 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ -# .github/workflows/bun-ci.yml +# .github/workflows/ci.yml -name: Bun CI +name: CI on: push: @@ -24,5 +24,11 @@ jobs: - name: Install dependencies run: bun install + - name: Run Biome lint + run: bunx @biomejs/biome lint . + + - name: Run Biome format check + run: bunx @biomejs/biome format --write=false . + - name: Run tests - run: bun test + run: bun test \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ad3b91b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +# .github/workflows/publish.yml + +name: Publish + +on: + push: + tags: + - "v*" + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + + - name: Run Biome lint + run: bunx @biomejs/biome lint . + + - name: Run Biome format check + run: bunx @biomejs/biome format --write=false . + + - name: Build project + run: bun run build + + - name: Publish to npm + run: bun publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 939620c2b61971cdb3d43b8728798a340ce2595e Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Thu, 24 Jul 2025 17:15:19 +1000 Subject: [PATCH 35/45] minor adjust readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee8bcf1..c40a17f 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ const config = { ### Secondary Broker Support -For high-availability and load balancing, you can configure multiple API keys per exchange: +For high-availability, load balancing and compartmentalized capital management, **you can configure multiple API keys per exchange**: ```env # Primary keys @@ -338,7 +338,7 @@ metadata.set('use-secondary-key', '2'); // Use secondary broker 2 ### Zero-Knowledge Proof Integration -Enable privacy-preserving operations with Verity integration: +**Enable privacy-preserving proof over CEX data** with [Verity zkTLS integration](https://github.com/usherlabs/verity-dp): ```bash # Start with Verity integration From b6395346a87ad3db77154b03a63033d21313f4ad Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 10:27:23 +0100 Subject: [PATCH 36/45] Refactor type checks and remove mock exchange --- src/helpers/index.test.ts | 21 +-------------------- src/index.ts | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index 307d268..57230b1 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -1,30 +1,11 @@ -import { describe, test, expect, beforeEach, mock } from "bun:test"; +import { describe, test, expect, beforeEach } from "bun:test"; import { validateWithdraw, validateOrder, validateDeposit } from "./index"; -import type { Exchange } from "@usherlabs/ccxt"; import type { PolicyConfig } from "../types"; describe("Helper Functions", () => { - let mockExchange: Exchange; let testPolicy: PolicyConfig; beforeEach(() => { - // Create mock exchange - mockExchange = { - fetchOrderBook: mock(async (symbol: string) => ({ - bids: [ - [100, 10], // price, volume - [99, 20], - [98, 30], - [97, 40], - ], - asks: [ - [101, 10], - [102, 20], - [103, 30], - [104, 40], - ], - })), - } as any; // Test policy configuration testPolicy = { diff --git a/src/index.ts b/src/index.ts index 7d76aec..64d396d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,11 +51,11 @@ export default class CEXBroker { if (!key.startsWith("CEX_BROKER_")) continue; // Match secondary keys like API_KEY_1, API_SECRET_1 - let match: any = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)_(\d+)$/); + let match = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)_(\d+)$/); if (match) { - const broker = match[1].toLowerCase(); - const type = match[2].toLowerCase(); - const index = +match[3]; + const broker = match[1]?.toLowerCase()??""; + const type = match[2]?.toLowerCase(); + const index = Number(match[3]?.toLowerCase()); if (!configMap[broker]) configMap[broker] = {}; if (!configMap[broker]._secondaryMap) @@ -77,8 +77,8 @@ export default class CEXBroker { continue; } - const broker = match[1].toLowerCase(); // normalize to lowercase - const type = match[2].toLowerCase(); // 'key' or 'secret' + const broker = match[1]?.toLowerCase()??""; // normalize to lowercase + const type = match[2]?.toLowerCase()??""; // 'key' or 'secret' if (!configMap[broker]) { configMap[broker] = {}; @@ -99,7 +99,12 @@ export default class CEXBroker { for (const [broker, creds] of Object.entries(configMap)) { const hasKey = !!creds.apiKey; const hasSecret = !!creds.apiSecret; - const ExchangeClass = (ccxt.pro as any)[broker]; + const ExchangeClass = (ccxt.pro as Record)[broker]; + + if(!ExchangeClass){ + throw new Error(`Invalid Broker : ${broker}`); + } + if (hasKey && hasSecret) { const secondaryKeys: { apiKey: string; apiSecret: string }[] = []; @@ -201,7 +206,11 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - const ExchangeClass = (ccxt.pro as any)[broker]; + const ExchangeClass = (ccxt.pro as Record)[broker]; + + if(!ExchangeClass){ + throw new Error(`Invalid Broker : ${broker}`); + } log.info( `✅ Loaded credentials for broker "${broker}" (${1 + (creds.secondaryKeys?.length || 0)} key sets)`, From b8a2381f00261ca9cae2c5cf53108ba08749f106 Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 10:31:09 +0100 Subject: [PATCH 37/45] Fix spacing issues in index.ts and index.test.ts --- src/helpers/index.test.ts | 1 - src/index.ts | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index 57230b1..12ea70e 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -6,7 +6,6 @@ describe("Helper Functions", () => { let testPolicy: PolicyConfig; beforeEach(() => { - // Test policy configuration testPolicy = { withdraw: { diff --git a/src/index.ts b/src/index.ts index 64d396d..a5a7736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ export default class CEXBroker { // Match secondary keys like API_KEY_1, API_SECRET_1 let match = key.match(/^CEX_BROKER_(\w+)_API_(KEY|SECRET)_(\d+)$/); if (match) { - const broker = match[1]?.toLowerCase()??""; + const broker = match[1]?.toLowerCase() ?? ""; const type = match[2]?.toLowerCase(); const index = Number(match[3]?.toLowerCase()); @@ -77,8 +77,8 @@ export default class CEXBroker { continue; } - const broker = match[1]?.toLowerCase()??""; // normalize to lowercase - const type = match[2]?.toLowerCase()??""; // 'key' or 'secret' + const broker = match[1]?.toLowerCase() ?? ""; // normalize to lowercase + const type = match[2]?.toLowerCase() ?? ""; // 'key' or 'secret' if (!configMap[broker]) { configMap[broker] = {}; @@ -99,13 +99,14 @@ export default class CEXBroker { for (const [broker, creds] of Object.entries(configMap)) { const hasKey = !!creds.apiKey; const hasSecret = !!creds.apiSecret; - const ExchangeClass = (ccxt.pro as Record)[broker]; + const ExchangeClass = (ccxt.pro as Record)[ + broker + ]; - if(!ExchangeClass){ + if (!ExchangeClass) { throw new Error(`Invalid Broker : ${broker}`); } - if (hasKey && hasSecret) { const secondaryKeys: { apiKey: string; apiSecret: string }[] = []; const secondaryBrokers: Exchange[] = []; @@ -206,9 +207,11 @@ export default class CEXBroker { // Finalize config and print result per broker for (const [broker, creds] of Object.entries(value)) { - const ExchangeClass = (ccxt.pro as Record)[broker]; + const ExchangeClass = (ccxt.pro as Record)[ + broker + ]; - if(!ExchangeClass){ + if (!ExchangeClass) { throw new Error(`Invalid Broker : ${broker}`); } From 7bdc5254c05329bbed45dfeb9cfbd8f13aff5fa7 Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 10:36:47 +0100 Subject: [PATCH 38/45] Add IP authentication to server requests --- src/helpers/index.ts | 14 ++++++++++++++ src/server.ts | 18 ++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index dc5c5a5..373b45e 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,6 +1,20 @@ import type { PolicyConfig } from "../types"; import fs from "fs"; import Joi from "joi"; +import { log } from "./logger"; +import type { ServerUnaryCall } from "@grpc/grpc-js"; + +export function authenticateRequest( + call: ServerUnaryCall, + whitelistIps: string[], +): boolean { + const clientIp = call.getPeer().split(":")[0]; + if (!clientIp || !whitelistIps.includes(clientIp)) { + log.warn(`Blocked access from unauthorized IP: ${clientIp || "unknown"}`); + return false; + } + return true; +} /** * Loads and validates policy configuration diff --git a/src/server.ts b/src/server.ts index ea9ba9b..7d7aa67 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { validateDeposit, validateOrder, validateWithdraw } from "./helpers"; +import { authenticateRequest, validateDeposit, validateOrder, validateWithdraw } from "./helpers"; import type { PolicyConfig } from "./types"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; @@ -7,17 +7,15 @@ import path from "path"; import type { Exchange } from "@usherlabs/ccxt"; import type { ActionRequest, - ActionRequest__Output, } from "../proto/cexBroker/ActionRequest"; import type { ActionResponse } from "../proto/cexBroker/ActionResponse"; import { Action } from "../proto/cexBroker/Action"; import type { SubscribeRequest } from "../proto/cexBroker/SubscribeRequest"; import type { SubscribeResponse } from "../proto/cexBroker/SubscribeResponse"; import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; -import Joi, { options } from "joi"; +import Joi from "joi"; import ccxt from "@usherlabs/ccxt"; import { log } from "./helpers/logger"; -import { Network } from "inspector/promises"; const PROTO_FILE = "../proto/node.proto"; @@ -27,18 +25,6 @@ const grpcObj = grpc.loadPackageDefinition( ) as unknown as ProtoGrpcType; const cexNode = grpcObj.cexBroker; -function authenticateRequest( - call: grpc.ServerUnaryCall, - whitelistIps: string[], -): boolean { - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !whitelistIps.includes(clientIp)) { - log.warn(`Blocked access from unauthorized IP: ${clientIp || "unknown"}`); - return false; - } - return true; -} - export function getServer( From 406422989d8ac935e666b0fdd3299ca9b66cc81a Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 10:42:44 +0100 Subject: [PATCH 39/45] Refactor IP auth logic into separate function --- src/server.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/server.ts b/src/server.ts index 7d7aa67..38c2773 100644 --- a/src/server.ts +++ b/src/server.ts @@ -469,15 +469,16 @@ export function getServer( call: grpc.ServerWritableStream, ) => { // IP Authentication - const clientIp = call.getPeer().split(":")[0]; - if (!clientIp || !whitelistIps.includes(clientIp)) { - log.warn( - `Blocked access from unauthorized IP: ${clientIp || "unknown"}`, + if (!authenticateRequest(call, whitelistIps)) { + call.emit('error', + { + code: grpc.status.PERMISSION_DENIED, + message: "Access denied: Unauthorized IP", + }, + null, ); - call.destroy(new Error("Access denied: Unauthorized IP")); - return; + call.destroy(new Error("Access denied: Unauthorized IP")); } - // Read incoming metadata const metadata = call.metadata; let broker: Exchange | null = null; From d9b76212fe96320074165ad1a33814d4a61a5d80 Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Tue, 29 Jul 2025 00:58:50 +1000 Subject: [PATCH 40/45] fix the biome json file to cover all ts files --- biome.json | 14 ++++++++------ package.json | 6 ++++-- src/helpers/index.test.ts | 4 ++-- src/helpers/index.ts | 4 ++-- src/index.ts | 10 +++++----- src/server.ts | 18 ++++++++++++------ src/types.ts | 2 +- 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/biome.json b/biome.json index 0a09e18..d91c60a 100644 --- a/biome.json +++ b/biome.json @@ -8,12 +8,10 @@ "files": { "ignoreUnknown": true, "includes": [ - "proto/", - "src/index.ts", - "src/helpers/*.ts", - "config/", - "./src/commands", - "policy/" + "proto/*", + "src/*", + "config/*", + "policy/*" ] }, "formatter": { @@ -26,6 +24,10 @@ "recommended": true, "style": { "useNodejsImportProtocol": "off" + }, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" } } }, diff --git a/package.json b/package.json index 2688094..12761f2 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,10 @@ "build:ts": "bun run ./src/build.ts", "test": "bun test", "format": "bunx biome format --write", - "lint": "bunx biome lint --write", - "check": "bunx biome check --write", + "lint": "bunx biome lint", + "lint:fix": "bunx biome lint --write", + "check": "bunx biome check", + "check:fix": "bunx biome check --write", "prepare": "bunx husky", "postinstall": "./proto-gen.sh" }, diff --git a/src/helpers/index.test.ts b/src/helpers/index.test.ts index 12ea70e..d6bee8f 100644 --- a/src/helpers/index.test.ts +++ b/src/helpers/index.test.ts @@ -1,6 +1,6 @@ -import { describe, test, expect, beforeEach } from "bun:test"; -import { validateWithdraw, validateOrder, validateDeposit } from "./index"; +import { beforeEach, describe, expect, test } from "bun:test"; import type { PolicyConfig } from "../types"; +import { validateDeposit, validateOrder, validateWithdraw } from "./index"; describe("Helper Functions", () => { let testPolicy: PolicyConfig; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 373b45e..deb66c8 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,8 +1,8 @@ -import type { PolicyConfig } from "../types"; +import type { ServerUnaryCall } from "@grpc/grpc-js"; import fs from "fs"; import Joi from "joi"; +import type { PolicyConfig } from "../types"; import { log } from "./logger"; -import type { ServerUnaryCall } from "@grpc/grpc-js"; export function authenticateRequest( call: ServerUnaryCall, diff --git a/src/index.ts b/src/index.ts index a5a7736..e32e76c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,16 @@ -import ccxt, { type Exchange } from "@usherlabs/ccxt"; import * as grpc from "@grpc/grpc-js"; -import { watchFile, unwatchFile } from "fs"; +import ccxt, { type Exchange } from "@usherlabs/ccxt"; +import { unwatchFile, watchFile } from "fs"; import Joi from "joi"; import { loadPolicy } from "./helpers"; +import { log } from "./helpers/logger"; +import { getServer } from "./server"; import { - BrokerList, type BrokerCredentials, + BrokerList, type ExchangeCredentials, type PolicyConfig, } from "./types"; -import { getServer } from "./server"; -import { log } from "./helpers/logger"; log.info("CCXT Version:", ccxt.version); diff --git a/src/server.ts b/src/server.ts index 38c2773..aff0f4e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -111,7 +111,7 @@ export function getServer( } switch (action) { - case Action.Deposit: + case Action.Deposit: { const transactionSchema = Joi.object({ recipientAddress: Joi.string().required(), amount: Joi.number().positive().required(), // Must be a positive number @@ -161,8 +161,9 @@ export function getServer( ); } break; + } - case Action.FetchDepositAddresses: + case Action.FetchDepositAddresses: { const fetchDepositAddressesSchema = Joi.object({ chain: Joi.string().required(), }) @@ -202,7 +203,8 @@ export function getServer( ); } break; - case Action.Transfer: + } + case Action.Transfer: { const transferSchema = Joi.object({ recipientAddress: Joi.string().required(), amount: Joi.number().positive().required(), // Must be a positive number @@ -273,8 +275,9 @@ export function getServer( ); } break; + } - case Action.CreateOrder: + case Action.CreateOrder: { const createOrderSchema = Joi.object({ orderType: Joi.string().valid("market", "limit").default("limit"), amount: Joi.number().positive().required(), // Must be a positive number @@ -354,8 +357,9 @@ export function getServer( } break; + } - case Action.GetOrderDetails: + case Action.GetOrderDetails: { const getOrderSchema = Joi.object({ orderId: Joi.string().required(), }); @@ -408,7 +412,8 @@ export function getServer( ); } break; - case Action.CancelOrder: + } + case Action.CancelOrder: { const cancelOrderSchema = Joi.object({ orderId: Joi.string().required(), }); @@ -433,6 +438,7 @@ export function getServer( result: JSON.stringify({ ...cancelledOrder }), }); break; + } case Action.FetchBalance: try { // Fetch balance from the specified CEX diff --git a/src/types.ts b/src/types.ts index 54ca574..714a89d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import ccxt from "@usherlabs/ccxt"; +import type ccxt from "@usherlabs/ccxt"; // Policy types based on the policy.json structure export type WithdrawRule = { From dd782e52aae17ea3c9de5bfcb0ca02f8645c7c81 Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 17:43:09 +0100 Subject: [PATCH 41/45] Refactor broker creation and selection logic --- src/helpers/index.ts | 52 +++++++++++++++++++++++++++++++++++++++++++- src/server.ts | 42 +++++------------------------------ 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index deb66c8..7eb6475 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,8 +1,9 @@ -import type { ServerUnaryCall } from "@grpc/grpc-js"; import fs from "fs"; import Joi from "joi"; import type { PolicyConfig } from "../types"; import { log } from "./logger"; +import type { Metadata, ServerUnaryCall } from "@grpc/grpc-js"; +import ccxt, { type Exchange } from "@usherlabs/ccxt"; export function authenticateRequest( call: ServerUnaryCall, @@ -16,6 +17,55 @@ export function authenticateRequest( return true; } +export function createBroker(cex: string, metadata: Metadata, useVerity: boolean, verityProverUrl: string): Exchange | null { + const api_key = metadata.get("api-key"); + const api_secret = metadata.get("api-secret"); + + const ExchangeClass = (ccxt.pro as Record)[cex]; + + metadata.remove("api-key"); + metadata.remove("api-secret"); + if (api_secret.length === 0 || api_key.length === 0 || !ExchangeClass) { + return null; + } + const exchange = new ExchangeClass({ + apiKey: api_key[0]?.toString(), + secret: api_secret[0]?.toString(), + enableRateLimit: true, + defaultType: "spot", + useVerity: useVerity, + verityProverUrl: verityProverUrl, + timeout: 150 * 1000, + options: { + adjustForTimeDifference: true, + recvWindow: 60000 + } + }); + exchange.options.recvWindow = 60000; + return exchange; +} + +export function selectBroker(brokers: { + primary: Exchange; + secondaryBrokers: Exchange[]; +} | undefined, metadata: Metadata): Exchange | null { + if (!brokers) { + return null + } else { + const use_secondary_key = metadata.get("use-secondary-key"); + if (!use_secondary_key || use_secondary_key.length === 0) { + return brokers.primary + } + else if (use_secondary_key.length > 0) { + const keyIndex = Number.isInteger(+(use_secondary_key[use_secondary_key.length - 1] ?? "0")) + return brokers.secondaryBrokers[+keyIndex] ?? null + }else{ + return null + } + } + +} + /** * Loads and validates policy configuration */ diff --git a/src/server.ts b/src/server.ts index aff0f4e..7256392 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { authenticateRequest, validateDeposit, validateOrder, validateWithdraw } from "./helpers"; +import { authenticateRequest, createBroker, selectBroker, validateDeposit, validateOrder, validateWithdraw } from "./helpers"; import type { PolicyConfig } from "./types"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; @@ -35,38 +35,7 @@ export function getServer( verityProverUrl: string, ) { const server = new grpc.Server(); - function createBroker(cex: string, metadata: grpc.Metadata, secondaryBrokers: Exchange[]): Exchange | null { - const api_key = metadata.get("api-key"); - const api_secret = metadata.get("api-secret"); - const use_secondary_key = metadata.get("use-secondary-key"); - if (use_secondary_key.length > 0) { - const keyIndex = Number.isInteger(+(use_secondary_key[use_secondary_key.length - 1] ?? "0")) - return secondaryBrokers[+keyIndex] ?? null - } - - const ExchangeClass = (ccxt.pro as any)[cex]; - - metadata.remove("api-key"); - metadata.remove("api-secret"); - if (api_secret.length == 0 || api_key.length == 0) { - return null; - } - const exchange = new ExchangeClass({ - apiKey: api_key[0], - secret: api_secret[0], - enableRateLimit: true, - defaultType: "spot", - useVerity: useVerity, - verityProverUrl: verityProverUrl, - timeout: 150 * 1000, - options: { - adjustForTimeDifference: true, - recvWindow: 60000 - } - }); - exchange.options['recvWindow'] = 60000; - return exchange; - } + server.addService(cexNode.CexService.service, { ExecuteAction: async ( call: grpc.ServerUnaryCall, @@ -96,8 +65,8 @@ export function getServer( ); } - const broker = - brokers[cex as keyof typeof brokers]?.primary ?? createBroker(cex, metadata, brokers[cex as keyof typeof brokers]?.secondaryBrokers ?? []); + const broker = selectBroker(brokers[cex as keyof typeof brokers],metadata )?? createBroker(cex, metadata,useVerity,verityProverUrl); + if (!broker) { @@ -510,8 +479,7 @@ export function getServer( } // Get or create broker - broker = - brokers[cex as keyof typeof brokers]?.primary ?? createBroker(cex, metadata, brokers[cex as keyof typeof brokers]?.secondaryBrokers ?? []); + broker = selectBroker(brokers[cex as keyof typeof brokers],metadata )?? createBroker(cex, metadata,useVerity,verityProverUrl); if (!broker) { call.write({ From 725aba19db29eff93473c621a28b84a40779376f Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 17:44:32 +0100 Subject: [PATCH 42/45] Format code and improve readability --- biome.json | 7 +--- proto/node.ts | 43 ++++++++++++++-------- src/cli.ts | 10 +++--- src/client.dev.ts | 26 +++++++++----- src/server.ts | 91 +++++++++++++++++++++++++++++++---------------- src/types.ts | 24 ++++++------- 6 files changed, 121 insertions(+), 80 deletions(-) diff --git a/biome.json b/biome.json index d91c60a..f6ad155 100644 --- a/biome.json +++ b/biome.json @@ -7,12 +7,7 @@ }, "files": { "ignoreUnknown": true, - "includes": [ - "proto/*", - "src/*", - "config/*", - "policy/*" - ] + "includes": ["proto/*", "src/*", "config/*", "policy/*"] }, "formatter": { "enabled": true, diff --git a/proto/node.ts b/proto/node.ts index c07f8cd..947f80e 100644 --- a/proto/node.ts +++ b/proto/node.ts @@ -1,21 +1,34 @@ -import type * as grpc from '@grpc/grpc-js'; -import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; +import type * as grpc from "@grpc/grpc-js"; +import type { + EnumTypeDefinition, + MessageTypeDefinition, +} from "@grpc/proto-loader"; -import type { CexServiceClient as _cexBroker_CexServiceClient, CexServiceDefinition as _cexBroker_CexServiceDefinition } from './cexBroker/CexService'; +import type { + CexServiceClient as _cexBroker_CexServiceClient, + CexServiceDefinition as _cexBroker_CexServiceDefinition, +} from "./cexBroker/CexService"; -type SubtypeConstructor any, Subtype> = { - new(...args: ConstructorParameters): Subtype; +type SubtypeConstructor< + Constructor extends new ( + ...args: any + ) => any, + Subtype, +> = { + new (...args: ConstructorParameters): Subtype; }; export interface ProtoGrpcType { - cexBroker: { - Action: EnumTypeDefinition - ActionRequest: MessageTypeDefinition - ActionResponse: MessageTypeDefinition - CexService: SubtypeConstructor & { service: _cexBroker_CexServiceDefinition } - SubscribeRequest: MessageTypeDefinition - SubscribeResponse: MessageTypeDefinition - SubscriptionType: EnumTypeDefinition - } + cexBroker: { + Action: EnumTypeDefinition; + ActionRequest: MessageTypeDefinition; + ActionResponse: MessageTypeDefinition; + CexService: SubtypeConstructor< + typeof grpc.Client, + _cexBroker_CexServiceClient + > & { service: _cexBroker_CexServiceDefinition }; + SubscribeRequest: MessageTypeDefinition; + SubscribeResponse: MessageTypeDefinition; + SubscriptionType: EnumTypeDefinition; + }; } - diff --git a/src/cli.ts b/src/cli.ts index 7fdd40d..2629780 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,13 +11,11 @@ program .requiredOption("-p, --policy ", "Policy JSON file") .option("--port ", "Port number (default: 8086)", "8086") .option( - "-w","--whitelist ", + "-w", + "--whitelist ", "IPv4 address whitelist (space-separated list)", ) - .option( - "-vu","--verityProverUrl ", - "Verity Prover Url", - ) + .option("-vu", "--verityProverUrl ", "Verity Prover Url") .action(async (options) => { try { // Optional: Validate IPv4 addresses @@ -40,7 +38,7 @@ program options.policy, parseInt(options.port, 10), options.whitelist ?? [], // Pass whitelist to your command, - options.verityProverUrl + options.verityProverUrl, ); } catch (err) { console.error("❌ Failed to start broker:", err); diff --git a/src/client.dev.ts b/src/client.dev.ts index 54c9293..b08668f 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -30,7 +30,6 @@ const broker = new CEXBroker({}, loadPolicy("./policy/policy.json"), { broker.loadEnvConfig(); broker.run(); - const metadata = new grpc.Metadata(); metadata.add("api-key", process.env.BYBIT_API_KEY ?? ""); // Example header metadata.add("api-secret", process.env.BYBIT_API_SECRET ?? ""); @@ -55,14 +54,23 @@ function onClientReady() { // log.info("ExecuteAction Balance Result:", { result }); // }); - // Test ExecuteAction for balance - client.executeAction({ cex: "bybit", symbol: "USDT",payload:{chain:"TRC20"},action: Action.FetchDepositAddresses },metadata, (err, result) => { - if (err) { - log.error({ err }); - return; - } - log.info("ExecuteAction Balance Result:", { result }); - }); + // Test ExecuteAction for balance + client.executeAction( + { + cex: "bybit", + symbol: "USDT", + payload: { chain: "TRC20" }, + action: Action.FetchDepositAddresses, + }, + metadata, + (err, result) => { + if (err) { + log.error({ err }); + return; + } + log.info("ExecuteAction Balance Result:", { result }); + }, + ); // // Test Subscribe for balance streaming // log.info("Starting balance subscription test..."); diff --git a/src/server.ts b/src/server.ts index 7256392..4ba135d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,13 +1,18 @@ -import { authenticateRequest, createBroker, selectBroker, validateDeposit, validateOrder, validateWithdraw } from "./helpers"; +import { + authenticateRequest, + createBroker, + selectBroker, + validateDeposit, + validateOrder, + validateWithdraw, +} from "./helpers"; import type { PolicyConfig } from "./types"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import path from "path"; import type { Exchange } from "@usherlabs/ccxt"; -import type { - ActionRequest, -} from "../proto/cexBroker/ActionRequest"; +import type { ActionRequest } from "../proto/cexBroker/ActionRequest"; import type { ActionResponse } from "../proto/cexBroker/ActionResponse"; import { Action } from "../proto/cexBroker/Action"; import type { SubscribeRequest } from "../proto/cexBroker/SubscribeRequest"; @@ -25,8 +30,6 @@ const grpcObj = grpc.loadPackageDefinition( ) as unknown as ProtoGrpcType; const cexNode = grpcObj.cexBroker; - - export function getServer( policy: PolicyConfig, brokers: Record, @@ -65,9 +68,9 @@ export function getServer( ); } - const broker = selectBroker(brokers[cex as keyof typeof brokers],metadata )?? createBroker(cex, metadata,useVerity,verityProverUrl); - - + const broker = + selectBroker(brokers[cex as keyof typeof brokers], metadata) ?? + createBroker(cex, metadata, useVerity, verityProverUrl); if (!broker) { return callback( @@ -110,7 +113,11 @@ export function getServer( log.info( `Amount ${value.amount} at ${value.transactionHash} . Paid to ${value.recipientAddress}`, ); - return callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...deposit }) }); + return callback(null, { + result: useVerity + ? broker.last_proof + : JSON.stringify({ ...deposit }), + }); } callback( { @@ -135,24 +142,37 @@ export function getServer( case Action.FetchDepositAddresses: { const fetchDepositAddressesSchema = Joi.object({ chain: Joi.string().required(), - }) - const { value: fetchDepositAddresses, error: errorFetchDepositAddresses } = fetchDepositAddressesSchema.validate( - call.request.payload ?? {}, - ); + }); + const { + value: fetchDepositAddresses, + error: errorFetchDepositAddresses, + } = fetchDepositAddressesSchema.validate(call.request.payload ?? {}); if (errorFetchDepositAddresses) { return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError: " + errorFetchDepositAddresses?.message, + message: + "ValidationError: " + errorFetchDepositAddresses?.message, }, null, ); } try { - const depositAddresses = broker.has.fetchDepositAddress == true ? await broker.fetchDepositAddress(symbol, { network: fetchDepositAddresses.chain }) : await broker.fetchDepositAddressesByNetwork(symbol, { network: fetchDepositAddresses.chain }); + const depositAddresses = + broker.has.fetchDepositAddress == true + ? await broker.fetchDepositAddress(symbol, { + network: fetchDepositAddresses.chain, + }) + : await broker.fetchDepositAddressesByNetwork(symbol, { + network: fetchDepositAddresses.chain, + }); if (depositAddresses) { - return callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...depositAddresses }) }); + return callback(null, { + result: useVerity + ? broker.last_proof + : JSON.stringify({ ...depositAddresses }), + }); } callback( { @@ -166,7 +186,9 @@ export function getServer( callback( { code: grpc.status.INTERNAL, - message: "Fetch Deposit Addresses confirmation failed: " + error.message, + message: + "Fetch Deposit Addresses confirmation failed: " + + error.message, }, null, ); @@ -180,7 +202,7 @@ export function getServer( chain: Joi.string().required(), }); const { value: transferValue, error: transferError } = - transferSchema.validate(call.request.payload ?? {}) + transferSchema.validate(call.request.payload ?? {}); if (transferError) { return callback( { @@ -229,10 +251,13 @@ export function getServer( undefined, { network: transferValue.chain }, ); - log.info("Transfer Transfer" + JSON.stringify(transaction) - ); + log.info("Transfer Transfer" + JSON.stringify(transaction)); - callback(null, { result: useVerity ? broker.last_proof : JSON.stringify({ ...transaction }) }); + callback(null, { + result: useVerity + ? broker.last_proof + : JSON.stringify({ ...transaction }), + }); } catch (error) { log.error({ error }); callback( @@ -333,7 +358,7 @@ export function getServer( orderId: Joi.string().required(), }); const { value: getOrderValue, error: getOrderError } = - getOrderSchema.validate(call.request.payload ?? {}) + getOrderSchema.validate(call.request.payload ?? {}); // Validate required fields if (getOrderError) { return callback( @@ -415,10 +440,12 @@ export function getServer( const currencyBalance = balance[symbol]; callback(null, { - result: useVerity ? broker.last_proof : JSON.stringify({ - balance: currencyBalance || 0, - currency: symbol, - }), + result: useVerity + ? broker.last_proof + : JSON.stringify({ + balance: currencyBalance || 0, + currency: symbol, + }), }); } catch (error) { log.error(`Error fetching balance from ${cex}:`, error); @@ -445,14 +472,15 @@ export function getServer( ) => { // IP Authentication if (!authenticateRequest(call, whitelistIps)) { - call.emit('error', + call.emit( + "error", { code: grpc.status.PERMISSION_DENIED, message: "Access denied: Unauthorized IP", }, null, ); - call.destroy(new Error("Access denied: Unauthorized IP")); + call.destroy(new Error("Access denied: Unauthorized IP")); } // Read incoming metadata const metadata = call.metadata; @@ -479,7 +507,9 @@ export function getServer( } // Get or create broker - broker = selectBroker(brokers[cex as keyof typeof brokers],metadata )?? createBroker(cex, metadata,useVerity,verityProverUrl); + broker = + selectBroker(brokers[cex as keyof typeof brokers], metadata) ?? + createBroker(cex, metadata, useVerity, verityProverUrl); if (!broker) { call.write({ @@ -526,7 +556,6 @@ export function getServer( case SubscriptionType.TRADES: try { while (true) { - const trades = await broker.watchTrades(symbol); call.write({ data: JSON.stringify(trades), diff --git a/src/types.ts b/src/types.ts index 714a89d..2fc5771 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type ccxt from "@usherlabs/ccxt"; +import type ccxt from "@usherlabs/ccxt"; // Policy types based on the policy.json structure export type WithdrawRule = { @@ -51,10 +51,9 @@ export type Policy = { // Dynamic type mapping using CCXT's exchange classes type BrokerInstanceMap = { - [K in ISupportedBroker]: InstanceType; + [K in ISupportedBroker]: InstanceType<(typeof ccxt)[K]>; }; - // Dynamic BrokerMap: each key maps to the correct broker type export type BrokerMap = Partial<{ [K in ISupportedBroker]: BrokerInstanceMap[K]; @@ -165,13 +164,13 @@ export const BrokerList = [ "xt", "yobit", "zaif", - "zonda" - ] as const; - + "zonda", +] as const; + export type brokers = Required; - + export type ISupportedBroker = (typeof BrokerList)[number]; -export type SupportedBrokers = typeof BrokerList[number] +export type SupportedBrokers = (typeof BrokerList)[number]; export const SupportedBroker = BrokerList.reduce( (acc, value) => { @@ -181,15 +180,14 @@ export const SupportedBroker = BrokerList.reduce( {} as Record<(typeof BrokerList)[number], string>, ); - export type BrokerCredentials = { apiKey: string; apiSecret: string; }; -export type SecondaryKeys={ - secondaryKeys: Array -} +export type SecondaryKeys = { + secondaryKeys: Array; +}; export interface ExchangeCredentials { - [exchange: string]: BrokerCredentials & SecondaryKeys + [exchange: string]: BrokerCredentials & SecondaryKeys; } From 61e940db66f12da44f389823dca9895ad62cf7db Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 12:47:07 +0100 Subject: [PATCH 43/45] Refactor function formatting for readability --- src/helpers/index.ts | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 7eb6475..3d32947 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -17,7 +17,12 @@ export function authenticateRequest( return true; } -export function createBroker(cex: string, metadata: Metadata, useVerity: boolean, verityProverUrl: string): Exchange | null { +export function createBroker( + cex: string, + metadata: Metadata, + useVerity: boolean, + verityProverUrl: string, +): Exchange | null { const api_key = metadata.get("api-key"); const api_secret = metadata.get("api-secret"); @@ -38,32 +43,37 @@ export function createBroker(cex: string, metadata: Metadata, useVerity: boolean timeout: 150 * 1000, options: { adjustForTimeDifference: true, - recvWindow: 60000 - } + recvWindow: 60000, + }, }); exchange.options.recvWindow = 60000; return exchange; } -export function selectBroker(brokers: { - primary: Exchange; - secondaryBrokers: Exchange[]; -} | undefined, metadata: Metadata): Exchange | null { +export function selectBroker( + brokers: + | { + primary: Exchange; + secondaryBrokers: Exchange[]; + } + | undefined, + metadata: Metadata, +): Exchange | null { if (!brokers) { - return null + return null; } else { const use_secondary_key = metadata.get("use-secondary-key"); if (!use_secondary_key || use_secondary_key.length === 0) { - return brokers.primary - } - else if (use_secondary_key.length > 0) { - const keyIndex = Number.isInteger(+(use_secondary_key[use_secondary_key.length - 1] ?? "0")) - return brokers.secondaryBrokers[+keyIndex] ?? null - }else{ - return null + return brokers.primary; + } else if (use_secondary_key.length > 0) { + const keyIndex = Number.isInteger( + +(use_secondary_key[use_secondary_key.length - 1] ?? "0"), + ); + return brokers.secondaryBrokers[+keyIndex] ?? null; + } else { + return null; } } - } /** From 5561d604cece82d3ac37b9e97feb972084494aa4 Mon Sep 17 00:00:00 2001 From: xlassix Date: Mon, 28 Jul 2025 17:58:52 +0100 Subject: [PATCH 44/45] Refactor error handling and remove unused imports --- biome.json | 2 +- src/client.dev.ts | 1 - src/server.ts | 116 +++++++++++++++++++++++++++++++--------------- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/biome.json b/biome.json index f6ad155..221d944 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "includes": ["proto/*", "src/*", "config/*", "policy/*"] + "includes": ["src/*", "config/*", "policy/*"] }, "formatter": { "enabled": true, diff --git a/src/client.dev.ts b/src/client.dev.ts index b08668f..1d88158 100644 --- a/src/client.dev.ts +++ b/src/client.dev.ts @@ -3,7 +3,6 @@ import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import type { ProtoGrpcType } from "../proto/node"; import { Action } from "../proto/cexBroker/Action"; -import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; import { config } from "dotenv"; import { log } from "./helpers/logger"; import CEXBroker from "."; diff --git a/src/server.ts b/src/server.ts index 4ba135d..e156e0f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,6 @@ import { authenticateRequest, createBroker, selectBroker, - validateDeposit, validateOrder, validateWithdraw, } from "./helpers"; @@ -19,7 +18,6 @@ import type { SubscribeRequest } from "../proto/cexBroker/SubscribeRequest"; import type { SubscribeResponse } from "../proto/cexBroker/SubscribeResponse"; import { SubscriptionType } from "../proto/cexBroker/SubscriptionType"; import Joi from "joi"; -import ccxt from "@usherlabs/ccxt"; import { log } from "./helpers/logger"; const PROTO_FILE = "../proto/node.proto"; @@ -56,7 +54,7 @@ export function getServer( } // Read incoming metadata const metadata = call.metadata; - const { action, payload, cex, symbol } = call.request; + const { action, cex, symbol } = call.request; // Validate required fields if (!action || !cex || !symbol) { return callback( @@ -96,7 +94,7 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError:" + error.message, + message: `ValidationError: ${error.message}`, }, null, ); @@ -105,8 +103,8 @@ export function getServer( const deposits = await broker.fetchDeposits(symbol, 50); const deposit = deposits.find( (deposit) => - deposit.id == value.transactionHash || - deposit.txid == value.transactionHash, + deposit.id === value.transactionHash || + deposit.txid === value.transactionHash, ); if (deposit) { @@ -151,15 +149,14 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: - "ValidationError: " + errorFetchDepositAddresses?.message, + message: `ValidationError: ${errorFetchDepositAddresses?.message}`, }, null, ); } try { const depositAddresses = - broker.has.fetchDepositAddress == true + broker.has.fetchDepositAddress === true ? await broker.fetchDepositAddress(symbol, { network: fetchDepositAddresses.chain, }) @@ -181,14 +178,19 @@ export function getServer( }, null, ); - } catch (error: any) { + } catch (error: unknown) { log.error({ error }); + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; callback( { code: grpc.status.INTERNAL, message: - "Fetch Deposit Addresses confirmation failed: " + - error.message, + "Fetch Deposit Addresses confirmation failed: " + message, }, null, ); @@ -207,7 +209,7 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError:" + transferError?.message, + message: `ValidationError:" ${transferError?.message}`, }, null, ); @@ -251,7 +253,7 @@ export function getServer( undefined, { network: transferValue.chain }, ); - log.info("Transfer Transfer" + JSON.stringify(transaction)); + log.info(`Transfer Transfer: ${JSON.stringify(transaction)}`); callback(null, { result: useVerity @@ -285,7 +287,7 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError:" + orderError.message, + message: `ValidationError:" ${orderError.message}`, }, null, ); @@ -364,7 +366,7 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError:" + getOrderError.message, + message: `ValidationError: ${getOrderError.message}`, }, null, ); @@ -418,7 +420,7 @@ export function getServer( return callback( { code: grpc.status.INVALID_ARGUMENT, - message: "ValidationError:" + cancelOrderError.message, + message: `ValidationError: ${cancelOrderError.message}`, }, null, ); @@ -436,6 +438,7 @@ export function getServer( case Action.FetchBalance: try { // Fetch balance from the specified CEX + // biome-ignore lint/suspicious/noExplicitAny: fetchFreeBalance const balance = (await broker.fetchFreeBalance()) as any; const currencyBalance = balance[symbol]; @@ -537,14 +540,20 @@ export function getServer( type, }); } - } catch (error: any) { + } catch (error: unknown) { log.error( `Error fetching orderbook for ${symbol} on ${cex}:`, error, ); + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; call.write({ data: JSON.stringify({ - error: `Failed to fetch orderbook: ${error.message}`, + error: `Failed to fetch orderbook: ${message}`, }), timestamp: Date.now(), symbol, @@ -564,14 +573,20 @@ export function getServer( type, }); } - } catch (error: any) { + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; log.error( `Error fetching trades for ${symbol} on ${cex}:`, - error.message, + error, ); call.write({ data: JSON.stringify({ - error: `Failed to fetch trades: ${error.message}`, + error: `Failed to fetch trades: ${message}`, }), timestamp: Date.now(), symbol, @@ -591,14 +606,20 @@ export function getServer( type, }); } - } catch (error: any) { + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; log.error( `Error fetching ticker for ${symbol} on ${cex}:`, - error.message, + error, ); call.write({ data: JSON.stringify({ - error: `Failed to fetch ticker: ${error.message}`, + error: `Failed to fetch ticker: ${message}`, }), timestamp: Date.now(), symbol, @@ -619,14 +640,17 @@ export function getServer( type, }); } - } catch (error: any) { - log.error( - `Error fetching OHLCV for ${symbol} on ${cex}:`, - error.message, - ); + } catch (error: unknown) { + log.error(`Error fetching OHLCV for ${symbol} on ${cex}:`, error); + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; call.write({ data: JSON.stringify({ - error: `Failed to fetch OHLCV: ${error.message}`, + error: `Failed to fetch OHLCV: ${message}`, }), timestamp: Date.now(), symbol, @@ -646,11 +670,17 @@ export function getServer( type, }); } - } catch (error: any) { - log.error(`Error fetching balance for ${cex}:`, error.message); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; + log.error(`Error fetching balance for ${cex}:`, error); call.write({ data: JSON.stringify({ - error: `Failed to fetch balance: ${error.message}`, + error: `Failed to fetch balance: ${message}`, }), timestamp: Date.now(), symbol, @@ -670,14 +700,20 @@ export function getServer( type, }); } - } catch (error: any) { + } catch (error: unknown) { log.error( `Error fetching orders for ${symbol} on ${cex}:`, error, ); + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; call.write({ data: JSON.stringify({ - error: `Failed to fetch orders: ${error.message}`, + error: `Failed to fetch orders: ${message}`, }), timestamp: Date.now(), symbol, @@ -696,8 +732,14 @@ export function getServer( } } catch (error) { log.error("Error in Subscribe stream:", error); + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : "Unknown error"; call.write({ - data: JSON.stringify({ error: `Internal server error: ${error}` }), + data: JSON.stringify({ error: `Internal server error: ${message}` }), timestamp: Date.now(), symbol: "", type: SubscriptionType.ORDERBOOK, From 93eeeb71129b904b10913ac09c38005b90b03762 Mon Sep 17 00:00:00 2001 From: Ryan Soury Date: Tue, 29 Jul 2025 03:33:09 +1000 Subject: [PATCH 45/45] remove useTemplates from biome --- biome.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 221d944..c444b31 100644 --- a/biome.json +++ b/biome.json @@ -18,7 +18,8 @@ "rules": { "recommended": true, "style": { - "useNodejsImportProtocol": "off" + "useNodejsImportProtocol": "off", + "useTemplate": "off" }, "correctness": { "noUnusedImports": "warn",