From 01e58d07b83d20032c79cf752775e579dda0f6f5 Mon Sep 17 00:00:00 2001 From: Siyuan Wang Date: Wed, 19 Feb 2025 21:35:10 +0800 Subject: [PATCH] feat(hyperliquid): add new vendor hyperliquid (#1070) --- apps/vendor-hyperliquid/api-extractor.json | 411 ++++++++++++++++++ .../config/jest.config.json | 3 + apps/vendor-hyperliquid/config/rig.json | 18 + .../vendor-hyperliquid/config/typescript.json | 87 ++++ .../etc/vendor-hyperliquid.api.md | 9 + apps/vendor-hyperliquid/package.json | 44 ++ apps/vendor-hyperliquid/src/api.ts | 234 ++++++++++ apps/vendor-hyperliquid/src/extension.ts | 88 ++++ apps/vendor-hyperliquid/src/index.ts | 165 +++++++ apps/vendor-hyperliquid/tsconfig.json | 8 + .../vendor-hyperliquid/2025-02-19-13-29.json | 10 + common/config/rush/deploy.json | 1 + common/config/rush/pnpm-lock.yaml | 121 ++++++ rush.json | 5 + 14 files changed, 1204 insertions(+) create mode 100644 apps/vendor-hyperliquid/api-extractor.json create mode 100644 apps/vendor-hyperliquid/config/jest.config.json create mode 100644 apps/vendor-hyperliquid/config/rig.json create mode 100644 apps/vendor-hyperliquid/config/typescript.json create mode 100644 apps/vendor-hyperliquid/etc/vendor-hyperliquid.api.md create mode 100644 apps/vendor-hyperliquid/package.json create mode 100644 apps/vendor-hyperliquid/src/api.ts create mode 100644 apps/vendor-hyperliquid/src/extension.ts create mode 100644 apps/vendor-hyperliquid/src/index.ts create mode 100644 apps/vendor-hyperliquid/tsconfig.json create mode 100644 common/changes/@yuants/vendor-hyperliquid/2025-02-19-13-29.json diff --git a/apps/vendor-hyperliquid/api-extractor.json b/apps/vendor-hyperliquid/api-extractor.json new file mode 100644 index 000000000..62f4fd324 --- /dev/null +++ b/apps/vendor-hyperliquid/api-extractor.json @@ -0,0 +1,411 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/lib/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportFolder": "/temp/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/", + + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/apps/vendor-hyperliquid/config/jest.config.json b/apps/vendor-hyperliquid/config/jest.config.json new file mode 100644 index 000000000..4bb17bde3 --- /dev/null +++ b/apps/vendor-hyperliquid/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json" +} diff --git a/apps/vendor-hyperliquid/config/rig.json b/apps/vendor-hyperliquid/config/rig.json new file mode 100644 index 000000000..f6c7b5537 --- /dev/null +++ b/apps/vendor-hyperliquid/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@rushstack/heft-node-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/apps/vendor-hyperliquid/config/typescript.json b/apps/vendor-hyperliquid/config/typescript.json new file mode 100644 index 000000000..854907e8a --- /dev/null +++ b/apps/vendor-hyperliquid/config/typescript.json @@ -0,0 +1,87 @@ +/** + * Configures the TypeScript plugin for Heft. This plugin also manages linting. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/typescript.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for standard + * settings to be shared across multiple projects. + */ + // "extends": "base-project/config/typescript.json", + + /** + * Can be set to "copy" or "hardlink". If set to "copy", copy files from cache. + * If set to "hardlink", files will be hardlinked to the cache location. + * This option is useful when producing a tarball of build output as TAR files don't + * handle these hardlinks correctly. "hardlink" is the default behavior. + */ + // "copyFromCacheMode": "copy", + + /** + * If provided, emit these module kinds in addition to the modules specified in the tsconfig. + * Note that this option only applies to the main tsconfig.json configuration. + */ + "additionalModuleKindsToEmit": [ + { + "moduleKind": "esnext", + "outFolderName": "dist" + } + // { + // /** + // * (Required) Must be one of "commonjs", "amd", "umd", "system", "es2015", "esnext" + // */ + // "moduleKind": "amd", + // + // /** + // * (Required) The name of the folder where the output will be written. + // */ + // "outFolderName": "lib-amd" + // } + ], + + /** + * Specifies the intermediary folder that tests will use. Because Jest uses the + * Node.js runtime to execute tests, the module format must be CommonJS. + * + * The default value is "lib". + */ + // "emitFolderNameForTests": "lib-commonjs", + + /** + * If set to "true", the TSlint task will not be invoked. + */ + // "disableTslint": true, + + /** + * Set this to change the maximum number of file handles that will be opened concurrently for writing. + * The default is 50. + */ + // "maxWriteParallelism": 50, + + /** + * Configures additional file types that should be copied into the TypeScript compiler's emit folders, for example + * so that these files can be resolved by import statements. + */ + "staticAssetsToCopy": { + /** + * File extensions that should be copied from the src folder to the destination folder(s). + */ + // "fileExtensions": [ + // ".json", ".css" + // ], + /** + * Glob patterns that should be explicitly included. + */ + // "includeGlobs": [ + // "some/path/*.js" + // ], + /** + * Glob patterns that should be explicitly excluded. This takes precedence over globs listed + * in "includeGlobs" and files that match the file extensions provided in "fileExtensions". + */ + // "excludeGlobs": [ + // "some/path/*.css" + // ] + } +} diff --git a/apps/vendor-hyperliquid/etc/vendor-hyperliquid.api.md b/apps/vendor-hyperliquid/etc/vendor-hyperliquid.api.md new file mode 100644 index 000000000..66675bd88 --- /dev/null +++ b/apps/vendor-hyperliquid/etc/vendor-hyperliquid.api.md @@ -0,0 +1,9 @@ +## API Report File for "@yuants/vendor-hyperliquid" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/apps/vendor-hyperliquid/package.json b/apps/vendor-hyperliquid/package.json new file mode 100644 index 000000000..d9e924b64 --- /dev/null +++ b/apps/vendor-hyperliquid/package.json @@ -0,0 +1,44 @@ +{ + "name": "@yuants/vendor-hyperliquid", + "version": "0.0.0", + "files": [ + "dist/extension.bundle.js" + ], + "scripts": { + "dev": "ts-node src/index.ts", + "build": "heft test --clean && api-extractor run --local && yuan-toolkit post-build" + }, + "dependencies": { + "@yuants/protocol": "workspace:*", + "@yuants/data-model": "workspace:*", + "@yuants/utils": "workspace:*", + "@yuants/data-series": "workspace:*", + "rxjs": "~7.5.6", + "crypto-js": "^4.2.0", + "hyperliquid": "~1.6.2", + "ethers": "~6.13.5" + }, + "devDependencies": { + "@microsoft/api-extractor": "~7.30.0", + "@rushstack/heft": "~0.47.5", + "@rushstack/heft-jest-plugin": "~0.3.30", + "@rushstack/heft-node-rig": "~1.10.7", + "@types/heft-jest": "1.0.3", + "@types/node": "18", + "@yuants/extension": "workspace:*", + "@yuants/tool-kit": "workspace:*", + "typescript": "~4.7.4", + "ts-node": "~10.9.2" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "io_ntnl": { + "deploy_files": [ + "dist", + "lib", + "temp" + ] + } +} diff --git a/apps/vendor-hyperliquid/src/api.ts b/apps/vendor-hyperliquid/src/api.ts new file mode 100644 index 000000000..6b8bde60f --- /dev/null +++ b/apps/vendor-hyperliquid/src/api.ts @@ -0,0 +1,234 @@ +import { UUID, formatTime } from '@yuants/data-model'; +// @ts-ignore +import { ethers } from 'ethers'; +import { Subject, filter, firstValueFrom, mergeMap, of, shareReplay, throwError, timeout, timer } from 'rxjs'; + +/** + * API: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/notation + */ +export class HyperliquidClient { + private wallet: ethers.Wallet | undefined; + public public_key: string | undefined; + constructor(config: { + auth?: { + private_key: string; + }; + }) { + this.wallet = config.auth ? new ethers.Wallet(config.auth.private_key) : undefined; + this.public_key = this.wallet?.address; + } + + async request(method: string, path: string, params?: any) { + const url = new URL('https://api.hyperliquid.xyz'); + + url.pathname = path; + + const headers: Record = { + 'Content-Type': 'application/json', + }; + const body = method === 'GET' ? '' : JSON.stringify(params); + // const str = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(signData, secret_key)); + + console.info(formatTime(Date.now()), method, url.href, JSON.stringify(headers), body); + const res = await fetch(url.href, { + method, + headers, + body: body || undefined, + }); + const retStr = await res.text(); + try { + if (process.env.LOG_LEVEL === 'DEBUG') { + console.debug(formatTime(Date.now()), 'HyperliquidResponse', path, JSON.stringify(params), retStr); + } + return JSON.parse(retStr); + } catch (e) { + console.error(formatTime(Date.now()), 'HyperliquidRequestFailed', path, JSON.stringify(params), retStr); + throw e; + } + } + + mapPathToRequestChannel: Record< + string, + { + requestQueue: Array<{ + trace_id: string; + method: string; + path: string; + params?: any; + }>; + responseChannel: Subject<{ trace_id: string; response?: any; error?: Error }>; + } + > = {}; + + setupChannel(path: string, period: number, limit: number) { + this.mapPathToRequestChannel[path] = { + requestQueue: [], + responseChannel: new Subject(), + }; + + const { requestQueue, responseChannel } = this.mapPathToRequestChannel[path]; + timer(0, period) + .pipe( + filter(() => requestQueue.length > 0), + mergeMap(() => requestQueue.splice(0, limit)), + mergeMap(async (request) => { + try { + const res = await this.request(request.method, request.path, request.params); + return { trace_id: request.trace_id, response: res }; + } catch (error) { + return { trace_id: request.trace_id, error }; + } + }), + ) + .subscribe(responseChannel); + } + + async requestWithFlowControl( + method: string, + path: string, + flowControl: { period: number; limit: number } = { period: 10, limit: Infinity }, + params?: any, + ) { + const { period, limit } = flowControl; + if (!this.mapPathToRequestChannel[path]) { + this.setupChannel(path, period, limit); + } + const uuid = UUID(); + + const { requestQueue, responseChannel } = this.mapPathToRequestChannel[path]; + const res$ = responseChannel.pipe( + // + filter((response) => response.trace_id === uuid), + mergeMap((response) => (response.error ? throwError(() => response.error) : of(response))), + timeout(30_000), + shareReplay(1), + ); + requestQueue.push({ trace_id: uuid, method, path, params }); + return (await firstValueFrom(res$)).response; + } + + /** + * info + * + * https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + */ + getUserPerpetualsAccountSummary = (params: { + user: string; + }): Promise<{ + marginSummary: { + accountValue: string; + totalNtlPos: string; + totalRawUsd: string; + totalMarginUsed: string; + }; + crossMarginSummary: { + accountValue: string; + totalNtlPos: string; + totalRawUsd: string; + totalMarginUsed: string; + }; + crossMaintenanceMarginUsed: string; + withdrawable: string; + assetPositions: { + type: string; + position: { + coin: string; + szi: string; + leverage: { + type: string; + value: number; + }; + entryPx: string; + positionValue: string; + unrealizedPnl: string; + returnOnEquity: string; + liquidationPx: string; + marginUsed: string; + maxLeverage: number; + cumFunding: { + allTime: string; + sinceOpen: string; + sinceChange: string; + }; + }; + }[]; + time: number; + }> => this.request('POST', 'info', { ...params, type: 'clearinghouseState' }); + + /** + * info + * + * https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-metadata + */ + getPerpetualsMetaData = (): Promise<{ + universe: { + name: string; + szDecimals: number; + maxLeverage: number; + onlyIsolated?: boolean; + isDelisted?: boolean; + }[]; + }> => this.request('POST', 'info', { type: 'meta' }); + + /** + * info + * + * https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-metadata + */ + getSpotMetaData = (): Promise<{ + tokens: { + name: string; + szDecimals: number; + weiDecimals: number; + index: number; + tokenId: string; + isCanonical: boolean; + evmContract: null; + fullName: null; + }[]; + universe: { + name: string; + tokens: number[]; + index: number; + isCanonical: boolean; + }[]; + }> => this.request('POST', 'info', { type: 'spotMeta' }); + + /** + * info + * + * https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-a-users-funding-history-or-non-funding-ledger-updates + */ + getUserFundingHistory = (params: { + user: string; + startTime?: number; + endTime?: number; + }): Promise<{ + time: number; + hash: string; + delta: { + type: string; + coin: string; + usdc: string; + szi: string; + fundingRate: string; + }; + }> => this.request('POST', 'info', { ...params, type: 'fundingHistory' }); + + /** + * info + * + * https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-metadata + */ + getUserTokenBalances = (params: { + user: string; + }): Promise<{ + balances: { + coin: string; + token: number; + hold: string; + total: string; + entryNtl: string; + }[]; + }> => this.request('POST', 'info', { ...params, type: 'tokenBalances' }); +} diff --git a/apps/vendor-hyperliquid/src/extension.ts b/apps/vendor-hyperliquid/src/extension.ts new file mode 100644 index 000000000..ec860c17e --- /dev/null +++ b/apps/vendor-hyperliquid/src/extension.ts @@ -0,0 +1,88 @@ +import { IExtensionContext, makeDockerEnvs, makeK8sEnvs } from '@yuants/extension'; +export default (context: IExtensionContext) => { + context.registerDeployProvider({ + make_json_schema: () => ({ + type: 'object', + properties: { + env: { + type: 'object', + required: ['HOST_URL'], + properties: { + // + PUBLIC_ONLY: { type: 'boolean' }, + HOST_URL: { type: 'string' }, + TERMINAL_ID: { type: 'string' }, + PRIVATE_KEY: { type: 'string' }, + }, + }, + }, + }), + make_docker_compose_file: async (ctx, envCtx) => { + return { + [`hyperliquid-${ctx.env!.ACCESS_KEY}`.replace(/\s/g, '')]: { + image: `ghcr.io/no-trade-no-life/vendor-hyperliquid:${ctx.version ?? envCtx.version}`, + restart: 'always', + + environment: makeDockerEnvs(ctx.env), + }, + }; + }, + make_k8s_resource_objects: async (ctx, envCtx) => { + const COMPONENT_NAME = 'hyperliquid'; + return { + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + labels: { + 'y.ntnl.io/version': ctx.version ?? envCtx.version, + 'y.ntnl.io/manifest_key': ctx.key, + 'y.ntnl.io/component': COMPONENT_NAME, + }, + name: `hyperliquid-${ctx.key}`.replace(/\s/g, '').toLocaleLowerCase(), + namespace: 'yuan', + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + 'y.ntnl.io/component': COMPONENT_NAME, + 'y.ntnl.io/manifest_key': ctx.key, + }, + }, + template: { + metadata: { + labels: { + 'y.ntnl.io/version': ctx.version ?? envCtx.version, + 'y.ntnl.io/manifest_key': ctx.key, + 'y.ntnl.io/component': COMPONENT_NAME, + }, + }, + spec: { + containers: [ + { + env: makeK8sEnvs(ctx.env), + image: `ghcr.io/no-trade-no-life/vendor-hyperliquid:${ctx.version ?? envCtx.version}`, + imagePullPolicy: 'IfNotPresent', + name: COMPONENT_NAME, + resources: { + limits: { + cpu: ctx.cpu?.max ?? '500m', + memory: ctx.memory?.max ?? '256Mi', + }, + requests: { + cpu: ctx.cpu?.min ?? '100m', + memory: ctx.memory?.min ?? '128Mi', + }, + }, + }, + ], + hostname: COMPONENT_NAME, + }, + }, + }, + }, + }; + }, + }); +}; diff --git a/apps/vendor-hyperliquid/src/index.ts b/apps/vendor-hyperliquid/src/index.ts new file mode 100644 index 000000000..df8a3a760 --- /dev/null +++ b/apps/vendor-hyperliquid/src/index.ts @@ -0,0 +1,165 @@ +import { + IAccountInfo, + IAccountMoney, + IPosition, + IProduct, + UUID, + encodePath, + formatTime, + getDataRecordWrapper, +} from '@yuants/data-model'; +import { Terminal, provideAccountInfo, writeDataRecords } from '@yuants/protocol'; +import '@yuants/protocol/lib/services'; +import '@yuants/protocol/lib/services/order'; +import '@yuants/protocol/lib/services/transfer'; +import { defer, delayWhen, from, map, merge, repeat, retry, shareReplay, tap } from 'rxjs'; +import { HyperliquidClient } from './api'; + +const DATASOURCE_ID = 'Hyperliquid'; + +const client = new HyperliquidClient({ + auth: process.env.PUBLIC_ONLY + ? undefined + : { + private_key: process.env.PRIVATE_KEY!, + }, +}); + +const memoizeMap = any>(fn: T): T => { + const cache: Record = {}; + return ((...params: any[]) => (cache[encodePath(params)] ??= fn(...params))) as T; +}; + +(async () => { + const terminal = new Terminal(process.env.HOST_URL!, { + terminal_id: process.env.TERMINAL_ID || `hyperliquid/${client.public_key}/${UUID()}`, + name: 'Hyperliquid', + }); + + const tokenProduct$ = defer(async () => { + const res = await client.getSpotMetaData(); + return res.tokens.map( + (token): IProduct => ({ + product_id: encodePath('SPOT', `${token.name}-USDC`), + datasource_id: DATASOURCE_ID, + quote_currency: 'USDC', + base_currency: token.name, + price_step: 1e-2, + volume_step: Number(`1e-${token.szDecimals}`), + }), + ); + }).pipe( + tap({ + error: (e) => { + console.error(formatTime(Date.now()), 'SpotProducts', e); + }, + }), + retry({ delay: 5000 }), + repeat({ delay: 86400_000 }), + shareReplay(1), + ); + + const perpetualProduct$ = defer(async () => { + const res = await client.getPerpetualsMetaData(); + return res.universe.map( + (product): IProduct => ({ + product_id: encodePath('PERPETUAL', `${product.name}-USDC`), + datasource_id: DATASOURCE_ID, + quote_currency: 'USD', + base_currency: product.name, + price_step: 1e-2, + volume_step: Number(`1e-${product.szDecimals}`), + }), + ); + }).pipe( + tap({ + error: (e) => { + console.error(formatTime(Date.now()), 'PerpetualProducts', e); + }, + }), + retry({ delay: 5000 }), + repeat({ delay: 86400_000 }), + shareReplay(1), + ); + + merge(tokenProduct$, perpetualProduct$) + .pipe( + // + delayWhen((products) => + from(writeDataRecords(terminal, products.map(getDataRecordWrapper('product')!))), + ), + ) + .subscribe((products) => { + console.info(formatTime(Date.now()), 'FUTUREProductsUpdated', products.length); + }); + + const mapProductIdToFuturesProduct$ = merge(tokenProduct$, perpetualProduct$).pipe( + // + map((products) => new Map(products.map((v) => [v.product_id, v]))), + shareReplay(1), + ); + + // swap account info + { + const swapAccountInfo$ = defer(async (): Promise => { + const accountRes = await client.getUserPerpetualsAccountSummary({ + user: client.public_key!, + }); + + const profit = accountRes.assetPositions + .map((pos) => +pos.position.unrealizedPnl) + .reduce((a, b) => a + b, 0); + + const money: IAccountMoney = { + currency: 'USDC', + equity: +accountRes.crossMarginSummary.accountValue, + profit: profit, + free: +accountRes.withdrawable, + used: +accountRes.crossMarginSummary.accountValue - +accountRes.withdrawable, + balance: +accountRes.crossMarginSummary.accountValue - profit, + }; + + return { + account_id: `Hyperliquid/${client.public_key}`, + money: money, + currencies: [money], + positions: accountRes.assetPositions.map( + (position): IPosition => ({ + position_id: `${position.position.coin}-USD`, + datasource_id: DATASOURCE_ID, + product_id: encodePath('PERPETUAL', `${position.position.coin}-USD`), + direction: +position.position.szi > 0 ? 'LONG' : 'SHORT', + volume: Math.abs(+position.position.szi), + free_volume: Math.abs(+position.position.szi), + position_price: +position.position.entryPx, + closable_price: Math.abs(+position.position.positionValue / +position.position.szi), + floating_profit: +position.position.unrealizedPnl, + valuation: +position.position.positionValue, + margin: +position.position.marginUsed, + }), + ), + orders: [], + updated_at: Date.now(), + }; + }).pipe( + // + tap({ + error: (e) => { + console.error(formatTime(Date.now()), 'PerpetualAccountInfo', e); + }, + }), + retry({ delay: 5000 }), + repeat({ delay: 1000 }), + shareReplay(1), + ); + provideAccountInfo(terminal, swapAccountInfo$); + } + + // TODO: spot account info + + // TODO: trade api + + // TODO: funding rate + + // TODO: transfer +})(); diff --git a/apps/vendor-hyperliquid/tsconfig.json b/apps/vendor-hyperliquid/tsconfig.json new file mode 100644 index 000000000..81da8f781 --- /dev/null +++ b/apps/vendor-hyperliquid/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "types": ["heft-jest", "node"], + "skipLibCheck": true + } +} diff --git a/common/changes/@yuants/vendor-hyperliquid/2025-02-19-13-29.json b/common/changes/@yuants/vendor-hyperliquid/2025-02-19-13-29.json new file mode 100644 index 000000000..c83ce265b --- /dev/null +++ b/common/changes/@yuants/vendor-hyperliquid/2025-02-19-13-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@yuants/vendor-hyperliquid", + "comment": "init hyperliquid", + "type": "patch" + } + ], + "packageName": "@yuants/vendor-hyperliquid" +} \ No newline at end of file diff --git a/common/config/rush/deploy.json b/common/config/rush/deploy.json index 13664403c..8bdcd3a65 100644 --- a/common/config/rush/deploy.json +++ b/common/config/rush/deploy.json @@ -46,6 +46,7 @@ "@yuants/vendor-binance", "@yuants/vendor-bitget", "@yuants/vendor-coinex", + "@yuants/vendor-hyperliquid", "@yuants/vendor-okx", "@yuants/vendor-solana", "@yuants/vendor-huobi", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2289af79b..c32c65094 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1307,6 +1307,47 @@ importers: '@yuants/tool-kit': link:../../tools/toolkit typescript: 4.7.4 + ../../apps/vendor-hyperliquid: + specifiers: + '@microsoft/api-extractor': ~7.30.0 + '@rushstack/heft': ~0.47.5 + '@rushstack/heft-jest-plugin': ~0.3.30 + '@rushstack/heft-node-rig': ~1.10.7 + '@types/heft-jest': 1.0.3 + '@types/node': '18' + '@yuants/data-model': workspace:* + '@yuants/data-series': workspace:* + '@yuants/extension': workspace:* + '@yuants/protocol': workspace:* + '@yuants/tool-kit': workspace:* + '@yuants/utils': workspace:* + crypto-js: ^4.2.0 + ethers: ~6.13.5 + hyperliquid: ~1.6.2 + rxjs: ~7.5.6 + ts-node: ~10.9.2 + typescript: ~4.7.4 + dependencies: + '@yuants/data-model': link:../../libraries/data-model + '@yuants/data-series': link:../../libraries/data-series + '@yuants/protocol': link:../../libraries/protocol + '@yuants/utils': link:../../libraries/utils + crypto-js: 4.2.0 + ethers: 6.13.5 + hyperliquid: 1.6.2 + rxjs: 7.5.7 + devDependencies: + '@microsoft/api-extractor': 7.30.1 + '@rushstack/heft': 0.47.11 + '@rushstack/heft-jest-plugin': 0.3.45_154bfccaf757d04bbe52f933b349182c + '@rushstack/heft-node-rig': 1.10.13_154bfccaf757d04bbe52f933b349182c + '@types/heft-jest': 1.0.3 + '@types/node': 18.17.12 + '@yuants/extension': link:../../libraries/extension + '@yuants/tool-kit': link:../../tools/toolkit + ts-node: 10.9.2_8b03ecee800523adaf3df18ee3a13756 + typescript: 4.7.4 + ../../apps/vendor-okx: specifiers: '@microsoft/api-extractor': ~7.30.0 @@ -2146,6 +2187,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@adraffy/ens-normalize/1.10.1: + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + dev: false + /@algolia/autocomplete-core/1.9.3_algoliasearch@4.20.0: resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} dependencies: @@ -7666,6 +7711,22 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true + /@msgpack/msgpack/3.0.1: + resolution: {integrity: sha512-9qysoVTITLcOFIIJeXbdtUgvvY25ojUp+WWfLc0O4H4KKWeamUNAqkjS5mej/PnVDnH70llWKNa7pzv5U4TqVQ==} + engines: {node: '>= 18'} + dev: false + + /@noble/curves/1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + dependencies: + '@noble/hashes': 1.3.2 + dev: false + + /@noble/hashes/1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + dev: false + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -9356,6 +9417,12 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node/22.7.5: + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + dependencies: + undici-types: 6.19.8 + dev: false + /@types/nodemailer/6.4.9: resolution: {integrity: sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==} dependencies: @@ -9836,6 +9903,10 @@ packages: engines: {node: '>= 10.0.0'} dev: false + /aes-js/4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + dev: false + /agent-base/6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -13692,6 +13763,22 @@ packages: engines: {node: '>= 0.6'} dev: false + /ethers/6.13.5: + resolution: {integrity: sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /eval/0.1.8: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} @@ -15277,6 +15364,19 @@ packages: ms: 2.1.3 dev: false + /hyperliquid/1.6.2: + resolution: {integrity: sha512-ENrYysYHd3Zzgl+SE039QFBpYd5dRX6177rn3pU+w9SRYMaieEGCL+Qzb8wrw679CIJn1VnGSSKJmjCdDHfAwA==} + engines: {node: '>=16.0.0'} + dependencies: + '@msgpack/msgpack': 3.0.1 + axios: 1.7.9 + ethers: 6.13.5 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: false + /i18next-browser-languagedetector/7.1.0: resolution: {integrity: sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==} dependencies: @@ -22431,6 +22531,10 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: false + /tslib/2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: false + /tunnel-agent/0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -22615,6 +22719,10 @@ packages: /undici-types/5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici-types/6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: false + /unherit/1.1.3: resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} dependencies: @@ -23932,6 +24040,19 @@ packages: optional: true dev: false + /ws/8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws/8.8.1: resolution: {integrity: sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==} engines: {node: '>=10.0.0'} diff --git a/rush.json b/rush.json index 3d798fc2f..128828f9d 100644 --- a/rush.json +++ b/rush.json @@ -665,6 +665,11 @@ "projectFolder": "apps/vendor-coinex", "shouldPublish": true }, + { + "packageName": "@yuants/vendor-hyperliquid", + "projectFolder": "apps/vendor-hyperliquid", + "shouldPublish": true + }, { "packageName": "@yuants/vendor-okx", "projectFolder": "apps/vendor-okx",