Skip to content

Commit 1793442

Browse files
committed
feat: enhance trade handling with optional identifiers and test mode support
1 parent 5df7ce3 commit 1793442

File tree

11 files changed

+165
-55
lines changed

11 files changed

+165
-55
lines changed

docker/docker-compose.relayer.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ services:
2424
condition: service_healthy
2525
command: ["moltrade-relayer", "--config", "/etc/moltrade-relayer/config.toml"]
2626
environment:
27-
RUST_LOG: moltrade_relayer=info
27+
DATABASE_URL: ${DATABASE_URL}
28+
REDIS_URL: ${REDIS_URL}
29+
# RUST_LOG: moltrade_relayer=info
2830
volumes:
2931
- ./relayer.config.toml:/etc/moltrade-relayer/config.toml
3032
ports:

relayer/config.template.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ follower_rate = 0.001
4242
leader_rate = 0.002
4343
min_credit = 0.5
4444
profit_multiplier = 1.2
45+
test_multiplier = 0.1
4546

4647
[subscriptions]
4748
daily_limit = 1000

relayer/src/api/rest_api.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,14 @@ struct RecordTradeRequest {
254254
side: String,
255255
size: f64,
256256
price: f64,
257-
tx_hash: String,
257+
tx_hash: Option<String>,
258+
oid: Option<String>,
258259
}
259260

260261
#[derive(Debug, Deserialize)]
261262
struct UpdateSettlementRequest {
262-
tx_hash: String,
263+
tx_hash: Option<String>,
264+
oid: Option<String>,
263265
status: String,
264266
pnl: Option<f64>,
265267
pnl_usd: Option<f64>,
@@ -398,7 +400,9 @@ async fn record_trade(
398400
&payload.side,
399401
payload.size,
400402
payload.price,
401-
&payload.tx_hash,
403+
payload.tx_hash.as_deref(),
404+
payload.oid.as_deref(),
405+
false,
402406
)
403407
.await
404408
.map_err(|e| {
@@ -412,7 +416,7 @@ async fn record_trade(
412416
return StatusCode::BAD_REQUEST;
413417
}
414418
if code == &SqlState::UNIQUE_VIOLATION {
415-
tracing::warn!("record_trade duplicate tx_hash: {}", db_err);
419+
tracing::warn!("record_trade duplicate tx_hash/oid: {}", db_err);
416420
return StatusCode::OK; // idempotent insert
417421
}
418422
}
@@ -443,7 +447,8 @@ async fn update_trade_settlement(
443447
}
444448

445449
svc.update_trade_settlement(
446-
&payload.tx_hash,
450+
payload.tx_hash.as_deref(),
451+
payload.oid.as_deref(),
447452
&payload.status,
448453
payload.pnl,
449454
payload.pnl_usd,

relayer/src/config/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub struct SettlementCreditConfig {
8989
pub min_credit: f64,
9090
#[serde(default = "default_profit_multiplier")]
9191
pub profit_multiplier: f64,
92+
#[serde(default = "default_test_multiplier")]
93+
pub test_multiplier: f64,
9294
#[serde(default = "default_credit_enable")]
9395
pub enable: bool,
9496
}
@@ -121,6 +123,10 @@ fn default_profit_multiplier() -> f64 {
121123
1.2
122124
}
123125

126+
fn default_test_multiplier() -> f64 {
127+
0.1
128+
}
129+
124130
fn default_credit_enable() -> bool {
125131
true
126132
}

relayer/src/core/event_router.rs

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,8 @@ impl EventRouter {
383383
// Followers for this bot
384384

385385
// Persist trade tx info if present in payload
386-
self.maybe_record_trade(subs, &bot.bot_pubkey, &plaintext)
386+
let event_id = event.id.to_hex();
387+
self.maybe_record_trade(subs, &bot.bot_pubkey, &plaintext, &event_id)
387388
.await;
388389
let followers = subs.list_subscriptions(&bot.bot_pubkey).await?;
389390
if followers.is_empty() {
@@ -502,7 +503,8 @@ impl EventRouter {
502503
None => return Ok(()),
503504
};
504505

505-
self.maybe_record_trade(subs, &bot.bot_pubkey, plaintext)
506+
let event_id = event.id.to_hex();
507+
self.maybe_record_trade(subs, &bot.bot_pubkey, plaintext, &event_id)
506508
.await;
507509
let followers = subs.list_subscriptions(&bot.bot_pubkey).await?;
508510
if followers.is_empty() {
@@ -609,16 +611,18 @@ impl EventRouter {
609611

610612
#[derive(Debug)]
611613
struct TradeMeta {
612-
tx_hash: String,
613-
symbol: String,
614-
side: String,
615-
size: f64,
616-
price: f64,
614+
tx_hash: Option<String>,
615+
oid: Option<String>,
616+
symbol: Option<String>,
617+
side: Option<String>,
618+
size: Option<f64>,
619+
price: Option<f64>,
617620
status: Option<String>,
618621
pnl: Option<f64>,
619622
pnl_usd: Option<f64>,
620623
follower_pubkey: Option<String>,
621624
role: String,
625+
is_test: bool,
622626
}
623627

624628
#[derive(Debug, Default)]
@@ -642,56 +646,85 @@ impl EventRouter {
642646
subs: &SubscriptionService,
643647
bot_pubkey: &str,
644648
plaintext: &str,
649+
event_id: &str,
645650
) {
646651
let meta = match extract_trade_meta(plaintext) {
647652
Some(m) => m,
648653
None => return,
649654
};
650655

656+
let oid_fallback = meta.oid.clone().or_else(|| Some(event_id.to_string()));
657+
651658
if let Err(e) = subs
652659
.record_trade_tx(
653660
bot_pubkey,
654661
meta.follower_pubkey.as_deref(),
655662
&meta.role,
656-
&meta.symbol,
657-
&meta.side,
658-
meta.size,
659-
meta.price,
660-
&meta.tx_hash,
663+
meta.symbol.as_deref().unwrap_or(""),
664+
meta.side.as_deref().unwrap_or(""),
665+
meta.size.unwrap_or(0.0),
666+
meta.price.unwrap_or(0.0),
667+
meta.tx_hash.as_deref(),
668+
oid_fallback.as_deref(),
669+
meta.is_test,
661670
)
662671
.await
663672
{
664-
error!("Failed to record trade tx {}: {}", meta.tx_hash, e);
673+
error!("Failed to record trade tx/oid: {}", e);
665674
}
666675

667676
if meta.status.is_some() || meta.pnl.is_some() || meta.pnl_usd.is_some() {
668677
if let Err(e) = subs
669678
.update_trade_settlement(
670-
&meta.tx_hash,
679+
meta.tx_hash.as_deref(),
680+
oid_fallback.as_deref(),
671681
meta.status.as_deref().unwrap_or("pending"),
672682
meta.pnl,
673683
meta.pnl_usd,
674684
)
675685
.await
676686
{
677-
error!("Failed to update settlement for {}: {}", meta.tx_hash, e);
687+
error!("Failed to update settlement for trade: {}", e);
678688
}
679689
}
680690
}
681691
}
682692

683693
fn extract_trade_meta(plaintext: &str) -> Option<TradeMeta> {
684694
let parsed: Value = serde_json::from_str(plaintext).ok()?;
685-
let tx_hash = parsed.get("tx_hash")?.as_str()?.to_string();
686-
let symbol = parsed.get("symbol")?.as_str()?.to_string();
687-
let side = parsed.get("side")?.as_str()?.to_string();
688-
let size = parsed.get("size")?.as_f64()?;
689-
let price = parsed.get("price")?.as_f64()?;
695+
let tx_hash = parsed
696+
.get("tx_hash")
697+
.and_then(|v| v.as_str())
698+
.map(|s| s.to_string());
699+
let oid = parsed
700+
.get("oid")
701+
.or_else(|| parsed.get("order_id"))
702+
.and_then(|v| v.as_str())
703+
.map(|s| s.to_string());
704+
let symbol = parsed
705+
.get("symbol")
706+
.and_then(|v| v.as_str())
707+
.map(|s| s.to_string());
708+
let side = parsed
709+
.get("side")
710+
.and_then(|v| v.as_str())
711+
.map(|s| s.to_string());
712+
let size = parsed.get("size").and_then(|v| v.as_f64());
713+
let price = parsed.get("price").and_then(|v| v.as_f64());
690714

691715
let status = parsed
692716
.get("status")
693717
.and_then(|v| v.as_str())
694718
.map(|s| s.to_string());
719+
let is_test = parsed
720+
.get("test_mode")
721+
.and_then(|v| v.as_bool())
722+
.unwrap_or(false)
723+
|| parsed
724+
.get("status")
725+
.and_then(|v| v.as_str())
726+
.map(|s| s.eq_ignore_ascii_case("simulated"))
727+
.unwrap_or(false);
695728
let pnl = parsed.get("pnl").and_then(|v| v.as_f64());
696729
let pnl_usd = parsed.get("pnl_usd").and_then(|v| v.as_f64());
697730
let follower_pubkey = parsed
@@ -705,8 +738,13 @@ fn extract_trade_meta(plaintext: &str) -> Option<TradeMeta> {
705738
.unwrap_or("leader")
706739
.to_string();
707740

741+
if tx_hash.is_none() && oid.is_none() {
742+
return None;
743+
}
744+
708745
Some(TradeMeta {
709746
tx_hash,
747+
oid,
710748
symbol,
711749
side,
712750
size,
@@ -716,6 +754,7 @@ fn extract_trade_meta(plaintext: &str) -> Option<TradeMeta> {
716754
pnl_usd,
717755
follower_pubkey,
718756
role,
757+
is_test,
719758
})
720759
}
721760

relayer/src/core/settlement_worker.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,46 @@ impl SettlementWorker {
5555
}
5656

5757
for t in trades {
58-
match self.verify_tx(&t.tx_hash).await {
58+
// Early-phase behavior: award credits as soon as we have an oid (tx hash may be absent for Hyperliquid).
59+
// If tx_hash is present we still attempt verification; otherwise we short-circuit to credit award.
60+
match self.verify_tx_opt(t.tx_hash.as_deref()).await {
5961
Ok(Some(true)) => {
6062
self.svc
61-
.update_trade_settlement(&t.tx_hash, "confirmed", None, None)
63+
.update_trade_settlement(t.tx_hash.as_deref(), t.oid.as_deref(), "confirmed", None, None)
6264
.await?;
6365
if let Some(credit) = self.compute_credit(&t) {
6466
let recipient = t.follower_pubkey.as_deref().unwrap_or(&t.bot_pubkey);
6567
self.svc
6668
.award_credits(&t.bot_pubkey, recipient, credit)
6769
.await?;
6870
}
69-
info!("settlement: confirmed {}", t.tx_hash);
71+
info!("settlement: confirmed tx_hash={:?} oid={:?}", t.tx_hash, t.oid);
7072
}
7173
Ok(Some(false)) => {
7274
self.svc
73-
.update_trade_settlement(&t.tx_hash, "failed", None, None)
75+
.update_trade_settlement(t.tx_hash.as_deref(), t.oid.as_deref(), "failed", None, None)
7476
.await?;
75-
warn!("settlement: marked failed {}", t.tx_hash);
77+
warn!("settlement: marked failed tx_hash={:?} oid={:?}", t.tx_hash, t.oid);
7678
}
7779
Ok(None) => {
78-
debug!("settlement: tx {} not yet found", t.tx_hash);
80+
// If no tx hash, treat pending entry as immediately credit-eligible.
81+
if t.tx_hash.is_none() {
82+
if let Some(credit) = self.compute_credit(&t) {
83+
let recipient = t.follower_pubkey.as_deref().unwrap_or(&t.bot_pubkey);
84+
self.svc
85+
.award_credits(&t.bot_pubkey, recipient, credit)
86+
.await?;
87+
}
88+
self.svc
89+
.update_trade_settlement(t.tx_hash.as_deref(), t.oid.as_deref(), "confirmed", None, None)
90+
.await?;
91+
info!("settlement: credited pending trade with oid={:?}", t.oid);
92+
} else {
93+
debug!("settlement: tx {:?} not yet found", t.tx_hash);
94+
}
7995
}
8096
Err(e) => {
81-
error!("settlement: verify {} error: {}", t.tx_hash, e);
97+
error!("settlement: verify tx_hash={:?} oid={:?} error: {}", t.tx_hash, t.oid, e);
8298
}
8399
}
84100
}
@@ -87,8 +103,12 @@ impl SettlementWorker {
87103
}
88104

89105
/// Naive verifier: HTTP GET the explorer endpoint; 200 -> confirmed, 404 -> unknown
90-
async fn verify_tx(&self, tx_hash: &str) -> Result<Option<bool>> {
91-
let url = format!("{}/{}", self.base_url.trim_end_matches('/'), tx_hash);
106+
async fn verify_tx_opt(&self, tx_hash: Option<&str>) -> Result<Option<bool>> {
107+
let tx = match tx_hash {
108+
Some(v) if !v.is_empty() => v,
109+
_ => return Ok(None),
110+
};
111+
let url = format!("{}/{}", self.base_url.trim_end_matches('/'), tx);
92112
let resp = self.client.get(&url).send().await?;
93113
match resp.status() {
94114
StatusCode::OK => Ok(Some(true)),
@@ -104,19 +124,23 @@ impl SettlementWorker {
104124
_ => return None,
105125
};
106126

107-
let rate = if trade.role == "leader" {
127+
let base_rate = if trade.role == "leader" {
108128
cfg.leader_rate
109129
} else {
110130
cfg.follower_rate
111131
};
112132

113-
let mut credit = (trade.size * trade.price * rate).max(cfg.min_credit);
133+
let mut credit = (trade.size * trade.price * base_rate).max(cfg.min_credit);
114134
if let Some(pnl) = trade.pnl_usd {
115135
if pnl > 0.0 {
116136
credit *= cfg.profit_multiplier;
117137
}
118138
}
119139

140+
if trade.is_test {
141+
credit *= cfg.test_multiplier;
142+
}
143+
120144
if credit.is_finite() && credit > 0.0 {
121145
Some(credit)
122146
} else {

0 commit comments

Comments
 (0)