feat: implement HyperliquidMonitor for position opening and migrate dependencies to git repository urls

This commit is contained in:
0xcathiefish
2026-05-30 09:00:53 +00:00
parent ffd98934e4
commit eadaa81438
15 changed files with 5397 additions and 224 deletions
+8 -2
View File
@@ -1,6 +1,12 @@
# Application Environment Variables Template # Application Environment Variables Template
# Name of the application (e.g. nosync-app) # Name of the application
APP_NAME= APP_NAME=nosync-monitor
# Logging configuration level (e.g. info, debug, trace) # Logging configuration level (e.g. info, debug, trace)
RUST_LOG=info RUST_LOG=info
# Hyperliquid configuration
# Ethereum wallet address to monitor on Hyperliquid
WALLET_ADDRESS=0xc64cc00b46101bd40aa1c3121195e85c0b0918d8
# Whether to monitor on Testnet (true) or Mainnet (false)
IS_TESTNET=true
+12 -1
View File
@@ -5,7 +5,18 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- 添加外部参考库作为 Git 子模块:`noc` (notion-client) 与 `hype` (hyperliquid-rust-sdk) - 实现 `HyperliquidMonitor` 用于实时订阅以太坊/Hyperliquid钱包账户的交易与开仓事件。
- 新增 `PositionOpenEvent` 数据结构,当检测到持仓从0变动为非0时触发。
- 添加 `alloy` 库依赖,用于强类型地址解析。
- 新增 integration test `tests/monitor_test.rs` 与使用示例 `examples/demo.rs`
- 添加外部参考库作为 Git 子模块:`noc` (notion-client) 与 `hype` (hyperliquid-rust-sdk)。
### Changed
-`hyperliquid_rust_sdk``notion-client` 依赖方式更改为指向官方 Git 仓库的远程依赖,移除本地 Path 依赖。
- 重构 `src/main.rs` 以使用 `HyperliquidMonitor` 并通过 `.env` 环境变量加载钱包与测试网选项。
### Removed
- 移除初始化模版中未使用的占位模块 (`ModuleA`, `ModuleB`) 及其对应测试文件。
## [0.1.0] - 2026-05-30 ## [0.1.0] - 2026-05-30
### Added ### Added
Generated
+5101 -10
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -4,8 +4,11 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
alloy = "2.0.5"
anyhow = "1.0.102" anyhow = "1.0.102"
dotenvy = "0.15.7" dotenvy = "0.15.7"
hyperliquid_rust_sdk = { git = "https://github.com/hyperliquid-dex/hyperliquid-rust-sdk.git" }
notion-client = { git = "https://github.com/takassh/notion-client.git" }
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
tracing = "0.1.44" tracing = "0.1.44"
+39 -43
View File
@@ -1,16 +1,15 @@
# nosync # nosync
一款高效、解耦的异步消息处理与任务编排框架 一款高效、解耦的 Hyperliquid 钱包仓位监听与 Notion 同步工具
## 1. 项目简介 ## 1. 项目简介
`nosync` 是一个高性能的 Rust 异步消息处理和组件协作框架,专为解决多模块间松耦合通信、结构化日志追踪以及鲁棒的错误恢复机制而设计。它主要面向需要高并发、低延迟以及强模块化设计的服务端开发人员,为构建复杂微服务或本地计算引擎提供核心底座 `nosync` 是一个基于 Rust 构建的实时钱包仓位监控与分析工具。它通过 WebSocket 订阅指定的 Hyperliquid 钱包地址事件,实时抓取开仓(及后续平仓)交易,并将相关交易数据自动同步整理到 Notion 对应的数据库中,帮助用户进行自动化交易跟踪与统计
## 2. 核心功能 ## 2. 核心功能
* **模块化封装架构**:遵循面向对象设计原则,将组件封装为高内聚的 `pub struct` 并持有独立状态,杜绝全局可变状态与裸函数 * **实时钱包订阅**:使用 `hyperliquid-rust-sdk` 长连接订阅钱包的 `UserEvents` 变动,毫秒级感知仓位变化
* **异步事件处理**:全面支持基于 `tokio` 运行时的多任务处理,提升高负载场景下的吞吐量 * **开仓事件识别**:通过解析成交明细的 `start_position` 是否为零,精准捕捉首次开仓(Position Opening)动作
* **结构化日志监控**:集成 `tracing` 系统,提供细粒度的业务追踪与故障还原能力 * **断线自动重连**:内部包含 robust 的重连与容错机制,确保网络抖动或服务关闭重启后自动恢复订阅
* **强类型错误管理**利用 `thiserror` 定义清晰的组件级错误,拒绝吞掉异常,确保系统的健壮性 * **Notion 数据同步**集成 `notion-client` API,将捕捉到的开仓事件格式化为对应的属性并写入 Notion 数据库中(Notion 数据库结构由后续接口配置定义)
* **多环境适配能力**:天然支持通过 `.env` 文件和环境变量在运行时动态配置系统属性。
## 3. 架构与模块 ## 3. 架构与模块
本项目的目录结构与模块划分如下: 本项目的目录结构与模块划分如下:
@@ -18,30 +17,26 @@
``` ```
nosync/ nosync/
├── src/ ├── src/
│ ├── lib.rs # 库入口,统一导出公共接口与类型 │ ├── lib.rs # 统一模块导出
│ ├── structs.rs # 存放跨模块共享的纯数据结构 │ ├── structs.rs # 数据模型与事件定义(如 PositionOpenEvent
── module_a.rs # 业务模块 A (消息接收与底层处理) ── hyperliquid.rs # Hyperliquid 监听器实现(包含 HyperliquidMonitor
│ └── module_b.rs # 业务模块 B (工作流编排与核心控制)
├── examples/ # 使用示例 ├── examples/ # 使用示例
│ └── demo.rs # 核心运行演示 │ └── demo.rs # 实时钱包监听演示
├── tests/ # 集成测试 ├── tests/ # 集成测试
── module_a_test.rs ── monitor_test.rs # 监听器连通性与重连循环测试
│ └── module_b_test.rs ├── references/ # 本地参考源(submodules
├── references/ # Git 子模块与外部参考仓库目录 ├── .env # 本地运行环境变量配置文件 (不提交)
├── .env # 实际运行环境变量配置文件 (本地开发,不提交)
├── .env.example # 环境变量配置模板 ├── .env.example # 环境变量配置模板
├── Cargo.toml # Cargo 配置文件 ├── Cargo.toml # 远程 Git 依赖及配置
└── CHANGELOG.md # 变更日志 └── CHANGELOG.md # 变更日志
``` ```
* **ModuleA** (`ModuleA`):负责消息接收、数据有效性校验与核心的异步解析处理 * **HyperliquidMonitor** (`hyperliquid`):长周期运行服务,维护与 Hyperliquid API 的长连接,订阅 `UserEvents`
* **ModuleB** (`ModuleB`):负责编排 `ModuleA` 的执行流,充当协调器(Orchestrator)角色 * **PositionOpenEvent** (`structs`):定义了捕获到的开仓事件明细,包括币种、买卖方向、成交价格、数量、时间戳等
* **Structs** (`structs`):定义了消息体 `SharedMessage` 等公共数据契约。
## 4. 环境要求 ## 4. 环境要求
* **Rust**: `1.85.0` 或更高版本(支持最新 edition 2024 * **Rust**: `1.85.0` 或更高版本(支持最新 edition 2024
* **OS**: Linux, macOS, Windows * **OS**: Linux, macOS, Windows
* **运行时**: `tokio` (Full features)
## 5. 安装与启动 ## 5. 安装与启动
```bash ```bash
@@ -51,38 +46,38 @@ cd nosync
# 复制并配置环境变量 # 复制并配置环境变量
cp .env.example .env cp .env.example .env
# 可以根据需要修改 .env 中的内容 # 编辑 .env 文件,填入您需要监控的 WALLET_ADDRESS 及网络类型
# 构建项目 # 构建项目
cargo build --release cargo build --release
# 运行默认二进制应用 # 运行监控服务
cargo run cargo run
# 运行使用示例 # 运行本地监听 Demo
cargo run --example demo cargo run --example demo
``` ```
## 6. 使用示例 ## 6. 使用示例
最小可运行示例位于 [examples/demo.rs](file:///home/cathiefish/App/nosync/examples/demo.rs) 最小可运行示例位于 [examples/demo.rs](file:///home/cathiefish/App/nosync/examples/demo.rs)
```rust ```rust
use anyhow::Result; use alloy::primitives::address;
use nosync::{ModuleA, ModuleB}; use nosync::HyperliquidMonitor;
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tokio::sync::mpsc;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let subscriber = FmtSubscriber::builder() let wallet = address!("0xc64cc00b46101bd40aa1c3121195e85c0b0918d8");
.with_env_filter(EnvFilter::new("info")) let monitor = HyperliquidMonitor::new(wallet, true); // true 代表使用 Testnet
.finish();
tracing::subscriber::set_global_default(subscriber)?;
// 初始化核心组件 let (tx, mut rx) = mpsc::unbounded_channel();
let processor = ModuleA::new("example-processor".to_string())?; tokio::spawn(async move {
let orchestrator = ModuleB::new(processor)?; let _ = monitor.run(tx).await;
});
// 运行工作流 while let Some(event) = rx.recv().await {
orchestrator.run().await?; println!("Captured Position Opening Event: {:?}", event);
}
Ok(()) Ok(())
} }
``` ```
@@ -90,13 +85,14 @@ async fn main() -> Result<()> {
## 7. 环境变量说明 ## 7. 环境变量说明
| 环境变量名 | 用途 | 是否必填 | 默认值 / 示例值 | | 环境变量名 | 用途 | 是否必填 | 默认值 / 示例值 |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| `APP_NAME` | 应用程序或当前节点的名称标识,用于日志和初始化 | 否 | `nosync-default` | | `WALLET_ADDRESS` | 需要监控的以太坊/Hyperliquid钱包地址 | **是** | `0xc64cc00b46101bd40aa1c3121195e85c0b0918d8` |
| `RUST_LOG` | 设定日志输出级别 (e.g. error, warn, info, debug, trace) | 否 | `info` | | `IS_TESTNET` | 是否是 Testnet 环境 (`true`/`false`) | 否 | `true` |
| `RUST_LOG` | 设定日志输出级别 (e.g. error, warn, info, debug) | 否 | `info` |
## 8. 测试与开发 ## 8. 测试与开发
### 开发分支约定 ### 开发分支约定
* `dev`:主开发分支,新功能与修复首发合并至此。 * `dev`:主开发分支,新功能与修复首发合并至此。
* `main`:生产稳定分支,当且仅当测试、Clippy 与格式化全部通过后才合并 * `main`:生产稳定分支。
### 本地验证命令 ### 本地验证命令
在提交代码前,**必须**运行以下命令进行本地验证: 在提交代码前,**必须**运行以下命令进行本地验证:
@@ -104,12 +100,12 @@ async fn main() -> Result<()> {
# 自动格式化代码 # 自动格式化代码
cargo fmt --all cargo fmt --all
# 运行代码规范检查(不能有 warnings # 运行代码规范检查
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
# 执行单元测试与集成测试 # 执行集成测试
cargo test cargo test
``` ```
## 9. 变更日志指引 ## 9. 变更日志指引
关于项目的历史演进和每个版本的详细改动,请参阅 [CHANGELOG.md](file:///home/cathiefish/App/nosync/CHANGELOG.md)。 关于项目的详细改动,请参阅 [CHANGELOG.md](file:///home/cathiefish/App/nosync/CHANGELOG.md)。
+23 -8
View File
@@ -1,25 +1,40 @@
use alloy::primitives::address;
use anyhow::Result; use anyhow::Result;
use nosync::{ModuleA, ModuleB}; use nosync::HyperliquidMonitor;
use tokio::sync::mpsc;
use tracing::info; use tracing::info;
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Configure simple standard output logging
let subscriber = FmtSubscriber::builder() let subscriber = FmtSubscriber::builder()
.with_env_filter(EnvFilter::new("info")) .with_env_filter(EnvFilter::new("info"))
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber) tracing::subscriber::set_global_default(subscriber)
.map_err(|e| anyhow::anyhow!("failed to set global tracing subscriber: {e}"))?; .map_err(|e| anyhow::anyhow!("failed to set global tracing subscriber: {e}"))?;
info!("--- Running nosync library example ---"); info!("--- Starting Hyperliquid monitor demo ---");
// Construct the structs and execute the logic // Standard wallet address for demo
let processor = ModuleA::new("example-processor".to_string())?; let wallet = address!("0xc64cc00b46101bd40aa1c3121195e85c0b0918d8");
let orchestrator = ModuleB::new(processor)?; let monitor = HyperliquidMonitor::new(wallet, true);
orchestrator.run().await?; let (tx, mut rx) = mpsc::unbounded_channel();
// Spawn the monitor in a background task
tokio::spawn(async move {
if let Err(e) = monitor.run(tx).await {
tracing::error!("Monitor error: {:?}", e);
}
});
info!("Listening for position open events. Close with Ctrl+C...");
while let Some(event) = rx.recv().await {
info!(
"DEMO RECEIVED POSITION OPEN: coin={}, side={}, px={}, sz={}, time={}, tid={}",
event.coin, event.side, event.px, event.sz, event.time, event.tid
);
}
info!("--- Example execution completed successfully ---");
Ok(()) Ok(())
} }
+144
View File
@@ -0,0 +1,144 @@
use crate::structs::PositionOpenEvent;
use alloy::primitives::Address;
use hyperliquid_rust_sdk::{BaseUrl, InfoClient, Message, Subscription, UserData};
use thiserror::Error;
use tokio::sync::mpsc::UnboundedSender;
use tokio::time::{Duration, sleep};
use tracing::{debug, error, info, warn};
/// Errors specific to the HyperliquidMonitor.
#[derive(Error, Debug)]
pub enum HyperliquidMonitorError {
#[error("Failed to parse wallet address: {0}")]
AddressParseError(String),
}
/// Monitor for Hyperliquid wallet position openings.
pub struct HyperliquidMonitor {
wallet_address: Address,
is_testnet: bool,
}
impl HyperliquidMonitor {
/// Creates a new HyperliquidMonitor instance.
pub fn new(wallet_address: Address, is_testnet: bool) -> Self {
Self {
wallet_address,
is_testnet,
}
}
/// Runs the monitor WebSocket subscription in a loop, reconnecting if disconnected.
pub async fn run(
&self,
event_tx: UnboundedSender<PositionOpenEvent>,
) -> Result<(), HyperliquidMonitorError> {
let base_url = if self.is_testnet {
BaseUrl::Testnet
} else {
BaseUrl::Mainnet
};
info!(
wallet = %self.wallet_address,
is_testnet = self.is_testnet,
"Starting Hyperliquid monitor"
);
loop {
info!("Connecting to Hyperliquid InfoClient...");
let mut info_client = match InfoClient::new(None, Some(base_url)).await {
Ok(client) => client,
Err(e) => {
error!(
error = ?e,
"Failed to connect to Hyperliquid InfoClient, retrying in 5 seconds..."
);
sleep(Duration::from_secs(5)).await;
continue;
}
};
let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::unbounded_channel();
info!(
"Subscribing to UserEvents for wallet: {}",
self.wallet_address
);
if let Err(e) = info_client
.subscribe(
Subscription::UserEvents {
user: self.wallet_address,
},
ws_sender,
)
.await
{
error!(
error = ?e,
"Failed to subscribe to UserEvents, retrying in 5 seconds..."
);
sleep(Duration::from_secs(5)).await;
continue;
}
info!("Successfully subscribed to UserEvents.");
while let Some(msg) = ws_receiver.recv().await {
match msg {
Message::User(user_msg) => {
debug!(user_msg = ?user_msg, "Received user event message");
if let UserData::Fills(fills) = user_msg.data {
for fill in fills {
// Check if start_position is 0, which indicates a new position is opening
let start_pos: f64 = fill.start_position.parse().unwrap_or(0.0);
if start_pos == 0.0 {
info!(
coin = %fill.coin,
side = %fill.side,
px = %fill.px,
sz = %fill.sz,
tid = fill.tid,
"Position opening detected!"
);
let event = PositionOpenEvent {
coin: fill.coin,
side: fill.side,
px: fill.px,
sz: fill.sz,
time: fill.time,
tid: fill.tid,
};
if let Err(e) = event_tx.send(event) {
error!(
error = ?e,
"Failed to send PositionOpenEvent through channel"
);
}
} else {
debug!(
coin = %fill.coin,
start_pos = start_pos,
"Ignore non-opening trade fill"
);
}
}
}
}
Message::HyperliquidError(err_msg) => {
error!(error = %err_msg, "Received error message from Hyperliquid WS");
}
Message::Pong => {
debug!("Received Pong from Hyperliquid WS");
}
other => {
debug!(msg = ?other, "Received other message from Hyperliquid WS");
}
}
}
warn!("Hyperliquid WebSocket connection closed. Reconnecting in 5 seconds...");
sleep(Duration::from_secs(5)).await;
}
}
}
+3 -5
View File
@@ -1,7 +1,5 @@
pub mod module_a; pub mod hyperliquid;
pub mod module_b;
pub mod structs; pub mod structs;
pub use module_a::{ModuleA, ModuleAError}; pub use hyperliquid::{HyperliquidMonitor, HyperliquidMonitorError};
pub use module_b::{ModuleB, ModuleBError}; pub use structs::PositionOpenEvent;
pub use structs::SharedMessage;
+37 -16
View File
@@ -1,8 +1,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use dotenvy::dotenv; use dotenvy::dotenv;
use nosync::{ModuleA, ModuleB}; use nosync::HyperliquidMonitor;
use std::env; use std::env;
use tracing::info; use tokio::sync::mpsc;
use tracing::{error, info};
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
#[tokio::main] #[tokio::main]
@@ -10,29 +11,49 @@ async fn main() -> Result<()> {
// Load environment variables from .env if present // Load environment variables from .env if present
let _ = dotenv(); let _ = dotenv();
// Initialize tracing subscriber with settings from environment (RUST_LOG) // Initialize tracing subscriber
let subscriber = FmtSubscriber::builder() let subscriber = FmtSubscriber::builder()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber) tracing::subscriber::set_global_default(subscriber)
.map_err(|e| anyhow::anyhow!("failed to set global tracing subscriber: {e}"))?; .map_err(|e| anyhow::anyhow!("failed to set global tracing subscriber: {e}"))?;
info!("Starting nosync application..."); info!("Starting Hyperliquid Monitor Service...");
// Retrieve APP_NAME from environment variables, defaulting if not found // Retrieve WALLET_ADDRESS from environment
let app_name = env::var("APP_NAME").unwrap_or_else(|_| "nosync-default".to_string()); let wallet_str =
info!(app_name = %app_name, "Environment configured"); env::var("WALLET_ADDRESS").context("Missing WALLET_ADDRESS in environment variables")?;
let wallet = wallet_str
.parse::<alloy::primitives::Address>()
.map_err(|e| anyhow::anyhow!("Invalid WALLET_ADDRESS: {e}"))?;
// Initialize ModuleA and ModuleB // Retrieve IS_TESTNET from environment, defaulting to true if not set
let module_a = ModuleA::new(app_name).context("Failed to initialize ModuleA")?; let is_testnet_str = env::var("IS_TESTNET").unwrap_or_else(|_| "true".to_string());
let module_b = ModuleB::new(module_a).context("Failed to initialize ModuleB")?; let is_testnet = is_testnet_str.parse::<bool>().unwrap_or(true);
// Run ModuleB logic let monitor = HyperliquidMonitor::new(wallet, is_testnet);
module_b let (tx, mut rx) = mpsc::unbounded_channel();
.run()
.await // Spawn the monitor loop in the background
.context("Error occurred during execution")?; tokio::spawn(async move {
if let Err(e) = monitor.run(tx).await {
error!("Monitor runtime error: {:?}", e);
}
});
info!("Service running, waiting for wallet events...");
while let Some(event) = rx.recv().await {
info!(
coin = %event.coin,
side = %event.side,
px = %event.px,
sz = %event.sz,
time = event.time,
tid = event.tid,
"Captured position opening event!"
);
// Notion database integration will be wired here in the future
}
info!("nosync application finished successfully!");
Ok(()) Ok(())
} }
-61
View File
@@ -1,61 +0,0 @@
use crate::structs::SharedMessage;
use thiserror::Error;
use tracing::{debug, info};
/// Errors specific to ModuleA.
#[derive(Error, Debug)]
pub enum ModuleAError {
#[error("Failed to process message: {0}")]
ProcessError(String),
}
/// ModuleA processes messages and manages a name field.
pub struct ModuleA {
name: String,
}
impl ModuleA {
/// Creates a new ModuleA instance.
pub fn new(name: String) -> Result<Self, ModuleAError> {
info!(name = %name, "Initializing ModuleA");
Ok(Self { name })
}
/// Processes a shared message asynchronously.
pub async fn process_message(&self, msg: SharedMessage) -> Result<String, ModuleAError> {
debug!(msg_id = msg.id, "Processing message in ModuleA");
if msg.content.is_empty() {
return Err(ModuleAError::ProcessError(
"Empty message content".to_string(),
));
}
Ok(format!("ModuleA[{}] processed: {}", self.name, msg.content))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_process_message_success() {
let module_a = ModuleA::new("test-a".to_string()).unwrap();
let msg = SharedMessage {
id: 42,
content: "Hello".to_string(),
};
let result = module_a.process_message(msg).await.unwrap();
assert_eq!(result, "ModuleA[test-a] processed: Hello");
}
#[tokio::test]
async fn test_process_message_empty_content() {
let module_a = ModuleA::new("test-a".to_string()).unwrap();
let msg = SharedMessage {
id: 42,
content: String::new(),
};
let result = module_a.process_message(msg).await;
assert!(result.is_err());
}
}
-48
View File
@@ -1,48 +0,0 @@
use crate::module_a::ModuleA;
use crate::structs::SharedMessage;
use thiserror::Error;
use tracing::info;
/// Errors specific to ModuleB.
#[derive(Error, Debug)]
pub enum ModuleBError {
#[error("ModuleA error occurred: {0}")]
AError(#[from] crate::module_a::ModuleAError),
}
/// ModuleB holds a reference to ModuleA and interacts with it.
pub struct ModuleB {
processor: ModuleA,
}
impl ModuleB {
/// Creates a new ModuleB instance by taking ownership of a ModuleA processor.
pub fn new(processor: ModuleA) -> Result<Self, ModuleBError> {
info!("Initializing ModuleB");
Ok(Self { processor })
}
/// Runs the business logic of ModuleB.
pub async fn run(&self) -> Result<(), ModuleBError> {
let msg = SharedMessage {
id: 100,
content: "Hello from ModuleB".to_string(),
};
let result = self.processor.process_message(msg).await?;
info!(result = %result, "ModuleB execution succeeded");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_module_b_run() {
let module_a = ModuleA::new("test-a".to_string()).unwrap();
let module_b = ModuleB::new(module_a).unwrap();
let result = module_b.run().await;
assert!(result.is_ok());
}
}
+8 -4
View File
@@ -1,6 +1,10 @@
/// Shared message structure across modules. /// Represents a detected position opening event on Hyperliquid.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SharedMessage { pub struct PositionOpenEvent {
pub id: u64, pub coin: String,
pub content: String, pub side: String, // "B" (Buy) or "S" (Sell)
pub px: String, // Price as string
pub sz: String, // Size as string
pub time: u64, // Epoch timestamp in milliseconds
pub tid: u64, // Unique trade/fill ID
} }
-16
View File
@@ -1,16 +0,0 @@
use nosync::{ModuleA, SharedMessage};
#[tokio::test]
async fn test_integration_module_a() {
let module_a = ModuleA::new("integration-a".to_string()).expect("Failed to create ModuleA");
let msg = SharedMessage {
id: 999,
content: "Integration content".to_string(),
};
let res = module_a
.process_message(msg)
.await
.expect("Failed to process");
assert!(res.contains("integration-a"));
assert!(res.contains("Integration content"));
}
-10
View File
@@ -1,10 +0,0 @@
use nosync::{ModuleA, ModuleB};
#[tokio::test]
async fn test_integration_module_b() {
let module_a =
ModuleA::new("integration-b-processor".to_string()).expect("Failed to create ModuleA");
let module_b = ModuleB::new(module_a).expect("Failed to create ModuleB");
let res = module_b.run().await;
assert!(res.is_ok());
}
+19
View File
@@ -0,0 +1,19 @@
use alloy::primitives::address;
use nosync::HyperliquidMonitor;
use tokio::sync::mpsc;
use tokio::time::{Duration, timeout};
#[tokio::test]
async fn test_monitor_initialization() {
// Standard test wallet address
let wallet = address!("0xc64cc00b46101bd40aa1c3121195e85c0b0918d8");
let monitor = HyperliquidMonitor::new(wallet, true);
let (tx, _rx) = mpsc::unbounded_channel();
// Verify it connects and runs within a timeout (since it runs an infinite reconnect loop)
let run_result = timeout(Duration::from_secs(3), monitor.run(tx)).await;
// Timeout should occur if the connection is successful and continues running
assert!(run_result.is_err(), "Monitor should have timed out");
}