From b2402352399ef3c94ad02dad20b086e47fe2730f Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 05:20:06 +0530 Subject: [PATCH 1/6] refactor: replace String fields with typed Address/B256/U256, remove dead helpers --- src/commands/approve.rs | 9 +-- src/commands/bridge.rs | 5 +- src/commands/clob.rs | 40 ++++++----- src/commands/comments.rs | 6 +- src/commands/ctf.rs | 141 +++++++++++++-------------------------- src/commands/data.rs | 36 +++++----- src/commands/events.rs | 2 +- src/commands/mod.rs | 51 +------------- src/commands/profiles.rs | 7 +- src/commands/setup.rs | 16 ++--- src/commands/sports.rs | 2 +- src/commands/wallet.rs | 51 ++++---------- src/output/events.rs | 24 ++----- src/output/markets.rs | 24 ++----- src/output/mod.rs | 10 +++ 15 files changed, 141 insertions(+), 283 deletions(-) diff --git a/src/commands/approve.rs b/src/commands/approve.rs index 329fa83..b27cb92 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -12,6 +12,7 @@ use crate::auth; use crate::output::OutputFormat; use crate::output::approve::{ApprovalStatus, print_approval_status, print_tx_result}; +/// Must match `USDC_ADDRESS_STR` in commands/mod.rs. const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"); sol! { @@ -39,7 +40,7 @@ pub enum ApproveCommand { /// Check current contract approvals for a wallet Check { /// Wallet address to check (defaults to configured wallet) - address: Option, + address: Option
, }, /// Approve all required contracts for trading (sends on-chain transactions) Set, @@ -82,18 +83,18 @@ pub async fn execute( private_key: Option<&str>, ) -> Result<()> { match args.command { - ApproveCommand::Check { address } => check(address.as_deref(), private_key, output).await, + ApproveCommand::Check { address } => check(address, private_key, output).await, ApproveCommand::Set => set(private_key, output).await, } } async fn check( - address_arg: Option<&str>, + address_arg: Option
, private_key: Option<&str>, output: OutputFormat, ) -> Result<()> { let owner: Address = if let Some(addr) = address_arg { - super::parse_address(addr)? + addr } else { let signer = auth::resolve_signer(private_key)?; polymarket_client_sdk::auth::Signer::address(&signer) diff --git a/src/commands/bridge.rs b/src/commands/bridge.rs index 75d474e..a6cb773 100644 --- a/src/commands/bridge.rs +++ b/src/commands/bridge.rs @@ -1,4 +1,3 @@ -use super::parse_address; use crate::output::OutputFormat; use crate::output::bridge::{print_deposit, print_status, print_supported_assets}; use anyhow::Result; @@ -19,7 +18,7 @@ pub enum BridgeCommand { /// Get deposit addresses for a wallet (EVM, Solana, Bitcoin) Deposit { /// Polymarket wallet address (0x...) - address: String, + address: polymarket_client_sdk::types::Address, }, /// List supported chains and tokens for deposits @@ -40,7 +39,7 @@ pub async fn execute( match args.command { BridgeCommand::Deposit { address } => { let request = DepositRequest::builder() - .address(parse_address(&address)?) + .address(address) .build(); let response = client.deposit(&request).await?; diff --git a/src/commands/clob.rs b/src/commands/clob.rs index d41302a..b3defe6 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -12,9 +12,7 @@ use polymarket_client_sdk::clob::types::{ PriceHistoryRequest, PriceRequest, SpreadRequest, TradesRequest, UserRewardsEarningRequest, }, }; -use polymarket_client_sdk::types::{Decimal, U256}; - -use super::parse_condition_id; +use polymarket_client_sdk::types::{B256, Decimal, U256}; use crate::auth; use crate::output::OutputFormat; use crate::output::clob::{ @@ -183,10 +181,10 @@ pub enum ClobCommand { Orders { /// Filter by market condition ID #[arg(long)] - market: Option, + market: Option, /// Filter by asset/token ID #[arg(long)] - asset: Option, + asset: Option, /// Pagination cursor #[arg(long)] cursor: Option, @@ -274,20 +272,20 @@ pub enum ClobCommand { CancelMarket { /// Market condition ID #[arg(long)] - market: Option, + market: Option, /// Asset/token ID #[arg(long)] - asset: Option, + asset: Option, }, /// List trades (authenticated) Trades { /// Filter by market condition ID #[arg(long)] - market: Option, + market: Option, /// Filter by asset/token ID #[arg(long)] - asset: Option, + asset: Option, /// Pagination cursor #[arg(long)] cursor: Option, @@ -300,7 +298,7 @@ pub enum ClobCommand { asset_type: CliAssetType, /// Token ID (required for conditional) #[arg(long)] - token: Option, + token: Option, }, /// Refresh balance allowance on-chain (authenticated) @@ -310,7 +308,7 @@ pub enum ClobCommand { asset_type: CliAssetType, /// Token ID (required for conditional) #[arg(long)] - token: Option, + token: Option, }, /// List notifications (authenticated) @@ -393,7 +391,7 @@ pub enum ClobCommand { AccountStatus, } -#[derive(Clone, Debug, clap::ValueEnum)] +#[derive(Clone, Copy, Debug, clap::ValueEnum)] pub enum CliSide { Buy, Sell, @@ -583,7 +581,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .map(|id| { PriceRequest::builder() .token_id(id) - .side(Side::from(side.clone())) + .side(Side::from(side)) .build() }) .collect(); @@ -763,8 +761,8 @@ async fn execute_trade( } => { let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = OrdersRequest::builder() - .maybe_market(market.map(|m| parse_condition_id(&m)).transpose()?) - .maybe_asset_id(asset.map(|a| parse_token_id(&a)).transpose()?) + .maybe_market(market) + .maybe_asset_id(asset) .build(); let result = client.orders(&request, cursor).await?; print_orders(&result, output)?; @@ -908,8 +906,8 @@ async fn execute_trade( ClobCommand::CancelMarket { market, asset } => { let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = CancelMarketOrderRequest::builder() - .maybe_market(market.map(|m| parse_condition_id(&m)).transpose()?) - .maybe_asset_id(asset.map(|a| parse_token_id(&a)).transpose()?) + .maybe_market(market) + .maybe_asset_id(asset) .build(); let result = client.cancel_market_orders(&request).await?; print_cancel_result(&result, output)?; @@ -922,8 +920,8 @@ async fn execute_trade( } => { let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = TradesRequest::builder() - .maybe_market(market.map(|m| parse_condition_id(&m)).transpose()?) - .maybe_asset_id(asset.map(|a| parse_token_id(&a)).transpose()?) + .maybe_market(market) + .maybe_asset_id(asset) .build(); let result = client.trades(&request, cursor).await?; print_trades(&result, output)?; @@ -934,7 +932,7 @@ async fn execute_trade( let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = BalanceAllowanceRequest::builder() .asset_type(AssetType::from(asset_type)) - .maybe_token_id(token.map(|t| parse_token_id(&t)).transpose()?) + .maybe_token_id(token) .build(); let result = client.balance_allowance(request).await?; print_balance(&result, is_collateral, output)?; @@ -944,7 +942,7 @@ async fn execute_trade( let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = BalanceAllowanceRequest::builder() .asset_type(AssetType::from(asset_type)) - .maybe_token_id(token.map(|t| parse_token_id(&t)).transpose()?) + .maybe_token_id(token) .build(); client.update_balance_allowance(request).await?; match output { diff --git a/src/commands/comments.rs b/src/commands/comments.rs index 9c55fad..be90ba2 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -1,4 +1,3 @@ -use super::parse_address; use crate::output::comments::{print_comment_detail, print_comments_table}; use crate::output::{OutputFormat, print_json}; use anyhow::Result; @@ -55,7 +54,7 @@ pub enum CommentsCommand { /// List comments by a user's wallet address ByUser { /// Wallet address (0x...) - address: String, + address: polymarket_client_sdk::types::Address, /// Max results #[arg(long, default_value = "25")] @@ -144,9 +143,8 @@ pub async fn execute( order, ascending, } => { - let addr = parse_address(&address)?; let request = CommentsByUserAddressRequest::builder() - .user_address(addr) + .user_address(address) .limit(limit) .maybe_offset(offset) .maybe_order(order) diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index eec7170..d3c63f8 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -15,6 +15,8 @@ use crate::output::ctf as ctf_output; const USDC_DECIMALS: Decimal = Decimal::from_parts(1_000_000, 0, 0, false, 0); +use super::USDC_ADDRESS_STR; + #[derive(Args)] pub struct CtfArgs { #[command(subcommand)] @@ -27,58 +29,58 @@ pub enum CtfCommand { Split { /// Condition ID (0x-prefixed 32-byte hex) #[arg(long)] - condition: String, + condition: B256, /// Amount in USDC (e.g. 10 for $10) #[arg(long)] amount: String, /// Collateral token address (defaults to USDC) - #[arg(long, default_value = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")] - collateral: String, + #[arg(long, default_value = USDC_ADDRESS_STR)] + collateral: Address, /// Custom partition as comma-separated index sets (e.g. "1,2" for binary, "1,2,4" for 3-outcome) #[arg(long)] partition: Option, /// Parent collection ID for nested positions (defaults to zero) #[arg(long)] - parent_collection: Option, + parent_collection: Option, }, /// Merge outcome tokens back into collateral Merge { /// Condition ID (0x-prefixed 32-byte hex) #[arg(long)] - condition: String, + condition: B256, /// Amount in USDC (e.g. 10 for $10) #[arg(long)] amount: String, /// Collateral token address (defaults to USDC) - #[arg(long, default_value = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")] - collateral: String, + #[arg(long, default_value = USDC_ADDRESS_STR)] + collateral: Address, /// Custom partition as comma-separated index sets (e.g. "1,2" for binary, "1,2,4" for 3-outcome) #[arg(long)] partition: Option, /// Parent collection ID for nested positions (defaults to zero) #[arg(long)] - parent_collection: Option, + parent_collection: Option, }, /// Redeem winning tokens after market resolution Redeem { /// Condition ID (0x-prefixed 32-byte hex) #[arg(long)] - condition: String, + condition: B256, /// Collateral token address (defaults to USDC) - #[arg(long, default_value = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")] - collateral: String, + #[arg(long, default_value = USDC_ADDRESS_STR)] + collateral: Address, /// Custom index sets as comma-separated values (e.g. "1,2" for binary, "1" for YES only) #[arg(long)] index_sets: Option, /// Parent collection ID for nested positions (defaults to zero) #[arg(long)] - parent_collection: Option, + parent_collection: Option, }, /// Redeem neg-risk positions RedeemNegRisk { /// Condition ID (0x-prefixed 32-byte hex) #[arg(long)] - condition: String, + condition: B256, /// Comma-separated amounts in USDC for each outcome (e.g. "10,5") #[arg(long)] amounts: String, @@ -87,10 +89,10 @@ pub enum CtfCommand { ConditionId { /// Oracle address (0x-prefixed) #[arg(long)] - oracle: String, + oracle: Address, /// Question ID (0x-prefixed 32-byte hex) #[arg(long)] - question: String, + question: B256, /// Number of outcomes (e.g. 2 for binary) #[arg(long)] outcomes: u64, @@ -99,22 +101,22 @@ pub enum CtfCommand { CollectionId { /// Condition ID (0x-prefixed 32-byte hex) #[arg(long)] - condition: String, + condition: B256, /// Index set (e.g. 1 for YES, 2 for NO in binary markets) #[arg(long)] index_set: u64, /// Parent collection ID (defaults to zero for top-level positions) #[arg(long)] - parent_collection: Option, + parent_collection: Option, }, /// Calculate a position ID (ERC1155 token ID) from collateral and collection PositionId { /// Collateral token address (defaults to USDC) - #[arg(long, default_value = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")] - collateral: String, + #[arg(long, default_value = USDC_ADDRESS_STR)] + collateral: Address, /// Collection ID (0x-prefixed 32-byte hex) #[arg(long)] - collection: String, + collection: B256, }, } @@ -164,23 +166,10 @@ fn parse_u256_csv(s: &str) -> Result> { .collect() } -fn parse_optional_parent(parent: Option<&str>) -> Result { - match parent { - Some(p) => super::parse_condition_id(p), - None => Ok(B256::default()), - } -} - -fn resolve_collateral(collateral: &str) -> Result
{ - super::parse_address(collateral) -} +const DEFAULT_BINARY_SETS: [u64; 2] = [1, 2]; -fn default_partition() -> Vec { - vec![U256::from(1), U256::from(2)] -} - -fn default_index_sets() -> Vec { - vec![U256::from(1), U256::from(2)] +fn binary_u256_vec() -> Vec { + DEFAULT_BINARY_SETS.iter().map(|&n| U256::from(n)).collect() } pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&str>) -> Result<()> { @@ -192,22 +181,20 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s partition, parent_collection, } => { - let condition_id = super::parse_condition_id(&condition)?; let usdc_amount = parse_usdc_amount(&amount)?; - let collateral_addr = resolve_collateral(&collateral)?; - let parent = parse_optional_parent(parent_collection.as_deref())?; + let parent = parent_collection.unwrap_or_default(); let partition = match partition { Some(p) => parse_u256_csv(&p)?, - None => default_partition(), + None => binary_u256_vec(), }; let provider = auth::create_provider(private_key).await?; let client = ctf::Client::new(provider, POLYGON)?; let req = SplitPositionRequest::builder() - .collateral_token(collateral_addr) + .collateral_token(collateral) .parent_collection_id(parent) - .condition_id(condition_id) + .condition_id(condition) .partition(partition) .amount(usdc_amount) .build(); @@ -226,22 +213,20 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s partition, parent_collection, } => { - let condition_id = super::parse_condition_id(&condition)?; let usdc_amount = parse_usdc_amount(&amount)?; - let collateral_addr = resolve_collateral(&collateral)?; - let parent = parse_optional_parent(parent_collection.as_deref())?; + let parent = parent_collection.unwrap_or_default(); let partition = match partition { Some(p) => parse_u256_csv(&p)?, - None => default_partition(), + None => binary_u256_vec(), }; let provider = auth::create_provider(private_key).await?; let client = ctf::Client::new(provider, POLYGON)?; let req = MergePositionsRequest::builder() - .collateral_token(collateral_addr) + .collateral_token(collateral) .parent_collection_id(parent) - .condition_id(condition_id) + .condition_id(condition) .partition(partition) .amount(usdc_amount) .build(); @@ -259,21 +244,19 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s index_sets, parent_collection, } => { - let condition_id = super::parse_condition_id(&condition)?; - let collateral_addr = resolve_collateral(&collateral)?; - let parent = parse_optional_parent(parent_collection.as_deref())?; + let parent = parent_collection.unwrap_or_default(); let index_sets = match index_sets { Some(s) => parse_u256_csv(&s)?, - None => default_index_sets(), + None => binary_u256_vec(), }; let provider = auth::create_provider(private_key).await?; let client = ctf::Client::new(provider, POLYGON)?; let req = RedeemPositionsRequest::builder() - .collateral_token(collateral_addr) + .collateral_token(collateral) .parent_collection_id(parent) - .condition_id(condition_id) + .condition_id(condition) .index_sets(index_sets) .build(); @@ -285,14 +268,13 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s ctf_output::print_tx_result("redeem", resp.transaction_hash, resp.block_number, &output) } CtfCommand::RedeemNegRisk { condition, amounts } => { - let condition_id = super::parse_condition_id(&condition)?; let amounts = parse_usdc_amounts(&amounts)?; let provider = auth::create_provider(private_key).await?; let client = ctf::Client::with_neg_risk(provider, POLYGON)?; let req = RedeemNegRiskRequest::builder() - .condition_id(condition_id) + .condition_id(condition) .amounts(amounts) .build(); @@ -313,15 +295,12 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s question, outcomes, } => { - let oracle_addr = super::parse_address(&oracle)?; - let question_id = super::parse_condition_id(&question)?; - let provider = auth::create_readonly_provider().await?; let client = ctf::Client::new(provider, POLYGON)?; let req = ConditionIdRequest::builder() - .oracle(oracle_addr) - .question_id(question_id) + .oracle(oracle) + .question_id(question) .outcome_slot_count(U256::from(outcomes)) .build(); @@ -333,15 +312,14 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s index_set, parent_collection, } => { - let condition_id = super::parse_condition_id(&condition)?; - let parent = parse_optional_parent(parent_collection.as_deref())?; + let parent = parent_collection.unwrap_or_default(); let provider = auth::create_readonly_provider().await?; let client = ctf::Client::new(provider, POLYGON)?; let req = CollectionIdRequest::builder() .parent_collection_id(parent) - .condition_id(condition_id) + .condition_id(condition) .index_set(U256::from(index_set)) .build(); @@ -352,15 +330,12 @@ pub async fn execute(args: CtfArgs, output: OutputFormat, private_key: Option<&s collateral, collection, } => { - let collateral_addr = super::parse_address(&collateral)?; - let collection_id = super::parse_condition_id(&collection)?; - let provider = auth::create_readonly_provider().await?; let client = ctf::Client::new(provider, POLYGON)?; let req = PositionIdRequest::builder() - .collateral_token(collateral_addr) - .collection_id(collection_id) + .collateral_token(collateral) + .collection_id(collection) .build(); let resp = client.position_id(&req).await?; @@ -503,32 +478,8 @@ mod tests { } #[test] - fn parse_optional_parent_none_is_zero() { - let result = parse_optional_parent(None).unwrap(); - assert_eq!(result, B256::default()); - } - - #[test] - fn parse_optional_parent_some_parses() { - let hex = "0x0000000000000000000000000000000000000000000000000000000000000001"; - let result = parse_optional_parent(Some(hex)).unwrap(); - assert_ne!(result, B256::default()); - } - - #[test] - fn parse_optional_parent_invalid_fails() { - assert!(parse_optional_parent(Some("garbage")).is_err()); - } - - #[test] - fn default_partition_is_binary() { - let p = default_partition(); + fn binary_u256_vec_is_binary() { + let p = binary_u256_vec(); assert_eq!(p, vec![U256::from(1u64), U256::from(2u64)]); } - - #[test] - fn default_index_sets_is_binary() { - let s = default_index_sets(); - assert_eq!(s, vec![U256::from(1u64), U256::from(2u64)]); - } } diff --git a/src/commands/data.rs b/src/commands/data.rs index bca7236..0ec0bd2 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -1,4 +1,4 @@ -use super::{parse_address, parse_condition_id}; +use polymarket_client_sdk::types::{Address, B256}; use crate::output::OutputFormat; use crate::output::data::{ print_activity, print_builder_leaderboard, print_builder_volume, print_closed_positions, @@ -27,7 +27,7 @@ pub enum DataCommand { /// Get open positions for a wallet address Positions { /// Wallet address (0x...) - address: String, + address: Address, /// Max results #[arg(long, default_value = "25")] @@ -41,7 +41,7 @@ pub enum DataCommand { /// Get closed positions for a wallet address ClosedPositions { /// Wallet address (0x...) - address: String, + address: Address, /// Max results #[arg(long, default_value = "25")] @@ -55,19 +55,19 @@ pub enum DataCommand { /// Get total position value for a wallet address Value { /// Wallet address (0x...) - address: String, + address: Address, }, /// Get count of unique markets traded by a wallet Traded { /// Wallet address (0x...) - address: String, + address: Address, }, /// Get trade history Trades { /// Wallet address (0x...) - address: String, + address: Address, /// Max results #[arg(long, default_value = "25")] @@ -81,7 +81,7 @@ pub enum DataCommand { /// Get on-chain activity for a wallet address Activity { /// Wallet address (0x...) - address: String, + address: Address, /// Max results #[arg(long, default_value = "25")] @@ -95,7 +95,7 @@ pub enum DataCommand { /// Get top token holders for a market Holders { /// Market condition ID (0x...) - market: String, + market: B256, /// Max results per token #[arg(long, default_value = "10")] @@ -105,7 +105,7 @@ pub enum DataCommand { /// Get open interest for markets OpenInterest { /// Market condition ID (0x...) - market: String, + market: B256, }, /// Get live volume for an event @@ -226,7 +226,7 @@ async fn execute_user( offset, } => { let request = PositionsRequest::builder() - .user(parse_address(&address)?) + .user(address) .limit(limit)? .maybe_offset(offset)? .build(); @@ -241,7 +241,7 @@ async fn execute_user( offset, } => { let request = ClosedPositionsRequest::builder() - .user(parse_address(&address)?) + .user(address) .limit(limit)? .maybe_offset(offset)? .build(); @@ -252,7 +252,7 @@ async fn execute_user( DataCommand::Value { address } => { let request = ValueRequest::builder() - .user(parse_address(&address)?) + .user(address) .build(); let values = client.value(&request).await?; @@ -261,7 +261,7 @@ async fn execute_user( DataCommand::Traded { address } => { let request = TradedRequest::builder() - .user(parse_address(&address)?) + .user(address) .build(); let traded = client.traded(&request).await?; @@ -274,7 +274,7 @@ async fn execute_user( offset, } => { let request = TradesRequest::builder() - .user(parse_address(&address)?) + .user(address) .limit(limit)? .maybe_offset(offset)? .build(); @@ -289,7 +289,7 @@ async fn execute_user( offset, } => { let request = ActivityRequest::builder() - .user(parse_address(&address)?) + .user(address) .limit(limit)? .maybe_offset(offset)? .build(); @@ -311,9 +311,8 @@ async fn execute_market( ) -> Result<()> { match command { DataCommand::Holders { market, limit } => { - let cid = parse_condition_id(&market)?; let request = HoldersRequest::builder() - .markets(vec![cid]) + .markets(vec![market]) .limit(limit)? .build(); @@ -322,8 +321,7 @@ async fn execute_market( } DataCommand::OpenInterest { market } => { - let cid = parse_condition_id(&market)?; - let request = OpenInterestRequest::builder().markets(vec![cid]).build(); + let request = OpenInterestRequest::builder().markets(vec![market]).build(); let oi = client.open_interest(&request).await?; print_open_interest(&oi, output)?; diff --git a/src/commands/events.rs b/src/commands/events.rs index b5d947a..cbe6b7b 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -81,7 +81,7 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .maybe_offset(offset) .maybe_ascending(if ascending { Some(true) } else { None }) .maybe_tag_slug(tag) - .order(order.into_iter().collect::>()) + .order(order.into_iter().collect()) .build(); let events = client.events(&request).await?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 671c0ee..1e8671e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ -use polymarket_client_sdk::types::{Address, B256}; +/// Polygon USDC contract address (shared across ctf and approve commands). +pub const USDC_ADDRESS_STR: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; pub mod approve; pub mod bridge; @@ -17,17 +18,7 @@ pub mod upgrade; pub mod wallet; pub fn is_numeric_id(id: &str) -> bool { - !id.is_empty() && id.chars().all(|c| c.is_ascii_digit()) -} - -pub fn parse_address(s: &str) -> anyhow::Result
{ - s.parse() - .map_err(|_| anyhow::anyhow!("Invalid address: must be a 0x-prefixed hex address")) -} - -pub fn parse_condition_id(s: &str) -> anyhow::Result { - s.parse() - .map_err(|_| anyhow::anyhow!("Invalid condition ID: must be a 0x-prefixed 32-byte hex")) + id.parse::().is_ok() } #[cfg(test)] @@ -51,40 +42,4 @@ mod tests { fn is_numeric_id_rejects_empty() { assert!(!is_numeric_id("")); } - - #[test] - fn parse_address_valid_hex() { - let addr = "0x0000000000000000000000000000000000000001"; - assert!(parse_address(addr).is_ok()); - } - - #[test] - fn parse_address_rejects_short_hex() { - let err = parse_address("0x1234").unwrap_err().to_string(); - assert!(err.contains("0x-prefixed"), "got: {err}"); - } - - #[test] - fn parse_address_rejects_garbage() { - let err = parse_address("not-an-address").unwrap_err().to_string(); - assert!(err.contains("0x-prefixed"), "got: {err}"); - } - - #[test] - fn parse_condition_id_valid_64_hex() { - let id = "0x0000000000000000000000000000000000000000000000000000000000000001"; - assert!(parse_condition_id(id).is_ok()); - } - - #[test] - fn parse_condition_id_rejects_wrong_length() { - let err = parse_condition_id("0x0001").unwrap_err().to_string(); - assert!(err.contains("32-byte"), "got: {err}"); - } - - #[test] - fn parse_condition_id_rejects_garbage() { - let err = parse_condition_id("garbage").unwrap_err().to_string(); - assert!(err.contains("32-byte"), "got: {err}"); - } } diff --git a/src/commands/profiles.rs b/src/commands/profiles.rs index c50c73d..f3964fc 100644 --- a/src/commands/profiles.rs +++ b/src/commands/profiles.rs @@ -1,9 +1,9 @@ -use super::parse_address; use crate::output::profiles::print_profile_detail; use crate::output::{OutputFormat, print_json}; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{self, types::request::PublicProfileRequest}; +use polymarket_client_sdk::types::Address; #[derive(Args)] pub struct ProfilesArgs { @@ -16,7 +16,7 @@ pub enum ProfilesCommand { /// Get a public profile by wallet address Get { /// Wallet address (0x...) - address: String, + address: Address, }, } @@ -27,8 +27,7 @@ pub async fn execute( ) -> Result<()> { match args.command { ProfilesCommand::Get { address } => { - let addr = parse_address(&address)?; - let req = PublicProfileRequest::builder().address(addr).build(); + let req = PublicProfileRequest::builder().address(address).build(); let profile = client.public_profile(&req).await?; match output { diff --git a/src/commands/setup.rs b/src/commands/setup.rs index dd04671..e9b8c47 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -1,4 +1,3 @@ -use std::fmt::Write as _; use std::io::{self, BufRead, Write}; use std::str::FromStr; @@ -7,7 +6,7 @@ use polymarket_client_sdk::auth::{LocalSigner, Signer as _}; use polymarket_client_sdk::types::Address; use polymarket_client_sdk::{POLYGON, derive_proxy_wallet}; -use super::wallet::normalize_key; +use super::wallet::signer_key_hex; use crate::config; fn print_banner() { @@ -115,20 +114,15 @@ fn setup_wallet() -> Result
{ let (address, key_hex) = if has_key { let key = prompt(" Enter private key: ")?; - let normalized = normalize_key(&key); - let signer = LocalSigner::from_str(&normalized) + let signer = LocalSigner::from_str(&key) .context("Invalid private key")? .with_chain_id(Some(POLYGON)); - (signer.address(), normalized) + let hex = signer_key_hex(&signer); + (signer.address(), hex) } else { let signer = LocalSigner::random().with_chain_id(Some(POLYGON)); let address = signer.address(); - let bytes = signer.credential().to_bytes(); - let mut hex = String::with_capacity(2 + bytes.len() * 2); - hex.push_str("0x"); - for b in &bytes { - write!(hex, "{b:02x}").unwrap(); - } + let hex = signer_key_hex(&signer); (address, hex) }; diff --git a/src/commands/sports.rs b/src/commands/sports.rs index 8f46c72..2f7b646 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -75,7 +75,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .maybe_offset(offset) .maybe_order(order) .maybe_ascending(if ascending { Some(true) } else { None }) - .league(league.into_iter().collect::>()) + .league(league.into_iter().collect()) .build(); let teams = client.teams(&request).await?; diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index ce7597f..8e6e7ea 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use anyhow::{Context, Result, bail}; use clap::{Args, Subcommand}; +use alloy::signers::local::PrivateKeySigner; use polymarket_client_sdk::auth::LocalSigner; use polymarket_client_sdk::auth::Signer as _; use polymarket_client_sdk::{POLYGON, derive_proxy_wallet}; @@ -81,12 +82,15 @@ fn guard_overwrite(force: bool) -> Result<()> { Ok(()) } -pub(crate) fn normalize_key(key: &str) -> String { - if key.starts_with("0x") || key.starts_with("0X") { - key.to_string() - } else { - format!("0x{key}") +/// Extract the canonical 0x-prefixed hex private key from a signer. +pub(crate) fn signer_key_hex(signer: &PrivateKeySigner) -> String { + let bytes = signer.credential().to_bytes(); + let mut hex = String::with_capacity(2 + bytes.len() * 2); + hex.push_str("0x"); + for b in &bytes { + write!(hex, "{b:02x}").unwrap(); } + hex } fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Result<()> { @@ -94,12 +98,7 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul let signer = LocalSigner::random().with_chain_id(Some(POLYGON)); let address = signer.address(); - let bytes = signer.credential().to_bytes(); - let mut key_hex = String::with_capacity(2 + bytes.len() * 2); - key_hex.push_str("0x"); - for b in &bytes { - write!(key_hex, "{b:02x}").unwrap(); - } + let key_hex = signer_key_hex(&signer); config::save_wallet(&key_hex, POLYGON, signature_type)?; let config_path = config::config_path()?; @@ -136,13 +135,13 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &str) -> Result<()> { guard_overwrite(force)?; - let normalized = normalize_key(key); - let signer = LocalSigner::from_str(&normalized) + let signer = LocalSigner::from_str(key) .context("Invalid private key")? .with_chain_id(Some(POLYGON)); let address = signer.address(); + let key_hex = signer_key_hex(&signer); - config::save_wallet(&normalized, POLYGON, signature_type)?; + config::save_wallet(&key_hex, POLYGON, signature_type)?; let config_path = config::config_path()?; let proxy_addr = derive_proxy_wallet(address, POLYGON); @@ -278,27 +277,3 @@ fn cmd_reset(output: &OutputFormat, force: bool) -> Result<()> { Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize_key_adds_prefix() { - assert_eq!( - normalize_key("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"), - "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - ); - } - - #[test] - fn normalize_key_with_prefix_unchanged() { - let key = "0xabcdef"; - assert_eq!(normalize_key(key), key); - } - - #[test] - fn normalize_key_uppercase_prefix() { - let key = "0Xabcdef"; - assert_eq!(normalize_key(key), key); - } -} diff --git a/src/output/events.rs b/src/output/events.rs index 9167e9c..840b57d 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::Event; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{detail_field, format_decimal, print_detail_table, truncate}; +use super::{active_status, detail_field, format_decimal, print_detail_table, truncate}; #[derive(Tabled)] struct EventRow { @@ -18,16 +18,6 @@ struct EventRow { status: String, } -fn event_status(e: &Event) -> &'static str { - if e.closed == Some(true) { - "Closed" - } else if e.active == Some(true) { - "Active" - } else { - "Inactive" - } -} - fn event_to_row(e: &Event) -> EventRow { let title = e.title.as_deref().unwrap_or("—"); let market_count = e @@ -40,7 +30,7 @@ fn event_to_row(e: &Event) -> EventRow { market_count, volume: e.volume.map_or_else(|| "—".into(), format_decimal), liquidity: e.liquidity.map_or_else(|| "—".into(), format_decimal), - status: event_status(e).into(), + status: active_status(e.closed, e.active).into(), } } @@ -114,7 +104,7 @@ pub fn print_event_detail(e: &Event) { "Volume (1mo)", e.volume_1mo.map(format_decimal).unwrap_or_default() ); - detail_field!(rows, "Status", event_status(e).into()); + detail_field!(rows, "Status", active_status(e.closed, e.active).into()); detail_field!( rows, "Neg Risk", @@ -181,25 +171,25 @@ mod tests { #[test] fn status_closed_overrides_active() { let e = make_event(json!({"id": "1", "closed": true, "active": true})); - assert_eq!(event_status(&e), "Closed"); + assert_eq!(active_status(e.closed, e.active), "Closed"); } #[test] fn status_active_when_not_closed() { let e = make_event(json!({"id": "1", "closed": false, "active": true})); - assert_eq!(event_status(&e), "Active"); + assert_eq!(active_status(e.closed, e.active), "Active"); } #[test] fn status_inactive_when_fields_missing() { let e = make_event(json!({"id": "1"})); - assert_eq!(event_status(&e), "Inactive"); + assert_eq!(active_status(e.closed, e.active), "Inactive"); } #[test] fn status_inactive_when_both_false() { let e = make_event(json!({"id": "1", "closed": false, "active": false})); - assert_eq!(event_status(&e), "Inactive"); + assert_eq!(active_status(e.closed, e.active), "Inactive"); } #[test] diff --git a/src/output/markets.rs b/src/output/markets.rs index 1698a23..b8db712 100644 --- a/src/output/markets.rs +++ b/src/output/markets.rs @@ -3,7 +3,7 @@ use polymarket_client_sdk::types::Decimal; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{detail_field, format_decimal, print_detail_table, truncate}; +use super::{active_status, detail_field, format_decimal, print_detail_table, truncate}; #[derive(Tabled)] struct MarketRow { @@ -19,16 +19,6 @@ struct MarketRow { status: String, } -fn market_status(m: &Market) -> &'static str { - if m.closed == Some(true) { - "Closed" - } else if m.active == Some(true) { - "Active" - } else { - "Inactive" - } -} - fn market_to_row(m: &Market) -> MarketRow { let question = m.question.as_deref().unwrap_or("—"); let price_yes = m @@ -42,7 +32,7 @@ fn market_to_row(m: &Market) -> MarketRow { price_yes, volume: m.volume_num.map_or_else(|| "—".into(), format_decimal), liquidity: m.liquidity_num.map_or_else(|| "—".into(), format_decimal), - status: market_status(m).into(), + status: active_status(m.closed, m.active).into(), } } @@ -119,7 +109,7 @@ pub fn print_market_detail(m: &Market) { .map(|v| format!("{v:.4}")) .unwrap_or_default() ); - detail_field!(rows, "Status", market_status(m).into()); + detail_field!(rows, "Status", active_status(m.closed, m.active).into()); detail_field!( rows, "Condition ID", @@ -173,25 +163,25 @@ mod tests { #[test] fn status_closed_overrides_active() { let m = make_market(json!({"id": "1", "closed": true, "active": true})); - assert_eq!(market_status(&m), "Closed"); + assert_eq!(active_status(m.closed, m.active), "Closed"); } #[test] fn status_active_when_not_closed() { let m = make_market(json!({"id": "1", "closed": false, "active": true})); - assert_eq!(market_status(&m), "Active"); + assert_eq!(active_status(m.closed, m.active), "Active"); } #[test] fn status_inactive_when_fields_missing() { let m = make_market(json!({"id": "1"})); - assert_eq!(market_status(&m), "Inactive"); + assert_eq!(active_status(m.closed, m.active), "Inactive"); } #[test] fn status_inactive_when_both_false() { let m = make_market(json!({"id": "1", "closed": false, "active": false})); - assert_eq!(market_status(&m), "Inactive"); + assert_eq!(active_status(m.closed, m.active), "Inactive"); } #[test] diff --git a/src/output/mod.rs b/src/output/mod.rs index cc76acd..6e84a66 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -43,6 +43,16 @@ pub fn format_decimal(n: Decimal) -> String { } } +pub fn active_status(closed: Option, active: Option) -> &'static str { + if closed == Some(true) { + "Closed" + } else if active == Some(true) { + "Active" + } else { + "Inactive" + } +} + pub fn print_json(data: &impl serde::Serialize) -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(data)?); Ok(()) From a2dd7c18b8e879afb4e2025621842f154234024f Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 05:28:53 +0530 Subject: [PATCH 2/6] refactor: create SDK clients once per invocation instead of per-subcommand --- src/commands/clob.rs | 76 ++++++++----------------------------- src/main.rs | 90 ++++++++------------------------------------ 2 files changed, 31 insertions(+), 135 deletions(-) diff --git a/src/commands/clob.rs b/src/commands/clob.rs index b3defe6..6a526e6 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -557,15 +557,15 @@ pub async fn execute( } async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> { + let client = clob::Client::default(); + match command { ClobCommand::Ok => { - let client = clob::Client::default(); let result = client.ok().await?; print_ok(&result, output)?; } ClobCommand::Price { token_id, side } => { - let client = clob::Client::default(); let request = PriceRequest::builder() .token_id(parse_token_id(&token_id)?) .side(Side::from(side)) @@ -575,7 +575,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::BatchPrices { token_ids, side } => { - let client = clob::Client::default(); let requests: Vec<_> = parse_token_ids(&token_ids)? .into_iter() .map(|id| { @@ -590,7 +589,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Midpoint { token_id } => { - let client = clob::Client::default(); let request = MidpointRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); @@ -599,7 +597,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Midpoints { token_ids } => { - let client = clob::Client::default(); let requests: Vec<_> = parse_token_ids(&token_ids)? .into_iter() .map(|id| MidpointRequest::builder().token_id(id).build()) @@ -609,7 +606,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Spread { token_id, side } => { - let client = clob::Client::default(); let request = SpreadRequest::builder() .token_id(parse_token_id(&token_id)?) .maybe_side(side.map(Side::from)) @@ -619,7 +615,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Spreads { token_ids } => { - let client = clob::Client::default(); let requests: Vec<_> = parse_token_ids(&token_ids)? .into_iter() .map(|id| SpreadRequest::builder().token_id(id).build()) @@ -629,7 +624,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Book { token_id } => { - let client = clob::Client::default(); let request = OrderBookSummaryRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); @@ -638,7 +632,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Books { token_ids } => { - let client = clob::Client::default(); let requests: Vec<_> = parse_token_ids(&token_ids)? .into_iter() .map(|id| OrderBookSummaryRequest::builder().token_id(id).build()) @@ -648,7 +641,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::LastTrade { token_id } => { - let client = clob::Client::default(); let request = LastTradePriceRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); @@ -657,7 +649,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::LastTrades { token_ids } => { - let client = clob::Client::default(); let requests: Vec<_> = parse_token_ids(&token_ids)? .into_iter() .map(|id| LastTradePriceRequest::builder().token_id(id).build()) @@ -667,49 +658,41 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Market { condition_id } => { - let client = clob::Client::default(); let result = client.market(&condition_id).await?; print_clob_market(&result, output)?; } ClobCommand::Markets { cursor } => { - let client = clob::Client::default(); let result = client.markets(cursor).await?; print_clob_markets(&result, output)?; } ClobCommand::SamplingMarkets { cursor } => { - let client = clob::Client::default(); let result = client.sampling_markets(cursor).await?; print_clob_markets(&result, output)?; } ClobCommand::SimplifiedMarkets { cursor } => { - let client = clob::Client::default(); let result = client.simplified_markets(cursor).await?; print_simplified_markets(&result, output)?; } ClobCommand::SamplingSimpMarkets { cursor } => { - let client = clob::Client::default(); let result = client.sampling_simplified_markets(cursor).await?; print_simplified_markets(&result, output)?; } ClobCommand::TickSize { token_id } => { - let client = clob::Client::default(); let result = client.tick_size(parse_token_id(&token_id)?).await?; print_tick_size(&result, output)?; } ClobCommand::FeeRate { token_id } => { - let client = clob::Client::default(); let result = client.fee_rate_bps(parse_token_id(&token_id)?).await?; print_fee_rate(&result, output)?; } ClobCommand::NegRisk { token_id } => { - let client = clob::Client::default(); let result = client.neg_risk(parse_token_id(&token_id)?).await?; print_neg_risk(&result, output)?; } @@ -719,7 +702,6 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> interval, fidelity, } => { - let client = clob::Client::default(); let request = PriceHistoryRequest::builder() .market(parse_token_id(&token_id)?) .time_range(TimeRange::from_interval(Interval::from(interval))) @@ -730,13 +712,11 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> } ClobCommand::Time => { - let client = clob::Client::default(); let result = client.server_time().await?; print_server_time(result, output)?; } ClobCommand::Geoblock => { - let client = clob::Client::default(); let result = client.check_geoblock().await?; print_geoblock(&result, output)?; } @@ -753,13 +733,15 @@ async fn execute_trade( private_key: Option<&str>, signature_type: Option<&str>, ) -> Result<()> { + let signer = auth::resolve_signer(private_key)?; + let client = auth::authenticate_with_signer(&signer, signature_type).await?; + match command { ClobCommand::Orders { market, asset, cursor, } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = OrdersRequest::builder() .maybe_market(market) .maybe_asset_id(asset) @@ -769,7 +751,6 @@ async fn execute_trade( } ClobCommand::Order { order_id } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.order(&order_id).await?; print_order_detail(&result, output)?; } @@ -782,9 +763,6 @@ async fn execute_trade( order_type, post_only, } => { - let signer = auth::resolve_signer(private_key)?; - let client = auth::authenticate_with_signer(&signer, signature_type).await?; - let price_dec = Decimal::from_str(&price).map_err(|_| anyhow::anyhow!("Invalid price: {price}"))?; let size_dec = @@ -812,9 +790,6 @@ async fn execute_trade( sizes, order_type, } => { - let signer = auth::resolve_signer(private_key)?; - let client = auth::authenticate_with_signer(&signer, signature_type).await?; - let token_ids = parse_token_ids(&tokens)?; let price_strs: Vec<&str> = prices.split(',').map(str::trim).collect(); let size_strs: Vec<&str> = sizes.split(',').map(str::trim).collect(); @@ -859,9 +834,6 @@ async fn execute_trade( amount, order_type, } => { - let signer = auth::resolve_signer(private_key)?; - let client = auth::authenticate_with_signer(&signer, signature_type).await?; - let amount_dec = Decimal::from_str(&amount) .map_err(|_| anyhow::anyhow!("Invalid amount: {amount}"))?; let sdk_side = Side::from(side); @@ -885,26 +857,22 @@ async fn execute_trade( } ClobCommand::Cancel { order_id } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.cancel_order(&order_id).await?; print_cancel_result(&result, output)?; } ClobCommand::CancelOrders { order_ids } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let ids: Vec<&str> = order_ids.split(',').map(str::trim).collect(); let result = client.cancel_orders(&ids).await?; print_cancel_result(&result, output)?; } ClobCommand::CancelAll => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.cancel_all_orders().await?; print_cancel_result(&result, output)?; } ClobCommand::CancelMarket { market, asset } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = CancelMarketOrderRequest::builder() .maybe_market(market) .maybe_asset_id(asset) @@ -918,7 +886,6 @@ async fn execute_trade( asset, cursor, } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = TradesRequest::builder() .maybe_market(market) .maybe_asset_id(asset) @@ -929,7 +896,6 @@ async fn execute_trade( ClobCommand::Balance { asset_type, token } => { let is_collateral = matches!(asset_type, CliAssetType::Collateral); - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = BalanceAllowanceRequest::builder() .asset_type(AssetType::from(asset_type)) .maybe_token_id(token) @@ -939,7 +905,6 @@ async fn execute_trade( } ClobCommand::UpdateBalance { asset_type, token } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = BalanceAllowanceRequest::builder() .asset_type(AssetType::from(asset_type)) .maybe_token_id(token) @@ -954,13 +919,11 @@ async fn execute_trade( } ClobCommand::Notifications => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.notifications().await?; print_notifications(&result, output)?; } ClobCommand::DeleteNotifications { ids } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let notification_ids: Vec = ids.split(',').map(|s| s.trim().to_string()).collect(); let request = DeleteNotificationsRequest::builder() @@ -987,9 +950,10 @@ async fn execute_rewards( private_key: Option<&str>, signature_type: Option<&str>, ) -> Result<()> { + let client = auth::authenticated_clob_client(private_key, signature_type).await?; + match command { ClobCommand::Rewards { date, cursor } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client .earnings_for_user_for_day(parse_date(&date)?, cursor) .await?; @@ -997,7 +961,6 @@ async fn execute_rewards( } ClobCommand::Earnings { date } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client .total_earnings_for_user_for_day(parse_date(&date)?) .await?; @@ -1005,7 +968,6 @@ async fn execute_rewards( } ClobCommand::EarningsMarkets { date, cursor } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let request = UserRewardsEarningRequest::builder() .date(parse_date(&date)?) .build(); @@ -1016,13 +978,11 @@ async fn execute_rewards( } ClobCommand::RewardPercentages => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.reward_percentages().await?; print_reward_percentages(&result, output)?; } ClobCommand::CurrentRewards { cursor } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.current_rewards(cursor).await?; print_current_rewards(&result, output)?; } @@ -1031,19 +991,16 @@ async fn execute_rewards( condition_id, cursor, } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.raw_rewards_for_market(&condition_id, cursor).await?; print_market_reward(&result, output)?; } ClobCommand::OrderScoring { order_id } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.is_order_scoring(&order_id).await?; print_order_scoring(&result, output)?; } ClobCommand::OrdersScoring { order_ids } => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let ids: Vec<&str> = order_ids.split(',').map(str::trim).collect(); let result = client.are_orders_scoring(&ids).await?; print_orders_scoring(&result, output)?; @@ -1061,28 +1018,27 @@ async fn execute_account( private_key: Option<&str>, signature_type: Option<&str>, ) -> Result<()> { + if matches!(command, ClobCommand::CreateApiKey) { + let signer = auth::resolve_signer(private_key)?; + let client = clob::Client::default(); + let result = client.create_or_derive_api_key(&signer, None).await?; + return print_create_api_key(&result, output); + } + + let client = auth::authenticated_clob_client(private_key, signature_type).await?; + match command { ClobCommand::ApiKeys => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.api_keys().await?; print_api_keys(&result, output)?; } ClobCommand::DeleteApiKey => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.delete_api_key().await?; print_delete_api_key(&result, output)?; } - ClobCommand::CreateApiKey => { - let signer = auth::resolve_signer(private_key)?; - let client = clob::Client::default(); - let result = client.create_or_derive_api_key(&signer, None).await?; - print_create_api_key(&result, output)?; - } - ClobCommand::AccountStatus => { - let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.closed_only_mode().await?; print_account_status(&result, output)?; } diff --git a/src/main.rs b/src/main.rs index 61af087..f18d539 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,68 +88,24 @@ async fn main() -> ExitCode { #[allow(clippy::too_many_lines)] pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { + // Lazy-init so we only pay for the client we actually use. + let gamma = std::cell::LazyCell::new(polymarket_client_sdk::gamma::Client::default); + let data = std::cell::LazyCell::new(polymarket_client_sdk::data::Client::default); + let bridge = std::cell::LazyCell::new(polymarket_client_sdk::bridge::Client::default); + match cli.command { Commands::Setup => commands::setup::execute(), Commands::Shell => { Box::pin(shell::run_shell()).await; Ok(()) } - Commands::Markets(args) => { - commands::markets::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Events(args) => { - commands::events::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Tags(args) => { - commands::tags::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Series(args) => { - commands::series::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Comments(args) => { - commands::comments::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Profiles(args) => { - commands::profiles::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Sports(args) => { - commands::sports::execute( - &polymarket_client_sdk::gamma::Client::default(), - args, - cli.output, - ) - .await - } + Commands::Markets(args) => commands::markets::execute(&gamma, args, cli.output).await, + Commands::Events(args) => commands::events::execute(&gamma, args, cli.output).await, + Commands::Tags(args) => commands::tags::execute(&gamma, args, cli.output).await, + Commands::Series(args) => commands::series::execute(&gamma, args, cli.output).await, + Commands::Comments(args) => commands::comments::execute(&gamma, args, cli.output).await, + Commands::Profiles(args) => commands::profiles::execute(&gamma, args, cli.output).await, + Commands::Sports(args) => commands::sports::execute(&gamma, args, cli.output).await, Commands::Approve(args) => { commands::approve::execute(args, cli.output, cli.private_key.as_deref()).await } @@ -165,30 +121,14 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { Commands::Ctf(args) => { commands::ctf::execute(args, cli.output, cli.private_key.as_deref()).await } - Commands::Data(args) => { - commands::data::execute( - &polymarket_client_sdk::data::Client::default(), - args, - cli.output, - ) - .await - } - Commands::Bridge(args) => { - commands::bridge::execute( - &polymarket_client_sdk::bridge::Client::default(), - args, - cli.output, - ) - .await - } + Commands::Data(args) => commands::data::execute(&data, args, cli.output).await, + Commands::Bridge(args) => commands::bridge::execute(&bridge, args, cli.output).await, Commands::Wallet(args) => { commands::wallet::execute(args, &cli.output, cli.private_key.as_deref()) } Commands::Upgrade => commands::upgrade::execute(), Commands::Status => { - let status = polymarket_client_sdk::gamma::Client::default() - .status() - .await?; + let status = gamma.status().await?; match cli.output { OutputFormat::Json => { println!("{}", serde_json::json!({"status": status})); From fc1999afe39e692029fe7c37c003f1faf35c804d Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 05:54:17 +0530 Subject: [PATCH 3/6] refactor: harden clob dispatch, deduplicate output helpers, standardize formatting --- src/auth.rs | 10 +- src/commands/clob.rs | 192 ++++++++++++++++++++++++++++++++------- src/commands/comments.rs | 10 +- src/commands/data.rs | 20 +--- src/commands/mod.rs | 13 +++ src/main.rs | 9 +- src/output/comments.rs | 17 ++-- src/output/events.rs | 16 ++-- src/output/markets.rs | 14 +-- src/output/mod.rs | 20 ++++ src/output/series.rs | 28 ++---- src/output/tags.rs | 18 ++-- src/shell.rs | 13 +-- 13 files changed, 248 insertions(+), 132 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 15ad61e..23a4756 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -9,7 +9,11 @@ use polymarket_client_sdk::{POLYGON, clob}; use crate::config; -pub const RPC_URL: &str = "https://polygon.drpc.org"; +const DEFAULT_RPC_URL: &str = "https://polygon.drpc.org"; + +fn rpc_url() -> String { + std::env::var("POLYMARKET_RPC_URL").unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()) +} fn parse_signature_type(s: &str) -> SignatureType { match s { @@ -53,7 +57,7 @@ pub async fn authenticate_with_signer( pub async fn create_readonly_provider() -> Result { ProviderBuilder::new() - .connect(RPC_URL) + .connect(&rpc_url()) .await .context("Failed to connect to Polygon RPC") } @@ -68,7 +72,7 @@ pub async fn create_provider( .with_chain_id(Some(POLYGON)); ProviderBuilder::new() .wallet(signer) - .connect(RPC_URL) + .connect(&rpc_url()) .await .context("Failed to connect to Polygon RPC with wallet") } diff --git a/src/commands/clob.rs b/src/commands/clob.rs index 6a526e6..db707a2 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -397,14 +397,7 @@ pub enum CliSide { Sell, } -impl From for Side { - fn from(s: CliSide) -> Self { - match s { - CliSide::Buy => Side::Buy, - CliSide::Sell => Side::Sell, - } - } -} +super::enum_from!(CliSide => Side { Buy, Sell }); #[derive(Clone, Debug, clap::ValueEnum)] pub enum CliInterval { @@ -421,18 +414,7 @@ pub enum CliInterval { Max, } -impl From for Interval { - fn from(i: CliInterval) -> Self { - match i { - CliInterval::OneMinute => Interval::OneMinute, - CliInterval::OneHour => Interval::OneHour, - CliInterval::SixHours => Interval::SixHours, - CliInterval::OneDay => Interval::OneDay, - CliInterval::OneWeek => Interval::OneWeek, - CliInterval::Max => Interval::Max, - } - } -} +super::enum_from!(CliInterval => Interval { OneMinute, OneHour, SixHours, OneDay, OneWeek, Max }); #[derive(Clone, Debug, clap::ValueEnum)] pub enum CliOrderType { @@ -463,14 +445,7 @@ pub enum CliAssetType { Conditional, } -impl From for AssetType { - fn from(a: CliAssetType) -> Self { - match a { - CliAssetType::Collateral => AssetType::Collateral, - CliAssetType::Conditional => AssetType::Conditional, - } - } -} +super::enum_from!(CliAssetType => AssetType { Collateral, Conditional }); fn parse_token_id(s: &str) -> Result { U256::from_str(s).map_err(|_| anyhow::anyhow!("Invalid token ID: {s}")) @@ -721,7 +696,35 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> print_geoblock(&result, output)?; } - _ => unreachable!(), + // Trade, reward, and account commands are dispatched by execute() above. + ClobCommand::Orders { .. } + | ClobCommand::Order { .. } + | ClobCommand::CreateOrder { .. } + | ClobCommand::PostOrders { .. } + | ClobCommand::MarketOrder { .. } + | ClobCommand::Cancel { .. } + | ClobCommand::CancelOrders { .. } + | ClobCommand::CancelAll + | ClobCommand::CancelMarket { .. } + | ClobCommand::Trades { .. } + | ClobCommand::Balance { .. } + | ClobCommand::UpdateBalance { .. } + | ClobCommand::Notifications + | ClobCommand::DeleteNotifications { .. } + | ClobCommand::Rewards { .. } + | ClobCommand::Earnings { .. } + | ClobCommand::EarningsMarkets { .. } + | ClobCommand::RewardPercentages + | ClobCommand::CurrentRewards { .. } + | ClobCommand::MarketReward { .. } + | ClobCommand::OrderScoring { .. } + | ClobCommand::OrdersScoring { .. } + | ClobCommand::ApiKeys + | ClobCommand::DeleteApiKey + | ClobCommand::CreateApiKey + | ClobCommand::AccountStatus => { + unreachable!("execute() routes authenticated commands to other handlers") + } } Ok(()) @@ -938,7 +941,43 @@ async fn execute_trade( } } - _ => unreachable!(), + // Read, reward, and account commands are dispatched by execute() above. + ClobCommand::Ok + | ClobCommand::Price { .. } + | ClobCommand::BatchPrices { .. } + | ClobCommand::Midpoint { .. } + | ClobCommand::Midpoints { .. } + | ClobCommand::Spread { .. } + | ClobCommand::Spreads { .. } + | ClobCommand::Book { .. } + | ClobCommand::Books { .. } + | ClobCommand::LastTrade { .. } + | ClobCommand::LastTrades { .. } + | ClobCommand::Market { .. } + | ClobCommand::Markets { .. } + | ClobCommand::SamplingMarkets { .. } + | ClobCommand::SimplifiedMarkets { .. } + | ClobCommand::SamplingSimpMarkets { .. } + | ClobCommand::TickSize { .. } + | ClobCommand::FeeRate { .. } + | ClobCommand::NegRisk { .. } + | ClobCommand::PriceHistory { .. } + | ClobCommand::Time + | ClobCommand::Geoblock + | ClobCommand::Rewards { .. } + | ClobCommand::Earnings { .. } + | ClobCommand::EarningsMarkets { .. } + | ClobCommand::RewardPercentages + | ClobCommand::CurrentRewards { .. } + | ClobCommand::MarketReward { .. } + | ClobCommand::OrderScoring { .. } + | ClobCommand::OrdersScoring { .. } + | ClobCommand::ApiKeys + | ClobCommand::DeleteApiKey + | ClobCommand::CreateApiKey + | ClobCommand::AccountStatus => { + unreachable!("execute() routes non-trade commands to other handlers") + } } Ok(()) @@ -1006,7 +1045,49 @@ async fn execute_rewards( print_orders_scoring(&result, output)?; } - _ => unreachable!(), + // Read, trade, and account commands are dispatched by execute() above. + ClobCommand::Ok + | ClobCommand::Price { .. } + | ClobCommand::BatchPrices { .. } + | ClobCommand::Midpoint { .. } + | ClobCommand::Midpoints { .. } + | ClobCommand::Spread { .. } + | ClobCommand::Spreads { .. } + | ClobCommand::Book { .. } + | ClobCommand::Books { .. } + | ClobCommand::LastTrade { .. } + | ClobCommand::LastTrades { .. } + | ClobCommand::Market { .. } + | ClobCommand::Markets { .. } + | ClobCommand::SamplingMarkets { .. } + | ClobCommand::SimplifiedMarkets { .. } + | ClobCommand::SamplingSimpMarkets { .. } + | ClobCommand::TickSize { .. } + | ClobCommand::FeeRate { .. } + | ClobCommand::NegRisk { .. } + | ClobCommand::PriceHistory { .. } + | ClobCommand::Time + | ClobCommand::Geoblock + | ClobCommand::Orders { .. } + | ClobCommand::Order { .. } + | ClobCommand::CreateOrder { .. } + | ClobCommand::PostOrders { .. } + | ClobCommand::MarketOrder { .. } + | ClobCommand::Cancel { .. } + | ClobCommand::CancelOrders { .. } + | ClobCommand::CancelAll + | ClobCommand::CancelMarket { .. } + | ClobCommand::Trades { .. } + | ClobCommand::Balance { .. } + | ClobCommand::UpdateBalance { .. } + | ClobCommand::Notifications + | ClobCommand::DeleteNotifications { .. } + | ClobCommand::ApiKeys + | ClobCommand::DeleteApiKey + | ClobCommand::CreateApiKey + | ClobCommand::AccountStatus => { + unreachable!("execute() routes non-reward commands to other handlers") + } } Ok(()) @@ -1043,7 +1124,54 @@ async fn execute_account( print_account_status(&result, output)?; } - _ => unreachable!(), + // Read, trade, and reward commands are dispatched by execute() above. + ClobCommand::Ok + | ClobCommand::Price { .. } + | ClobCommand::BatchPrices { .. } + | ClobCommand::Midpoint { .. } + | ClobCommand::Midpoints { .. } + | ClobCommand::Spread { .. } + | ClobCommand::Spreads { .. } + | ClobCommand::Book { .. } + | ClobCommand::Books { .. } + | ClobCommand::LastTrade { .. } + | ClobCommand::LastTrades { .. } + | ClobCommand::Market { .. } + | ClobCommand::Markets { .. } + | ClobCommand::SamplingMarkets { .. } + | ClobCommand::SimplifiedMarkets { .. } + | ClobCommand::SamplingSimpMarkets { .. } + | ClobCommand::TickSize { .. } + | ClobCommand::FeeRate { .. } + | ClobCommand::NegRisk { .. } + | ClobCommand::PriceHistory { .. } + | ClobCommand::Time + | ClobCommand::Geoblock + | ClobCommand::Orders { .. } + | ClobCommand::Order { .. } + | ClobCommand::CreateOrder { .. } + | ClobCommand::PostOrders { .. } + | ClobCommand::MarketOrder { .. } + | ClobCommand::Cancel { .. } + | ClobCommand::CancelOrders { .. } + | ClobCommand::CancelAll + | ClobCommand::CancelMarket { .. } + | ClobCommand::Trades { .. } + | ClobCommand::Balance { .. } + | ClobCommand::UpdateBalance { .. } + | ClobCommand::Notifications + | ClobCommand::DeleteNotifications { .. } + | ClobCommand::Rewards { .. } + | ClobCommand::Earnings { .. } + | ClobCommand::EarningsMarkets { .. } + | ClobCommand::RewardPercentages + | ClobCommand::CurrentRewards { .. } + | ClobCommand::MarketReward { .. } + | ClobCommand::OrderScoring { .. } + | ClobCommand::OrdersScoring { .. } + | ClobCommand::CreateApiKey => { + unreachable!("execute() routes non-account commands to other handlers") + } } Ok(()) diff --git a/src/commands/comments.rs b/src/commands/comments.rs index be90ba2..28347ee 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -81,15 +81,7 @@ pub enum EntityType { Series, } -impl From for ParentEntityType { - fn from(e: EntityType) -> Self { - match e { - EntityType::Event => ParentEntityType::Event, - EntityType::Market => ParentEntityType::Market, - EntityType::Series => ParentEntityType::Series, - } - } -} +super::enum_from!(EntityType => ParentEntityType { Event, Market, Series }); pub async fn execute( client: &gamma::Client, diff --git a/src/commands/data.rs b/src/commands/data.rs index 0ec0bd2..b8cd6c2 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -164,16 +164,7 @@ pub enum TimePeriod { All, } -impl From for polymarket_client_sdk::data::types::TimePeriod { - fn from(t: TimePeriod) -> Self { - match t { - TimePeriod::Day => Self::Day, - TimePeriod::Week => Self::Week, - TimePeriod::Month => Self::Month, - TimePeriod::All => Self::All, - } - } -} +super::enum_from!(TimePeriod => polymarket_client_sdk::data::types::TimePeriod { Day, Week, Month, All }); #[derive(Clone, Debug, clap::ValueEnum)] pub enum OrderBy { @@ -181,14 +172,7 @@ pub enum OrderBy { Vol, } -impl From for polymarket_client_sdk::data::types::LeaderboardOrderBy { - fn from(o: OrderBy) -> Self { - match o { - OrderBy::Pnl => Self::Pnl, - OrderBy::Vol => Self::Vol, - } - } -} +super::enum_from!(OrderBy => polymarket_client_sdk::data::types::LeaderboardOrderBy { Pnl, Vol }); pub async fn execute(client: &data::Client, args: DataArgs, output: OutputFormat) -> Result<()> { match args.command { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1e8671e..c4ee0ac 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -17,6 +17,19 @@ pub mod tags; pub mod upgrade; pub mod wallet; +/// Implement `From` for an SDK enum when variant names match 1:1. +macro_rules! enum_from { + ($from:ty => $to:ty { $($variant:ident),+ $(,)? }) => { + impl From<$from> for $to { + fn from(v: $from) -> Self { + match v { $( <$from>::$variant => <$to>::$variant, )+ } + } + } + }; +} + +pub(crate) use enum_from; + pub fn is_numeric_id(id: &str) -> bool { id.parse::().is_ok() } diff --git a/src/main.rs b/src/main.rs index f18d539..db7ea38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,14 +72,7 @@ async fn main() -> ExitCode { let output = cli.output; if let Err(e) = run(cli).await { - match output { - OutputFormat::Json => { - println!("{}", serde_json::json!({"error": e.to_string()})); - } - OutputFormat::Table => { - eprintln!("Error: {e}"); - } - } + output::print_error(&e, output); return ExitCode::FAILURE; } diff --git a/src/output/comments.rs b/src/output/comments.rs index a71572c..85a5188 100644 --- a/src/output/comments.rs +++ b/src/output/comments.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::Comment; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{detail_field, print_detail_table, truncate}; +use super::{NONE, detail_field, format_date, print_detail_table, truncate}; #[derive(Tabled)] struct CommentRow { @@ -24,20 +24,21 @@ fn comment_author(c: &Comment) -> String { .and_then(|p| p.name.as_deref().or(p.pseudonym.as_deref())) .map(String::from) .or_else(|| c.user_address.map(|a| truncate(&format!("{a}"), 10))) - .unwrap_or_else(|| "—".into()) + .unwrap_or_else(|| NONE.into()) } fn comment_to_row(c: &Comment) -> CommentRow { CommentRow { id: truncate(&c.id, 12), author: comment_author(c), - body: truncate(c.body.as_deref().unwrap_or("—"), 60), + body: truncate(c.body.as_deref().unwrap_or(NONE), 60), reactions: c .reaction_count - .map_or_else(|| "—".into(), |n| n.to_string()), + .map_or_else(|| NONE.into(), |n| n.to_string()), created: c .created_at - .map_or_else(|| "—".into(), |d| d.format("%Y-%m-%d %H:%M").to_string()), + .as_ref() + .map_or_else(|| NONE.into(), format_date), } } @@ -91,7 +92,7 @@ pub fn print_comment_detail(c: &Comment) { rows, "Reactions", c.reaction_count - .map_or_else(|| "—".into(), |n| n.to_string()) + .map_or_else(|| NONE.into(), |n| n.to_string()) ); detail_field!( rows, @@ -101,12 +102,12 @@ pub fn print_comment_detail(c: &Comment) { detail_field!( rows, "Created At", - c.created_at.map(|d| d.to_string()).unwrap_or_default() + c.created_at.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "Updated At", - c.updated_at.map(|d| d.to_string()).unwrap_or_default() + c.updated_at.as_ref().map(format_date).unwrap_or_default() ); print_detail_table(rows); diff --git a/src/output/events.rs b/src/output/events.rs index 840b57d..0ca79a5 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::Event; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{active_status, detail_field, format_decimal, print_detail_table, truncate}; +use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; #[derive(Tabled)] struct EventRow { @@ -19,17 +19,17 @@ struct EventRow { } fn event_to_row(e: &Event) -> EventRow { - let title = e.title.as_deref().unwrap_or("—"); + let title = e.title.as_deref().unwrap_or(NONE); let market_count = e .markets .as_ref() - .map_or_else(|| "—".into(), |m| m.len().to_string()); + .map_or_else(|| NONE.into(), |m| m.len().to_string()); EventRow { title: truncate(title, 60), market_count, - volume: e.volume.map_or_else(|| "—".into(), format_decimal), - liquidity: e.liquidity.map_or_else(|| "—".into(), format_decimal), + volume: e.volume.map_or_else(|| NONE.into(), format_decimal), + liquidity: e.liquidity.map_or_else(|| NONE.into(), format_decimal), status: active_status(e.closed, e.active).into(), } } @@ -125,17 +125,17 @@ pub fn print_event_detail(e: &Event) { detail_field!( rows, "Start Date", - e.start_date.map(|d| d.to_string()).unwrap_or_default() + e.start_date.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "End Date", - e.end_date.map(|d| d.to_string()).unwrap_or_default() + e.end_date.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "Created At", - e.created_at.map(|d| d.to_string()).unwrap_or_default() + e.created_at.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, diff --git a/src/output/markets.rs b/src/output/markets.rs index b8db712..a26690a 100644 --- a/src/output/markets.rs +++ b/src/output/markets.rs @@ -3,7 +3,7 @@ use polymarket_client_sdk::types::Decimal; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{active_status, detail_field, format_decimal, print_detail_table, truncate}; +use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; #[derive(Tabled)] struct MarketRow { @@ -20,18 +20,18 @@ struct MarketRow { } fn market_to_row(m: &Market) -> MarketRow { - let question = m.question.as_deref().unwrap_or("—"); + let question = m.question.as_deref().unwrap_or(NONE); let price_yes = m .outcome_prices .as_ref() .and_then(|p| p.first()) - .map_or_else(|| "—".into(), |p| format!("{:.2}¢", p * Decimal::from(100))); + .map_or_else(|| NONE.into(), |p| format!("{:.2}¢", p * Decimal::from(100))); MarketRow { question: truncate(question, 60), price_yes, - volume: m.volume_num.map_or_else(|| "—".into(), format_decimal), - liquidity: m.liquidity_num.map_or_else(|| "—".into(), format_decimal), + volume: m.volume_num.map_or_else(|| NONE.into(), format_decimal), + liquidity: m.liquidity_num.map_or_else(|| NONE.into(), format_decimal), status: active_status(m.closed, m.active).into(), } } @@ -130,12 +130,12 @@ pub fn print_market_detail(m: &Market) { detail_field!( rows, "Start Date", - m.start_date.map(|d| d.to_string()).unwrap_or_default() + m.start_date.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "End Date", - m.end_date.map(|d| d.to_string()).unwrap_or_default() + m.end_date.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, diff --git a/src/output/mod.rs b/src/output/mod.rs index 6e84a66..c6c135d 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -11,12 +11,16 @@ pub mod series; pub mod sports; pub mod tags; +use chrono::{DateTime, Utc}; use polymarket_client_sdk::types::Decimal; use rust_decimal::prelude::ToPrimitive; use tabled::Table; use tabled::settings::object::Columns; use tabled::settings::{Modify, Style, Width}; +/// Display string for missing/null values in table output. +pub const NONE: &str = "—"; + #[derive(Clone, Copy, Debug, clap::ValueEnum)] pub enum OutputFormat { Table, @@ -43,6 +47,10 @@ pub fn format_decimal(n: Decimal) -> String { } } +pub fn format_date(d: &DateTime) -> String { + d.format("%Y-%m-%d %H:%M UTC").to_string() +} + pub fn active_status(closed: Option, active: Option) -> &'static str { if closed == Some(true) { "Closed" @@ -58,6 +66,18 @@ pub fn print_json(data: &impl serde::Serialize) -> anyhow::Result<()> { Ok(()) } +/// Print an error in the appropriate format for the current output mode. +pub fn print_error(error: &anyhow::Error, format: OutputFormat) { + match format { + OutputFormat::Json => { + println!("{}", serde_json::json!({"error": error.to_string()})); + } + OutputFormat::Table => { + eprintln!("Error: {error}"); + } + } +} + pub fn print_detail_table(rows: Vec<[String; 2]>) { let table = Table::from_iter(rows) .with(Style::rounded()) diff --git a/src/output/series.rs b/src/output/series.rs index 8c15095..a1d1d39 100644 --- a/src/output/series.rs +++ b/src/output/series.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::Series; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{detail_field, format_decimal, print_detail_table, truncate}; +use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; #[derive(Tabled)] struct SeriesRow { @@ -18,23 +18,13 @@ struct SeriesRow { status: String, } -fn series_status(s: &Series) -> &'static str { - if s.closed == Some(true) { - "Closed" - } else if s.active == Some(true) { - "Active" - } else { - "Inactive" - } -} - fn series_to_row(s: &Series) -> SeriesRow { SeriesRow { - title: truncate(s.title.as_deref().unwrap_or("—"), 50), - series_type: s.series_type.as_deref().unwrap_or("—").into(), - volume: s.volume.map_or_else(|| "—".into(), format_decimal), - liquidity: s.liquidity.map_or_else(|| "—".into(), format_decimal), - status: series_status(s).into(), + title: truncate(s.title.as_deref().unwrap_or(NONE), 50), + series_type: s.series_type.as_deref().unwrap_or(NONE).into(), + volume: s.volume.map_or_else(|| NONE.into(), format_decimal), + liquidity: s.liquidity.map_or_else(|| NONE.into(), format_decimal), + status: active_status(s.closed, s.active).into(), } } @@ -76,7 +66,7 @@ pub fn print_series_detail(s: &Series) { "Volume (24hr)", s.volume_24hr.map(format_decimal).unwrap_or_default() ); - detail_field!(rows, "Status", series_status(s).into()); + detail_field!(rows, "Status", active_status(s.closed, s.active).into()); detail_field!( rows, "Events", @@ -93,12 +83,12 @@ pub fn print_series_detail(s: &Series) { detail_field!( rows, "Start Date", - s.start_date.map(|d| d.to_string()).unwrap_or_default() + s.start_date.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "Created At", - s.created_at.map(|d| d.to_string()).unwrap_or_default() + s.created_at.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, diff --git a/src/output/tags.rs b/src/output/tags.rs index d5e895f..40d7b77 100644 --- a/src/output/tags.rs +++ b/src/output/tags.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::{RelatedTag, Tag}; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{detail_field, print_detail_table, truncate}; +use super::{NONE, detail_field, format_date, print_detail_table, truncate}; #[derive(Tabled)] struct TagRow { @@ -19,9 +19,9 @@ struct TagRow { fn tag_to_row(t: &Tag) -> TagRow { TagRow { id: truncate(&t.id, 20), - label: t.label.as_deref().unwrap_or("—").into(), - slug: t.slug.as_deref().unwrap_or("—").into(), - carousel: t.is_carousel.map_or_else(|| "—".into(), |v| v.to_string()), + label: t.label.as_deref().unwrap_or(NONE).into(), + slug: t.slug.as_deref().unwrap_or(NONE).into(), + carousel: t.is_carousel.map_or_else(|| NONE.into(), |v| v.to_string()), } } @@ -50,9 +50,9 @@ struct RelatedTagRow { fn related_tag_to_row(r: &RelatedTag) -> RelatedTagRow { RelatedTagRow { id: truncate(&r.id, 20), - tag_id: r.tag_id.as_deref().unwrap_or("—").into(), - related_tag_id: r.related_tag_id.as_deref().unwrap_or("—").into(), - rank: r.rank.map_or_else(|| "—".into(), |v| v.to_string()), + tag_id: r.tag_id.as_deref().unwrap_or(NONE).into(), + related_tag_id: r.related_tag_id.as_deref().unwrap_or(NONE).into(), + rank: r.rank.map_or_else(|| NONE.into(), |v| v.to_string()), } } @@ -91,12 +91,12 @@ pub fn print_tag_detail(t: &Tag) { detail_field!( rows, "Created At", - t.created_at.map(|d| d.to_string()).unwrap_or_default() + t.created_at.as_ref().map(format_date).unwrap_or_default() ); detail_field!( rows, "Updated At", - t.updated_at.map(|d| d.to_string()).unwrap_or_default() + t.updated_at.as_ref().map(format_date).unwrap_or_default() ); print_detail_table(rows); diff --git a/src/shell.rs b/src/shell.rs index b0db00d..65c2384 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,6 +1,4 @@ -use clap::Parser; - -use crate::output::OutputFormat; +use clap::Parser as _; pub async fn run_shell() { println!(); @@ -48,14 +46,7 @@ pub async fn run_shell() { Ok(cli) => { let output = cli.output; if let Err(e) = crate::run(cli).await { - match output { - OutputFormat::Json => { - println!("{}", serde_json::json!({"error": e.to_string()})); - } - OutputFormat::Table => { - eprintln!("Error: {e}"); - } - } + crate::output::print_error(&e, output); } } Err(e) => { From 2acd0704eb9e54595df7c40f6ba8193556a4472e Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 05:54:33 +0530 Subject: [PATCH 4/6] run formatter --- src/commands/bridge.rs | 4 +--- src/commands/clob.rs | 26 +++++++++++++------------- src/commands/data.rs | 10 +++------- src/commands/wallet.rs | 3 +-- src/output/events.rs | 4 +++- src/output/markets.rs | 9 +++++++-- src/output/series.rs | 4 +++- 7 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/commands/bridge.rs b/src/commands/bridge.rs index a6cb773..20ab200 100644 --- a/src/commands/bridge.rs +++ b/src/commands/bridge.rs @@ -38,9 +38,7 @@ pub async fn execute( ) -> Result<()> { match args.command { BridgeCommand::Deposit { address } => { - let request = DepositRequest::builder() - .address(address) - .build(); + let request = DepositRequest::builder().address(address).build(); let response = client.deposit(&request).await?; print_deposit(&response, &output)?; diff --git a/src/commands/clob.rs b/src/commands/clob.rs index db707a2..1bfba6f 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -1,18 +1,5 @@ use std::str::FromStr; -use anyhow::Result; -use chrono::NaiveDate; -use clap::{Args, Subcommand}; -use polymarket_client_sdk::clob; -use polymarket_client_sdk::clob::types::{ - Amount, AssetType, Interval, OrderType, Side, TimeRange, - request::{ - BalanceAllowanceRequest, CancelMarketOrderRequest, DeleteNotificationsRequest, - LastTradePriceRequest, MidpointRequest, OrderBookSummaryRequest, OrdersRequest, - PriceHistoryRequest, PriceRequest, SpreadRequest, TradesRequest, UserRewardsEarningRequest, - }, -}; -use polymarket_client_sdk::types::{B256, Decimal, U256}; use crate::auth; use crate::output::OutputFormat; use crate::output::clob::{ @@ -26,6 +13,19 @@ use crate::output::clob::{ print_rewards, print_server_time, print_simplified_markets, print_spread, print_spreads, print_tick_size, print_trades, print_user_earnings_markets, }; +use anyhow::Result; +use chrono::NaiveDate; +use clap::{Args, Subcommand}; +use polymarket_client_sdk::clob; +use polymarket_client_sdk::clob::types::{ + Amount, AssetType, Interval, OrderType, Side, TimeRange, + request::{ + BalanceAllowanceRequest, CancelMarketOrderRequest, DeleteNotificationsRequest, + LastTradePriceRequest, MidpointRequest, OrderBookSummaryRequest, OrdersRequest, + PriceHistoryRequest, PriceRequest, SpreadRequest, TradesRequest, UserRewardsEarningRequest, + }, +}; +use polymarket_client_sdk::types::{B256, Decimal, U256}; #[derive(Args)] pub struct ClobArgs { diff --git a/src/commands/data.rs b/src/commands/data.rs index b8cd6c2..0bf22a9 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -1,4 +1,3 @@ -use polymarket_client_sdk::types::{Address, B256}; use crate::output::OutputFormat; use crate::output::data::{ print_activity, print_builder_leaderboard, print_builder_volume, print_closed_positions, @@ -15,6 +14,7 @@ use polymarket_client_sdk::data::{ TraderLeaderboardRequest, TradesRequest, ValueRequest, }, }; +use polymarket_client_sdk::types::{Address, B256}; #[derive(Args)] pub struct DataArgs { @@ -235,18 +235,14 @@ async fn execute_user( } DataCommand::Value { address } => { - let request = ValueRequest::builder() - .user(address) - .build(); + let request = ValueRequest::builder().user(address).build(); let values = client.value(&request).await?; print_value(&values, output)?; } DataCommand::Traded { address } => { - let request = TradedRequest::builder() - .user(address) - .build(); + let request = TradedRequest::builder().user(address).build(); let traded = client.traded(&request).await?; print_traded(&traded, output)?; diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index 8e6e7ea..a28cc1c 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -1,9 +1,9 @@ use std::fmt::Write as _; use std::str::FromStr; +use alloy::signers::local::PrivateKeySigner; use anyhow::{Context, Result, bail}; use clap::{Args, Subcommand}; -use alloy::signers::local::PrivateKeySigner; use polymarket_client_sdk::auth::LocalSigner; use polymarket_client_sdk::auth::Signer as _; use polymarket_client_sdk::{POLYGON, derive_proxy_wallet}; @@ -276,4 +276,3 @@ fn cmd_reset(output: &OutputFormat, force: bool) -> Result<()> { } Ok(()) } - diff --git a/src/output/events.rs b/src/output/events.rs index 0ca79a5..387d081 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -2,7 +2,9 @@ use polymarket_client_sdk::gamma::types::response::Event; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; +use super::{ + NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, +}; #[derive(Tabled)] struct EventRow { diff --git a/src/output/markets.rs b/src/output/markets.rs index a26690a..7324310 100644 --- a/src/output/markets.rs +++ b/src/output/markets.rs @@ -3,7 +3,9 @@ use polymarket_client_sdk::types::Decimal; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; +use super::{ + NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, +}; #[derive(Tabled)] struct MarketRow { @@ -25,7 +27,10 @@ fn market_to_row(m: &Market) -> MarketRow { .outcome_prices .as_ref() .and_then(|p| p.first()) - .map_or_else(|| NONE.into(), |p| format!("{:.2}¢", p * Decimal::from(100))); + .map_or_else( + || NONE.into(), + |p| format!("{:.2}¢", p * Decimal::from(100)), + ); MarketRow { question: truncate(question, 60), diff --git a/src/output/series.rs b/src/output/series.rs index a1d1d39..3d2a5ac 100644 --- a/src/output/series.rs +++ b/src/output/series.rs @@ -2,7 +2,9 @@ use polymarket_client_sdk::gamma::types::response::Series; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate}; +use super::{ + NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, +}; #[derive(Tabled)] struct SeriesRow { From 7431398b6a8de40bc90922ab236e0e132b35a5a5 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 06:08:18 +0530 Subject: [PATCH 5/6] refactor: extract ascending helper, tighten visibility, exhaust data.rs matches --- src/commands/comments.rs | 6 ++++-- src/commands/data.rs | 33 ++++++++++++++++++++++++++++++--- src/commands/events.rs | 4 ++-- src/commands/markets.rs | 4 ++-- src/commands/mod.rs | 4 ++++ src/commands/series.rs | 3 ++- src/commands/sports.rs | 3 ++- src/commands/tags.rs | 4 ++-- src/main.rs | 5 +---- src/output/mod.rs | 14 +++++++------- src/shell.rs | 11 +++-------- 11 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/commands/comments.rs b/src/commands/comments.rs index 28347ee..30b5439 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -81,6 +81,8 @@ pub enum EntityType { Series, } +use super::ascending_flag; + super::enum_from!(EntityType => ParentEntityType { Event, Market, Series }); pub async fn execute( @@ -103,7 +105,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .build(); let comments = client.comments(&request).await?; @@ -140,7 +142,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .build(); let comments = client.comments_by_user_address(&request).await?; diff --git a/src/commands/data.rs b/src/commands/data.rs index 0bf22a9..eb51f3a 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -278,7 +278,14 @@ async fn execute_user( print_activity(&activity, output)?; } - _ => unreachable!(), + DataCommand::Holders { .. } + | DataCommand::OpenInterest { .. } + | DataCommand::Volume { .. } + | DataCommand::Leaderboard { .. } + | DataCommand::BuilderLeaderboard { .. } + | DataCommand::BuilderVolume { .. } => { + unreachable!("execute() routes market/leaderboard commands to other handlers") + } } Ok(()) @@ -313,7 +320,17 @@ async fn execute_market( print_live_volume(&volume, output)?; } - _ => unreachable!(), + DataCommand::Positions { .. } + | DataCommand::ClosedPositions { .. } + | DataCommand::Value { .. } + | DataCommand::Traded { .. } + | DataCommand::Trades { .. } + | DataCommand::Activity { .. } + | DataCommand::Leaderboard { .. } + | DataCommand::BuilderLeaderboard { .. } + | DataCommand::BuilderVolume { .. } => { + unreachable!("execute() routes user/leaderboard commands to other handlers") + } } Ok(()) @@ -366,7 +383,17 @@ async fn execute_leaderboard( print_builder_volume(&entries, output)?; } - _ => unreachable!(), + DataCommand::Positions { .. } + | DataCommand::ClosedPositions { .. } + | DataCommand::Value { .. } + | DataCommand::Traded { .. } + | DataCommand::Trades { .. } + | DataCommand::Activity { .. } + | DataCommand::Holders { .. } + | DataCommand::OpenInterest { .. } + | DataCommand::Volume { .. } => { + unreachable!("execute() routes user/market commands to other handlers") + } } Ok(()) diff --git a/src/commands/events.rs b/src/commands/events.rs index cbe6b7b..71ff0d4 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -5,7 +5,7 @@ use polymarket_client_sdk::gamma::{ types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest}, }; -use super::is_numeric_id; +use super::{ascending_flag, is_numeric_id}; use crate::output::events::{print_event_detail, print_events_table}; use crate::output::tags::print_tags_table; use crate::output::{OutputFormat, print_json}; @@ -79,7 +79,7 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .limit(limit) .maybe_closed(resolved_closed) .maybe_offset(offset) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .maybe_tag_slug(tag) .order(order.into_iter().collect()) .build(); diff --git a/src/commands/markets.rs b/src/commands/markets.rs index 68e5491..cd0975b 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -11,7 +11,7 @@ use polymarket_client_sdk::gamma::{ }, }; -use super::is_numeric_id; +use super::{ascending_flag, is_numeric_id}; use crate::output::markets::{print_market_detail, print_markets_table}; use crate::output::tags::print_tags_table; use crate::output::{OutputFormat, print_json}; @@ -95,7 +95,7 @@ pub async fn execute( .maybe_closed(resolved_closed) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .build(); let markets = client.markets(&request).await?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c4ee0ac..a853f5c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -30,6 +30,10 @@ macro_rules! enum_from { pub(crate) use enum_from; +pub fn ascending_flag(ascending: bool) -> Option { + ascending.then_some(true) +} + pub fn is_numeric_id(id: &str) -> bool { id.parse::().is_ok() } diff --git a/src/commands/series.rs b/src/commands/series.rs index 11dba91..6c6c99f 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -5,6 +5,7 @@ use polymarket_client_sdk::gamma::{ types::request::{SeriesByIdRequest, SeriesListRequest}, }; +use super::ascending_flag; use crate::output::series::{print_series_detail, print_series_table}; use crate::output::{OutputFormat, print_json}; @@ -59,7 +60,7 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .maybe_closed(closed) .build(); diff --git a/src/commands/sports.rs b/src/commands/sports.rs index 2f7b646..0368a4b 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{self, types::request::TeamsRequest}; +use super::ascending_flag; use crate::output::sports::{print_sport_types, print_sports_table, print_teams_table}; use crate::output::{OutputFormat, print_json}; @@ -74,7 +75,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .league(league.into_iter().collect()) .build(); diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 537a2ff..49e8d39 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -8,7 +8,7 @@ use polymarket_client_sdk::gamma::{ }, }; -use super::is_numeric_id; +use super::{ascending_flag, is_numeric_id}; use crate::output::tags::{print_related_tags_table, print_tag_detail, print_tags_table}; use crate::output::{OutputFormat, print_json}; @@ -72,7 +72,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma let request = TagsRequest::builder() .limit(limit) .maybe_offset(offset) - .maybe_ascending(if ascending { Some(true) } else { None }) + .maybe_ascending(ascending_flag(ascending)) .build(); let tags = client.tags(&request).await?; diff --git a/src/main.rs b/src/main.rs index db7ea38..05c9349 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,10 +88,7 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Setup => commands::setup::execute(), - Commands::Shell => { - Box::pin(shell::run_shell()).await; - Ok(()) - } + Commands::Shell => Box::pin(shell::run_shell()).await, Commands::Markets(args) => commands::markets::execute(&gamma, args, cli.output).await, Commands::Events(args) => commands::events::execute(&gamma, args, cli.output).await, Commands::Tags(args) => commands::tags::execute(&gamma, args, cli.output).await, diff --git a/src/output/mod.rs b/src/output/mod.rs index c6c135d..14a81b0 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -27,7 +27,7 @@ pub enum OutputFormat { Json, } -pub fn truncate(s: &str, max: usize) -> String { +pub(crate) fn truncate(s: &str, max: usize) -> String { if s.chars().count() <= max { return s.to_string(); } @@ -36,7 +36,7 @@ pub fn truncate(s: &str, max: usize) -> String { truncated } -pub fn format_decimal(n: Decimal) -> String { +pub(crate) fn format_decimal(n: Decimal) -> String { let f = n.to_f64().unwrap_or(0.0); if f >= 1_000_000.0 { format!("${:.1}M", f / 1_000_000.0) @@ -47,11 +47,11 @@ pub fn format_decimal(n: Decimal) -> String { } } -pub fn format_date(d: &DateTime) -> String { +pub(crate) fn format_date(d: &DateTime) -> String { d.format("%Y-%m-%d %H:%M UTC").to_string() } -pub fn active_status(closed: Option, active: Option) -> &'static str { +pub(crate) fn active_status(closed: Option, active: Option) -> &'static str { if closed == Some(true) { "Closed" } else if active == Some(true) { @@ -61,13 +61,13 @@ pub fn active_status(closed: Option, active: Option) -> &'static str } } -pub fn print_json(data: &impl serde::Serialize) -> anyhow::Result<()> { +pub(crate) fn print_json(data: &impl serde::Serialize) -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(data)?); Ok(()) } /// Print an error in the appropriate format for the current output mode. -pub fn print_error(error: &anyhow::Error, format: OutputFormat) { +pub(crate) fn print_error(error: &anyhow::Error, format: OutputFormat) { match format { OutputFormat::Json => { println!("{}", serde_json::json!({"error": error.to_string()})); @@ -78,7 +78,7 @@ pub fn print_error(error: &anyhow::Error, format: OutputFormat) { } } -pub fn print_detail_table(rows: Vec<[String; 2]>) { +pub(crate) fn print_detail_table(rows: Vec<[String; 2]>) { let table = Table::from_iter(rows) .with(Style::rounded()) .with(Modify::new(Columns::first()).with(Width::wrap(20))) diff --git a/src/shell.rs b/src/shell.rs index 65c2384..0c2df7b 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,18 +1,12 @@ use clap::Parser as _; -pub async fn run_shell() { +pub async fn run_shell() -> anyhow::Result<()> { println!(); println!(" Polymarket CLI · Interactive Shell"); println!(" Type 'help' for commands, 'exit' to quit."); println!(); - let mut rl = match rustyline::DefaultEditor::new() { - Ok(rl) => rl, - Err(e) => { - eprintln!("Failed to initialize shell: {e}"); - return; - } - }; + let mut rl = rustyline::DefaultEditor::new()?; loop { match rl.readline("polymarket> ") { @@ -64,6 +58,7 @@ pub async fn run_shell() { } println!("Goodbye!"); + Ok(()) } fn split_args(input: &str) -> Vec { From 313860acaa2fdae2dabf711d89b3eef5ecb7c8ad Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 20:32:07 +0530 Subject: [PATCH 6/6] refactor clob client --- src/output/clob.rs | 1467 ------------------------------------ src/output/clob/account.rs | 567 ++++++++++++++ src/output/clob/books.rs | 185 +++++ src/output/clob/markets.rs | 230 ++++++ src/output/clob/mod.rs | 44 ++ src/output/clob/orders.rs | 334 ++++++++ src/output/clob/prices.rs | 169 +++++ 7 files changed, 1529 insertions(+), 1467 deletions(-) delete mode 100644 src/output/clob.rs create mode 100644 src/output/clob/account.rs create mode 100644 src/output/clob/books.rs create mode 100644 src/output/clob/markets.rs create mode 100644 src/output/clob/mod.rs create mode 100644 src/output/clob/orders.rs create mode 100644 src/output/clob/prices.rs diff --git a/src/output/clob.rs b/src/output/clob.rs deleted file mode 100644 index 165e972..0000000 --- a/src/output/clob.rs +++ /dev/null @@ -1,1467 +0,0 @@ -#![allow(clippy::items_after_statements)] - -use polymarket_client_sdk::auth::Credentials; -use polymarket_client_sdk::clob::types::response::{ - ApiKeysResponse, BalanceAllowanceResponse, BanStatusResponse, CancelOrdersResponse, - CurrentRewardResponse, FeeRateResponse, GeoblockResponse, LastTradePriceResponse, - LastTradesPricesResponse, MarketResponse, MarketRewardResponse, MidpointResponse, - MidpointsResponse, NegRiskResponse, NotificationResponse, OpenOrderResponse, - OrderBookSummaryResponse, OrderScoringResponse, OrdersScoringResponse, Page, PostOrderResponse, - PriceHistoryResponse, PriceResponse, PricesResponse, RewardsPercentagesResponse, - SimplifiedMarketResponse, SpreadResponse, SpreadsResponse, TickSizeResponse, - TotalUserEarningResponse, TradeResponse, UserEarningResponse, UserRewardsEarningResponse, -}; -use polymarket_client_sdk::types::Decimal; -use serde_json::json; -use tabled::settings::Style; -use tabled::{Table, Tabled}; - -use super::{OutputFormat, format_decimal, truncate}; - -/// Base64-encoded empty cursor returned by the CLOB API when there are no more pages. -const END_CURSOR: &str = "LTE="; - -pub fn print_ok(result: &str, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("CLOB API: {result}"), - OutputFormat::Json => { - super::print_json(&json!({"status": result}))?; - } - } - Ok(()) -} - -pub fn print_price(result: &PriceResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Price: {}", result.price), - OutputFormat::Json => { - super::print_json(&json!({"price": result.price.to_string()}))?; - } - } - Ok(()) -} - -pub fn print_batch_prices(result: &PricesResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - let Some(prices) = &result.prices else { - println!("No prices available."); - return Ok(()); - }; - if prices.is_empty() { - println!("No prices available."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Token ID")] - token_id: String, - #[tabled(rename = "Side")] - side: String, - #[tabled(rename = "Price")] - price: String, - } - let mut rows = Vec::new(); - for (token_id, sides) in prices { - for (side, price) in sides { - rows.push(Row { - token_id: truncate(&token_id.to_string(), 20), - side: side.to_string(), - price: price.to_string(), - }); - } - } - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data = result.prices.as_ref().map(|prices| { - prices - .iter() - .map(|(token_id, sides)| { - let side_map: serde_json::Map = sides - .iter() - .map(|(side, price)| (side.to_string(), json!(price.to_string()))) - .collect(); - (token_id.to_string(), json!(side_map)) - }) - .collect::>() - }); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_midpoint(result: &MidpointResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Midpoint: {}", result.mid), - OutputFormat::Json => { - super::print_json(&json!({"midpoint": result.mid.to_string()}))?; - } - } - Ok(()) -} - -pub fn print_midpoints(result: &MidpointsResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.midpoints.is_empty() { - println!("No midpoints available."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Token ID")] - token_id: String, - #[tabled(rename = "Midpoint")] - midpoint: String, - } - let rows: Vec = result - .midpoints - .iter() - .map(|(id, mid)| Row { - token_id: truncate(&id.to_string(), 20), - midpoint: mid.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: serde_json::Map = result - .midpoints - .iter() - .map(|(id, mid)| (id.to_string(), json!(mid.to_string()))) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_spread(result: &SpreadResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Spread: {}", result.spread), - OutputFormat::Json => { - super::print_json(&json!({"spread": result.spread.to_string()}))?; - } - } - Ok(()) -} - -pub fn print_spreads(result: &SpreadsResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - let Some(spreads) = &result.spreads else { - println!("No spreads available."); - return Ok(()); - }; - if spreads.is_empty() { - println!("No spreads available."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Token ID")] - token_id: String, - #[tabled(rename = "Spread")] - spread: String, - } - let rows: Vec = spreads - .iter() - .map(|(id, spread)| Row { - token_id: truncate(&id.to_string(), 20), - spread: spread.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data = result.spreads.as_ref().map(|spreads| { - spreads - .iter() - .map(|(id, spread)| (id.to_string(), json!(spread.to_string()))) - .collect::>() - }); - super::print_json(&data)?; - } - } - Ok(()) -} - -fn order_book_to_json(book: &OrderBookSummaryResponse) -> serde_json::Value { - let bids: Vec<_> = book - .bids - .iter() - .map(|o| json!({"price": o.price.to_string(), "size": o.size.to_string()})) - .collect(); - let asks: Vec<_> = book - .asks - .iter() - .map(|o| json!({"price": o.price.to_string(), "size": o.size.to_string()})) - .collect(); - json!({ - "market": book.market.to_string(), - "asset_id": book.asset_id.to_string(), - "timestamp": book.timestamp.to_rfc3339(), - "bids": bids, - "asks": asks, - "min_order_size": book.min_order_size.to_string(), - "neg_risk": book.neg_risk, - "tick_size": book.tick_size.as_decimal().to_string(), - "last_trade_price": book.last_trade_price.map(|p| p.to_string()), - }) -} - -pub fn print_order_book( - result: &OrderBookSummaryResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("Market: {}", result.market); - println!("Asset: {}", result.asset_id); - println!( - "Last Trade: {}", - result - .last_trade_price - .map_or("—".into(), |p| p.to_string()) - ); - println!(); - - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Price")] - price: String, - #[tabled(rename = "Size")] - size: String, - } - - if result.bids.is_empty() { - println!("No bids."); - } else { - println!("Bids:"); - let rows: Vec = result - .bids - .iter() - .map(|o| Row { - price: o.price.to_string(), - size: o.size.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - - println!(); - - if result.asks.is_empty() { - println!("No asks."); - } else { - println!("Asks:"); - let rows: Vec = result - .asks - .iter() - .map(|o| Row { - price: o.price.to_string(), - size: o.size.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - } - OutputFormat::Json => { - super::print_json(&order_book_to_json(result))?; - } - } - Ok(()) -} - -pub fn print_order_books( - result: &[OrderBookSummaryResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No order books found."); - return Ok(()); - } - for (i, book) in result.iter().enumerate() { - if i > 0 { - println!(); - } - print_order_book(book, output)?; - } - } - OutputFormat::Json => { - let data: Vec<_> = result.iter().map(order_book_to_json).collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_last_trade( - result: &LastTradePriceResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Last Trade: {} ({})", result.price, result.side), - OutputFormat::Json => { - super::print_json(&json!({ - "price": result.price.to_string(), - "side": result.side.to_string(), - }))?; - } - } - Ok(()) -} - -pub fn print_last_trades_prices( - result: &[LastTradesPricesResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No last trade prices found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Token ID")] - token_id: String, - #[tabled(rename = "Price")] - price: String, - #[tabled(rename = "Side")] - side: String, - } - let rows: Vec = result - .iter() - .map(|t| Row { - token_id: truncate(&t.token_id.to_string(), 20), - price: t.price.to_string(), - side: t.side.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: Vec<_> = result - .iter() - .map(|t| { - json!({ - "token_id": t.token_id.to_string(), - "price": t.price.to_string(), - "side": t.side.to_string(), - }) - }) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - let mut rows = vec![ - ["Question".into(), result.question.clone()], - ["Description".into(), truncate(&result.description, 80)], - ["Slug".into(), result.market_slug.clone()], - [ - "Condition ID".into(), - result.condition_id.map_or("—".into(), |c| c.to_string()), - ], - ["Active".into(), result.active.to_string()], - ["Closed".into(), result.closed.to_string()], - [ - "Accepting Orders".into(), - result.accepting_orders.to_string(), - ], - [ - "Min Order Size".into(), - result.minimum_order_size.to_string(), - ], - ["Min Tick Size".into(), result.minimum_tick_size.to_string()], - ["Neg Risk".into(), result.neg_risk.to_string()], - [ - "End Date".into(), - result.end_date_iso.map_or("—".into(), |d| d.to_rfc3339()), - ], - ]; - for token in &result.tokens { - rows.push([ - format!("Token ({})", token.outcome), - format!( - "ID: {} | Price: {} | Winner: {}", - token.token_id, token.price, token.winner - ), - ]); - } - super::print_detail_table(rows); - } - OutputFormat::Json => { - super::print_json(result)?; - } - } - Ok(()) -} - -pub fn print_clob_markets( - result: &Page, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No markets found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Question")] - question: String, - #[tabled(rename = "Active")] - active: String, - #[tabled(rename = "Tokens")] - tokens: String, - #[tabled(rename = "Min Tick")] - min_tick: String, - } - let rows: Vec = result - .data - .iter() - .map(|m| Row { - question: truncate(&m.question, 50), - active: if m.active { "Yes" } else { "No" }.into(), - tokens: m.tokens.len().to_string(), - min_tick: m.minimum_tick_size.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - super::print_json(result)?; - } - } - Ok(()) -} - -pub fn print_simplified_markets( - result: &Page, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No markets found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Condition ID")] - condition_id: String, - #[tabled(rename = "Tokens")] - tokens: String, - #[tabled(rename = "Active")] - active: String, - #[tabled(rename = "Closed")] - closed: String, - #[tabled(rename = "Orders")] - accepting_orders: String, - } - let rows: Vec = result - .data - .iter() - .map(|m| Row { - condition_id: m - .condition_id - .map_or("—".into(), |c| truncate(&c.to_string(), 14)), - tokens: m.tokens.len().to_string(), - active: if m.active { "Yes" } else { "No" }.into(), - closed: if m.closed { "Yes" } else { "No" }.into(), - accepting_orders: if m.accepting_orders { "Yes" } else { "No" }.into(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - super::print_json(result)?; - } - } - Ok(()) -} - -pub fn print_tick_size(result: &TickSizeResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("Tick size: {}", result.minimum_tick_size.as_decimal()); - } - OutputFormat::Json => { - super::print_json(&json!({ - "minimum_tick_size": result.minimum_tick_size.as_decimal().to_string(), - }))?; - } - } - Ok(()) -} - -pub fn print_fee_rate(result: &FeeRateResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("Fee rate: {} bps", result.base_fee); - } - OutputFormat::Json => { - super::print_json(&json!({ - "base_fee_bps": result.base_fee, - }))?; - } - } - Ok(()) -} - -pub fn print_neg_risk(result: &NegRiskResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Neg risk: {}", result.neg_risk), - OutputFormat::Json => { - super::print_json(&json!({"neg_risk": result.neg_risk}))?; - } - } - Ok(()) -} - -pub fn print_price_history( - result: &PriceHistoryResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.history.is_empty() { - println!("No price history found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Timestamp")] - timestamp: String, - #[tabled(rename = "Price")] - price: String, - } - let rows: Vec = result - .history - .iter() - .map(|p| Row { - timestamp: chrono::DateTime::from_timestamp(p.t, 0) - .map_or(p.t.to_string(), |dt| { - dt.format("%Y-%m-%d %H:%M").to_string() - }), - price: p.p.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: Vec<_> = result - .history - .iter() - .map(|p| json!({"timestamp": p.t, "price": p.p.to_string()})) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_server_time(timestamp: i64, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - let dt = chrono::DateTime::from_timestamp(timestamp, 0); - match dt { - Some(dt) => { - println!( - "Server time: {} ({timestamp})", - dt.format("%Y-%m-%d %H:%M:%S UTC") - ); - } - None => println!("Server time: {timestamp}"), - } - } - OutputFormat::Json => { - super::print_json(&json!({"timestamp": timestamp}))?; - } - } - Ok(()) -} - -pub fn print_geoblock(result: &GeoblockResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("Blocked: {}", result.blocked); - println!("IP: {}", result.ip); - println!("Country: {}", result.country); - println!("Region: {}", result.region); - } - OutputFormat::Json => { - super::print_json(&json!({ - "blocked": result.blocked, - "ip": result.ip, - "country": result.country, - "region": result.region, - }))?; - } - } - Ok(()) -} - -pub fn print_orders(result: &Page, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No open orders."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "ID")] - id: String, - #[tabled(rename = "Side")] - side: String, - #[tabled(rename = "Price")] - price: String, - #[tabled(rename = "Size")] - original_size: String, - #[tabled(rename = "Matched")] - size_matched: String, - #[tabled(rename = "Status")] - status: String, - #[tabled(rename = "Type")] - order_type: String, - } - let rows: Vec = result - .data - .iter() - .map(|o| Row { - id: truncate(&o.id, 12), - side: o.side.to_string(), - price: o.price.to_string(), - original_size: o.original_size.to_string(), - size_matched: o.size_matched.to_string(), - status: o.status.to_string(), - order_type: o.order_type.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .data - .iter() - .map(|o| { - json!({ - "id": o.id, - "status": o.status.to_string(), - "market": o.market.to_string(), - "asset_id": o.asset_id.to_string(), - "side": o.side.to_string(), - "price": o.price.to_string(), - "original_size": o.original_size.to_string(), - "size_matched": o.size_matched.to_string(), - "outcome": o.outcome, - "order_type": o.order_type.to_string(), - "created_at": o.created_at.to_rfc3339(), - "expiration": o.expiration.to_rfc3339(), - }) - }) - .collect(); - let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); - super::print_json(&wrapper)?; - } - } - Ok(()) -} - -pub fn print_order_detail(result: &OpenOrderResponse, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - let rows = vec![ - ["ID".into(), result.id.clone()], - ["Status".into(), result.status.to_string()], - ["Market".into(), result.market.to_string()], - ["Asset ID".into(), result.asset_id.to_string()], - ["Side".into(), result.side.to_string()], - ["Price".into(), result.price.to_string()], - ["Original Size".into(), result.original_size.to_string()], - ["Size Matched".into(), result.size_matched.to_string()], - ["Outcome".into(), result.outcome.clone()], - ["Order Type".into(), result.order_type.to_string()], - ["Created".into(), result.created_at.to_rfc3339()], - ["Expiration".into(), result.expiration.to_rfc3339()], - ["Trades".into(), result.associate_trades.join(", ")], - ]; - super::print_detail_table(rows); - } - OutputFormat::Json => { - let data = json!({ - "id": result.id, - "status": result.status.to_string(), - "owner": result.owner.to_string(), - "maker_address": result.maker_address.to_string(), - "market": result.market.to_string(), - "asset_id": result.asset_id.to_string(), - "side": result.side.to_string(), - "price": result.price.to_string(), - "original_size": result.original_size.to_string(), - "size_matched": result.size_matched.to_string(), - "outcome": result.outcome, - "order_type": result.order_type.to_string(), - "created_at": result.created_at.to_rfc3339(), - "expiration": result.expiration.to_rfc3339(), - "associate_trades": result.associate_trades, - }); - super::print_json(&data)?; - } - } - Ok(()) -} - -fn post_order_to_json(r: &PostOrderResponse) -> serde_json::Value { - let tx_hashes: Vec<_> = r - .transaction_hashes - .iter() - .map(std::string::ToString::to_string) - .collect(); - json!({ - "order_id": r.order_id, - "status": r.status.to_string(), - "success": r.success, - "error_msg": r.error_msg, - "making_amount": r.making_amount.to_string(), - "taking_amount": r.taking_amount.to_string(), - "transaction_hashes": tx_hashes, - "trade_ids": r.trade_ids, - }) -} - -pub fn print_post_order_result( - result: &PostOrderResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("Order ID: {}", result.order_id); - println!("Status: {}", result.status); - println!("Success: {}", result.success); - if let Some(err) = &result.error_msg - && !err.is_empty() - { - println!("Error: {err}"); - } - println!("Making: {}", result.making_amount); - println!("Taking: {}", result.taking_amount); - } - OutputFormat::Json => { - super::print_json(&post_order_to_json(result))?; - } - } - Ok(()) -} - -pub fn print_post_orders_result( - results: &[PostOrderResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - for (i, r) in results.iter().enumerate() { - if i > 0 { - println!("---"); - } - print_post_order_result(r, output)?; - } - } - OutputFormat::Json => { - let data: Vec<_> = results.iter().map(post_order_to_json).collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_cancel_result( - result: &CancelOrdersResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if !result.canceled.is_empty() { - println!("Canceled: {}", result.canceled.join(", ")); - } - if !result.not_canceled.is_empty() { - println!("Not canceled:"); - for (id, reason) in &result.not_canceled { - println!(" {id}: {reason}"); - } - } - if result.canceled.is_empty() && result.not_canceled.is_empty() { - println!("No orders to cancel."); - } - } - OutputFormat::Json => { - let data = json!({ - "canceled": result.canceled, - "not_canceled": result.not_canceled, - }); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_trades(result: &Page, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No trades found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "ID")] - id: String, - #[tabled(rename = "Side")] - side: String, - #[tabled(rename = "Price")] - price: String, - #[tabled(rename = "Size")] - size: String, - #[tabled(rename = "Status")] - status: String, - #[tabled(rename = "Time")] - match_time: String, - } - let rows: Vec = result - .data - .iter() - .map(|t| Row { - id: truncate(&t.id, 12), - side: t.side.to_string(), - price: t.price.to_string(), - size: t.size.to_string(), - status: t.status.to_string(), - match_time: t.match_time.format("%Y-%m-%d %H:%M").to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .data - .iter() - .map(|t| { - json!({ - "id": t.id, - "taker_order_id": t.taker_order_id, - "market": t.market.to_string(), - "asset_id": t.asset_id.to_string(), - "side": t.side.to_string(), - "size": t.size.to_string(), - "price": t.price.to_string(), - "fee_rate_bps": t.fee_rate_bps.to_string(), - "status": t.status.to_string(), - "match_time": t.match_time.to_rfc3339(), - "outcome": t.outcome, - "trader_side": format!("{:?}", t.trader_side), - "transaction_hash": t.transaction_hash.to_string(), - }) - }) - .collect(); - let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); - super::print_json(&wrapper)?; - } - } - Ok(()) -} - -/// USDC uses 6 decimal places on-chain. -const USDC_DECIMALS: u32 = 6; - -pub fn print_balance( - result: &BalanceAllowanceResponse, - is_collateral: bool, - output: &OutputFormat, -) -> anyhow::Result<()> { - let divisor = Decimal::from(10u64.pow(USDC_DECIMALS)); - let human_balance = result.balance / divisor; - match output { - OutputFormat::Table => { - if is_collateral { - println!("Balance: {}", format_decimal(human_balance)); - } else { - println!("Balance: {human_balance} shares"); - } - if !result.allowances.is_empty() { - println!("Allowances:"); - for (addr, allowance) in &result.allowances { - println!(" {}: {allowance}", truncate(&addr.to_string(), 14)); - } - } - } - OutputFormat::Json => { - let allowances: serde_json::Map = result - .allowances - .iter() - .map(|(addr, val)| (addr.to_string(), json!(val))) - .collect(); - let data = json!({ - "balance": human_balance.to_string(), - "allowances": allowances, - }); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_notifications( - result: &[NotificationResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No notifications."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Type")] - notif_type: String, - #[tabled(rename = "Question")] - question: String, - #[tabled(rename = "Side")] - side: String, - #[tabled(rename = "Price")] - price: String, - #[tabled(rename = "Size")] - size: String, - } - let rows: Vec = result - .iter() - .map(|n| Row { - notif_type: n.r#type.to_string(), - question: truncate(&n.payload.question, 40), - side: n.payload.side.to_string(), - price: n.payload.price.to_string(), - size: n.payload.matched_size.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: Vec<_> = result - .iter() - .map(|n| { - json!({ - "type": n.r#type, - "question": n.payload.question, - "side": n.payload.side.to_string(), - "price": n.payload.price.to_string(), - "outcome": n.payload.outcome, - "matched_size": n.payload.matched_size.to_string(), - "original_size": n.payload.original_size.to_string(), - "order_id": n.payload.order_id, - "trade_id": n.payload.trade_id, - "market": n.payload.market.to_string(), - }) - }) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_rewards( - result: &Page, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No reward earnings found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Date")] - date: String, - #[tabled(rename = "Condition ID")] - condition_id: String, - #[tabled(rename = "Earnings")] - earnings: String, - #[tabled(rename = "Rate")] - rate: String, - } - let rows: Vec = result - .data - .iter() - .map(|e| Row { - date: e.date.to_string(), - condition_id: truncate(&e.condition_id.to_string(), 14), - earnings: format_decimal(e.earnings), - rate: e.asset_rate.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .data - .iter() - .map(|e| { - json!({ - "date": e.date.to_string(), - "condition_id": e.condition_id.to_string(), - "asset_address": e.asset_address.to_string(), - "maker_address": e.maker_address.to_string(), - "earnings": e.earnings.to_string(), - "asset_rate": e.asset_rate.to_string(), - }) - }) - .collect(); - let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); - super::print_json(&wrapper)?; - } - } - Ok(()) -} - -pub fn print_earnings( - result: &[TotalUserEarningResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No earnings data found."); - return Ok(()); - } - for (i, e) in result.iter().enumerate() { - if i > 0 { - println!("---"); - } - println!("Date: {}", e.date); - println!("Earnings: {}", format_decimal(e.earnings)); - println!("Asset Rate: {}", e.asset_rate); - println!("Maker: {}", e.maker_address); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .iter() - .map(|e| { - json!({ - "date": e.date.to_string(), - "asset_address": e.asset_address.to_string(), - "maker_address": e.maker_address.to_string(), - "earnings": e.earnings.to_string(), - "asset_rate": e.asset_rate.to_string(), - }) - }) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_user_earnings_markets( - result: &[UserRewardsEarningResponse], - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No earnings data found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Question")] - question: String, - #[tabled(rename = "Condition ID")] - condition_id: String, - #[tabled(rename = "Earn %")] - earning_pct: String, - #[tabled(rename = "Max Spread")] - max_spread: String, - #[tabled(rename = "Min Size")] - min_size: String, - } - let rows: Vec = result - .iter() - .map(|e| Row { - question: truncate(&e.question, 40), - condition_id: truncate(&e.condition_id.to_string(), 14), - earning_pct: format!("{}%", e.earning_percentage), - max_spread: e.rewards_max_spread.to_string(), - min_size: e.rewards_min_size.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: Vec<_> = result - .iter() - .map(|e| { - json!({ - "condition_id": e.condition_id.to_string(), - "question": e.question, - "market_slug": e.market_slug, - "event_slug": e.event_slug, - "earning_percentage": e.earning_percentage.to_string(), - "rewards_max_spread": e.rewards_max_spread.to_string(), - "rewards_min_size": e.rewards_min_size.to_string(), - "market_competitiveness": e.market_competitiveness.to_string(), - "maker_address": e.maker_address.to_string(), - "tokens": e.tokens.iter().map(|t| json!({ - "token_id": t.token_id.to_string(), - "outcome": t.outcome, - "price": t.price.to_string(), - "winner": t.winner, - })).collect::>(), - "rewards_config": e.rewards_config.iter().map(|r| json!({ - "asset_address": r.asset_address.to_string(), - "start_date": r.start_date.to_string(), - "end_date": r.end_date.to_string(), - "rate_per_day": r.rate_per_day.to_string(), - "total_rewards": r.total_rewards.to_string(), - })).collect::>(), - "earnings": e.earnings.iter().map(|ear| json!({ - "asset_address": ear.asset_address.to_string(), - "earnings": ear.earnings.to_string(), - "asset_rate": ear.asset_rate.to_string(), - })).collect::>(), - }) - }) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_reward_percentages( - result: &RewardsPercentagesResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No reward percentages found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Market")] - market: String, - #[tabled(rename = "Percentage")] - percentage: String, - } - let rows: Vec = result - .iter() - .map(|(market, pct)| Row { - market: truncate(market, 20), - percentage: format!("{pct}%"), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - let data: serde_json::Map = result - .iter() - .map(|(k, v)| (k.clone(), json!(v.to_string()))) - .collect(); - super::print_json(&data)?; - } - } - Ok(()) -} - -pub fn print_current_rewards( - result: &Page, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No current rewards found."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Condition ID")] - condition_id: String, - #[tabled(rename = "Max Spread")] - max_spread: String, - #[tabled(rename = "Min Size")] - min_size: String, - #[tabled(rename = "Configs")] - configs: String, - } - let rows: Vec = result - .data - .iter() - .map(|r| Row { - condition_id: truncate(&r.condition_id.to_string(), 14), - max_spread: r.rewards_max_spread.to_string(), - min_size: r.rewards_min_size.to_string(), - configs: r.rewards_config.len().to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .data - .iter() - .map(|r| { - json!({ - "condition_id": r.condition_id.to_string(), - "rewards_max_spread": r.rewards_max_spread.to_string(), - "rewards_min_size": r.rewards_min_size.to_string(), - "rewards_config": r.rewards_config.iter().map(|c| json!({ - "asset_address": c.asset_address.to_string(), - "start_date": c.start_date.to_string(), - "end_date": c.end_date.to_string(), - "rate_per_day": c.rate_per_day.to_string(), - "total_rewards": c.total_rewards.to_string(), - })).collect::>(), - }) - }) - .collect(); - let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); - super::print_json(&wrapper)?; - } - } - Ok(()) -} - -pub fn print_market_reward( - result: &Page, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.data.is_empty() { - println!("No market reward data found."); - return Ok(()); - } - for (i, r) in result.data.iter().enumerate() { - if i > 0 { - println!("---"); - } - println!("Question: {}", r.question); - println!("Condition ID: {}", r.condition_id); - println!("Slug: {}", r.market_slug); - println!("Max Spread: {}", r.rewards_max_spread); - println!("Min Size: {}", r.rewards_min_size); - println!("Competitiveness: {}", r.market_competitiveness); - for token in &r.tokens { - println!( - " Token ({}): {} | Price: {}", - token.outcome, token.token_id, token.price - ); - } - } - if result.next_cursor != END_CURSOR { - println!("Next cursor: {}", result.next_cursor); - } - } - OutputFormat::Json => { - let data: Vec<_> = result - .data - .iter() - .map(|r| { - json!({ - "condition_id": r.condition_id.to_string(), - "question": r.question, - "market_slug": r.market_slug, - "event_slug": r.event_slug, - "rewards_max_spread": r.rewards_max_spread.to_string(), - "rewards_min_size": r.rewards_min_size.to_string(), - "market_competitiveness": r.market_competitiveness.to_string(), - "tokens": r.tokens.iter().map(|t| json!({ - "token_id": t.token_id.to_string(), - "outcome": t.outcome, - "price": t.price.to_string(), - "winner": t.winner, - })).collect::>(), - "rewards_config": r.rewards_config.iter().map(|c| json!({ - "id": c.id, - "asset_address": c.asset_address.to_string(), - "start_date": c.start_date.to_string(), - "end_date": c.end_date.to_string(), - "rate_per_day": c.rate_per_day.to_string(), - "total_rewards": c.total_rewards.to_string(), - "total_days": c.total_days.to_string(), - })).collect::>(), - }) - }) - .collect(); - let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); - super::print_json(&wrapper)?; - } - } - Ok(()) -} - -pub fn print_order_scoring( - result: &OrderScoringResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("Scoring: {}", result.scoring), - OutputFormat::Json => { - super::print_json(&json!({"scoring": result.scoring}))?; - } - } - Ok(()) -} - -pub fn print_orders_scoring( - result: &OrdersScoringResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - if result.is_empty() { - println!("No scoring data."); - return Ok(()); - } - #[derive(Tabled)] - struct Row { - #[tabled(rename = "Order ID")] - order_id: String, - #[tabled(rename = "Scoring")] - scoring: String, - } - let rows: Vec = result - .iter() - .map(|(id, scoring)| Row { - order_id: truncate(id, 16), - scoring: scoring.to_string(), - }) - .collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); - } - OutputFormat::Json => { - super::print_json(result)?; - } - } - Ok(()) -} - -pub fn print_api_keys(result: &ApiKeysResponse, output: &OutputFormat) -> anyhow::Result<()> { - // SDK limitation: ApiKeysResponse.keys is private with no public accessor or Serialize impl. - // We use Debug output as the only available representation. - let debug = format!("{result:?}"); - match output { - OutputFormat::Table => { - println!("API Keys: {debug}"); - } - OutputFormat::Json => { - super::print_json(&json!({"api_keys": debug}))?; - } - } - Ok(()) -} - -pub fn print_delete_api_key( - result: &serde_json::Value, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => println!("API key deleted: {result}"), - OutputFormat::Json => { - super::print_json(result)?; - } - } - Ok(()) -} - -pub fn print_create_api_key(result: &Credentials, output: &OutputFormat) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!("API Key: {}", result.key()); - println!("Secret: [redacted]"); - println!("Passphrase: [redacted]"); - } - OutputFormat::Json => { - super::print_json(&json!({ - "api_key": result.key().to_string(), - "secret": "[redacted]", - "passphrase": "[redacted]", - }))?; - } - } - Ok(()) -} - -pub fn print_account_status( - result: &BanStatusResponse, - output: &OutputFormat, -) -> anyhow::Result<()> { - match output { - OutputFormat::Table => { - println!( - "Account status: {}", - if result.closed_only { - "Closed-only mode (restricted)" - } else { - "Active" - } - ); - } - OutputFormat::Json => { - super::print_json(&json!({"closed_only": result.closed_only}))?; - } - } - Ok(()) -} diff --git a/src/output/clob/account.rs b/src/output/clob/account.rs new file mode 100644 index 0000000..596be78 --- /dev/null +++ b/src/output/clob/account.rs @@ -0,0 +1,567 @@ +use polymarket_client_sdk::auth::Credentials; +use polymarket_client_sdk::clob::types::response::{ + ApiKeysResponse, BalanceAllowanceResponse, BanStatusResponse, CurrentRewardResponse, + GeoblockResponse, MarketRewardResponse, NotificationResponse, Page, RewardsPercentagesResponse, + TotalUserEarningResponse, UserEarningResponse, UserRewardsEarningResponse, +}; +use polymarket_client_sdk::types::Decimal; +use serde_json::json; +use tabled::settings::Style; +use tabled::{Table, Tabled}; + +use super::END_CURSOR; +use crate::output::{OutputFormat, format_decimal, truncate}; + +pub fn print_server_time(timestamp: i64, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + let dt = chrono::DateTime::from_timestamp(timestamp, 0); + match dt { + Some(dt) => { + println!( + "Server time: {} ({timestamp})", + dt.format("%Y-%m-%d %H:%M:%S UTC") + ); + } + None => println!("Server time: {timestamp}"), + } + } + OutputFormat::Json => { + crate::output::print_json(&json!({"timestamp": timestamp}))?; + } + } + Ok(()) +} + +pub fn print_geoblock(result: &GeoblockResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("Blocked: {}", result.blocked); + println!("IP: {}", result.ip); + println!("Country: {}", result.country); + println!("Region: {}", result.region); + } + OutputFormat::Json => { + crate::output::print_json(&json!({ + "blocked": result.blocked, + "ip": result.ip, + "country": result.country, + "region": result.region, + }))?; + } + } + Ok(()) +} + +/// USDC uses 6 decimal places on-chain. +const USDC_DECIMALS: u32 = 6; + +pub fn print_balance( + result: &BalanceAllowanceResponse, + is_collateral: bool, + output: &OutputFormat, +) -> anyhow::Result<()> { + let divisor = Decimal::from(10u64.pow(USDC_DECIMALS)); + let human_balance = result.balance / divisor; + match output { + OutputFormat::Table => { + if is_collateral { + println!("Balance: {}", format_decimal(human_balance)); + } else { + println!("Balance: {human_balance} shares"); + } + if !result.allowances.is_empty() { + println!("Allowances:"); + for (addr, allowance) in &result.allowances { + println!(" {}: {allowance}", truncate(&addr.to_string(), 14)); + } + } + } + OutputFormat::Json => { + let allowances: serde_json::Map = result + .allowances + .iter() + .map(|(addr, val)| (addr.to_string(), json!(val))) + .collect(); + let data = json!({ + "balance": human_balance.to_string(), + "allowances": allowances, + }); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_notifications( + result: &[NotificationResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No notifications."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Type")] + notif_type: String, + #[tabled(rename = "Question")] + question: String, + #[tabled(rename = "Side")] + side: String, + #[tabled(rename = "Price")] + price: String, + #[tabled(rename = "Size")] + size: String, + } + let rows: Vec = result + .iter() + .map(|n| Row { + notif_type: n.r#type.to_string(), + question: truncate(&n.payload.question, 40), + side: n.payload.side.to_string(), + price: n.payload.price.to_string(), + size: n.payload.matched_size.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: Vec<_> = result + .iter() + .map(|n| { + json!({ + "type": n.r#type, + "question": n.payload.question, + "side": n.payload.side.to_string(), + "price": n.payload.price.to_string(), + "outcome": n.payload.outcome, + "matched_size": n.payload.matched_size.to_string(), + "original_size": n.payload.original_size.to_string(), + "order_id": n.payload.order_id, + "trade_id": n.payload.trade_id, + "market": n.payload.market.to_string(), + }) + }) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_rewards( + result: &Page, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No reward earnings found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Date")] + date: String, + #[tabled(rename = "Condition ID")] + condition_id: String, + #[tabled(rename = "Earnings")] + earnings: String, + #[tabled(rename = "Rate")] + rate: String, + } + let rows: Vec = result + .data + .iter() + .map(|e| Row { + date: e.date.to_string(), + condition_id: truncate(&e.condition_id.to_string(), 14), + earnings: format_decimal(e.earnings), + rate: e.asset_rate.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .data + .iter() + .map(|e| { + json!({ + "date": e.date.to_string(), + "condition_id": e.condition_id.to_string(), + "asset_address": e.asset_address.to_string(), + "maker_address": e.maker_address.to_string(), + "earnings": e.earnings.to_string(), + "asset_rate": e.asset_rate.to_string(), + }) + }) + .collect(); + let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); + crate::output::print_json(&wrapper)?; + } + } + Ok(()) +} + +pub fn print_earnings( + result: &[TotalUserEarningResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No earnings data found."); + return Ok(()); + } + for (i, e) in result.iter().enumerate() { + if i > 0 { + println!("---"); + } + println!("Date: {}", e.date); + println!("Earnings: {}", format_decimal(e.earnings)); + println!("Asset Rate: {}", e.asset_rate); + println!("Maker: {}", e.maker_address); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .iter() + .map(|e| { + json!({ + "date": e.date.to_string(), + "asset_address": e.asset_address.to_string(), + "maker_address": e.maker_address.to_string(), + "earnings": e.earnings.to_string(), + "asset_rate": e.asset_rate.to_string(), + }) + }) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_user_earnings_markets( + result: &[UserRewardsEarningResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No earnings data found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Question")] + question: String, + #[tabled(rename = "Condition ID")] + condition_id: String, + #[tabled(rename = "Earn %")] + earning_pct: String, + #[tabled(rename = "Max Spread")] + max_spread: String, + #[tabled(rename = "Min Size")] + min_size: String, + } + let rows: Vec = result + .iter() + .map(|e| Row { + question: truncate(&e.question, 40), + condition_id: truncate(&e.condition_id.to_string(), 14), + earning_pct: format!("{}%", e.earning_percentage), + max_spread: e.rewards_max_spread.to_string(), + min_size: e.rewards_min_size.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: Vec<_> = result + .iter() + .map(|e| { + json!({ + "condition_id": e.condition_id.to_string(), + "question": e.question, + "market_slug": e.market_slug, + "event_slug": e.event_slug, + "earning_percentage": e.earning_percentage.to_string(), + "rewards_max_spread": e.rewards_max_spread.to_string(), + "rewards_min_size": e.rewards_min_size.to_string(), + "market_competitiveness": e.market_competitiveness.to_string(), + "maker_address": e.maker_address.to_string(), + "tokens": e.tokens.iter().map(|t| json!({ + "token_id": t.token_id.to_string(), + "outcome": t.outcome, + "price": t.price.to_string(), + "winner": t.winner, + })).collect::>(), + "rewards_config": e.rewards_config.iter().map(|r| json!({ + "asset_address": r.asset_address.to_string(), + "start_date": r.start_date.to_string(), + "end_date": r.end_date.to_string(), + "rate_per_day": r.rate_per_day.to_string(), + "total_rewards": r.total_rewards.to_string(), + })).collect::>(), + "earnings": e.earnings.iter().map(|ear| json!({ + "asset_address": ear.asset_address.to_string(), + "earnings": ear.earnings.to_string(), + "asset_rate": ear.asset_rate.to_string(), + })).collect::>(), + }) + }) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_reward_percentages( + result: &RewardsPercentagesResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No reward percentages found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Market")] + market: String, + #[tabled(rename = "Percentage")] + percentage: String, + } + let rows: Vec = result + .iter() + .map(|(market, pct)| Row { + market: truncate(market, 20), + percentage: format!("{pct}%"), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: serde_json::Map = result + .iter() + .map(|(k, v)| (k.clone(), json!(v.to_string()))) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_current_rewards( + result: &Page, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No current rewards found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Condition ID")] + condition_id: String, + #[tabled(rename = "Max Spread")] + max_spread: String, + #[tabled(rename = "Min Size")] + min_size: String, + #[tabled(rename = "Configs")] + configs: String, + } + let rows: Vec = result + .data + .iter() + .map(|r| Row { + condition_id: truncate(&r.condition_id.to_string(), 14), + max_spread: r.rewards_max_spread.to_string(), + min_size: r.rewards_min_size.to_string(), + configs: r.rewards_config.len().to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .data + .iter() + .map(|r| { + json!({ + "condition_id": r.condition_id.to_string(), + "rewards_max_spread": r.rewards_max_spread.to_string(), + "rewards_min_size": r.rewards_min_size.to_string(), + "rewards_config": r.rewards_config.iter().map(|c| json!({ + "asset_address": c.asset_address.to_string(), + "start_date": c.start_date.to_string(), + "end_date": c.end_date.to_string(), + "rate_per_day": c.rate_per_day.to_string(), + "total_rewards": c.total_rewards.to_string(), + })).collect::>(), + }) + }) + .collect(); + let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); + crate::output::print_json(&wrapper)?; + } + } + Ok(()) +} + +pub fn print_market_reward( + result: &Page, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No market reward data found."); + return Ok(()); + } + for (i, r) in result.data.iter().enumerate() { + if i > 0 { + println!("---"); + } + println!("Question: {}", r.question); + println!("Condition ID: {}", r.condition_id); + println!("Slug: {}", r.market_slug); + println!("Max Spread: {}", r.rewards_max_spread); + println!("Min Size: {}", r.rewards_min_size); + println!("Competitiveness: {}", r.market_competitiveness); + for token in &r.tokens { + println!( + " Token ({}): {} | Price: {}", + token.outcome, token.token_id, token.price + ); + } + } + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .data + .iter() + .map(|r| { + json!({ + "condition_id": r.condition_id.to_string(), + "question": r.question, + "market_slug": r.market_slug, + "event_slug": r.event_slug, + "rewards_max_spread": r.rewards_max_spread.to_string(), + "rewards_min_size": r.rewards_min_size.to_string(), + "market_competitiveness": r.market_competitiveness.to_string(), + "tokens": r.tokens.iter().map(|t| json!({ + "token_id": t.token_id.to_string(), + "outcome": t.outcome, + "price": t.price.to_string(), + "winner": t.winner, + })).collect::>(), + "rewards_config": r.rewards_config.iter().map(|c| json!({ + "id": c.id, + "asset_address": c.asset_address.to_string(), + "start_date": c.start_date.to_string(), + "end_date": c.end_date.to_string(), + "rate_per_day": c.rate_per_day.to_string(), + "total_rewards": c.total_rewards.to_string(), + "total_days": c.total_days.to_string(), + })).collect::>(), + }) + }) + .collect(); + let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); + crate::output::print_json(&wrapper)?; + } + } + Ok(()) +} + +pub fn print_api_keys(result: &ApiKeysResponse, output: &OutputFormat) -> anyhow::Result<()> { + // SDK limitation: ApiKeysResponse.keys is private with no public accessor or Serialize impl. + // We use Debug output as the only available representation. + let debug = format!("{result:?}"); + match output { + OutputFormat::Table => { + println!("API Keys: {debug}"); + } + OutputFormat::Json => { + crate::output::print_json(&json!({"api_keys": debug}))?; + } + } + Ok(()) +} + +pub fn print_delete_api_key( + result: &serde_json::Value, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("API key deleted: {result}"), + OutputFormat::Json => { + crate::output::print_json(result)?; + } + } + Ok(()) +} + +pub fn print_create_api_key(result: &Credentials, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("API Key: {}", result.key()); + println!("Secret: [redacted]"); + println!("Passphrase: [redacted]"); + } + OutputFormat::Json => { + crate::output::print_json(&json!({ + "api_key": result.key().to_string(), + "secret": "[redacted]", + "passphrase": "[redacted]", + }))?; + } + } + Ok(()) +} + +pub fn print_account_status( + result: &BanStatusResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!( + "Account status: {}", + if result.closed_only { + "Closed-only mode (restricted)" + } else { + "Active" + } + ); + } + OutputFormat::Json => { + crate::output::print_json(&json!({"closed_only": result.closed_only}))?; + } + } + Ok(()) +} diff --git a/src/output/clob/books.rs b/src/output/clob/books.rs new file mode 100644 index 0000000..d8a00b1 --- /dev/null +++ b/src/output/clob/books.rs @@ -0,0 +1,185 @@ +use polymarket_client_sdk::clob::types::response::{ + LastTradePriceResponse, LastTradesPricesResponse, OrderBookSummaryResponse, +}; +use serde_json::json; +use tabled::settings::Style; +use tabled::{Table, Tabled}; + +use crate::output::{OutputFormat, truncate}; + +fn order_book_to_json(book: &OrderBookSummaryResponse) -> serde_json::Value { + let bids: Vec<_> = book + .bids + .iter() + .map(|o| json!({"price": o.price.to_string(), "size": o.size.to_string()})) + .collect(); + let asks: Vec<_> = book + .asks + .iter() + .map(|o| json!({"price": o.price.to_string(), "size": o.size.to_string()})) + .collect(); + json!({ + "market": book.market.to_string(), + "asset_id": book.asset_id.to_string(), + "timestamp": book.timestamp.to_rfc3339(), + "bids": bids, + "asks": asks, + "min_order_size": book.min_order_size.to_string(), + "neg_risk": book.neg_risk, + "tick_size": book.tick_size.as_decimal().to_string(), + "last_trade_price": book.last_trade_price.map(|p| p.to_string()), + }) +} + +pub fn print_order_book( + result: &OrderBookSummaryResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("Market: {}", result.market); + println!("Asset: {}", result.asset_id); + println!( + "Last Trade: {}", + result + .last_trade_price + .map_or("—".into(), |p| p.to_string()) + ); + println!(); + + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Price")] + price: String, + #[tabled(rename = "Size")] + size: String, + } + + if result.bids.is_empty() { + println!("No bids."); + } else { + println!("Bids:"); + let rows: Vec = result + .bids + .iter() + .map(|o| Row { + price: o.price.to_string(), + size: o.size.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + + println!(); + + if result.asks.is_empty() { + println!("No asks."); + } else { + println!("Asks:"); + let rows: Vec = result + .asks + .iter() + .map(|o| Row { + price: o.price.to_string(), + size: o.size.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + } + OutputFormat::Json => { + crate::output::print_json(&order_book_to_json(result))?; + } + } + Ok(()) +} + +pub fn print_order_books( + result: &[OrderBookSummaryResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No order books found."); + return Ok(()); + } + for (i, book) in result.iter().enumerate() { + if i > 0 { + println!(); + } + print_order_book(book, output)?; + } + } + OutputFormat::Json => { + let data: Vec<_> = result.iter().map(order_book_to_json).collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_last_trade( + result: &LastTradePriceResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Last Trade: {} ({})", result.price, result.side), + OutputFormat::Json => { + crate::output::print_json(&json!({ + "price": result.price.to_string(), + "side": result.side.to_string(), + }))?; + } + } + Ok(()) +} + +pub fn print_last_trades_prices( + result: &[LastTradesPricesResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No last trade prices found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Token ID")] + token_id: String, + #[tabled(rename = "Price")] + price: String, + #[tabled(rename = "Side")] + side: String, + } + let rows: Vec = result + .iter() + .map(|t| Row { + token_id: truncate(&t.token_id.to_string(), 20), + price: t.price.to_string(), + side: t.side.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: Vec<_> = result + .iter() + .map(|t| { + json!({ + "token_id": t.token_id.to_string(), + "price": t.price.to_string(), + "side": t.side.to_string(), + }) + }) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} diff --git a/src/output/clob/markets.rs b/src/output/clob/markets.rs new file mode 100644 index 0000000..b98251c --- /dev/null +++ b/src/output/clob/markets.rs @@ -0,0 +1,230 @@ +use polymarket_client_sdk::clob::types::response::{ + FeeRateResponse, MarketResponse, NegRiskResponse, Page, PriceHistoryResponse, + SimplifiedMarketResponse, TickSizeResponse, +}; +use serde_json::json; +use tabled::settings::Style; +use tabled::{Table, Tabled}; + +use super::END_CURSOR; +use crate::output::{OutputFormat, truncate}; + +pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + let mut rows = vec![ + ["Question".into(), result.question.clone()], + ["Description".into(), truncate(&result.description, 80)], + ["Slug".into(), result.market_slug.clone()], + [ + "Condition ID".into(), + result.condition_id.map_or("—".into(), |c| c.to_string()), + ], + ["Active".into(), result.active.to_string()], + ["Closed".into(), result.closed.to_string()], + [ + "Accepting Orders".into(), + result.accepting_orders.to_string(), + ], + [ + "Min Order Size".into(), + result.minimum_order_size.to_string(), + ], + ["Min Tick Size".into(), result.minimum_tick_size.to_string()], + ["Neg Risk".into(), result.neg_risk.to_string()], + [ + "End Date".into(), + result.end_date_iso.map_or("—".into(), |d| d.to_rfc3339()), + ], + ]; + for token in &result.tokens { + rows.push([ + format!("Token ({})", token.outcome), + format!( + "ID: {} | Price: {} | Winner: {}", + token.token_id, token.price, token.winner + ), + ]); + } + crate::output::print_detail_table(rows); + } + OutputFormat::Json => { + crate::output::print_json(result)?; + } + } + Ok(()) +} + +pub fn print_clob_markets( + result: &Page, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No markets found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Question")] + question: String, + #[tabled(rename = "Active")] + active: String, + #[tabled(rename = "Tokens")] + tokens: String, + #[tabled(rename = "Min Tick")] + min_tick: String, + } + let rows: Vec = result + .data + .iter() + .map(|m| Row { + question: truncate(&m.question, 50), + active: if m.active { "Yes" } else { "No" }.into(), + tokens: m.tokens.len().to_string(), + min_tick: m.minimum_tick_size.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + crate::output::print_json(result)?; + } + } + Ok(()) +} + +pub fn print_simplified_markets( + result: &Page, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No markets found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Condition ID")] + condition_id: String, + #[tabled(rename = "Tokens")] + tokens: String, + #[tabled(rename = "Active")] + active: String, + #[tabled(rename = "Closed")] + closed: String, + #[tabled(rename = "Orders")] + accepting_orders: String, + } + let rows: Vec = result + .data + .iter() + .map(|m| Row { + condition_id: m + .condition_id + .map_or("—".into(), |c| truncate(&c.to_string(), 14)), + tokens: m.tokens.len().to_string(), + active: if m.active { "Yes" } else { "No" }.into(), + closed: if m.closed { "Yes" } else { "No" }.into(), + accepting_orders: if m.accepting_orders { "Yes" } else { "No" }.into(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + crate::output::print_json(result)?; + } + } + Ok(()) +} + +pub fn print_tick_size(result: &TickSizeResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("Tick size: {}", result.minimum_tick_size.as_decimal()); + } + OutputFormat::Json => { + crate::output::print_json(&json!({ + "minimum_tick_size": result.minimum_tick_size.as_decimal().to_string(), + }))?; + } + } + Ok(()) +} + +pub fn print_fee_rate(result: &FeeRateResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("Fee rate: {} bps", result.base_fee); + } + OutputFormat::Json => { + crate::output::print_json(&json!({ + "base_fee_bps": result.base_fee, + }))?; + } + } + Ok(()) +} + +pub fn print_neg_risk(result: &NegRiskResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Neg risk: {}", result.neg_risk), + OutputFormat::Json => { + crate::output::print_json(&json!({"neg_risk": result.neg_risk}))?; + } + } + Ok(()) +} + +pub fn print_price_history( + result: &PriceHistoryResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.history.is_empty() { + println!("No price history found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Timestamp")] + timestamp: String, + #[tabled(rename = "Price")] + price: String, + } + let rows: Vec = result + .history + .iter() + .map(|p| Row { + timestamp: chrono::DateTime::from_timestamp(p.t, 0) + .map_or(p.t.to_string(), |dt| { + dt.format("%Y-%m-%d %H:%M").to_string() + }), + price: p.p.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: Vec<_> = result + .history + .iter() + .map(|p| json!({"timestamp": p.t, "price": p.p.to_string()})) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} diff --git a/src/output/clob/mod.rs b/src/output/clob/mod.rs new file mode 100644 index 0000000..83f3c3d --- /dev/null +++ b/src/output/clob/mod.rs @@ -0,0 +1,44 @@ +#![allow(clippy::items_after_statements)] + +mod account; +mod books; +mod markets; +mod orders; +mod prices; + +/// Base64-encoded empty cursor returned by the CLOB API when there are no more pages. +const END_CURSOR: &str = "LTE="; + +// Shared utility used by multiple submodules. +pub(crate) use super::OutputFormat; + +pub use account::{ + print_account_status, print_api_keys, print_balance, print_create_api_key, + print_current_rewards, print_delete_api_key, print_earnings, print_geoblock, + print_market_reward, print_notifications, print_reward_percentages, print_rewards, + print_server_time, print_user_earnings_markets, +}; +pub use books::{print_last_trade, print_last_trades_prices, print_order_book, print_order_books}; +pub use markets::{ + print_clob_market, print_clob_markets, print_fee_rate, print_neg_risk, print_price_history, + print_simplified_markets, print_tick_size, +}; +pub use orders::{ + print_cancel_result, print_order_detail, print_order_scoring, print_orders, + print_orders_scoring, print_post_order_result, print_post_orders_result, print_trades, +}; +pub use prices::{ + print_batch_prices, print_midpoint, print_midpoints, print_price, print_spread, print_spreads, +}; + +use serde_json::json; + +pub fn print_ok(result: &str, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("CLOB API: {result}"), + OutputFormat::Json => { + super::print_json(&json!({"status": result}))?; + } + } + Ok(()) +} diff --git a/src/output/clob/orders.rs b/src/output/clob/orders.rs new file mode 100644 index 0000000..2a5711d --- /dev/null +++ b/src/output/clob/orders.rs @@ -0,0 +1,334 @@ +use polymarket_client_sdk::clob::types::response::{ + CancelOrdersResponse, OpenOrderResponse, OrderScoringResponse, OrdersScoringResponse, Page, + PostOrderResponse, TradeResponse, +}; +use serde_json::json; +use tabled::settings::Style; +use tabled::{Table, Tabled}; + +use super::END_CURSOR; +use crate::output::{OutputFormat, truncate}; + +pub fn print_orders(result: &Page, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No open orders."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "ID")] + id: String, + #[tabled(rename = "Side")] + side: String, + #[tabled(rename = "Price")] + price: String, + #[tabled(rename = "Size")] + original_size: String, + #[tabled(rename = "Matched")] + size_matched: String, + #[tabled(rename = "Status")] + status: String, + #[tabled(rename = "Type")] + order_type: String, + } + let rows: Vec = result + .data + .iter() + .map(|o| Row { + id: truncate(&o.id, 12), + side: o.side.to_string(), + price: o.price.to_string(), + original_size: o.original_size.to_string(), + size_matched: o.size_matched.to_string(), + status: o.status.to_string(), + order_type: o.order_type.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .data + .iter() + .map(|o| { + json!({ + "id": o.id, + "status": o.status.to_string(), + "market": o.market.to_string(), + "asset_id": o.asset_id.to_string(), + "side": o.side.to_string(), + "price": o.price.to_string(), + "original_size": o.original_size.to_string(), + "size_matched": o.size_matched.to_string(), + "outcome": o.outcome, + "order_type": o.order_type.to_string(), + "created_at": o.created_at.to_rfc3339(), + "expiration": o.expiration.to_rfc3339(), + }) + }) + .collect(); + let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); + crate::output::print_json(&wrapper)?; + } + } + Ok(()) +} + +pub fn print_order_detail(result: &OpenOrderResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + let rows = vec![ + ["ID".into(), result.id.clone()], + ["Status".into(), result.status.to_string()], + ["Market".into(), result.market.to_string()], + ["Asset ID".into(), result.asset_id.to_string()], + ["Side".into(), result.side.to_string()], + ["Price".into(), result.price.to_string()], + ["Original Size".into(), result.original_size.to_string()], + ["Size Matched".into(), result.size_matched.to_string()], + ["Outcome".into(), result.outcome.clone()], + ["Order Type".into(), result.order_type.to_string()], + ["Created".into(), result.created_at.to_rfc3339()], + ["Expiration".into(), result.expiration.to_rfc3339()], + ["Trades".into(), result.associate_trades.join(", ")], + ]; + crate::output::print_detail_table(rows); + } + OutputFormat::Json => { + let data = json!({ + "id": result.id, + "status": result.status.to_string(), + "owner": result.owner.to_string(), + "maker_address": result.maker_address.to_string(), + "market": result.market.to_string(), + "asset_id": result.asset_id.to_string(), + "side": result.side.to_string(), + "price": result.price.to_string(), + "original_size": result.original_size.to_string(), + "size_matched": result.size_matched.to_string(), + "outcome": result.outcome, + "order_type": result.order_type.to_string(), + "created_at": result.created_at.to_rfc3339(), + "expiration": result.expiration.to_rfc3339(), + "associate_trades": result.associate_trades, + }); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +fn post_order_to_json(r: &PostOrderResponse) -> serde_json::Value { + let tx_hashes: Vec<_> = r + .transaction_hashes + .iter() + .map(std::string::ToString::to_string) + .collect(); + json!({ + "order_id": r.order_id, + "status": r.status.to_string(), + "success": r.success, + "error_msg": r.error_msg, + "making_amount": r.making_amount.to_string(), + "taking_amount": r.taking_amount.to_string(), + "transaction_hashes": tx_hashes, + "trade_ids": r.trade_ids, + }) +} + +pub fn print_post_order_result( + result: &PostOrderResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + println!("Order ID: {}", result.order_id); + println!("Status: {}", result.status); + println!("Success: {}", result.success); + if let Some(err) = &result.error_msg + && !err.is_empty() + { + println!("Error: {err}"); + } + println!("Making: {}", result.making_amount); + println!("Taking: {}", result.taking_amount); + } + OutputFormat::Json => { + crate::output::print_json(&post_order_to_json(result))?; + } + } + Ok(()) +} + +pub fn print_post_orders_result( + results: &[PostOrderResponse], + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + for (i, r) in results.iter().enumerate() { + if i > 0 { + println!("---"); + } + print_post_order_result(r, output)?; + } + } + OutputFormat::Json => { + let data: Vec<_> = results.iter().map(post_order_to_json).collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_cancel_result( + result: &CancelOrdersResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if !result.canceled.is_empty() { + println!("Canceled: {}", result.canceled.join(", ")); + } + if !result.not_canceled.is_empty() { + println!("Not canceled:"); + for (id, reason) in &result.not_canceled { + println!(" {id}: {reason}"); + } + } + if result.canceled.is_empty() && result.not_canceled.is_empty() { + println!("No orders to cancel."); + } + } + OutputFormat::Json => { + let data = json!({ + "canceled": result.canceled, + "not_canceled": result.not_canceled, + }); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_trades(result: &Page, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.data.is_empty() { + println!("No trades found."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "ID")] + id: String, + #[tabled(rename = "Side")] + side: String, + #[tabled(rename = "Price")] + price: String, + #[tabled(rename = "Size")] + size: String, + #[tabled(rename = "Status")] + status: String, + #[tabled(rename = "Time")] + match_time: String, + } + let rows: Vec = result + .data + .iter() + .map(|t| Row { + id: truncate(&t.id, 12), + side: t.side.to_string(), + price: t.price.to_string(), + size: t.size.to_string(), + status: t.status.to_string(), + match_time: t.match_time.format("%Y-%m-%d %H:%M").to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + if result.next_cursor != END_CURSOR { + println!("Next cursor: {}", result.next_cursor); + } + } + OutputFormat::Json => { + let data: Vec<_> = result + .data + .iter() + .map(|t| { + json!({ + "id": t.id, + "taker_order_id": t.taker_order_id, + "market": t.market.to_string(), + "asset_id": t.asset_id.to_string(), + "side": t.side.to_string(), + "size": t.size.to_string(), + "price": t.price.to_string(), + "fee_rate_bps": t.fee_rate_bps.to_string(), + "status": t.status.to_string(), + "match_time": t.match_time.to_rfc3339(), + "outcome": t.outcome, + "trader_side": format!("{:?}", t.trader_side), + "transaction_hash": t.transaction_hash.to_string(), + }) + }) + .collect(); + let wrapper = json!({"data": data, "next_cursor": result.next_cursor}); + crate::output::print_json(&wrapper)?; + } + } + Ok(()) +} + +pub fn print_order_scoring( + result: &OrderScoringResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Scoring: {}", result.scoring), + OutputFormat::Json => { + crate::output::print_json(&json!({"scoring": result.scoring}))?; + } + } + Ok(()) +} + +pub fn print_orders_scoring( + result: &OrdersScoringResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.is_empty() { + println!("No scoring data."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Order ID")] + order_id: String, + #[tabled(rename = "Scoring")] + scoring: String, + } + let rows: Vec = result + .iter() + .map(|(id, scoring)| Row { + order_id: truncate(id, 16), + scoring: scoring.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + crate::output::print_json(result)?; + } + } + Ok(()) +} diff --git a/src/output/clob/prices.rs b/src/output/clob/prices.rs new file mode 100644 index 0000000..00f7588 --- /dev/null +++ b/src/output/clob/prices.rs @@ -0,0 +1,169 @@ +use polymarket_client_sdk::clob::types::response::{ + MidpointResponse, MidpointsResponse, PriceResponse, PricesResponse, SpreadResponse, + SpreadsResponse, +}; +use serde_json::json; +use tabled::settings::Style; +use tabled::{Table, Tabled}; + +use crate::output::{OutputFormat, truncate}; + +pub fn print_price(result: &PriceResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Price: {}", result.price), + OutputFormat::Json => { + crate::output::print_json(&json!({"price": result.price.to_string()}))?; + } + } + Ok(()) +} + +pub fn print_batch_prices(result: &PricesResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + let Some(prices) = &result.prices else { + println!("No prices available."); + return Ok(()); + }; + if prices.is_empty() { + println!("No prices available."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Token ID")] + token_id: String, + #[tabled(rename = "Side")] + side: String, + #[tabled(rename = "Price")] + price: String, + } + let mut rows = Vec::new(); + for (token_id, sides) in prices { + for (side, price) in sides { + rows.push(Row { + token_id: truncate(&token_id.to_string(), 20), + side: side.to_string(), + price: price.to_string(), + }); + } + } + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data = result.prices.as_ref().map(|prices| { + prices + .iter() + .map(|(token_id, sides)| { + let side_map: serde_json::Map = sides + .iter() + .map(|(side, price)| (side.to_string(), json!(price.to_string()))) + .collect(); + (token_id.to_string(), json!(side_map)) + }) + .collect::>() + }); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_midpoint(result: &MidpointResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Midpoint: {}", result.mid), + OutputFormat::Json => { + crate::output::print_json(&json!({"midpoint": result.mid.to_string()}))?; + } + } + Ok(()) +} + +pub fn print_midpoints(result: &MidpointsResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if result.midpoints.is_empty() { + println!("No midpoints available."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Token ID")] + token_id: String, + #[tabled(rename = "Midpoint")] + midpoint: String, + } + let rows: Vec = result + .midpoints + .iter() + .map(|(id, mid)| Row { + token_id: truncate(&id.to_string(), 20), + midpoint: mid.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data: serde_json::Map = result + .midpoints + .iter() + .map(|(id, mid)| (id.to_string(), json!(mid.to_string()))) + .collect(); + crate::output::print_json(&data)?; + } + } + Ok(()) +} + +pub fn print_spread(result: &SpreadResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => println!("Spread: {}", result.spread), + OutputFormat::Json => { + crate::output::print_json(&json!({"spread": result.spread.to_string()}))?; + } + } + Ok(()) +} + +pub fn print_spreads(result: &SpreadsResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + let Some(spreads) = &result.spreads else { + println!("No spreads available."); + return Ok(()); + }; + if spreads.is_empty() { + println!("No spreads available."); + return Ok(()); + } + #[derive(Tabled)] + struct Row { + #[tabled(rename = "Token ID")] + token_id: String, + #[tabled(rename = "Spread")] + spread: String, + } + let rows: Vec = spreads + .iter() + .map(|(id, spread)| Row { + token_id: truncate(&id.to_string(), 20), + spread: spread.to_string(), + }) + .collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => { + let data = result.spreads.as_ref().map(|spreads| { + spreads + .iter() + .map(|(id, spread)| (id.to_string(), json!(spread.to_string()))) + .collect::>() + }); + crate::output::print_json(&data)?; + } + } + Ok(()) +}