mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat: integrate Extended exchange support with new API, adapter, and order handling functionality
This commit is contained in:
@@ -114,3 +114,16 @@ PARADEX_WALLET_ADDRESS=
|
||||
|
||||
# Enable verbose adapter logging: set to "1" or "true"
|
||||
# PARADEX_DEBUG=false
|
||||
|
||||
# Extended exchange configuration (Starknet)
|
||||
# Set EXCHANGE=extended to activate. Requires on-chain Stark key + vault id.
|
||||
EXTENDED_API_KEY=
|
||||
EXTENDED_STARK_PRIVATE_KEY= # 0x-prefixed Stark private key for signing orders
|
||||
EXTENDED_VAULT_ID= # Vault/sub-account id from Extended UI
|
||||
EXTENDED_MARKET=BTC-USD # Defaults to TRADE_SYMBOL when omitted
|
||||
|
||||
# Optional overrides (defaults target mainnet host)
|
||||
# EXTENDED_API_HOST=api.starknet.extended.exchange
|
||||
# EXTENDED_STREAM_HOST=api.starknet.extended.exchange
|
||||
# EXTENDED_PRIVATE_STREAM_HOST=api.starknet.extended.exchange
|
||||
# EXTENDED_USER_AGENT=ritmex-bot
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"@x10xchange/stark-crypto-wrapper-wasm": "^0.2.0",
|
||||
"axios": "^1.12.2",
|
||||
"bignumber.js": "^9.3.1",
|
||||
"ccxt": "^4.5.12",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ink": "^6.3.1",
|
||||
"react": "^19.1.1",
|
||||
"starknet": "^8.5.4",
|
||||
"ws": "^8.18.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -132,12 +136,18 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
|
||||
|
||||
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
|
||||
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
|
||||
|
||||
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="],
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
|
||||
|
||||
"@scure/starknet": ["@scure/starknet@1.1.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0" } }, "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ=="],
|
||||
|
||||
"@starknet-io/starknet-types-08": ["@starknet-io/types-js@0.8.4", "", {}, "sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ=="],
|
||||
|
||||
"@starknet-io/starknet-types-09": ["@starknet-io/types-js@0.9.2", "", {}, "sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
@@ -168,12 +178,18 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"@x10xchange/stark-crypto-wrapper-wasm": ["@x10xchange/stark-crypto-wrapper-wasm@0.2.0", "", {}, "sha512-pi7cHvDgPf9Ri9q0cvrUnCnucafB+F3ch7ia0oEJhKMY+bWvxWRYA2vve9s6UMMNV6PnmTyFZPOYptVGGdLTWA=="],
|
||||
|
||||
"abi-wan-kanabi": ["abi-wan-kanabi@2.2.4", "", { "dependencies": { "ansicolors": "^0.3.2", "cardinal": "^2.1.1", "fs-extra": "^10.0.0", "yargs": "^17.7.2" }, "bin": { "generate": "dist/generate.js" } }, "sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.1.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
@@ -182,12 +198,16 @@
|
||||
|
||||
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"cardinal": ["cardinal@2.1.1", "", { "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" }, "bin": { "cdl": "./bin/cdl.js" } }, "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw=="],
|
||||
|
||||
"ccxt": ["ccxt@4.5.12", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-2lfL2TKjq4vBkQUQWJfDqFywhvYCZmk9r0SWC8GqA4AHZ6qozKVUJowxQTvdRsLX9jBwYSE0nc7JVurBrQ6SHg=="],
|
||||
|
||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||
@@ -202,14 +222,22 @@
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
@@ -238,8 +266,12 @@
|
||||
|
||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="],
|
||||
@@ -252,10 +284,14 @@
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "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-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"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-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"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=="],
|
||||
@@ -264,6 +300,8 @@
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -280,6 +318,10 @@
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||
|
||||
"lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
@@ -298,6 +340,8 @@
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@@ -316,6 +360,10 @@
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
|
||||
|
||||
"redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="],
|
||||
@@ -334,6 +382,8 @@
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"starknet": ["starknet@8.9.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0", "@scure/base": "~1.2.1", "@scure/starknet": "1.1.0", "@starknet-io/starknet-types-08": "npm:@starknet-io/types-js@~0.8.4", "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", "abi-wan-kanabi": "2.2.4", "lossless-json": "^4.2.0", "pako": "^2.0.4", "ts-mixer": "^6.0.3" } }, "sha512-uhXSCKW0SMBGckd4yVlscVz4DMkQqZ+/RTpfxZ5J46e6AuCUHOqGKv61tqgUP9GhdvgHV/XyN+kdmlyZn3rXjA=="],
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
@@ -354,12 +404,16 @@
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
||||
|
||||
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
@@ -374,10 +428,56 @@
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
|
||||
|
||||
"@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
|
||||
|
||||
"@scure/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
|
||||
|
||||
"@scure/starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
|
||||
|
||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"cliui/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=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/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=="],
|
||||
|
||||
"starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
|
||||
|
||||
"starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
|
||||
|
||||
"yargs/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=="],
|
||||
|
||||
"@scure/starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
|
||||
|
||||
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
|
||||
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,16 @@
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"@x10xchange/stark-crypto-wrapper-wasm": "^0.2.0",
|
||||
"axios": "^1.12.2",
|
||||
"bignumber.js": "^9.3.1",
|
||||
"ccxt": "^4.5.12",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ink": "^6.3.1",
|
||||
"react": "^19.1.1",
|
||||
"starknet": "^8.5.4",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
|
||||
lighter: { envKeys: ["LIGHTER_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
|
||||
backpack: { envKeys: ["BACKPACK_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDC" },
|
||||
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
|
||||
extended: { envKeys: ["EXTENDED_MARKET", "EXTENDED_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
|
||||
};
|
||||
|
||||
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
||||
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
||||
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
||||
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
|
||||
import { ExtendedExchangeAdapter, type ExtendedCredentials } from "./extended/adapter";
|
||||
|
||||
export interface ExchangeFactoryOptions {
|
||||
symbol: string;
|
||||
@@ -13,9 +14,10 @@ export interface ExchangeFactoryOptions {
|
||||
lighter?: LighterCredentials;
|
||||
backpack?: BackpackCredentials;
|
||||
paradex?: ParadexCredentials;
|
||||
extended?: ExtendedCredentials;
|
||||
}
|
||||
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex";
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "extended";
|
||||
|
||||
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
||||
@@ -26,6 +28,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
if (fallback === "lighter") return "lighter";
|
||||
if (fallback === "backpack") return "backpack";
|
||||
if (fallback === "paradex") return "paradex";
|
||||
if (fallback === "extended") return "extended";
|
||||
return "aster";
|
||||
}
|
||||
|
||||
@@ -34,6 +37,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
|
||||
if (id === "lighter") return "Lighter";
|
||||
if (id === "backpack") return "Backpack";
|
||||
if (id === "paradex") return "Paradex";
|
||||
if (id === "extended") return "Extended";
|
||||
return "AsterDex";
|
||||
}
|
||||
|
||||
@@ -51,5 +55,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
|
||||
if (id === "paradex") {
|
||||
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
|
||||
}
|
||||
if (id === "extended") {
|
||||
return new ExtendedExchangeAdapter({ ...options.extended, market: options.symbol });
|
||||
}
|
||||
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
ExchangeAdapter,
|
||||
ExchangePrecision,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { ExtendedGateway, type ExtendedGatewayOptions } from "./gateway";
|
||||
|
||||
export interface ExtendedCredentials {
|
||||
apiKey?: string;
|
||||
starkPrivateKey?: string;
|
||||
vaultId?: string;
|
||||
market?: string;
|
||||
apiHost?: string;
|
||||
streamHost?: string;
|
||||
privateStreamHost?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export class ExtendedExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "extended";
|
||||
private readonly gateway: ExtendedGateway;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: ExtendedCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.EXTENDED_API_KEY;
|
||||
const starkPrivateKey = credentials.starkPrivateKey ?? process.env.EXTENDED_STARK_PRIVATE_KEY;
|
||||
const vaultId = credentials.vaultId ?? process.env.EXTENDED_VAULT_ID;
|
||||
const market = credentials.market ?? process.env.EXTENDED_MARKET ?? process.env.TRADE_SYMBOL ?? "BTC-USD";
|
||||
if (!apiKey) throw new Error("Missing EXTENDED_API_KEY");
|
||||
if (!starkPrivateKey) throw new Error("Missing EXTENDED_STARK_PRIVATE_KEY");
|
||||
if (!vaultId) throw new Error("Missing EXTENDED_VAULT_ID");
|
||||
const options: ExtendedGatewayOptions = {
|
||||
apiKey,
|
||||
starkPrivateKey,
|
||||
vaultId,
|
||||
market,
|
||||
apiHost: credentials.apiHost ?? process.env.EXTENDED_API_HOST,
|
||||
streamHost: credentials.streamHost ?? process.env.EXTENDED_STREAM_HOST,
|
||||
privateStreamHost: credentials.privateStreamHost ?? process.env.EXTENDED_PRIVATE_STREAM_HOST,
|
||||
userAgent: credentials.userAgent ?? process.env.EXTENDED_USER_AGENT,
|
||||
logger: (context, error) => this.log(context, error),
|
||||
};
|
||||
this.gateway = new ExtendedGateway(options);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized(`watchKlines:${interval}`);
|
||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder({ orderId: params.orderId });
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders();
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||
try {
|
||||
const precision = await this.gateway.getPrecision();
|
||||
if (!precision) return null;
|
||||
return {
|
||||
priceTick: precision.priceTick,
|
||||
qtyStep: precision.qtyStep,
|
||||
};
|
||||
} catch (error) {
|
||||
this.log("getPrecision", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
this.log(`${context} handler`, error);
|
||||
}
|
||||
}) as T;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private ensureInitialized(context?: string): Promise<void> {
|
||||
if (!this.initPromise) {
|
||||
this.initContexts.clear();
|
||||
this.initPromise = this.gateway.ensureInitialized().catch((error) => {
|
||||
this.handleInitError("initialize", error);
|
||||
this.initPromise = null;
|
||||
this.scheduleRetry();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
if (context && !this.initContexts.has(context)) {
|
||||
this.initContexts.add(context);
|
||||
this.initPromise.catch((error) => {
|
||||
this.handleInitError(context, error);
|
||||
this.scheduleRetry();
|
||||
});
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private scheduleRetry(): void {
|
||||
if (this.retryTimer) return;
|
||||
this.retryTimer = setTimeout(() => {
|
||||
this.retryTimer = null;
|
||||
if (this.initPromise) return;
|
||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
||||
void this.ensureInitialized("retry");
|
||||
}, this.retryDelayMs);
|
||||
}
|
||||
|
||||
private handleInitError(context: string, error: unknown): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastInitErrorAt < 5000) return;
|
||||
this.lastInitErrorAt = now;
|
||||
this.log(context, error);
|
||||
}
|
||||
|
||||
private log(context: string, error: unknown): void {
|
||||
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
|
||||
console.error(`[ExtendedExchangeAdapter] ${context}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import NodeWebSocket from "ws";
|
||||
import { setTimeout, clearTimeout, setInterval, clearInterval } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type {
|
||||
AsterAccountAsset,
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
} from "../types";
|
||||
import { toDecimal } from "./math";
|
||||
import { ExtendedOrderBuilder, type ExtendedOrderContext } from "./order-builder";
|
||||
import { tryInitExtendedWasm } from "./signing";
|
||||
import type {
|
||||
ExtendedBalance,
|
||||
ExtendedCandle,
|
||||
ExtendedDepthMessage,
|
||||
ExtendedFees,
|
||||
ExtendedMarket,
|
||||
ExtendedOrder,
|
||||
ExtendedPosition,
|
||||
ExtendedStarknetDomain,
|
||||
ExtendedTrade,
|
||||
} from "./types";
|
||||
|
||||
const WebSocketCtor: typeof globalThis.WebSocket =
|
||||
typeof globalThis.WebSocket !== "undefined" ? globalThis.WebSocket : ((NodeWebSocket as unknown) as typeof globalThis.WebSocket);
|
||||
type WebSocket = typeof WebSocketCtor extends { new (...args: any[]): infer R } ? R : NodeWebSocket;
|
||||
|
||||
const RECONNECT_DELAY_MS = 3000;
|
||||
|
||||
export interface ExtendedGatewayOptions {
|
||||
apiKey: string;
|
||||
starkPrivateKey: string;
|
||||
vaultId: string;
|
||||
market: string;
|
||||
apiHost?: string;
|
||||
streamHost?: string;
|
||||
privateStreamHost?: string;
|
||||
userAgent?: string;
|
||||
logger?: (context: string, error: unknown) => void;
|
||||
}
|
||||
|
||||
interface AccountState {
|
||||
balance: ExtendedBalance | null;
|
||||
positions: ExtendedPosition[];
|
||||
orders: ExtendedOrder[];
|
||||
}
|
||||
|
||||
interface DepthState {
|
||||
bids: [string, string][];
|
||||
asks: [string, string][];
|
||||
seq: number;
|
||||
ts: number;
|
||||
}
|
||||
|
||||
export class ExtendedGateway {
|
||||
private readonly options: ExtendedGatewayOptions;
|
||||
private readonly logger: (context: string, error: unknown) => void;
|
||||
private readonly axios: AxiosInstance;
|
||||
private marketInfo: ExtendedMarket | null = null;
|
||||
private fees: ExtendedFees | null = null;
|
||||
private domain: ExtendedStarknetDomain | null = null;
|
||||
private orderBuilder: ExtendedOrderBuilder | null = null;
|
||||
private initialized = false;
|
||||
|
||||
private accountState: AccountState = { balance: null, positions: [], orders: [] };
|
||||
private readonly accountListeners = new Set<AccountListener>();
|
||||
private readonly orderListeners = new Set<OrderListener>();
|
||||
private readonly depthListeners = new Set<DepthListener>();
|
||||
private readonly tickerListeners = new Set<TickerListener>();
|
||||
private readonly klineListeners = new Map<string, Set<KlineListener>>();
|
||||
|
||||
private accountWs: WebSocket | null = null;
|
||||
private depthWs: WebSocket | null = null;
|
||||
private tradesWs: WebSocket | null = null;
|
||||
private candleWs = new Map<string, WebSocket>();
|
||||
private reconnectTimers: Array<ReturnType<typeof setTimeout>> = [];
|
||||
private lastDepth: DepthState | null = null;
|
||||
private lastTrade: ExtendedTrade | null = null;
|
||||
|
||||
constructor(options: ExtendedGatewayOptions) {
|
||||
this.options = options;
|
||||
this.logger =
|
||||
options.logger ??
|
||||
((context, error) => {
|
||||
console.error(`[ExtendedGateway] ${context}`, error);
|
||||
});
|
||||
const baseURL = `https://${options.apiHost ?? "api.starknet.extended.exchange"}`;
|
||||
this.axios = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"X-Api-Key": options.apiKey,
|
||||
"User-Agent": options.userAgent ?? "ritmex-bot",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async ensureInitialized(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
this.logInfo("init", `Initializing Extended for ${this.options.market}`);
|
||||
await tryInitExtendedWasm();
|
||||
await Promise.all([this.loadMarket(), this.loadFees(), this.loadDomain()]);
|
||||
this.orderBuilder = new ExtendedOrderBuilder(this.buildOrderContext());
|
||||
await this.loadInitialAccount();
|
||||
this.startAccountStream();
|
||||
this.startDepthStream();
|
||||
this.startTradesStream();
|
||||
this.initialized = true;
|
||||
this.logInfo("init", "Extended gateway ready");
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.closeSocket(this.accountWs);
|
||||
this.closeSocket(this.depthWs);
|
||||
this.closeSocket(this.tradesWs);
|
||||
for (const ws of this.candleWs.values()) {
|
||||
this.closeSocket(ws);
|
||||
}
|
||||
this.candleWs.clear();
|
||||
this.reconnectTimers.forEach((timer) => clearTimeout(timer));
|
||||
this.reconnectTimers = [];
|
||||
}
|
||||
|
||||
onAccount(listener: AccountListener): void {
|
||||
this.accountListeners.add(listener);
|
||||
const snapshot = this.buildAccountSnapshot();
|
||||
if (snapshot) {
|
||||
try {
|
||||
listener(snapshot);
|
||||
} catch (error) {
|
||||
this.logger("accountReplay", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOrders(listener: OrderListener): void {
|
||||
this.orderListeners.add(listener);
|
||||
if (this.accountState.orders.length) {
|
||||
try {
|
||||
listener(this.accountState.orders.map((order) => this.mapOrder(order)));
|
||||
} catch (error) {
|
||||
this.logger("ordersReplay", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDepth(listener: DepthListener): void {
|
||||
this.depthListeners.add(listener);
|
||||
if (this.lastDepth) {
|
||||
listener(this.mapDepth(this.lastDepth));
|
||||
}
|
||||
}
|
||||
|
||||
onTicker(listener: TickerListener): void {
|
||||
this.tickerListeners.add(listener);
|
||||
const ticker = this.buildTicker();
|
||||
if (ticker) {
|
||||
listener(ticker);
|
||||
}
|
||||
}
|
||||
|
||||
watchKlines(interval: string, listener: KlineListener): void {
|
||||
const set = this.klineListeners.get(interval) ?? new Set<KlineListener>();
|
||||
set.add(listener);
|
||||
this.klineListeners.set(interval, set);
|
||||
this.ensureCandleStream(interval);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
if (!this.orderBuilder) {
|
||||
await this.ensureInitialized();
|
||||
}
|
||||
const builder = this.orderBuilder!;
|
||||
const marketPrice = this.estimateMarketPrice(params.side);
|
||||
const built = builder.build({ ...params, marketPrice });
|
||||
const { data } = await this.axios.post<{ data?: ExtendedOrder; status?: string }>("/api/v1/user/order", built.payload);
|
||||
// Some responses only return status; fallback to local mapping using request params.
|
||||
const order = data?.data ?? null;
|
||||
if (order) {
|
||||
return this.mapOrder(order);
|
||||
}
|
||||
// Fallback: return lightweight order reflecting submission.
|
||||
return this.mapOrder({
|
||||
id: built.orderId,
|
||||
market: this.options.market,
|
||||
type: this.normalizeOrderType(params.type),
|
||||
side: params.side,
|
||||
status: "NEW",
|
||||
price: built.payload.price as string,
|
||||
qty: built.payload.qty as string,
|
||||
filledQty: "0",
|
||||
reduceOnly: built.payload.reduceOnly as boolean,
|
||||
postOnly: built.payload.postOnly as boolean,
|
||||
createdTime: Date.now(),
|
||||
updatedTime: Date.now(),
|
||||
trigger: built.payload.trigger as any,
|
||||
});
|
||||
}
|
||||
|
||||
async cancelOrder(params: { orderId: number | string }): Promise<void> {
|
||||
await this.axios.delete(`/api/v1/user/order/${params.orderId}`);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.axios.post("/api/v1/user/order/massCancel", { orderIds: params.orderIdList });
|
||||
}
|
||||
|
||||
async cancelAllOrders(): Promise<void> {
|
||||
await this.axios.post("/api/v1/user/order/massCancel", { markets: [this.options.market] });
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<{ priceTick: number; qtyStep: number } | null> {
|
||||
if (!this.marketInfo) return null;
|
||||
const { tradingConfig } = this.marketInfo;
|
||||
return {
|
||||
priceTick: Number(tradingConfig.minPriceChange),
|
||||
qtyStep: Number(tradingConfig.minOrderSizeChange),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Internal fetchers --------------------------------------------------
|
||||
|
||||
private async loadMarket(): Promise<void> {
|
||||
const { data } = await this.axios.get<{ data: ExtendedMarket[] }>("/api/v1/info/markets", {
|
||||
params: { market: [this.options.market] },
|
||||
});
|
||||
const market = (data?.data ?? [])[0];
|
||||
if (!market) {
|
||||
throw new Error(`Extended market not found: ${this.options.market}`);
|
||||
}
|
||||
this.marketInfo = market;
|
||||
}
|
||||
|
||||
private async loadFees(): Promise<void> {
|
||||
const { data } = await this.axios.get<{ data: ExtendedFees[] }>("/api/v1/user/fees", {
|
||||
params: { market: [this.options.market] },
|
||||
});
|
||||
const fees = (data?.data ?? [])[0];
|
||||
if (!fees) {
|
||||
throw new Error("Unable to fetch Extended fee configuration");
|
||||
}
|
||||
this.fees = fees;
|
||||
}
|
||||
|
||||
private async loadDomain(): Promise<void> {
|
||||
const { data } = await this.axios.get<{ data: ExtendedStarknetDomain }>("/api/v1/info/starknet");
|
||||
if (!data?.data) {
|
||||
throw new Error("Failed to load Extended Starknet domain");
|
||||
}
|
||||
this.domain = data.data;
|
||||
}
|
||||
|
||||
private async loadInitialAccount(): Promise<void> {
|
||||
try {
|
||||
const [balanceRes, positionsRes, ordersRes] = await Promise.all([
|
||||
this.axios.get<{ data: ExtendedBalance }>("/api/v1/user/balance"),
|
||||
this.axios.get<{ data: ExtendedPosition[] }>("/api/v1/user/positions", {
|
||||
params: { market: [this.options.market] },
|
||||
}),
|
||||
this.axios.get<{ data: ExtendedOrder[] }>("/api/v1/user/orders", {
|
||||
params: { market: [this.options.market] },
|
||||
}),
|
||||
]);
|
||||
this.accountState.balance = balanceRes.data?.data ?? null;
|
||||
this.accountState.positions = positionsRes.data?.data ?? [];
|
||||
this.accountState.orders = ordersRes.data?.data ?? [];
|
||||
this.emitAccount();
|
||||
this.emitOrders();
|
||||
} catch (error) {
|
||||
this.logger("loadInitialAccount", error);
|
||||
}
|
||||
}
|
||||
|
||||
private buildOrderContext(): ExtendedOrderContext {
|
||||
if (!this.marketInfo || !this.fees || !this.domain) {
|
||||
throw new Error("Extended gateway not ready");
|
||||
}
|
||||
return {
|
||||
market: this.marketInfo,
|
||||
fees: this.fees,
|
||||
domain: this.domain,
|
||||
vaultId: this.options.vaultId,
|
||||
starkPrivateKey: this.options.starkPrivateKey,
|
||||
builderFeeRate: this.fees.builderFeeRate,
|
||||
};
|
||||
}
|
||||
|
||||
// ---- WebSocket handling -------------------------------------------------
|
||||
|
||||
private startAccountStream(): void {
|
||||
const host = this.options.privateStreamHost ?? this.options.streamHost ?? "api.starknet.extended.exchange";
|
||||
const url = `wss://${host}/stream.extended.exchange/v1/account`;
|
||||
this.accountWs = this.createSocket(
|
||||
url,
|
||||
{ headers: { "X-Api-Key": this.options.apiKey, "User-Agent": this.options.userAgent ?? "ritmex-bot" } },
|
||||
"account",
|
||||
(payload) => {
|
||||
this.handleAccountMessage(payload);
|
||||
},
|
||||
(socket) => {
|
||||
this.accountWs = socket;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private startDepthStream(): void {
|
||||
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
|
||||
const url = `wss://${host}/stream.extended.exchange/v1/orderbooks/${encodeURIComponent(this.options.market)}?depth=1`;
|
||||
this.depthWs = this.createSocket(
|
||||
url,
|
||||
undefined,
|
||||
"depth",
|
||||
(payload) => {
|
||||
this.handleDepthMessage(payload as ExtendedDepthMessage);
|
||||
},
|
||||
(socket) => {
|
||||
this.depthWs = socket;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private startTradesStream(): void {
|
||||
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
|
||||
const url = `wss://${host}/stream.extended.exchange/v1/publicTrades/${encodeURIComponent(this.options.market)}`;
|
||||
this.tradesWs = this.createSocket(
|
||||
url,
|
||||
undefined,
|
||||
"trades",
|
||||
(payload) => {
|
||||
this.handleTradesMessage(payload as { data?: ExtendedTrade[]; ts?: number; seq?: number });
|
||||
},
|
||||
(socket) => {
|
||||
this.tradesWs = socket;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private ensureCandleStream(interval: string): void {
|
||||
if (this.candleWs.has(interval)) return;
|
||||
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
|
||||
const url = `wss://${host}/stream.extended.exchange/v1/candles/${encodeURIComponent(this.options.market)}/trades?interval=${encodeURIComponent(interval)}`;
|
||||
const ws = this.createSocket(
|
||||
url,
|
||||
undefined,
|
||||
`candle:${interval}`,
|
||||
(payload) => {
|
||||
this.handleCandleMessage(interval, payload as { data?: ExtendedCandle[] });
|
||||
},
|
||||
(socket) => {
|
||||
this.candleWs.set(interval, socket);
|
||||
}
|
||||
);
|
||||
this.candleWs.set(interval, ws);
|
||||
}
|
||||
|
||||
private createSocket(
|
||||
url: string,
|
||||
options: NodeWebSocket.ClientOptions | undefined,
|
||||
context: string,
|
||||
handler: (data: any) => void,
|
||||
onCreate?: (socket: WebSocket) => void
|
||||
): WebSocket {
|
||||
const ws = new WebSocketCtor(url, options as any);
|
||||
if (onCreate) {
|
||||
onCreate(ws as any);
|
||||
}
|
||||
let pingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
ws.onopen = () => {
|
||||
if (pingTimer) clearInterval(pingTimer);
|
||||
pingTimer = setInterval(() => {
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: "ping" }));
|
||||
} catch (_error) {
|
||||
/* ignore */
|
||||
}
|
||||
}, 15_000);
|
||||
this.logInfo(`${context}:open`, url);
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
||||
if (data === "ping") {
|
||||
ws.send("pong");
|
||||
return;
|
||||
}
|
||||
handler(JSON.parse(data));
|
||||
} catch (error) {
|
||||
this.logger(`${context}:message`, error);
|
||||
}
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
this.logError(`${context}:error`, error);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (pingTimer) clearInterval(pingTimer);
|
||||
const timer = setTimeout(() => {
|
||||
this.logInfo(`${context}:reconnect`, `reconnecting to ${url}`);
|
||||
this.reconnectTimers = this.reconnectTimers.filter((t) => t !== timer);
|
||||
this.createSocket(url, options, context, handler, onCreate);
|
||||
}, RECONNECT_DELAY_MS);
|
||||
this.reconnectTimers.push(timer);
|
||||
this.logInfo(`${context}:close`, `closed ${url}`);
|
||||
};
|
||||
return ws;
|
||||
}
|
||||
|
||||
private closeSocket(socket: WebSocket | null): void {
|
||||
if (socket && socket.readyState === socket.OPEN) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- WS handlers -------------------------------------------------------
|
||||
|
||||
private handleAccountMessage(message: any): void {
|
||||
if (!message || typeof message !== "object") return;
|
||||
const type = message.type;
|
||||
const data = message.data ?? {};
|
||||
if (type === "BALANCE" && data.balance) {
|
||||
this.accountState.balance = data.balance as ExtendedBalance;
|
||||
this.emitAccount();
|
||||
this.logInfo("account:balance", "updated");
|
||||
return;
|
||||
}
|
||||
if (type === "POSITION" && Array.isArray(data.positions)) {
|
||||
this.accountState.positions = data.positions as ExtendedPosition[];
|
||||
this.emitAccount();
|
||||
this.logInfo("account:positions", `positions=${data.positions.length}`);
|
||||
return;
|
||||
}
|
||||
if (type === "ORDER" && Array.isArray(data.orders)) {
|
||||
this.accountState.orders = data.orders as ExtendedOrder[];
|
||||
this.emitOrders();
|
||||
this.logInfo("account:orders", `orders=${data.orders.length}`);
|
||||
return;
|
||||
}
|
||||
if (type === "TRADE" && Array.isArray(data.trades)) {
|
||||
// Trades can update fills; refresh orders if present.
|
||||
if (Array.isArray(data.orders)) {
|
||||
this.accountState.orders = data.orders as ExtendedOrder[];
|
||||
this.emitOrders();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Fallback: handle messages where data is nested without type
|
||||
const payload = message.data ?? message;
|
||||
if (payload?.balance) {
|
||||
this.accountState.balance = payload.balance as ExtendedBalance;
|
||||
this.emitAccount();
|
||||
this.logInfo("account:balance", "snapshot");
|
||||
}
|
||||
if (payload?.positions) {
|
||||
this.accountState.positions = payload.positions as ExtendedPosition[];
|
||||
this.emitAccount();
|
||||
this.logInfo("account:positions", `positions=${(payload.positions as ExtendedPosition[]).length}`);
|
||||
}
|
||||
if (payload?.orders) {
|
||||
this.accountState.orders = payload.orders as ExtendedOrder[];
|
||||
this.emitOrders();
|
||||
this.logInfo("account:orders", `orders=${(payload.orders as ExtendedOrder[]).length}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDepthMessage(message: ExtendedDepthMessage): void {
|
||||
if (!message?.data) return;
|
||||
const bids = (message.data.b ?? []).map((level) => [String(level.p), String(level.q)] as [string, string]);
|
||||
const asks = (message.data.a ?? []).map((level) => [String(level.p), String(level.q)] as [string, string]);
|
||||
this.lastDepth = { bids, asks, seq: message.seq ?? Date.now(), ts: message.ts ?? Date.now() };
|
||||
this.emitDepth();
|
||||
this.emitTicker();
|
||||
if (bids.length || asks.length) {
|
||||
this.logDebug("depth", `bids=${bids[0]?.[0] ?? "-"} asks=${asks[0]?.[0] ?? "-"}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTradesMessage(message: { data?: ExtendedTrade[]; ts?: number }): void {
|
||||
const trades = message?.data ?? [];
|
||||
if (!trades.length) return;
|
||||
this.lastTrade = trades[trades.length - 1];
|
||||
this.emitTicker();
|
||||
this.logDebug("trades", `last=${this.lastTrade.p} side=${this.lastTrade.S}`);
|
||||
}
|
||||
|
||||
private handleCandleMessage(interval: string, payload: { data?: ExtendedCandle[] }): void {
|
||||
const candles = payload?.data ?? [];
|
||||
if (!candles.length) return;
|
||||
const mapped = candles.map((candle) => this.mapKline(candle));
|
||||
const listeners = this.klineListeners.get(interval);
|
||||
if (!listeners) return;
|
||||
listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(mapped);
|
||||
} catch (error) {
|
||||
this.logger(`kline:${interval}`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Emitters ----------------------------------------------------------
|
||||
|
||||
private emitAccount(): void {
|
||||
const snapshot = this.buildAccountSnapshot();
|
||||
if (!snapshot) return;
|
||||
for (const listener of this.accountListeners) {
|
||||
try {
|
||||
listener(snapshot);
|
||||
} catch (error) {
|
||||
this.logError("accountListener", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitOrders(): void {
|
||||
const mapped = this.accountState.orders.map((order) => this.mapOrder(order));
|
||||
for (const listener of this.orderListeners) {
|
||||
try {
|
||||
listener(mapped);
|
||||
} catch (error) {
|
||||
this.logError("orderListener", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitDepth(): void {
|
||||
if (!this.lastDepth) return;
|
||||
const depth = this.mapDepth(this.lastDepth);
|
||||
for (const listener of this.depthListeners) {
|
||||
try {
|
||||
listener(depth);
|
||||
} catch (error) {
|
||||
this.logError("depthListener", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitTicker(): void {
|
||||
const ticker = this.buildTicker();
|
||||
if (!ticker) return;
|
||||
for (const listener of this.tickerListeners) {
|
||||
try {
|
||||
listener(ticker);
|
||||
} catch (error) {
|
||||
this.logError("tickerListener", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mapping helpers ---------------------------------------------------
|
||||
|
||||
private buildAccountSnapshot(): AsterAccountSnapshot | null {
|
||||
const balance = this.accountState.balance;
|
||||
if (!balance) return null;
|
||||
const positions = (this.accountState.positions ?? []).map((position) => this.mapPosition(position));
|
||||
const totalUnrealized = positions.reduce((acc, pos) => acc + (Number(pos.unrealizedProfit) || 0), 0);
|
||||
const assets: AsterAccountAsset[] = [
|
||||
{
|
||||
asset: balance.collateralName ?? "USDC",
|
||||
walletBalance: balance.balance ?? "0",
|
||||
availableBalance: balance.availableForTrade ?? balance.balance ?? "0",
|
||||
updateTime: balance.updatedTime ?? Date.now(),
|
||||
unrealizedProfit: balance.unrealisedPnl,
|
||||
marginBalance: balance.equity,
|
||||
maxWithdrawAmount: balance.availableForWithdrawal,
|
||||
},
|
||||
];
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
updateTime: balance.updatedTime ?? Date.now(),
|
||||
totalWalletBalance: balance.balance ?? "0",
|
||||
totalUnrealizedProfit: Number.isFinite(totalUnrealized) ? String(totalUnrealized) : "0",
|
||||
availableBalance: balance.availableForTrade ?? balance.balance ?? "0",
|
||||
maxWithdrawAmount: balance.availableForWithdrawal,
|
||||
positions,
|
||||
assets,
|
||||
};
|
||||
}
|
||||
|
||||
private mapPosition(position: ExtendedPosition): AsterAccountPosition {
|
||||
const size = toDecimal(position.size);
|
||||
const signed = position.side === "SHORT" ? size.negated() : size;
|
||||
return {
|
||||
symbol: position.market,
|
||||
positionAmt: signed.toString(10),
|
||||
entryPrice: position.openPrice ?? "0",
|
||||
unrealizedProfit: position.unrealisedPnl ?? "0",
|
||||
positionSide: position.side === "SHORT" ? "SHORT" : "LONG",
|
||||
updateTime: position.updatedAt ?? Date.now(),
|
||||
markPrice: position.markPrice,
|
||||
leverage: position.leverage,
|
||||
liquidationPrice: position.liquidationPrice,
|
||||
marginType: "cross",
|
||||
};
|
||||
}
|
||||
|
||||
private mapOrder(order: ExtendedOrder): AsterOrder {
|
||||
const stopPrice =
|
||||
order.trigger?.triggerPrice ??
|
||||
order.takeProfit?.triggerPrice ??
|
||||
order.stopLoss?.triggerPrice ??
|
||||
null;
|
||||
return {
|
||||
orderId: order.id,
|
||||
clientOrderId: order.externalId ?? String(order.id),
|
||||
symbol: order.market,
|
||||
side: order.side,
|
||||
type: this.normalizeOrderType(order.type),
|
||||
status: order.status,
|
||||
price: order.price ?? "0",
|
||||
origQty: order.qty ?? "0",
|
||||
executedQty: order.filledQty ?? "0",
|
||||
stopPrice: stopPrice ? String(stopPrice) : "0",
|
||||
time: order.createdTime ?? Date.now(),
|
||||
updateTime: order.updatedTime ?? order.createdTime ?? Date.now(),
|
||||
reduceOnly: Boolean(order.reduceOnly),
|
||||
closePosition: Boolean(order.reduceOnly),
|
||||
workingType: order.trigger?.triggerPriceType,
|
||||
activationPrice: stopPrice ? String(stopPrice) : undefined,
|
||||
avgPrice: order.averagePrice ?? undefined,
|
||||
cumQuote: order.payedFee,
|
||||
timeInForce: order.type === "MARKET" ? "IOC" : "GTC",
|
||||
};
|
||||
}
|
||||
|
||||
private mapDepth(state: DepthState): AsterDepth {
|
||||
return {
|
||||
lastUpdateId: state.seq,
|
||||
bids: state.bids,
|
||||
asks: state.asks,
|
||||
eventTime: state.ts,
|
||||
tradeTime: state.ts,
|
||||
};
|
||||
}
|
||||
|
||||
private mapKline(candle: ExtendedCandle): AsterKline {
|
||||
return {
|
||||
eventType: "kline",
|
||||
eventTime: candle.T,
|
||||
openTime: candle.T,
|
||||
closeTime: candle.T,
|
||||
interval: "stream",
|
||||
open: candle.o,
|
||||
high: candle.h,
|
||||
low: candle.l,
|
||||
close: candle.c,
|
||||
volume: candle.v ?? "0",
|
||||
numberOfTrades: 0,
|
||||
isClosed: true,
|
||||
};
|
||||
}
|
||||
|
||||
private buildTicker(): AsterTicker | null {
|
||||
const lastPrice = this.lastTrade?.p ?? this.marketInfo?.marketStats?.lastPrice;
|
||||
if (!lastPrice && !this.lastDepth) return null;
|
||||
return {
|
||||
symbol: this.options.market,
|
||||
lastPrice: lastPrice ?? "0",
|
||||
openPrice: this.marketInfo?.marketStats?.indexPrice ?? lastPrice ?? "0",
|
||||
highPrice: this.marketInfo?.marketStats?.markPrice ?? lastPrice ?? "0",
|
||||
lowPrice: this.marketInfo?.marketStats?.markPrice ?? lastPrice ?? "0",
|
||||
volume: "0",
|
||||
quoteVolume: "0",
|
||||
bidPrice: this.lastDepth?.bids?.[0]?.[0],
|
||||
askPrice: this.lastDepth?.asks?.[0]?.[0],
|
||||
eventTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
private estimateMarketPrice(side: "BUY" | "SELL", fallback?: number): number | undefined {
|
||||
const bestBid = Number(this.lastDepth?.bids?.[0]?.[0]);
|
||||
const bestAsk = Number(this.lastDepth?.asks?.[0]?.[0]);
|
||||
if (side === "BUY" && Number.isFinite(bestAsk)) return bestAsk;
|
||||
if (side === "SELL" && Number.isFinite(bestBid)) return bestBid;
|
||||
if (Number.isFinite(fallback)) return fallback;
|
||||
const last = Number(this.lastTrade?.p ?? this.marketInfo?.marketStats?.lastPrice);
|
||||
return Number.isFinite(last) ? last : undefined;
|
||||
}
|
||||
|
||||
private normalizeOrderType(type: string): AsterOrder["type"] {
|
||||
const normalized = type.toUpperCase();
|
||||
if (normalized === "LIMIT" || normalized === "MARKET") return normalized as any;
|
||||
return "STOP_MARKET";
|
||||
}
|
||||
|
||||
private logInfo(context: string, message: unknown): void {
|
||||
console.info(`[ExtendedGateway] ${context}: ${message as string}`);
|
||||
}
|
||||
|
||||
private logDebug(context: string, message: unknown): void {
|
||||
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
|
||||
console.debug(`[ExtendedGateway] ${context}: ${message as string}`);
|
||||
}
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
this.logger(context, error);
|
||||
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
|
||||
console.error(`[ExtendedGateway] ${context}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import BigNumber from "bignumber.js";
|
||||
|
||||
export const Decimal = BigNumber;
|
||||
export type Decimal = BigNumber;
|
||||
export type Long = BigNumber;
|
||||
export type RoundingMode = BigNumber.RoundingMode;
|
||||
|
||||
export function toDecimal(value: string | number | BigNumber): Decimal {
|
||||
return new Decimal(value);
|
||||
}
|
||||
|
||||
export function toLong(value: string | number | BigNumber): Long {
|
||||
return new Decimal(value);
|
||||
}
|
||||
|
||||
export function roundToStep(value: Decimal, step: Decimal, mode: RoundingMode = Decimal.ROUND_DOWN): Decimal {
|
||||
if (step.lte(0)) return value;
|
||||
return value
|
||||
.div(step)
|
||||
.decimalPlaces(0, mode)
|
||||
.times(step)
|
||||
.decimalPlaces(step.decimalPlaces());
|
||||
}
|
||||
|
||||
export function isValidNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type { CreateOrderParams, OrderType } from "../types";
|
||||
import { Decimal, roundToStep, toDecimal, toLong } from "./math";
|
||||
import { calcStarknetExpiration, generateNonce, getStarkPublicKey, getStarknetOrderMsgHash, signMessageHash, toHexString } from "./signing";
|
||||
import type { ExtendedFees, ExtendedMarket, ExtendedStarknetDomain } from "./types";
|
||||
|
||||
export interface ExtendedOrderContext {
|
||||
market: ExtendedMarket;
|
||||
fees: ExtendedFees;
|
||||
domain: ExtendedStarknetDomain;
|
||||
vaultId: string;
|
||||
starkPrivateKey: string;
|
||||
builderId?: string;
|
||||
builderFeeRate?: string;
|
||||
}
|
||||
|
||||
export interface BuiltOrderPayload {
|
||||
payload: Record<string, unknown>;
|
||||
orderId: string;
|
||||
params: CreateOrderParams;
|
||||
}
|
||||
|
||||
function resolveTimeInForce(input?: CreateOrderParams["timeInForce"]): "GTT" | "IOC" {
|
||||
if (!input) return "GTT";
|
||||
const normalized = input.toString().toUpperCase();
|
||||
if (normalized === "IOC" || normalized === "FOK") return "IOC";
|
||||
return "GTT";
|
||||
}
|
||||
|
||||
function resolveTotalFeeRate(ctx: ExtendedOrderContext, postOnly?: boolean): Decimal {
|
||||
const maker = toDecimal(ctx.fees.makerFeeRate);
|
||||
const taker = toDecimal(ctx.fees.takerFeeRate);
|
||||
const builderRate = ctx.builderFeeRate ? toDecimal(ctx.builderFeeRate) : null;
|
||||
const base = postOnly ? maker : Decimal.max(maker, taker);
|
||||
return builderRate ? base.plus(builderRate) : base;
|
||||
}
|
||||
|
||||
function resolveOrderType(input: OrderType, stopPrice?: number | undefined): { type: "LIMIT" | "MARKET" | "CONDITIONAL"; triggerPrice?: Decimal } {
|
||||
if (input === "MARKET") {
|
||||
return { type: "MARKET" };
|
||||
}
|
||||
if (input === "STOP" || input === "STOP_MARKET" || input === "TAKE_PROFIT" || input === "TAKE_PROFIT_MARKET") {
|
||||
const triggerPrice = stopPrice !== undefined ? toDecimal(stopPrice) : undefined;
|
||||
return { type: "CONDITIONAL", triggerPrice };
|
||||
}
|
||||
return { type: "LIMIT" };
|
||||
}
|
||||
|
||||
export class ExtendedOrderBuilder {
|
||||
private readonly ctx: ExtendedOrderContext;
|
||||
|
||||
constructor(context: ExtendedOrderContext) {
|
||||
this.ctx = context;
|
||||
}
|
||||
|
||||
build(params: CreateOrderParams & { marketPrice?: number; now?: number }): BuiltOrderPayload {
|
||||
const market = this.ctx.market;
|
||||
const l2 = market.l2Config;
|
||||
const trading = market.tradingConfig;
|
||||
const isPostOnly = params.timeInForce === "GTX";
|
||||
const timeInForce = resolveTimeInForce(isPostOnly ? "GTT" : params.timeInForce);
|
||||
const now = params.now ?? Date.now();
|
||||
const expiryEpochMillis = now + 60 * 60 * 1000;
|
||||
const nonce = toLong(generateNonce());
|
||||
|
||||
const minPriceChange = toDecimal(trading.minPriceChange);
|
||||
const minQtyChange = toDecimal(trading.minOrderSizeChange);
|
||||
const qty = roundToStep(toDecimal(params.quantity ?? 0), minQtyChange, Decimal.ROUND_DOWN);
|
||||
|
||||
const orderType = resolveOrderType(params.type, params.stopPrice);
|
||||
const basePrice = this.resolvePrice(params, orderType, params.marketPrice, minPriceChange);
|
||||
const price = roundToStep(basePrice, minPriceChange, params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN);
|
||||
|
||||
const totalFeeRate = resolveTotalFeeRate(this.ctx, isPostOnly);
|
||||
|
||||
const collateralAmount = qty.times(price);
|
||||
const fee = totalFeeRate.times(collateralAmount);
|
||||
|
||||
const roundingMode = params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN;
|
||||
const collateralAmountStark = collateralAmount.times(l2.collateralResolution).integerValue(roundingMode);
|
||||
const feeStark = fee.times(l2.collateralResolution).integerValue(Decimal.ROUND_UP);
|
||||
const syntheticAmountStark = qty.times(l2.syntheticResolution).integerValue(roundingMode);
|
||||
|
||||
const expiration = calcStarknetExpiration(expiryEpochMillis).toString(10);
|
||||
const starkPublicKey = toHexString(getStarkPublicKey(this.ctx.starkPrivateKey as any));
|
||||
const starknetOrderHash = getStarknetOrderMsgHash({
|
||||
positionId: toLong(this.ctx.vaultId).toString(10),
|
||||
baseAssetIdHex: toHexString(l2.syntheticId),
|
||||
baseAmount: syntheticAmountStark.toString(10),
|
||||
quoteAssetIdHex: toHexString(l2.collateralId),
|
||||
quoteAmount: collateralAmountStark.toString(10),
|
||||
feeAssetIdHex: toHexString(l2.collateralId),
|
||||
feeAmount: feeStark.toString(10),
|
||||
expiration,
|
||||
salt: nonce.toString(10),
|
||||
starkPublicKey,
|
||||
domain: this.ctx.domain,
|
||||
});
|
||||
|
||||
const signature = signMessageHash(starknetOrderHash, this.ctx.starkPrivateKey as any);
|
||||
const reduceOnly = params.reduceOnly === "true" || params.reduceOnly === true;
|
||||
const payload: Record<string, unknown> = {
|
||||
id: starknetOrderHash,
|
||||
market: market.name,
|
||||
type: orderType.type,
|
||||
side: params.side,
|
||||
qty: qty.toString(10),
|
||||
price: price.toString(10),
|
||||
timeInForce,
|
||||
expiryEpochMillis,
|
||||
fee: totalFeeRate.toString(10),
|
||||
nonce: nonce.toString(10),
|
||||
reduceOnly,
|
||||
postOnly: isPostOnly,
|
||||
settlement: {
|
||||
signature: {
|
||||
r: toHexString(signature.signature.r),
|
||||
s: toHexString(signature.signature.s),
|
||||
},
|
||||
starkKey: toHexString(signature.starkKey),
|
||||
collateralPosition: toLong(this.ctx.vaultId).toString(10),
|
||||
},
|
||||
};
|
||||
|
||||
if (orderType.type === "CONDITIONAL" && orderType.triggerPrice) {
|
||||
const direction = params.side === "BUY" ? "UP" : "DOWN";
|
||||
payload.trigger = {
|
||||
triggerPrice: orderType.triggerPrice.toString(10),
|
||||
triggerPriceType: params.triggerType ?? "LAST",
|
||||
direction,
|
||||
triggerPriceDirection: direction,
|
||||
executionPriceType: params.type === "STOP_MARKET" || params.type === "TAKE_PROFIT_MARKET" ? "MARKET" : "LIMIT",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
payload,
|
||||
orderId: starknetOrderHash,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
private resolvePrice(
|
||||
params: CreateOrderParams,
|
||||
orderType: { type: "LIMIT" | "MARKET" | "CONDITIONAL"; triggerPrice?: Decimal },
|
||||
marketPrice: number | undefined,
|
||||
minPriceChange: Decimal
|
||||
): Decimal {
|
||||
if (params.price !== undefined && params.price !== null) {
|
||||
return toDecimal(params.price);
|
||||
}
|
||||
|
||||
const fallbackPrice = Number.isFinite(marketPrice) ? marketPrice ?? NaN : NaN;
|
||||
if (orderType.type === "MARKET") {
|
||||
const basis = Number.isFinite(fallbackPrice) ? fallbackPrice! : minPriceChange.toNumber() || 1;
|
||||
const factor = params.side === "BUY" ? 1.0075 : 0.9925;
|
||||
return roundToStep(toDecimal(basis * factor), minPriceChange, params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN);
|
||||
}
|
||||
|
||||
if (orderType.type === "CONDITIONAL" && orderType.triggerPrice) {
|
||||
return orderType.triggerPrice;
|
||||
}
|
||||
|
||||
const base = Number.isFinite(fallbackPrice) ? fallbackPrice! : minPriceChange.toNumber() || 1;
|
||||
return toDecimal(base);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
price: intent.expectedPrice ?? undefined,
|
||||
timeInForce: "IOC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "STOP_MARKET",
|
||||
quantity: intent.quantity,
|
||||
stopPrice: intent.stopPrice,
|
||||
price: intent.stopPrice,
|
||||
timeInForce: intent.timeInForce ?? "GTC",
|
||||
triggerType: intent.triggerType ?? (intent.side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS"),
|
||||
closePosition: toStringBoolean(intent.closePosition ?? false),
|
||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||
throw new Error("Extended exchange does not support trailing stop orders");
|
||||
}
|
||||
|
||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
reduceOnly: "true",
|
||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||
timeInForce: "IOC",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { createRequire } from "module";
|
||||
import path from "path";
|
||||
import wasmInit, {
|
||||
get_order_msg as wasmGetOrderMsgHash,
|
||||
sign_message as wasmSignMessage,
|
||||
} from "@x10xchange/stark-crypto-wrapper-wasm";
|
||||
import { ec as starkEc, hash as starkHash, selector as starkSelector, shortString as starkShortString } from "starknet";
|
||||
import type { ExtendedStarknetDomain } from "./types";
|
||||
|
||||
export type HexString = `0x${string}`;
|
||||
|
||||
export function isHexString(value: string | undefined | null): value is HexString {
|
||||
return typeof value === "string" && value.startsWith("0x");
|
||||
}
|
||||
|
||||
export function toHexString(value: string): HexString {
|
||||
return (value.startsWith("0x") ? value : `0x${value}`) as HexString;
|
||||
}
|
||||
|
||||
export function fromHexString(value: HexString): string {
|
||||
return value.slice(2);
|
||||
}
|
||||
|
||||
export async function tryInitExtendedWasm(): Promise<void> {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const wasmDir = path.dirname(require.resolve("@x10xchange/stark-crypto-wrapper-wasm"));
|
||||
const wasmBuffer = readFileSync(path.join(wasmDir, "stark_crypto_wrapper_wasm_bg.wasm"));
|
||||
await wasmInit({ module_or_path: wasmBuffer });
|
||||
} catch (error) {
|
||||
// Revert to JS implementation if wasm init fails; do not throw to keep bot running.
|
||||
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
|
||||
console.warn("[ExtendedSigning] WASM init failed, falling back to JS:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const STARKNET_SETTLEMENT_BUFFER_SECONDS = 14 * 24 * 60 * 60;
|
||||
const MILLIS_IN_SECOND = 1000;
|
||||
|
||||
export function calcStarknetExpiration(expiryEpochMillis: number): number {
|
||||
return Math.ceil(expiryEpochMillis / MILLIS_IN_SECOND) + STARKNET_SETTLEMENT_BUFFER_SECONDS;
|
||||
}
|
||||
|
||||
export function getStarkPublicKey(privateKey: HexString): string {
|
||||
return fromHexString(starkEc.starkCurve.getStarkKey(privateKey) as HexString);
|
||||
}
|
||||
|
||||
export function signMessageHash(messageHash: string, starkPrivateKey: HexString): { signature: { r: string; s: string }; starkKey: string } {
|
||||
const starkPublicKey = getStarkPublicKey(starkPrivateKey);
|
||||
try {
|
||||
const signature = wasmSignMessage(starkPrivateKey, messageHash);
|
||||
const result = {
|
||||
signature: {
|
||||
r: fromHexString(signature.r as HexString),
|
||||
s: fromHexString(signature.s as HexString),
|
||||
},
|
||||
starkKey: starkPublicKey,
|
||||
};
|
||||
if (typeof signature.free === "function") {
|
||||
signature.free();
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
const signature = starkEc.starkCurve.sign(messageHash, starkPrivateKey);
|
||||
return {
|
||||
signature: {
|
||||
r: signature.r.toString(16),
|
||||
s: signature.s.toString(16),
|
||||
},
|
||||
starkKey: starkPublicKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function jsGetObjMsgHash(domainHash: string, publicKey: string, objHash: string): string {
|
||||
const messageFelt = starkShortString.encodeShortString("StarkNet Message");
|
||||
return starkHash.computePoseidonHashOnElements([messageFelt, domainHash, publicKey, objHash]);
|
||||
}
|
||||
|
||||
function jsGetStarknetDomainObjHash(domain: ExtendedStarknetDomain): string {
|
||||
const selector = starkSelector.getSelector(
|
||||
'"StarknetDomain"("name":"shortstring","version":"shortstring","chainId":"shortstring","revision":"shortstring")'
|
||||
);
|
||||
return starkHash.computePoseidonHashOnElements([
|
||||
selector,
|
||||
starkShortString.encodeShortString(domain.name),
|
||||
starkShortString.encodeShortString(domain.version),
|
||||
starkShortString.encodeShortString(domain.chainId),
|
||||
domain.revision,
|
||||
]);
|
||||
}
|
||||
|
||||
function jsGetOrderMsgHash(
|
||||
positionId: string,
|
||||
baseAssetIdHex: string,
|
||||
baseAmount: string,
|
||||
quoteAssetIdHex: string,
|
||||
quoteAmount: string,
|
||||
feeAssetIdHex: string,
|
||||
feeAmount: string,
|
||||
expiration: string,
|
||||
salt: string,
|
||||
userPublicKeyHex: string,
|
||||
domainName: string,
|
||||
domainVersion: string,
|
||||
domainChainId: string,
|
||||
domainRevision: string
|
||||
): string {
|
||||
const domainHash = jsGetStarknetDomainObjHash({
|
||||
name: domainName,
|
||||
version: domainVersion,
|
||||
chainId: domainChainId,
|
||||
revision: parseInt(domainRevision, 10),
|
||||
});
|
||||
|
||||
const orderSelector = starkSelector.getSelector(
|
||||
'"Order"("position_id":"felt","base_asset_id":"AssetId","base_amount":"i64","quote_asset_id":"AssetId","quote_amount":"i64","fee_asset_id":"AssetId","fee_amount":"u64","expiration":"Timestamp","salt":"felt")"PositionId"("value":"u32")"AssetId"("value":"felt")"Timestamp"("seconds":"u64")'
|
||||
);
|
||||
const orderHash = starkHash.computePoseidonHashOnElements([
|
||||
orderSelector,
|
||||
positionId,
|
||||
baseAssetIdHex,
|
||||
baseAmount,
|
||||
quoteAssetIdHex,
|
||||
quoteAmount,
|
||||
feeAssetIdHex,
|
||||
feeAmount,
|
||||
expiration,
|
||||
salt,
|
||||
]);
|
||||
|
||||
return jsGetObjMsgHash(domainHash, userPublicKeyHex, orderHash);
|
||||
}
|
||||
|
||||
export function getStarknetOrderMsgHash(args: {
|
||||
positionId: string;
|
||||
baseAssetIdHex: string;
|
||||
baseAmount: string;
|
||||
quoteAssetIdHex: string;
|
||||
quoteAmount: string;
|
||||
feeAssetIdHex: string;
|
||||
feeAmount: string;
|
||||
expiration: string;
|
||||
salt: string;
|
||||
starkPublicKey: string;
|
||||
domain: ExtendedStarknetDomain;
|
||||
}): string {
|
||||
const {
|
||||
positionId,
|
||||
baseAssetIdHex,
|
||||
baseAmount,
|
||||
quoteAssetIdHex,
|
||||
quoteAmount,
|
||||
feeAssetIdHex,
|
||||
feeAmount,
|
||||
expiration,
|
||||
salt,
|
||||
starkPublicKey,
|
||||
domain,
|
||||
} = args;
|
||||
try {
|
||||
const wasmHash = wasmGetOrderMsgHash(
|
||||
positionId,
|
||||
baseAssetIdHex,
|
||||
baseAmount,
|
||||
quoteAssetIdHex,
|
||||
quoteAmount,
|
||||
feeAssetIdHex,
|
||||
feeAmount,
|
||||
expiration,
|
||||
salt,
|
||||
starkPublicKey,
|
||||
domain.name,
|
||||
domain.version,
|
||||
domain.chainId,
|
||||
domain.revision.toString()
|
||||
);
|
||||
return fromHexString(wasmHash as HexString);
|
||||
} catch (error) {
|
||||
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
|
||||
console.warn("[ExtendedSigning] wasm order hash failed, using JS", error);
|
||||
}
|
||||
return jsGetOrderMsgHash(
|
||||
positionId,
|
||||
baseAssetIdHex,
|
||||
baseAmount,
|
||||
quoteAssetIdHex,
|
||||
quoteAmount,
|
||||
feeAssetIdHex,
|
||||
feeAmount,
|
||||
expiration,
|
||||
salt,
|
||||
starkPublicKey,
|
||||
domain.name,
|
||||
domain.version,
|
||||
domain.chainId,
|
||||
domain.revision.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateNonce(): number {
|
||||
return Math.floor(Math.random() * (2 ** 31 - 1));
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
export interface ExtendedMarket {
|
||||
name: string;
|
||||
assetName?: string;
|
||||
collateralAssetName?: string;
|
||||
marketStats?: {
|
||||
lastPrice?: string;
|
||||
askPrice?: string;
|
||||
bidPrice?: string;
|
||||
markPrice?: string;
|
||||
indexPrice?: string;
|
||||
};
|
||||
tradingConfig: {
|
||||
minOrderSize: string;
|
||||
minOrderSizeChange: string;
|
||||
minPriceChange: string;
|
||||
maxPositionValue: string;
|
||||
};
|
||||
l2Config: {
|
||||
collateralId: string;
|
||||
collateralResolution: number;
|
||||
syntheticId: string;
|
||||
syntheticResolution: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExtendedFees {
|
||||
market: string;
|
||||
makerFeeRate: string;
|
||||
takerFeeRate: string;
|
||||
builderFeeRate?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedBalance {
|
||||
collateralName?: string;
|
||||
balance: string;
|
||||
equity?: string;
|
||||
availableForTrade?: string;
|
||||
availableForWithdrawal?: string;
|
||||
unrealisedPnl?: string;
|
||||
initialMargin?: string;
|
||||
marginRatio?: string;
|
||||
updatedTime?: number;
|
||||
exposure?: string;
|
||||
leverage?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedPosition {
|
||||
id?: number | string;
|
||||
accountId?: number | string;
|
||||
market: string;
|
||||
side: "LONG" | "SHORT";
|
||||
leverage?: string;
|
||||
size: string;
|
||||
value?: string;
|
||||
openPrice?: string;
|
||||
markPrice?: string;
|
||||
liquidationPrice?: string;
|
||||
margin?: string;
|
||||
unrealisedPnl?: string;
|
||||
realisedPnl?: string;
|
||||
tpTriggerPrice?: string;
|
||||
tpLimitPrice?: string;
|
||||
slTriggerPrice?: string;
|
||||
slLimitPrice?: string;
|
||||
adl?: number;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
export interface ExtendedTrigger {
|
||||
triggerPrice?: string;
|
||||
triggerPriceType?: "LAST" | "MARK" | "INDEX";
|
||||
triggerPriceDirection?: "UP" | "DOWN";
|
||||
direction?: "UP" | "DOWN";
|
||||
executionPriceType?: "LIMIT" | "MARKET";
|
||||
}
|
||||
|
||||
export interface ExtendedOrder {
|
||||
id: string | number;
|
||||
externalId?: string;
|
||||
accountId?: string | number;
|
||||
market: string;
|
||||
type: "LIMIT" | "MARKET" | "CONDITIONAL" | "TPSL" | string;
|
||||
side: "BUY" | "SELL";
|
||||
status: string;
|
||||
price?: string;
|
||||
averagePrice?: string;
|
||||
qty: string;
|
||||
filledQty?: string;
|
||||
payedFee?: string;
|
||||
reduceOnly?: boolean;
|
||||
postOnly?: boolean;
|
||||
trigger?: ExtendedTrigger;
|
||||
takeProfit?: {
|
||||
triggerPrice?: string;
|
||||
triggerPriceType?: ExtendedTrigger["triggerPriceType"];
|
||||
price?: string;
|
||||
priceType?: "LIMIT" | "MARKET";
|
||||
};
|
||||
stopLoss?: {
|
||||
triggerPrice?: string;
|
||||
triggerPriceType?: ExtendedTrigger["triggerPriceType"];
|
||||
price?: string;
|
||||
priceType?: "LIMIT" | "MARKET";
|
||||
};
|
||||
tpSlType?: "ORDER" | "POSITION";
|
||||
createdTime: number;
|
||||
updatedTime?: number;
|
||||
expireTime?: number;
|
||||
}
|
||||
|
||||
export interface ExtendedTrade {
|
||||
m: string; // market
|
||||
S: "BUY" | "SELL";
|
||||
tT: string;
|
||||
T: number;
|
||||
p: string;
|
||||
q: string;
|
||||
i: number;
|
||||
}
|
||||
|
||||
export interface ExtendedCandle {
|
||||
T: number;
|
||||
o: string;
|
||||
h: string;
|
||||
l: string;
|
||||
c: string;
|
||||
v?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedStarknetDomain {
|
||||
name: string;
|
||||
version: string;
|
||||
chainId: string;
|
||||
revision: number;
|
||||
}
|
||||
|
||||
export interface ExtendedDepthLevel {
|
||||
p: string;
|
||||
q: string;
|
||||
}
|
||||
|
||||
export interface ExtendedDepthMessage {
|
||||
ts: number;
|
||||
type?: "SNAPSHOT" | "DELTA";
|
||||
data: {
|
||||
m: string;
|
||||
b?: ExtendedDepthLevel[];
|
||||
a?: ExtendedDepthLevel[];
|
||||
};
|
||||
seq?: number;
|
||||
}
|
||||
@@ -13,8 +13,9 @@ import * as backpackOrders from "./backpack/order";
|
||||
import * as grvtOrders from "./grvt/order";
|
||||
import * as lighterOrders from "./lighter/order";
|
||||
import * as paradexOrders from "./paradex/order";
|
||||
import * as extendedOrders from "./extended/order";
|
||||
|
||||
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex";
|
||||
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "extended";
|
||||
|
||||
interface ExchangeOrderHandlers {
|
||||
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
||||
@@ -60,9 +61,16 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||
trailingStop: paradexOrders.createTrailingStopOrder,
|
||||
close: paradexOrders.createClosePositionOrder,
|
||||
},
|
||||
extended: {
|
||||
limit: extendedOrders.createLimitOrder,
|
||||
market: extendedOrders.createMarketOrder,
|
||||
stop: extendedOrders.createStopOrder,
|
||||
trailingStop: extendedOrders.createTrailingStopOrder,
|
||||
close: extendedOrders.createClosePositionOrder,
|
||||
},
|
||||
};
|
||||
|
||||
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex"];
|
||||
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex", "extended"];
|
||||
|
||||
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||
if (!value) return undefined;
|
||||
@@ -115,4 +123,3 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
|
||||
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).close(intent);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AsterCredentials } from "./aster-adapter";
|
||||
import type { LighterCredentials } from "./lighter/adapter";
|
||||
import type { BackpackCredentials } from "./backpack/adapter";
|
||||
import type { ParadexCredentials } from "./paradex/adapter";
|
||||
import type { ExtendedCredentials } from "./extended/adapter";
|
||||
|
||||
interface BuildAdapterOptions {
|
||||
symbol: string;
|
||||
@@ -34,6 +35,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
|
||||
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
|
||||
}
|
||||
|
||||
if (id === "extended") {
|
||||
const credentials = resolveExtendedCredentials(symbol);
|
||||
return createExchangeAdapter({ exchange: id, symbol, extended: credentials });
|
||||
}
|
||||
|
||||
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
|
||||
}
|
||||
|
||||
@@ -114,6 +120,25 @@ function resolveParadexCredentials(): ParadexCredentials {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function resolveExtendedCredentials(symbol: string): ExtendedCredentials {
|
||||
const apiKey = process.env.EXTENDED_API_KEY;
|
||||
const starkPrivateKey = process.env.EXTENDED_STARK_PRIVATE_KEY;
|
||||
const vaultId = process.env.EXTENDED_VAULT_ID;
|
||||
if (!apiKey || !starkPrivateKey || !vaultId) {
|
||||
throw new Error("缺少 EXTENDED_API_KEY / EXTENDED_STARK_PRIVATE_KEY / EXTENDED_VAULT_ID 环境变量");
|
||||
}
|
||||
return {
|
||||
apiKey,
|
||||
starkPrivateKey,
|
||||
vaultId,
|
||||
market: process.env.EXTENDED_MARKET ?? symbol,
|
||||
apiHost: process.env.EXTENDED_API_HOST,
|
||||
streamHost: process.env.EXTENDED_STREAM_HOST,
|
||||
privateStreamHost: process.env.EXTENDED_PRIVATE_STREAM_HOST,
|
||||
userAgent: process.env.EXTENDED_USER_AGENT,
|
||||
};
|
||||
}
|
||||
|
||||
function isHex32(value: string): boolean {
|
||||
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user