use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tokio::time::{Instant, sleep};
use crate::Client;
use crate::error::ClientError;
use crate::wallet::{Address, Wallet};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuctionKind {
TokenRegister,
PerpDeploy,
SpotPairDeploy,
}
impl AuctionKind {
#[must_use]
pub fn type_id(&self) -> &'static str {
match self {
Self::TokenRegister => "token_register",
Self::PerpDeploy => "perp_deploy",
Self::SpotPairDeploy => "spot_pair_deploy",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AuctionBid {
pub kind: AuctionKind,
pub bid_amount_usdc_cents: u128,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct BidReceipt {
pub round_id: u64,
pub bidder: Address,
pub accepted_amount_usdc_cents: u128,
pub status: String,
}
impl Client {
pub async fn submit_gas_auction_bid(
&self,
wallet: &Wallet,
bid: AuctionBid,
) -> Result<BidReceipt, ClientError> {
let action = json!({
"type": "submit_gas_auction_bid",
"bidder": wallet.address(),
"kind": bid.kind,
"bid_amount_usdc_cents": bid.bid_amount_usdc_cents,
});
self.rest().exchange().post_signed(wallet, action).await
}
pub async fn check_deploy_credit(&self, address: Address) -> Result<u32, ClientError> {
#[derive(Deserialize)]
struct CreditResp {
credit_count: u32,
}
let body = json!({
"type": "deploy_credit",
"address": address,
});
let resp: CreditResp = self.rest().info().raw(body).await.and_then(|v: Value| {
serde_json::from_value::<CreditResp>(v).map_err(ClientError::from)
})?;
Ok(resp.credit_count)
}
pub async fn await_deploy_credit(
&self,
wallet: &Wallet,
max_wait: Duration,
) -> Result<(), ClientError> {
if max_wait.is_zero() {
return Err(ClientError::Validation(
"await_deploy_credit: max_wait must be > 0".into(),
));
}
let deadline = Instant::now() + max_wait;
let mut delay = Duration::from_millis(500);
let cap = Duration::from_secs(5);
loop {
let credits = self.check_deploy_credit(wallet.address()).await?;
if credits > 0 {
return Ok(());
}
let now = Instant::now();
if now >= deadline {
return Err(ClientError::Validation(format!(
"await_deploy_credit: timed out after {:?} without seeing credit",
max_wait
)));
}
let remaining = deadline - now;
let sleep_for = delay.min(remaining);
sleep(sleep_for).await;
delay = (delay * 2).min(cap);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auction_kind_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&AuctionKind::TokenRegister).unwrap(),
"\"token_register\""
);
assert_eq!(
serde_json::to_string(&AuctionKind::PerpDeploy).unwrap(),
"\"perp_deploy\""
);
assert_eq!(
serde_json::to_string(&AuctionKind::SpotPairDeploy).unwrap(),
"\"spot_pair_deploy\""
);
}
#[test]
fn auction_bid_round_trips() {
let b = AuctionBid {
kind: AuctionKind::PerpDeploy,
bid_amount_usdc_cents: 150_000_000, };
let j = serde_json::to_string(&b).unwrap();
let dec: AuctionBid = serde_json::from_str(&j).unwrap();
assert_eq!(b, dec);
}
#[test]
fn bid_receipt_round_trips() {
let r = BidReceipt {
round_id: 42,
bidder: Address::ZERO,
accepted_amount_usdc_cents: 100_000_000, status: "accepted".into(),
};
let j = serde_json::to_string(&r).unwrap();
let dec: BidReceipt = serde_json::from_str(&j).unwrap();
assert_eq!(r, dec);
}
#[tokio::test]
async fn await_deploy_credit_rejects_zero_wait() {
let c = Client::new("https://devnet-gateway.mtf.exchange").unwrap();
let w = Wallet::random_for_testing();
let err = c.await_deploy_credit(&w, Duration::ZERO).await.unwrap_err();
assert!(matches!(err, ClientError::Validation(_)));
}
}