mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732525b394 | ||
|
|
627ed36de4 |
@@ -79,6 +79,16 @@ BACKPACK_SYMBOL=BTC_USD_PERP
|
||||
# Enable verbose adapter logging: set to "1" or "true"
|
||||
BACKPACK_DEBUG=false
|
||||
|
||||
# EdgeX exchange configuration
|
||||
EDGEX_ACCOUNT_ID=
|
||||
EDGEX_PRIVATE_KEY=
|
||||
# EDGEX_POSITION_ID= # Defaults to EDGEX_ACCOUNT_ID when omitted
|
||||
# EDGEX_BASE_URL=https://pro.edgex.exchange
|
||||
# EDGEX_WS_PUBLIC_URL=wss://quote.edgex.exchange
|
||||
# EDGEX_WS_PRIVATE_URL=wss://quote.edgex.exchange
|
||||
# EDGEX_ORDER_TTL_MS=21600000 # Order expiration window (ms), default 6 hours
|
||||
|
||||
|
||||
# Paradex exchange configuration
|
||||
# Provide the EVM private key & wallet address for onboarded accounts.
|
||||
# When EXCHANGE=paradex these values are used automatically.
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
基于 Bun 的 Aster 永续合约量化终端,内置趋势跟随(SMA30)与做市策略,支持快速恢复、实时行情订阅与日志追踪。
|
||||
|
||||
* [Aster 30% 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||
* [Binance 手续费优惠注册链接](https://www.binance.com/join?ref=KNKCA9XC)
|
||||
* [GRVT 手续费优惠注册链接](https://grvt.io/exchange/sign-up?ref=sea)
|
||||
* [Backpack 手续费优惠注册链接](https://backpack.exchange/join/41d60948-2a75-4d16-b7e9-523df74f2904)
|
||||
* [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL)
|
||||
|
||||
## 文档索引
|
||||
- [English README](README_en.md)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"name": "ritmex-bot",
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@starkware-industries/starkware-crypto-utils": "^0.2.1",
|
||||
"axios": "^1.12.2",
|
||||
"ccxt": "^4.5.5",
|
||||
"dotenv": "^17.2.2",
|
||||
@@ -135,6 +136,10 @@
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
|
||||
|
||||
"@starkware-industries/starkware-crypto-utils": ["@starkware-industries/starkware-crypto-utils@0.2.1", "", { "dependencies": { "assert": "^2.0.0", "bip39": "^3.0.4", "bn.js": "^4.12.0", "brorand": "^1.1.0", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", "elliptic": "^6.5.4", "enc-utils": "^3.0.0", "ethereumjs-wallet": "^1.0.2", "hash.js": "^1.1.7", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "js-sha3": "^0.8.0", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1", "stream-browserify": "^3.0.0" } }, "sha512-rA5O9b53zaoBOQwQxBd0cbumFbQoBm9NH/vfu+o0Cq3oouEbNPALneLlLjOmFEId2/WOJ5ecC64rFLI/PwuIPQ=="],
|
||||
|
||||
"@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -149,8 +154,12 @@
|
||||
|
||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||
|
||||
"@types/pbkdf2": ["@types/pbkdf2@3.1.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
|
||||
"@types/secp256k1": ["@types/secp256k1@4.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
@@ -165,26 +174,68 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"aes-js": ["aes-js@3.1.2", "", {}, "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="],
|
||||
|
||||
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bip39": ["bip39@3.1.0", "", { "dependencies": { "@noble/hashes": "^1.2.0" } }, "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A=="],
|
||||
|
||||
"blakejs": ["blakejs@1.2.1", "", {}, "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="],
|
||||
|
||||
"bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="],
|
||||
|
||||
"brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="],
|
||||
|
||||
"browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="],
|
||||
|
||||
"browserify-cipher": ["browserify-cipher@1.0.1", "", { "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w=="],
|
||||
|
||||
"browserify-des": ["browserify-des@1.0.2", "", { "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A=="],
|
||||
|
||||
"browserify-rsa": ["browserify-rsa@4.1.1", "", { "dependencies": { "bn.js": "^5.2.1", "randombytes": "^2.1.0", "safe-buffer": "^5.2.1" } }, "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ=="],
|
||||
|
||||
"browserify-sign": ["browserify-sign@4.2.5", "", { "dependencies": { "bn.js": "^5.2.2", "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "elliptic": "^6.6.1", "inherits": "^2.0.4", "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" } }, "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw=="],
|
||||
|
||||
"bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="],
|
||||
|
||||
"bs58check": ["bs58check@2.1.2", "", { "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", "safe-buffer": "^5.1.2" } }, "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="],
|
||||
|
||||
"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": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"ccxt": ["ccxt@4.5.5", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-AyhwTFLkx4sO985ImIOfumEBox7AHD/iqk5tPGICObUSZG6wTXg0aRzU8Hjz974aCMG4msFwLk3A/iXPKAU4wA=="],
|
||||
|
||||
"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=="],
|
||||
@@ -193,6 +244,8 @@
|
||||
|
||||
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
|
||||
|
||||
"cipher-base": ["cipher-base@1.0.7", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.2" } }, "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
@@ -205,20 +258,42 @@
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="],
|
||||
|
||||
"create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="],
|
||||
|
||||
"create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="],
|
||||
|
||||
"crypto-browserify": ["crypto-browserify@3.12.1", "", { "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", "create-ecdh": "^4.0.4", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "diffie-hellman": "^5.0.3", "hash-base": "~3.0.4", "inherits": "^2.0.4", "pbkdf2": "^3.1.2", "public-encrypt": "^4.0.3", "randombytes": "^2.1.0", "randomfill": "^1.0.4" } }, "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="],
|
||||
|
||||
"diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
|
||||
|
||||
"enc-utils": ["enc-utils@3.0.0", "", { "dependencies": { "is-typedarray": "1.0.0", "typedarray-to-buffer": "3.1.5" } }, "sha512-e57t/Z2HzWOLwOp7DZcV0VMEY8t7ptWwsxyp6kM2b2zrk6JqIpXxzkruHAMiBsy5wg9jp/183GdiRXCvBtzsYg=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@@ -241,18 +316,28 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"ethereumjs-util": ["ethereumjs-util@7.1.5", "", { "dependencies": { "@types/bn.js": "^5.1.0", "bn.js": "^5.1.2", "create-hash": "^1.1.2", "ethereum-cryptography": "^0.1.3", "rlp": "^2.2.4" } }, "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg=="],
|
||||
|
||||
"ethereumjs-wallet": ["ethereumjs-wallet@1.0.2", "", { "dependencies": { "aes-js": "^3.1.2", "bs58check": "^2.1.2", "ethereum-cryptography": "^0.1.3", "ethereumjs-util": "^7.1.2", "randombytes": "^2.1.0", "scrypt-js": "^3.0.1", "utf8": "^3.0.0", "uuid": "^8.3.2" } }, "sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA=="],
|
||||
|
||||
"evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"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=="],
|
||||
@@ -261,64 +346,148 @@
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hash-base": ["hash-base@3.0.5", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg=="],
|
||||
|
||||
"hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ink": ["ink@6.3.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-3wGwITGrzL6rkWsi2gEKzgwdafGn4ZYd3u4oRp+sOPvfoxEHlnoB5Vnk9Uy5dMRUhDOqF3hqr4rLQ4lEzBc2sQ=="],
|
||||
|
||||
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
|
||||
|
||||
"is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="],
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="],
|
||||
|
||||
"miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
||||
|
||||
"minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@2.0.2", "", {}, "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="],
|
||||
|
||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||
|
||||
"object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"parse-asn1": ["parse-asn1@5.1.9", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" } }, "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
||||
|
||||
"pbkdf2": ["pbkdf2@3.1.5", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.1" } }, "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="],
|
||||
|
||||
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
||||
|
||||
"randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="],
|
||||
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="],
|
||||
|
||||
"rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"scrypt-js": ["scrypt-js@3.0.1", "", {}, "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="],
|
||||
|
||||
"secp256k1": ["secp256k1@4.0.4", "", { "dependencies": { "elliptic": "^6.5.7", "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" } }, "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
||||
|
||||
"sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
@@ -333,8 +502,12 @@
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||
@@ -351,18 +524,34 @@
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
||||
|
||||
"to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"utf8": ["utf8@3.0.0", "", {}, "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
@@ -373,8 +562,38 @@
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"browserify-rsa/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="],
|
||||
|
||||
"browserify-sign/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="],
|
||||
|
||||
"browserify-sign/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ethereumjs-util/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="],
|
||||
|
||||
"ethereumjs-util/ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="],
|
||||
|
||||
"ethereumjs-wallet/ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="],
|
||||
|
||||
"ripemd160/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="],
|
||||
|
||||
"rlp/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="],
|
||||
|
||||
"secp256k1/node-addon-api": ["node-addon-api@5.1.0", "", {}, "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="],
|
||||
|
||||
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"browserify-sign/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"ripemd160/hash-base/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"ripemd160/hash-base/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
2 Signature
|
||||
How To Sign Message
|
||||
How To GET Your L2 Private Key
|
||||
To sign messages on Layer 2, you need to obtain your L2 private key. This key is used to generate signatures that authorize various actions on the platform.
|
||||
|
||||
|
||||
How To GET Your L2 Private Key
|
||||
Warning: Keep your private key secure and never share it with anyone. Anyone with access to your private key can sign messages on your behalf.
|
||||
|
||||
Signature Algorithm
|
||||
The signature algorithm used is Ecdsa (Elliptic Curve Digital Signature Algorithm). This algorithm ensures that signatures are secure and verifiable.
|
||||
|
||||
L2Signature for Operations (e.g., Order, Transfer, Withdraw): This will use Pedersen hash for signing. However, this hash computation will consume significantly more CPU resources.
|
||||
|
||||
<!-- logo -->
|
||||
<h1 align='center'>StarkWare Crypto Utils</h1>
|
||||
|
||||
<!-- tag line -->
|
||||
<h4 align='center'> Signatures, keys and Pedersen hash on STARK friendly elliptic curve</h4>
|
||||
|
||||
<!-- primary badges -->
|
||||
<p align="center">
|
||||
<a href="https://www.w3schools.com/js/">
|
||||
<img src='https://badges.aleen42.com/src/javascript.svg' />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/@starkware-industries/starkware-crypto-utils">
|
||||
<img src='https://img.shields.io/npm/v/@starkware-industries/starkware-crypto-utils?label=npm' />
|
||||
</a>
|
||||
<a href="https://starkware.co/">
|
||||
<img src="https://img.shields.io/badge/powered_by-StarkWare-navy">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
// using npm
|
||||
npm i @starkware-industries/starkware-crypto-utils
|
||||
|
||||
// using yarn
|
||||
yarn add @starkware-industries/starkware-crypto-utils
|
||||
```
|
||||
|
||||
## How to use it
|
||||
|
||||
```js
|
||||
const starkwareCrypto = require('@starkware-industries/starkware-crypto-utils');
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```javascript
|
||||
|
||||
{
|
||||
prime,
|
||||
ec: starkEc,
|
||||
constantPoints,
|
||||
shiftPoint,
|
||||
maxEcdsaVal, // Data.
|
||||
pedersen,
|
||||
getLimitOrderMsgHash,
|
||||
getTransferMsgHash,
|
||||
sign,
|
||||
verify,
|
||||
assertInRange,
|
||||
getTransferMsgHashWithFee,
|
||||
getLimitOrderMsgHashWithFee // Function.
|
||||
|
||||
asset: {
|
||||
getAssetType,
|
||||
getAssetId // Function.
|
||||
},
|
||||
|
||||
keyDerivation: {
|
||||
StarkExEc: ec.n, // Data.
|
||||
getPrivateKeyFromEthSignature,
|
||||
privateToStarkKey,
|
||||
getKeyPairFromPath,
|
||||
getAccountPath,
|
||||
grindKey // Function.
|
||||
},
|
||||
|
||||
messageUtils: {
|
||||
assertInRange // Function.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Signing a StarkEx order
|
||||
|
||||
```javascript
|
||||
const starkwareCrypto = require('@starkware-libs/starkware-crypto-utils');
|
||||
const testData = require('test/config/signature_test_data.json');
|
||||
|
||||
const privateKey = testData.meta_data.party_a_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) ===
|
||||
testData.settlement.party_a_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.settlement.party_a_order.public_key.substring(2)}`
|
||||
);
|
||||
|
||||
const {party_a_order: partyAOrder} = testData.settlement;
|
||||
const msgHash = starkwareCrypto.getLimitOrderMsgHash(
|
||||
partyAOrder.vault_id_sell, // - vault_sell (uint31)
|
||||
partyAOrder.vault_id_buy, // - vault_buy (uint31)
|
||||
partyAOrder.amount_sell, // - amount_sell (uint63 decimal str)
|
||||
partyAOrder.amount_buy, // - amount_buy (uint63 decimal str)
|
||||
partyAOrder.token_sell, // - token_sell (hex str with 0x prefix < prime)
|
||||
partyAOrder.token_buy, // - token_buy (hex str with 0x prefix < prime)
|
||||
partyAOrder.nonce, // - nonce (uint31)
|
||||
partyAOrder.expiration_timestamp // - expiration_timestamp (uint22)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash === testData.meta_data.party_a_order.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.party_a_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
const msgSignature = starkwareCrypto.sign(keyPair, msgHash);
|
||||
const {r, s} = msgSignature;
|
||||
|
||||
assert(starkwareCrypto.verify(publicKey, msgHash, msgSignature));
|
||||
assert(
|
||||
r.toString(16) === partyAOrder.signature.r.substring(2),
|
||||
`Got: ${r.toString(16)}. Expected: ${partyAOrder.signature.r.substring(2)}`
|
||||
);
|
||||
assert(
|
||||
s.toString(16) === partyAOrder.signature.s.substring(2),
|
||||
`Got: ${s.toString(16)}. Expected: ${partyAOrder.signature.s.substring(2)}`
|
||||
);
|
||||
|
||||
// The following is the JSON representation of an order:
|
||||
console.log('Order JSON representation: ');
|
||||
console.log(partyAOrder);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### StarkEx key serialization
|
||||
|
||||
```javascript
|
||||
const starkwareCrypto = require('@starkware-libs/starkware-crypto-utils');
|
||||
|
||||
const pubXStr = publicKey.pub.getX().toString('hex');
|
||||
const pubYStr = publicKey.pub.getY().toString('hex');
|
||||
|
||||
// Verify Deserialization.
|
||||
const pubKeyDeserialized = starkwareCrypto.ec.keyFromPublic(
|
||||
{x: pubXStr, y: pubYStr},
|
||||
'hex'
|
||||
);
|
||||
assert(starkwareCrypto.verify(pubKeyDeserialized, msgHash, msgSignature));
|
||||
```
|
||||
|
||||
### Signing a StarkEx order with fee
|
||||
|
||||
```javascript
|
||||
const privateKey = testData.meta_data.party_a_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) ===
|
||||
testData.settlement.party_a_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.settlement.party_a_order.public_key.substring(2)}`
|
||||
);
|
||||
|
||||
const {party_a_order: partyAOrder} = testData.settlement;
|
||||
const feeInfo = testData.fee_info_user;
|
||||
const msgHash = starkwareCrypto.getLimitOrderMsgHashWithFee(
|
||||
partyAOrder.vault_id_sell, // - vault_sell (uint64)
|
||||
partyAOrder.vault_id_buy, // - vault_buy (uint64)
|
||||
partyAOrder.amount_sell, // - amount_sell (uint63 decimal str)
|
||||
partyAOrder.amount_buy, // - amount_buy (uint63 decimal str)
|
||||
partyAOrder.token_sell, // - token_sell (hex str with 0x prefix < prime)
|
||||
partyAOrder.token_buy, // - token_buy (hex str with 0x prefix < prime)
|
||||
partyAOrder.nonce, // - nonce (uint31)
|
||||
partyAOrder.expiration_timestamp, // - expiration_timestamp (uint22)
|
||||
feeInfo.token_id, // - token (hex str with 0x prefix < prime)
|
||||
feeInfo.source_vault_id, // - fee_source_vault_id (uint31)
|
||||
feeInfo.fee_limit // - amount (uint63 decimal str)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash ===
|
||||
testData.meta_data.party_a_order_with_fee.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.party_a_order_with_fee.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of an order:
|
||||
console.log('Order With Fee JSON representation: ');
|
||||
// Fee info is added to the order, and will be also be seen in the JSON of Settlement.
|
||||
partyAOrder.fee_info = feeInfo; // eslint-disable-line
|
||||
console.log(partyAOrder);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### StarkEx transfer
|
||||
|
||||
```javascript
|
||||
const starkwareCrypto = require('@starkware-libs/starkware-crypto-utils');
|
||||
const testData = require('test/config/signature_test_data.json');
|
||||
|
||||
const privateKey = testData.meta_data.transfer_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) === testData.transfer_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.transfer_order.public_key.substring(2)}`
|
||||
);
|
||||
|
||||
const transfer = testData.transfer_order;
|
||||
const msgHash = starkwareCrypto.getTransferMsgHash(
|
||||
transfer.amount, // - amount (uint63 decimal str)
|
||||
transfer.nonce, // - nonce (uint31)
|
||||
transfer.sender_vault_id, // - sender_vault_id (uint31)
|
||||
transfer.token, // - token (hex str with 0x prefix < prime)
|
||||
transfer.target_vault_id, // - target_vault_id (uint31)
|
||||
transfer.target_public_key, // - target_public_key (hex str with 0x prefix < prime)
|
||||
transfer.expiration_timestamp // - expiration_timestamp (uint22)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash === testData.meta_data.transfer_order.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.transfer_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a transfer:
|
||||
console.log('Transfer JSON representation: ');
|
||||
console.log(transfer);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### StarkEx conditional transfer
|
||||
|
||||
```javascript
|
||||
const starkwareCrypto = require('@starkware-libs/starkware-crypto-utils');
|
||||
const testData = require('test/config/signature_test_data.json');
|
||||
|
||||
const privateKey =
|
||||
testData.meta_data.conditional_transfer_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) ===
|
||||
testData.conditional_transfer_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.conditional_transfer_order.public_key.substring(
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
const transfer = testData.conditional_transfer_order;
|
||||
const msgHash = starkwareCrypto.getTransferMsgHash(
|
||||
transfer.amount, // - amount (uint63 decimal str)
|
||||
transfer.nonce, // - nonce (uint31)
|
||||
transfer.sender_vault_id, // - sender_vault_id (uint31)
|
||||
transfer.token, // - token (hex str with 0x prefix < prime)
|
||||
transfer.target_vault_id, // - target_vault_id (uint31)
|
||||
transfer.target_public_key, // - target_public_key (hex str with 0x prefix < prime)
|
||||
transfer.expiration_timestamp, // - expiration_timestamp (uint22)
|
||||
transfer.condition // - condition (hex str with 0x prefix < prime)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash ===
|
||||
testData.meta_data.conditional_transfer_order.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.conditional_transfer_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a transfer:
|
||||
console.log('Conditional Transfer JSON representation: ');
|
||||
console.log(transfer);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### StarkEx transfer with fee
|
||||
|
||||
```javascript
|
||||
const privateKey = testData.meta_data.transfer_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) === testData.transfer_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.transfer_order.public_key.substring(2)}`
|
||||
);
|
||||
|
||||
const transfer = testData.transfer_order;
|
||||
const feeInfo = testData.fee_info_user;
|
||||
const msgHash = starkwareCrypto.getTransferMsgHashWithFee(
|
||||
transfer.amount, // - amount (uint63 decimal str)
|
||||
transfer.nonce, // - nonce (uint31)
|
||||
transfer.sender_vault_id, // - sender_vault_id (uint64)
|
||||
transfer.token, // - token (hex str with 0x prefix < prime)
|
||||
transfer.target_vault_id, // - target_vault_id (uint64)
|
||||
transfer.target_public_key, // - target_public_key (hex str with 0x prefix < prime)
|
||||
transfer.expiration_timestamp, // - expiration_timestamp (uint22)
|
||||
feeInfo.token_id, // - token (hex str with 0x prefix < prime)
|
||||
feeInfo.source_vault_id, // - fee_source_vault_id (uint64)
|
||||
feeInfo.fee_limit // - amount (uint63 decimal str)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash ===
|
||||
testData.meta_data.transfer_order_with_fee.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.transfer_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a transfer:
|
||||
console.log('Transfer With Fee JSON representation: ');
|
||||
console.log(transfer);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### StarkEx conditional Transfer with fee
|
||||
|
||||
```javascript
|
||||
const privateKey =
|
||||
testData.meta_data.conditional_transfer_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) ===
|
||||
testData.conditional_transfer_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.conditional_transfer_order.public_key.substring(
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
const transfer = testData.conditional_transfer_order;
|
||||
const feeInfo = testData.fee_info_user;
|
||||
const msgHash = starkwareCrypto.getTransferMsgHashWithFee(
|
||||
transfer.amount, // - amount (uint63 decimal str)
|
||||
transfer.nonce, // - nonce (uint31)
|
||||
transfer.sender_vault_id, // - sender_vault_id (uint64)
|
||||
transfer.token, // - token (hex str with 0x prefix < prime)
|
||||
transfer.target_vault_id, // - target_vault_id (uint64)
|
||||
transfer.target_public_key, // - target_public_key (hex str with 0x prefix < prime)
|
||||
transfer.expiration_timestamp, // - expiration_timestamp (uint22)
|
||||
feeInfo.token_id, // - token (hex str with 0x prefix < prime)
|
||||
feeInfo.source_vault_id, // - fee_source_vault_id (uint64)
|
||||
feeInfo.fee_limit, // - amount (uint63 decimal str)
|
||||
transfer.condition // - condition (hex str with 0x prefix < prime)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash ===
|
||||
testData.meta_data.conditional_transfer_order_with_fee.message_hash.substring(
|
||||
2
|
||||
),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.conditional_transfer_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a transfer:
|
||||
console.log('Conditional Transfer With Fee JSON representation: ');
|
||||
console.log(transfer);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
### Adding a matching order to create a settlement
|
||||
|
||||
```javascript
|
||||
const starkwareCrypto = require('@starkware-libs/starkware-crypto-utils');
|
||||
const testData = require('test/config/signature_test_data.json');
|
||||
|
||||
const privateKey = testData.meta_data.party_b_order.private_key.substring(2);
|
||||
const keyPair = starkwareCrypto.ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = starkwareCrypto.ec.keyFromPublic(
|
||||
keyPair.getPublic(true, 'hex'),
|
||||
'hex'
|
||||
);
|
||||
const publicKeyX = publicKey.pub.getX();
|
||||
|
||||
assert(
|
||||
publicKeyX.toString(16) ===
|
||||
testData.settlement.party_b_order.public_key.substring(2),
|
||||
`Got: ${publicKeyX.toString(16)}.
|
||||
Expected: ${testData.settlement.party_b_order.public_key.substring(2)}`
|
||||
);
|
||||
|
||||
const {party_b_order: partyBOrder} = testData.settlement;
|
||||
const msgHash = starkwareCrypto.getLimitOrderMsgHash(
|
||||
partyBOrder.vault_id_sell, // - vault_sell (uint31)
|
||||
partyBOrder.vault_id_buy, // - vault_buy (uint31)
|
||||
partyBOrder.amount_sell, // - amount_sell (uint63 decimal str)
|
||||
partyBOrder.amount_buy, // - amount_buy (uint63 decimal str)
|
||||
partyBOrder.token_sell, // - token_sell (hex str with 0x prefix < prime)
|
||||
partyBOrder.token_buy, // - token_buy (hex str with 0x prefix < prime)
|
||||
partyBOrder.nonce, // - nonce (uint31)
|
||||
partyBOrder.expiration_timestamp // - expiration_timestamp (uint22)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash === testData.meta_data.party_b_order.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.party_b_order.message_hash.substring(2)
|
||||
);
|
||||
|
||||
const msgSignature = starkwareCrypto.sign(keyPair, msgHash);
|
||||
const {r, s} = msgSignature;
|
||||
|
||||
assert(starkwareCrypto.verify(publicKey, msgHash, msgSignature));
|
||||
assert(
|
||||
r.toString(16) === partyBOrder.signature.r.substring(2),
|
||||
`Got: ${r.toString(16)}. Expected: ${partyBOrder.signature.r.substring(2)}`
|
||||
);
|
||||
assert(
|
||||
s.toString(16) === partyBOrder.signature.s.substring(2),
|
||||
`Got: ${s.toString(16)}. Expected: ${partyBOrder.signature.s.substring(2)}`
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a settlement:
|
||||
console.log('Settlement JSON representation: ');
|
||||
console.log(testData.settlement);
|
||||
```
|
||||
|
||||
## Valid transfer with sender_vault_id=2\*\*63+10
|
||||
|
||||
```javascript
|
||||
const transfer = testData.transfer_order_2nd_valid_range;
|
||||
const feeInfo = testData.fee_info_user;
|
||||
|
||||
const msgHash = starkwareCrypto.getTransferMsgHashWithFee(
|
||||
transfer.amount, // - amount (uint63 decimal str)
|
||||
transfer.nonce, // - nonce (uint31)
|
||||
transfer.sender_vault_id, // - sender_vault_id (uint64)
|
||||
transfer.token, // - token (hex str with 0x prefix < prime)
|
||||
transfer.target_vault_id, // - target_vault_id (uint64)
|
||||
transfer.target_public_key, // - target_public_key (hex str with 0x prefix < prime)
|
||||
transfer.expiration_timestamp, // - expiration_timestamp (uint22)
|
||||
feeInfo.token_id, // - token (hex str with 0x prefix < prime)
|
||||
feeInfo.source_vault_id, // - fee_source_vault_id (uint64)
|
||||
feeInfo.fee_limit, // - amount (uint63 decimal str)
|
||||
transfer.condition // - condition (hex str with 0x prefix < prime)
|
||||
);
|
||||
|
||||
assert(
|
||||
msgHash ===
|
||||
testData.meta_data.transfer_order_2nd_valid_range.message_hash.substring(2),
|
||||
`Got: ${msgHash}. Expected: ` +
|
||||
testData.meta_data.transfer_order_2nd_valid_range.message_hash.substring(2)
|
||||
);
|
||||
|
||||
// The following is the JSON representation of a transfer with sender_vault_id in the second
|
||||
// valid range:
|
||||
console.log('Transfer JSON representation: ');
|
||||
console.log(transfer);
|
||||
console.log('\n');
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE.md)
|
||||
|
||||
Java L2Signature Demo
|
||||
Below is a Java implementation of the Ecdsa signature algorithm. This example demonstrates how to sign a message using a private key.
|
||||
|
||||
Copy
|
||||
|
||||
public static CreateOrderRequest signOrder(
|
||||
CreateOrderRequest request,
|
||||
Contract contract,
|
||||
Coin quotelCoin,
|
||||
PrivateKey privateKey) {
|
||||
BigInteger msgHash = L2SignUtil.hashLimitOrder(
|
||||
request.getSide() == OrderSide.BUY,
|
||||
BigIntUtil.toBigInt(quotelCoin.getStarkExAssetId()),
|
||||
BigIntUtil.toBigInt(contract.getStarkExSyntheticAssetId()),
|
||||
BigIntUtil.toBigInt(quotelCoin.getStarkExAssetId()),
|
||||
UnsignedLong.valueOf(new BigDecimal(request.getL2Value())
|
||||
.multiply(new BigDecimal(BigIntUtil.toBigInt(quotelCoin.getStarkExResolution())))
|
||||
.toBigIntegerExact()),
|
||||
UnsignedLong.valueOf(new BigDecimal(request.getL2Size())
|
||||
.multiply(new BigDecimal(BigIntUtil.toBigInt(contract.getStarkExResolution())))
|
||||
.toBigIntegerExact()),
|
||||
UnsignedLong.valueOf(new BigDecimal(request.getL2LimitFee())
|
||||
.multiply(new BigDecimal(BigIntUtil.toBigInt(quotelCoin.getStarkExResolution())))
|
||||
.toBigIntegerExact()),
|
||||
UnsignedLong.fromLongBits(request.getAccountId()),
|
||||
UnsignedInteger.valueOf(request.getL2Nonce()),
|
||||
UnsignedInteger.valueOf(request.getL2ExpireTime() / (60 * 60 * 1000L)));
|
||||
Signature signature = Ecdsa.sign(msgHash, privateKey);
|
||||
return request.toBuilder()
|
||||
.setL2Signature(L2Signature.newBuilder()
|
||||
.setR(BigIntUtil.toHexStr(signature.r))
|
||||
.setS(BigIntUtil.toHexStr(signature.s))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static BigInteger hashLimitOrder(
|
||||
boolean isBuyingSynthetic,
|
||||
BigInteger assetIdCollateral,
|
||||
BigInteger assetIdSynthetic,
|
||||
BigInteger assetIdFee,
|
||||
UnsignedLong amountCollateral,
|
||||
UnsignedLong amountSynthetic,
|
||||
UnsignedLong maxAmountFee,
|
||||
UnsignedLong positionId,
|
||||
UnsignedInteger nonce,
|
||||
UnsignedInteger expirationTimestamp) {
|
||||
BigInteger assetIdSell;
|
||||
BigInteger assetIdBuy;
|
||||
UnsignedLong amountSell;
|
||||
UnsignedLong amountBuy;
|
||||
if (isBuyingSynthetic) {
|
||||
assetIdSell = assetIdCollateral;
|
||||
assetIdBuy = assetIdSynthetic;
|
||||
amountSell = amountCollateral;
|
||||
amountBuy = amountSynthetic;
|
||||
} else {
|
||||
assetIdSell = assetIdSynthetic;
|
||||
assetIdBuy = assetIdCollateral;
|
||||
amountSell = amountSynthetic;
|
||||
amountBuy = amountCollateral;
|
||||
}
|
||||
BigInteger packedMessage0 = amountSell.bigIntegerValue();
|
||||
packedMessage0 = packedMessage0.shiftLeft(64).add(amountBuy.bigIntegerValue());
|
||||
packedMessage0 = packedMessage0.shiftLeft(64).add(maxAmountFee.bigIntegerValue());
|
||||
packedMessage0 = packedMessage0.shiftLeft(32).add(nonce.bigIntegerValue());
|
||||
|
||||
BigInteger packedMessage1 = BigInteger.valueOf(3);
|
||||
packedMessage1 = packedMessage1.shiftLeft(64).add(positionId.bigIntegerValue());
|
||||
packedMessage1 = packedMessage1.shiftLeft(64).add(positionId.bigIntegerValue());
|
||||
packedMessage1 = packedMessage1.shiftLeft(64).add(positionId.bigIntegerValue());
|
||||
packedMessage1 = packedMessage1.shiftLeft(32).add(expirationTimestamp.bigIntegerValue());
|
||||
packedMessage1 = packedMessage1.shiftLeft(17);
|
||||
|
||||
BigInteger msg = pedersenHash(assetIdSell, assetIdBuy);
|
||||
msg = pedersenHash(msg, assetIdFee);
|
||||
msg = pedersenHash(msg, packedMessage0);
|
||||
msg = pedersenHash(msg, packedMessage1);
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static BigInteger pedersenHash(BigInteger... input) {
|
||||
BigInteger[][] points = PEDERSEN_POINTS;
|
||||
Point shiftPoint = new Point(points[0][0], points[0][1]);
|
||||
for (int i = 0; i < input.length; i++) {
|
||||
BigInteger x = input[i];
|
||||
for (int j = 0; j < 252; j++) {
|
||||
int pos = 2 + i * 252 + j;
|
||||
Point pt = new Point(points[pos][0], points[pos][1]);
|
||||
if (x.and(BigInteger.ONE).intValue() != 0) {
|
||||
shiftPoint = EcMath.add(shiftPoint, pt, Curve.secp256k1.A, Curve.secp256k1.P);
|
||||
}
|
||||
x = x.shiftRight(1);
|
||||
}
|
||||
}
|
||||
return shiftPoint.x;
|
||||
}
|
||||
|
||||
public static Signature sign(BigInteger msgHash, PrivateKey privateKey) {
|
||||
Curve curve = privateKey.curve;
|
||||
BigInteger randNum = new BigInteger(curve.N.toByteArray().length * 8 - 1, new SecureRandom()).abs().add(BigInteger.ONE);
|
||||
Point randomSignPoint = EcMath.multiply(curve.G, randNum, curve.N, curve.A, curve.P);
|
||||
BigInteger r = randomSignPoint.x.mod(curve.N);
|
||||
BigInteger s = ((msgHash.add(r.multiply(privateKey.secret))).multiply(EcMath.inv(randNum, curve.N))).mod(curve.N);
|
||||
return Signature.create(r, s);
|
||||
}
|
||||
Signature Construction Guide
|
||||
This section provides detailed instructions on constructing signatures for various actions on the platform.
|
||||
|
||||
Withdrawal Signature
|
||||
Used to authorize withdrawing assets from Layer 2 to an Ethereum address.
|
||||
|
||||
Parameters
|
||||
assetIdCollateral - Asset ID for the collateral token from meta_data.coinList.starkExAssetId
|
||||
|
||||
positionId - User's account ID in Layer 2
|
||||
|
||||
ethAddress - Destination Ethereum address for withdrawal
|
||||
|
||||
nonce - Unique transaction identifier to prevent replay attacks
|
||||
|
||||
expirationTimestamp - Unix timestamp when signature expires
|
||||
|
||||
amount - Amount to withdraw in base units
|
||||
|
||||
Calculation
|
||||
The following TypeScript function constructs the withdrawal message for signing:
|
||||
|
||||
Copy
|
||||
// Construct withdrawal message for signing
|
||||
function getWithdrawalToAddressMsg({
|
||||
assetIdCollateral,
|
||||
positionId,
|
||||
ethAddress,
|
||||
nonce,
|
||||
expirationTimestamp,
|
||||
amount
|
||||
}) {
|
||||
// Pack parameters into 256-bit words
|
||||
const w1 = assetIdCollateral;
|
||||
let w5 = BigInt(withdrawalToAddress); // Constant identifier
|
||||
w5 = (w5 << 64) + BigInt(positionId);
|
||||
w5 = (w5 << 32) + BigInt(nonce);
|
||||
w5 = (w5 << 64) + BigInt(amount);
|
||||
w5 = (w5 << 32) + BigInt(expirationTimestamp);
|
||||
w5 = w5 << 49;
|
||||
|
||||
// Calculate Pedersen hash
|
||||
return pedersen([
|
||||
pedersen([w1, ethAddress]),
|
||||
w5.toString(16)
|
||||
]);
|
||||
}
|
||||
Limit Order Signature
|
||||
Used to authorize a limit order for perpetual trading.
|
||||
|
||||
Parameters
|
||||
assetIdSynthetic - Synthetic asset ID from meta_data.contractList.starkExSyntheticAssetId
|
||||
|
||||
assetIdCollateral - Collateral asset ID from meta_data.coinList.starkExAssetId
|
||||
|
||||
isBuyingSynthetic - true for buy orders, false for sell orders
|
||||
|
||||
assetIdFee - Fee token asset ID from meta_data.coinList.starkExAssetId
|
||||
|
||||
amountSynthetic - Amount of synthetic asset
|
||||
|
||||
amountCollateral - Amount of collateral asset
|
||||
|
||||
maxAmountFee - Maximum fee amount allowed
|
||||
|
||||
nonce - Unique order identifier
|
||||
|
||||
positionId - User's position ID
|
||||
|
||||
expirationTimestamp - Unix timestamp when order expires
|
||||
|
||||
Calculation
|
||||
The following TypeScript function constructs the limit order message for signing:
|
||||
|
||||
Copy
|
||||
function getLimitOrderMsg({
|
||||
assetIdSynthetic,
|
||||
assetIdCollateral,
|
||||
isBuyingSynthetic,
|
||||
assetIdFee,
|
||||
amountSynthetic,
|
||||
amountCollateral,
|
||||
maxAmountFee,
|
||||
nonce,
|
||||
positionId,
|
||||
expirationTimestamp
|
||||
}) {
|
||||
// Determine sell/buy assets based on order side
|
||||
const [assetIdSell, assetIdBuy] = isBuyingSynthetic
|
||||
? [assetIdCollateral, assetIdSynthetic]
|
||||
: [assetIdSynthetic, assetIdCollateral];
|
||||
const [amountSell, amountBuy] = isBuyingSynthetic
|
||||
? [amountCollateral, amountSynthetic]
|
||||
: [amountSynthetic, amountCollateral];
|
||||
|
||||
// Pack order data into 256-bit words
|
||||
const w1 = assetIdSell;
|
||||
const w2 = assetIdBuy;
|
||||
const w3 = assetIdFee;
|
||||
|
||||
// Calculate message hash
|
||||
let msg = pedersen([w1, w2]);
|
||||
msg = pedersen([msg, w3]);
|
||||
|
||||
let w4 = BigInt(amountSell);
|
||||
w4 = (w4 << 64) + BigInt(amountBuy);
|
||||
w4 = (w4 << 64) + BigInt(maxAmountFee);
|
||||
w4 = (w4 << 32) + BigInt(nonce);
|
||||
msg = pedersen([msg, w4.toString(16)]);
|
||||
|
||||
let w5 = BigInt(limitOrderWithFees); // Constant identifier
|
||||
w5 = (w5 << 64) + BigInt(positionId);
|
||||
w5 = (w5 << 64) + BigInt(positionId);
|
||||
w5 = (w5 << 64) + BigInt(positionId);
|
||||
w5 = (w5 << 32) + BigInt(expirationTimestamp);
|
||||
w5 = w5 << 17;
|
||||
|
||||
return pedersen([msg, w5.toString(16)]);
|
||||
}
|
||||
Transfer Signature
|
||||
Used to authorize transfers between Layer 2 accounts.
|
||||
|
||||
Parameters
|
||||
assetId - Asset ID being transferred
|
||||
|
||||
receiverPublicKey - Recipient's public key
|
||||
|
||||
senderPositionId - Sender's position ID
|
||||
|
||||
receiverPositionId - Recipient's position ID
|
||||
|
||||
srcFeePositionId - Fee source position ID
|
||||
|
||||
nonce - Unique transfer identifier
|
||||
|
||||
amount - Transfer amount
|
||||
|
||||
expirationTimestamp - Unix timestamp when transfer expires
|
||||
|
||||
assetIdFee - Fee token asset ID (optional, default '0')
|
||||
|
||||
maxAmountFee - Maximum fee amount (optional, default '0')
|
||||
|
||||
Calculation
|
||||
The following TypeScript function constructs the transfer message for signing:
|
||||
|
||||
Copy
|
||||
function getTransferMsg({
|
||||
assetId,
|
||||
receiverPublicKey,
|
||||
senderPositionId,
|
||||
receiverPositionId,
|
||||
srcFeePositionId,
|
||||
nonce,
|
||||
amount,
|
||||
expirationTimestamp,
|
||||
assetIdFee = '0',
|
||||
maxAmountFee = '0'
|
||||
}) {
|
||||
// Pack transfer data into 256-bit words
|
||||
const w1 = assetId;
|
||||
const w2 = assetIdFee;
|
||||
const w3 = receiverPublicKey;
|
||||
|
||||
let w4 = BigInt(senderPositionId);
|
||||
w4 = (w4 << 64) + BigInt(receiverPositionId);
|
||||
w4 = (w4 << 64) + BigInt(srcFeePositionId);
|
||||
w4 = (w4 << 32) + BigInt(nonce);
|
||||
|
||||
let w5 = BigInt(transfer); // Constant identifier
|
||||
w5 = (w5 << 64) + BigInt(amount);
|
||||
w5 = (w5 << 64) + BigInt(maxAmountFee);
|
||||
w5 = (w5 << 32) + BigInt(expirationTimestamp);
|
||||
w5 = w5 << 81;
|
||||
|
||||
// Calculate message hash
|
||||
let msg = pedersen([w1, w2]);
|
||||
msg = pedersen([msg, w3]);
|
||||
msg = pedersen([msg, w4.toString(16)]);
|
||||
return pedersen([msg, w5.toString(16)]);
|
||||
}
|
||||
For more details on the signature construction, see the StarkEx documentation.
|
||||
@@ -0,0 +1,172 @@
|
||||
Authentication
|
||||
Authentication is crucial for ensuring that only authorized users can access private APIs. This document outlines the authentication mechanisms used for public and private APIs.
|
||||
|
||||
Public API
|
||||
Public APIs do not require authentication. These interfaces are accessible to anyone without the need for any credentials.
|
||||
|
||||
Copy
|
||||
No authentication is required for public interfaces.
|
||||
Private API
|
||||
Private APIs require authentication to ensure that only authorized users can access them. Authentication is achieved using custom headers that include a timestamp and a signature.
|
||||
|
||||
Auth Header
|
||||
The following headers must be included in the request to authenticate access to private APIs:
|
||||
|
||||
Name
|
||||
Location
|
||||
Type
|
||||
Required
|
||||
Description
|
||||
X-edgeX-Api-Timestamp
|
||||
|
||||
header
|
||||
|
||||
string
|
||||
|
||||
must
|
||||
|
||||
The timestamp when the request was made. This helps prevent replay attacks.
|
||||
|
||||
X-edgeX-Api-Signature
|
||||
|
||||
header
|
||||
|
||||
string
|
||||
|
||||
must
|
||||
|
||||
The signature generated using the private key and request details.
|
||||
|
||||
CURL Examble
|
||||
Copy
|
||||
curl --location --request GET 'https://pro.edgex.exchange/api/v1/private/account/getPositionTransactionPage?filterTypeList=SETTLE_FUNDING_FEE&size=10&accountId=544159487963955214' \
|
||||
--header 'X-edgeX-Api-Signature: 06d28020763542c0afc296dc8743797c6fda8ea9727745b57b671f70326dfed6077cd******************************aff3162e39d05d9df1c3ddf9648650382d6e62ff1076b14c0e6c687088d3917d8490e5412a080a6e9ea940c720ddd' \
|
||||
--header 'X-edgeX-Api-Timestamp: 1736313025024'
|
||||
Signature Elements
|
||||
The signature is generated using the following elements:
|
||||
|
||||
Signature Element
|
||||
|
||||
Description
|
||||
|
||||
X-edgeX-Api-Timestamp
|
||||
|
||||
The timestamp when the request was made. This is retrieved from the request header.
|
||||
|
||||
Request Method (Uppercase)
|
||||
|
||||
The HTTP method of the request, converted to uppercase (e.g., GET, POST).
|
||||
|
||||
Request Path
|
||||
|
||||
The URI path of the request (e.g., /api/v1/resource).
|
||||
|
||||
Request Parameter/Body
|
||||
|
||||
The query parameters or request body, sorted alphabetically.
|
||||
|
||||
Request Parameter To Signature Content
|
||||
The request parameters are concatenated into a single string that forms the signature content. This string includes the timestamp, HTTP method, request path, and sorted query parameters or request body, ensuring the integrity and authenticity of the request.
|
||||
|
||||
For example, the following request parameters are concatenated into a single string:
|
||||
|
||||
1735542383256GET/api/v1/private/account/getPositionTransactionPageaccountId=543429922991899150&filterTypeList=SETTLE_FUNDING_FEE&size=10
|
||||
|
||||
Generate Signature Java Example
|
||||
Below is a Java implementation of the Ecdsa signature algorithm. This example demonstrates how to sign a message using a private key.
|
||||
|
||||
Private API Auth Signature: This is used for authentication. We do not want the hash computation to consume excessive CPU resources. Therefore, this will use SHA3 to hash the request body string before signing.
|
||||
|
||||
Copy
|
||||
import java.math.BigInteger;
|
||||
import org.web3j.abi.TypeEncoder;
|
||||
import org.web3j.abi.datatypes.Utf8String;
|
||||
import org.web3j.abi.datatypes.generated.Uint256;
|
||||
import org.web3j.crypto.Hash;
|
||||
import org.web3j.utils.Numeric;
|
||||
|
||||
public class EcdsaSignatureDemo {
|
||||
public static final BigInteger K_MODULUS = Numeric
|
||||
.toBigInt("0x0800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f");
|
||||
|
||||
public static void main(String[] args) {
|
||||
String privateKeyHex = "0463ac809cc7d7c1baf*********************baff9fc6e3d8e5b160ea3fc";
|
||||
|
||||
// Ensure that the private key is a hexadecimal string without the "0x" prefix.
|
||||
if (privateKeyHex.startsWith("0x")) {
|
||||
privateKeyHex = privateKeyHex.substring(2);
|
||||
}
|
||||
|
||||
BigInteger mySecretKey = new BigInteger(privateKeyHex, 16);
|
||||
PrivateKey privateKey = PrivateKey.create(mySecretKey);
|
||||
|
||||
String message = "1735542383256GET/api/v1/private/account/getPositionTransactionPageaccountId=543429922991899150&filterTypeList=SETTLE_FUNDING_FEE&size=10";
|
||||
String msg = TypeEncoder.encodePacked(new Utf8String(message));
|
||||
|
||||
BigInteger msgHash = Numeric.toBigInt(Hash.sha3(Numeric.hexStringToByteArray(msg)));
|
||||
|
||||
msgHash = msgHash.mod(K_MODULUS);
|
||||
|
||||
Signature signature = Ecdsa.sign(msgHash, privateKey);
|
||||
|
||||
String starkSignature = TypeEncoder.encodePacked(new Uint256(signature.r)) +
|
||||
TypeEncoder.encodePacked(new Uint256(signature.s)) +
|
||||
TypeEncoder.encodePacked(new Uint256(privateKey.publicKey().point.y));
|
||||
|
||||
System.out.println(starkSignature);
|
||||
}
|
||||
|
||||
public static Signature sign(BigInteger msgHash, PrivateKey privateKey) {
|
||||
Curve curve = privateKey.curve;
|
||||
BigInteger randNum = new BigInteger(curve.N.toByteArray().length * 8 - 1, new SecureRandom()).abs().add(BigInteger.ONE);
|
||||
Point randomSignPoint = EcMath.multiply(curve.G, randNum, curve.N, curve.A, curve.P);
|
||||
BigInteger r = randomSignPoint.x.mod(curve.N);
|
||||
BigInteger s = ((msgHash.add(r.multiply(privateKey.secret))).multiply(EcMath.inv(randNum, curve.N))).mod(curve.N);
|
||||
return Signature.create(r, s);
|
||||
}
|
||||
}
|
||||
Request Body To Body String Code Example
|
||||
The following Java code example demonstrates how to convert a JSON request body into a sorted string format suitable for signature generation:
|
||||
|
||||
Copy
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RequestBodyToString {
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
private static String getValue(JsonElement valueJson) {
|
||||
if (valueJson.isJsonNull()) {
|
||||
return EMPTY_STRING;
|
||||
} else if (valueJson.isJsonPrimitive()) {
|
||||
return valueJson.getAsString();
|
||||
} else if (valueJson.isJsonArray()) {
|
||||
JsonArray valueArray = valueJson.getAsJsonArray();
|
||||
if (valueArray.isEmpty()) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
List<String> values = new ArrayList<>();
|
||||
for (JsonElement itemValue : valueArray) {
|
||||
values.add(getValue(itemValue));
|
||||
}
|
||||
return String.join("&", values);
|
||||
} else if (valueJson.isJsonObject()) {
|
||||
TreeMap<String, String> sortedDataMap = new TreeMap<>();
|
||||
JsonObject valueJsonObj = valueJson.getAsJsonObject();
|
||||
for (String key : valueJsonObj.keySet()) {
|
||||
sortedDataMap.put(key, getValue(valueJsonObj.get(key)));
|
||||
}
|
||||
return sortedDataMap.keySet().stream()
|
||||
.map(key -> key + "=" + sortedDataMap.get(key))
|
||||
.collect(Collectors.joining("&"));
|
||||
}
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
}
|
||||
Signature Algorithm
|
||||
The signature algorithm used is Ecdsa (Elliptic Curve Digital Signature Algorithm).
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Check package
|
||||
run: twine check dist/*
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
run: |
|
||||
twine upload --repository testpypi dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
twine upload dist/*
|
||||
@@ -0,0 +1,31 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Python bytecode files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,516 @@
|
||||
# EdgeX Python SDK
|
||||
|
||||
A Python SDK for interacting with the EdgeX Exchange API. This SDK provides a comprehensive interface to the EdgeX API, allowing you to easily integrate EdgeX functionality into your Python applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Complete API Coverage**: Access all EdgeX API endpoints
|
||||
- **WebSocket Support**: Real-time data streaming
|
||||
- **Async/Await**: Modern Python async interface
|
||||
- **Type Hints**: Comprehensive type annotations for better IDE support
|
||||
- **Error Handling**: Proper error handling and validation
|
||||
- **Pagination**: Support for paginated API endpoints
|
||||
- **Authentication**: Automatic request signing
|
||||
|
||||
## Installation
|
||||
|
||||
### From PyPI
|
||||
|
||||
```bash
|
||||
pip install edgex-python-sdk
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/edgex-Tech/edgex-python-sdk.git
|
||||
cd edgex-python-sdk
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Using Requirements Files
|
||||
|
||||
For production use:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
For development (includes testing and linting tools):
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
### Virtual Environment (Recommended)
|
||||
|
||||
It's recommended to use a virtual environment:
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or install in development mode
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
from edgex_sdk import Client, OrderSide
|
||||
|
||||
async def main():
|
||||
# Create a new client
|
||||
client = Client(
|
||||
base_url="https://pro.edgex.exchange", # Use https://testnet.edgex.exchange for testnet
|
||||
account_id=12345, # Your account ID
|
||||
stark_private_key="your-stark-private-key" # Your private key
|
||||
)
|
||||
|
||||
# Get server time
|
||||
server_time = await client.get_server_time()
|
||||
print(f"Server Time: {server_time}")
|
||||
|
||||
# Get exchange metadata
|
||||
metadata = await client.get_metadata()
|
||||
print(f"Available contracts: {len(metadata.get('data', {}).get('contractList', []))}")
|
||||
|
||||
# Get account assets
|
||||
assets = await client.get_account_asset()
|
||||
print(f"Account Assets: {assets}")
|
||||
|
||||
# Get account positions
|
||||
positions = await client.get_account_positions()
|
||||
print(f"Account Positions: {positions}")
|
||||
|
||||
# Get 24-hour market data for BNB2USDT (contract ID: 10000004)
|
||||
quote = await client.get_24_hour_quote("10000004")
|
||||
print(f"BNB2USDT Price: {quote}")
|
||||
|
||||
# Create a limit order (uncomment to place real order)
|
||||
# order = await client.create_limit_order(
|
||||
# contract_id="10000004", # BNB2USDT
|
||||
# size="0.01",
|
||||
# price="600.00",
|
||||
# side=OrderSide.BUY
|
||||
# )
|
||||
# print(f"Order created: {order}")
|
||||
|
||||
# Run the async function
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The SDK is organized into modules that correspond to the EdgeX API structure:
|
||||
|
||||
```
|
||||
edgex_sdk/
|
||||
├── __init__.py
|
||||
├── client.py # Main client
|
||||
├── account/ # Account API
|
||||
├── asset/ # Asset API
|
||||
├── funding/ # Funding API
|
||||
├── internal/ # Internal utilities
|
||||
├── metadata/ # Metadata API
|
||||
├── order/ # Order API
|
||||
├── quote/ # Quote API
|
||||
├── transfer/ # Transfer API
|
||||
└── ws/ # WebSocket API
|
||||
```
|
||||
|
||||
## Available APIs
|
||||
|
||||
The SDK currently supports the following API modules:
|
||||
|
||||
- **Account API**: Manage account positions, retrieve position transactions, and handle collateral transactions
|
||||
- Get account positions
|
||||
- Get position by contract ID
|
||||
- Get position transaction history
|
||||
- Get collateral transaction details
|
||||
- Update leverage settings
|
||||
|
||||
- **Asset API**: Handle asset management and withdrawals
|
||||
- Get asset orders with pagination
|
||||
- Get coin rates
|
||||
- Manage withdrawals (normal, cross-chain, and fast)
|
||||
- Get withdrawal records and sign information
|
||||
- Check withdrawable amounts
|
||||
|
||||
- **Funding API**: Manage funding operations and account balance
|
||||
- Handle funding transactions
|
||||
- Manage funding accounts
|
||||
- Get funding transaction history
|
||||
|
||||
- **Metadata API**: Access exchange system information
|
||||
- Get server time
|
||||
- Get exchange metadata (trading pairs, contracts, etc.)
|
||||
|
||||
- **Order API**: Comprehensive order management
|
||||
- Create and cancel orders
|
||||
- Get active orders
|
||||
- Get order fill transactions
|
||||
- Calculate maximum order sizes
|
||||
- Manage order history
|
||||
|
||||
- **Quote API**: Access market data and pricing
|
||||
- Get multi-contract K-line data
|
||||
- Get order book depth
|
||||
- Access real-time market quotes
|
||||
- Get 24-hour ticker data
|
||||
|
||||
- **Transfer API**: Handle asset transfers
|
||||
- Create transfer out orders
|
||||
- Get transfer records (in/out)
|
||||
- Check available withdrawal amounts
|
||||
- Manage transfer history
|
||||
|
||||
- **WebSocket API**: Real-time data streaming
|
||||
- Market data (tickers, K-lines, order book, trades)
|
||||
- Account updates
|
||||
- Order updates
|
||||
- Position updates
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
The SDK provides a WebSocket manager for handling real-time data:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from edgex_sdk import WebSocketManager
|
||||
|
||||
async def main():
|
||||
# Create a WebSocket manager
|
||||
ws_manager = WebSocketManager(
|
||||
base_url="wss://quote.edgex.exchange", # Use wss://quote-testnet.edgex.exchange for testnet
|
||||
account_id=12345,
|
||||
stark_pri_key="your-stark-private-key"
|
||||
)
|
||||
|
||||
# Define message handlers
|
||||
def ticker_handler(message):
|
||||
print(f"Ticker Update: {message}")
|
||||
|
||||
def kline_handler(message):
|
||||
print(f"K-line Update: {message}")
|
||||
|
||||
# Connect to public WebSocket for market data
|
||||
ws_manager.connect_public()
|
||||
|
||||
# Subscribe to real-time updates for BNB2USDT (contract ID: 10000004)
|
||||
ws_manager.subscribe_ticker("10000004", ticker_handler)
|
||||
ws_manager.subscribe_kline("10000004", "1m", kline_handler)
|
||||
|
||||
# Connect to private WebSocket for account updates
|
||||
ws_manager.connect_private()
|
||||
|
||||
# Wait for updates
|
||||
await asyncio.sleep(30)
|
||||
|
||||
# Disconnect all connections
|
||||
ws_manager.disconnect_all()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Signing Adapters
|
||||
|
||||
The SDK provides a flexible signing mechanism through signing adapters. **StarkExSigningAdapter is used by default**, so you don't need to explicitly create one:
|
||||
|
||||
```python
|
||||
from edgex_sdk import Client
|
||||
|
||||
# Create a client (uses StarkExSigningAdapter by default)
|
||||
client = Client(
|
||||
base_url="https://pro.edgex.exchange", # Use https://testnet.edgex.exchange for testnet
|
||||
account_id=12345,
|
||||
stark_private_key="your-stark-private-key"
|
||||
)
|
||||
```
|
||||
|
||||
If you need to use a custom signing adapter, you can still provide one:
|
||||
|
||||
```python
|
||||
from edgex_sdk import Client, StarkExSigningAdapter
|
||||
|
||||
# Create a custom signing adapter (optional)
|
||||
signing_adapter = StarkExSigningAdapter()
|
||||
|
||||
# Create a client with a custom signing adapter
|
||||
client = Client(
|
||||
base_url="https://pro.edgex.exchange", # Use https://testnet.edgex.exchange for testnet
|
||||
account_id=12345,
|
||||
stark_private_key="your-stark-private-key",
|
||||
signing_adapter=signing_adapter
|
||||
)
|
||||
```
|
||||
|
||||
The SDK includes the following signing adapters:
|
||||
|
||||
- **StarkExSigningAdapter** (default): Full implementation using StarkWare cryptographic operations for production use
|
||||
|
||||
You can also create your own signing adapter by implementing the `SigningAdapter` interface if you need custom cryptographic operations.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The SDK provides proper error handling for API requests:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from edgex_sdk import Client, OrderSide
|
||||
|
||||
async def main():
|
||||
client = Client(
|
||||
base_url="https://pro.edgex.exchange", # Use https://testnet.edgex.exchange for testnet
|
||||
account_id=12345,
|
||||
stark_private_key="your-stark-private-key"
|
||||
)
|
||||
|
||||
try:
|
||||
# Create a limit order for BNB2USDT
|
||||
order = await client.create_limit_order(
|
||||
contract_id="10000004", # BNB2USDT
|
||||
size="0.01",
|
||||
price="600.00",
|
||||
side=OrderSide.BUY
|
||||
)
|
||||
print(f"Order created: {order}")
|
||||
|
||||
# Cancel the order
|
||||
from edgex_sdk import CancelOrderParams
|
||||
cancel_params = CancelOrderParams(
|
||||
order_id=order.get("data", {}).get("orderId")
|
||||
)
|
||||
cancel_result = await client.cancel_order(cancel_params)
|
||||
print(f"Order cancelled: {cancel_result}")
|
||||
|
||||
except ValueError as e:
|
||||
print(f"Failed to create/cancel order: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {str(e)}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
Many API endpoints support pagination:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from edgex_sdk import Client, GetActiveOrderParams
|
||||
|
||||
async def main():
|
||||
client = Client(
|
||||
base_url="https://pro.edgex.exchange", # Use https://testnet.edgex.exchange for testnet
|
||||
account_id=12345,
|
||||
stark_private_key="your-stark-private-key"
|
||||
)
|
||||
|
||||
# Create pagination parameters
|
||||
params = GetActiveOrderParams(
|
||||
size="10",
|
||||
offset_data=""
|
||||
)
|
||||
|
||||
# Get active orders
|
||||
orders = await client.get_active_orders(params)
|
||||
print(f"Active orders: {orders}")
|
||||
|
||||
# Get next page if available
|
||||
if orders.get("data", {}).get("hasNext"):
|
||||
params.offset_data = orders.get("data", {}).get("offsetData")
|
||||
next_page = await client.get_active_orders(params)
|
||||
print(f"Next page: {next_page}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## API Examples
|
||||
|
||||
### Market Data
|
||||
|
||||
```python
|
||||
from edgex_sdk import Client, GetKLineParams, GetOrderBookDepthParams
|
||||
|
||||
# Get 24-hour market quotes for BNB2USDT (contract ID: 10000004)
|
||||
quote = await client.get_24_hour_quote("10000004")
|
||||
print(f"Current price: {quote}")
|
||||
|
||||
# Get K-line data for BTCUSDT (contract ID: 10000001)
|
||||
kline_params = GetKLineParams(
|
||||
contract_id="10000001", # BTCUSDT
|
||||
interval="1m",
|
||||
size="10"
|
||||
)
|
||||
klines = await client.quote.get_k_line(kline_params)
|
||||
print(f"K-lines: {klines}")
|
||||
|
||||
# Get order book depth for ETHUSDT (contract ID: 10000002)
|
||||
depth_params = GetOrderBookDepthParams(
|
||||
contract_id="10000002", # ETHUSDT
|
||||
limit=10
|
||||
)
|
||||
depth = await client.quote.get_order_book_depth(depth_params)
|
||||
print(f"Order book: {depth}")
|
||||
```
|
||||
|
||||
### Account Management
|
||||
|
||||
```python
|
||||
# Get account assets
|
||||
assets = await client.get_account_asset()
|
||||
print(f"Account assets: {assets}")
|
||||
|
||||
# Get account positions
|
||||
positions = await client.get_account_positions()
|
||||
print(f"Positions: {positions}")
|
||||
|
||||
# Get position transactions
|
||||
from edgex_sdk import GetPositionTransactionPageParams
|
||||
tx_params = GetPositionTransactionPageParams(
|
||||
size="10",
|
||||
offset_data=""
|
||||
)
|
||||
transactions = await client.account.get_position_transaction_page(tx_params)
|
||||
print(f"Transactions: {transactions}")
|
||||
```
|
||||
|
||||
### Order Management
|
||||
|
||||
```python
|
||||
from edgex_sdk import OrderSide, CreateOrderParams, CancelOrderParams
|
||||
|
||||
# Create a limit order for BNBUSDT
|
||||
order = await client.create_limit_order(
|
||||
contract_id="10000004", # BNBUSDT
|
||||
size="0.01",
|
||||
price="600.00",
|
||||
side=OrderSide.BUY
|
||||
)
|
||||
print(f"Order created: {order}")
|
||||
|
||||
# Get maximum order size for BNBUSDT
|
||||
max_size = await client.get_max_order_size("10000004", 600.00)
|
||||
print(f"Max order size: {max_size}")
|
||||
|
||||
# Cancel an order
|
||||
cancel_params = CancelOrderParams(
|
||||
order_id=order.get("data", {}).get("orderId")
|
||||
)
|
||||
cancel_result = await client.cancel_order(cancel_params)
|
||||
print(f"Order cancelled: {cancel_result}")
|
||||
```
|
||||
|
||||
### Contract IDs
|
||||
|
||||
EdgeX uses numeric contract IDs instead of symbol-based identifiers. Here are some common contract mappings:
|
||||
|
||||
| Contract ID | Symbol | Tick Size |
|
||||
|-------------|---------------|-----------|
|
||||
| 10000001 | BTCUSDT | 0.1 |
|
||||
| 10000002 | ETHUSDT | 0.01 |
|
||||
| 10000003 | SOLUSDT | 0.01 |
|
||||
|
||||
To get the complete list of available contracts:
|
||||
|
||||
```python
|
||||
metadata = await client.get_metadata()
|
||||
contracts = metadata.get("data", {}).get("contractList", [])
|
||||
for contract in contracts:
|
||||
print(f"ID: {contract['contractId']} - {contract['contractName']}")
|
||||
```
|
||||
|
||||
For more detailed examples, please refer to the [examples](examples) directory.
|
||||
|
||||
## Testing
|
||||
|
||||
The SDK includes comprehensive test coverage with multiple test suites:
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
# Run unit tests (no API credentials required)
|
||||
python -m pytest tests/test_client.py tests/test_starkex_signing_adapter.py -v
|
||||
```
|
||||
|
||||
### Public API Tests
|
||||
```bash
|
||||
# Run public endpoint tests (no authentication required)
|
||||
python run_public_tests.py
|
||||
```
|
||||
|
||||
### Mock Integration Tests
|
||||
```bash
|
||||
# Run mock tests (test structure without real API calls)
|
||||
python run_mock_tests.py
|
||||
```
|
||||
|
||||
### Full Integration Tests
|
||||
```bash
|
||||
# Run full integration tests (requires real API credentials)
|
||||
python run_integration_tests.py
|
||||
```
|
||||
|
||||
### All Tests
|
||||
```bash
|
||||
# Run all available tests
|
||||
python run_tests.py
|
||||
```
|
||||
|
||||
For more testing information, see [TESTING.md](TESTING.md).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
For testing and development, you can set the following environment variables or create a `.env` file:
|
||||
|
||||
```bash
|
||||
# API Configuration
|
||||
EDGEX_BASE_URL=https://pro.edgex.exchange # Use https://testnet.edgex.exchange for testnet
|
||||
EDGEX_WS_URL=wss://quote.edgex.exchange # Use wss://quote-testnet.edgex.exchange for testnet
|
||||
|
||||
# Account Credentials
|
||||
EDGEX_ACCOUNT_ID=12345
|
||||
EDGEX_STARK_PRIVATE_KEY=your-stark-private-key
|
||||
|
||||
# Signing Configuration
|
||||
EDGEX_SIGNING_ADAPTER=starkex
|
||||
```
|
||||
|
||||
Then load them in your code:
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from edgex_sdk import Client
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
client = Client(
|
||||
base_url=os.getenv("EDGEX_BASE_URL"),
|
||||
account_id=int(os.getenv("EDGEX_ACCOUNT_ID")),
|
||||
stark_private_key=os.getenv("EDGEX_STARK_PRIVATE_KEY")
|
||||
)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed API documentation, please refer to the [EdgeX API documentation](https://docs.edgex.exchange).
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin feature/my-new-feature`)
|
||||
5. Create a new Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
EdgeX Python SDK - A Python SDK for interacting with the EdgeX Exchange API.
|
||||
"""
|
||||
|
||||
from .client import Client
|
||||
from .internal.signing_adapter import SigningAdapter
|
||||
from .internal.starkex_signing_adapter import StarkExSigningAdapter
|
||||
from .order.types import (
|
||||
OrderType,
|
||||
OrderSide,
|
||||
TimeInForce,
|
||||
CreateOrderParams,
|
||||
CancelOrderParams,
|
||||
GetActiveOrderParams,
|
||||
OrderFillTransactionParams
|
||||
)
|
||||
from .account.client import (
|
||||
GetPositionTransactionPageParams,
|
||||
GetCollateralTransactionPageParams,
|
||||
GetPositionTermPageParams,
|
||||
GetAccountAssetSnapshotPageParams
|
||||
)
|
||||
from .quote.client import (
|
||||
GetKLineParams,
|
||||
GetOrderBookDepthParams,
|
||||
GetMultiContractKLineParams
|
||||
)
|
||||
from .transfer.client import (
|
||||
GetTransferOutByIdParams,
|
||||
GetTransferInByIdParams,
|
||||
GetWithdrawAvailableAmountParams,
|
||||
CreateTransferOutParams,
|
||||
GetTransferOutPageParams,
|
||||
GetTransferInPageParams
|
||||
)
|
||||
from .asset.client import (
|
||||
GetAssetOrdersParams,
|
||||
CreateWithdrawalParams,
|
||||
GetWithdrawalRecordsParams
|
||||
)
|
||||
from .ws.manager import Manager as WebSocketManager
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__all__ = [
|
||||
"Client",
|
||||
"OrderType",
|
||||
"OrderSide",
|
||||
"TimeInForce",
|
||||
"CreateOrderParams",
|
||||
"CancelOrderParams",
|
||||
"GetActiveOrderParams",
|
||||
"OrderFillTransactionParams",
|
||||
"GetPositionTransactionPageParams",
|
||||
"GetCollateralTransactionPageParams",
|
||||
"GetPositionTermPageParams",
|
||||
"GetAccountAssetSnapshotPageParams",
|
||||
"GetKLineParams",
|
||||
"GetOrderBookDepthParams",
|
||||
"GetMultiContractKLineParams",
|
||||
"GetTransferOutByIdParams",
|
||||
"GetTransferInByIdParams",
|
||||
"GetWithdrawAvailableAmountParams",
|
||||
"CreateTransferOutParams",
|
||||
"GetTransferOutPageParams",
|
||||
"GetTransferInPageParams",
|
||||
"GetAssetOrdersParams",
|
||||
"CreateWithdrawalParams",
|
||||
"GetWithdrawalRecordsParams",
|
||||
"WebSocketManager",
|
||||
"SigningAdapter",
|
||||
"StarkExSigningAdapter"
|
||||
]
|
||||
@@ -0,0 +1,437 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class GetPositionTransactionPageParams:
|
||||
"""Parameters for getting position transactions with pagination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_contract_id_list: List[str] = None,
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_contract_id_list = filter_contract_id_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class GetCollateralTransactionPageParams:
|
||||
"""Parameters for getting collateral transactions with pagination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class GetPositionTermPageParams:
|
||||
"""Parameters for getting position terms with pagination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_contract_id_list: List[str] = None,
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_contract_id_list = filter_contract_id_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class GetAccountAssetSnapshotPageParams:
|
||||
"""Parameters for getting account asset snapshots with pagination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for account-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the account client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_account_asset(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the account asset information.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account asset information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/account/getAccountAsset",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def get_account_positions(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the account positions.
|
||||
|
||||
Note: This calls the same endpoint as get_account_asset, which returns both
|
||||
collateral and position data. The position data is in the 'positionAssetList' field.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account positions (same as account asset response)
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Use the same endpoint as get_account_asset (matching Go SDK behavior)
|
||||
return await self.get_account_asset()
|
||||
|
||||
async def get_position_transaction_page(self, params: GetPositionTransactionPageParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the position transactions with pagination.
|
||||
|
||||
Args:
|
||||
params: Position transaction query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The position transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/account/getPositionTransactionPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_collateral_transaction_page(self, params: GetCollateralTransactionPageParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the collateral transactions with pagination.
|
||||
|
||||
Args:
|
||||
params: Collateral transaction query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The collateral transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/account/getCollateralTransactionPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_position_term_page(self, params: GetPositionTermPageParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the position terms with pagination.
|
||||
|
||||
Args:
|
||||
params: Position term query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The position terms
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/getPositionTermPage"
|
||||
query_params = {
|
||||
"accountId": str(self.internal_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
response = self.session.get(url, params=query_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
async def get_account_by_id(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account information by ID.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/account/getAccountById",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def get_account_deleverage_light(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account deleverage light information.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account deleverage light information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/getAccountDeleverageLight"
|
||||
params = {
|
||||
"accountId": str(self.internal_client.get_account_id())
|
||||
}
|
||||
|
||||
response = self.session.get(url, params=params)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
async def get_account_asset_snapshot_page(self, params: GetAccountAssetSnapshotPageParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account asset snapshots with pagination.
|
||||
|
||||
Args:
|
||||
params: Account asset snapshot query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account asset snapshots
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/getAccountAssetSnapshotPage"
|
||||
query_params = {
|
||||
"accountId": str(self.internal_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
response = self.session.get(url, params=query_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
async def get_position_transaction_by_id(self, transaction_ids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get position transactions by IDs.
|
||||
|
||||
Args:
|
||||
transaction_ids: List of transaction IDs
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The position transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/getPositionTransactionById"
|
||||
query_params = {
|
||||
"accountId": str(self.internal_client.get_account_id()),
|
||||
"transactionIdList": ",".join(transaction_ids)
|
||||
}
|
||||
|
||||
response = self.session.get(url, params=query_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
async def get_collateral_transaction_by_id(self, transaction_ids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get collateral transactions by IDs.
|
||||
|
||||
Args:
|
||||
transaction_ids: List of transaction IDs
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The collateral transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/getCollateralTransactionById"
|
||||
query_params = {
|
||||
"accountId": str(self.internal_client.get_account_id()),
|
||||
"transactionIdList": ",".join(transaction_ids)
|
||||
}
|
||||
|
||||
response = self.session.get(url, params=query_params)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
async def update_leverage_setting(self, contract_id: str, leverage: str) -> None:
|
||||
"""
|
||||
Update the account leverage settings.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
leverage: The leverage value
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/private/account/updateLeverageSetting"
|
||||
data = {
|
||||
"accountId": str(self.internal_client.get_account_id()),
|
||||
"contractId": contract_id,
|
||||
"leverage": leverage
|
||||
}
|
||||
|
||||
response = self.session.post(url, json=data)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"request failed with status code: {response.status_code}")
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
if resp_data.get("code") != ResponseCode.SUCCESS:
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
@@ -0,0 +1,300 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class GetAssetOrdersParams:
|
||||
"""Parameters for getting asset orders."""
|
||||
|
||||
def __init__(self, size: str = "10", offset_data: str = "", filter_coin_id_list: List[str] = None,
|
||||
filter_start_created_time_inclusive: int = 0, filter_end_created_time_exclusive: int = 0):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_coin_id_list = filter_coin_id_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class CreateWithdrawalParams:
|
||||
"""Parameters for creating a withdrawal."""
|
||||
|
||||
def __init__(self, coin_id: str, amount: str, address: str, tag: str = ""):
|
||||
self.coin_id = coin_id
|
||||
self.amount = amount
|
||||
self.address = address
|
||||
self.tag = tag
|
||||
|
||||
|
||||
class GetWithdrawalRecordsParams:
|
||||
"""Parameters for getting withdrawal records."""
|
||||
|
||||
def __init__(self, size: str = "10", offset_data: str = "", filter_coin_id_list: List[str] = None,
|
||||
filter_status_list: List[str] = None, filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_coin_id_list = filter_coin_id_list or []
|
||||
self.filter_status_list = filter_status_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for asset-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the asset client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_account_asset(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the account asset information.
|
||||
Note: This method delegates to the account client since it's an account endpoint.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The account asset information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# This is actually an account endpoint, not an asset endpoint
|
||||
# We should delegate to the account client
|
||||
raise NotImplementedError("This method should be called from the account client: client.account.get_account_asset()")
|
||||
|
||||
async def get_asset_orders(
|
||||
self,
|
||||
params: GetAssetOrdersParams
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get asset orders with pagination.
|
||||
|
||||
Args:
|
||||
params: Parameters for the request
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The asset orders
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/assets/getAllOrdersPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_coin_rates(self, chain_id: str = "1", coin: str = "0xdac17f958d2ee523a2206206994597c13d831ec7") -> Dict[str, Any]:
|
||||
"""
|
||||
Get coin rates.
|
||||
|
||||
Args:
|
||||
chain_id: Chain ID (default: "1" for Ethereum mainnet)
|
||||
coin: Coin contract address (default: USDT)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The coin rates
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
params = {
|
||||
"chainId": chain_id,
|
||||
"coin": coin
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/assets/getCoinRate",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def create_withdrawal(
|
||||
self,
|
||||
coin_id: str,
|
||||
amount: str,
|
||||
address: str,
|
||||
network: str,
|
||||
memo: str = "",
|
||||
client_order_id: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a withdrawal request.
|
||||
|
||||
Args:
|
||||
coin_id: The coin ID
|
||||
amount: The withdrawal amount
|
||||
address: The withdrawal address
|
||||
network: The network
|
||||
memo: Optional memo
|
||||
client_order_id: Optional client order ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The withdrawal result
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
data = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"coinId": coin_id,
|
||||
"amount": amount,
|
||||
"address": address,
|
||||
"network": network
|
||||
}
|
||||
|
||||
if memo:
|
||||
data["memo"] = memo
|
||||
|
||||
if client_order_id:
|
||||
data["clientOrderId"] = client_order_id
|
||||
else:
|
||||
data["clientOrderId"] = self.async_client.generate_uuid()
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/assets/createNormalWithdraw",
|
||||
data=data
|
||||
)
|
||||
|
||||
async def get_withdrawal_records(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_coin_id_list: List[str] = None,
|
||||
filter_status_list: List[str] = None,
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get withdrawal records with pagination.
|
||||
|
||||
Args:
|
||||
size: Size of the page
|
||||
offset_data: Offset data for pagination
|
||||
filter_coin_id_list: Filter by coin IDs
|
||||
filter_status_list: Filter by status
|
||||
filter_start_created_time_inclusive: Filter start time (inclusive)
|
||||
filter_end_created_time_exclusive: Filter end time (exclusive)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The withdrawal records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if size:
|
||||
query_params["size"] = size
|
||||
if offset_data:
|
||||
query_params["offsetData"] = offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(filter_coin_id_list)
|
||||
if filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(filter_status_list)
|
||||
|
||||
# Add time filters
|
||||
if filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(filter_start_created_time_inclusive)
|
||||
if filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/assets/getNormalWithdrawById",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_withdrawable_amount(self, address: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the withdrawable amount for a coin.
|
||||
|
||||
Args:
|
||||
address: The coin contract address
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The withdrawable amount information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"address": address
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/assets/getNormalWithdrawableAmount",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_withdrawal_records(self, params: GetWithdrawalRecordsParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get withdrawal records with pagination.
|
||||
|
||||
Args:
|
||||
params: Parameters for the request
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The withdrawal records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(params.filter_status_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/assets/getNormalWithdrawById",
|
||||
params=query_params
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
from decimal import Decimal
|
||||
|
||||
from .internal.async_client import AsyncClient
|
||||
from .internal.signing_adapter import SigningAdapter
|
||||
from .internal.starkex_signing_adapter import StarkExSigningAdapter
|
||||
from .account.client import Client as AccountClient
|
||||
from .asset.client import Client as AssetClient
|
||||
from .funding.client import Client as FundingClient
|
||||
from .metadata.client import Client as MetadataClient
|
||||
from .order.client import Client as OrderClient
|
||||
from .quote.client import Client as QuoteClient
|
||||
from .transfer.client import Client as TransferClient
|
||||
from .order.types import CreateOrderParams, CancelOrderParams, GetActiveOrderParams, OrderFillTransactionParams
|
||||
|
||||
|
||||
class Client:
|
||||
"""Main EdgeX SDK client."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_private_key: str,
|
||||
signing_adapter: Optional[SigningAdapter] = None, timeout: float = 30.0):
|
||||
"""
|
||||
Initialize the EdgeX SDK client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_private_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter (defaults to StarkExSigningAdapter)
|
||||
timeout: Request timeout in seconds
|
||||
"""
|
||||
# Use StarkExSigningAdapter as default if none provided
|
||||
if signing_adapter is None:
|
||||
signing_adapter = StarkExSigningAdapter()
|
||||
|
||||
# Create async client
|
||||
self.async_client = AsyncClient(
|
||||
base_url=base_url,
|
||||
account_id=account_id,
|
||||
stark_pri_key=stark_private_key,
|
||||
signing_adapter=signing_adapter,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# Initialize API clients
|
||||
self.metadata = MetadataClient(self.async_client)
|
||||
self.account = AccountClient(self.async_client)
|
||||
self.order = OrderClient(self.async_client)
|
||||
self.quote = QuoteClient(self.async_client)
|
||||
self.funding = FundingClient(self.async_client)
|
||||
self.transfer = TransferClient(self.async_client)
|
||||
self.asset = AssetClient(self.async_client)
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
await self.async_client._ensure_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
"""Close the client and cleanup resources."""
|
||||
await self.async_client.close()
|
||||
|
||||
@property
|
||||
def internal_client(self):
|
||||
"""Backward compatibility property for accessing internal client."""
|
||||
return self.async_client
|
||||
|
||||
async def get_metadata(self) -> Dict[str, Any]:
|
||||
"""Get the exchange metadata."""
|
||||
return await self.metadata.get_metadata()
|
||||
|
||||
async def get_server_time(self) -> Dict[str, Any]:
|
||||
"""Get the current server time."""
|
||||
return await self.metadata.get_server_time()
|
||||
|
||||
async def create_order(self, params: CreateOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new order with the given parameters.
|
||||
|
||||
Args:
|
||||
params: Order parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
"""
|
||||
# Get metadata first
|
||||
metadata = await self.get_metadata()
|
||||
if not metadata:
|
||||
raise ValueError("failed to get metadata")
|
||||
|
||||
return await self.order.create_order(params, metadata.get("data", {}))
|
||||
|
||||
async def get_max_order_size(self, contract_id: str, price: Decimal) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the maximum order size for a given contract and price.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
price: The price
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The maximum order size information
|
||||
"""
|
||||
return await self.order.get_max_order_size(contract_id, float(price))
|
||||
|
||||
async def cancel_order(self, params: CancelOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel a specific order.
|
||||
|
||||
Args:
|
||||
params: Cancel order parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cancellation result
|
||||
"""
|
||||
return await self.order.cancel_order(params)
|
||||
|
||||
async def get_active_orders(self, params: GetActiveOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get active orders with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Active order query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The active orders
|
||||
"""
|
||||
return await self.order.get_active_orders(params)
|
||||
|
||||
async def get_order_fill_transactions(self, params: OrderFillTransactionParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get order fill transactions with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Order fill transaction query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order fill transactions
|
||||
"""
|
||||
return await self.order.get_order_fill_transactions(params)
|
||||
|
||||
async def get_account_asset(self) -> Dict[str, Any]:
|
||||
"""Get the account asset information."""
|
||||
return await self.account.get_account_asset()
|
||||
|
||||
async def get_account_positions(self) -> Dict[str, Any]:
|
||||
"""Get the account positions."""
|
||||
return await self.account.get_account_positions()
|
||||
|
||||
async def create_limit_order(
|
||||
self,
|
||||
contract_id: str,
|
||||
size: str,
|
||||
price: str,
|
||||
side: str,
|
||||
client_order_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new limit order with the given parameters.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
size: The order size
|
||||
price: The order price
|
||||
side: The order side (BUY or SELL)
|
||||
client_order_id: Optional client order ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
"""
|
||||
from .order.types import OrderType
|
||||
|
||||
params = CreateOrderParams(
|
||||
contract_id=contract_id,
|
||||
size=size,
|
||||
price=price,
|
||||
side=side,
|
||||
type=OrderType.LIMIT,
|
||||
client_order_id=client_order_id
|
||||
)
|
||||
|
||||
return await self.create_order(params)
|
||||
|
||||
async def create_market_order(
|
||||
self,
|
||||
contract_id: str,
|
||||
size: str,
|
||||
side: str,
|
||||
client_order_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new market order with the given parameters.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
size: The order size
|
||||
side: The order side (BUY or SELL)
|
||||
client_order_id: Optional client order ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
"""
|
||||
# Get metadata for contract info
|
||||
metadata = await self.get_metadata()
|
||||
if not metadata:
|
||||
raise ValueError("failed to get metadata")
|
||||
|
||||
# Find the contract
|
||||
contract = None
|
||||
contract_list = metadata.get("data", {}).get("contractList", [])
|
||||
for c in contract_list:
|
||||
if c.get("contractId") == contract_id:
|
||||
contract = c
|
||||
break
|
||||
|
||||
if not contract:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
|
||||
# Calculate price based on side
|
||||
from .order.types import OrderSide, OrderType
|
||||
|
||||
if side == OrderSide.BUY:
|
||||
# For buy orders: oracle_price * 10, rounded to price precision
|
||||
quote = await self.get_24_hour_quote(contract_id)
|
||||
if not quote:
|
||||
raise ValueError("failed to get 24-hour quotes")
|
||||
|
||||
oracle_price = Decimal(quote.get("data", [])[0].get("oraclePrice", "0"))
|
||||
multiplier = Decimal("10")
|
||||
tick_size = Decimal(contract.get("tickSize", "0"))
|
||||
precision = abs(tick_size.as_tuple().exponent)
|
||||
price = str(round(oracle_price * multiplier, precision))
|
||||
else:
|
||||
# For sell orders: use tick size
|
||||
price = contract.get("tickSize", "0")
|
||||
|
||||
params = CreateOrderParams(
|
||||
contract_id=contract_id,
|
||||
size=size,
|
||||
price=price,
|
||||
side=side,
|
||||
type=OrderType.MARKET,
|
||||
client_order_id=client_order_id
|
||||
)
|
||||
|
||||
return await self.create_order(params)
|
||||
|
||||
async def get_24_hour_quote(self, contract_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the 24-hour quotes for a given contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The 24-hour quotes
|
||||
"""
|
||||
return await self.quote.get_24_hour_quote(contract_id)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Cryptographic utilities for the EdgeX Python SDK.
|
||||
|
||||
This module provides cryptographic functions including Pedersen hash
|
||||
implementation compatible with StarkWare's specifications.
|
||||
"""
|
||||
|
||||
from .pedersen_hash import pedersen_hash, pedersen_hash_as_point
|
||||
|
||||
__all__ = [
|
||||
'pedersen_hash',
|
||||
'pedersen_hash_as_point',
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
Pedersen hash implementation for StarkWare cryptography.
|
||||
|
||||
This module provides a full implementation of the Pedersen hash function
|
||||
as specified by StarkWare, compatible with the reference implementation.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
# Handle both relative and absolute imports
|
||||
try:
|
||||
from .constants import (
|
||||
FIELD_PRIME, ALPHA, BETA, N_ELEMENT_BITS_HASH,
|
||||
SHIFT_POINT, CONSTANT_POINTS
|
||||
)
|
||||
except ImportError:
|
||||
from constants import (
|
||||
FIELD_PRIME, ALPHA, BETA, N_ELEMENT_BITS_HASH,
|
||||
SHIFT_POINT, CONSTANT_POINTS
|
||||
)
|
||||
|
||||
|
||||
def _div_mod(n: int, m: int, p: int) -> int:
|
||||
"""
|
||||
Calculate (n / m) mod p.
|
||||
|
||||
Args:
|
||||
n: The numerator
|
||||
m: The denominator
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
int: The result of the division modulo p
|
||||
"""
|
||||
return (n * pow(m, -1, p)) % p
|
||||
|
||||
|
||||
def _ec_add(p1: Tuple[int, int], p2: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Add two points on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p1: The first point as (x, y) coordinates
|
||||
p2: The second point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if p1[0] == p2[0]:
|
||||
if (p1[1] + p2[1]) % FIELD_PRIME == 0:
|
||||
# The points are negatives of each other, return the point at infinity
|
||||
# We represent the point at infinity as None, but this should never happen
|
||||
# in our use case, so we raise an exception instead
|
||||
raise ValueError("Points are negatives of each other")
|
||||
|
||||
# The points are the same, so we're doubling
|
||||
return _ec_double(p1)
|
||||
|
||||
# Calculate the slope
|
||||
slope = _div_mod(p2[1] - p1[1], p2[0] - p1[0], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - p1[0] - p2[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p1[0] - x3) - p1[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
|
||||
def _ec_double(p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Double a point on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p: The point to double as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
# Calculate the slope
|
||||
slope = _div_mod(3 * p[0] * p[0] + ALPHA, 2 * p[1], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - 2 * p[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p[0] - x3) - p[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
|
||||
def _ec_mult(m: int, p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Multiply a point on the elliptic curve by a scalar.
|
||||
|
||||
Args:
|
||||
m: The scalar
|
||||
p: The point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if m == 0:
|
||||
raise ValueError("Cannot multiply by 0")
|
||||
|
||||
if m == 1:
|
||||
return p
|
||||
|
||||
if m % 2 == 0:
|
||||
return _ec_mult(m // 2, _ec_double(p))
|
||||
else:
|
||||
return _ec_add(p, _ec_mult(m - 1, p))
|
||||
|
||||
|
||||
def pedersen_hash_as_point(*elements: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers and return the full EC point.
|
||||
|
||||
This is the full implementation following StarkWare's specification:
|
||||
For each element, iterate through its 252 bits and add corresponding
|
||||
constant points based on the bit values.
|
||||
|
||||
Args:
|
||||
*elements: Variable number of integers to hash
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting EC point as (x, y) coordinates
|
||||
|
||||
Raises:
|
||||
ValueError: If any element is out of range or if there are insufficient constant points
|
||||
"""
|
||||
# Start with the shift point
|
||||
point = tuple(SHIFT_POINT)
|
||||
|
||||
for i, element in enumerate(elements):
|
||||
# Validate element is in valid range
|
||||
if not (0 <= element < FIELD_PRIME):
|
||||
raise ValueError(f"Element {element} is out of range [0, {FIELD_PRIME})")
|
||||
|
||||
# Calculate the starting index for this element's constant points
|
||||
start_idx = 2 + i * N_ELEMENT_BITS_HASH
|
||||
|
||||
# Check if we have enough constant points
|
||||
if start_idx + N_ELEMENT_BITS_HASH > len(CONSTANT_POINTS):
|
||||
raise ValueError(f"Insufficient constant points for element {i}. Need {start_idx + N_ELEMENT_BITS_HASH}, have {len(CONSTANT_POINTS)}")
|
||||
|
||||
# Full implementation using all 252 bits
|
||||
for j in range(N_ELEMENT_BITS_HASH):
|
||||
pt = tuple(CONSTANT_POINTS[start_idx + j])
|
||||
|
||||
# Check for unhashable input (same x coordinate)
|
||||
if point[0] == pt[0]:
|
||||
raise ValueError('Unhashable input: point collision detected')
|
||||
|
||||
if element & 1:
|
||||
point = _ec_add(point, pt)
|
||||
element >>= 1
|
||||
|
||||
# Ensure all bits have been processed
|
||||
if element != 0:
|
||||
raise ValueError(f"Element too large: remaining bits {element}")
|
||||
|
||||
return point
|
||||
|
||||
|
||||
def pedersen_hash(*elements: int) -> int:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers.
|
||||
|
||||
This function returns only the x-coordinate of the resulting EC point,
|
||||
which is the standard Pedersen hash value.
|
||||
|
||||
Args:
|
||||
*elements: Variable number of integers to hash
|
||||
|
||||
Returns:
|
||||
int: The Pedersen hash as an integer (x-coordinate of the EC point)
|
||||
|
||||
Raises:
|
||||
ValueError: If any element is out of range
|
||||
"""
|
||||
point = pedersen_hash_as_point(*elements)
|
||||
return point[0]
|
||||
|
||||
|
||||
def pedersen_hash_bytes(*elements: Union[int, bytes]) -> bytes:
|
||||
"""
|
||||
Calculate the Pedersen hash and return as bytes.
|
||||
|
||||
Args:
|
||||
*elements: Variable number of integers or bytes to hash
|
||||
|
||||
Returns:
|
||||
bytes: The hash result as 32 bytes (big-endian)
|
||||
|
||||
Raises:
|
||||
ValueError: If any element is invalid
|
||||
"""
|
||||
# Convert bytes to integers if needed
|
||||
int_elements = []
|
||||
for element in elements:
|
||||
if isinstance(element, bytes):
|
||||
if len(element) > 32:
|
||||
raise ValueError(f"Bytes element too long: {len(element)} > 32")
|
||||
int_elements.append(int.from_bytes(element, byteorder='big'))
|
||||
elif isinstance(element, int):
|
||||
int_elements.append(element)
|
||||
else:
|
||||
raise ValueError(f"Invalid element type: {type(element)}")
|
||||
|
||||
hash_result = pedersen_hash(*int_elements)
|
||||
return hash_result.to_bytes(32, byteorder='big')
|
||||
@@ -0,0 +1,114 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for funding-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the funding client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_funding_transactions(
|
||||
self,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_coin_id_list: List[str] = None,
|
||||
filter_type_list: List[str] = None,
|
||||
filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get funding transactions with pagination.
|
||||
|
||||
Args:
|
||||
size: Size of the page
|
||||
offset_data: Offset data for pagination
|
||||
filter_coin_id_list: Filter by coin IDs
|
||||
filter_type_list: Filter by transaction types
|
||||
filter_start_created_time_inclusive: Filter start time (inclusive)
|
||||
filter_end_created_time_exclusive: Filter end time (exclusive)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The funding transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if size:
|
||||
query_params["size"] = size
|
||||
if offset_data:
|
||||
query_params["offsetData"] = offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(filter_coin_id_list)
|
||||
if filter_type_list:
|
||||
query_params["filterTypeList"] = ",".join(filter_type_list)
|
||||
|
||||
# Add time filters
|
||||
if filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(filter_start_created_time_inclusive)
|
||||
if filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/public/funding/getFundingRatePage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_funding_account(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get funding account information.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The funding account information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/account/getAccountAsset",
|
||||
params=params
|
||||
)
|
||||
|
||||
async def get_funding_transaction_by_id(self, transaction_ids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get funding transactions by IDs.
|
||||
|
||||
Args:
|
||||
transaction_ids: List of transaction IDs
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The funding transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"transactionIdList": ",".join(transaction_ids)
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/public/funding/getLatestFundingRate",
|
||||
params=query_params
|
||||
)
|
||||
@@ -0,0 +1,466 @@
|
||||
import asyncio
|
||||
import binascii
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, Tuple, List, Union
|
||||
import json
|
||||
|
||||
import aiohttp
|
||||
from Crypto.Hash import keccak
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
|
||||
# Import field prime for modular arithmetic
|
||||
try:
|
||||
from ..crypto.constants import FIELD_PRIME
|
||||
except ImportError:
|
||||
# Fallback if crypto module is not available
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
|
||||
# Constants
|
||||
LIMIT_ORDER_WITH_FEE_TYPE = 3
|
||||
|
||||
|
||||
class L2Signature:
|
||||
"""Represents a signature for L2 operations."""
|
||||
|
||||
def __init__(self, r: str, s: str, v: str = ""):
|
||||
self.r = r
|
||||
self.s = s
|
||||
self.v = v
|
||||
|
||||
|
||||
class AsyncClient:
|
||||
"""Async base client with common functionality."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_pri_key: str,
|
||||
signing_adapter: Optional[SigningAdapter] = None,
|
||||
timeout: float = 30.0, connector_limit: int = 100):
|
||||
"""
|
||||
Initialize the async internal client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter to use for cryptographic operations
|
||||
timeout: Request timeout in seconds
|
||||
connector_limit: Maximum number of connections in the pool
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use the provided signing adapter (required)
|
||||
if signing_adapter is None:
|
||||
raise ValueError("signing_adapter is required")
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
# Store configuration for later session creation
|
||||
self._session = None
|
||||
self._timeout = timeout
|
||||
self._connector_limit = connector_limit
|
||||
self._closed = False
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
await self._ensure_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
|
||||
async def _ensure_session(self):
|
||||
"""Ensure the aiohttp session is created."""
|
||||
if self._session is None or self._session.closed:
|
||||
# Create connector and session when needed (inside event loop)
|
||||
timeout_config = aiohttp.ClientTimeout(total=self._timeout)
|
||||
connector = aiohttp.TCPConnector(
|
||||
limit=self._connector_limit,
|
||||
limit_per_host=30,
|
||||
keepalive_timeout=30,
|
||||
enable_cleanup_closed=True
|
||||
)
|
||||
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=timeout_config,
|
||||
connector=connector,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP session and cleanup resources."""
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._closed = True
|
||||
|
||||
@property
|
||||
def session(self) -> aiohttp.ClientSession:
|
||||
"""Get the HTTP session, ensuring it's created."""
|
||||
if self._session is None or self._session.closed:
|
||||
raise RuntimeError("Session not initialized. Use 'async with client:' or call '_ensure_session()'")
|
||||
return self._session
|
||||
|
||||
def get_account_id(self) -> int:
|
||||
"""Get the account ID."""
|
||||
return self.account_id
|
||||
|
||||
def get_stark_pri_key(self) -> str:
|
||||
"""Get the stark private key."""
|
||||
return self.stark_pri_key
|
||||
|
||||
def sign(self, message_hash: bytes) -> L2Signature:
|
||||
"""
|
||||
Sign a message hash using the client's Stark private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
|
||||
Returns:
|
||||
L2Signature: The signature components
|
||||
|
||||
Raises:
|
||||
ValueError: If the stark private key is not set or invalid
|
||||
"""
|
||||
private_key = self.get_stark_pri_key()
|
||||
if not private_key:
|
||||
raise ValueError("stark private key not set")
|
||||
|
||||
# Sign the message using the signing adapter
|
||||
try:
|
||||
r, s = self.signing_adapter.sign(message_hash, private_key)
|
||||
return L2Signature(r=r, s=s, v="")
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to sign message: {str(e)}")
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
"""Generate a UUID for client order IDs."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def calc_nonce(self, client_order_id: str) -> int:
|
||||
"""
|
||||
Calculate a nonce from a client order ID.
|
||||
|
||||
Args:
|
||||
client_order_id: The client order ID
|
||||
|
||||
Returns:
|
||||
int: The calculated nonce
|
||||
"""
|
||||
# Use SHA256 like the Go SDK (not Keccak256)
|
||||
h = hashlib.sha256()
|
||||
h.update(client_order_id.encode())
|
||||
hash_hex = h.hexdigest()
|
||||
return int(hash_hex[:8], 16)
|
||||
|
||||
async def make_authenticated_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Make an authenticated HTTP request.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
path: API path (e.g., '/api/v1/private/order/createOrder')
|
||||
data: JSON data for POST requests
|
||||
params: Query parameters for GET requests
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Response JSON data
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
await self._ensure_session()
|
||||
|
||||
# Generate timestamp
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
# Build full URL
|
||||
url = f"{self.base_url}{path}"
|
||||
|
||||
# Generate signature content
|
||||
sign_content = self._build_signature_content(timestamp, method, path, data, params)
|
||||
|
||||
# Sign the content
|
||||
keccak_hash = keccak.new(digest_bits=256)
|
||||
keccak_hash.update(sign_content.encode())
|
||||
content_hash = keccak_hash.digest()
|
||||
|
||||
sig = self.sign(content_hash)
|
||||
|
||||
# Prepare headers
|
||||
headers = {
|
||||
"X-edgeX-Api-Timestamp": str(timestamp),
|
||||
"X-edgeX-Api-Signature": f"{sig.r}{sig.s}"
|
||||
}
|
||||
|
||||
# Make the request
|
||||
try:
|
||||
async with self.session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=data,
|
||||
params=params,
|
||||
headers=headers
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError):
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
# Check response code
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise ValueError(f"HTTP request failed: {str(e)}")
|
||||
|
||||
def _build_signature_content(
|
||||
self,
|
||||
timestamp: int,
|
||||
method: str,
|
||||
path: str,
|
||||
data: Optional[Dict[str, Any]],
|
||||
params: Optional[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""Build the content string for signature generation."""
|
||||
if data:
|
||||
# Convert body to sorted string format
|
||||
body_str = self.get_value(data)
|
||||
sign_content = f"{timestamp}{method}{path}{body_str}"
|
||||
else:
|
||||
# For requests without body, use query parameters if present
|
||||
if params:
|
||||
# Sort query parameters as strings (matching Go SDK exactly)
|
||||
param_pairs = []
|
||||
for key, value in sorted(params.items()):
|
||||
param_pairs.append(f"{key}={value}")
|
||||
query_string = "&".join(param_pairs)
|
||||
sign_content = f"{timestamp}{method}{path}{query_string}"
|
||||
else:
|
||||
sign_content = f"{timestamp}{method}{path}"
|
||||
|
||||
return sign_content
|
||||
|
||||
def get_value(self, data: Union[Dict[str, Any], List[Any], str, int, float, None]) -> str:
|
||||
"""
|
||||
Convert a value to a string representation for signing.
|
||||
This function recursively processes dictionaries, lists, and primitive types.
|
||||
|
||||
Args:
|
||||
data: The value to convert
|
||||
|
||||
Returns:
|
||||
str: The string representation
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
|
||||
if isinstance(data, bool):
|
||||
# Convert boolean to lowercase string to match Go SDK
|
||||
return str(data).lower()
|
||||
|
||||
if isinstance(data, (int, float)):
|
||||
return str(data)
|
||||
|
||||
if isinstance(data, list):
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
values = [self.get_value(item) for item in data]
|
||||
return "&".join(values)
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Convert all values to strings and sort by keys
|
||||
sorted_map = {}
|
||||
for key, val in data.items():
|
||||
sorted_map[key] = self.get_value(val)
|
||||
|
||||
# Get sorted keys
|
||||
keys = sorted(sorted_map.keys())
|
||||
|
||||
# Build key=value pairs
|
||||
pairs = [f"{key}={sorted_map[key]}" for key in keys]
|
||||
return "&".join(pairs)
|
||||
|
||||
# Handle other types by converting to string
|
||||
return str(data)
|
||||
|
||||
def calc_limit_order_hash(
|
||||
self,
|
||||
synthetic_asset_id: str,
|
||||
collateral_asset_id: str,
|
||||
fee_asset_id: str,
|
||||
is_buy: bool,
|
||||
amount_synthetic: int,
|
||||
amount_collateral: int,
|
||||
amount_fee: int,
|
||||
nonce: int,
|
||||
account_id: int,
|
||||
expire_time: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a limit order using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
synthetic_asset_id: The synthetic asset ID (hex string)
|
||||
collateral_asset_id: The collateral asset ID (hex string)
|
||||
fee_asset_id: The fee asset ID (hex string)
|
||||
is_buy: Whether the order is a buy order
|
||||
amount_synthetic: The synthetic amount
|
||||
amount_collateral: The collateral amount
|
||||
amount_fee: The fee amount
|
||||
nonce: The nonce
|
||||
account_id: The account ID (position ID)
|
||||
expire_time: The expiration time
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# Remove 0x prefix if present
|
||||
if synthetic_asset_id.startswith('0x'):
|
||||
synthetic_asset_id = synthetic_asset_id[2:]
|
||||
if collateral_asset_id.startswith('0x'):
|
||||
collateral_asset_id = collateral_asset_id[2:]
|
||||
if fee_asset_id.startswith('0x'):
|
||||
fee_asset_id = fee_asset_id[2:]
|
||||
|
||||
# Convert hex strings to integers and ensure they're within the field
|
||||
asset_id_synthetic = int(synthetic_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_collateral = int(collateral_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_fee = int(fee_asset_id, 16) % FIELD_PRIME
|
||||
|
||||
# Determine buy/sell assets based on order direction
|
||||
if is_buy:
|
||||
asset_id_sell = asset_id_collateral
|
||||
asset_id_buy = asset_id_synthetic
|
||||
amount_sell = amount_collateral
|
||||
amount_buy = amount_synthetic
|
||||
else:
|
||||
asset_id_sell = asset_id_synthetic
|
||||
asset_id_buy = asset_id_collateral
|
||||
amount_sell = amount_synthetic
|
||||
amount_buy = amount_collateral
|
||||
|
||||
# Use the signing adapter to calculate the Pedersen hash
|
||||
# First hash: hash(asset_id_sell, asset_id_buy)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id_sell, asset_id_buy])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_message0 = amount_sell * 2^64 + amount_buy * 2^64 + max_amount_fee * 2^32 + nonce
|
||||
packed_message0 = amount_sell
|
||||
packed_message0 = (packed_message0 << 64) + amount_buy
|
||||
packed_message0 = (packed_message0 << 64) + amount_fee
|
||||
packed_message0 = (packed_message0 << 32) + nonce
|
||||
packed_message0 = packed_message0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_message0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_message1 = LIMIT_ORDER_WITH_FEES * 2^64 + position_id * 2^64 + position_id * 2^64 + position_id * 2^32 + expiration_timestamp * 2^17
|
||||
packed_message1 = LIMIT_ORDER_WITH_FEE_TYPE
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 32) + expire_time
|
||||
packed_message1 = packed_message1 << 17 # Padding
|
||||
packed_message1 = packed_message1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_message1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message1])
|
||||
|
||||
return msg
|
||||
|
||||
def calc_transfer_hash(
|
||||
self,
|
||||
asset_id: int,
|
||||
asset_id_fee: int,
|
||||
receiver_public_key: int,
|
||||
sender_position_id: int,
|
||||
receiver_position_id: int,
|
||||
fee_position_id: int,
|
||||
nonce: int,
|
||||
amount: int,
|
||||
max_amount_fee: int,
|
||||
expiration_timestamp: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a transfer using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
asset_id: The asset ID
|
||||
asset_id_fee: The fee asset ID
|
||||
receiver_public_key: The receiver's public key
|
||||
sender_position_id: The sender's position ID
|
||||
receiver_position_id: The receiver's position ID
|
||||
fee_position_id: The fee position ID
|
||||
nonce: The nonce
|
||||
amount: The transfer amount
|
||||
max_amount_fee: The maximum fee amount
|
||||
expiration_timestamp: The expiration timestamp
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# First hash: hash(asset_id, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, receiver_public_key)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, receiver_public_key])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_msg0 = sender_position_id * 2^64 + receiver_position_id * 2^64 + fee_position_id * 2^32 + nonce
|
||||
packed_msg0 = sender_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + receiver_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + fee_position_id
|
||||
packed_msg0 = (packed_msg0 << 32) + nonce
|
||||
packed_msg0 = packed_msg0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_msg0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_msg1 = 4 * 2^64 + amount * 2^64 + max_amount_fee * 2^32 + expiration_timestamp * 2^81
|
||||
packed_msg1 = 4 # Transfer type
|
||||
packed_msg1 = (packed_msg1 << 64) + amount
|
||||
packed_msg1 = (packed_msg1 << 64) + max_amount_fee
|
||||
packed_msg1 = (packed_msg1 << 32) + expiration_timestamp
|
||||
packed_msg1 = packed_msg1 << 81 # Padding
|
||||
packed_msg1 = packed_msg1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_msg1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg1])
|
||||
|
||||
return msg
|
||||
@@ -0,0 +1,312 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, Tuple, List, Union
|
||||
|
||||
import requests
|
||||
from Crypto.Hash import keccak
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
|
||||
# Import field prime for modular arithmetic
|
||||
try:
|
||||
from ..crypto.constants import FIELD_PRIME
|
||||
except ImportError:
|
||||
# Fallback if crypto module is not available
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
|
||||
# Constants
|
||||
LIMIT_ORDER_WITH_FEE_TYPE = 3
|
||||
|
||||
|
||||
class L2Signature:
|
||||
"""Represents a signature for L2 operations."""
|
||||
|
||||
def __init__(self, r: str, s: str, v: str = ""):
|
||||
self.r = r
|
||||
self.s = s
|
||||
self.v = v
|
||||
|
||||
|
||||
class Client:
|
||||
"""Base client with common functionality."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_pri_key: str, signing_adapter: Optional[SigningAdapter] = None):
|
||||
"""
|
||||
Initialize the internal client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter to use for cryptographic operations
|
||||
"""
|
||||
self.http_client = requests.Session()
|
||||
self.http_client.headers.update({
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
})
|
||||
self.base_url = base_url
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use the provided signing adapter (required)
|
||||
if signing_adapter is None:
|
||||
raise ValueError("signing_adapter is required")
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
def get_account_id(self) -> int:
|
||||
"""Get the account ID."""
|
||||
return self.account_id
|
||||
|
||||
def get_stark_pri_key(self) -> str:
|
||||
"""Get the stark private key."""
|
||||
return self.stark_pri_key
|
||||
|
||||
def sign(self, message_hash: bytes) -> L2Signature:
|
||||
"""
|
||||
Sign a message hash using the client's Stark private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
|
||||
Returns:
|
||||
L2Signature: The signature components
|
||||
|
||||
Raises:
|
||||
ValueError: If the stark private key is not set or invalid
|
||||
"""
|
||||
private_key = self.get_stark_pri_key()
|
||||
if not private_key:
|
||||
raise ValueError("stark private key not set")
|
||||
|
||||
# Sign the message using the signing adapter
|
||||
try:
|
||||
r, s = self.signing_adapter.sign(message_hash, private_key)
|
||||
return L2Signature(r=r, s=s, v="")
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to sign message: {str(e)}")
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
"""Generate a UUID for client order IDs."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def calc_nonce(self, client_order_id: str) -> int:
|
||||
"""
|
||||
Calculate a nonce from a client order ID.
|
||||
|
||||
Args:
|
||||
client_order_id: The client order ID
|
||||
|
||||
Returns:
|
||||
int: The calculated nonce
|
||||
"""
|
||||
# Use SHA256 like the Go SDK (not Keccak256)
|
||||
h = hashlib.sha256()
|
||||
h.update(client_order_id.encode())
|
||||
hash_hex = h.hexdigest()
|
||||
return int(hash_hex[:8], 16)
|
||||
|
||||
def calc_limit_order_hash(
|
||||
self,
|
||||
synthetic_asset_id: str,
|
||||
collateral_asset_id: str,
|
||||
fee_asset_id: str,
|
||||
is_buy: bool,
|
||||
amount_synthetic: int,
|
||||
amount_collateral: int,
|
||||
amount_fee: int,
|
||||
nonce: int,
|
||||
account_id: int,
|
||||
expire_time: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a limit order using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
synthetic_asset_id: The synthetic asset ID (hex string)
|
||||
collateral_asset_id: The collateral asset ID (hex string)
|
||||
fee_asset_id: The fee asset ID (hex string)
|
||||
is_buy: Whether the order is a buy order
|
||||
amount_synthetic: The synthetic amount
|
||||
amount_collateral: The collateral amount
|
||||
amount_fee: The fee amount
|
||||
nonce: The nonce
|
||||
account_id: The account ID (position ID)
|
||||
expire_time: The expiration time
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# Remove 0x prefix if present
|
||||
if synthetic_asset_id.startswith('0x'):
|
||||
synthetic_asset_id = synthetic_asset_id[2:]
|
||||
if collateral_asset_id.startswith('0x'):
|
||||
collateral_asset_id = collateral_asset_id[2:]
|
||||
if fee_asset_id.startswith('0x'):
|
||||
fee_asset_id = fee_asset_id[2:]
|
||||
|
||||
# Convert hex strings to integers and ensure they're within the field
|
||||
asset_id_synthetic = int(synthetic_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_collateral = int(collateral_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_fee = int(fee_asset_id, 16) % FIELD_PRIME
|
||||
|
||||
# Determine buy/sell assets based on order direction
|
||||
if is_buy:
|
||||
asset_id_sell = asset_id_collateral
|
||||
asset_id_buy = asset_id_synthetic
|
||||
amount_sell = amount_collateral
|
||||
amount_buy = amount_synthetic
|
||||
else:
|
||||
asset_id_sell = asset_id_synthetic
|
||||
asset_id_buy = asset_id_collateral
|
||||
amount_sell = amount_synthetic
|
||||
amount_buy = amount_collateral
|
||||
|
||||
# Use the signing adapter to calculate the Pedersen hash
|
||||
# First hash: hash(asset_id_sell, asset_id_buy)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id_sell, asset_id_buy])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_message0 = amount_sell * 2^64 + amount_buy * 2^64 + max_amount_fee * 2^32 + nonce
|
||||
packed_message0 = amount_sell
|
||||
packed_message0 = (packed_message0 << 64) + amount_buy
|
||||
packed_message0 = (packed_message0 << 64) + amount_fee
|
||||
packed_message0 = (packed_message0 << 32) + nonce
|
||||
packed_message0 = packed_message0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_message0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_message1 = LIMIT_ORDER_WITH_FEES * 2^64 + position_id * 2^64 + position_id * 2^64 + position_id * 2^32 + expiration_timestamp * 2^17
|
||||
packed_message1 = LIMIT_ORDER_WITH_FEE_TYPE
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 32) + expire_time
|
||||
packed_message1 = packed_message1 << 17 # Padding
|
||||
packed_message1 = packed_message1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_message1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message1])
|
||||
|
||||
return msg
|
||||
|
||||
def calc_transfer_hash(
|
||||
self,
|
||||
asset_id: int,
|
||||
asset_id_fee: int,
|
||||
receiver_public_key: int,
|
||||
sender_position_id: int,
|
||||
receiver_position_id: int,
|
||||
fee_position_id: int,
|
||||
nonce: int,
|
||||
amount: int,
|
||||
max_amount_fee: int,
|
||||
expiration_timestamp: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a transfer using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
asset_id: The asset ID
|
||||
asset_id_fee: The fee asset ID
|
||||
receiver_public_key: The receiver's public key
|
||||
sender_position_id: The sender's position ID
|
||||
receiver_position_id: The receiver's position ID
|
||||
fee_position_id: The fee position ID
|
||||
nonce: The nonce
|
||||
amount: The transfer amount
|
||||
max_amount_fee: The maximum fee amount
|
||||
expiration_timestamp: The expiration timestamp
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# First hash: hash(asset_id, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, receiver_public_key)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, receiver_public_key])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_msg0 = sender_position_id * 2^64 + receiver_position_id * 2^64 + fee_position_id * 2^32 + nonce
|
||||
packed_msg0 = sender_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + receiver_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + fee_position_id
|
||||
packed_msg0 = (packed_msg0 << 32) + nonce
|
||||
packed_msg0 = packed_msg0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_msg0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_msg1 = 4 * 2^64 + amount * 2^64 + max_amount_fee * 2^32 + expiration_timestamp * 2^81
|
||||
packed_msg1 = 4 # Transfer type
|
||||
packed_msg1 = (packed_msg1 << 64) + amount
|
||||
packed_msg1 = (packed_msg1 << 64) + max_amount_fee
|
||||
packed_msg1 = (packed_msg1 << 32) + expiration_timestamp
|
||||
packed_msg1 = packed_msg1 << 81 # Padding
|
||||
packed_msg1 = packed_msg1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_msg1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg1])
|
||||
|
||||
return msg
|
||||
|
||||
def get_value(self, data: Union[Dict[str, Any], List[Any], str, int, float, None]) -> str:
|
||||
"""
|
||||
Convert a value to a string representation for signing.
|
||||
This function recursively processes dictionaries, lists, and primitive types.
|
||||
|
||||
Args:
|
||||
data: The value to convert
|
||||
|
||||
Returns:
|
||||
str: The string representation
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
|
||||
if isinstance(data, bool):
|
||||
# Convert boolean to lowercase string to match Go SDK
|
||||
return str(data).lower()
|
||||
|
||||
if isinstance(data, (int, float)):
|
||||
return str(data)
|
||||
|
||||
if isinstance(data, list):
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
values = [self.get_value(item) for item in data]
|
||||
return "&".join(values)
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Convert all values to strings and sort by keys
|
||||
sorted_map = {}
|
||||
for key, val in data.items():
|
||||
sorted_map[key] = self.get_value(val)
|
||||
|
||||
# Get sorted keys
|
||||
keys = sorted(sorted_map.keys())
|
||||
|
||||
# Build key=value pairs
|
||||
pairs = [f"{key}={sorted_map[key]}" for key in keys]
|
||||
return "&".join(pairs)
|
||||
|
||||
# Handle other types by converting to string
|
||||
return str(data)
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Signing adapter interface for the EdgeX Python SDK.
|
||||
|
||||
This module defines the interface for signing adapters that can be used with the SDK.
|
||||
Different implementations can be provided for different environments (development, testing, production).
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple, List
|
||||
|
||||
|
||||
class SigningAdapter(ABC):
|
||||
"""Interface for signing adapters."""
|
||||
|
||||
@abstractmethod
|
||||
def sign(self, message_hash: bytes, private_key: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: The signature as (r, s) hex strings
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid or the signing fails
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_public_key(self, private_key: str) -> str:
|
||||
"""
|
||||
Get the public key from a private key.
|
||||
|
||||
Args:
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
str: The public key as a hex string
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, message_hash: bytes, signature: Tuple[str, str], public_key: str) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message
|
||||
signature: The signature as (r, s) hex strings
|
||||
public_key: The public key as a hex string
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pedersen_hash(self, elements: List[int]) -> bytes:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers.
|
||||
|
||||
Args:
|
||||
elements: List of integers to hash
|
||||
|
||||
Returns:
|
||||
bytes: The hash result
|
||||
|
||||
Raises:
|
||||
ValueError: If the calculation fails
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,496 @@
|
||||
"""
|
||||
StarkEx signing adapter for the EdgeX Python SDK.
|
||||
|
||||
This module provides an implementation of the signing adapter interface
|
||||
that uses the StarkWare cryptographic primitives for signing operations.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import math
|
||||
import secrets
|
||||
from typing import List, Tuple
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
from ..crypto.pedersen_hash import pedersen_hash_bytes
|
||||
|
||||
|
||||
# StarkEx curve parameters
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
ALPHA = 1
|
||||
BETA = 0x6f21413efbe40de150e596d72f7a8c5609ad26c15c915c1f4cdfcb99cee9e89
|
||||
EC_ORDER = 0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f
|
||||
N_ELEMENT_BITS_ECDSA = math.floor(math.log(FIELD_PRIME, 2))
|
||||
assert N_ELEMENT_BITS_ECDSA == 251
|
||||
|
||||
# Generator point for the Stark curve
|
||||
EC_GEN = (
|
||||
0x1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca,
|
||||
0x5668060aa49730b7be4801df46ec62de53ecd11abe43a32873000c36e8dc1f
|
||||
)
|
||||
|
||||
|
||||
class StarkExSigningAdapter(SigningAdapter):
|
||||
"""StarkEx implementation of the signing adapter interface."""
|
||||
|
||||
def sign(self, message_hash: bytes, private_key: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: The signature as (r, s) hex strings
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid or the signing fails
|
||||
"""
|
||||
try:
|
||||
# Validate private key format
|
||||
binascii.unhexlify(private_key)
|
||||
except binascii.Error:
|
||||
raise ValueError("Invalid private key hex string")
|
||||
|
||||
# Convert message hash to integer
|
||||
msg_hash_int = int.from_bytes(message_hash, byteorder='big')
|
||||
|
||||
# Ensure the message hash is in the valid range
|
||||
# Use the same modulus as the Golang SDK (EC_ORDER, which is starkcurve.N)
|
||||
msg_hash_int = msg_hash_int % EC_ORDER
|
||||
|
||||
# Convert private key to integer
|
||||
priv_key_int = int(private_key, 16)
|
||||
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key_int = priv_key_int % EC_ORDER
|
||||
if priv_key_int == 0:
|
||||
priv_key_int = 1
|
||||
|
||||
# Sign the message
|
||||
r, s = self._sign(msg_hash_int, priv_key_int)
|
||||
|
||||
# Convert r and s to hex strings
|
||||
r_hex = format(r, '064x')
|
||||
s_hex = format(s, '064x')
|
||||
|
||||
return r_hex, s_hex
|
||||
|
||||
def get_public_key(self, private_key: str) -> str:
|
||||
"""
|
||||
Get the public key from a private key.
|
||||
|
||||
Args:
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
str: The public key as a hex string
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate private key format
|
||||
binascii.unhexlify(private_key)
|
||||
except binascii.Error:
|
||||
raise ValueError("Invalid private key hex string")
|
||||
|
||||
# Convert private key to integer
|
||||
priv_key_int = int(private_key, 16)
|
||||
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key_int = priv_key_int % EC_ORDER
|
||||
if priv_key_int == 0:
|
||||
priv_key_int = 1
|
||||
|
||||
# Get the public key
|
||||
public_key = self._private_to_stark_key(priv_key_int)
|
||||
|
||||
# Convert public key to hex string
|
||||
public_key_hex = format(public_key, '064x')
|
||||
|
||||
return public_key_hex
|
||||
|
||||
def verify(self, message_hash: bytes, signature: Tuple[str, str], public_key: str) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message
|
||||
signature: The signature as (r, s) hex strings
|
||||
public_key: The public key as a hex string
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
try:
|
||||
# Convert message hash to integer
|
||||
msg_hash_int = int.from_bytes(message_hash, byteorder='big')
|
||||
|
||||
# Ensure the message hash is in the valid range
|
||||
# Use the same modulus as the sign method (EC_ORDER)
|
||||
msg_hash_int = msg_hash_int % EC_ORDER
|
||||
|
||||
# Convert signature components to integers
|
||||
r_int = int(signature[0], 16)
|
||||
s_int = int(signature[1], 16)
|
||||
|
||||
# Ensure r and s are in the valid range
|
||||
if not (1 <= r_int < 2**N_ELEMENT_BITS_ECDSA and 1 <= s_int < EC_ORDER):
|
||||
return False
|
||||
|
||||
# Convert public key to integer
|
||||
pub_key_int = int(public_key, 16)
|
||||
|
||||
# Verify the signature
|
||||
return self._verify(msg_hash_int, r_int, s_int, pub_key_int)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def pedersen_hash(self, elements: List[int]) -> bytes:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers.
|
||||
|
||||
This method now uses the full Pedersen hash implementation
|
||||
that follows StarkWare's specification.
|
||||
|
||||
Args:
|
||||
elements: List of integers to hash
|
||||
|
||||
Returns:
|
||||
bytes: The hash result
|
||||
|
||||
Raises:
|
||||
ValueError: If the calculation fails
|
||||
"""
|
||||
try:
|
||||
# Use the full Pedersen hash implementation
|
||||
return pedersen_hash_bytes(*elements)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to calculate Pedersen hash: {str(e)}")
|
||||
|
||||
def _sign(self, msg_hash: int, priv_key: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
msg_hash: The hash of the message to sign as an integer
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The signature as (r, s) integers
|
||||
"""
|
||||
# Choose a valid k. In our version of ECDSA not every k value is valid,
|
||||
# and there is a negligible probability a drawn k cannot be used for signing.
|
||||
# This is why we have this loop.
|
||||
while True:
|
||||
# Use random nonce generation like the Go SDK
|
||||
k = self._generate_random_k()
|
||||
|
||||
# Cannot fail because 0 < k < EC_ORDER and EC_ORDER is prime.
|
||||
x = self._ec_mult(k, EC_GEN)[0]
|
||||
|
||||
# DIFF: in classic ECDSA, we take int(x) % n.
|
||||
r = int(x)
|
||||
if not (1 <= r < 2**N_ELEMENT_BITS_ECDSA):
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
if (msg_hash + r * priv_key) % EC_ORDER == 0:
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
w = self._div_mod(k, msg_hash + r * priv_key, EC_ORDER)
|
||||
if not (1 <= w < 2**N_ELEMENT_BITS_ECDSA):
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
s = self._inv_mod_curve_size(w)
|
||||
return r, s
|
||||
|
||||
def _verify(self, msg_hash: int, r: int, s: int, public_key: int) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
msg_hash: The hash of the message as an integer
|
||||
r: The r component of the signature as an integer
|
||||
s: The s component of the signature as an integer
|
||||
public_key: The public key as an integer
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
# Compute w = s^-1 (mod EC_ORDER).
|
||||
if not (1 <= s < EC_ORDER):
|
||||
return False
|
||||
|
||||
w = self._inv_mod_curve_size(s)
|
||||
|
||||
# Preassumptions:
|
||||
# DIFF: in classic ECDSA, we assert 1 <= r, w <= EC_ORDER-1.
|
||||
# Since r, w < 2**N_ELEMENT_BITS_ECDSA < EC_ORDER, we only need to verify r, w != 0.
|
||||
if not (1 <= r < 2**N_ELEMENT_BITS_ECDSA and 1 <= w < 2**N_ELEMENT_BITS_ECDSA):
|
||||
return False
|
||||
|
||||
if not (0 <= msg_hash < 2**N_ELEMENT_BITS_ECDSA):
|
||||
return False
|
||||
|
||||
# Only the x coordinate of the point is given, check the two possibilities for the y
|
||||
# coordinate.
|
||||
try:
|
||||
y = self._get_y_coordinate(public_key)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Verify it is on the curve.
|
||||
if (y**2 - (public_key**3 + ALPHA * public_key + BETA)) % FIELD_PRIME != 0:
|
||||
return False
|
||||
|
||||
# Try both possible y coordinates.
|
||||
for y_candidate in [y, (-y) % FIELD_PRIME]:
|
||||
public_key_point = (public_key, y_candidate)
|
||||
|
||||
# Signature validation.
|
||||
try:
|
||||
# Calculate u1 = msg_hash * w mod n
|
||||
u1 = (msg_hash * w) % EC_ORDER
|
||||
|
||||
# Calculate u2 = r * w mod n
|
||||
u2 = (r * w) % EC_ORDER
|
||||
|
||||
# Calculate u1*G + u2*Q
|
||||
point1 = self._ec_mult(u1, EC_GEN)
|
||||
point2 = self._ec_mult(u2, public_key_point)
|
||||
point = self._ec_add(point1, point2)
|
||||
|
||||
# The signature is valid if the x-coordinate of the resulting point equals r
|
||||
if point[0] == r:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def _generate_random_k(self) -> int:
|
||||
"""
|
||||
Generate a cryptographically secure random k value.
|
||||
|
||||
Returns:
|
||||
int: The generated k value in range [1, EC_ORDER)
|
||||
"""
|
||||
# Generate a cryptographically secure random number in the range [1, EC_ORDER)
|
||||
# This matches the Go implementation's approach of using random nonces
|
||||
return secrets.randbelow(EC_ORDER - 1) + 1
|
||||
|
||||
def _private_to_stark_key(self, priv_key: int) -> int:
|
||||
"""
|
||||
Convert a private key to a Stark public key.
|
||||
|
||||
Args:
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
int: The public key as an integer
|
||||
"""
|
||||
return self._private_key_to_ec_point_on_stark_curve(priv_key)[0]
|
||||
|
||||
def _private_key_to_ec_point_on_stark_curve(self, priv_key: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Convert a private key to an EC point on the Stark curve.
|
||||
|
||||
Args:
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The EC point as (x, y) coordinates
|
||||
"""
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key = priv_key % EC_ORDER
|
||||
if priv_key == 0:
|
||||
priv_key = 1
|
||||
|
||||
return self._ec_mult(priv_key, EC_GEN)
|
||||
|
||||
def _inv_mod_curve_size(self, x: int) -> int:
|
||||
"""
|
||||
Calculate the modular inverse of x modulo the curve order.
|
||||
|
||||
Args:
|
||||
x: The value to invert
|
||||
|
||||
Returns:
|
||||
int: The modular inverse
|
||||
"""
|
||||
return self._div_mod(1, x, EC_ORDER)
|
||||
|
||||
def _div_mod(self, n: int, m: int, p: int) -> int:
|
||||
"""
|
||||
Calculate (n / m) mod p.
|
||||
|
||||
Args:
|
||||
n: The numerator
|
||||
m: The denominator
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
int: The result of the division modulo p
|
||||
"""
|
||||
return (n * pow(m, -1, p)) % p
|
||||
|
||||
def _is_quad_residue(self, n: int, p: int) -> bool:
|
||||
"""
|
||||
Check if n is a quadratic residue modulo p.
|
||||
|
||||
Args:
|
||||
n: The number to check
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
bool: True if n is a quadratic residue modulo p, False otherwise
|
||||
"""
|
||||
return pow(n, (p - 1) // 2, p) == 1
|
||||
|
||||
def _sqrt_mod(self, n: int, p: int) -> int:
|
||||
"""
|
||||
Calculate the square root of n modulo p.
|
||||
|
||||
Args:
|
||||
n: The number to take the square root of
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
int: The square root of n modulo p
|
||||
"""
|
||||
# Handle the case where p = 3 mod 4
|
||||
if p % 4 == 3:
|
||||
return pow(n, (p + 1) // 4, p)
|
||||
|
||||
# Handle the general case using the Tonelli-Shanks algorithm
|
||||
q = p - 1
|
||||
s = 0
|
||||
while q % 2 == 0:
|
||||
q //= 2
|
||||
s += 1
|
||||
|
||||
# Find a non-residue
|
||||
z = 2
|
||||
while self._is_quad_residue(z, p):
|
||||
z += 1
|
||||
|
||||
m = s
|
||||
c = pow(z, q, p)
|
||||
t = pow(n, q, p)
|
||||
r = pow(n, (q + 1) // 2, p)
|
||||
|
||||
while t != 1:
|
||||
# Find the least i, 0 < i < m, such that t^(2^i) = 1
|
||||
i = 0
|
||||
t_sq = t
|
||||
while t_sq != 1 and i < m - 1:
|
||||
t_sq = (t_sq * t_sq) % p
|
||||
i += 1
|
||||
|
||||
# Calculate b = c^(2^(m-i-1))
|
||||
b = pow(c, 2**(m - i - 1), p)
|
||||
|
||||
m = i
|
||||
c = (b * b) % p
|
||||
t = (t * b * b) % p
|
||||
r = (r * b) % p
|
||||
|
||||
return r
|
||||
|
||||
def _get_y_coordinate(self, x: int) -> int:
|
||||
"""
|
||||
Given the x coordinate of a point, returns a possible y coordinate such that
|
||||
together the point (x,y) is on the curve.
|
||||
|
||||
Args:
|
||||
x: The x coordinate
|
||||
|
||||
Returns:
|
||||
int: A possible y coordinate
|
||||
|
||||
Raises:
|
||||
ValueError: If x is not a valid x coordinate on the curve
|
||||
"""
|
||||
y_squared = (x * x * x + ALPHA * x + BETA) % FIELD_PRIME
|
||||
if not self._is_quad_residue(y_squared, FIELD_PRIME):
|
||||
raise ValueError("Given x coordinate does not represent any point on the elliptic curve.")
|
||||
|
||||
return self._sqrt_mod(y_squared, FIELD_PRIME)
|
||||
|
||||
def _ec_add(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Add two points on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p1: The first point as (x, y) coordinates
|
||||
p2: The second point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if p1[0] == p2[0]:
|
||||
if (p1[1] + p2[1]) % FIELD_PRIME == 0:
|
||||
# The points are negatives of each other, return the point at infinity
|
||||
# We represent the point at infinity as None, but this should never happen
|
||||
# in our use case, so we raise an exception instead
|
||||
raise ValueError("Points are negatives of each other")
|
||||
|
||||
# The points are the same, so we're doubling
|
||||
return self._ec_double(p1)
|
||||
|
||||
# Calculate the slope
|
||||
slope = self._div_mod(p2[1] - p1[1], p2[0] - p1[0], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - p1[0] - p2[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p1[0] - x3) - p1[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
def _ec_double(self, p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Double a point on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p: The point to double as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
# Calculate the slope
|
||||
slope = self._div_mod(3 * p[0] * p[0] + ALPHA, 2 * p[1], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - 2 * p[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p[0] - x3) - p[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
def _ec_mult(self, m: int, p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Multiply a point on the elliptic curve by a scalar.
|
||||
|
||||
Args:
|
||||
m: The scalar
|
||||
p: The point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if m == 0:
|
||||
raise ValueError("Cannot multiply by 0")
|
||||
|
||||
if m == 1:
|
||||
return p
|
||||
|
||||
if m % 2 == 0:
|
||||
return self._ec_mult(m // 2, self._ec_double(p))
|
||||
else:
|
||||
return self._ec_add(p, self._ec_mult(m - 1, p))
|
||||
@@ -0,0 +1,96 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for metadata-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the metadata client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_metadata(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the exchange metadata.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The exchange metadata
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/meta/getMetaData"
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
|
||||
async def get_server_time(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the current server time.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The server time information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/meta/getServerTime"
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
@@ -0,0 +1,343 @@
|
||||
import math
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
from .types import (
|
||||
CreateOrderParams,
|
||||
CancelOrderParams,
|
||||
GetActiveOrderParams,
|
||||
OrderFillTransactionParams,
|
||||
TimeInForce,
|
||||
OrderType
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for order-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the order client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def create_order(self, params: CreateOrderParams, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new order with the given parameters.
|
||||
|
||||
Args:
|
||||
params: Order parameters
|
||||
metadata: Exchange metadata
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
|
||||
Raises:
|
||||
ValueError: If required parameters are missing or invalid
|
||||
"""
|
||||
# Set default TimeInForce based on order type if not specified
|
||||
if not params.time_in_force:
|
||||
if params.type == OrderType.MARKET:
|
||||
params.time_in_force = TimeInForce.IMMEDIATE_OR_CANCEL
|
||||
elif params.type == OrderType.LIMIT:
|
||||
params.time_in_force = TimeInForce.GOOD_TIL_CANCEL
|
||||
|
||||
# Find the contract from metadata
|
||||
contract = None
|
||||
contract_list = metadata.get("contractList", [])
|
||||
for c in contract_list:
|
||||
if c.get("contractId") == params.contract_id:
|
||||
contract = c
|
||||
break
|
||||
|
||||
if not contract:
|
||||
raise ValueError(f"contract not found: {params.contract_id}")
|
||||
|
||||
# Get collateral coin from metadata
|
||||
global_data = metadata.get("global", {})
|
||||
collateral_coin = global_data.get("starkExCollateralCoin", {})
|
||||
|
||||
# Parse decimal values
|
||||
try:
|
||||
size = Decimal(params.size)
|
||||
price = Decimal(params.price)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse size or price")
|
||||
|
||||
# Convert hex resolution to decimal
|
||||
hex_resolution = contract.get("starkExResolution", "0x0")
|
||||
# Remove "0x" prefix if present
|
||||
hex_resolution = hex_resolution.replace("0x", "")
|
||||
# Parse hex string to int
|
||||
try:
|
||||
resolution_int = int(hex_resolution, 16)
|
||||
resolution = Decimal(resolution_int)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse hex resolution")
|
||||
|
||||
client_order_id = params.client_order_id or self.async_client.generate_uuid()
|
||||
|
||||
# Calculate values
|
||||
value_dm = price * size
|
||||
amount_synthetic = int(size * resolution)
|
||||
amount_collateral = int(value_dm * Decimal("1000000")) # Shift 6 decimal places
|
||||
|
||||
# Calculate fee based on order type (maker/taker)
|
||||
try:
|
||||
fee_rate = Decimal(contract.get("defaultTakerFeeRate", "0"))
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse fee rate")
|
||||
|
||||
# Calculate fee amount in decimal with ceiling to integer
|
||||
amount_fee_dm = Decimal(str(math.ceil(float(value_dm * fee_rate))))
|
||||
amount_fee_str = str(amount_fee_dm)
|
||||
|
||||
# Convert to the required integer format for the protocol
|
||||
amount_fee = int(amount_fee_dm * Decimal("1000000")) # Shift 6 decimal places
|
||||
|
||||
nonce = self.async_client.calc_nonce(client_order_id)
|
||||
l2_expire_time = int(time.time() * 1000) + (14 * 24 * 60 * 60 * 1000) # 14 days
|
||||
|
||||
# Calculate signature using asset IDs from metadata
|
||||
expire_time_unix = l2_expire_time // (60 * 60 * 1000)
|
||||
|
||||
sig_hash = self.async_client.calc_limit_order_hash(
|
||||
contract.get("starkExSyntheticAssetId", ""),
|
||||
collateral_coin.get("starkExAssetId", ""),
|
||||
collateral_coin.get("starkExAssetId", ""),
|
||||
params.side.value == "BUY",
|
||||
amount_synthetic,
|
||||
amount_collateral,
|
||||
amount_fee,
|
||||
nonce,
|
||||
self.async_client.get_account_id(),
|
||||
expire_time_unix
|
||||
)
|
||||
|
||||
# Sign the order
|
||||
sig = self.async_client.sign(sig_hash)
|
||||
|
||||
# Convert signature to string (include v component like Go SDK, even though it's empty)
|
||||
sig_str = f"{sig.r}{sig.s}{sig.v if hasattr(sig, 'v') and sig.v else ''}"
|
||||
|
||||
|
||||
|
||||
# Create order request
|
||||
account_id = str(self.async_client.get_account_id())
|
||||
nonce_str = str(nonce)
|
||||
l2_expire_time_str = str(l2_expire_time)
|
||||
expire_time_str = str(l2_expire_time - 864000000) # 10 days earlier
|
||||
value_str = str(value_dm)
|
||||
|
||||
price_str = params.price if params.type == OrderType.LIMIT else "0"
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"contractId": params.contract_id,
|
||||
"price": price_str,
|
||||
"size": params.size,
|
||||
"type": params.type.value, # Use .value to get the string value
|
||||
"timeInForce": params.time_in_force.value, # Use .value to get the string value
|
||||
"side": params.side.value, # Use .value to get the string value
|
||||
"l2Signature": sig_str,
|
||||
"l2Nonce": nonce_str,
|
||||
"l2ExpireTime": l2_expire_time_str,
|
||||
"l2Value": value_str,
|
||||
"l2Size": params.size,
|
||||
"l2LimitFee": amount_fee_str,
|
||||
"clientOrderId": client_order_id,
|
||||
"expireTime": expire_time_str,
|
||||
"reduceOnly": params.reduce_only
|
||||
}
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/order/createOrder",
|
||||
data=request_data
|
||||
)
|
||||
|
||||
async def cancel_order(self, params: CancelOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel a specific order.
|
||||
|
||||
Args:
|
||||
params: Cancel order parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cancellation result
|
||||
|
||||
Raises:
|
||||
ValueError: If required parameters are missing or invalid
|
||||
"""
|
||||
account_id = str(self.async_client.get_account_id())
|
||||
|
||||
if params.order_id:
|
||||
path = "/api/v1/private/order/cancelOrderById"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"orderIdList": [params.order_id]
|
||||
}
|
||||
elif params.client_id:
|
||||
path = "/api/v1/private/order/cancelOrderByClientOrderId"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"clientOrderIdList": [params.client_id]
|
||||
}
|
||||
elif params.contract_id:
|
||||
path = "/api/v1/private/order/cancelAllOrder"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"filterContractIdList": [params.contract_id]
|
||||
}
|
||||
else:
|
||||
raise ValueError("must provide either order_id, client_id, or contract_id")
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path=path,
|
||||
data=request_data
|
||||
)
|
||||
|
||||
async def get_active_orders(self, params: GetActiveOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get active orders with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Active order query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The active orders
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build query parameters
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
if params.filter_type_list:
|
||||
query_params["filterTypeList"] = ",".join(params.filter_type_list)
|
||||
if params.filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(params.filter_status_list)
|
||||
|
||||
# Add boolean filters
|
||||
if params.filter_is_liquidate is not None:
|
||||
query_params["filterIsLiquidateList"] = str(params.filter_is_liquidate).lower()
|
||||
if params.filter_is_deleverage is not None:
|
||||
query_params["filterIsDeleverageList"] = str(params.filter_is_deleverage).lower()
|
||||
if params.filter_is_position_tpsl is not None:
|
||||
query_params["filterIsPositionTpslList"] = str(params.filter_is_position_tpsl).lower()
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/order/getActiveOrderPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_order_fill_transactions(self, params: OrderFillTransactionParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get order fill transactions with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Order fill transaction query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order fill transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build query parameters
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
if params.filter_order_id_list:
|
||||
query_params["filterOrderIdList"] = ",".join(params.filter_order_id_list)
|
||||
|
||||
# Add boolean filters
|
||||
if params.filter_is_liquidate is not None:
|
||||
query_params["filterIsLiquidateList"] = str(params.filter_is_liquidate).lower()
|
||||
if params.filter_is_deleverage is not None:
|
||||
query_params["filterIsDeleverageList"] = str(params.filter_is_deleverage).lower()
|
||||
if params.filter_is_position_tpsl is not None:
|
||||
query_params["filterIsPositionTpslList"] = str(params.filter_is_position_tpsl).lower()
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/order/getHistoryOrderFillTransactionPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_max_order_size(self, contract_id: str, price: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the maximum order size for a given contract and price.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
price: The price
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The maximum order size information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build request body (API expects POST with JSON body)
|
||||
data = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"contractId": contract_id,
|
||||
"price": str(price)
|
||||
}
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/order/getMaxCreateOrderSize",
|
||||
data=data
|
||||
)
|
||||
@@ -0,0 +1,165 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
class TimeInForce(str, Enum):
|
||||
"""Time in force options for orders."""
|
||||
UNKNOWN_TIME_IN_FORCE = "UNKNOWN_TIME_IN_FORCE"
|
||||
GOOD_TIL_CANCEL = "GOOD_TIL_CANCEL"
|
||||
FILL_OR_KILL = "FILL_OR_KILL"
|
||||
IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL"
|
||||
POST_ONLY = "POST_ONLY"
|
||||
|
||||
|
||||
class OrderSide(str, Enum):
|
||||
"""Order side options."""
|
||||
BUY = "BUY"
|
||||
SELL = "SELL"
|
||||
|
||||
|
||||
class ResponseCode(str, Enum):
|
||||
"""API response codes."""
|
||||
SUCCESS = "SUCCESS"
|
||||
|
||||
|
||||
class OrderType(str, Enum):
|
||||
"""Order type options."""
|
||||
UNKNOWN = "UNKNOWN_ORDER_TYPE"
|
||||
LIMIT = "LIMIT"
|
||||
MARKET = "MARKET"
|
||||
STOP_LIMIT = "STOP_LIMIT"
|
||||
STOP_MARKET = "STOP_MARKET"
|
||||
TAKE_PROFIT_LIMIT = "TAKE_PROFIT_LIMIT"
|
||||
TAKE_PROFIT_MARKET = "TAKE_PROFIT_MARKET"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderFilterParams:
|
||||
"""Common filter types used across different order APIs."""
|
||||
filter_coin_id_list: List[str] = None # Filter by coin IDs, empty means all coins
|
||||
filter_contract_id_list: List[str] = None # Filter by contract IDs, empty means all contracts
|
||||
filter_type_list: List[str] = None # Filter by order types
|
||||
filter_status_list: List[str] = None # Filter by order statuses
|
||||
filter_is_liquidate: Optional[bool] = None # Filter by liquidation status
|
||||
filter_is_deleverage: Optional[bool] = None # Filter by deleverage status
|
||||
filter_is_position_tpsl: Optional[bool] = None # Filter by position take-profit/stop-loss status
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize empty lists."""
|
||||
if self.filter_coin_id_list is None:
|
||||
self.filter_coin_id_list = []
|
||||
if self.filter_contract_id_list is None:
|
||||
self.filter_contract_id_list = []
|
||||
if self.filter_type_list is None:
|
||||
self.filter_type_list = []
|
||||
if self.filter_status_list is None:
|
||||
self.filter_status_list = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaginationParams:
|
||||
"""Common pagination parameters."""
|
||||
size: str = "" # Size of the page, must be greater than 0 and less than or equal to 100/200
|
||||
offset_data: str = "" # Offset data for pagination. Empty string gets the first page
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderFillTransactionParams(PaginationParams, OrderFilterParams):
|
||||
"""Parameters for getting order fill transactions."""
|
||||
filter_order_id_list: List[str] = None # Filter by order IDs, empty means all orders
|
||||
filter_start_created_time_inclusive: int = 0 # Filter start time (inclusive), 0 means from earliest
|
||||
filter_end_created_time_exclusive: int = 0 # Filter end time (exclusive), 0 means until latest
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize empty lists."""
|
||||
super().__post_init__()
|
||||
if self.filter_order_id_list is None:
|
||||
self.filter_order_id_list = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetActiveOrderParams(PaginationParams, OrderFilterParams):
|
||||
"""Parameters for getting active orders."""
|
||||
filter_start_created_time_inclusive: int = 0 # Filter start time (inclusive), 0 means from earliest
|
||||
filter_end_created_time_exclusive: int = 0 # Filter end time (exclusive), 0 means until latest
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetHistoryOrderParams(PaginationParams, OrderFilterParams):
|
||||
"""Parameters for getting historical orders."""
|
||||
filter_start_created_time_inclusive: int = 0 # Filter start time (inclusive), 0 means from earliest
|
||||
filter_end_created_time_exclusive: int = 0 # Filter end time (exclusive), 0 means until latest
|
||||
|
||||
|
||||
@dataclass
|
||||
class CreateOrderParams:
|
||||
"""Parameters for creating an order."""
|
||||
contract_id: str
|
||||
price: str
|
||||
size: str
|
||||
type: OrderType
|
||||
side: str
|
||||
client_order_id: Optional[str] = None
|
||||
l2_expire_time: Optional[int] = None
|
||||
time_in_force: Optional[str] = None
|
||||
reduce_only: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class CancelOrderParams:
|
||||
"""Parameters for canceling orders."""
|
||||
order_id: str = "" # Order ID to cancel
|
||||
client_id: str = "" # Client order ID to cancel
|
||||
contract_id: str = "" # Contract ID for canceling all orders
|
||||
|
||||
|
||||
class OrderResponse:
|
||||
"""Response from creating an order."""
|
||||
code: str
|
||||
data: Dict[str, Any]
|
||||
error_param: Optional[Dict[str, Any]]
|
||||
request_time: str
|
||||
response_time: str
|
||||
trace_id: str
|
||||
|
||||
def __init__(self, response_data: Dict[str, Any]):
|
||||
"""Initialize from response data."""
|
||||
self.code = response_data.get("code", "")
|
||||
self.data = response_data.get("data", {})
|
||||
self.error_param = response_data.get("errorParam")
|
||||
self.request_time = response_data.get("requestTime", "")
|
||||
self.response_time = response_data.get("responseTime", "")
|
||||
self.trace_id = response_data.get("traceId", "")
|
||||
|
||||
|
||||
class MaxOrderSizeResponse(OrderResponse):
|
||||
"""Response from getting max order size."""
|
||||
pass
|
||||
|
||||
|
||||
class OrderListResponse(OrderResponse):
|
||||
"""Response from getting a list of orders."""
|
||||
pass
|
||||
|
||||
|
||||
class OrderPageResponse(OrderResponse):
|
||||
"""Response from getting paginated orders."""
|
||||
pass
|
||||
|
||||
|
||||
class OrderFillTransactionResponse(OrderResponse):
|
||||
"""Response from getting order fill transactions."""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderFillFilterParams(OrderFilterParams):
|
||||
"""Parameters for filtering order fill transactions."""
|
||||
filter_order_id_list: List[str] = None # Filter by order IDs, empty means all orders
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize empty lists."""
|
||||
super().__post_init__()
|
||||
if self.filter_order_id_list is None:
|
||||
self.filter_order_id_list = []
|
||||
@@ -0,0 +1,312 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class GetKLineParams:
|
||||
"""Parameters for getting K-line data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
contract_id: str,
|
||||
interval: str,
|
||||
size: str = "",
|
||||
offset_data: str = "",
|
||||
filter_start_time_inclusive: int = 0,
|
||||
filter_end_time_exclusive: int = 0
|
||||
):
|
||||
self.contract_id = contract_id
|
||||
self.interval = interval
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_start_time_inclusive = filter_start_time_inclusive
|
||||
self.filter_end_time_exclusive = filter_end_time_exclusive
|
||||
|
||||
|
||||
class GetOrderBookDepthParams:
|
||||
"""Parameters for getting order book depth."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
contract_id: str,
|
||||
limit: int = 50
|
||||
):
|
||||
self.contract_id = contract_id
|
||||
self.limit = limit
|
||||
|
||||
|
||||
class GetMultiContractKLineParams:
|
||||
"""Parameters for getting K-line data for multiple contracts."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
contract_id_list: List[str],
|
||||
interval: str,
|
||||
limit: int = 1
|
||||
):
|
||||
self.contract_id_list = contract_id_list
|
||||
self.interval = interval
|
||||
self.limit = limit
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for quote-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the quote client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_quote_summary(self, contract_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the quote summary for a given contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The quote summary
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getTicketSummary"
|
||||
params = {
|
||||
"contractId": contract_id
|
||||
}
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url, params=params) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
|
||||
async def get_24_hour_quote(self, contract_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the 24-hour quotes for a given contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The 24-hour quotes
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getTicker"
|
||||
params = {
|
||||
"contractId": contract_id
|
||||
}
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url, params=params) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
|
||||
async def get_k_line(self, params: GetKLineParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the K-line data for a contract.
|
||||
|
||||
Args:
|
||||
params: K-line query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The K-line data
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getKline"
|
||||
query_params = {
|
||||
"contractId": params.contract_id,
|
||||
"interval": params.interval
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_time_inclusive > 0:
|
||||
query_params["filterStartTimeInclusive"] = str(params.filter_start_time_inclusive)
|
||||
if params.filter_end_time_exclusive > 0:
|
||||
query_params["filterEndTimeExclusive"] = str(params.filter_end_time_exclusive)
|
||||
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getKline"
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url, params=query_params) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
|
||||
async def get_order_book_depth(self, params: GetOrderBookDepthParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the order book depth for a contract.
|
||||
|
||||
Args:
|
||||
params: Order book depth query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order book depth
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getDepth"
|
||||
query_params = {
|
||||
"contractId": params.contract_id,
|
||||
"level": str(params.limit) # The API expects 'level', not 'limit'
|
||||
}
|
||||
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getDepth"
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url, params=query_params) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
|
||||
async def get_multi_contract_k_line(self, params: GetMultiContractKLineParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the K-line data for multiple contracts.
|
||||
|
||||
Args:
|
||||
params: Multi-contract K-line query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The K-line data for multiple contracts
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Public endpoint - use simple GET request
|
||||
await self.async_client._ensure_session()
|
||||
|
||||
url = f"{self.async_client.base_url}/api/v1/public/quote/getMultiContractKline"
|
||||
query_params = {
|
||||
"contractIdList": ",".join(params.contract_id_list),
|
||||
"interval": params.interval,
|
||||
"limit": str(params.limit)
|
||||
}
|
||||
|
||||
try:
|
||||
async with self.async_client.session.get(url, params=query_params) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except:
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"request failed: {str(e)}")
|
||||
@@ -0,0 +1,288 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
|
||||
|
||||
class GetTransferOutByIdParams:
|
||||
"""Parameters for getting transfer out records by ID."""
|
||||
|
||||
def __init__(self, transfer_id_list: List[str]):
|
||||
self.transfer_id_list = transfer_id_list
|
||||
|
||||
|
||||
class GetTransferInByIdParams:
|
||||
"""Parameters for getting transfer in records by ID."""
|
||||
|
||||
def __init__(self, transfer_id_list: List[str]):
|
||||
self.transfer_id_list = transfer_id_list
|
||||
|
||||
|
||||
class GetWithdrawAvailableAmountParams:
|
||||
"""Parameters for getting available withdrawal amount."""
|
||||
|
||||
def __init__(self, coin_id: str):
|
||||
self.coin_id = coin_id
|
||||
|
||||
|
||||
class CreateTransferOutParams:
|
||||
"""Parameters for creating a transfer out order."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coin_id: str,
|
||||
amount: str,
|
||||
address: str,
|
||||
network: str,
|
||||
memo: str = "",
|
||||
client_order_id: str = None
|
||||
):
|
||||
self.coin_id = coin_id
|
||||
self.amount = amount
|
||||
self.address = address
|
||||
self.network = network
|
||||
self.memo = memo
|
||||
self.client_order_id = client_order_id
|
||||
|
||||
|
||||
class GetTransferOutPageParams:
|
||||
"""Parameters for getting transfer out page."""
|
||||
|
||||
def __init__(self, size: str = "10", offset_data: str = "", filter_coin_id_list: List[str] = None,
|
||||
filter_status_list: List[str] = None, filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_coin_id_list = filter_coin_id_list or []
|
||||
self.filter_status_list = filter_status_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class GetTransferInPageParams:
|
||||
"""Parameters for getting transfer in page."""
|
||||
|
||||
def __init__(self, size: str = "10", offset_data: str = "", filter_coin_id_list: List[str] = None,
|
||||
filter_status_list: List[str] = None, filter_start_created_time_inclusive: int = 0,
|
||||
filter_end_created_time_exclusive: int = 0):
|
||||
self.size = size
|
||||
self.offset_data = offset_data
|
||||
self.filter_coin_id_list = filter_coin_id_list or []
|
||||
self.filter_status_list = filter_status_list or []
|
||||
self.filter_start_created_time_inclusive = filter_start_created_time_inclusive
|
||||
self.filter_end_created_time_exclusive = filter_end_created_time_exclusive
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for transfer-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the transfer client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def get_transfer_out_by_id(self, params: GetTransferOutByIdParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get transfer out records by ID.
|
||||
|
||||
Args:
|
||||
params: Transfer out query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The transfer out records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"transferIdList": ",".join(params.transfer_id_list)
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/transfer/getTransferOutById",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_transfer_in_by_id(self, params: GetTransferInByIdParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get transfer in records by ID.
|
||||
|
||||
Args:
|
||||
params: Transfer in query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The transfer in records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"transferIdList": ",".join(params.transfer_id_list)
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/transfer/getTransferInById",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_withdraw_available_amount(self, params: GetWithdrawAvailableAmountParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the available withdrawal amount.
|
||||
|
||||
Args:
|
||||
params: Withdrawal available amount query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The available withdrawal amount
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"coinId": params.coin_id
|
||||
}
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/transfer/getTransferOutAvailableAmount",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def create_transfer_out(self, params: CreateTransferOutParams, metadata: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new transfer out order.
|
||||
|
||||
Args:
|
||||
params: Transfer out parameters
|
||||
metadata: Exchange metadata (optional, not used in current implementation)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created transfer out order
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
client_order_id = params.client_order_id or self.async_client.generate_uuid()
|
||||
|
||||
data = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"coinId": params.coin_id,
|
||||
"amount": params.amount,
|
||||
"address": params.address,
|
||||
"network": params.network,
|
||||
"clientOrderId": client_order_id
|
||||
}
|
||||
|
||||
if params.memo:
|
||||
data["memo"] = params.memo
|
||||
|
||||
# TODO: Implement signature calculation for transfer out
|
||||
# This would require:
|
||||
# 1. Asset ID from metadata based on coin_id
|
||||
# 2. Receiver public key from address
|
||||
# 3. Position IDs for sender, receiver, and fee
|
||||
# 4. Proper expiration time calculation
|
||||
# 5. Call to calc_transfer_hash and sign the result
|
||||
# For now, the API call is made without signature (may fail on actual server)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/transfer/createTransferOut",
|
||||
data=data
|
||||
)
|
||||
|
||||
async def get_transfer_out_page(
|
||||
self,
|
||||
params: GetTransferOutPageParams
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get transfer out records with pagination.
|
||||
|
||||
Args:
|
||||
params: Parameters for the request
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The transfer out records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(params.filter_status_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/transfer/getActiveTransferOut",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_transfer_in_page(
|
||||
self,
|
||||
params: GetTransferInPageParams
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get transfer in records with pagination.
|
||||
|
||||
Args:
|
||||
params: Parameters for the request
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The transfer in records
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(params.filter_status_list)
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/transfer/getActiveTransferIn",
|
||||
params=query_params
|
||||
)
|
||||
@@ -0,0 +1,302 @@
|
||||
import asyncio
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional, Callable, Union
|
||||
|
||||
import websocket
|
||||
from Crypto.Hash import keccak
|
||||
|
||||
from ..internal.signing_adapter import SigningAdapter
|
||||
|
||||
from ..internal.client import Client as InternalClient
|
||||
|
||||
|
||||
class Client:
|
||||
"""WebSocket client for real-time data."""
|
||||
|
||||
def __init__(self, url: str, is_private: bool, account_id: int, stark_pri_key: str, signing_adapter: Optional[SigningAdapter] = None):
|
||||
"""
|
||||
Initialize the WebSocket client.
|
||||
|
||||
Args:
|
||||
url: WebSocket URL
|
||||
is_private: Whether this is a private WebSocket connection
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
"""
|
||||
self.url = url
|
||||
self.is_private = is_private
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use the provided signing adapter (required)
|
||||
if signing_adapter is None:
|
||||
raise ValueError("signing_adapter is required")
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
self.conn = None
|
||||
self.handlers = {}
|
||||
self.done = threading.Event()
|
||||
self.ping_thread = None
|
||||
self.subscriptions = set()
|
||||
self.on_connect_hooks = []
|
||||
self.on_message_hooks = []
|
||||
self.on_disconnect_hooks = []
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Establish a WebSocket connection.
|
||||
|
||||
Raises:
|
||||
ValueError: If the connection fails
|
||||
"""
|
||||
headers = {}
|
||||
url = self.url
|
||||
|
||||
# Add timestamp parameter for both public and private connections
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
if self.is_private:
|
||||
# Add timestamp header
|
||||
headers["X-edgeX-Api-Timestamp"] = str(timestamp)
|
||||
|
||||
# Generate signature content (no ? separator, matching Go SDK)
|
||||
path = f"/api/v1/private/wsaccountId={self.account_id}"
|
||||
sign_content = f"{timestamp}GET{path}"
|
||||
|
||||
# Hash the content
|
||||
keccak_hash = keccak.new(digest_bits=256)
|
||||
keccak_hash.update(sign_content.encode())
|
||||
message_hash = keccak_hash.digest()
|
||||
|
||||
# Sign the message using the signing adapter
|
||||
try:
|
||||
r, s = self.signing_adapter.sign(message_hash, self.stark_pri_key)
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to sign message: {str(e)}")
|
||||
|
||||
# Set signature header
|
||||
headers["X-edgeX-Api-Signature"] = f"{r}{s}"
|
||||
else:
|
||||
# For public connections, add timestamp as URL parameter
|
||||
separator = "&" if "?" in url else "?"
|
||||
url = f"{url}{separator}timestamp={timestamp}"
|
||||
|
||||
# Create WebSocket connection
|
||||
try:
|
||||
self.conn = websocket.create_connection(url, header=headers)
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to connect to WebSocket: {str(e)}")
|
||||
|
||||
# Start ping thread
|
||||
self.done.clear()
|
||||
self.ping_thread = threading.Thread(target=self._ping_loop)
|
||||
self.ping_thread.daemon = True
|
||||
self.ping_thread.start()
|
||||
|
||||
# Start message handling thread
|
||||
self.message_thread = threading.Thread(target=self._handle_messages)
|
||||
self.message_thread.daemon = True
|
||||
self.message_thread.start()
|
||||
|
||||
# Call connect hooks
|
||||
for hook in self.on_connect_hooks:
|
||||
hook()
|
||||
|
||||
def close(self):
|
||||
"""Close the WebSocket connection."""
|
||||
self.done.set()
|
||||
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def _ping_loop(self):
|
||||
"""Send periodic ping messages."""
|
||||
while not self.done.is_set():
|
||||
if self.conn:
|
||||
ping_msg = {
|
||||
"type": "ping",
|
||||
"time": str(int(time.time() * 1000))
|
||||
}
|
||||
|
||||
try:
|
||||
self.conn.send(json.dumps(ping_msg))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to send ping: {str(e)}")
|
||||
break
|
||||
|
||||
# Wait for 30 seconds or until done
|
||||
self.done.wait(30)
|
||||
|
||||
def _handle_messages(self):
|
||||
"""Process incoming WebSocket messages."""
|
||||
while not self.done.is_set():
|
||||
if not self.conn:
|
||||
break
|
||||
|
||||
try:
|
||||
message = self.conn.recv()
|
||||
|
||||
# Call message hooks
|
||||
for hook in self.on_message_hooks:
|
||||
hook(message)
|
||||
|
||||
# Parse message
|
||||
try:
|
||||
msg = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Handle ping messages
|
||||
if msg.get("type") == "ping":
|
||||
self._handle_pong(msg.get("time", ""))
|
||||
continue
|
||||
|
||||
# Handle quote events
|
||||
if msg.get("type") == "quote-event":
|
||||
channel = msg.get("channel", "")
|
||||
channel_type = channel.split(".")[0] if "." in channel else channel
|
||||
|
||||
if channel_type in self.handlers:
|
||||
self.handlers[channel_type](message)
|
||||
continue
|
||||
|
||||
# Call registered handlers for other message types
|
||||
msg_type = msg.get("type", "")
|
||||
if msg_type in self.handlers:
|
||||
self.handlers[msg_type](message)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling message: {str(e)}")
|
||||
|
||||
# Call disconnect hooks
|
||||
for hook in self.on_disconnect_hooks:
|
||||
hook(e)
|
||||
|
||||
break
|
||||
|
||||
def _handle_pong(self, timestamp: str):
|
||||
"""
|
||||
Send pong response to server ping.
|
||||
|
||||
Args:
|
||||
timestamp: The timestamp from the ping message
|
||||
"""
|
||||
pong_msg = {
|
||||
"type": "pong",
|
||||
"time": timestamp
|
||||
}
|
||||
|
||||
try:
|
||||
self.conn.send(json.dumps(pong_msg))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to send pong: {str(e)}")
|
||||
|
||||
def subscribe(self, topic: str, params: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
Subscribe to a topic (for public WebSocket).
|
||||
|
||||
Args:
|
||||
topic: The topic to subscribe to
|
||||
params: Optional parameters for the subscription
|
||||
|
||||
Returns:
|
||||
bool: Whether the subscription was successful
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
if self.is_private:
|
||||
raise ValueError("cannot subscribe on private WebSocket connection")
|
||||
|
||||
if not self.conn:
|
||||
raise ValueError("WebSocket connection is not established")
|
||||
|
||||
sub_msg = {
|
||||
"type": "subscribe",
|
||||
"channel": topic
|
||||
}
|
||||
|
||||
if params:
|
||||
sub_msg.update(params)
|
||||
|
||||
try:
|
||||
self.conn.send(json.dumps(sub_msg))
|
||||
self.subscriptions.add(topic)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to subscribe: {str(e)}")
|
||||
|
||||
def unsubscribe(self, topic: str) -> bool:
|
||||
"""
|
||||
Unsubscribe from a topic (for public WebSocket).
|
||||
|
||||
Args:
|
||||
topic: The topic to unsubscribe from
|
||||
|
||||
Returns:
|
||||
bool: Whether the unsubscription was successful
|
||||
|
||||
Raises:
|
||||
ValueError: If the unsubscription fails
|
||||
"""
|
||||
if self.is_private:
|
||||
raise ValueError("cannot unsubscribe on private WebSocket connection")
|
||||
|
||||
if not self.conn:
|
||||
raise ValueError("WebSocket connection is not established")
|
||||
|
||||
unsub_msg = {
|
||||
"type": "unsubscribe",
|
||||
"channel": topic
|
||||
}
|
||||
|
||||
try:
|
||||
self.conn.send(json.dumps(unsub_msg))
|
||||
self.subscriptions.discard(topic)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to unsubscribe: {str(e)}")
|
||||
|
||||
def on_message(self, msg_type: str, handler: Callable[[str], None]):
|
||||
"""
|
||||
Register a handler for a specific message type.
|
||||
|
||||
Args:
|
||||
msg_type: The message type to handle
|
||||
handler: The handler function
|
||||
"""
|
||||
self.handlers[msg_type] = handler
|
||||
|
||||
def on_message_hook(self, hook: Callable[[str], None]):
|
||||
"""
|
||||
Register a hook that will be called for all messages.
|
||||
|
||||
Args:
|
||||
hook: The hook function
|
||||
"""
|
||||
self.on_message_hooks.append(hook)
|
||||
|
||||
def on_connect(self, hook: Callable[[], None]):
|
||||
"""
|
||||
Register a hook that will be called when connection is established.
|
||||
|
||||
Args:
|
||||
hook: The hook function
|
||||
"""
|
||||
self.on_connect_hooks.append(hook)
|
||||
|
||||
def on_disconnect(self, hook: Callable[[Exception], None]):
|
||||
"""
|
||||
Register a hook that will be called when connection is closed.
|
||||
|
||||
Args:
|
||||
hook: The hook function
|
||||
"""
|
||||
self.on_disconnect_hooks.append(hook)
|
||||
@@ -0,0 +1,231 @@
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
|
||||
from ..internal.signing_adapter import SigningAdapter
|
||||
from ..internal.starkex_signing_adapter import StarkExSigningAdapter
|
||||
from .client import Client
|
||||
|
||||
|
||||
class Manager:
|
||||
"""Manager for WebSocket connections."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_pri_key: str, signing_adapter: Optional[SigningAdapter] = None):
|
||||
"""
|
||||
Initialize the WebSocket manager.
|
||||
|
||||
Args:
|
||||
base_url: Base WebSocket URL
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter (defaults to StarkExSigningAdapter)
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use StarkExSigningAdapter as default if none provided
|
||||
if signing_adapter is None:
|
||||
signing_adapter = StarkExSigningAdapter()
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
self.public_client = None
|
||||
self.private_client = None
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_public_client(self) -> Client:
|
||||
"""
|
||||
Get the public WebSocket client.
|
||||
|
||||
Returns:
|
||||
Client: The public WebSocket client
|
||||
"""
|
||||
if not self.public_client:
|
||||
self.public_client = Client(
|
||||
url=f"{self.base_url}/api/v1/public/ws",
|
||||
is_private=False,
|
||||
account_id=self.account_id,
|
||||
stark_pri_key=self.stark_pri_key,
|
||||
signing_adapter=self.signing_adapter
|
||||
)
|
||||
|
||||
return self.public_client
|
||||
|
||||
def get_private_client(self) -> Client:
|
||||
"""
|
||||
Get the private WebSocket client.
|
||||
|
||||
Returns:
|
||||
Client: The private WebSocket client
|
||||
"""
|
||||
if not self.private_client:
|
||||
self.private_client = Client(
|
||||
url=f"{self.base_url}/api/v1/private/ws?accountId={self.account_id}",
|
||||
is_private=True,
|
||||
account_id=self.account_id,
|
||||
stark_pri_key=self.stark_pri_key,
|
||||
signing_adapter=self.signing_adapter
|
||||
)
|
||||
|
||||
return self.private_client
|
||||
|
||||
def connect_public(self):
|
||||
"""
|
||||
Connect to the public WebSocket.
|
||||
|
||||
Raises:
|
||||
ValueError: If the connection fails
|
||||
"""
|
||||
client = self.get_public_client()
|
||||
client.connect()
|
||||
|
||||
def connect_private(self):
|
||||
"""
|
||||
Connect to the private WebSocket.
|
||||
|
||||
Raises:
|
||||
ValueError: If the connection fails
|
||||
"""
|
||||
client = self.get_private_client()
|
||||
client.connect()
|
||||
|
||||
def disconnect_public(self):
|
||||
"""Disconnect from the public WebSocket."""
|
||||
if self.public_client:
|
||||
self.public_client.close()
|
||||
|
||||
def disconnect_private(self):
|
||||
"""Disconnect from the private WebSocket."""
|
||||
if self.private_client:
|
||||
self.private_client.close()
|
||||
|
||||
def disconnect_all(self):
|
||||
"""Disconnect from all WebSockets."""
|
||||
self.disconnect_public()
|
||||
self.disconnect_private()
|
||||
|
||||
def subscribe_ticker(self, contract_id: str, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to ticker updates for a contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_public_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("ticker", handler)
|
||||
|
||||
# Subscribe to ticker channel
|
||||
channel = f"ticker.{contract_id}"
|
||||
client.subscribe(channel)
|
||||
|
||||
def subscribe_kline(self, contract_id: str, interval: str, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to K-line updates for a contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
interval: The K-line interval
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_public_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("kline", handler)
|
||||
|
||||
# Subscribe to kline channel
|
||||
channel = f"kline.{contract_id}.{interval}"
|
||||
client.subscribe(channel)
|
||||
|
||||
def subscribe_depth(self, contract_id: str, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to depth updates for a contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_public_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("depth", handler)
|
||||
|
||||
# Subscribe to depth channel
|
||||
channel = f"depth.{contract_id}"
|
||||
client.subscribe(channel)
|
||||
|
||||
def subscribe_trade(self, contract_id: str, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to trade updates for a contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_public_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("trade", handler)
|
||||
|
||||
# Subscribe to trade channel
|
||||
channel = f"trade.{contract_id}"
|
||||
client.subscribe(channel)
|
||||
|
||||
def subscribe_account_update(self, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to account updates.
|
||||
|
||||
Args:
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_private_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("account", handler)
|
||||
|
||||
def subscribe_order_update(self, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to order updates.
|
||||
|
||||
Args:
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_private_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("order", handler)
|
||||
|
||||
def subscribe_position_update(self, handler: Callable[[str], None]):
|
||||
"""
|
||||
Subscribe to position updates.
|
||||
|
||||
Args:
|
||||
handler: The handler function
|
||||
|
||||
Raises:
|
||||
ValueError: If the subscription fails
|
||||
"""
|
||||
client = self.get_private_client()
|
||||
|
||||
# Register handler
|
||||
client.on_message("position", handler)
|
||||
@@ -0,0 +1,120 @@
|
||||
# EdgeX Python SDK Examples
|
||||
|
||||
This directory contains examples demonstrating how to use the EdgeX Python SDK.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running the examples, make sure you have installed the EdgeX Python SDK:
|
||||
|
||||
```bash
|
||||
pip install edgex-python-sdk
|
||||
```
|
||||
|
||||
Or, if you're working with the source code:
|
||||
|
||||
```bash
|
||||
cd edgex-python-sdk
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The examples use the following environment variables:
|
||||
|
||||
- `EDGEX_BASE_URL`: Base URL for HTTP API endpoints (e.g., "https://pro.edgex.exchange" for production, "https://testnet.edgex.exchange" for testnet)
|
||||
- `EDGEX_WS_URL`: Base URL for WebSocket endpoints (e.g., "wss://quote.edgex.exchange" for production, "wss://quote-testnet.edgex.exchange" for testnet)
|
||||
- `EDGEX_ACCOUNT_ID`: Your account ID
|
||||
- `EDGEX_STARK_PRIVATE_KEY`: Your stark private key
|
||||
|
||||
You can set these variables in your environment or create a `.env` file in the examples directory:
|
||||
|
||||
```
|
||||
EDGEX_BASE_URL=https://pro.edgex.exchange # Use https://testnet.edgex.exchange for testnet
|
||||
EDGEX_WS_URL=wss://quote.edgex.exchange # Use wss://quote-testnet.edgex.exchange for testnet
|
||||
EDGEX_ACCOUNT_ID=12345
|
||||
EDGEX_STARK_PRIVATE_KEY=your-stark-private-key
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
The `basic_usage.py` example demonstrates the basic functionality of the SDK:
|
||||
|
||||
- Creating a client
|
||||
- Getting server time and metadata
|
||||
- Getting account assets and positions
|
||||
- Getting market data (K-lines, order book depth)
|
||||
- Creating orders (commented out to avoid actual order creation)
|
||||
- Using WebSockets for real-time data
|
||||
|
||||
To run the example:
|
||||
|
||||
```bash
|
||||
python basic_usage.py
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
The `advanced_usage.py` example demonstrates more advanced features of the SDK:
|
||||
|
||||
- Order management (creating and canceling orders)
|
||||
- WebSocket integration with proper handlers
|
||||
- Error handling
|
||||
- Pagination
|
||||
- Using a trader class to encapsulate functionality
|
||||
|
||||
To run the example:
|
||||
|
||||
```bash
|
||||
python advanced_usage.py
|
||||
```
|
||||
|
||||
## Contract IDs
|
||||
|
||||
EdgeX uses numeric contract IDs instead of symbol-based identifiers. Here are some common contract mappings:
|
||||
|
||||
| Contract ID | Symbol | Tick Size |
|
||||
|-------------|---------------|-----------|
|
||||
| 10000001 | BTCUSDT | 0.1 |
|
||||
| 10000002 | ETHUSDT | 0.01 |
|
||||
| 10000003 | SOLUSDT | 0.01 |
|
||||
| 10000004 | BNBUSDT | 0.01 |
|
||||
|
||||
To get the complete list of available contracts:
|
||||
|
||||
```python
|
||||
metadata = await client.get_metadata()
|
||||
contracts = metadata.get("data", {}).get("contractList", [])
|
||||
for contract in contracts:
|
||||
print(f"ID: {contract['contractId']} - {contract['contractName']}")
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The examples include order creation code that is commented out to avoid creating actual orders. Uncomment this code if you want to create real orders.
|
||||
- The WebSocket examples will run for a short time and then disconnect. Adjust the sleep time if you want to receive more updates.
|
||||
- The examples use asyncio for asynchronous operations. Make sure you're using Python 3.7 or later.
|
||||
- All examples use numeric contract IDs (e.g., "10000001" for BTCUSDT) as required by the EdgeX API.
|
||||
- For order book depth queries, valid limit values are 15 or 200.
|
||||
|
||||
## Customization
|
||||
|
||||
Feel free to modify the examples to suit your needs. Some ideas:
|
||||
|
||||
- Implement a trading strategy
|
||||
- Add more error handling
|
||||
- Implement a command-line interface
|
||||
- Create a web interface using a framework like Flask or FastAPI
|
||||
- Add logging to a file
|
||||
- Add more sophisticated order management
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check that your environment variables are set correctly
|
||||
2. Verify that you have the latest version of the SDK
|
||||
3. Check the EdgeX API documentation for any changes
|
||||
4. Look for error messages in the console output
|
||||
5. Try with a smaller subset of functionality to isolate the issue
|
||||
@@ -0,0 +1,657 @@
|
||||
"""
|
||||
Advanced usage example for the EdgeX Python SDK.
|
||||
|
||||
This example demonstrates more advanced features of the SDK, including:
|
||||
- Order management
|
||||
- WebSocket integration
|
||||
- Error handling
|
||||
- Pagination
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from edgex_sdk import (
|
||||
Client,
|
||||
OrderSide,
|
||||
OrderType,
|
||||
TimeInForce,
|
||||
CreateOrderParams,
|
||||
CancelOrderParams,
|
||||
GetActiveOrderParams,
|
||||
OrderFillTransactionParams,
|
||||
GetKLineParams,
|
||||
GetOrderBookDepthParams,
|
||||
WebSocketManager
|
||||
)
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdgeXTrader:
|
||||
"""Example trader using the EdgeX Python SDK."""
|
||||
|
||||
def __init__(self, base_url: str, ws_url: str, account_id: int, stark_private_key: str):
|
||||
"""
|
||||
Initialize the trader.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
ws_url: Base URL for WebSocket endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_private_key: Stark private key for signing
|
||||
"""
|
||||
self.client = Client(
|
||||
base_url=base_url,
|
||||
account_id=account_id,
|
||||
stark_private_key=stark_private_key
|
||||
)
|
||||
|
||||
self.ws_manager = WebSocketManager(
|
||||
base_url=ws_url,
|
||||
account_id=account_id,
|
||||
stark_pri_key=stark_private_key
|
||||
)
|
||||
|
||||
self.metadata = None
|
||||
self.contracts = {}
|
||||
self.market_data = {}
|
||||
self.active_orders = {}
|
||||
self.positions = {}
|
||||
self.assets = {}
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the trader by fetching metadata and account information."""
|
||||
logger.info("Initializing trader...")
|
||||
|
||||
try:
|
||||
# Get metadata
|
||||
self.metadata = await self.client.get_metadata()
|
||||
logger.info("Metadata retrieved")
|
||||
|
||||
# Extract contracts
|
||||
contract_list = self.metadata.get("data", {}).get("contractList", [])
|
||||
for contract in contract_list:
|
||||
contract_id = contract.get("contractId")
|
||||
if contract_id:
|
||||
self.contracts[contract_id] = contract
|
||||
|
||||
logger.info(f"Found {len(self.contracts)} contracts")
|
||||
|
||||
# Get account assets
|
||||
assets_response = await self.client.get_account_asset()
|
||||
self.assets = assets_response.get("data", {})
|
||||
logger.info("Account assets retrieved")
|
||||
|
||||
# Get account positions
|
||||
positions_response = await self.client.get_account_positions()
|
||||
positions_data = positions_response.get("data", {})
|
||||
position_list = positions_data.get("positionList", [])
|
||||
for position in position_list:
|
||||
contract_id = position.get("contractId")
|
||||
if contract_id:
|
||||
self.positions[contract_id] = position
|
||||
|
||||
logger.info(f"Found {len(self.positions)} positions")
|
||||
|
||||
# Get active orders
|
||||
await self.update_active_orders()
|
||||
|
||||
# Initialize WebSocket
|
||||
await self.initialize_websocket()
|
||||
|
||||
logger.info("Trader initialized successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize trader: {str(e)}")
|
||||
return False
|
||||
|
||||
async def update_active_orders(self):
|
||||
"""Update the list of active orders."""
|
||||
try:
|
||||
params = GetActiveOrderParams()
|
||||
active_orders_response = await self.client.get_active_orders(params)
|
||||
|
||||
order_list = active_orders_response.get("data", {}).get("list", [])
|
||||
self.active_orders = {}
|
||||
|
||||
for order in order_list:
|
||||
order_id = order.get("orderId")
|
||||
if order_id:
|
||||
self.active_orders[order_id] = order
|
||||
|
||||
logger.info(f"Found {len(self.active_orders)} active orders")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update active orders: {str(e)}")
|
||||
return False
|
||||
|
||||
async def initialize_websocket(self):
|
||||
"""Initialize WebSocket connections and subscriptions."""
|
||||
try:
|
||||
# Connect to public WebSocket
|
||||
self.ws_manager.connect_public()
|
||||
logger.info("Connected to public WebSocket")
|
||||
|
||||
# Connect to private WebSocket
|
||||
self.ws_manager.connect_private()
|
||||
logger.info("Connected to private WebSocket")
|
||||
|
||||
# Subscribe to account updates
|
||||
self.ws_manager.subscribe_account_update(self.handle_account_update)
|
||||
logger.info("Subscribed to account updates")
|
||||
|
||||
# Subscribe to order updates
|
||||
self.ws_manager.subscribe_order_update(self.handle_order_update)
|
||||
logger.info("Subscribed to order updates")
|
||||
|
||||
# Subscribe to position updates
|
||||
self.ws_manager.subscribe_position_update(self.handle_position_update)
|
||||
logger.info("Subscribed to position updates")
|
||||
|
||||
# Subscribe to market data for BTCUSDT (contract ID: 10000001)
|
||||
self.ws_manager.subscribe_ticker("10000001", self.handle_ticker_update)
|
||||
self.ws_manager.subscribe_kline("10000001", "1m", self.handle_kline_update)
|
||||
self.ws_manager.subscribe_depth("10000001", self.handle_depth_update)
|
||||
logger.info("Subscribed to market data for BTCUSDT (10000001)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize WebSocket: {str(e)}")
|
||||
return False
|
||||
|
||||
def handle_account_update(self, message: str):
|
||||
"""
|
||||
Handle account update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
logger.info(f"Account update: {data}")
|
||||
|
||||
# Update assets
|
||||
account_data = data.get("content", {}).get("data", {})
|
||||
if account_data:
|
||||
self.assets = account_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle account update: {str(e)}")
|
||||
|
||||
def handle_order_update(self, message: str):
|
||||
"""
|
||||
Handle order update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
logger.info(f"Order update: {data}")
|
||||
|
||||
# Update active orders
|
||||
asyncio.create_task(self.update_active_orders())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle order update: {str(e)}")
|
||||
|
||||
def handle_position_update(self, message: str):
|
||||
"""
|
||||
Handle position update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
logger.info(f"Position update: {data}")
|
||||
|
||||
# Update positions
|
||||
position_data = data.get("content", {}).get("data", {})
|
||||
contract_id = position_data.get("contractId")
|
||||
|
||||
if contract_id:
|
||||
self.positions[contract_id] = position_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle position update: {str(e)}")
|
||||
|
||||
def handle_ticker_update(self, message: str):
|
||||
"""
|
||||
Handle ticker update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
|
||||
# Extract ticker data
|
||||
content = data.get("content", {})
|
||||
ticker_data_list = content.get("data", [])
|
||||
|
||||
# Handle both single ticker and list of tickers
|
||||
if isinstance(ticker_data_list, list) and ticker_data_list:
|
||||
ticker_data = ticker_data_list[0] # Take the first ticker
|
||||
else:
|
||||
ticker_data = ticker_data_list
|
||||
|
||||
contract_id = ticker_data.get("contractId") if isinstance(ticker_data, dict) else None
|
||||
|
||||
if contract_id:
|
||||
if "ticker" not in self.market_data:
|
||||
self.market_data["ticker"] = {}
|
||||
|
||||
self.market_data["ticker"][contract_id] = ticker_data
|
||||
logger.info(f"Ticker update for {contract_id}: {ticker_data.get('lastPrice')}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle ticker update: {str(e)}")
|
||||
|
||||
def handle_kline_update(self, message: str):
|
||||
"""
|
||||
Handle K-line update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
|
||||
# Extract K-line data
|
||||
kline_data = data.get("content", {}).get("data", {})
|
||||
contract_id = kline_data.get("contractId")
|
||||
interval = kline_data.get("interval")
|
||||
|
||||
if contract_id and interval:
|
||||
if "kline" not in self.market_data:
|
||||
self.market_data["kline"] = {}
|
||||
|
||||
if contract_id not in self.market_data["kline"]:
|
||||
self.market_data["kline"][contract_id] = {}
|
||||
|
||||
self.market_data["kline"][contract_id][interval] = kline_data
|
||||
logger.info(f"K-line update for {contract_id} {interval}: {kline_data.get('close')}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle K-line update: {str(e)}")
|
||||
|
||||
def handle_depth_update(self, message: str):
|
||||
"""
|
||||
Handle depth update messages from WebSocket.
|
||||
|
||||
Args:
|
||||
message: The WebSocket message
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(message)
|
||||
|
||||
# Extract depth data
|
||||
depth_data = data.get("content", {}).get("data", {})
|
||||
contract_id = depth_data.get("contractId")
|
||||
|
||||
if contract_id:
|
||||
if "depth" not in self.market_data:
|
||||
self.market_data["depth"] = {}
|
||||
|
||||
self.market_data["depth"][contract_id] = depth_data
|
||||
logger.info(f"Depth update for {contract_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle depth update: {str(e)}")
|
||||
|
||||
async def create_limit_order(
|
||||
self,
|
||||
contract_id: str,
|
||||
size: str,
|
||||
price: str,
|
||||
side: str,
|
||||
time_in_force: str = TimeInForce.GOOD_TIL_CANCEL,
|
||||
reduce_only: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a limit order.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
size: The order size
|
||||
price: The order price
|
||||
side: The order side (BUY or SELL)
|
||||
time_in_force: The time in force
|
||||
reduce_only: Whether the order is reduce-only
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
|
||||
Raises:
|
||||
ValueError: If the order creation fails
|
||||
"""
|
||||
try:
|
||||
# Create order parameters
|
||||
params = CreateOrderParams(
|
||||
contract_id=contract_id,
|
||||
size=size,
|
||||
price=price,
|
||||
type=OrderType.LIMIT,
|
||||
side=side,
|
||||
time_in_force=time_in_force,
|
||||
reduce_only=reduce_only
|
||||
)
|
||||
|
||||
# Create the order
|
||||
result = await self.client.create_order(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to create order: {error_param}")
|
||||
raise ValueError(f"Failed to create order: {result.get('code')}")
|
||||
|
||||
# Update active orders
|
||||
await self.update_active_orders()
|
||||
|
||||
logger.info(f"Created limit order: {result.get('data', {}).get('orderId')}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create limit order: {str(e)}")
|
||||
raise
|
||||
|
||||
async def cancel_order(self, order_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel an order.
|
||||
|
||||
Args:
|
||||
order_id: The order ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cancellation result
|
||||
|
||||
Raises:
|
||||
ValueError: If the order cancellation fails
|
||||
"""
|
||||
try:
|
||||
# Create cancel order parameters
|
||||
params = CancelOrderParams(order_id=order_id)
|
||||
|
||||
# Cancel the order
|
||||
result = await self.client.cancel_order(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to cancel order: {error_param}")
|
||||
raise ValueError(f"Failed to cancel order: {result.get('code')}")
|
||||
|
||||
# Update active orders
|
||||
await self.update_active_orders()
|
||||
|
||||
logger.info(f"Cancelled order: {order_id}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel order: {str(e)}")
|
||||
raise
|
||||
|
||||
async def cancel_all_orders(self, contract_id: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel all orders for a contract.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID (optional)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cancellation result
|
||||
|
||||
Raises:
|
||||
ValueError: If the order cancellation fails
|
||||
"""
|
||||
try:
|
||||
# Create cancel order parameters
|
||||
params = CancelOrderParams(contract_id=contract_id or "")
|
||||
|
||||
# Cancel the orders
|
||||
result = await self.client.cancel_order(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to cancel orders: {error_param}")
|
||||
raise ValueError(f"Failed to cancel orders: {result.get('code')}")
|
||||
|
||||
# Update active orders
|
||||
await self.update_active_orders()
|
||||
|
||||
logger.info(f"Cancelled all orders for contract: {contract_id or 'all'}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel all orders: {str(e)}")
|
||||
raise
|
||||
|
||||
async def get_order_fill_transactions(
|
||||
self,
|
||||
contract_id: str = None,
|
||||
size: str = "10",
|
||||
offset_data: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get order fill transactions.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID (optional)
|
||||
size: The page size
|
||||
offset_data: The offset data for pagination
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order fill transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
try:
|
||||
# Create parameters
|
||||
params = OrderFillTransactionParams(
|
||||
size=size,
|
||||
offset_data=offset_data
|
||||
)
|
||||
|
||||
if contract_id:
|
||||
params.filter_contract_id_list = [contract_id]
|
||||
|
||||
# Get order fill transactions
|
||||
result = await self.client.get_order_fill_transactions(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to get order fill transactions: {error_param}")
|
||||
raise ValueError(f"Failed to get order fill transactions: {result.get('code')}")
|
||||
|
||||
logger.info(f"Got order fill transactions: {len(result.get('data', {}).get('list', []))}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get order fill transactions: {str(e)}")
|
||||
raise
|
||||
|
||||
async def get_k_line(
|
||||
self,
|
||||
contract_id: str,
|
||||
interval: str,
|
||||
size: str = "100",
|
||||
offset_data: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get K-line data.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
interval: The K-line interval
|
||||
size: The page size
|
||||
offset_data: The offset data for pagination
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The K-line data
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
try:
|
||||
# Create parameters
|
||||
params = GetKLineParams(
|
||||
contract_id=contract_id,
|
||||
interval=interval,
|
||||
size=size,
|
||||
offset_data=offset_data
|
||||
)
|
||||
|
||||
# Get K-line data
|
||||
result = await self.client.quote.get_k_line(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to get K-line data: {error_param}")
|
||||
raise ValueError(f"Failed to get K-line data: {result.get('code')}")
|
||||
|
||||
logger.info(f"Got K-line data: {len(result.get('data', {}).get('list', []))}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get K-line data: {str(e)}")
|
||||
raise
|
||||
|
||||
async def get_order_book_depth(
|
||||
self,
|
||||
contract_id: str,
|
||||
limit: int = 15
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get order book depth.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
limit: The depth limit (valid values are 15 or 200)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order book depth
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
try:
|
||||
# Create parameters
|
||||
params = GetOrderBookDepthParams(
|
||||
contract_id=contract_id,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# Get order book depth
|
||||
result = await self.client.quote.get_order_book_depth(params)
|
||||
|
||||
# Check for success
|
||||
if result.get("code") != "SUCCESS":
|
||||
error_param = result.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"Failed to get order book depth: {error_param}")
|
||||
raise ValueError(f"Failed to get order book depth: {result.get('code')}")
|
||||
|
||||
logger.info(f"Got order book depth for {contract_id}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get order book depth: {str(e)}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""Close all connections."""
|
||||
try:
|
||||
# Disconnect WebSocket
|
||||
self.ws_manager.disconnect_all()
|
||||
logger.info("Disconnected from WebSocket")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close connections: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function."""
|
||||
# Load configuration from environment variables
|
||||
base_url = os.getenv("EDGEX_BASE_URL", "https://testnet.edgex.exchange")
|
||||
ws_url = os.getenv("EDGEX_WS_URL", "wss://quote-testnet.edgex.exchange")
|
||||
account_id = int(os.getenv("EDGEX_ACCOUNT_ID", "12345"))
|
||||
stark_private_key = os.getenv("EDGEX_STARK_PRIVATE_KEY", "your-stark-private-key")
|
||||
|
||||
# Create trader
|
||||
trader = EdgeXTrader(
|
||||
base_url=base_url,
|
||||
ws_url=ws_url,
|
||||
account_id=account_id,
|
||||
stark_private_key=stark_private_key
|
||||
)
|
||||
|
||||
# Initialize trader
|
||||
if not await trader.initialize():
|
||||
logger.error("Failed to initialize trader")
|
||||
return
|
||||
|
||||
try:
|
||||
# Get K-line data for BTCUSDT (contract ID: 10000001)
|
||||
klines = await trader.get_k_line("10000001", "1m")
|
||||
logger.info(f"Retrieved K-line data: {len(klines.get('data', {}).get('list', []))} entries")
|
||||
|
||||
# Get order book depth for BTCUSDT (contract ID: 10000001)
|
||||
await trader.get_order_book_depth("10000001")
|
||||
logger.info(f"Retrieved order book depth")
|
||||
|
||||
# Create a limit order (commented out to avoid actual order creation)
|
||||
# order = await trader.create_limit_order(
|
||||
# contract_id="10000001", # BTCUSDT
|
||||
# size="0.001",
|
||||
# price="30000",
|
||||
# side=OrderSide.BUY
|
||||
# )
|
||||
#
|
||||
# # Cancel the order
|
||||
# if order and order.get("data", {}).get("orderId"):
|
||||
# await trader.cancel_order(order.get("data", {}).get("orderId"))
|
||||
|
||||
# Wait for some WebSocket updates
|
||||
logger.info("Waiting for WebSocket updates...")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
finally:
|
||||
# Close connections
|
||||
await trader.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Basic usage example for the EdgeX Python SDK.
|
||||
|
||||
This example demonstrates the basic functionality of the SDK:
|
||||
- Creating a client
|
||||
- Getting server time and metadata
|
||||
- Getting account assets and positions
|
||||
- Getting market data (K-lines, order book depth)
|
||||
- Creating orders (commented out to avoid actual order creation)
|
||||
- Using WebSockets for real-time data
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from edgex_sdk import (
|
||||
Client,
|
||||
OrderSide,
|
||||
GetKLineParams,
|
||||
GetOrderBookDepthParams,
|
||||
WebSocketManager
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# Load configuration from environment variables
|
||||
base_url = os.getenv("EDGEX_BASE_URL", "https://testnet.edgex.exchange")
|
||||
account_id = int(os.getenv("EDGEX_ACCOUNT_ID", "12345"))
|
||||
stark_private_key = os.getenv("EDGEX_STARK_PRIVATE_KEY", "your-stark-private-key")
|
||||
|
||||
# Create a new client
|
||||
client = Client(
|
||||
base_url=base_url,
|
||||
account_id=account_id,
|
||||
stark_private_key=stark_private_key
|
||||
)
|
||||
|
||||
# Get server time
|
||||
server_time = await client.get_server_time()
|
||||
print(f"Server Time: {server_time}")
|
||||
|
||||
# Get exchange metadata
|
||||
metadata = await client.get_metadata()
|
||||
print(f"Available contracts: {len(metadata.get('data', {}).get('contractList', []))}")
|
||||
|
||||
# Get account assets
|
||||
assets = await client.get_account_asset()
|
||||
print(f"Account Assets: {assets}")
|
||||
|
||||
# Get account positions
|
||||
positions = await client.get_account_positions()
|
||||
print(f"Account Positions: {positions}")
|
||||
|
||||
# Get 24-hour market data for BNBUSDT (contract ID: 10000004)
|
||||
quote = await client.get_24_hour_quote("10000004")
|
||||
print(f"BNBUSDT Price: {quote}")
|
||||
|
||||
# Get K-line data for BTCUSDT (contract ID: 10000001)
|
||||
kline_params = GetKLineParams(
|
||||
contract_id="10000001", # BTCUSDT
|
||||
interval="1m",
|
||||
size="10"
|
||||
)
|
||||
klines = await client.quote.get_k_line(kline_params)
|
||||
print(f"K-lines: {klines}")
|
||||
|
||||
# Get order book depth for ETHUSDT (contract ID: 10000002)
|
||||
depth_params = GetOrderBookDepthParams(
|
||||
contract_id="10000002", # ETHUSDT
|
||||
limit=15 # Valid values are 15 or 200
|
||||
)
|
||||
depth = await client.quote.get_order_book_depth(depth_params)
|
||||
print(f"Order Book Depth: {depth}")
|
||||
|
||||
# Create a limit order (commented out to avoid actual order creation)
|
||||
# order = await client.create_limit_order(
|
||||
# contract_id="10000004", # BNBUSDT
|
||||
# size="0.01",
|
||||
# price="600.00",
|
||||
# side=OrderSide.BUY
|
||||
# )
|
||||
# print(f"Order created: {order}")
|
||||
|
||||
# WebSocket example
|
||||
ws_url = os.getenv("EDGEX_WS_URL", "wss://quote-testnet.edgex.exchange")
|
||||
ws_manager = WebSocketManager(
|
||||
base_url=ws_url,
|
||||
account_id=account_id,
|
||||
stark_pri_key=stark_private_key
|
||||
)
|
||||
|
||||
# Define message handlers
|
||||
def ticker_handler(message):
|
||||
print(f"Ticker Update: {message}")
|
||||
|
||||
def kline_handler(message):
|
||||
print(f"K-line Update: {message}")
|
||||
|
||||
# Connect to public WebSocket for market data
|
||||
ws_manager.connect_public()
|
||||
|
||||
# Subscribe to real-time updates for BNBUSDT (contract ID: 10000004)
|
||||
ws_manager.subscribe_ticker("10000004", ticker_handler)
|
||||
ws_manager.subscribe_kline("10000004", "1m", kline_handler)
|
||||
|
||||
# Wait for updates
|
||||
await asyncio.sleep(30)
|
||||
|
||||
# Disconnect all connections
|
||||
ws_manager.disconnect_all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
API Endpoint Domain
|
||||
HTTP Endpoint
|
||||
Copy
|
||||
https://pro.edgex.exchange
|
||||
WebSocket Endpoint
|
||||
Copy
|
||||
wss://quote.edgex.exchange
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@starkware-industries/starkware-crypto-utils": "^0.2.1",
|
||||
"axios": "^1.12.2",
|
||||
"ccxt": "^4.5.5",
|
||||
"dotenv": "^17.2.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter";
|
||||
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
||||
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
||||
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
||||
import { EdgeXExchangeAdapter, type EdgeXCredentials } from "./edgex/adapter";
|
||||
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
|
||||
|
||||
export interface ExchangeFactoryOptions {
|
||||
@@ -12,10 +13,11 @@ export interface ExchangeFactoryOptions {
|
||||
grvt?: GrvtCredentials;
|
||||
lighter?: LighterCredentials;
|
||||
backpack?: BackpackCredentials;
|
||||
edgex?: EdgeXCredentials;
|
||||
paradex?: ParadexCredentials;
|
||||
}
|
||||
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex";
|
||||
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "edgex" | "paradex";
|
||||
|
||||
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
||||
@@ -25,6 +27,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
if (fallback === "grvt") return "grvt";
|
||||
if (fallback === "lighter") return "lighter";
|
||||
if (fallback === "backpack") return "backpack";
|
||||
if (fallback === "edgex") return "edgex";
|
||||
if (fallback === "paradex") return "paradex";
|
||||
return "aster";
|
||||
}
|
||||
@@ -33,6 +36,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
|
||||
if (id === "grvt") return "GRVT";
|
||||
if (id === "lighter") return "Lighter";
|
||||
if (id === "backpack") return "Backpack";
|
||||
if (id === "edgex") return "EdgeX";
|
||||
if (id === "paradex") return "Paradex";
|
||||
return "AsterDex";
|
||||
}
|
||||
@@ -48,6 +52,9 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
|
||||
if (id === "backpack") {
|
||||
return new BackpackExchangeAdapter({ ...options.backpack, symbol: options.symbol });
|
||||
}
|
||||
if (id === "edgex") {
|
||||
return new EdgeXExchangeAdapter(options.symbol, options.edgex);
|
||||
}
|
||||
if (id === "paradex") {
|
||||
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
ExchangeAdapter,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { AsterAccountSnapshot, AsterOrder, AsterDepth, AsterTicker, AsterKline, CreateOrderParams } from "../types";
|
||||
import { EdgeXGateway } from "./gateway";
|
||||
|
||||
export interface EdgeXCredentials {
|
||||
accountId?: string;
|
||||
privateKey?: string;
|
||||
positionId?: bigint;
|
||||
baseUrl?: string;
|
||||
wsPublicUrl?: string;
|
||||
wsPrivateUrl?: string;
|
||||
orderExpirationMs?: number;
|
||||
logger?: (context: string, error: unknown) => void;
|
||||
}
|
||||
|
||||
export class EdgeXExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "edgex";
|
||||
|
||||
private readonly gateway: EdgeXGateway;
|
||||
private initialized = false;
|
||||
|
||||
constructor(symbol: string, credentials: EdgeXCredentials = {}) {
|
||||
const accountId = credentials.accountId ?? process.env.EDGEX_ACCOUNT_ID;
|
||||
const privateKey = credentials.privateKey ?? process.env.EDGEX_PRIVATE_KEY;
|
||||
const positionIdValue = credentials.positionId ?? parseOptionalBigInt(process.env.EDGEX_POSITION_ID);
|
||||
|
||||
if (!accountId) throw new Error("Missing EDGEX_ACCOUNT_ID environment variable");
|
||||
if (!privateKey) throw new Error("Missing EDGEX_PRIVATE_KEY environment variable");
|
||||
|
||||
this.gateway = new EdgeXGateway({
|
||||
accountId,
|
||||
privateKey,
|
||||
symbol,
|
||||
positionId: positionIdValue,
|
||||
baseUrl: credentials.baseUrl ?? process.env.EDGEX_BASE_URL,
|
||||
wsPublicUrl: credentials.wsPublicUrl ?? process.env.EDGEX_WS_PUBLIC_URL,
|
||||
wsPrivateUrl: credentials.wsPrivateUrl ?? process.env.EDGEX_WS_PRIVATE_URL,
|
||||
orderExpirationMs:
|
||||
credentials.orderExpirationMs ?? parseOptionalInt(process.env.EDGEX_ORDER_TTL_MS) ?? undefined,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized();
|
||||
this.gateway.onAccount((snapshot: AsterAccountSnapshot) => cb(snapshot));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized();
|
||||
this.gateway.onOrders((orders: AsterOrder[]) => cb(orders));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized();
|
||||
this.gateway.onDepth(_symbol, (depth: AsterDepth) => cb(depth));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized();
|
||||
this.gateway.onTicker(_symbol, (ticker: AsterTicker) => cb(ticker));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized();
|
||||
this.gateway.onKlines(interval, (klines: AsterKline[]) => cb(klines));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized();
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
await this.gateway.cancelOrder(String(params.orderId));
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
await this.gateway.cancelOrders(params.orderIdList.map(String));
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
await this.gateway.cancelAllOrders();
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
await this.gateway.ensureInitialized();
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptionalInt(value?: string): number | undefined {
|
||||
if (!value) return undefined;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseOptionalBigInt(value?: string): bigint | undefined {
|
||||
if (!value) return undefined;
|
||||
try {
|
||||
return BigInt(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as crypto from "crypto";
|
||||
import type { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import axios from "axios";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { EdgeXSignature, buildQueryString } from "./signature";
|
||||
|
||||
export interface EdgeXHttpClientOptions {
|
||||
baseUrl: string;
|
||||
privateKey: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface EdgeXResponse<T = any> {
|
||||
code: string;
|
||||
data: T;
|
||||
msg?: string | null;
|
||||
errorParam?: unknown;
|
||||
}
|
||||
|
||||
export class EdgeXHttpClient {
|
||||
private readonly axios: AxiosInstance;
|
||||
private readonly signer: EdgeXSignature;
|
||||
|
||||
constructor(options: EdgeXHttpClientOptions) {
|
||||
this.axios = axios.create({
|
||||
baseURL: options.baseUrl,
|
||||
timeout: options.timeout ?? 30_000,
|
||||
});
|
||||
this.signer = new EdgeXSignature(options.privateKey);
|
||||
}
|
||||
|
||||
getSigner(): EdgeXSignature {
|
||||
return this.signer;
|
||||
}
|
||||
|
||||
async get<T = any>(path: string, query?: Record<string, unknown>): Promise<EdgeXResponse<T>> {
|
||||
return this.request<T>({ method: "GET", path, params: query });
|
||||
}
|
||||
|
||||
async post<T = any>(path: string, body?: unknown): Promise<EdgeXResponse<T>> {
|
||||
return this.request<T>({ method: "POST", path, data: body });
|
||||
}
|
||||
|
||||
private buildHeaders(signature: string, timestamp: string): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"X-edgeX-Api-Timestamp": timestamp,
|
||||
"X-edgeX-Api-Signature": signature,
|
||||
};
|
||||
}
|
||||
|
||||
private async request<T = any>(input: {
|
||||
method: string;
|
||||
path: string;
|
||||
data?: unknown;
|
||||
params?: Record<string, unknown>;
|
||||
}): Promise<EdgeXResponse<T>> {
|
||||
const normalizedParams = normalizeParams(input.params);
|
||||
const signature = this.signer.createHttpSignature({
|
||||
method: input.method,
|
||||
path: input.path,
|
||||
body: input.data,
|
||||
query: normalizedParams,
|
||||
});
|
||||
const serializedQuery = normalizedParams ? buildQueryString(normalizedParams) : "";
|
||||
const url = serializedQuery ? appendQueryString(input.path, serializedQuery) : input.path;
|
||||
const config: AxiosRequestConfig = {
|
||||
method: input.method,
|
||||
url,
|
||||
data: input.data,
|
||||
headers: this.buildHeaders(signature.signature, signature.timestamp),
|
||||
};
|
||||
try {
|
||||
const response = await this.axios.request<EdgeXResponse<T>>(config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
let detail: string | undefined;
|
||||
if (error.response?.data != null) {
|
||||
try {
|
||||
detail = typeof error.response.data === "string"
|
||||
? error.response.data
|
||||
: JSON.stringify(error.response.data);
|
||||
} catch {
|
||||
detail = undefined;
|
||||
}
|
||||
}
|
||||
const statusLabel = status ? ` (${status})` : "";
|
||||
const message = detail ?? extractMessage(error);
|
||||
throw new Error(`EdgeX request failed${statusLabel}: ${message}`);
|
||||
}
|
||||
throw new Error(extractMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function appendQueryString(path: string, query: string): string {
|
||||
if (!query) return path;
|
||||
const separator = path.includes("?")
|
||||
? path.endsWith("?") || path.endsWith("&") ? "" : "&"
|
||||
: "?";
|
||||
return `${path}${separator}${query}`;
|
||||
}
|
||||
|
||||
export function computeNonceFromClientOrderId(clientOrderId: string): number {
|
||||
const hash = crypto.createHash("sha256").update(clientOrderId).digest("hex");
|
||||
return parseInt(hash.slice(0, 8), 16);
|
||||
}
|
||||
|
||||
function normalizeParams(params?: Record<string, unknown>): Record<string, string> | undefined {
|
||||
if (!params) return undefined;
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value == null) continue;
|
||||
if (Array.isArray(value)) {
|
||||
normalized[key] = value.map(stringifyPrimitive).join(",");
|
||||
continue;
|
||||
}
|
||||
normalized[key] = stringifyPrimitive(value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function stringifyPrimitive(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
if (typeof value === "boolean") return value ? "true" : "false";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toString() : "";
|
||||
return String(value);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { strict as assert } from "assert";
|
||||
|
||||
export function countBase10Scale(resolution: bigint): number {
|
||||
let scale = 0;
|
||||
let value = resolution;
|
||||
while (value % 10n === 0n) {
|
||||
value /= 10n;
|
||||
scale += 1;
|
||||
}
|
||||
if (value !== 1n) {
|
||||
throw new Error(`Resolution ${resolution.toString()} is not a power of 10`);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
export function decimalToBigInt(value: number | string, scale: number): bigint {
|
||||
const normalized = normalizeDecimal(typeof value === "number" ? value.toString() : value);
|
||||
const [intPart, fracPart = ""] = normalized.split(".");
|
||||
assert(fracPart.length <= scale, `Value ${value} exceeds scale ${scale}`);
|
||||
const paddedFraction = (fracPart + "0".repeat(scale)).slice(0, scale);
|
||||
const digits = stripLeadingZeros(intPart + paddedFraction);
|
||||
return digits.length === 0 ? 0n : BigInt(digits);
|
||||
}
|
||||
|
||||
export function bigIntToDecimal(value: bigint, scale: number): string {
|
||||
const negative = value < 0n;
|
||||
const abs = negative ? -value : value;
|
||||
const factor = 10n ** BigInt(scale);
|
||||
const intPart = abs / factor;
|
||||
const fracPart = abs % factor;
|
||||
if (scale === 0) {
|
||||
return `${negative ? "-" : ""}${intPart.toString()}`;
|
||||
}
|
||||
const fracStr = fracPart.toString().padStart(scale, "0").replace(/0+$/, "");
|
||||
if (fracStr.length === 0) {
|
||||
return `${negative ? "-" : ""}${intPart.toString()}`;
|
||||
}
|
||||
return `${negative ? "-" : ""}${intPart.toString()}.${fracStr}`;
|
||||
}
|
||||
|
||||
export function multiplyByDecimal(value: bigint, rate: string, roundUp = false): bigint {
|
||||
const { numerator, denominator } = decimalToFraction(rate);
|
||||
const product = value * numerator;
|
||||
if (!roundUp) {
|
||||
return product / denominator;
|
||||
}
|
||||
return (product + denominator - 1n) / denominator;
|
||||
}
|
||||
|
||||
export function decimalToFraction(value: string): { numerator: bigint; denominator: bigint } {
|
||||
const normalized = normalizeDecimal(value);
|
||||
if (!normalized.includes(".")) {
|
||||
return { numerator: BigInt(normalized), denominator: 1n };
|
||||
}
|
||||
const negative = normalized.startsWith("-");
|
||||
const unsigned = negative ? normalized.slice(1) : normalized;
|
||||
const parts = unsigned.split(".");
|
||||
const intPart = parts[0] ?? "0";
|
||||
const fracPart = parts[1] ?? "";
|
||||
const denominator = 10n ** BigInt(fracPart.length);
|
||||
const magnitude = BigInt(stripLeadingZeros(intPart + fracPart));
|
||||
const numerator = negative ? -magnitude : magnitude;
|
||||
return { numerator, denominator };
|
||||
}
|
||||
|
||||
export function getScaleFromDenominator(denominator: bigint): number {
|
||||
let scale = 0;
|
||||
let value = denominator;
|
||||
while (value > 1n) {
|
||||
if (value % 10n !== 0n) {
|
||||
throw new Error(`Denominator ${denominator.toString()} is not a power of 10`);
|
||||
}
|
||||
value /= 10n;
|
||||
scale += 1;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
export function formatDecimal(numerator: bigint, scale: number): string {
|
||||
const negative = numerator < 0n;
|
||||
let absValue = negative ? -numerator : numerator;
|
||||
if (scale === 0) {
|
||||
return `${negative ? "-" : ""}${absValue.toString()}`;
|
||||
}
|
||||
const factor = 10n ** BigInt(scale);
|
||||
const intPart = absValue / factor;
|
||||
let fracPart = (absValue % factor).toString().padStart(scale, "0");
|
||||
fracPart = fracPart.replace(/0+$/, "");
|
||||
if (fracPart.length === 0) {
|
||||
return `${negative ? "-" : ""}${intPart.toString()}`;
|
||||
}
|
||||
return `${negative ? "-" : ""}${intPart.toString()}.${fracPart}`;
|
||||
}
|
||||
|
||||
function normalizeDecimal(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!/^[-+]?((\d+\.?\d*)|(\.\d+))$/.test(trimmed)) {
|
||||
throw new Error(`Invalid decimal value: ${input}`);
|
||||
}
|
||||
const negative = trimmed.startsWith("-");
|
||||
const unsigned = trimmed.replace(/^[-+]/, "");
|
||||
const [rawInt = "0", rawFrac = ""] = unsigned.split(".");
|
||||
const intDigits = stripLeadingZeros(rawInt);
|
||||
const fracDigits = rawFrac.replace(/0+$/, "");
|
||||
const magnitude = fracDigits.length > 0 ? `${intDigits}.${fracDigits}` : intDigits;
|
||||
if (magnitude === "0") {
|
||||
return "0";
|
||||
}
|
||||
return negative ? `-${magnitude}` : magnitude;
|
||||
}
|
||||
|
||||
function stripLeadingZeros(value: string): string {
|
||||
const stripped = value.replace(/^0+/, "");
|
||||
return stripped.length === 0 ? "0" : stripped;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
import { pedersen } from "@starkware-industries/starkware-crypto-utils";
|
||||
import { ec as starkEc, sign as starkSign } from "@starkware-industries/starkware-crypto-utils";
|
||||
|
||||
const FIELD_PRIME = BigInt("0x080000000000011000000000000000000000000000000000000000000000001");
|
||||
const LIMIT_ORDER_WITH_FEE_TYPE = 3n;
|
||||
|
||||
export interface EdgeXL2OrderSignInput {
|
||||
isBuy: boolean;
|
||||
amountSynthetic: bigint;
|
||||
amountCollateral: bigint;
|
||||
amountFee: bigint;
|
||||
syntheticAssetId: string;
|
||||
collateralAssetId: string;
|
||||
feeAssetId: string;
|
||||
positionId: bigint;
|
||||
nonce: number;
|
||||
expirationHours: number;
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
export interface EdgeXL2SignatureResult {
|
||||
signature: string;
|
||||
r: string;
|
||||
s: string;
|
||||
}
|
||||
|
||||
export function signLimitOrder(input: EdgeXL2OrderSignInput): EdgeXL2SignatureResult {
|
||||
const messageHash = calcLimitOrderHash({
|
||||
syntheticAssetId: input.syntheticAssetId,
|
||||
collateralAssetId: input.collateralAssetId,
|
||||
feeAssetId: input.feeAssetId,
|
||||
isBuy: input.isBuy,
|
||||
amountSynthetic: input.amountSynthetic,
|
||||
amountCollateral: input.amountCollateral,
|
||||
amountFee: input.amountFee,
|
||||
nonce: BigInt(input.nonce),
|
||||
positionId: input.positionId,
|
||||
expirationHours: BigInt(input.expirationHours),
|
||||
});
|
||||
|
||||
const keyPair = starkEc.keyFromPrivate(stripHexPrefix(input.privateKey), "hex");
|
||||
const signature = starkSign(keyPair, messageHash, { canonical: true });
|
||||
const r = signature.r.toString(16).padStart(64, "0");
|
||||
const s = signature.s.toString(16).padStart(64, "0");
|
||||
return {
|
||||
signature: `${r}${s}`,
|
||||
r,
|
||||
s,
|
||||
};
|
||||
}
|
||||
|
||||
interface LimitOrderHashParams {
|
||||
syntheticAssetId: string;
|
||||
collateralAssetId: string;
|
||||
feeAssetId: string;
|
||||
isBuy: boolean;
|
||||
amountSynthetic: bigint;
|
||||
amountCollateral: bigint;
|
||||
amountFee: bigint;
|
||||
nonce: bigint;
|
||||
positionId: bigint;
|
||||
expirationHours: bigint;
|
||||
}
|
||||
|
||||
function calcLimitOrderHash(params: LimitOrderHashParams): string {
|
||||
const syntheticAsset = hexToField(params.syntheticAssetId);
|
||||
const collateralAsset = hexToField(params.collateralAssetId);
|
||||
const feeAsset = hexToField(params.feeAssetId);
|
||||
|
||||
const amountSynthetic = toField(params.amountSynthetic);
|
||||
const amountCollateral = toField(params.amountCollateral);
|
||||
const amountFee = toField(params.amountFee);
|
||||
const nonce = toField(params.nonce);
|
||||
const positionId = toField(params.positionId);
|
||||
const expiration = toField(params.expirationHours);
|
||||
|
||||
const assetSell = params.isBuy ? collateralAsset : syntheticAsset;
|
||||
const assetBuy = params.isBuy ? syntheticAsset : collateralAsset;
|
||||
|
||||
const amountSell = params.isBuy ? amountCollateral : amountSynthetic;
|
||||
const amountBuy = params.isBuy ? amountSynthetic : amountCollateral;
|
||||
|
||||
let msg = pedersenPair(assetSell, assetBuy);
|
||||
msg = pedersenPair(msg, feeAsset);
|
||||
|
||||
let packed0 = amountSell;
|
||||
packed0 = shiftLeft(packed0, 64n) + amountBuy;
|
||||
packed0 = shiftLeft(packed0, 64n) + amountFee;
|
||||
packed0 = shiftLeft(packed0, 32n) + nonce;
|
||||
packed0 = toField(packed0);
|
||||
|
||||
msg = pedersenPair(msg, packed0);
|
||||
|
||||
let packed1 = LIMIT_ORDER_WITH_FEE_TYPE;
|
||||
packed1 = shiftLeft(packed1, 64n) + positionId;
|
||||
packed1 = shiftLeft(packed1, 64n) + positionId;
|
||||
packed1 = shiftLeft(packed1, 64n) + positionId;
|
||||
packed1 = shiftLeft(packed1, 32n) + expiration;
|
||||
packed1 = shiftLeft(packed1, 17n);
|
||||
packed1 = toField(packed1);
|
||||
|
||||
const final = pedersenPair(msg, packed1);
|
||||
return final.toString(16);
|
||||
}
|
||||
|
||||
function pedersenPair(a: bigint, b: bigint): bigint {
|
||||
const result = pedersen([toHex(a), toHex(b)]);
|
||||
return BigInt(`0x${result}`);
|
||||
}
|
||||
|
||||
function toField(value: bigint): bigint {
|
||||
let normalized = value % FIELD_PRIME;
|
||||
if (normalized < 0n) normalized += FIELD_PRIME;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function hexToField(value: string): bigint {
|
||||
const stripped = stripHexPrefix(value);
|
||||
return toField(BigInt(`0x${stripped || "0"}`));
|
||||
}
|
||||
|
||||
function toHex(value: bigint): string {
|
||||
return toField(value).toString(16);
|
||||
}
|
||||
|
||||
function stripHexPrefix(value: string): string {
|
||||
if (!value) return "";
|
||||
return value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value;
|
||||
}
|
||||
|
||||
function shiftLeft(value: bigint, bits: bigint): bigint {
|
||||
return toField(value << bits);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as crypto from "crypto";
|
||||
import { keccak256 } from "ethereum-cryptography/keccak";
|
||||
import { ec as starkEc } from "@starkware-industries/starkware-crypto-utils";
|
||||
|
||||
export interface HttpSignatureInput {
|
||||
method: string;
|
||||
path: string;
|
||||
query?: Record<string, unknown> | URLSearchParams | null;
|
||||
body?: unknown;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface HttpSignatureResult {
|
||||
signature: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface EdgeXWsHeaders {
|
||||
timestamp: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export class EdgeXSignature {
|
||||
private readonly privateKey: string;
|
||||
private readonly keyPair:
|
||||
| ReturnType<typeof starkEc.keyFromPrivate>
|
||||
| null;
|
||||
|
||||
constructor(privateKey: string) {
|
||||
this.privateKey = stripHexPrefix(privateKey);
|
||||
this.keyPair = starkEc.keyFromPrivate(this.privateKey, "hex");
|
||||
}
|
||||
|
||||
createHttpSignature(input: HttpSignatureInput): HttpSignatureResult {
|
||||
const timestamp = input.timestamp ?? Date.now();
|
||||
const content = buildSignatureContent({
|
||||
timestamp,
|
||||
method: input.method,
|
||||
path: input.path,
|
||||
body: input.body,
|
||||
query: input.query,
|
||||
});
|
||||
const { r, s } = this.sign(content);
|
||||
return {
|
||||
signature: `${r}${s}`,
|
||||
timestamp: timestamp.toString(),
|
||||
message: content,
|
||||
};
|
||||
}
|
||||
|
||||
createWebsocketHeaders(accountId: string): EdgeXWsHeaders {
|
||||
const timestamp = Date.now();
|
||||
const path = `/api/v1/private/wsaccountId=${accountId}`;
|
||||
const content = `${timestamp}GET${path}`;
|
||||
const { r, s } = this.sign(content);
|
||||
return {
|
||||
timestamp: timestamp.toString(),
|
||||
signature: `${r}${s}`,
|
||||
};
|
||||
}
|
||||
|
||||
signRaw(message: string | Buffer): { r: string; s: string } {
|
||||
const buffer = typeof message === "string" ? Buffer.from(message, "utf8") : message;
|
||||
return this.sign(buffer);
|
||||
}
|
||||
|
||||
randomNonce(max: number = 0xffffffff): number {
|
||||
return crypto.randomInt(0, max + 1);
|
||||
}
|
||||
|
||||
private sign(message: string | Buffer): { r: string; s: string } {
|
||||
if (!this.keyPair) throw new Error("EdgeX signer not initialized");
|
||||
const buffer = typeof message === "string" ? Buffer.from(message, "utf8") : message;
|
||||
const hash = keccak256(buffer);
|
||||
const msgHex = Buffer.from(hash).toString("hex");
|
||||
const signature = this.keyPair.sign(msgHex, { canonical: true });
|
||||
const r = signature.r.toString(16).padStart(64, "0");
|
||||
const s = signature.s.toString(16).padStart(64, "0");
|
||||
return { r, s };
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSignatureContent(input: {
|
||||
timestamp: number;
|
||||
method: string;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
query?: Record<string, unknown> | URLSearchParams | null;
|
||||
}): string {
|
||||
const { timestamp, method } = input;
|
||||
const upperMethod = method.toUpperCase();
|
||||
const normalizedPath = ensureLeadingSlash(input.path);
|
||||
if (input.body != null && input.body !== "") {
|
||||
const bodyString = stringifyForSignature(input.body);
|
||||
return `${timestamp}${upperMethod}${normalizedPath}${bodyString}`;
|
||||
}
|
||||
const queryString = buildQueryString(input.query);
|
||||
if (queryString) {
|
||||
return `${timestamp}${upperMethod}${normalizedPath}${queryString}`;
|
||||
}
|
||||
return `${timestamp}${upperMethod}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export function buildQueryString(query?: Record<string, unknown> | URLSearchParams | null): string {
|
||||
if (!query) return "";
|
||||
const entries: Array<[string, string]> = [];
|
||||
if (query instanceof URLSearchParams) {
|
||||
query.forEach((value, key) => {
|
||||
entries.push([key, value]);
|
||||
});
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value == null) continue;
|
||||
entries.push([key, stringifyPrimitive(value)]);
|
||||
}
|
||||
}
|
||||
if (entries.length === 0) return "";
|
||||
entries.sort(([a], [b]) => a.localeCompare(b));
|
||||
return entries.map(([key, value]) => `${key}=${value}`).join("&");
|
||||
}
|
||||
|
||||
function stringifyPrimitive(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
if (typeof value === "boolean") return value.toString().toLowerCase();
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toString() : "";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function stringifyForSignature(data: unknown): string {
|
||||
if (data == null) return "";
|
||||
if (typeof data === "string") return data;
|
||||
if (typeof data === "number") return Number.isFinite(data) ? data.toString() : "";
|
||||
if (typeof data === "boolean") return data.toString().toLowerCase();
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) return "";
|
||||
return data.map((item) => stringifyForSignature(item)).join("&");
|
||||
}
|
||||
if (typeof data === "object") {
|
||||
const map = new Map<string, string>();
|
||||
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
||||
map.set(key, stringifyForSignature(value));
|
||||
}
|
||||
const keys = Array.from(map.keys()).sort((a, b) => a.localeCompare(b));
|
||||
return keys.map((key) => `${key}=${map.get(key) ?? ""}`).join("&");
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
function ensureLeadingSlash(path: string): string {
|
||||
if (!path.startsWith("/")) return `/${path}`;
|
||||
return path;
|
||||
}
|
||||
|
||||
function stripHexPrefix(input: string): string {
|
||||
return input.startsWith("0x") || input.startsWith("0X") ? input.slice(2) : input;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
export interface EdgeXMetaResponse {
|
||||
code: string;
|
||||
data: {
|
||||
global: EdgeXGlobalMeta;
|
||||
coinList: EdgeXCoinMeta[];
|
||||
contractList: EdgeXContractMeta[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface EdgeXGlobalMeta {
|
||||
appEnv: string;
|
||||
starkExCollateralCoin: EdgeXCoinMeta;
|
||||
}
|
||||
|
||||
export interface EdgeXCoinMeta {
|
||||
coinId: string;
|
||||
coinName: string;
|
||||
stepSize?: string;
|
||||
starkExAssetId: string;
|
||||
starkExResolution: string;
|
||||
}
|
||||
|
||||
export interface EdgeXContractMeta {
|
||||
contractId: string;
|
||||
contractName: string;
|
||||
baseCoinId: string;
|
||||
quoteCoinId: string;
|
||||
tickSize: string;
|
||||
stepSize: string;
|
||||
minOrderSize: string;
|
||||
maxOrderSize: string;
|
||||
defaultTakerFeeRate: string;
|
||||
defaultMakerFeeRate: string;
|
||||
displayDigitMerge?: string;
|
||||
displayMaxLeverage?: string;
|
||||
displayMinLeverage?: string;
|
||||
starkExSyntheticAssetId: string;
|
||||
starkExResolution: string;
|
||||
}
|
||||
|
||||
export interface EdgeXAccountSnapshotResponse<T> {
|
||||
code: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface EdgeXOrderRequest {
|
||||
accountId: string;
|
||||
contractId: string;
|
||||
side: "BUY" | "SELL";
|
||||
type: "LIMIT" | "MARKET" | "STOP_MARKET" | "TAKE_PROFIT" | string;
|
||||
timeInForce: string;
|
||||
price?: string;
|
||||
size: string;
|
||||
triggerPrice?: string;
|
||||
triggerPriceType?: string;
|
||||
reduceOnly?: boolean;
|
||||
isPositionTpsl?: boolean;
|
||||
isSetOpenTp?: boolean;
|
||||
isSetOpenSl?: boolean;
|
||||
clientOrderId: string;
|
||||
expireTime: string;
|
||||
l2Nonce: string;
|
||||
l2Value: string;
|
||||
l2Size: string;
|
||||
l2LimitFee: string;
|
||||
l2ExpireTime: string;
|
||||
l2Signature: string;
|
||||
extraType?: string;
|
||||
extraDataJson?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXCancelOrderRequest {
|
||||
accountId: string;
|
||||
orderId: string;
|
||||
}
|
||||
|
||||
export interface EdgeXCancelAllRequest {
|
||||
accountId: string;
|
||||
contractId?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXOpenOrder {
|
||||
orderId: string;
|
||||
clientOrderId: string;
|
||||
contractId: string;
|
||||
accountId: string;
|
||||
side: "BUY" | "SELL";
|
||||
type: string;
|
||||
price: string;
|
||||
size: string;
|
||||
filledSize?: string;
|
||||
status: string;
|
||||
createTime: string;
|
||||
updateTime?: string;
|
||||
l2Nonce?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXPrivateWsMessage<T = unknown> {
|
||||
type: string;
|
||||
content?: {
|
||||
event: string;
|
||||
version?: string;
|
||||
data?: T;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EdgeXTradeEvent {
|
||||
account?: EdgeXAccountUpdate[];
|
||||
order?: EdgeXOrderUpdate[];
|
||||
position?: EdgeXPositionUpdate[];
|
||||
collateral?: EdgeXCollateralUpdate[];
|
||||
orderFillTransaction?: EdgeXOrderFillUpdate[];
|
||||
}
|
||||
|
||||
export interface EdgeXAccountUpdate {
|
||||
accountId: string;
|
||||
totalEquity?: string;
|
||||
availableBalance?: string;
|
||||
totalMaintenanceMargin?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXCollateralUpdate {
|
||||
coinId: string;
|
||||
balance: string;
|
||||
availableBalance: string;
|
||||
}
|
||||
|
||||
export interface EdgeXPositionUpdate {
|
||||
contractId: string;
|
||||
size: string;
|
||||
averageEntryPrice?: string;
|
||||
unrealizedPnl?: string;
|
||||
leverage?: string;
|
||||
maintenanceMargin?: string;
|
||||
markPrice?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXOrderUpdate {
|
||||
orderId: string;
|
||||
clientOrderId: string;
|
||||
contractId: string;
|
||||
accountId: string;
|
||||
status: string;
|
||||
price: string;
|
||||
size: string;
|
||||
filledSize?: string;
|
||||
side: "BUY" | "SELL";
|
||||
type: string;
|
||||
updateTime?: string;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
export interface EdgeXOrderFillUpdate {
|
||||
orderId: string;
|
||||
fillPrice: string;
|
||||
fillSize: string;
|
||||
fee: string;
|
||||
side: "BUY" | "SELL";
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface EdgeXDepthMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
content?: {
|
||||
dataType: "Snapshot" | "Changed" | string;
|
||||
data: Array<{
|
||||
bids: Array<[string, string]>;
|
||||
asks: Array<[string, string]>;
|
||||
depthType?: string;
|
||||
startVersion?: string;
|
||||
endVersion?: string;
|
||||
contractId: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EdgeXTickerMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
content?: {
|
||||
dataType: "Snapshot" | "Changed" | string;
|
||||
data: Array<{
|
||||
contractId: string;
|
||||
lastPrice?: string;
|
||||
high?: string;
|
||||
low?: string;
|
||||
open?: string;
|
||||
close?: string;
|
||||
size?: string;
|
||||
value?: string;
|
||||
trades?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EdgeXKlineMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
content?: {
|
||||
dataType: "Snapshot" | "Changed" | string;
|
||||
data: Array<{
|
||||
contractId: string;
|
||||
klineType: string;
|
||||
klineTime: string;
|
||||
open: string;
|
||||
high: string;
|
||||
low: string;
|
||||
close: string;
|
||||
size: string;
|
||||
value: string;
|
||||
trades: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user