90 Commits
Author SHA1 Message Date
discountry d80518853e Merge branch 'main' into dev 2026-02-27 10:51:05 +08:00
discountry 422ee6f465 update maker points config 2026-02-08 09:41:06 +08:00
discountry bee7bdd8fe update maker points 2026-02-08 09:37:39 +08:00
discountry 3f67b99291 Add immediate reprice logic to MakerPointsEngine
- Enhanced the MakerPointsEngine by introducing a new method `shouldTriggerImmediateReprice` to trigger an immediate tick when the market depth deviates beyond a specified minimum reprice basis points threshold.
- Updated the existing depth protection logic to include this new reprice condition.
- Added a comprehensive test suite to validate the immediate reprice functionality and its integration with the MakerPoints engine.
2026-02-08 00:08:38 +08:00
discountry 61e6e4cdde Add Binance depth monitoring configuration to MakerPoints
- Introduced new environment variables `MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS` and `MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO` to configure Binance depth monitoring.
- Updated `MakerPointsConfig` interface and implementation to include these new parameters.
- Enhanced translations to reflect dynamic depth window information in the UI.
- Added tests to validate the new configuration options and their integration into the MakerPoints engine.
2026-02-07 23:58:01 +08:00
discountry 858b2f304b Update imbalance ratio in MakerPointsEngine and binance-depth.ts for improved trading strategy
- Adjusted the `ratio` in `MakerPointsEngine` from 8 to 2 to better align with current market conditions.
- Updated `DEFAULT_IMBALANCE_RATIO` in `binance-depth.ts` from 8 to 2 to maintain consistency across the trading strategy.
2026-02-07 23:35:22 +08:00
discountry 6998afebb1 Refactor price calculation in MakerPointsEngine for improved accuracy
- Replaced direct price calculations with a new method `normalizeDepthTargetPrice` to ensure valid target prices for buy and sell orders.
- Updated all instances of price calculations in the MakerPointsEngine to utilize the new normalization method.
- Added boundary tests for `getDepthBetweenPrices` to validate behavior when prices are exactly at the target.
2026-02-07 23:27:42 +08:00
discountry b8942c18e6 Add immediate depth protection logic to MakerPointsEngine
- Introduced a new private property `forceTickRequested` to manage immediate tick requests.
- Implemented `shouldTriggerImmediateDepthProtection` method to trigger a tick when depth falls below the configured threshold.
- Updated the tick processing logic to accommodate immediate depth protection.
- Added unit tests to validate the immediate tick triggering behavior based on depth changes.
2026-02-07 23:10:56 +08:00
discountry 6f85e609f8 Update minimum depth threshold and imbalance ratio for improved trading performance
- Changed `filterMinDepth` in `MakerPointsConfig` from 5 to 10 to enhance trading strategy.
- Adjusted `ratio` in `MakerPointsEngine` and `DEFAULT_IMBALANCE_RATIO` in `binance-depth.ts` from 9 to 8 for better alignment with market conditions.
2026-02-07 22:46:48 +08:00
discountry a851257149 Update minimum depth threshold in MakerPointsConfig from 50 to 5 for improved trading performance 2026-02-07 22:19:39 +08:00
DisneyandGitHub 9355ec5019 Enhance Binance depth health monitoring and defense mode logic (#21)
- Updated translations for Binance depth status messages to include depth window information.
- Modified `MakerPointsEngine` to incorporate health checks for the Binance depth tracker, including handling of unhealthy states.
- Improved defense mode activation logic to respond to Binance depth health status, ensuring appropriate logging and notifications.
- Added integration tests for defense mode behavior based on Binance depth health, validating transitions into and out of defense mode.
- Refactored `BinanceDepthTracker` to support health checks and improved connection management.
2026-02-07 22:01:49 +08:00
DisneyandGitHub fa82d45bfb fix size (#20) 2026-02-04 13:26:06 +08:00
discountry d0154e5721 fix den 2026-02-03 18:48:06 +08:00
discountry 52b6a8a076 Add invitation links for Nado registration in trading tutorial 2026-02-03 12:04:36 +08:00
discountry db6a9cfc68 Add Nado trading tutorial for ritmex-bot 2026-02-03 12:02:55 +08:00
DisneyandGitHub 03b8e53d30 Merge pull request #19 from discountry/feat/arb
Feat/arb
2026-02-01 11:15:42 +08:00
discountry 8d79ace8b3 Refactor triggerType handling in order placement logic
- Updated the triggerType assignment in placeStopLossOrder and related functions to default to "STOP_LOSS" instead of conditionally setting it based on the order side.
- This change simplifies the logic for stop market orders across the order coordinator and GRVT exchange gateway, ensuring consistent behavior.
2026-02-01 11:15:00 +08:00
discountry 1fb6d3d62d Add swing trading configuration options to .env.example
- Added new environment variables for swing trading, including SWING_DIRECTION and SWING_STOP_LOSS_PCT.
- Updated documentation in .env.example to reflect the new swing trading parameters for better clarity and usability.
2026-01-31 16:19:19 +08:00
discountry c4559cb0d7 Add swing trading strategy with RSI signals and Binance integration
- Introduced a new swing trading strategy utilizing the RSI indicator on the ETHBTC pair from Binance.
- Implemented the `SwingEngine` to manage trading logic, including entry and exit conditions based on RSI thresholds.
- Added configuration options for swing direction, trade amount, and RSI parameters in `config.ts`.
- Created new documentation for the swing strategy, detailing its behavior and configuration.
- Enhanced CLI to support the new swing strategy option.
- Added tests for swing logic to ensure correct behavior under various market conditions.
2026-01-31 16:15:07 +08:00
discountry 1d88ddefb5 Enhance account snapshot handling and staleness checks in MakerPointsEngine
- Updated `emitAccountSnapshot` method in `StandxGateway` to accept an optional `updateTime` parameter, allowing for more accurate timestamping.
- Introduced logic to determine the appropriate `updateTime` based on the latest position or balance data.
- Added `time` property to `StandxPosition` interface for improved timestamp management.
- Implemented `applyAccountSnapshot` method in `MakerPointsEngine` to streamline account snapshot processing and ensure accurate time tracking.
- Added tests to validate the behavior of account staleness checks and defense mode activation based on account data freshness.
2026-01-24 23:53:36 +08:00
discountry 683352f737 Add changeMarginMode method to ExchangeAdapter and Standx classes
- Introduced `changeMarginMode` method in `ExchangeAdapter` interface to allow margin mode adjustments.
- Implemented the `changeMarginMode` method in `StandxExchangeAdapter` to interact with the gateway for changing margin modes.
- Added corresponding `changeMarginMode` method in `StandxGateway` to handle API requests for margin mode changes.
- Enhanced `MakerPointsEngine` to ensure isolated margin mode before order placement, with appropriate logging and defense mode activation if the change fails.
- Created tests for margin mode functionality to validate behavior under different scenarios.
2026-01-24 22:57:03 +08:00
discountry a629bc940c Enhance environment variable parsing and account snapshot validation
- Introduced `normalizeEnvValue` function to improve handling of environment variable values, including trimming, unquoting, and stripping inline comments.
- Updated `resolveSymbolFromEnv` and parsing functions to utilize the new normalization logic.
- Added `validateAccountSnapshotForSymbol` function to validate account snapshots, ensuring numeric fields are correctly formatted and flagging any issues.
- Implemented tests for environment variable parsing and account snapshot validation to ensure robustness and correctness.
2026-01-24 22:46:14 +08:00
discountry fe7b8eb6f3 Update Binance WebSocket configuration and enhance depth handling
- Changed WebSocket base URL to support both spot and futures trading.
- Adjusted depth tracking parameters for improved performance, increasing the ratio and reducing speed.
- Enhanced payload parsing to accommodate additional data structures from Binance, ensuring robust handling of bids and asks.
- Updated comments for clarity on connection behavior and heartbeat monitoring.
2026-01-22 10:23:07 +08:00
discountry 24339929dc Enhance MakerPoints functionality and configuration
- Updated `filterMinDepth` in `config.ts` from 1 to 50 to improve depth filtering logic.
- Added new translation entries for band depth display in `i18n/index.ts`.
- Introduced `bandDepths` to `MakerPointsSnapshot` in `maker-points-engine.ts` to track depth across different bands.
- Enhanced `BinanceDepthTracker` to support dynamic depth levels and speed settings.
- Updated `MakerPointsApp` to display band depth information, improving user interface clarity.
2026-01-22 02:27:45 +08:00
discountry ed855f6859 Refine data staleness checks in MakerPointsEngine
- Updated the logic to only consider depth data for staleness checks, excluding account data from the criteria.
- Removed unnecessary account staleness checks from defense mode activation, streamlining the data validation process.
- Enhanced comments for clarity on the rationale behind the changes.
2026-01-21 16:30:51 +08:00
discountry e144c1822f Implement data staleness defense mode in MakerPointsEngine
- Introduced a defense mode that activates when data from StandX or Binance is stale for over 5 seconds.
- Added methods to check data freshness, enter and exit defense mode, and cancel all orders during defense mode.
- Enhanced logging to provide insights into data staleness and defense mode transitions.
- Updated connection state management for clarity and consistency.
2026-01-21 16:24:17 +08:00
discountry 69271d33ca Refactor MakerPointsEngine and BinanceDepthTracker for improved connection management
- Renamed connection state variable in MakerPointsEngine for clarity.
- Added connection state change listeners in BinanceDepthTracker to handle connection status updates.
- Implemented heartbeat monitoring and connection duration checks in BinanceDepthTracker to enhance WebSocket reliability.
- Introduced data staleness checks and improved error handling for WebSocket connections.
- Enhanced logging for connection events to provide better insights into connection status changes.
2026-01-21 16:03:34 +08:00
discountry f1140f106a Enhance WebSocket connection management and data handling
- Introduced constants for WebSocket reconnection delays, heartbeat timeout, and data staleness thresholds.
- Implemented heartbeat monitoring to ensure timely reconnections on inactivity.
- Added data staleness checks to trigger REST API calls when market or account data is outdated.
- Enhanced the StandxGateway class with methods for managing heartbeat and data checks, improving overall connection reliability and data integrity.
2026-01-21 15:40:28 +08:00
discountry 3b935b7979 Refactor MakerPoints configuration and depth handling
- Renamed `band0To10MinDepth` to `filterMinDepth` in `config.ts` for clarity.
- Updated `MakerPointsEngine` to utilize the new `filterMinDepth` for depth checks across all bands.
- Introduced a method to track depth status changes, enhancing order placement logic based on market depth.
- Improved logging for depth-related order skips to provide clearer insights into trading decisions.
2026-01-21 11:26:21 +08:00
discountry 00388f9166 add filter 2026-01-21 11:11:58 +08:00
discountry a32efa2ba0 Refine target price calculation in LiquidityMakerEngine
- Updated target price logic to consider entry price when no recent fills are available, enhancing order placement accuracy.
- Adjusted conditions to ensure target prices are set appropriately based on market conditions and entry prices, preventing potential losses.
- Improved comments for clarity on the logic behind target price adjustments.
2026-01-20 01:28:24 +08:00
discountry 76704b6bdd Enhance entry price logic in Maker and Liquidity Maker strategies
- Added `entryDepthLevel` configuration option to `MakerConfig` and `LiquidityMakerConfig` for specifying order entry levels.
- Implemented `getPricesAtLevel` utility function to retrieve bid and ask prices at specified depth levels.
- Updated `MakerEngine`, `LiquidityMakerEngine`, and `OffsetMakerEngine` to utilize the new entry level logic for determining opening prices based on market depth.
- Improved price handling to ensure more accurate order placements in varying market conditions.
2026-01-20 01:03:38 +08:00
discountry 168d8cbb08 Add Claude instructions and enhance stop-loss logic
- Introduced a new `CLAUDE.md` file with instructions for using Bun as the package manager.
- Adjusted stop-loss cooldown and check intervals in `MakerPointsEngine` for improved responsiveness.
- Implemented a new method to compute real-time PnL using live depth data, enhancing stop-loss decision-making.
- Added retry logic for stop-loss execution to ensure positions are closed effectively, with detailed logging for failures.
2026-01-20 00:51:15 +08:00
discountry 9629c22496 Enhance MakerPoints configuration and logic
- Added new configuration options for band-specific order amounts in `config.ts`.
- Implemented conditional logic in `MakerPointsEngine` to utilize the new band amounts based on the Binance depth cancel setting.
- Refactored order amount handling to improve clarity and maintainability.
2026-01-18 01:49:36 +08:00
discountry a34d06f9b4 fix slprice 2026-01-16 23:05:30 +08:00
discountry 2ba3e80ad9 fix sl 2026-01-16 22:59:38 +08:00
discountry 12e8e3e064 Update API token creation date in documentation and configuration
- Revised the `.env.example` and `maker-points-guide.md` to reflect the updated token creation date from 2025-01-15 to 2026-01-15.
- Enhanced the `order-coordinator.ts`, `order-schema.ts`, and `types.ts` files to support stop-loss and take-profit price parameters in order intents.
- Updated the `StandxGateway` and `order.ts` to handle new stop-loss and take-profit parameters in order creation.
- Improved the `MakerPointsEngine` to calculate stop-loss prices based on order type, enhancing order management capabilities.
2026-01-16 11:29:26 +08:00
discountry aa24995d28 Enhance WebSocket and API documentation; implement connection protection features
- Added a note in the HTTP API documentation regarding the non-guaranteed sequence of price levels in order book responses.
- Updated WebSocket documentation to include a connection duration limit and a note on local sorting requirements for price levels.
- Introduced connection event handling in the ExchangeAdapter interface, allowing for disconnection and reconnection events.
- Implemented connection protection logic in the StandxExchangeAdapter and MakerPointsEngine to manage order states during connection disruptions.
- Enhanced the StandxGateway with methods for querying open orders and forcefully canceling all orders, improving reliability during network issues.
2026-01-16 10:49:16 +08:00
discountry d493642935 update doc 2026-01-15 22:06:10 +08:00
discountry 86670486a6 Update StandX API documentation and configuration
- Revised `.env.example` to reflect new API token generation process, emphasizing the use of creation date and validity days for token expiry management.
- Enhanced `auth.md` with detailed instructions for obtaining API tokens and signing transactions for both EVM and Solana wallets.
- Updated `maker-points-guide.md` to clarify the API token retrieval process and the significance of the Ed25519 private key.
- Refactored `config.ts` and `gateway.ts` to support new token expiry configuration methods and improved private key handling, including Base58 decoding.
- Improved overall documentation clarity and user guidance for new and existing users.
2026-01-15 16:11:08 +08:00
discountry 6496011d8f Add Nado exchange support to README
- Included details for the Nado USDC perpetuals, specifying required environment variables and configuration options.
- Updated both English and Chinese versions of the README to reflect the new exchange integration, enhancing user guidance and clarity.
2026-01-14 18:28:14 +08:00
discountry 792351ab8a Add Liquidity Maker strategy and related configurations
- Introduced a new `LiquidityMakerConfig` interface and corresponding configuration settings in `config.ts`.
- Updated CLI argument handling to include the new "liquidity-maker" strategy option.
- Implemented the `LiquidityMakerEngine` class to manage the liquidity making strategy, including order handling and risk management.
- Added a new `LiquidityMakerApp` component for user interaction and display of strategy status.
- Enhanced internationalization support with translations for the liquidity maker strategy.
- Updated the main application to integrate the new liquidity maker strategy into the existing framework.
2026-01-14 00:56:29 +08:00
discountry 4915dc574e Implement precision error handling in MakerPointsEngine
- Added a new `isPrecisionError` function to identify precision-related errors in the error utility module.
- Updated the MakerPointsEngine to handle precision errors by logging warnings and synchronizing precision when such errors occur during order processing and stop-loss execution.
- Enhanced the `syncPrecision` method to allow forced synchronization, improving the handling of precision-related issues.
2026-01-13 20:23:18 +08:00
discountry 9866e8068f Clarify instructions in Maker Points guide regarding the proxy wallet private key format and environment variable setup. Emphasize that the private key should be copied as is, without the '0x' prefix, to enhance user understanding and security practices. 2026-01-12 18:03:26 +08:00
discountry 099af3ce01 Update Maker Points guide to clarify proxy wallet private key format and environment variable instructions. Specify that the private key should generally not include the '0x' prefix, enhancing user understanding and security practices. 2026-01-12 18:02:05 +08:00
discountry 445e634aa1 Refactor Telegram notification handling and remove unused functions
- Removed deprecated functions for masking sensitive data and previewing text, streamlining the Telegram notification process.
- Simplified logging by eliminating unnecessary console outputs related to notification configuration and sending.
- Updated the `TelegramNotifier` class to enhance clarity and maintainability, focusing on essential notification functionality.
2026-01-12 12:39:37 +08:00
discountry 4bb1fee995 Refactor Telegram notification handling in MakerPointsEngine
- Introduced a dedicated `notify` method to streamline notification sending and improve logging for Telegram notifications.
- Added a new environment variable check for enabling debug logging of Telegram notifications.
- Enhanced logging to include detailed information about notification attempts, including masked sensitive data for security.
- Updated various notification calls to utilize the new `notify` method, ensuring consistent logging and functionality.
2026-01-12 12:23:24 +08:00
discountry aad14395e0 Enhance Telegram notification functionality
- Introduced functions to mask sensitive information and preview notification text for improved logging and security.
- Added detailed logging for notification sending process, including configuration details and response handling.
- Implemented checks to prevent sending notifications when bot token or chat ID is missing, with appropriate warnings logged.
2026-01-12 12:12:14 +08:00
discountry 598f2a0eb6 Add token expiry and Telegram notification features
- Introduced `STANDX_TOKEN_EXPIRY` configuration to manage token expiration, including handling logic for active, expired, and silent states.
- Implemented Telegram notifications for key events such as order filled, position opened/closed, stop loss triggered, and token expiration.
- Updated Maker Points engine to integrate token expiry checks and notification sending, enhancing user awareness of trading conditions.
- Enhanced documentation to include details on configuring token expiry and Telegram notifications for improved user guidance.
2026-01-10 12:44:52 +08:00
discountry cb1cef6f1b Revise Maker Points guide to provide a comprehensive step-by-step tutorial for new users. Update installation instructions for Bun, enhance clarity on obtaining StandX login credentials, and improve environment variable configuration details. Add safety tips and common troubleshooting questions to support user onboarding. 2026-01-09 01:11:10 +08:00
discountry fd034d493f Enhance README with language setting instructions, updated referral links, and additional documentation for StandX and Nado exchanges. Clarify environment variable setup and improve formatting for better readability. 2026-01-07 23:39:40 +08:00
discountry 2551670874 Update README and Maker Points guide to clarify the export of StandX login credentials, specifying token and proxy wallet private key for enhanced user security. 2026-01-07 02:46:38 +08:00
discountry 3ec7e9b8d6 Update Maker Points guide to include details on exporting the generated proxy wallet private key along with the token, enhancing security instructions for users. 2026-01-06 21:49:28 +08:00
discountry 499ee692da Merge branch 'main' into feat/mm 2026-01-06 20:17:49 +08:00
discountry de13142950 Add StandX Maker Points strategy tutorial and update README with configuration details. Include steps for obtaining login token and environment variable setup for new users, enhancing documentation clarity and usability. 2026-01-06 20:08:38 +08:00
DisneyandGitHub 597e41f053 Merge pull request #16 from discountry/feat/mm
Feat/mm
2026-01-06 17:50:38 +08:00
discountry b0a33a58d1 Remove market maker documentation from StandX. This deletion includes all content related to Maker Points, order management, and WebSocket API details, streamlining the documentation for clarity and focus. 2026-01-06 17:49:29 +08:00
discountry 65b9f21981 Add new PM2 start command for Maker Points strategy in package.json. This includes a dedicated command for 'maker-points' with specific exchange settings, enhancing deployment options for the application. 2026-01-06 16:37:51 +08:00
discountry 33b5407245 Refactor Maker Points logic by removing dislocation calculations and related UI elements. Update MakerPointsEngine to utilize new price fetching methods and streamline order synchronization. Adjust translations and tests accordingly to reflect these changes. 2026-01-06 16:18:54 +08:00
discountry 7aafc3b69d Add Maker Points strategy support in StandX. Introduce new configuration for Maker Points, including point bands and order management logic. Implement MakerPointsEngine for handling order placement and tracking. Update CLI and UI components to integrate Maker Points functionality, enhancing user experience and strategy options. 2026-01-06 16:01:10 +08:00
discountry 9a093459bc Add debugging options and enhance WebSocket handling in StandxGateway. Introduce debugWs and debugWsRaw flags for improved logging of WebSocket events and payloads. Implement decrossDepthBook function for better order book management and update message handling to support multiple JSON payloads. 2025-12-21 16:54:14 +08:00
discountry aa36b0cfdc Implement depth level normalization in StandxGateway for improved order book handling. Refactor depth data processing to utilize the new normalizeDepthLevels function, ensuring consistent bid and ask sorting. Update symbol handling to accommodate fallback logic. 2025-12-21 16:15:44 +08:00
discountry 93c6409688 Integrate StandX exchange support by updating configuration files, adding environment variables, and enhancing documentation. Include new API endpoints and authentication details for StandX in README and dedicated documentation files. Update CLI and adapter logic to accommodate StandX functionalities. 2025-12-21 15:37:03 +08:00
discountry 84d5e1f3d7 Update README files to include Nado referral link and detailed setup instructions for Nado integration, enhancing user guidance for configuration and usage. 2025-12-20 14:53:25 +08:00
discountry 6e5413ec1e Refactor order status handling in NadoGateway and MakerEngine. Introduce isOrderActiveStatus utility to streamline order filtering logic. Add tests for error handling and order status utilities. 2025-12-20 13:10:48 +08:00
discountry 84e8ce1d43 Enhance NadoGateway with min size policy handling and related utility functions. Update .env.example to include NADO_MIN_SIZE_POLICY configuration option. 2025-12-19 11:11:35 +08:00
discountry c69ea72860 Add Nado documentation and examples, including new API endpoints, FAQs, and guides for using the TypeScript SDK. Update .env.example with additional configuration options. 2025-12-19 01:38:09 +08:00
discountry 624fecfa70 add nado packages 2025-12-18 03:14:20 +08:00
discountry 6dcf13481d Enhance README.md with a detailed description of the Bun-powered multi-exchange perpetuals workstation, highlighting key features such as the SMA30 trend engine, Guardian stop sentinel, and market-making modes. 2025-12-09 00:38:23 +08:00
discountry 3fd0f715a1 Update README.md to instruct users to set LANG=en in .env for English interface support. 2025-12-09 00:36:50 +08:00
discountry 85e7f245c0 Implement internationalization support by adding translation functionality and updating UI components to use translated strings. Add language configuration in .env.example and integrate translations across various strategy and UI components. 2025-12-09 00:35:28 +08:00
discountry 03df1006cc Update README.md to include a link for English users and remove the outdated English README reference. 2025-12-09 00:00:28 +08:00
DisneyandGitHub 3099cb1319 Merge pull request #13 from discountry/feat/lighter-spot
Feat/lighter spot
2025-12-08 23:57:07 +08:00
discountry 49b8f0bcf1 fix perp 2025-12-08 23:55:13 +08:00
discountry a23eb91f04 Remove lighter-go-main directory and associated files, including client, types, and documentation, to streamline the project structure. Update .gitignore to exclude temporary files. 2025-12-08 23:44:05 +08:00
discountry 974d3017e0 fix position 2025-12-08 19:57:57 +08:00
discountry 668c3a57d3 fix open orders 2025-12-08 01:28:33 +08:00
discountry 819ce93a90 fix stoploss 2025-12-07 23:35:10 +08:00
discountry 061a8dd8f1 fix: improve lighter ws reconnect 2025-12-07 23:18:29 +08:00
discountry 49df78c075 fix nonce 2025-12-07 22:48:23 +08:00
discountry 6a6b61c491 fix nonce 2025-12-07 22:36:37 +08:00
discountry 7612e25dd1 add stoploss 2025-12-07 22:33:56 +08:00
discountry 705e489c17 fix nonce 2025-12-07 22:28:48 +08:00
discountry 9a418aba41 add trend skip 2025-12-07 22:15:34 +08:00
discountry 2cb6c4fef1 feat: add minBaseAmount and minQuoteAmount to ExchangePrecision and LighterGateway for improved trading logic 2025-12-07 21:52:35 +08:00
discountry 180f47b6e0 support lighter spot 2025-12-07 21:37:58 +08:00
discountry 5f79b134f2 refactor: restructure Lighter SDK by removing deprecated Go files and enhancing Python documentation with new examples and models 2025-12-07 19:14:56 +08:00
discountry 85705dd27c chore: update referral links in README files for Lighter and Aster 2025-11-29 01:57:44 +08:00
discountry 732525b394 Merge branch 'main' into dev 2025-10-06 20:15:05 +08:00
discountry 627ed36de4 feat: 添加 EdgeX 交易所适配器及相关客户端实现,支持订单、深度和K线数据处理 2025-10-04 20:15:41 +08:00
612 changed files with 100166 additions and 8775 deletions
+68 -12
View File
@@ -1,14 +1,36 @@
# UI language (zh | en)
LANG=zh
# Exchange selection
EXCHANGE=aster # Pick aster (default) or grvt/lighter/backpack/paradex
EXCHANGE=aster # Pick aster (default) or standx/grvt/lighter/backpack/paradex/nado
# Aster API credentials
ASTER_API_KEY=
ASTER_API_SECRET=
# StandX authentication (set when EXCHANGE=standx)
STANDX_TOKEN=
STANDX_SYMBOL=BTC-USD
# STANDX_BASE_URL=https://perps.standx.com
# STANDX_WS_URL=wss://perps.standx.com/ws-stream/v1
# STANDX_SESSION_ID=
# Optional: request signing key (ed25519 private key, supports hex or base58 format)
# STANDX_REQUEST_PRIVATE_KEY=
# Token expiry configuration (recommended method: creation date + validity days)
# Get these values when generating API token at https://standx.com/user/session
# STANDX_TOKEN_CREATE_DATE=2026-01-15 # Token creation date (YYYY-MM-DD format)
# STANDX_TOKEN_VALIDITY_DAYS=30 # Token validity period in days
# Legacy method: direct expiry timestamp (Unix seconds)
# STANDX_TOKEN_EXPIRY=1737092800
# Core trading symbol and sizing
TRADE_SYMBOL=BTCUSDT # Trading pair symbol
TRADE_AMOUNT=0.001 # Base order quantity (base asset, e.g. BTC)
# Swing Trading
SWING_DIRECTION=short # short | long | both
SWING_STOP_LOSS_PCT=0.05 # 0.05 = 5%
# Risk management (USD amounts unless noted)
LOSS_LIMIT=0.04 # Max loss per trade in USDT before forced close
TRAILING_PROFIT=0.2 # Trailing stop activation profit (USDT)
@@ -37,6 +59,10 @@ MAKER_REFRESH_INTERVAL_MS=500 # Maker refresh cadence (ms)
MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Maker close slippage guard (fallbacks to MAX_CLOSE_SLIPPAGE_PCT)
MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK)
# Maker-points Binance depth imbalance monitor
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3 # Binance depth monitor window around best bid/ask (bps)
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9 # Imbalance threshold ratio (e.g. 9 => one side >= 9x)
# Grid strategy defaults
GRID_LOWER_PRICE=25000 # Grid lower bound price (quote currency)
GRID_UPPER_PRICE=35000 # Grid upper bound price
@@ -95,6 +121,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.
@@ -115,15 +151,35 @@ PARADEX_WALLET_ADDRESS=
# Enable verbose adapter logging: set to "1" or "true"
# PARADEX_DEBUG=false
# Extended exchange configuration (Starknet)
# Set EXCHANGE=extended to activate. Requires on-chain Stark key + vault id.
EXTENDED_API_KEY=
EXTENDED_STARK_PRIVATE_KEY= # 0x-prefixed Stark private key for signing orders
EXTENDED_VAULT_ID= # Vault/sub-account id from Extended UI
EXTENDED_MARKET=BTC-USD # Defaults to TRADE_SYMBOL when omitted
# Nado exchange configuration (Ink mainnet)
# Requires a linked signer private key + your original subaccount owner EVM address.
# When EXCHANGE=nado these values are used automatically.
# Optional overrides (defaults target mainnet host)
# EXTENDED_API_HOST=api.starknet.extended.exchange
# EXTENDED_STREAM_HOST=api.starknet.extended.exchange
# EXTENDED_PRIVATE_STREAM_HOST=api.starknet.extended.exchange
# EXTENDED_USER_AGENT=ritmex-bot
NADO_ENV=inkMainnet # inkMainnet | inkTestnet
NADO_SYMBOL=BTC-PERP # Trading product symbol (e.g., BTC-PERP / ETH-PERP)
NADO_SIGNER_PRIVATE_KEY= # 32-byte 0x-prefixed private key (0x...)
NADO_SUBACCOUNT_OWNER= # EVM address of the subaccount owner (0x...)
NADO_SUBACCOUNT_NAME=default # Subaccount name (bytes12, default "default")
# Optional: market-order slippage buffer (used for IOC limit-as-market, e.g. 0.01 => 1%)
NADO_MARKET_SLIPPAGE_PCT=0.01
# Optional: stop trigger source for STOP_MARKET orders (oracle | last | mid)
NADO_STOP_TRIGGER_SOURCE=oracle
# Optional: how to handle orders smaller than Nado min_size (USDT0 notional)
# - adjust: round quantity up to the minimum allowed size (default)
# - reject: throw an error instead of auto-adjusting
NADO_MIN_SIZE_POLICY=adjust
# Optional endpoint overrides
# NADO_GATEWAY_WS_URL=wss://gateway.prod.nado.xyz/v1/ws
# NADO_SUBSCRIPTIONS_WS_URL=wss://gateway.prod.nado.xyz/v1/subscribe
# NADO_ARCHIVE_URL=https://archive.prod.nado.xyz/v1
# NADO_TRIGGER_URL=https://trigger.prod.nado.xyz/v1
# NADO_DEBUG=false
# Telegram notification configuration
# TELEGRAM_BOT_TOKEN= # Telegram bot token from @BotFather
# TELEGRAM_CHAT_ID= # Chat ID to receive notifications
# TELEGRAM_ACCOUNT_LABEL= # Account label to distinguish multiple bot instances (e.g., "Account-A")
+3
View File
@@ -33,3 +33,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
.tmp
.tmp/*
+12
View File
@@ -0,0 +1,12 @@
# RitMEX Bot - Claude Instructions
## Package Manager
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
- 安装依赖: `bun install`
- 运行脚本: `bun run <script>`
- 执行测试: `bun test`
- 类型检查: `bun run typecheck`
**不要使用 npm、yarn 或 npx**
+50 -3
View File
@@ -1,19 +1,27 @@
# ritmex-bot
> For English users, please see [README_en.md](README_en.md).
Please set `LANG=en` in `.env` for English interface.
A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard.
基于 Bun 的多交易所永续合约量化终端,内置趋势跟随(SMA30)、Guardian 防守与做市策略,支持快速恢复、实时行情订阅、日志追踪与 CLI 仪表盘。
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
* [Aster 30% 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
* [Lighter 手续费优惠注册链接](https://app.lighter.xyz/?referral=111909FA)
* [Aster 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
* [StandX 手续费优惠注册链接](https://standx.com/referral?code=xingxingjun)
* [Binance 手续费优惠注册链接](https://www.binance.com/join?ref=KNKCA9XC)
* [GRVT 手续费优惠注册链接](https://grvt.io/exchange/sign-up?ref=sea)
* [Nado 手续费优惠注册链接](https://app.nado.xyz?join=LKbIUs5)
* [Backpack 手续费优惠注册链接](https://backpack.exchange/join/ritmex)
* [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL)
* [Paradex 手续费优惠注册链接](https://paradex.io/ref/xingxingjun)
* [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA)
## 文档索引
- [English README](README_en.md)
- [简明上手指南(零基础)](simple-readme.md)
- [基础网格策略使用教程](grid-trading.md)
@@ -28,10 +36,12 @@
| 交易所 | 合约类型 | 必填环境变量 | 备注 |
| --- | --- | --- | --- |
| Aster | USDT 永续 | `ASTER_API_KEY`, `ASTER_API_SECRET` | 默认交易所;兼容脚本引导
| StandX | USD 永续 | `STANDX_TOKEN` | 使用 JWT Token 登录,优先走 WebSocket 推送
| GRVT | USDT 永续 | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID` | `GRVT_ENV` 可切换 `prod`/`testnet`
| Lighter | zkLighter 永续 | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_PRIVATE_KEY` | 默认 `LIGHTER_ENV=testnet`
| Backpack | USDC 永续 | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | `BACKPACK_SANDBOX=true` 启用沙盒
| Paradex | StarkEx 永续 | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_SANDBOX=true` 使用测试网
| Nado | USDC 永续 | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | `NADO_ENV` 可切换 `inkMainnet`/`inkTestnet`
## 系统要求
- Bun ≥ 1.2(需同时包含 `bun``bunx` 命令)
@@ -76,7 +86,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
| 变量 | 说明 |
| --- | --- |
| `EXCHANGE` | 选择交易所(`aster`/`grvt`/`lighter`/`backpack`/`paradex` |
| `EXCHANGE` | 选择交易所(`aster`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado` |
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT` |
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
@@ -101,6 +111,36 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
3. 根据交易对调整 `TRADE_SYMBOL`、`PRICE_TICK`、`QTY_STEP` 等精度参数。
4. 一键脚本会自动写入这些变量,手动部署时需自行维护。
### StandX
* [StandX 做市策略教程](docs/standx/maker-points-guide.md)
策略需要 StandX 的 API Token 和签名私钥才能下单。
**获取方式(使用 StandX 官方 API 生成功能):**
1. 打开 StandX 官方 API 创建页面:https://standx.com/user/session
2. 连接钱包并登录
3. 点击 **"Generate API Token"** 按钮
4. 页面会显示以下信息:
- **Token**(以 `eyJ` 开头的 JWT 字符串)→ 填入 `STANDX_TOKEN`
- **Ed25519 Private Key**Base58 格式私钥,类似 `HdsyJD7oWgT...`)→ 填入 `STANDX_REQUEST_PRIVATE_KEY`
- **创建日期** 和 **有效期天数** → 用于配置 Token 过期提醒
> Ed25519 Private Key 是系统自动生成的签名私钥,仅用于交易请求签名,你的资产仍在主钱包中,非常安全。
请妥善保存这些凭证,不要分享给他人。
**配置步骤:**
1. 设置 `EXCHANGE=standx`。
2. 填写 `STANDX_TOKEN`Perps API 的 JWT Token)。
3. 填写 `STANDX_REQUEST_PRIVATE_KEY`Ed25519 签名私钥,Base58 格式)。
4. 设置 `STANDX_SYMBOL`(默认 `BTC-USD`),并校准 `PRICE_TICK` / `QTY_STEP`。
5. 推荐配置 Token 过期时间:
- `STANDX_TOKEN_CREATE_DATE`(创建日期,格式 `YYYY-MM-DD`
- `STANDX_TOKEN_VALIDITY_DAYS`(有效期天数)
6. 可选:`STANDX_BASE_URL`、`STANDX_WS_URL`、`STANDX_SESSION_ID` 用于自定义环境。
### GRVT
1. 在 `.env` 中设置 `EXCHANGE=grvt`。
2. 填写 `GRVT_API_KEY`、`GRVT_API_SECRET`、`GRVT_SUB_ACCOUNT_ID`。
@@ -125,6 +165,13 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
3. 默认连接主网,若需测试网,将 `PARADEX_SANDBOX=true` 并根据需要调整 `PARADEX_SYMBOL`。
4. 复杂环境可额外设置 `PARADEX_USE_PRO`、`PARADEX_RECONNECT_DELAY_MS` 或调试开关。
### Nado
1. 设置 `EXCHANGE=nado`。
2. 在 Nado 官网(交易界面)打开开发者工具(F12)→ 切换到 `Application` → `Local Storage`,找到 `nado.userSettings`,在其内容中取出 `privateKey` 字段并填入 `.env` 的 `NADO_SIGNER_PRIVATE_KEY`。
3. 提供 `NADO_SUBACCOUNT_OWNER`(或 `NADO_EVM_ADDRESS`)。
4. 选择网络 `NADO_ENV=inkMainnet`(主网)或 `inkTestnet`(测试网)。
5. 设置交易品种 `NADO_SYMBOL`(交易对格式类似 `BTC-PERP`;也支持输入 `BTCUSDT0`,会自动映射为 `BTC-PERP`)。
## 命令速查
```bash
bun run index.ts # 启动 CLI(默认入口)
+55 -9
View File
@@ -1,18 +1,25 @@
# ritmex-bot
**Language Setting**: Set `LANG=en` in your `.env` file to display the CLI interface in English.
A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard.
* [Aster referral (30% fee discount)](https://www.asterdex.com/en/referral/4665f3)
If you'd like to support this project and get fee discounts, please consider using these referral links:
* [Lighter referral link](https://app.lighter.xyz/?referral=111909FA)
* [Aster referral link](https://www.asterdex.com/en/referral/4665f3)
* [StandX referral link](https://standx.com/referral?code=xingxingjun)
* [Binance referral link](https://www.binance.com/join?ref=KNKCA9XC)
* [GRVT referral link](https://grvt.io/exchange/sign-up?ref=sea)
* [Nado referral link](https://app.nado.xyz?join=LKbIUs5)
* [Backpack referral link](https://backpack.exchange/join/ritmex)
* [edgex referral link](https://pro.edgex.exchange/referral/BULL)
* [Paradex referral link](https://paradex.io/ref/xingxingjun)
* [Apex referral link](https://join.omni.apex.exchange/RITHMEX)
* [Apex referral link](https://join.omni.apex.exchange/SEA)
## Documentation Map
- [中文 README](README.md)
- [Beginner-friendly Quick Start](simple-readme.md)
- [Grid Trading Strategy Guide](grid-trading.md)
## Highlights
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
@@ -25,13 +32,15 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
| Exchange | Contract Type | Required Environment Variables | Notes |
| --- | --- | --- | --- |
| Aster | USDT perpetuals | `ASTER_API_KEY`, `ASTER_API_SECRET` | Default venue; works with the bootstrap script |
| StandX | USD perpetuals | `STANDX_TOKEN` | Uses JWT token auth; prefer websocket streams |
| GRVT | USDT perpetuals | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID` | Switch `GRVT_ENV` between `prod` and `testnet` |
| Lighter | zkLighter perpetuals | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_PRIVATE_KEY` | Defaults to `LIGHTER_ENV=testnet` |
| Backpack | USDC perpetuals | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | Set `BACKPACK_SANDBOX=true` for the sandbox |
| Paradex | StarkEx perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | Toggle `PARADEX_SANDBOX=true` for the testnet |
| Nado | USDC perpetuals | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | Switch `NADO_ENV` between `inkMainnet` and `inkTestnet` |
## Requirements
- Bun 1.2 (both `bun` and `bunx` on PATH)
- Bun >= 1.2 (both `bun` and `bunx` on PATH)
- macOS, Linux, or Windows via WSL (native Windows works but WSL is recommended)
- Node.js is optional unless your tooling requires it
@@ -73,7 +82,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
| Variable | Purpose |
| --- | --- |
| `EXCHANGE` | Choose the venue (`aster` / `grvt` / `lighter` / `backpack` / `paradex`) |
| `EXCHANGE` | Choose the venue (`aster` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
| `TRADE_AMOUNT` | Order size in base asset units |
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
@@ -92,12 +101,42 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
> ```
## Exchange Setup Guides
### Aster
1. Keep `EXCHANGE=aster` (default value).
2. Supply `ASTER_API_KEY` and `ASTER_API_SECRET`.
3. Adjust `TRADE_SYMBOL`, `PRICE_TICK`, and `QTY_STEP` to match the requested market.
4. The bootstrap script auto-populates these variables; manual installs must maintain them.
### StandX
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
The strategy requires a StandX API Token and signing private key to place orders.
**How to obtain (using StandX's official API generation feature):**
1. Open the StandX official API creation page: https://standx.com/user/session
2. Connect your wallet and log in
3. Click the **"Generate API Token"** button
4. The page will display the following information:
- **Token** (JWT string starting with `eyJ`) → Fill in `STANDX_TOKEN`
- **Ed25519 Private Key** (Base58 format, like `HdsyJD7oWgT...`) → Fill in `STANDX_REQUEST_PRIVATE_KEY`
- **Creation date** and **Validity days** → Used to configure token expiry reminders
> The Ed25519 Private Key is an auto-generated signing key used only for trade request signatures. Your assets remain in your main wallet and are completely safe.
Please keep these credentials safe and do not share them with anyone.
**Configuration steps:**
1. Set `EXCHANGE=standx`.
2. Provide `STANDX_TOKEN` (JWT token for perps API).
3. Provide `STANDX_REQUEST_PRIVATE_KEY` (Ed25519 signing private key, Base58 format).
4. Set `STANDX_SYMBOL` (defaults to `BTC-USD`) and align `PRICE_TICK` / `QTY_STEP`.
5. Recommended: configure token expiry settings:
- `STANDX_TOKEN_CREATE_DATE` (creation date, format `YYYY-MM-DD`)
- `STANDX_TOKEN_VALIDITY_DAYS` (validity days)
6. Optional: `STANDX_BASE_URL`, `STANDX_WS_URL`, or `STANDX_SESSION_ID` for custom endpoints.
### GRVT
1. Set `EXCHANGE=grvt` inside `.env`.
2. Fill `GRVT_API_KEY`, `GRVT_API_SECRET`, and `GRVT_SUB_ACCOUNT_ID`.
@@ -106,22 +145,29 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
### Lighter
1. Set `EXCHANGE=lighter`.
2. Provide `LIGHTER_ACCOUNT_INDEX` and `LIGHTER_API_PRIVATE_KEY` (40-byte hex private key).
2. Provide `LIGHTER_ACCOUNT_INDEX` and `LIGHTER_API_PRIVATE_KEY` (40-byte hex private key). `LIGHTER_ACCOUNT_INDEX` is your account index, which you can find by opening DevTools (F12) on the official website and observing API requests. `LIGHTER_API_PRIVATE_KEY` is your API private key.
3. Switch `LIGHTER_ENV` to `mainnet`, `staging`, or `dev` when necessary; override `LIGHTER_BASE_URL` if endpoints differ.
4. `LIGHTER_SYMBOL` defaults to `BTCUSDT`; override price/size decimals when markets differ.
### Backpack
1. Set `EXCHANGE=backpack`.
2. Populate `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, and `BACKPACK_PASSWORD`; add `BACKPACK_SUBACCOUNT` if you trade from a subaccount.
2. Populate `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, and `BACKPACK_PASSWORD`; add `BACKPACK_SUBACCOUNT` if you trade from a subaccount (defaults to main account ID).
3. Toggle `BACKPACK_SANDBOX=true` for the sandbox environment and verify `BACKPACK_SYMBOL` matches the contract (defaults to `BTC_USD_PERP`).
4. Enable `BACKPACK_DEBUG=true` for verbose adapter logging.
### Paradex
1. Set `EXCHANGE=paradex`.
2. Provide `PARADEX_PRIVATE_KEY` (EVM private key) and `PARADEX_WALLET_ADDRESS`.
2. Provide `PARADEX_PRIVATE_KEY` (EVM private key) and `PARADEX_WALLET_ADDRESS`. Note: These are your EVM wallet address and private key. It is recommended to create a brand new wallet and avoid storing unrelated assets in it.
3. The adapter connects to mainnet by default; enable `PARADEX_SANDBOX=true` and adjust `PARADEX_SYMBOL` for testnet usage.
4. Advanced tuning: use `PARADEX_USE_PRO`, `PARADEX_RECONNECT_DELAY_MS`, or debug flags as needed.
### Nado
1. Set `EXCHANGE=nado`.
2. On the Nado web app (trading interface), open DevTools (F12) -> switch to the `Application` tab -> `Local Storage`, locate `nado.userSettings`, then grab the `privateKey` field from its JSON value and paste it into `.env` as `NADO_SIGNER_PRIVATE_KEY`.
3. Provide `NADO_SUBACCOUNT_OWNER` (or `NADO_EVM_ADDRESS`).
4. Select network via `NADO_ENV=inkMainnet` (mainnet) or `inkTestnet` (testnet).
5. Set `NADO_SYMBOL` using Nado product symbols like `BTC-PERP` (it also accepts `BTCUSDT0` and maps it to `BTC-PERP`).
## Command Cheatsheet
```bash
bun run index.ts # Launch the CLI (default entrypoint)
@@ -169,7 +215,7 @@ bun x vitest --watch
```
## Troubleshooting
- Keep at least 50100 USDT in the account before deploying a live strategy.
- Keep at least 50-100 USDT in the account before deploying a live strategy.
- Configure leverage on the exchange manually (~50x is recommended); the bot will not change it.
- Ensure your server or workstation clock is in sync to avoid signature errors.
- Accounts must run in one-way position mode.
+351 -159
View File
@@ -1,168 +1,186 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "ritmex-bot",
"dependencies": {
"@grvt/client": "^1.6.4",
"@grvt/client": "^1.6.25",
"@nadohq/client": "^0.1.0-alpha.45",
"@noble/ed25519": "^3.0.0",
"@x10xchange/stark-crypto-wrapper-wasm": "^0.2.0",
"axios": "^1.12.2",
"@starkware-industries/starkware-crypto-utils": "^0.2.1",
"axios": "^1.13.4",
"bignumber.js": "^9.3.1",
"ccxt": "^4.5.12",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"ethereum-cryptography": "^2.1.3",
"ink": "^6.3.1",
"react": "^19.1.1",
"starknet": "^8.5.4",
"ws": "^8.18.3",
"ccxt": "^4.5.35",
"dotenv": "^17.2.3",
"ethereum-cryptography": "^2.2.1",
"ink": "^6.6.0",
"react": "^19.2.4",
"trading-signals": "^7.4.3",
"viem": "^2.45.1",
"ws": "^8.19.0",
},
"devDependencies": {
"@types/bun": "latest",
"vitest": "^3.2.4",
},
"peerDependencies": {
"typescript": "^5",
"typescript": "^5.9.2",
},
},
},
"packages": {
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA=="],
"@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@grvt/client": ["@grvt/client@1.6.4", "", { "dependencies": { "axios": "^1.12.2" } }, "sha512-yZfEvsC/BtkcdMjoB7Eh6gshBraz/9810qCllvNXDx6OmdxR+NfxPO+ucNlPbyjEt1HfSVzHBWB1yGnVl1usNA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@grvt/client": ["@grvt/client@1.6.26", "", { "dependencies": { "axios": "^1.13.5" } }, "sha512-Y6uWa3rirdYPU0BgQS7vYKghscTR7AwykQYZVSjib2GGbMW4JZJc/XX/IcZq0aL59W/S7ISechw+DhU/Amw2SQ=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.50", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.50", "@nadohq/indexer-client": "0.1.0-alpha.50", "@nadohq/shared": "0.1.0-alpha.50", "@nadohq/trigger-client": "0.1.0-alpha.50", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-9A7I5pDuvdKJCGrhadxmORceaW3YVlrMTXoSQg+dYPBflsQpKLJQaEXZZyn1Hb9tz9K6rNUxdt33aXL0gb6IKw=="],
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.50", "", { "dependencies": { "@nadohq/shared": "0.1.0-alpha.50", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-/mC0tH/h7oqmvj4lo+kWUTwDjGWtQl930WpvFdxQThIWJzXFxnb1eM0oGW5tCm2OxB05dwo5wHCmXH2fj+SO2Q=="],
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.50", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.50", "@nadohq/shared": "0.1.0-alpha.50", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-65+I8w/OWQBueUUwimhZf4QAYLH3pNOD19ZWNrkd9jdgigkDnqdra/620ZE1+RR1lUUSzvFy6Apus3M//uwxvw=="],
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.50", "", { "dependencies": { "abitype": "^1.2.3" }, "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-7WndVvF4M71yNiuuB8WNp61+QCN/J3Ke11xRQUO83hO6FZDd0Hc3NWINt2BUmgBnDDpmFkOopA59pYjxJX1v+A=="],
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.50", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.50", "@nadohq/shared": "0.1.0-alpha.50", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-kg5ytYVI/wszuGo6aG24iDoEk24ePygRl+Mq02OEz1CPbgt/S2aqRcTVmKLsw3R+6DK/zLFVSXo7odeJGXpZ3g=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="],
"@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="],
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="],
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
"@scure/starknet": ["@scure/starknet@1.1.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0" } }, "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ=="],
"@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=="],
"@starknet-io/starknet-types-08": ["@starknet-io/types-js@0.8.4", "", {}, "sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ=="],
"@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="],
"@starknet-io/starknet-types-09": ["@starknet-io/types-js@0.9.2", "", {}, "sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@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=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
"@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=="],
@@ -178,17 +196,19 @@
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@x10xchange/stark-crypto-wrapper-wasm": ["@x10xchange/stark-crypto-wrapper-wasm@0.2.0", "", {}, "sha512-pi7cHvDgPf9Ri9q0cvrUnCnucafB+F3ch7ia0oEJhKMY+bWvxWRYA2vve9s6UMMNV6PnmTyFZPOYptVGGdLTWA=="],
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
"abi-wan-kanabi": ["abi-wan-kanabi@2.2.4", "", { "dependencies": { "ansicolors": "^0.3.2", "cardinal": "^2.1.1", "fs-extra": "^10.0.0", "yargs": "^17.7.2" }, "bin": { "generate": "dist/generate.js" } }, "sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg=="],
"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-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="],
"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=="],
@@ -196,59 +216,107 @@
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"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=="],
"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.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
"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=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
"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.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"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.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"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=="],
"cardinal": ["cardinal@2.1.1", "", { "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" }, "bin": { "cdl": "./bin/cdl.js" } }, "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw=="],
"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.12", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-2lfL2TKjq4vBkQUQWJfDqFywhvYCZmk9r0SWC8GqA4AHZ6qozKVUJowxQTvdRsLX9jBwYSE0nc7JVurBrQ6SHg=="],
"ccxt": ["ccxt@4.5.40", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-8nJhhNKft7+fELDNQuCV2AQZBxLJfYQaD2LpqxHrucSszosyVl0u6lCIFfcnDUbeUW925uhrsKevHf2fNbLjKw=="],
"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=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
"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=="],
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"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=="],
"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=="],
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
"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.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
"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.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"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=="],
@@ -262,37 +330,41 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
"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=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
"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.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"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=="],
"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=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"form-data": ["form-data@4.0.5", "", { "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-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"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-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"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=="],
@@ -300,47 +372,95 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-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=="],
"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=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.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": "^5.1.1", "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.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.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-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="],
"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=="],
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
"js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="],
"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=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"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=="],
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
"ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
"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=="],
@@ -348,33 +468,61 @@
"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=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"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=="],
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
"redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="],
"randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
"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=="],
"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=="],
"ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"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.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"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=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -382,15 +530,21 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"starknet": ["starknet@8.9.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0", "@scure/base": "~1.2.1", "@scure/starknet": "1.1.0", "@starknet-io/starknet-types-08": "npm:@starknet-io/types-js@~0.8.4", "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", "abi-wan-kanabi": "2.2.4", "lossless-json": "^4.2.0", "pako": "^2.0.4", "ts-mixer": "^6.0.3" } }, "sha512-uhXSCKW0SMBGckd4yVlscVz4DMkQqZ+/RTpfxZ5J46e6AuCUHOqGKv61tqgUP9GhdvgHV/XyN+kdmlyZn3rXjA=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"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-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
@@ -404,80 +558,118 @@
"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=="],
"trading-signals": ["trading-signals@7.4.3", "", {}, "sha512-kOyzd85qhuhU7yGkB5z74MVP6j30xBCfZy5+bgBnvfxY1ZdvaQsdD+C5j+CoIRtCAgSOuxB7jmPpBrqO6h17sQ=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"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=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"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=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"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=="],
"viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"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.20", "", { "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-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
"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=="],
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"bip39/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"browserify-rsa/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="],
"@scure/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
"browserify-sign/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="],
"@scure/starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
"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=="],
"cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"ethereumjs-util/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"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=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"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=="],
"starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
"md5.js/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=="],
"starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
"ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@scure/starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
"ox/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"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=="],
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"rlp/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"secp256k1/node-addon-api": ["node-addon-api@5.1.0", "", {}, "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
"viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"viem/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"viem/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"wrap-ansi/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=="],
"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=="],
"md5.js/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=="],
"ox/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"ox/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"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=="],
"viem/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"viem/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"md5.js/hash-base/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"md5.js/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"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=="],
}
}
+785
View File
@@ -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.
+172
View File
@@ -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
+516
View File
@@ -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())
+7
View File
@@ -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
-5526
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
*.go text eol=lf
+2 -1
View File
@@ -1,4 +1,5 @@
.idea
vendor
build/*
!build/.keep
!build/.keep
/build
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+91 -6
View File
@@ -1,9 +1,94 @@
# lighter-go
In its current form, this repo serves as a starting point for anyone who wants to trade on Lighter using GO.
It covers all the signing procedures in order to trade on Lighter with an API key.
Minimal HTTP calls are implemented
On chain support, like depositing on Ethereum or modifying an API key directly with an Ethereum Tx are not supported yet.
This repository serves as the reference implementation of signing & hashing of Lighter transactions.
The sharedlib is compiled for a variety of platforms.
- macOS (darwin) dynamic library (.dylib) for arm architecture (M processor, not Intel)
- linux shared object (.so) for both amd64 and arm architectures
- windows .ddl (dynamic-link library) for amd64 architecture
At the moment, its main purpose is to offer visibility on the code behind the precompiled libraries used by the Python SDK.
If you'd like to compile your own binaries, the commands are in the `justfile`
The go SDK implements just the core signing, as well as a small HTTP client so that users can:
- not specify the nonce of the transaction (this will result in an HTTP call, so beware)
- check that a client was initialized correctly, by verifying that the given API key matches the one on the server
The [Python SDK](https://github.com/elliottech/lighter-python) offers support for HTTP and WebSocket functionality as well as [examples](https://github.com/elliottech/lighter-python/tree/main/examples) on how to generate the API keys, how to create and cancel orders, generate AUTH tokens for various HTTP/WS endpoints which require them.
All generated shared libraries follow the naming convention `lighter_signer_{os}_{arch}` where os is linux/windows/darwin and arch is amd64(x86) or arm64.\
The build & accompanying `.h` files can be found in the release notes [here](https://github.com/elliottech/lighter-go/releases).\
If you'd like to compile your own binaries, the commands are in the `justfile`.
## Transactions
```
=== Client ===
CreateClient
CheckClient
=== API Key ===
CreateAuthToken
SignChangePubKey
GenerateAPIKey
=== Order ===
SignCreateOrder
SignCreateGroupedOrders
SignCancelOrder
SignCancelAllOrders
SignModifyOrder
=== Leverage & Margin ===
SignUpdateLeverage
SignUpdateMargin
=== Transfers ===
SignWithdraw
SignTransfer
=== Sub account & pools ===
SignCreateSubAccount
SignCreatePublicPool
SignUpdatePublicPool
SignMintShares
SignBurnShares
```
## How to specify an account
Accounts are loaded into the signer by calling the `CreateClient` method. If you wish to load multiple API keys in the signer, you need to call the method multiple times, each time with the correct private key.
By default, signer will work out of the box with 1 client and no need to manage nonces in any specific way. Just pass `-1, 255, 0` for all methods (more explanations below).
You can call `CheckClient` to verify that the provided Private key & (apiKeyIndex, accountIndex) are configured correctly.
This checks that the public key associated with the pair (apiKey,account) matches the one from the exchange.
The majority of methods receive 3 arguments at the end:
- `nonce`
- default `-1`
- required to be strictly incremental
- you can fetch the next nonce using `nextNonce` HTTP call
- if default is passed, signer will do the HTTP call automatically
- ideally the caller should manage nonces locally to avoid latency
- `apiKeyIndex`
- default `255`
- specified which API key will be used for the specific transaction
- all API keys are equal. Orders created by one API key can be canceled by others, for example
- each API key has its own nonce
- if default is passed, signer will use the default txClient
- `accountIndex`
- default `0`
- specified which account will be used for the specific transaction
- this can be a subaccount or a different main account all together
- if default is passed, signer will use the default txClient
**Note:** in order to use the default client, you need to bash both the default values for `apiKeyIndex` and `accountIndex`
## Auth tokens
Auth tokens are used to call various HTTP & WS endpoints which hold sensitive information, like open orders.
An auth token is valid for 8 hours.
`CreateAuthToken(deadline=0)` will result in a token that's valid for 7 hours from now.
Calling `CreateAuthToken` with an expiry 20 hours in the future will work, but the token will start to be valid in 12 hours, because the max accepted deadline duration by the server is 8 hours. \
This still allows you to generate all the tokens ahead of time and use them accordingly. \
Such an approach (both implementation & how to manage them) can be found in great details in the [python-sdk](https://github.com/elliottech/lighter-python/tree/main/examples/read-only-auth).
**Note:** auth tokens are bound to an API key. Changing the API key to something else **will invalidate** all generated auth tokens.
@@ -0,0 +1,116 @@
package client
import (
"fmt"
"strings"
"sync"
curve "github.com/elliottech/poseidon_crypto/curve/ecgfp5"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// SharedClientManager holds the global txClient and backupTxClients
// This will be managed by both sharedlib and wasm builds
// Supports multiple accounts and API keys with thread safety
var (
txClientMu sync.Mutex
defaultTxClient *TxClient
defaultClientPerAccount = make(map[int64]*TxClient)
allTxClients map[int64]map[uint8]*TxClient // accountIndex -> apiKeyIndex -> client
)
// GenerateAPIKey generates a new API key pair from a seed
func GenerateAPIKey(seed string) (string, string, error) {
var seedP *string
if seed != "" {
seedP = &seed
}
key := curve.SampleScalar(seedP)
publicKeyStr := hexutil.Encode(schnorr.SchnorrPkFromSk(key).ToLittleEndianBytes())
privateKeyStr := hexutil.Encode(key.ToLittleEndianBytes())
return privateKeyStr, publicKeyStr, nil
}
// GetClient retrieves a client for specific account and API key
// If apiKeyIndex==255 && accountIndex==-1, returns default client
func GetClient(apiKeyIndex uint8, accountIndex int64) (*TxClient, error) {
txClientMu.Lock()
defer txClientMu.Unlock()
if apiKeyIndex == 255 && accountIndex != -1 {
client := defaultClientPerAccount[accountIndex]
if client != nil {
return client, nil
}
}
// Special case: return default client
if apiKeyIndex == 255 && accountIndex == -1 {
if defaultTxClient == nil {
return nil, fmt.Errorf("client is not created, call CreateClient() first")
}
return defaultTxClient, nil
}
// Look up client in double map
var c *TxClient
if allTxClients[accountIndex] != nil {
c = allTxClients[accountIndex][apiKeyIndex]
}
if c == nil {
return nil, fmt.Errorf("client is not created for apiKeyIndex: %v accountIndex: %v", apiKeyIndex, accountIndex)
}
return c, nil
}
// CreateClient creates a new TxClient and stores it
// httpClientFactory is a function that creates an HTTP client from a URL string
func CreateClient(httpClient MinimalHTTPClient, privateKey string, chainId uint32, apiKeyIndex uint8, accountIndex int64) (*TxClient, error) {
if accountIndex <= 0 {
return nil, fmt.Errorf("invalid account index")
}
txClientInstance, err := NewTxClient(httpClient, privateKey, accountIndex, apiKeyIndex, chainId)
if err != nil {
return nil, fmt.Errorf("error occurred when creating TxClient. err: %v", err)
}
txClientMu.Lock()
if allTxClients == nil {
allTxClients = make(map[int64]map[uint8]*TxClient)
}
if allTxClients[accountIndex] == nil {
allTxClients[accountIndex] = make(map[uint8]*TxClient)
}
allTxClients[accountIndex][apiKeyIndex] = txClientInstance
// Update default client (most recently created becomes default)
defaultTxClient = txClientInstance
defaultClientPerAccount[accountIndex] = txClientInstance
txClientMu.Unlock()
return txClientInstance, nil
}
// Check validates that the client exists and the API key matches the one on the server
func (c *TxClient) Check() error {
// check that the API key registered on Lighter matches this one
publicKey, err := c.HTTP().GetApiKey(c.accountIndex, c.apiKeyIndex)
if err != nil {
return fmt.Errorf("failed to get Api Keys. err: %v", err)
}
pubKeyBytes := c.GetKeyManager().PubKeyBytes()
pubKeyStr := hexutil.Encode(pubKeyBytes[:])
pubKeyStr = strings.Replace(pubKeyStr, "0x", "", 1)
if publicKey != pubKeyStr {
return fmt.Errorf("private key does not match the one on Lighter. ownPubKey: %s response: %+v", pubKeyStr, publicKey)
}
return nil
}
@@ -0,0 +1,6 @@
# HTTP
The HTTP module is a simple implementation that aims to provide just 2 things:
- `GetNextNonce` so that users can send transactions w/out calling managing nonces on their side
- `GetApiKey` so that users can call `CheckClient` from other sources, which makes sure that the client was configured correctly.
Other usages, like sending trades, fetching open orders or any WebSocket operations should happen outside the core SDK.
@@ -0,0 +1,45 @@
package http
import (
"crypto/tls"
"net"
"net/http"
"time"
core "github.com/elliottech/lighter-go/client"
)
var (
dialer = &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 60 * time.Second,
}
transport = &http.Transport{
DialContext: dialer.DialContext,
MaxConnsPerHost: 1000,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
httpClient = &http.Client{
Timeout: time.Second * 30,
Transport: transport,
}
)
var _ core.MinimalHTTPClient = (*client)(nil)
type client struct {
endpoint string
}
func NewClient(baseUrl string) core.MinimalHTTPClient {
if baseUrl == "" {
return nil
}
return &client{
endpoint: baseUrl,
}
}
@@ -0,0 +1,27 @@
package http
const (
CodeOK = 200
)
type ResultCode struct {
Code int32 `json:"code,example=200"`
Message string `json:"message,omitempty"`
}
type NextNonce struct {
ResultCode
Nonce int64 `json:"nonce,example=722"`
}
type ApiKey struct {
AccountIndex int64 `json:"account_index,example=3"`
ApiKeyIndex uint8 `json:"api_key_index,example=0"`
Nonce int64 `json:"nonce,example=722"`
PublicKey string `json:"public_key"`
}
type AccountApiKeys struct {
ResultCode
ApiKeys []*ApiKey `json:"api_keys"`
}
@@ -0,0 +1,75 @@
package http
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
)
func (c *client) parseResultStatus(respBody []byte) error {
resultStatus := &ResultCode{}
if err := json.Unmarshal(respBody, resultStatus); err != nil {
return err
}
if resultStatus.Code != CodeOK {
return errors.New(resultStatus.Message)
}
return nil
}
func (c *client) getAndParseL2HTTPResponse(path string, params map[string]any, result interface{}) error {
u, err := url.Parse(c.endpoint)
if err != nil {
return err
}
u.Path = path
q := u.Query()
for k, v := range params {
q.Set(k, fmt.Sprintf("%v", v))
}
u.RawQuery = q.Encode()
resp, err := httpClient.Get(u.String())
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(body))
}
if err = c.parseResultStatus(body); err != nil {
return err
}
if err := json.Unmarshal(body, result); err != nil {
return err
}
return nil
}
func (c *client) GetNextNonce(accountIndex int64, apiKeyIndex uint8) (int64, error) {
result := &NextNonce{}
err := c.getAndParseL2HTTPResponse("api/v1/nextNonce", map[string]any{"account_index": accountIndex, "api_key_index": apiKeyIndex}, result)
if err != nil {
return -1, err
}
return result.Nonce, nil
}
func (c *client) GetApiKey(accountIndex int64, apiKeyIndex uint8) (string, error) {
result := &AccountApiKeys{}
err := c.getAndParseL2HTTPResponse("api/v1/apikeys", map[string]any{"account_index": accountIndex, "api_key_index": apiKeyIndex}, result)
if err != nil {
return "", err
}
if len(result.ApiKeys) == 0 {
return "", fmt.Errorf("no api keys returned")
}
return result.ApiKeys[0].PublicKey, nil
}
@@ -0,0 +1,6 @@
package client
type MinimalHTTPClient interface {
GetNextNonce(accountIndex int64, apiKeyIndex uint8) (int64, error)
GetApiKey(accountIndex int64, apiKeyIndex uint8) (string, error)
}
@@ -0,0 +1,104 @@
package client
import (
"encoding/hex"
"fmt"
"time"
"github.com/elliottech/lighter-go/signer"
"github.com/elliottech/lighter-go/types"
)
var (
// DefaultExpireTime is a public var, so it can be changed directly in the SDK if required.
// The encouraged behaviour is the manually specify the TX deadline in types.TransactOpts.ExpiredAt
DefaultExpireTime = time.Minute*10 - time.Second // we need to give a second margin, to eliminate millisecond differences
)
type TxClient struct {
apiClient MinimalHTTPClient
chainId uint32
keyManager signer.KeyManager
accountIndex int64
apiKeyIndex uint8
}
// NewTxClient is linked to a specific (account, apiKey) pair
// apiKeyPrivateKey should be hex-encoded bytes generated using `hexutil.Encode(TxClient.GetKeyManager().PrvKeyBytes())`
func NewTxClient(apiClient MinimalHTTPClient, apiKeyPrivateKey string, accountIndex int64, apiKeyIndex uint8, chainId uint32) (*TxClient, error) {
// remove 0x from private key, if any, and parse to bytes
if len(apiKeyPrivateKey) < 2 {
return nil, fmt.Errorf("empty private key")
}
if apiKeyPrivateKey[:2] == "0x" {
apiKeyPrivateKey = apiKeyPrivateKey[2:]
}
b, err := hex.DecodeString(apiKeyPrivateKey)
if err != nil {
return nil, err
}
keyManager, err := signer.NewKeyManager(b)
if err != nil {
return nil, err
}
return &TxClient{
apiClient: apiClient,
apiKeyIndex: apiKeyIndex,
accountIndex: accountIndex,
chainId: chainId,
keyManager: keyManager,
}, nil
}
// FullFillDefaultOps returns a usable TransactOpts object if none was provided.
// This should not the be case for sharedlib, except for the nonce, which is optional.
// Still, the behaviour is implemented, so it can be extended easily by extending the code GO SDK.
func (c *TxClient) FullFillDefaultOps(ops *types.TransactOpts) (*types.TransactOpts, error) {
if ops == nil {
ops = new(types.TransactOpts)
}
if ops.ExpiredAt == 0 {
ops.ExpiredAt = time.Now().Add(DefaultExpireTime).UnixMilli()
}
if ops.FromAccountIndex == nil {
ops.FromAccountIndex = &c.accountIndex
}
if ops.ApiKeyIndex == nil {
ops.ApiKeyIndex = &c.apiKeyIndex
}
if ops.Nonce == nil || *ops.Nonce == -1 {
if c.apiClient == nil {
return nil, fmt.Errorf("nonce was not provided & HTTPClient is nil. Either provide the nonce or enable HTTPClient to get the nonce from Lighter")
}
nonce, err := c.apiClient.GetNextNonce(*ops.FromAccountIndex, *ops.ApiKeyIndex)
if err != nil {
return nil, err
}
ops.Nonce = &nonce
}
return ops, nil
}
func (c *TxClient) GetChainId() uint32 {
return c.chainId
}
func (c *TxClient) GetKeyManager() signer.KeyManager {
return c.keyManager
}
func (c *TxClient) GetAccountIndex() int64 {
return c.accountIndex
}
func (c *TxClient) GetApiKeyIndex() uint8 {
return c.apiKeyIndex
}
func (c *TxClient) HTTP() MinimalHTTPClient {
return c.apiClient
}
@@ -0,0 +1,209 @@
package client
import (
"fmt"
"time"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
)
func (c *TxClient) GetAuthToken(deadline time.Time) (string, error) {
return types.ConstructAuthToken(c.keyManager, deadline, &types.TransactOpts{
ApiKeyIndex: &c.apiKeyIndex,
FromAccountIndex: &c.accountIndex,
})
}
func (c *TxClient) GetChangePubKeyTransaction(tx *types.ChangePubKeyReq, ops *types.TransactOpts) (*txtypes.L2ChangePubKeyTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructChangePubKeyTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
pk := c.keyManager.PubKeyBytes()
msgHash, _ := txInfo.Hash(c.chainId)
if err := schnorr.Validate(pk[:], msgHash, txInfo.Sig); err != nil {
return nil, fmt.Errorf("failed to validate signature. error: %v", err)
}
return txInfo, nil
}
func (c *TxClient) GetCreateSubAccountTransaction(ops *types.TransactOpts) (*txtypes.L2CreateSubAccountTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreateSubAccountTx(c.keyManager, c.chainId, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCreatePublicPoolTransaction(tx *types.CreatePublicPoolTxReq, ops *types.TransactOpts) (*txtypes.L2CreatePublicPoolTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreatePublicPoolTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdatePublicPoolTransaction(tx *types.UpdatePublicPoolTxReq, ops *types.TransactOpts) (*txtypes.L2UpdatePublicPoolTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdatePublicPoolTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetTransferTransaction(tx *types.TransferTxReq, ops *types.TransactOpts) (*txtypes.L2TransferTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructTransferTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetWithdrawTransaction(tx *types.WithdrawTxReq, ops *types.TransactOpts) (*txtypes.L2WithdrawTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructWithdrawTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCreateOrderTransaction(tx *types.CreateOrderTxReq, ops *types.TransactOpts) (*txtypes.L2CreateOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreateOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCreateGroupedOrdersTransaction(tx *types.CreateGroupedOrdersTxReq, ops *types.TransactOpts) (*txtypes.L2CreateGroupedOrdersTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CreateGroupedOrdersTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCancelOrderTransaction(tx *types.CancelOrderTxReq, ops *types.TransactOpts) (*txtypes.L2CancelOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CancelOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetModifyOrderTransaction(tx *types.ModifyOrderTxReq, ops *types.TransactOpts) (*txtypes.L2ModifyOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2ModifyOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCancelAllOrdersTransaction(tx *types.CancelAllOrdersTxReq, ops *types.TransactOpts) (*txtypes.L2CancelAllOrdersTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CancelAllOrdersTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetMintSharesTransaction(tx *types.MintSharesTxReq, ops *types.TransactOpts) (*txtypes.L2MintSharesTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructMintSharesTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetBurnSharesTransaction(tx *types.BurnSharesTxReq, ops *types.TransactOpts) (*txtypes.L2BurnSharesTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructBurnSharesTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdateLeverageTransaction(tx *types.UpdateLeverageTxReq, ops *types.TransactOpts) (*txtypes.L2UpdateLeverageTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdateLeverageTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdateMarginTransaction(tx *types.UpdateMarginTxReq, ops *types.TransactOpts) (*txtypes.L2UpdateMarginTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdateMarginTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
+28
View File
@@ -0,0 +1,28 @@
module github.com/elliottech/lighter-go
go 1.23.0
toolchain go1.23.1
require (
github.com/elliottech/poseidon_crypto v0.0.11
github.com/ethereum/go-ethereum v1.15.6
)
require (
github.com/bits-and-blooms/bitset v1.17.0 // indirect
github.com/consensys/bavard v0.1.22 // indirect
github.com/consensys/gnark-crypto v0.14.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/supranational/blst v0.3.14 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
+76
View File
@@ -0,0 +1,76 @@
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A=
github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/elliottech/poseidon_crypto v0.0.11 h1:iX4rCg0m1XIX/7mhXVUEYUJIdQD57zNGNLeb6RZRl7g=
github.com/elliottech/poseidon_crypto v0.0.11/go.mod h1:NhWxSjPGr5JXRuB2Aepl/+ZrbmUG3hvku/GarB1JR8c=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-ethereum v1.15.6 h1:jgLoUM6/pNjp0uEnXyWcWikDwa4j1wZlcqkX8Pm8A+I=
github.com/ethereum/go-ethereum v1.15.6/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
+49
View File
@@ -0,0 +1,49 @@
### Local builds
build-darwin-local:
go mod vendor
go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-darwin-arm64.dylib ./sharedlib/main.go
# Note: build-linux-local does not append -arm or amd64 at end
build-linux-local:
go mod vendor
CGO_ENABLED=1 go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-linux.so ./sharedlib/main.go
# Note: build-windows-local does not append -arm or amd64 at end
# Windows build (requires gcc from msys2: choco install msys2)
# CMD: set PATH=C:\msys64\mingw64\bin;%PATH% && set CGO_ENABLED=1 && go mod vendor && go build -buildmode=c-shared -trimpath -o ./build/signer-amd64.dll ./sharedlib/main.go
# PowerShell: $env:Path='C:\msys64\mingw64\bin;'+$env:Path; $env:CGO_ENABLED='1'; go mod vendor; go build -buildmode=c-shared -trimpath -o ./build/signer-amd64.dll ./sharedlib/main.go
build-windows-local:
go mod vendor
$env:Path='C:\msys64\mingw64\bin;'+$env:Path; $env:CGO_ENABLED='1'; go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-windows.dll ./sharedlib/main.go
### Docker builds
# Note: I don't think this works TBH
#build-darwin-arm64-docker:
# docker run --rm -v ${PWD}:/go/src/sdk -w /go/src/sdk golang:1.23.2-bullseye bash -c " \
# cd /go/src/sdk && \
# go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-darwin-arm64.dylib ./sharedlib"
build-linux-amd64-docker:
go mod vendor
docker run --rm --platform linux/amd64 -v ${PWD}:/go/src/sdk -w /go/src/sdk golang:1.23.2-bullseye /bin/sh -c " \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-linux-amd64.so ./sharedlib"
build-linux-arm64-docker:
go mod vendor
docker run --rm --platform linux/arm64 -v ${PWD}:/go/src/sdk -w /go/src/sdk golang:1.23.2-bullseye /bin/sh -c " \
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-linux-arm64.so ./sharedlib"
build-windows-amd64-docker:
go mod vendor
docker run --rm --platform linux/amd64 -v ${PWD}:/go/src/sdk -w /go/src/sdk golang:1.23.2-bullseye bash -c " \
apt-get update && \
apt-get install -y gcc-mingw-w64-x86-64 && \
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc go build -buildmode=c-shared -trimpath -o ./build/lighter-signer-windows-amd64.dll ./sharedlib"
### WASM builds
build-wasm:
go mod vendor
GOOS=js GOARCH=wasm go build -trimpath -o ./build/lighter-signer.wasm ./wasm/
@@ -0,0 +1,690 @@
package main
import (
"encoding/hex"
"fmt"
"time"
"unsafe"
"github.com/elliottech/lighter-go/client"
"github.com/elliottech/lighter-go/client/http"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
"github.com/ethereum/go-ethereum/common/hexutil"
)
/*
#include <stdlib.h>
#include <stdint.h>
typedef struct {
char* str;
char* err;
} StrOrErr;
typedef struct {
uint8_t txType;
char* txInfo;
char* txHash;
char* messageToSign;
char* err;
} SignedTxResponse;
typedef struct {
char* privateKey;
char* publicKey;
char* err;
} ApiKeyResponse;
typedef struct {
uint8_t MarketIndex;
int64_t ClientOrderIndex;
int64_t BaseAmount;
uint32_t Price;
uint8_t IsAsk;
uint8_t Type;
uint8_t TimeInForce;
uint8_t ReduceOnly;
uint32_t TriggerPrice;
int64_t OrderExpiry;
} CreateOrderTxReq;
*/
import "C"
var chainId uint32
func wrapErr(err any) *C.char {
if err == nil {
return nil
}
return C.CString(fmt.Sprintf("%v", err))
}
func messageToSign(txInfo txtypes.TxInfo) string {
switch typed := txInfo.(type) {
case *txtypes.L2ChangePubKeyTxInfo:
return typed.GetL1SignatureBody()
case *txtypes.L2TransferTxInfo:
return typed.GetL1SignatureBody(chainId)
default:
return ""
}
}
func signedTxResponseErr(err any) C.SignedTxResponse {
return C.SignedTxResponse{err: wrapErr(err)}
}
func signedTxResponsePanic(err any) C.SignedTxResponse {
return signedTxResponseErr(fmt.Errorf("panic: %v", err))
}
func convertTxInfoToResponse(txInfo txtypes.TxInfo, err error) C.SignedTxResponse {
if err != nil {
return signedTxResponseErr(err)
}
if txInfo == nil {
return signedTxResponseErr("nil transaction info")
}
txInfoStr, err := txInfo.GetTxInfo()
if err != nil {
return signedTxResponseErr(err)
}
resp := C.SignedTxResponse{
txType: C.uint8_t(txInfo.GetTxType()),
txInfo: C.CString(txInfoStr),
txHash: C.CString(txInfo.GetTxHash()),
}
if msg := messageToSign(txInfo); msg != "" {
resp.messageToSign = C.CString(msg)
}
return resp
}
// getClient returns the go TxClient from the specified cApiKeyIndex and cAccountIndex
func getClient(cApiKeyIndex C.int, cAccountIndex C.longlong) (*client.TxClient, error) {
apiKeyIndex := uint8(cApiKeyIndex)
accountIndex := int64(cAccountIndex)
return client.GetClient(apiKeyIndex, accountIndex)
}
func getTransactOpts(cNonce C.longlong) *types.TransactOpts {
nonce := int64(cNonce)
return &types.TransactOpts{
Nonce: &nonce,
}
}
//export GenerateAPIKey
func GenerateAPIKey(cSeed *C.char) (ret C.ApiKeyResponse) {
defer func() {
if r := recover(); r != nil {
ret = C.ApiKeyResponse{err: wrapErr(fmt.Errorf("panic: %v", r))}
}
}()
seed := C.GoString(cSeed)
privateKeyStr, publicKeyStr, err := client.GenerateAPIKey(seed)
if err != nil {
return C.ApiKeyResponse{err: wrapErr(err)}
}
return C.ApiKeyResponse{
privateKey: C.CString(privateKeyStr),
publicKey: C.CString(publicKeyStr),
}
}
//export CreateClient
func CreateClient(cUrl *C.char, cPrivateKey *C.char, cChainId C.int, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret *C.char) {
defer func() {
if r := recover(); r != nil {
ret = wrapErr(fmt.Errorf("panic: %v", r))
}
}()
url := C.GoString(cUrl)
privateKey := C.GoString(cPrivateKey)
chainId = uint32(cChainId)
apiKeyIndex := uint8(cApiKeyIndex)
accountIndex := int64(cAccountIndex)
httpClient := http.NewClient(url)
_, err := client.CreateClient(httpClient, privateKey, chainId, apiKeyIndex, accountIndex)
return wrapErr(err)
}
//export CheckClient
func CheckClient(cApiKeyIndex C.int, cAccountIndex C.longlong) (ret *C.char) {
defer func() {
if r := recover(); r != nil {
ret = wrapErr(fmt.Errorf("panic: %v", r))
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return wrapErr(err)
}
return wrapErr(c.Check())
}
//export SignChangePubKey
func SignChangePubKey(cPubKey *C.char, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
pubKeyStr := C.GoString(cPubKey)
pubKeyBytes, err := hexutil.Decode(pubKeyStr)
if err != nil {
return signedTxResponseErr(err)
}
if len(pubKeyBytes) != 40 {
return signedTxResponseErr(fmt.Errorf("invalid pub key length. expected 40 but got %v", len(pubKeyBytes)))
}
var pubKey [40]byte
copy(pubKey[:], pubKeyBytes)
tx := &types.ChangePubKeyReq{
PubKey: pubKey,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetChangePubKeyTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCreateOrder
func SignCreateOrder(cMarketIndex C.int, cClientOrderIndex C.longlong, cBaseAmount C.longlong, cPrice C.int, cIsAsk C.int, cOrderType C.int, cTimeInForce C.int, cReduceOnly C.int, cTriggerPrice C.int, cOrderExpiry C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
marketIndex := int16(cMarketIndex)
clientOrderIndex := int64(cClientOrderIndex)
baseAmount := int64(cBaseAmount)
price := uint32(cPrice)
isAsk := uint8(cIsAsk)
orderType := uint8(cOrderType)
timeInForce := uint8(cTimeInForce)
reduceOnly := uint8(cReduceOnly)
triggerPrice := uint32(cTriggerPrice)
orderExpiry := int64(cOrderExpiry)
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli() // 28 days
}
tx := &types.CreateOrderTxReq{
MarketIndex: marketIndex,
ClientOrderIndex: clientOrderIndex,
BaseAmount: baseAmount,
Price: price,
IsAsk: isAsk,
Type: orderType,
TimeInForce: timeInForce,
ReduceOnly: reduceOnly,
TriggerPrice: triggerPrice,
OrderExpiry: orderExpiry,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCreateOrderTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCreateGroupedOrders
func SignCreateGroupedOrders(cGroupingType C.uint8_t, cOrders *C.CreateOrderTxReq, cLen C.int, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
length := int(cLen)
orders := make([]*types.CreateOrderTxReq, length)
size := unsafe.Sizeof(*cOrders)
for i := 0; i < length; i++ {
order := (*C.CreateOrderTxReq)(unsafe.Pointer(uintptr(unsafe.Pointer(cOrders)) + uintptr(i)*uintptr(size)))
orderExpiry := int64(order.OrderExpiry)
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli()
}
orders[i] = &types.CreateOrderTxReq{
MarketIndex: int16(order.MarketIndex),
ClientOrderIndex: int64(order.ClientOrderIndex),
BaseAmount: int64(order.BaseAmount),
Price: uint32(order.Price),
IsAsk: uint8(order.IsAsk),
Type: uint8(order.Type),
TimeInForce: uint8(order.TimeInForce),
ReduceOnly: uint8(order.ReduceOnly),
TriggerPrice: uint32(order.TriggerPrice),
OrderExpiry: orderExpiry,
}
}
tx := &types.CreateGroupedOrdersTxReq{
GroupingType: uint8(cGroupingType),
Orders: orders,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCreateGroupedOrdersTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCancelOrder
func SignCancelOrder(cMarketIndex C.int, cOrderIndex C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
marketIndex := int16(cMarketIndex)
orderIndex := int64(cOrderIndex)
tx := &types.CancelOrderTxReq{
MarketIndex: marketIndex,
Index: orderIndex,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCancelOrderTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignWithdraw
func SignWithdraw(cAssetIndex C.int, cRouteType C.int, cAmount C.ulonglong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
assetIndex := int16(cAssetIndex)
routeType := uint8(cRouteType)
amount := uint64(cAmount)
tx := &types.WithdrawTxReq{
AssetIndex: assetIndex,
RouteType: routeType,
Amount: amount,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetWithdrawTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCreateSubAccount
func SignCreateSubAccount(cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCreateSubAccountTransaction(ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCancelAllOrders
func SignCancelAllOrders(cTimeInForce C.int, cTime C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
timeInForce := uint8(cTimeInForce)
t := int64(cTime)
tx := &types.CancelAllOrdersTxReq{
TimeInForce: timeInForce,
Time: t,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCancelAllOrdersTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignModifyOrder
func SignModifyOrder(cMarketIndex C.int, cIndex C.longlong, cBaseAmount C.longlong, cPrice C.longlong, cTriggerPrice C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
marketIndex := int16(cMarketIndex)
index := int64(cIndex)
baseAmount := int64(cBaseAmount)
price := uint32(cPrice)
triggerPrice := uint32(cTriggerPrice)
tx := &types.ModifyOrderTxReq{
MarketIndex: marketIndex,
Index: index,
BaseAmount: baseAmount,
Price: price,
TriggerPrice: triggerPrice,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetModifyOrderTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignTransfer
func SignTransfer(cToAccountIndex C.longlong, cAssetIndex C.int16_t, cFromRouteType, cToRouteType C.uint8_t, cAmount, cUsdcFee C.longlong, cMemo *C.char, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
toAccountIndex := int64(cToAccountIndex)
assetIndex := int16(cAssetIndex)
fromRouteType := uint8(cFromRouteType)
toRouteType := uint8(cToRouteType)
amount := int64(cAmount)
usdcFee := int64(cUsdcFee)
memo := [32]byte{}
memoStr := C.GoString(cMemo)
if len(memoStr) == 66 {
if memoStr[0:2] == "0x" {
memoStr = memoStr[2:66]
} else {
return signedTxResponseErr(fmt.Sprintf("memo expected to be 32 bytes or 64 hex encoded or 66 if 0x hex encoded -- long but received %v", len(memoStr)))
}
}
// assume hex encoded here
if len(memoStr) == 64 {
b, err := hex.DecodeString(memoStr)
if err != nil {
return signedTxResponseErr(fmt.Sprintf("failed to decode hex string. err: %v", err))
}
for i := 0; i < 32; i += 1 {
memo[i] = b[i]
}
} else if len(memoStr) == 32 {
for i := 0; i < 32; i++ {
memo[i] = byte(memoStr[i])
}
} else {
return signedTxResponseErr(fmt.Sprintf("memo expected to be 32 bytes or 64 hex encoded or 66 if 0x hex encoded -- long but received %v", len(memoStr)))
}
tx := &types.TransferTxReq{
ToAccountIndex: toAccountIndex,
AssetIndex: assetIndex,
FromRouteType: fromRouteType,
ToRouteType: toRouteType,
Amount: amount,
USDCFee: usdcFee,
Memo: memo,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetTransferTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignCreatePublicPool
func SignCreatePublicPool(cOperatorFee C.longlong, cInitialTotalShares C.int, cMinOperatorShareRate C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
operatorFee := int64(cOperatorFee)
initialTotalShares := int64(cInitialTotalShares)
minOperatorShareRate := uint16(cMinOperatorShareRate)
tx := &types.CreatePublicPoolTxReq{
OperatorFee: operatorFee,
InitialTotalShares: initialTotalShares,
MinOperatorShareRate: minOperatorShareRate,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetCreatePublicPoolTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignUpdatePublicPool
func SignUpdatePublicPool(cPublicPoolIndex C.longlong, cStatus C.int, cOperatorFee C.longlong, cMinOperatorShareRate C.int, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
publicPoolIndex := int64(cPublicPoolIndex)
status := uint8(cStatus)
operatorFee := int64(cOperatorFee)
minOperatorShareRate := uint16(cMinOperatorShareRate)
tx := &types.UpdatePublicPoolTxReq{
PublicPoolIndex: publicPoolIndex,
Status: status,
OperatorFee: operatorFee,
MinOperatorShareRate: minOperatorShareRate,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetUpdatePublicPoolTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignMintShares
func SignMintShares(cPublicPoolIndex C.longlong, cShareAmount C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
publicPoolIndex := int64(cPublicPoolIndex)
shareAmount := int64(cShareAmount)
tx := &types.MintSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetMintSharesTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignBurnShares
func SignBurnShares(cPublicPoolIndex C.longlong, cShareAmount C.longlong, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
publicPoolIndex := int64(cPublicPoolIndex)
shareAmount := int64(cShareAmount)
tx := &types.BurnSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetBurnSharesTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export SignUpdateLeverage
func SignUpdateLeverage(cMarketIndex C.int, cInitialMarginFraction C.int, cMarginMode C.int, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
marketIndex := int16(cMarketIndex)
initialMarginFraction := uint16(cInitialMarginFraction)
marginMode := uint8(cMarginMode)
tx := &types.UpdateLeverageTxReq{
MarketIndex: marketIndex,
InitialMarginFraction: initialMarginFraction,
MarginMode: marginMode,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetUpdateLeverageTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
//export CreateAuthToken
func CreateAuthToken(cDeadline C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.StrOrErr) {
defer func() {
if r := recover(); r != nil {
ret = C.StrOrErr{err: wrapErr(fmt.Errorf("panic: %v", r))}
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return C.StrOrErr{err: wrapErr(err)}
}
deadline := int64(cDeadline)
if deadline == 0 {
deadline = time.Now().Add(time.Hour * 7).Unix()
}
authToken, err := c.GetAuthToken(time.Unix(deadline, 0))
if err != nil {
return C.StrOrErr{err: wrapErr(err)}
}
return C.StrOrErr{str: C.CString(authToken)}
}
//export SignUpdateMargin
func SignUpdateMargin(cMarketIndex C.int, cUSDCAmount C.longlong, cDirection C.int, cNonce C.longlong, cApiKeyIndex C.int, cAccountIndex C.longlong) (ret C.SignedTxResponse) {
defer func() {
if r := recover(); r != nil {
ret = signedTxResponsePanic(r)
}
}()
c, err := getClient(cApiKeyIndex, cAccountIndex)
if err != nil {
return signedTxResponseErr(err)
}
marketIndex := int16(cMarketIndex)
usdcAmount := int64(cUSDCAmount)
direction := uint8(cDirection)
tx := &types.UpdateMarginTxReq{
MarketIndex: marketIndex,
USDCAmount: usdcAmount,
Direction: direction,
}
ops := getTransactOpts(cNonce)
txInfo, err := c.GetUpdateMarginTransaction(tx, ops)
return convertTxInfoToResponse(txInfo, err)
}
func main() {}
@@ -0,0 +1,54 @@
package signer
import (
"fmt"
"hash"
curve "github.com/elliottech/poseidon_crypto/curve/ecgfp5"
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
)
type Signer interface {
Sign(message []byte, hFunc hash.Hash) ([]byte, error)
}
type KeyManager interface {
Signer
PubKey() gFp5.Element
PubKeyBytes() [40]byte
PrvKeyBytes() []byte
}
type keyManager struct {
key curve.ECgFp5Scalar
}
func NewKeyManager(b []byte) (KeyManager, error) {
if len(b) != 40 {
return nil, fmt.Errorf("invalid private key length. expected: 40 got: %v", len(b))
}
return &keyManager{key: curve.ScalarElementFromLittleEndianBytes(b)}, nil
}
func (key *keyManager) Sign(hashedMessage []byte, hFunc hash.Hash) ([]byte, error) {
hashedMessageAsQuinticExtension, err := gFp5.FromCanonicalLittleEndianBytes(hashedMessage)
if err != nil {
return nil, fmt.Errorf("failed to parse message while signing. message: %v err: %w", hashedMessage, err)
}
return schnorr.SchnorrSignHashedMessage(hashedMessageAsQuinticExtension, key.key).ToBytes(), nil
}
func (key *keyManager) PubKey() gFp5.Element {
return schnorr.SchnorrPkFromSk(key.key)
}
func (key *keyManager) PubKeyBytes() (res [40]byte) {
bytes := key.PubKey().ToLittleEndianBytes()
copy(res[:], bytes[:])
return
}
func (key *keyManager) PrvKeyBytes() []byte {
return key.key.ToLittleEndianBytes()
}
@@ -28,17 +28,22 @@ type ChangePubKeyReq struct {
type TransferTxReq struct {
ToAccountIndex int64
USDCAmount int64
Fee int64
AssetIndex int16
FromRouteType uint8
ToRouteType uint8
Amount int64
USDCFee int64
Memo [32]byte
}
type WithdrawTxReq struct {
USDCAmount uint64
AssetIndex int16
RouteType uint8
Amount uint64
}
type CreateOrderTxReq struct {
MarketIndex uint8
MarketIndex int16
ClientOrderIndex int64
BaseAmount int64
Price uint32
@@ -56,7 +61,7 @@ type CreateGroupedOrdersTxReq struct {
}
type ModifyOrderTxReq struct {
MarketIndex uint8
MarketIndex int16
Index int64
BaseAmount int64
Price uint32
@@ -64,7 +69,7 @@ type ModifyOrderTxReq struct {
}
type CancelOrderTxReq struct {
MarketIndex uint8
MarketIndex int16
Index int64
}
@@ -76,14 +81,14 @@ type CancelAllOrdersTxReq struct {
type CreatePublicPoolTxReq struct {
OperatorFee int64
InitialTotalShares int64
MinOperatorShareRate int64
MinOperatorShareRate uint16
}
type UpdatePublicPoolTxReq struct {
PublicPoolIndex int64
Status uint8
OperatorFee int64
MinOperatorShareRate int64
MinOperatorShareRate uint16
}
type MintSharesTxReq struct {
@@ -97,13 +102,13 @@ type BurnSharesTxReq struct {
}
type UpdateLeverageTxReq struct {
MarketIndex uint8
MarketIndex int16
InitialMarginFraction uint16
MarginMode uint8
}
type UpdateMarginTxReq struct {
MarketIndex uint8
MarketIndex int16
USDCAmount int64
Direction uint8
}
@@ -468,8 +473,11 @@ func ConvertTransferTx(tx *TransferTxReq, ops *TransactOpts) *txtypes.L2Transfer
FromAccountIndex: *ops.FromAccountIndex,
ApiKeyIndex: *ops.ApiKeyIndex,
ToAccountIndex: tx.ToAccountIndex,
USDCAmount: tx.USDCAmount,
Fee: tx.Fee,
AssetIndex: tx.AssetIndex,
FromRouteType: tx.FromRouteType,
ToRouteType: tx.ToRouteType,
Amount: tx.Amount,
USDCFee: tx.USDCFee,
Memo: tx.Memo,
ExpiredAt: ops.ExpiredAt,
Nonce: *ops.Nonce,
@@ -563,7 +571,9 @@ func ConvertWithdrawTx(tx *WithdrawTxReq, ops *TransactOpts) *txtypes.L2Withdraw
return &txtypes.L2WithdrawTxInfo{
FromAccountIndex: *ops.FromAccountIndex,
ApiKeyIndex: *ops.ApiKeyIndex,
USDCAmount: tx.USDCAmount,
AssetIndex: tx.AssetIndex,
RouteType: tx.RouteType,
Amount: tx.Amount,
ExpiredAt: ops.ExpiredAt,
Nonce: *ops.Nonce,
}
@@ -641,6 +651,7 @@ func ConvertUpdateLeverageTx(tx *UpdateLeverageTxReq, ops *TransactOpts) *txtype
ApiKeyIndex: *ops.ApiKeyIndex,
MarketIndex: tx.MarketIndex,
InitialMarginFraction: tx.InitialMarginFraction,
MarginMode: tx.MarginMode,
ExpiredAt: ops.ExpiredAt,
Nonce: *ops.Nonce,
}
@@ -49,7 +49,7 @@ func (txInfo *L2BurnSharesTxInfo) Validate() error {
}
// PublicPoolIndex
if txInfo.PublicPoolIndex < MinAccountIndex {
if txInfo.PublicPoolIndex < MinSubAccountIndex {
return ErrPublicPoolIndexTooLow
}
if txInfo.PublicPoolIndex > MaxAccountIndex {
@@ -73,7 +73,6 @@ func (txInfo *L2BurnSharesTxInfo) Validate() error {
return nil
}
func (txInfo *L2BurnSharesTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
elems := make([]g.Element, 0, 8)
@@ -69,7 +69,7 @@ func (txInfo *L2CancelAllOrdersTxInfo) Validate() error {
}
case AbortScheduledCancelAll:
if txInfo.Time != 0 {
return ErrCancelAllTimeIsNotInRange
return ErrCancelAllTimeisNotNill
}
default:
return ErrInvalidCancelAllTimeInForce
@@ -11,7 +11,7 @@ type L2CancelOrderTxInfo struct {
AccountIndex int64
ApiKeyIndex uint8
MarketIndex uint8
MarketIndex int16
Index int64 // Client Order Index or Order Index of the order to cancel
ExpiredAt int64
@@ -50,11 +50,10 @@ func (txInfo *L2CancelOrderTxInfo) Validate() error {
}
// MarketIndex
if txInfo.MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
}
if txInfo.MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
isSpotMarket := txInfo.MarketIndex >= MinSpotMarketIndex && txInfo.MarketIndex <= MaxSpotMarketIndex
isPerpsMarket := txInfo.MarketIndex >= MinPerpsMarketIndex && txInfo.MarketIndex <= MaxPerpsMarketIndex
if !isSpotMarket && !isPerpsMarket {
return ErrInvalidMarketIndex
}
// Index
@@ -2,35 +2,12 @@ package txtypes
import (
"fmt"
"strings"
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
const (
templateChangePubKey = "Register Lighter Account\n\npubkey: 0x%s\nnonce: %s\naccount index: %s\napi key index: %s\nOnly sign this message for a trusted client!"
)
func getHex10FromUint64(value uint64) string {
v := hexutil.EncodeUint64(value)
v = strings.Replace(v, "0x", "", 1)
// Make sure result has fixed bytes
vBytes := []byte(v)
if len(vBytes) < 16 {
toAppend := make([]byte, 16-len(vBytes))
for i := range toAppend {
toAppend[i] = 48
}
vBytes = append(toAppend, vBytes...)
}
return fmt.Sprintf("0x%s", string(vBytes))
}
var _ TxInfo = (*L2ChangePubKeyTxInfo)(nil)
type L2ChangePubKeyTxInfo struct {
@@ -84,7 +61,7 @@ func (txInfo *L2ChangePubKeyTxInfo) Validate() error {
return ErrExpiredAtInvalid
}
if !IsValidPubKey(txInfo.PubKey) {
if !IsValidPubKeyLength(txInfo.PubKey) {
return ErrPubKeyInvalid
}
@@ -92,7 +69,8 @@ func (txInfo *L2ChangePubKeyTxInfo) Validate() error {
}
func (txInfo *L2ChangePubKeyTxInfo) GetL1SignatureBody() string {
signatureBody := fmt.Sprintf(templateChangePubKey,
signatureBody := fmt.Sprintf(
TemplateChangePubKey,
common.Bytes2Hex(txInfo.PubKey),
getHex10FromUint64(uint64(txInfo.Nonce)),
getHex10FromUint64(uint64(txInfo.AccountIndex)),
@@ -101,6 +79,10 @@ func (txInfo *L2ChangePubKeyTxInfo) GetL1SignatureBody() string {
return signatureBody
}
func (txInfo *L2ChangePubKeyTxInfo) GetL1AddressBySignature() common.Address {
return calculateL1AddressBySignature(txInfo.GetL1SignatureBody(), txInfo.L1Sig)
}
func (txInfo *L2ChangePubKeyTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
elems := make([]g.Element, 0, 11)
@@ -17,6 +17,15 @@ const (
)
const (
TxTypeEmpty = 0
TxTypeL1Deposit = 1
TxTypeL1ChangePubKey = 2
TxTypeL1CreateMarket = 3
TxTypeL1UpdateMarket = 4
TxTypeL1CancelAllOrders = 5
TxTypeL1Withdraw = 6
TxTypeL1CreateOrder = 7
TxTypeL2ChangePubKey = 8
TxTypeL2CreateSubAccount = 9
TxTypeL2CreatePublicPool = 10
@@ -41,6 +50,7 @@ const (
TxTypeL2CreateGroupedOrders = 28
TxTypeL2UpdateMargin = 29
TxTypeL1BurnShares = 30
)
// Order Type
@@ -83,23 +93,57 @@ const (
AbortScheduledCancelAll = 2
)
// Asset Margin Mode
const (
HashLength int = 32
AssetMarginMode_Disabled = 0
AssetMarginMode_Enabled = 1
AssetMarginMode_Max = AssetMarginMode_Enabled
)
// Asset Route Type
const (
AssetRouteType_Perps = 0
AssetRouteType_Spot = 1
)
// Position Margin Mode
const (
CrossMargin = iota
IsolatedMargin = 1
)
// Margin Update Direction
const (
RemoveFromIsolatedMargin = iota
AddToIsolatedMargin = 1
)
const (
OneUSDC = 1000000
FeeTick int64 = 1_000_000
MarginFractionTick int64 = 10_000
ShareTick int64 = 10_000
FeeTick int64 = 1_000_000
MarginFractionTick int64 = 10_000
ShareTick uint16 = 10_000
MinAccountIndex int64 = 0
MaxAccountIndex int64 = 281474976710654 // (1 << 48) - 2
MinApiKeyIndex uint8 = 0
MaxApiKeyIndex uint8 = 254 // (1 << 8) - 2
MaxMasterAccountIndex int64 = 140737488355327 // (1 << 47) - 1
MinSubAccountIndex int64 = 140737488355328 // (1 << 47)
MinApiKeyIndex uint8 = 0
MaxApiKeyIndex uint8 = 254 // (1 << 8) - 2
MinMarketIndex uint8 = 0
MaxMarketIndex uint8 = 254 // (1 << 8) - 2
MinMarketIndex int16 = 0
MinPerpsMarketIndex int16 = 0
MaxPerpsMarketIndex int16 = 254 // (1 << 8) - 2
NilMarketIndex int16 = 255
MinSpotMarketIndex int16 = 2048 // (1 << 11)
MaxSpotMarketIndex int16 = 4094 // (1 << 12) - 2
NativeAssetIndex = uint16(1)
USDCAssetIndex = uint16(3)
MinAssetIndex = 1
MaxAssetIndex = (1 << 6) - 2
NilAssetIndex = 0
MaxInvestedPublicPoolCount int64 = 16
InitialPoolShareValue int64 = 1_000 // 0.001 USDC
@@ -124,7 +168,7 @@ const (
MaxClientOrderIndex int64 = (1 << 48) - 1
MinOrderIndex int64 = MaxClientOrderIndex + 1
MaxOrderIndex int64 = (1 << 56) - 1
MaxOrderIndex int64 = (1 << 60) - 1
MinOrderBaseAmount int64 = 1
MaxOrderBaseAmount int64 = (1 << 48) - 1
@@ -162,14 +206,3 @@ const (
MinWithdrawalAmount uint64 = 1
MaxWithdrawalAmount uint64 = MaxExchangeUSDC
)
// Margin Modes
const (
CrossMargin = iota
IsolatedMargin = 1
)
const (
RemoveFromIsolatedMargin = 0
AddToIsolatedMargin = 1
)
@@ -55,11 +55,8 @@ func (txInfo *L2CreateGroupedOrdersTxInfo) Validate() error {
}
// MarketIndex for first order
if txInfo.Orders[0].MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
}
if txInfo.Orders[0].MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
if txInfo.Orders[0].MarketIndex < MinPerpsMarketIndex || txInfo.Orders[0].MarketIndex > MaxPerpsMarketIndex {
return ErrInvalidMarketIndex
}
// Perform range checks for all orders
@@ -48,11 +48,10 @@ func (txInfo *L2CreateOrderTxInfo) Validate() error {
}
// MarketIndex
if txInfo.MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
}
if txInfo.MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
isSpotMarket := txInfo.MarketIndex >= MinSpotMarketIndex && txInfo.MarketIndex <= MaxSpotMarketIndex
isPerpsMarket := txInfo.MarketIndex >= MinPerpsMarketIndex && txInfo.MarketIndex <= MaxPerpsMarketIndex
if !isSpotMarket && !isPerpsMarket {
return ErrInvalidMarketIndex
}
// ClientOrderIndex
@@ -89,14 +88,17 @@ func (txInfo *L2CreateOrderTxInfo) Validate() error {
return ErrIsAskInvalid
}
// Type
if txInfo.TimeInForce != ImmediateOrCancel && txInfo.TimeInForce != GoodTillTime && txInfo.TimeInForce != PostOnly {
return ErrOrderTimeInForceInvalid
}
if txInfo.ReduceOnly != 0 && txInfo.ReduceOnly != 1 {
// ReduceOnly
if (txInfo.ReduceOnly != 0 && txInfo.ReduceOnly != 1) || (isSpotMarket && txInfo.ReduceOnly == 1) {
return ErrOrderReduceOnlyInvalid
}
// OrderExpiry
if (txInfo.OrderExpiry < MinOrderExpiry || txInfo.OrderExpiry > MaxOrderExpiry) && txInfo.OrderExpiry != NilOrderExpiry {
return ErrOrderExpiryInvalid
}
@@ -119,7 +121,9 @@ func (txInfo *L2CreateOrderTxInfo) Validate() error {
return ErrOrderExpiryInvalid
}
case StopLossOrder, TakeProfitOrder:
if txInfo.TimeInForce != ImmediateOrCancel {
if !isPerpsMarket {
return ErrOrderTypeInvalid
} else if txInfo.TimeInForce != ImmediateOrCancel {
return ErrOrderTimeInForceInvalid
} else if txInfo.TriggerPrice == NilOrderTriggerPrice {
return ErrOrderTriggerPriceInvalid
@@ -127,7 +131,9 @@ func (txInfo *L2CreateOrderTxInfo) Validate() error {
return ErrOrderExpiryInvalid
}
case StopLossLimitOrder, TakeProfitLimitOrder:
if txInfo.TriggerPrice == NilOrderTriggerPrice {
if !isPerpsMarket {
return ErrOrderTypeInvalid
} else if txInfo.TriggerPrice == NilOrderTriggerPrice {
return ErrOrderTriggerPriceInvalid
} else if txInfo.OrderExpiry == NilOrderExpiry {
return ErrOrderExpiryInvalid
@@ -13,7 +13,7 @@ type L2CreatePublicPoolTxInfo struct {
OperatorFee int64
InitialTotalShares int64
MinOperatorShareRate int64
MinOperatorShareRate uint16
ExpiredAt int64
Nonce int64
@@ -64,9 +64,6 @@ func (txInfo *L2CreatePublicPoolTxInfo) Validate() error {
}
// MinOperatorShareRate
if txInfo.MinOperatorShareRate < 0 {
return ErrPoolMinOperatorShareRateTooLow
}
if txInfo.MinOperatorShareRate > ShareTick {
return ErrPoolMinOperatorShareRateTooHigh
}
@@ -95,7 +92,7 @@ func (txInfo *L2CreatePublicPoolTxInfo) Hash(lighterChainId uint32, extra ...g.E
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
elems = append(elems, g.FromInt64(txInfo.OperatorFee))
elems = append(elems, g.FromInt64(txInfo.InitialTotalShares))
elems = append(elems, g.FromInt64(txInfo.MinOperatorShareRate))
elems = append(elems, g.FromUint32(uint32(txInfo.MinOperatorShareRate)))
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
}
@@ -34,7 +34,7 @@ func (txInfo *L2CreateSubAccountTxInfo) Validate() error {
if txInfo.AccountIndex < MinAccountIndex {
return ErrFromAccountIndexTooLow
}
if txInfo.AccountIndex > MaxAccountIndex {
if txInfo.AccountIndex > MaxMasterAccountIndex {
return ErrFromAccountIndexTooHigh
}
@@ -3,6 +3,9 @@ package txtypes
import "fmt"
var (
ErrAssetIndexTooLow = fmt.Errorf("AssetIndex should not be less than %d", MinAssetIndex)
ErrAssetIndexTooHigh = fmt.Errorf("AssetIndex should not be larger than %d", MaxAssetIndex)
ErrRouteTypeInvalid = fmt.Errorf("RouteType is invalid")
ErrAccountIndexTooLow = fmt.Errorf("AccountIndex should not be less than %d", MinAccountIndex)
ErrAccountIndexTooHigh = fmt.Errorf("AccountIndex should not be larger than %d", MaxAccountIndex)
ErrNonceTooLow = fmt.Errorf("AccountNonce should not be less than %d", MinNonce)
@@ -36,9 +39,12 @@ var (
ErrWithdrawalAmountTooHigh = fmt.Errorf("WithdrawalAmount should not be larger than %d", MaxWithdrawalAmount)
ErrTransferAmountTooLow = fmt.Errorf("TransferAmount should be larger than %d", MinTransferAmount)
ErrTransferAmountTooHigh = fmt.Errorf("TransferAmount should not be larger than %d", MaxTransferAmount)
ErrTransferFeeNegative = fmt.Errorf("TransferFee should not be negative")
ErrTransferFeeTooHigh = fmt.Errorf("TransferFee should not be larger than %d", MaxTransferAmount)
ErrMarketIndexTooLow = fmt.Errorf("MarketIndex should not be less than %d", MinMarketIndex)
ErrMarketIndexTooHigh = fmt.Errorf("MarketIndex should not be larger than %d", MaxMarketIndex)
ErrMarketIndexTooHigh = fmt.Errorf("MarketIndex should not be larger than %d", MaxSpotMarketIndex)
ErrMarketIndexMismatch = fmt.Errorf("MarketIndex should match the market index of the order")
ErrInvalidMarketIndex = fmt.Errorf("MarketIndex is not valid")
ErrInitialMarginFractionTooLow = fmt.Errorf("InitialMarginFraction should not be less than %d", 0)
ErrInitialMarginFractionTooHigh = fmt.Errorf("InitialMarginFraction should not be larger than %d", MarginFractionTick)
ErrClientOrderIndexTooLow = fmt.Errorf("ClientOrderIndex should not be less than %d", MinClientOrderIndex)
@@ -61,6 +67,4 @@ var (
ErrInvalidMarginMode = fmt.Errorf("MarginMode is not valid")
ErrCancelModeInvalid = fmt.Errorf("CancelMode is not valid")
ErrInvalidUpdateMarginDirection = fmt.Errorf("Margin movement direction is not valid")
ErrTransferFeeNegative = fmt.Errorf("Transfer fee is negative")
ErrTransferFeeTooHigh = fmt.Errorf("Transfer fee is higher than %d", MaxTransferAmount)
)
@@ -21,7 +21,7 @@ type TxInfo interface {
}
type OrderInfo struct {
MarketIndex uint8
MarketIndex int16
ClientOrderIndex int64
@@ -49,7 +49,7 @@ func (txInfo *L2MintSharesTxInfo) Validate() error {
}
// PublicPoolIndex
if txInfo.PublicPoolIndex < MinAccountIndex {
if txInfo.PublicPoolIndex < MinSubAccountIndex {
return ErrPublicPoolIndexTooLow
}
if txInfo.PublicPoolIndex > MaxAccountIndex {
@@ -11,7 +11,7 @@ type L2ModifyOrderTxInfo struct {
AccountIndex int64
ApiKeyIndex uint8
MarketIndex uint8
MarketIndex int16
Index int64 // Client Order Index or Order Index of the order to modify
BaseAmount int64
Price uint32
@@ -52,11 +52,10 @@ func (txInfo *L2ModifyOrderTxInfo) Validate() error {
}
// MarketIndex
if txInfo.MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
}
if txInfo.MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
isSpotMarket := txInfo.MarketIndex >= MinSpotMarketIndex && txInfo.MarketIndex <= MaxSpotMarketIndex
isPerpsMarket := txInfo.MarketIndex >= MinPerpsMarketIndex && txInfo.MarketIndex <= MaxPerpsMarketIndex
if !isSpotMarket && !isPerpsMarket {
return ErrInvalidMarketIndex
}
// Index
@@ -7,29 +7,31 @@ import (
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
p2 "github.com/elliottech/poseidon_crypto/hash/poseidon2_goldilocks"
"github.com/ethereum/go-ethereum/common"
)
const templateTransfer = "Transfer\n\nnonce: %s\nfrom: %s\napi key: %s\nto: %s\namount: %s\nfee: %s\nmemo: %s\nOnly sign this message for a trusted client!"
var _ TxInfo = (*L2TransferTxInfo)(nil)
type L2TransferTxInfo struct {
FromAccountIndex int64
ApiKeyIndex uint8
ToAccountIndex int64
AssetIndex int16
FromRouteType uint8
ToRouteType uint8
Amount int64
USDCFee int64
ToAccountIndex int64
USDCAmount int64 // USDCAmount is given with 6 decimals
Fee int64
Memo [32]byte
Memo [32]byte
ExpiredAt int64
Nonce int64
Sig []byte
L1Sig string
SignedHash string `json:"-"`
}
func (txInfo *L2TransferTxInfo) Validate() error {
// plus one for treasury account
// FromAccountIndex
if txInfo.FromAccountIndex < MinAccountIndex+1 {
return ErrFromAccountIndexTooLow
}
@@ -41,29 +43,47 @@ func (txInfo *L2TransferTxInfo) Validate() error {
if txInfo.ApiKeyIndex < MinApiKeyIndex {
return ErrApiKeyIndexTooLow
}
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
return ErrApiKeyIndexTooHigh
}
if txInfo.ToAccountIndex < MinAccountIndex+1 {
// ToAccountIndex
if txInfo.ToAccountIndex < MinAccountIndex {
return ErrToAccountIndexTooLow
}
if txInfo.ToAccountIndex > MaxAccountIndex {
return ErrToAccountIndexTooHigh
}
if txInfo.USDCAmount <= 0 {
// AssetIndex
if txInfo.AssetIndex < MinAssetIndex {
return ErrAssetIndexTooLow
}
if txInfo.AssetIndex > MaxAssetIndex {
return ErrAssetIndexTooHigh
}
// FromRouteType
if txInfo.FromRouteType != AssetRouteType_Perps && txInfo.FromRouteType != AssetRouteType_Spot {
return ErrRouteTypeInvalid
}
// ToRouteType
if txInfo.ToRouteType != AssetRouteType_Perps && txInfo.ToRouteType != AssetRouteType_Spot {
return ErrRouteTypeInvalid
}
if txInfo.Amount <= 0 {
return ErrTransferAmountTooLow
}
if txInfo.USDCAmount > MaxTransferAmount {
if txInfo.Amount > MaxTransferAmount {
return ErrTransferAmountTooHigh
}
if txInfo.Fee < 0 {
if txInfo.USDCFee < 0 {
return ErrTransferFeeNegative
}
if txInfo.Fee > MaxTransferAmount {
if txInfo.USDCFee > MaxTransferAmount {
return ErrTransferFeeTooHigh
}
@@ -90,8 +110,33 @@ func (txInfo *L2TransferTxInfo) GetTxInfo() (string, error) {
return getTxInfo(txInfo)
}
func (txInfo *L2TransferTxInfo) GetL1SignatureBody(chainId uint32) string {
hexMemo := hex.EncodeToString(txInfo.Memo[:])
hexMemo = strings.Replace(hexMemo, "0x", "", 1)
signatureBody := fmt.Sprintf(
TemplateTransfer,
getHex10FromUint64(uint64(txInfo.Nonce)),
getHex10FromUint64(uint64(txInfo.FromAccountIndex)),
getHex10FromUint64(uint64(txInfo.FromRouteType)),
getHex10FromUint64(uint64(txInfo.ApiKeyIndex)),
getHex10FromUint64(uint64(txInfo.ToAccountIndex)),
getHex10FromUint64(uint64(txInfo.ToRouteType)),
getHex10FromUint64(uint64(txInfo.AssetIndex)),
getHex10FromUint64(uint64(txInfo.Amount)), //nolint:gosec
getHex10FromUint64(uint64(txInfo.USDCFee)), //nolint:gosec
getHex10FromUint64(uint64(chainId)), //nolint:gosec
hexMemo,
)
return signatureBody
}
func (txInfo *L2TransferTxInfo) GetL1AddressBySignature(chainId uint32) common.Address {
return calculateL1AddressBySignature(txInfo.GetL1SignatureBody(chainId), txInfo.L1Sig)
}
func (txInfo *L2TransferTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
elems := make([]g.Element, 0, 11)
elems := make([]g.Element, 0, 14)
elems = append(elems, g.FromUint32(lighterChainId))
elems = append(elems, g.FromUint32(TxTypeL2Transfer))
@@ -101,28 +146,13 @@ func (txInfo *L2TransferTxInfo) Hash(lighterChainId uint32, extra ...g.Element)
elems = append(elems, g.FromInt64(txInfo.FromAccountIndex))
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
elems = append(elems, g.FromInt64(txInfo.ToAccountIndex))
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)&0xFFFFFFFF)) //nolint:gosec
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)>>32)) //nolint:gosec
elems = append(elems, g.FromUint64(uint64(txInfo.Fee)&0xFFFFFFFF)) //nolint:gosec
elems = append(elems, g.FromUint64(uint64(txInfo.Fee)>>32)) //nolint:gosec
elems = append(elems, g.FromUint32(uint32(txInfo.AssetIndex)))
elems = append(elems, g.FromUint32(uint32(txInfo.FromRouteType)))
elems = append(elems, g.FromUint32(uint32(txInfo.ToRouteType)))
elems = append(elems, g.FromUint64((uint64(txInfo.Amount))&0xFFFFFFFF)) //nolint:gosec
elems = append(elems, g.FromUint64(uint64(txInfo.Amount)>>32)) //nolint:gosec
elems = append(elems, g.FromUint64((uint64(txInfo.USDCFee))&0xFFFFFFFF)) //nolint:gosec
elems = append(elems, g.FromUint64((uint64(txInfo.USDCFee))>>32)) //nolint:gosec
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
}
func (txInfo *L2TransferTxInfo) GetL1SignatureBody() string {
hexMemo := hex.EncodeToString(txInfo.Memo[:])
hexMemo = strings.Replace(hexMemo, "0x", "", 1)
signatureBody := fmt.Sprintf(
templateTransfer,
getHex10FromUint64(uint64(txInfo.Nonce)),
getHex10FromUint64(uint64(txInfo.FromAccountIndex)),
getHex10FromUint64(uint64(txInfo.ApiKeyIndex)),
getHex10FromUint64(uint64(txInfo.ToAccountIndex)),
getHex10FromUint64(uint64(txInfo.USDCAmount)),
getHex10FromUint64(uint64(txInfo.Fee)),
hexMemo,
)
return signatureBody
}
@@ -11,7 +11,7 @@ type L2UpdateLeverageTxInfo struct {
AccountIndex int64
ApiKeyIndex uint8
MarketIndex uint8
MarketIndex int16
InitialMarginFraction uint16
MarginMode uint8
@@ -48,13 +48,13 @@ func (txInfo *L2UpdateLeverageTxInfo) Validate() error {
if txInfo.ApiKeyIndex > MaxApiKeyIndex {
return ErrApiKeyIndexTooHigh
}
// MarketIndex
if txInfo.MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
if txInfo.MarketIndex == NilMarketIndex {
return ErrInvalidMarketIndex
}
if txInfo.MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
if txInfo.MarginMode != CrossMargin && txInfo.MarginMode != IsolatedMargin {
return ErrInvalidMarginMode
}
// InitialMarginFraction
@@ -73,10 +73,6 @@ func (txInfo *L2UpdateLeverageTxInfo) Validate() error {
return ErrExpiredAtInvalid
}
if txInfo.MarginMode != CrossMargin && txInfo.MarginMode != IsolatedMargin {
return ErrInvalidMarginMode
}
return nil
}
@@ -11,7 +11,7 @@ type L2UpdateMarginTxInfo struct {
AccountIndex int64
ApiKeyIndex uint8
MarketIndex uint8
MarketIndex int16
USDCAmount int64
Direction uint8
@@ -50,20 +50,16 @@ func (txInfo *L2UpdateMarginTxInfo) Validate() error {
}
// MarketIndex
if txInfo.MarketIndex < MinMarketIndex {
return ErrMarketIndexTooLow
}
if txInfo.MarketIndex > MaxMarketIndex {
return ErrMarketIndexTooHigh
if txInfo.MarketIndex < MinPerpsMarketIndex || txInfo.MarketIndex > MaxPerpsMarketIndex {
return ErrInvalidMarketIndex
}
if txInfo.USDCAmount <= 0 {
if txInfo.USDCAmount == 0 {
return ErrTransferAmountTooLow
}
if txInfo.USDCAmount > MaxTransferAmount {
return ErrTransferAmountTooHigh
}
if txInfo.Direction != RemoveFromIsolatedMargin && txInfo.Direction != AddToIsolatedMargin {
return ErrInvalidUpdateMarginDirection
}
@@ -15,7 +15,7 @@ type L2UpdatePublicPoolTxInfo struct {
Status uint8
OperatorFee int64
MinOperatorShareRate int64
MinOperatorShareRate uint16
ExpiredAt int64
Nonce int64
@@ -71,9 +71,6 @@ func (txInfo *L2UpdatePublicPoolTxInfo) Validate() error {
}
// MinOperatorShareRate
if txInfo.MinOperatorShareRate < 0 {
return ErrPoolMinOperatorShareRateTooLow
}
if txInfo.MinOperatorShareRate > ShareTick {
return ErrPoolMinOperatorShareRateTooHigh
}
@@ -103,7 +100,7 @@ func (txInfo *L2UpdatePublicPoolTxInfo) Hash(lighterChainId uint32, extra ...g.E
elems = append(elems, g.FromInt64(txInfo.PublicPoolIndex))
elems = append(elems, g.FromUint32(uint32(txInfo.Status)))
elems = append(elems, g.FromInt64(txInfo.OperatorFee))
elems = append(elems, g.FromInt64(txInfo.MinOperatorShareRate))
elems = append(elems, g.FromUint32(uint32(txInfo.MinOperatorShareRate)))
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
}
@@ -1,16 +1,35 @@
package txtypes
import "encoding/json"
import (
"encoding/json"
"fmt"
"strings"
func IsValidPubKey(bytes []byte) bool {
if len(bytes) != 40 {
return false
}
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
gQuint "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
return !isZeroByteSlice(bytes)
const (
TemplateChangePubKey = "Register Lighter Account\n\npubkey: 0x%s\nnonce: %s\naccount index: %s\napi key index: %s\nOnly sign this message for a trusted client!"
TemplateTransfer = "Transfer\n\nnonce: %s\nfrom: %s (route %s)\napi key: %s\nto: %s (route %s)\nasset: %s\namount: %s\nfee: %s" +
"\nchainId: %s\nmemo: %s\nOnly sign this message for a trusted client!"
TemplateSubAccount = "Create Lighter Sub Account\n\nmaster account index: %s\nOnly sign this message for a trusted client!"
)
const SignatureLength = 80
const L1SignatureLength = 65
const PubKeyLength = gFp5.Bytes
const HashLength = gQuint.Bytes
func IsValidPubKeyLength(bytes []byte) bool {
return len(bytes) == gFp5.Bytes
}
func isZeroByteSlice(bytes []byte) bool {
func IsZeroByteSlice(bytes []byte) bool {
for _, s := range bytes {
if s != 0 {
return false
@@ -26,3 +45,44 @@ func getTxInfo(tx interface{}) (string, error) {
}
return string(txInfoBytes), nil
}
func getHex10FromUint64(value uint64) string {
v := hexutil.EncodeUint64(value)
v = strings.Replace(v, "0x", "", 1)
// Make sure result has fixed bytes
vBytes := []byte(v)
if len(vBytes) < 16 {
toAppend := make([]byte, 16-len(vBytes))
for i := range toAppend {
toAppend[i] = 48
}
vBytes = append(toAppend, vBytes...)
}
return fmt.Sprintf("0x%s", string(vBytes))
}
func calculateL1AddressBySignature(signatureBody, l1Signature string) common.Address {
message := accounts.TextHash([]byte(signatureBody))
// Decode from signature string to get the signature byte array
signatureContent, err := hexutil.Decode(l1Signature)
if err != nil {
return [20]byte{}
}
// Transform yellow paper V from 27/28 to 0/1
if signatureContent[64] >= 27 {
signatureContent[64] -= 27
}
// Calculate the public key from the signature and source string
signaturePublicKey, err := crypto.SigToPub(message, signatureContent)
if err != nil {
return [20]byte{}
}
// Calculate the address from the public key
publicAddress := crypto.PubkeyToAddress(*signaturePublicKey)
return publicAddress
}
@@ -10,13 +10,13 @@ var _ TxInfo = (*L2WithdrawTxInfo)(nil)
type L2WithdrawTxInfo struct {
FromAccountIndex int64
ApiKeyIndex uint8
USDCAmount uint64 // USDCAmount is given with 6 decimals
ExpiredAt int64
Nonce int64
Sig []byte
SignedHash string `json:"-"`
AssetIndex int16
RouteType uint8
Amount uint64
ExpiredAt int64
Nonce int64
Sig []byte
SignedHash string `json:"-"`
}
func (txInfo *L2WithdrawTxInfo) Validate() error {
@@ -35,10 +35,24 @@ func (txInfo *L2WithdrawTxInfo) Validate() error {
return ErrApiKeyIndexTooHigh
}
if txInfo.USDCAmount == 0 {
// AssetIndex
if txInfo.AssetIndex < MinAssetIndex {
return ErrAssetIndexTooLow
}
if txInfo.AssetIndex > MaxAssetIndex {
return ErrAssetIndexTooHigh
}
// RouteType
if txInfo.RouteType != AssetRouteType_Perps && txInfo.RouteType != AssetRouteType_Spot {
return ErrRouteTypeInvalid
}
// Amount
if txInfo.Amount == 0 {
return ErrWithdrawalAmountTooLow
}
if txInfo.USDCAmount > MaxWithdrawalAmount {
if txInfo.Amount > MaxWithdrawalAmount {
return ErrWithdrawalAmountTooHigh
}
@@ -66,7 +80,7 @@ func (txInfo *L2WithdrawTxInfo) GetTxHash() string {
}
func (txInfo *L2WithdrawTxInfo) Hash(lighterChainId uint32, extra ...g.Element) (msgHash []byte, err error) {
elems := make([]g.Element, 0, 8)
elems := make([]g.Element, 0, 14)
elems = append(elems, g.FromUint32(lighterChainId))
elems = append(elems, g.FromUint32(TxTypeL2Withdraw))
@@ -75,8 +89,10 @@ func (txInfo *L2WithdrawTxInfo) Hash(lighterChainId uint32, extra ...g.Element)
elems = append(elems, g.FromInt64(txInfo.FromAccountIndex))
elems = append(elems, g.FromUint32(uint32(txInfo.ApiKeyIndex)))
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)&0xFFFFFFFF)) //nolint:gosec
elems = append(elems, g.FromUint64(uint64(txInfo.USDCAmount)>>32)) //nolint:gosec
elems = append(elems, g.FromUint32(uint32(txInfo.AssetIndex)))
elems = append(elems, g.FromUint32(uint32(txInfo.RouteType)))
elems = append(elems, g.FromUint64(uint64(txInfo.Amount&0xFFFFFFFF)))
elems = append(elems, g.FromUint64(uint64(txInfo.Amount>>32)))
return p2.HashToQuinticExtension(elems).ToLittleEndianBytes(), nil
}
+731
View File
@@ -0,0 +1,731 @@
//go:build js
// +build js
package main
import (
"fmt"
"strconv"
"syscall/js"
"time"
"github.com/elliottech/lighter-go/client"
"github.com/elliottech/lighter-go/client/http"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func wrapErr(err error) js.Value {
if err != nil {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("%v", err)})
}
return js.ValueOf(map[string]interface{}{})
}
func messageToSign(info txtypes.TxInfo) string {
switch tx := info.(type) {
case *txtypes.L2ChangePubKeyTxInfo:
return tx.GetL1SignatureBody()
case *txtypes.L2TransferTxInfo:
return tx.GetL1SignatureBody()
default:
return ""
}
}
func convertTxInfoToJS(info txtypes.TxInfo, err error) js.Value {
if err != nil {
return wrapErr(err)
}
if info == nil {
return js.ValueOf(map[string]interface{}{"error": "nil response"})
}
txInfoStr, strErr := info.GetTxInfo()
if strErr != nil {
return wrapErr(strErr)
}
out := map[string]interface{}{
"txType": info.GetTxType(),
"txInfo": txInfoStr,
"txHash": info.GetTxHash(),
}
if msg := messageToSign(info); msg != "" {
out["messageToSign"] = msg
}
return js.ValueOf(out)
}
// safeInt safely extracts an int from a js.Value, handling undefined values
func safeInt(v js.Value, index int) (int64, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return int64(v.Int()), nil
}
// safeUint8 safely extracts a uint8 from a js.Value, handling undefined values
func safeUint8(v js.Value, index int) (uint8, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return uint8(v.Int()), nil
}
// safeUint32 safely extracts a uint32 from a js.Value, handling undefined values
func safeUint32(v js.Value, index int) (uint32, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return uint32(v.Int()), nil
}
func getClient(args []js.Value) (*client.TxClient, error) {
l := len(args)
if l < 2 {
return nil, fmt.Errorf("insufficient arguments: need at least 2 for apiKeyIndex and accountIndex")
}
// Check if the last two arguments are valid and extract safely
if args[l-2].Type() == js.TypeUndefined || args[l-1].Type() == js.TypeUndefined {
return nil, fmt.Errorf("apiKeyIndex or accountIndex is undefined")
}
apiKeyIndexVal, err := safeUint8(args[l-2], l-2)
if err != nil {
return nil, err
}
accountIndexVal, err := safeInt(args[l-1], l-1)
if err != nil {
return nil, err
}
return client.GetClient(apiKeyIndexVal, accountIndexVal)
}
// recoverPanic wraps a function execution with panic recovery
func recoverPanic(fn func() js.Value) (result js.Value) {
defer func() {
if r := recover(); r != nil {
result = wrapErr(fmt.Errorf("panic: %v", r))
}
}()
return fn()
}
func main() {
js.Global().Set("GenerateAPIKey", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 1 {
return js.ValueOf(map[string]interface{}{"error": "GenerateAPIKey expects 1 arg: seed"})
}
seed := args[0].String()
privateKey, publicKey, err := client.GenerateAPIKey(seed)
if err != nil {
return wrapErr(err)
}
return js.ValueOf(map[string]interface{}{"privateKey": privateKey, "publicKey": publicKey})
})
}))
js.Global().Set("CreateClient", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "CreateClient expects 5 args: url, privateKey, chainId, apiKeyIndex, accountIndex"})
}
url := args[0].String()
privateKey := args[1].String()
chainId := uint32(args[2].Int())
apiKeyIndex := uint8(args[3].Int())
accountIndex := int64(args[4].Int())
httpClient := http.NewClient(url)
_, err := client.CreateClient(httpClient, privateKey, chainId, apiKeyIndex, accountIndex)
if err != nil {
return wrapErr(err)
}
return wrapErr(nil)
})
}))
js.Global().Set("CheckClient", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 2 {
return js.ValueOf(map[string]interface{}{"error": "CheckClient expects 2 args: apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
err = c.Check()
if err != nil {
return wrapErr(err)
}
return wrapErr(nil)
})
}))
js.Global().Set("CreateAuthToken", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 3 {
return js.ValueOf(map[string]interface{}{"error": "CreateAuthToken expects 3 args: deadline, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
deadline := int64(args[0].Int())
if deadline == 0 {
deadline = time.Now().Add(time.Hour * 7).Unix()
}
token, err := c.GetAuthToken(time.Unix(deadline, 0))
if err != nil {
return wrapErr(err)
}
return js.ValueOf(map[string]interface{}{"authToken": token})
})
}))
js.Global().Set("SignChangePubKey", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 4 {
return js.ValueOf(map[string]interface{}{"error": "SignChangePubKey expects 4 args: pubKeyHex, nonce, apiKeyIndex, accountIndex"})
}
pubKeyHex := args[0].String()
nonce := int64(args[1].Int())
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
pubKeyBytes, err := hexutil.Decode(pubKeyHex)
if err != nil {
return wrapErr(err)
}
if len(pubKeyBytes) != 40 {
return js.ValueOf(map[string]interface{}{"error": "invalid pub key length. expected 40 but got " + strconv.Itoa(len(pubKeyBytes))})
}
var pubKey [40]byte
copy(pubKey[:], pubKeyBytes)
txInfo := &types.ChangePubKeyReq{
PubKey: pubKey,
}
ops := &types.TransactOpts{
Nonce: &nonce,
}
tx, err := c.GetChangePubKeyTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 13 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateOrder expects 13 args: marketIndex, clientOrderIndex, baseAmount, price, isAsk, orderType, timeInForce, reduceOnly, triggerPrice, orderExpiry, nonce, apiKeyIndex, accountIndex"})
}
// Validate all arguments are defined before accessing
for i := 0; i < 13; i++ {
if args[i].Type() == js.TypeUndefined {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("argument %d is undefined", i)})
}
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex, err := safeUint8(args[0], 0)
if err != nil {
return wrapErr(err)
}
clientOrderIndex, err := safeInt(args[1], 1)
if err != nil {
return wrapErr(err)
}
baseAmount, err := safeInt(args[2], 2)
if err != nil {
return wrapErr(err)
}
price, err := safeUint32(args[3], 3)
if err != nil {
return wrapErr(err)
}
isAsk, err := safeUint8(args[4], 4)
if err != nil {
return wrapErr(err)
}
orderType, err := safeUint8(args[5], 5)
if err != nil {
return wrapErr(err)
}
timeInForce, err := safeUint8(args[6], 6)
if err != nil {
return wrapErr(err)
}
reduceOnly, err := safeUint8(args[7], 7)
if err != nil {
return wrapErr(err)
}
triggerPrice, err := safeUint32(args[8], 8)
if err != nil {
return wrapErr(err)
}
orderExpiry, err := safeInt(args[9], 9)
if err != nil {
return wrapErr(err)
}
nonce, err := safeInt(args[10], 10)
if err != nil {
return wrapErr(err)
}
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli() // 28 days
}
txInfo := &types.CreateOrderTxReq{
MarketIndex: marketIndex,
ClientOrderIndex: clientOrderIndex,
BaseAmount: baseAmount,
Price: price,
IsAsk: isAsk,
Type: orderType,
TimeInForce: timeInForce,
ReduceOnly: reduceOnly,
TriggerPrice: triggerPrice,
OrderExpiry: orderExpiry,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreateOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCancelOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCancelOrder expects 5 args: marketIndex, orderIndex, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
orderIndex := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.CancelOrderTxReq{
MarketIndex: marketIndex,
Index: orderIndex,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCancelOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCancelAllOrders", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCancelAllOrders expects 5 args: timeInForce, time, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
timeInForce := uint8(args[0].Int())
timeVal := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.CancelAllOrdersTxReq{
TimeInForce: timeInForce,
Time: timeVal,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCancelAllOrdersTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignTransfer", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 7 {
return js.ValueOf(map[string]interface{}{"error": "SignTransfer expects 7 args: toAccount, usdcAmount, fee, memo, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
toAccount := int64(args[0].Int())
usdcAmount := int64(args[1].Int())
fee := int64(args[2].Int())
memoStr := args[3].String()
nonce := int64(args[4].Int())
var memoArr [32]byte
bs := []byte(memoStr)
if len(bs) != 32 {
return wrapErr(fmt.Errorf("memo expected to be 32 bytes long"))
}
for i := 0; i < 32; i++ {
memoArr[i] = bs[i]
}
txInfo := &types.TransferTxReq{
ToAccountIndex: toAccount,
USDCAmount: usdcAmount,
Fee: fee,
Memo: memoArr,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetTransferTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignWithdraw", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 4 {
return js.ValueOf(map[string]interface{}{"error": "SignWithdraw expects 4 args: usdcAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
usdcAmount := uint64(args[0].Int())
nonce := int64(args[1].Int())
txInfo := &types.WithdrawTxReq{
USDCAmount: usdcAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetWithdrawTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdateLeverage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdateLeverage expects 6 args: marketIndex, fraction, marginMode, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
fraction := uint16(args[1].Int())
marginMode := uint8(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.UpdateLeverageTxReq{
MarketIndex: marketIndex,
InitialMarginFraction: fraction,
MarginMode: marginMode,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdateLeverageTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignModifyOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 8 {
return js.ValueOf(map[string]interface{}{"error": "SignModifyOrder expects 8 args: marketIndex, index, baseAmount, price, triggerPrice, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
index := int64(args[1].Int())
baseAmount := int64(args[2].Int())
price := uint32(args[3].Int())
triggerPrice := uint32(args[4].Int())
nonce := int64(args[5].Int())
txInfo := &types.ModifyOrderTxReq{
MarketIndex: marketIndex,
Index: index,
BaseAmount: baseAmount,
Price: price,
TriggerPrice: triggerPrice,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetModifyOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateSubAccount", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 3 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateSubAccount expects 3 args: nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
nonce := int64(args[0].Int())
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreateSubAccountTransaction(ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreatePublicPool", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignCreatePublicPool expects 6 args: operatorFee, initialTotalShares, minOperatorShareRate, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
operatorFee := int64(args[0].Int())
initialTotalShares := int64(args[1].Int())
minOperatorShareRate := int64(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.CreatePublicPoolTxReq{
OperatorFee: operatorFee,
InitialTotalShares: initialTotalShares,
MinOperatorShareRate: minOperatorShareRate,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreatePublicPoolTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdatePublicPool", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 7 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdatePublicPool expects 7 args: publicPoolIndex, status, operatorFee, minOperatorShareRate, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := uint8(args[0].Int())
status := uint8(args[1].Int())
operatorFee := int64(args[2].Int())
minOperatorShareRate := int64(args[3].Int())
nonce := int64(args[4].Int())
txInfo := &types.UpdatePublicPoolTxReq{
PublicPoolIndex: int64(publicPoolIndex),
Status: status,
OperatorFee: operatorFee,
MinOperatorShareRate: minOperatorShareRate,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdatePublicPoolTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignMintShares", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignMintShares expects 5 args: publicPoolIndex, shareAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := int64(args[0].Int())
shareAmount := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.MintSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetMintSharesTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignBurnShares", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignBurnShares expects 5 args: publicPoolIndex, shareAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := int64(args[0].Int())
shareAmount := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.BurnSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetBurnSharesTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdateMargin", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdateMargin expects 6 args: marketIndex, usdcAmount, direction, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
usdcAmount := int64(args[1].Int())
direction := uint8(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.UpdateMarginTxReq{
MarketIndex: marketIndex,
USDCAmount: usdcAmount,
Direction: direction,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdateMarginTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateGroupedOrders", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateGroupedOrders expects 5 args: groupingType, orders array, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
groupingType := uint8(args[0].Int())
// Parse orders array from JS
ordersArg := args[1]
if ordersArg.Type() != js.TypeObject {
return js.ValueOf(map[string]interface{}{"error": "orders must be an array"})
}
length := ordersArg.Length()
orders := make([]*types.CreateOrderTxReq, length)
for i := 0; i < length; i++ {
orderObj := ordersArg.Index(i)
if orderObj.Type() != js.TypeObject {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("order %d must be an object", i)})
}
orderExpiry := int64(orderObj.Get("OrderExpiry").Int())
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli()
}
orders[i] = &types.CreateOrderTxReq{
MarketIndex: uint8(orderObj.Get("MarketIndex").Int()),
ClientOrderIndex: int64(orderObj.Get("ClientOrderIndex").Int()),
BaseAmount: int64(orderObj.Get("BaseAmount").Int()),
Price: uint32(orderObj.Get("Price").Int()),
IsAsk: uint8(orderObj.Get("IsAsk").Int()),
Type: uint8(orderObj.Get("Type").Int()),
TimeInForce: uint8(orderObj.Get("TimeInForce").Int()),
ReduceOnly: uint8(orderObj.Get("ReduceOnly").Int()),
TriggerPrice: uint32(orderObj.Get("TriggerPrice").Int()),
OrderExpiry: orderExpiry,
}
}
nonce := int64(args[2].Int())
req := &types.CreateGroupedOrdersTxReq{
GroupingType: groupingType,
Orders: orders,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
txInfo, err := c.GetCreateGroupedOrdersTransaction(req, ops)
return convertTxInfoToJS(txInfo, err)
})
}))
select {}
}
+6 -1
View File
@@ -68,4 +68,9 @@ openapi-generator-cli.jar
.idea
examples/secrets.py
examples/secrets.py
# Environment variables
.env
.env.*
api_key_config.json
@@ -1,6 +1,7 @@
docs/Account.md
docs/AccountApi.md
docs/AccountApiKeys.md
docs/AccountAsset.md
docs/AccountLimits.md
docs/AccountMarginStats.md
docs/AccountMarketStats.md
@@ -14,9 +15,12 @@ docs/Announcement.md
docs/AnnouncementApi.md
docs/Announcements.md
docs/ApiKey.md
docs/Asset.md
docs/AssetDetails.md
docs/Block.md
docs/BlockApi.md
docs/Blocks.md
docs/Bridge.md
docs/BridgeApi.md
docs/BridgeSupportedNetwork.md
docs/Candlestick.md
@@ -46,28 +50,27 @@ docs/LiqTrade.md
docs/Liquidation.md
docs/LiquidationInfo.md
docs/LiquidationInfos.md
docs/MarketInfo.md
docs/MarketConfig.md
docs/NextNonce.md
docs/NotificationApi.md
docs/Order.md
docs/OrderApi.md
docs/OrderBook.md
docs/OrderBookDepth.md
docs/OrderBookDetail.md
docs/OrderBookDetails.md
docs/OrderBookOrders.md
docs/OrderBookStats.md
docs/OrderBooks.md
docs/Orders.md
docs/PerpsMarketStats.md
docs/PerpsOrderBookDetail.md
docs/PnLEntry.md
docs/PositionFunding.md
docs/PositionFundings.md
docs/PriceLevel.md
docs/PublicPool.md
docs/PublicPoolInfo.md
docs/PublicPoolMetadata.md
docs/PublicPoolShare.md
docs/PublicPools.md
docs/ReferralApi.md
docs/ReferralPointEntry.md
docs/ReferralPoints.md
@@ -81,8 +84,10 @@ docs/ReqGetAccountLimits.md
docs/ReqGetAccountMetadata.md
docs/ReqGetAccountPnL.md
docs/ReqGetAccountTxs.md
docs/ReqGetAssetDetails.md
docs/ReqGetBlock.md
docs/ReqGetBlockTxs.md
docs/ReqGetBridgesByL1Addr.md
docs/ReqGetByAccount.md
docs/ReqGetCandlesticks.md
docs/ReqGetDepositHistory.md
@@ -97,7 +102,6 @@ docs/ReqGetOrderBookDetails.md
docs/ReqGetOrderBookOrders.md
docs/ReqGetOrderBooks.md
docs/ReqGetPositionFunding.md
docs/ReqGetPublicPools.md
docs/ReqGetPublicPoolsMetadata.md
docs/ReqGetRangeWithCursor.md
docs/ReqGetRangeWithIndex.md
@@ -110,10 +114,14 @@ docs/ReqGetTransferHistory.md
docs/ReqGetTx.md
docs/ReqGetWithdrawHistory.md
docs/RespChangeAccountTier.md
docs/RespGetBridgesByL1Addr.md
docs/RespGetFastBridgeInfo.md
docs/RespGetIsNextBridgeFast.md
docs/RespPublicPoolsMetadata.md
docs/RespSendTx.md
docs/RespSendTxBatch.md
docs/RespUpdateKickback.md
docs/RespUpdateReferralCode.md
docs/RespWithdrawalDelay.md
docs/ResultCode.md
docs/RiskInfo.md
@@ -121,6 +129,8 @@ docs/RiskParameters.md
docs/RootApi.md
docs/SharePrice.md
docs/SimpleOrder.md
docs/SpotMarketStats.md
docs/SpotOrderBookDetail.md
docs/Status.md
docs/SubAccounts.md
docs/Ticker.md
@@ -160,6 +170,7 @@ lighter/exceptions.py
lighter/models/__init__.py
lighter/models/account.py
lighter/models/account_api_keys.py
lighter/models/account_asset.py
lighter/models/account_limits.py
lighter/models/account_margin_stats.py
lighter/models/account_market_stats.py
@@ -172,8 +183,11 @@ lighter/models/account_trade_stats.py
lighter/models/announcement.py
lighter/models/announcements.py
lighter/models/api_key.py
lighter/models/asset.py
lighter/models/asset_details.py
lighter/models/block.py
lighter/models/blocks.py
lighter/models/bridge.py
lighter/models/bridge_supported_network.py
lighter/models/candlestick.py
lighter/models/candlesticks.py
@@ -199,26 +213,25 @@ lighter/models/liq_trade.py
lighter/models/liquidation.py
lighter/models/liquidation_info.py
lighter/models/liquidation_infos.py
lighter/models/market_info.py
lighter/models/market_config.py
lighter/models/next_nonce.py
lighter/models/order.py
lighter/models/order_book.py
lighter/models/order_book_depth.py
lighter/models/order_book_detail.py
lighter/models/order_book_details.py
lighter/models/order_book_orders.py
lighter/models/order_book_stats.py
lighter/models/order_books.py
lighter/models/orders.py
lighter/models/perps_market_stats.py
lighter/models/perps_order_book_detail.py
lighter/models/pn_l_entry.py
lighter/models/position_funding.py
lighter/models/position_fundings.py
lighter/models/price_level.py
lighter/models/public_pool.py
lighter/models/public_pool_info.py
lighter/models/public_pool_metadata.py
lighter/models/public_pool_share.py
lighter/models/public_pools.py
lighter/models/referral_point_entry.py
lighter/models/referral_points.py
lighter/models/req_export_data.py
@@ -231,8 +244,10 @@ lighter/models/req_get_account_limits.py
lighter/models/req_get_account_metadata.py
lighter/models/req_get_account_pn_l.py
lighter/models/req_get_account_txs.py
lighter/models/req_get_asset_details.py
lighter/models/req_get_block.py
lighter/models/req_get_block_txs.py
lighter/models/req_get_bridges_by_l1_addr.py
lighter/models/req_get_by_account.py
lighter/models/req_get_candlesticks.py
lighter/models/req_get_deposit_history.py
@@ -247,7 +262,6 @@ lighter/models/req_get_order_book_details.py
lighter/models/req_get_order_book_orders.py
lighter/models/req_get_order_books.py
lighter/models/req_get_position_funding.py
lighter/models/req_get_public_pools.py
lighter/models/req_get_public_pools_metadata.py
lighter/models/req_get_range_with_cursor.py
lighter/models/req_get_range_with_index.py
@@ -260,16 +274,22 @@ lighter/models/req_get_transfer_history.py
lighter/models/req_get_tx.py
lighter/models/req_get_withdraw_history.py
lighter/models/resp_change_account_tier.py
lighter/models/resp_get_bridges_by_l1_addr.py
lighter/models/resp_get_fast_bridge_info.py
lighter/models/resp_get_is_next_bridge_fast.py
lighter/models/resp_public_pools_metadata.py
lighter/models/resp_send_tx.py
lighter/models/resp_send_tx_batch.py
lighter/models/resp_update_kickback.py
lighter/models/resp_update_referral_code.py
lighter/models/resp_withdrawal_delay.py
lighter/models/result_code.py
lighter/models/risk_info.py
lighter/models/risk_parameters.py
lighter/models/share_price.py
lighter/models/simple_order.py
lighter/models/spot_market_stats.py
lighter/models/spot_order_book_detail.py
lighter/models/status.py
lighter/models/sub_accounts.py
lighter/models/ticker.py
@@ -291,4 +311,19 @@ lighter/rest.py
setup.cfg
test-requirements.txt
test/__init__.py
test/test_account_asset.py
test/test_asset.py
test/test_asset_details.py
test/test_bridge.py
test/test_market_config.py
test/test_perps_market_stats.py
test/test_perps_order_book_detail.py
test/test_req_get_asset_details.py
test/test_req_get_bridges_by_l1_addr.py
test/test_resp_get_bridges_by_l1_addr.py
test/test_resp_get_is_next_bridge_fast.py
test/test_resp_update_kickback.py
test/test_resp_update_referral_code.py
test/test_spot_market_stats.py
test/test_spot_order_book_detail.py
tox.ini
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+6 -3
View File
@@ -36,9 +36,12 @@ import asyncio
async def main():
client = lighter.ApiClient()
account_api = lighter.AccountApi(client)
account = await account_api.get_account(by="index", value="1")
print(account)
try:
account_api = lighter.AccountApi(client)
account = await account_api.account(by="index", value="1")
print(account)
finally:
await client.close() # Make sure connection is cleanly closed
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,4 @@
disallowAdditionalPropertiesIfNotPresent: false
library: asyncio
packageName: lighter-sdk
projectName: lighter-sdk
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
**l1_address** | **str** | |
**cancel_all_time** | **int** | |
**total_order_count** | **int** | |
**total_isolated_order_count** | **int** | |
**pending_order_count** | **int** | |
**available_balance** | **str** | |
**status** | **int** | |
@@ -14,7 +14,6 @@ Method | HTTP request | Description
[**liquidations**](AccountApi.md#liquidations) | **GET** /api/v1/liquidations | liquidations
[**pnl**](AccountApi.md#pnl) | **GET** /api/v1/pnl | pnl
[**position_funding**](AccountApi.md#position_funding) | **GET** /api/v1/positionFunding | positionFunding
[**public_pools**](AccountApi.md#public_pools) | **GET** /api/v1/publicPools | publicPools
[**public_pools_metadata**](AccountApi.md#public_pools_metadata) | **GET** /api/v1/publicPoolsMetadata | publicPoolsMetadata
@@ -770,85 +769,6 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **public_pools**
> PublicPools public_pools(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
publicPools
Get public pools
### Example
```python
import lighter
from lighter.models.public_pools import PublicPools
from lighter.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
# See configuration.py for a list of all supported configuration parameters.
configuration = lighter.Configuration(
host = "https://mainnet.zklighter.elliot.ai"
)
# Enter a context with an instance of the API client
async with lighter.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = lighter.AccountApi(api_client)
index = 56 # int |
limit = 56 # int |
authorization = 'authorization_example' # str | (optional)
auth = 'auth_example' # str | (optional)
filter = 'filter_example' # str | (optional)
account_index = 56 # int | (optional)
try:
# publicPools
api_response = await api_instance.public_pools(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
print("The response of AccountApi->public_pools:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AccountApi->public_pools: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**index** | **int**| |
**limit** | **int**| |
**authorization** | **str**| | [optional]
**auth** | **str**| | [optional]
**filter** | **str**| | [optional]
**account_index** | **int**| | [optional]
### Return type
[**PublicPools**](PublicPools.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | A successful response. | - |
**400** | Bad request | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **public_pools_metadata**
> RespPublicPoolsMetadata public_pools_metadata(index, limit, authorization=authorization, auth=auth, filter=filter, account_index=account_index)
@@ -0,0 +1,32 @@
# AccountAsset
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**symbol** | **str** | |
**asset_id** | **int** | |
**balance** | **str** | |
**locked_balance** | **str** | |
## Example
```python
from lighter.models.account_asset import AccountAsset
# TODO update the JSON string below
json = "{}"
# create an instance of AccountAsset from a JSON string
account_asset_instance = AccountAsset.from_json(json)
# print the JSON string representation of the object
print(AccountAsset.to_json())
# convert the object into a dict
account_asset_dict = account_asset_instance.to_dict()
# create an instance of AccountAsset from a dict
account_asset_from_dict = AccountAsset.from_dict(account_asset_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -8,7 +8,9 @@ Name | Type | Description | Notes
**code** | **int** | |
**message** | **str** | | [optional]
**max_llp_percentage** | **int** | |
**max_llp_amount** | **str** | |
**user_tier** | **str** | |
**can_create_public_pool** | **bool** | |
## Example
@@ -8,6 +8,7 @@ Name | Type | Description | Notes
**title** | **str** | |
**content** | **str** | |
**created_at** | **int** | |
**expired_at** | **int** | |
## Example
@@ -0,0 +1,37 @@
# Asset
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**asset_id** | **int** | |
**symbol** | **str** | |
**l1_decimals** | **int** | |
**decimals** | **int** | |
**min_transfer_amount** | **str** | |
**min_withdrawal_amount** | **str** | |
**margin_mode** | **str** | |
**index_price** | **str** | |
**l1_address** | **str** | |
## Example
```python
from lighter.models.asset import Asset
# TODO update the JSON string below
json = "{}"
# create an instance of Asset from a JSON string
asset_instance = Asset.from_json(json)
# print the JSON string representation of the object
print(Asset.to_json())
# convert the object into a dict
asset_dict = asset_instance.to_dict()
# create an instance of Asset from a dict
asset_from_dict = Asset.from_dict(asset_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -1,4 +1,4 @@
# PublicPools
# AssetDetails
## Properties
@@ -7,25 +7,24 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**code** | **int** | |
**message** | **str** | | [optional]
**total** | **int** | |
**public_pools** | [**List[PublicPool]**](PublicPool.md) | |
**asset_details** | [**List[Asset]**](Asset.md) | |
## Example
```python
from lighter.models.public_pools import PublicPools
from lighter.models.asset_details import AssetDetails
# TODO update the JSON string below
json = "{}"
# create an instance of PublicPools from a JSON string
public_pools_instance = PublicPools.from_json(json)
# create an instance of AssetDetails from a JSON string
asset_details_instance = AssetDetails.from_json(json)
# print the JSON string representation of the object
print(PublicPools.to_json())
print(AssetDetails.to_json())
# convert the object into a dict
public_pools_dict = public_pools_instance.to_dict()
# create an instance of PublicPools from a dict
public_pools_from_dict = PublicPools.from_dict(public_pools_dict)
asset_details_dict = asset_details_instance.to_dict()
# create an instance of AssetDetails from a dict
asset_details_from_dict = AssetDetails.from_dict(asset_details_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,43 @@
# Bridge
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **int** | |
**version** | **int** | |
**source** | **str** | |
**source_chain_id** | **str** | |
**fast_bridge_tx_hash** | **str** | |
**batch_claim_tx_hash** | **str** | |
**cctp_burn_tx_hash** | **str** | |
**amount** | **str** | |
**intent_address** | **str** | |
**status** | **str** | |
**step** | **str** | |
**description** | **str** | |
**created_at** | **int** | |
**updated_at** | **int** | |
**is_external_deposit** | **bool** | |
## Example
```python
from lighter.models.bridge import Bridge
# TODO update the JSON string below
json = "{}"
# create an instance of Bridge from a JSON string
bridge_instance = Bridge.from_json(json)
# print the JSON string representation of the object
print(Bridge.to_json())
# convert the object into a dict
bridge_dict = bridge_instance.to_dict()
# create an instance of Bridge from a dict
bridge_from_dict = Bridge.from_dict(bridge_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -4,9 +4,149 @@ All URIs are relative to *https://mainnet.zklighter.elliot.ai*
Method | HTTP request | Description
------------- | ------------- | -------------
[**bridges**](BridgeApi.md#bridges) | **GET** /api/v1/bridges | bridges
[**bridges_is_next_bridge_fast**](BridgeApi.md#bridges_is_next_bridge_fast) | **GET** /api/v1/bridges/isNextBridgeFast | bridges_isNextBridgeFast
[**fastbridge_info**](BridgeApi.md#fastbridge_info) | **GET** /api/v1/fastbridge/info | fastbridge_info
# **bridges**
> RespGetBridgesByL1Addr bridges(l1_address)
bridges
Get bridges for given l1 address
### Example
```python
import lighter
from lighter.models.resp_get_bridges_by_l1_addr import RespGetBridgesByL1Addr
from lighter.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
# See configuration.py for a list of all supported configuration parameters.
configuration = lighter.Configuration(
host = "https://mainnet.zklighter.elliot.ai"
)
# Enter a context with an instance of the API client
async with lighter.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = lighter.BridgeApi(api_client)
l1_address = 'l1_address_example' # str |
try:
# bridges
api_response = await api_instance.bridges(l1_address)
print("The response of BridgeApi->bridges:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BridgeApi->bridges: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**l1_address** | **str**| |
### Return type
[**RespGetBridgesByL1Addr**](RespGetBridgesByL1Addr.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | A successful response. | - |
**400** | Bad request | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **bridges_is_next_bridge_fast**
> RespGetIsNextBridgeFast bridges_is_next_bridge_fast(l1_address)
bridges_isNextBridgeFast
Get if next bridge is fast
### Example
```python
import lighter
from lighter.models.resp_get_is_next_bridge_fast import RespGetIsNextBridgeFast
from lighter.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to https://mainnet.zklighter.elliot.ai
# See configuration.py for a list of all supported configuration parameters.
configuration = lighter.Configuration(
host = "https://mainnet.zklighter.elliot.ai"
)
# Enter a context with an instance of the API client
async with lighter.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = lighter.BridgeApi(api_client)
l1_address = 'l1_address_example' # str |
try:
# bridges_isNextBridgeFast
api_response = await api_instance.bridges_is_next_bridge_fast(l1_address)
print("The response of BridgeApi->bridges_is_next_bridge_fast:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BridgeApi->bridges_is_next_bridge_fast: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**l1_address** | **str**| |
### Return type
[**RespGetIsNextBridgeFast**](RespGetIsNextBridgeFast.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | A successful response. | - |
**400** | Bad request | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **fastbridge_info**
> RespGetFastBridgeInfo fastbridge_info()
@@ -10,6 +10,10 @@ Name | Type | Description | Notes
**high** | **float** | |
**low** | **float** | |
**close** | **float** | |
**open_raw** | **float** | |
**high_raw** | **float** | |
**low_raw** | **float** | |
**close_raw** | **float** | |
**volume0** | **float** | |
**volume1** | **float** | |
**last_trade_id** | **int** | |

Some files were not shown because too many files have changed in this diff Show More