From b2402352399ef3c94ad02dad20b086e47fe2730f Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Wed, 25 Feb 2026 05:20:06 +0530 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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(()) +} From ec099170cc37d6bd0f2d104dfe7ec8665820203a Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 11:49:37 +0530 Subject: [PATCH 07/13] replace maybe_ascending with ascending --- src/commands/comments.rs | 5 ++--- 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 ++-- 7 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/commands/comments.rs b/src/commands/comments.rs index 30b5439..08f275e 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -81,7 +81,6 @@ pub enum EntityType { Series, } -use super::ascending_flag; super::enum_from!(EntityType => ParentEntityType { Event, Market, Series }); @@ -105,7 +104,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending_flag(ascending)) + .ascending(ascending) .build(); let comments = client.comments(&request).await?; @@ -142,7 +141,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending_flag(ascending)) + .ascending(ascending) .build(); let comments = client.comments_by_user_address(&request).await?; diff --git a/src/commands/events.rs b/src/commands/events.rs index 71ff0d4..a888524 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::{ascending_flag, is_numeric_id}; +use super::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(ascending_flag(ascending)) + .ascending(ascending) .maybe_tag_slug(tag) .order(order.into_iter().collect()) .build(); diff --git a/src/commands/markets.rs b/src/commands/markets.rs index cd0975b..bb0f311 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -11,7 +11,7 @@ use polymarket_client_sdk::gamma::{ }, }; -use super::{ascending_flag, is_numeric_id}; +use super::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(ascending_flag(ascending)) + .ascending(ascending) .build(); let markets = client.markets(&request).await?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a853f5c..c4ee0ac 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -30,10 +30,6 @@ 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 6c6c99f..e8df5b8 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -5,7 +5,6 @@ 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}; @@ -60,7 +59,7 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending_flag(ascending)) + .ascending(ascending) .maybe_closed(closed) .build(); diff --git a/src/commands/sports.rs b/src/commands/sports.rs index 0368a4b..c0ad9de 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -2,7 +2,6 @@ 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}; @@ -75,7 +74,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending_flag(ascending)) + .ascending(ascending) .league(league.into_iter().collect()) .build(); diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 49e8d39..0742900 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -8,7 +8,7 @@ use polymarket_client_sdk::gamma::{ }, }; -use super::{ascending_flag, is_numeric_id}; +use super::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(ascending_flag(ascending)) + .ascending(ascending) .build(); let tags = client.tags(&request).await?; From cf9def20496574e82f26e978dc98e20217a04dbc Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 12:29:47 +0530 Subject: [PATCH 08/13] refactor: deep codebase cleanup - flatten dispatch, fix constants, better error handling --- src/auth.rs | 6 +- src/commands/approve.rs | 2 +- src/commands/clob.rs | 429 ++++++++++--------------------------- src/commands/comments.rs | 30 ++- src/commands/ctf.rs | 7 +- src/commands/data.rs | 123 +++-------- src/commands/events.rs | 19 +- src/commands/markets.rs | 23 +- src/commands/mod.rs | 53 ++--- src/commands/profiles.rs | 7 +- src/commands/series.rs | 13 +- src/commands/setup.rs | 2 +- src/commands/sports.rs | 20 +- src/commands/tags.rs | 23 +- src/commands/wallet.rs | 19 +- src/config.rs | 67 +++--- src/main.rs | 2 +- src/output/approve.rs | 3 - src/output/bridge.rs | 6 +- src/output/clob/account.rs | 5 +- src/output/clob/books.rs | 33 +-- src/output/clob/markets.rs | 8 +- src/output/clob/mod.rs | 3 - src/output/comments.rs | 28 ++- src/output/data.rs | 10 +- src/output/events.rs | 29 ++- src/output/markets.rs | 29 ++- src/output/mod.rs | 51 +++-- src/output/profiles.rs | 8 +- src/output/series.rs | 29 ++- src/output/sports.rs | 70 +++--- src/output/tags.rs | 48 +++-- 32 files changed, 464 insertions(+), 741 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 23a4756..a5cf34d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -26,7 +26,7 @@ fn parse_signature_type(s: &str) -> SignatureType { pub fn resolve_signer( private_key: Option<&str>, ) -> Result { - let (key, _) = config::resolve_key(private_key); + let (key, _) = config::resolve_key(private_key)?; let key = key.ok_or_else(|| anyhow::anyhow!("{}", config::NO_WALLET_MSG))?; LocalSigner::from_str(&key) .context("Invalid private key") @@ -45,7 +45,7 @@ pub async fn authenticate_with_signer( signer: &(impl polymarket_client_sdk::auth::Signer + Sync), signature_type_flag: Option<&str>, ) -> Result>> { - let sig_type = parse_signature_type(&config::resolve_signature_type(signature_type_flag)); + let sig_type = parse_signature_type(&config::resolve_signature_type(signature_type_flag)?); clob::Client::default() .authentication_builder(signer) @@ -65,7 +65,7 @@ pub async fn create_readonly_provider() -> Result, ) -> Result { - let (key, _) = config::resolve_key(private_key); + let (key, _) = config::resolve_key(private_key)?; let key = key.ok_or_else(|| anyhow::anyhow!("{}", config::NO_WALLET_MSG))?; let signer = LocalSigner::from_str(&key) .context("Invalid private key")? diff --git a/src/commands/approve.rs b/src/commands/approve.rs index b27cb92..b52dc0c 100644 --- a/src/commands/approve.rs +++ b/src/commands/approve.rs @@ -12,7 +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. +/// Polygon USDC (same address as `USDC_ADDRESS_STR`; `address!` requires a literal). const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"); sol! { diff --git a/src/commands/clob.rs b/src/commands/clob.rs index 1bfba6f..878a576 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -397,7 +397,14 @@ pub enum CliSide { Sell, } -super::enum_from!(CliSide => Side { Buy, Sell }); +impl From for Side { + fn from(v: CliSide) -> Self { + match v { + CliSide::Buy => Side::Buy, + CliSide::Sell => Side::Sell, + } + } +} #[derive(Clone, Debug, clap::ValueEnum)] pub enum CliInterval { @@ -414,7 +421,18 @@ pub enum CliInterval { Max, } -super::enum_from!(CliInterval => Interval { OneMinute, OneHour, SixHours, OneDay, OneWeek, Max }); +impl From for Interval { + fn from(v: CliInterval) -> Self { + match v { + CliInterval::OneMinute => Interval::OneMinute, + CliInterval::OneHour => Interval::OneHour, + CliInterval::SixHours => Interval::SixHours, + CliInterval::OneDay => Interval::OneDay, + CliInterval::OneWeek => Interval::OneWeek, + CliInterval::Max => Interval::Max, + } + } +} #[derive(Clone, Debug, clap::ValueEnum)] pub enum CliOrderType { @@ -445,7 +463,14 @@ pub enum CliAssetType { Conditional, } -super::enum_from!(CliAssetType => AssetType { Collateral, Conditional }); +impl From for AssetType { + fn from(v: CliAssetType) -> Self { + match v { + CliAssetType::Collateral => AssetType::Collateral, + CliAssetType::Conditional => AssetType::Conditional, + } + } +} fn parse_token_id(s: &str) -> Result { U256::from_str(s).map_err(|_| anyhow::anyhow!("Invalid token ID: {s}")) @@ -460,83 +485,22 @@ fn parse_date(s: &str) -> Result { .map_err(|_| anyhow::anyhow!("Invalid date: expected YYYY-MM-DD format")) } +#[allow(clippy::too_many_lines)] pub async fn execute( args: ClobArgs, output: OutputFormat, private_key: Option<&str>, signature_type: Option<&str>, ) -> Result<()> { - match args.command { - // Unauthenticated read commands - 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 => execute_read(args.command, &output).await, - - // Authenticated trading commands - 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 { .. } => { - execute_trade(args.command, &output, private_key, signature_type).await - } + // Unauthenticated client — cheap to construct, used by read commands and CreateApiKey. + let unauth = clob::Client::default(); + let output = &output; - // Authenticated reward commands - ClobCommand::Rewards { .. } - | ClobCommand::Earnings { .. } - | ClobCommand::EarningsMarkets { .. } - | ClobCommand::RewardPercentages - | ClobCommand::CurrentRewards { .. } - | ClobCommand::MarketReward { .. } - | ClobCommand::OrderScoring { .. } - | ClobCommand::OrdersScoring { .. } => { - execute_rewards(args.command, &output, private_key, signature_type).await - } - - // Account management commands - ClobCommand::ApiKeys - | ClobCommand::DeleteApiKey - | ClobCommand::CreateApiKey - | ClobCommand::AccountStatus => { - execute_account(args.command, &output, private_key, signature_type).await - } - } -} - -async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> { - let client = clob::Client::default(); + match args.command { + // ── Unauthenticated read commands ──────────────────────────────── - match command { ClobCommand::Ok => { - let result = client.ok().await?; + let result = unauth.ok().await?; print_ok(&result, output)?; } @@ -545,7 +509,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .token_id(parse_token_id(&token_id)?) .side(Side::from(side)) .build(); - let result = client.price(&request).await?; + let result = unauth.price(&request).await?; print_price(&result, output)?; } @@ -559,7 +523,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .build() }) .collect(); - let result = client.prices(&requests).await?; + let result = unauth.prices(&requests).await?; print_batch_prices(&result, output)?; } @@ -567,7 +531,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> let request = MidpointRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); - let result = client.midpoint(&request).await?; + let result = unauth.midpoint(&request).await?; print_midpoint(&result, output)?; } @@ -576,7 +540,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .into_iter() .map(|id| MidpointRequest::builder().token_id(id).build()) .collect(); - let result = client.midpoints(&requests).await?; + let result = unauth.midpoints(&requests).await?; print_midpoints(&result, output)?; } @@ -585,7 +549,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .token_id(parse_token_id(&token_id)?) .maybe_side(side.map(Side::from)) .build(); - let result = client.spread(&request).await?; + let result = unauth.spread(&request).await?; print_spread(&result, output)?; } @@ -594,7 +558,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .into_iter() .map(|id| SpreadRequest::builder().token_id(id).build()) .collect(); - let result = client.spreads(&requests).await?; + let result = unauth.spreads(&requests).await?; print_spreads(&result, output)?; } @@ -602,7 +566,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> let request = OrderBookSummaryRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); - let result = client.order_book(&request).await?; + let result = unauth.order_book(&request).await?; print_order_book(&result, output)?; } @@ -611,7 +575,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .into_iter() .map(|id| OrderBookSummaryRequest::builder().token_id(id).build()) .collect(); - let result = client.order_books(&requests).await?; + let result = unauth.order_books(&requests).await?; print_order_books(&result, output)?; } @@ -619,7 +583,7 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> let request = LastTradePriceRequest::builder() .token_id(parse_token_id(&token_id)?) .build(); - let result = client.last_trade_price(&request).await?; + let result = unauth.last_trade_price(&request).await?; print_last_trade(&result, output)?; } @@ -628,47 +592,47 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .into_iter() .map(|id| LastTradePriceRequest::builder().token_id(id).build()) .collect(); - let result = client.last_trades_prices(&requests).await?; + let result = unauth.last_trades_prices(&requests).await?; print_last_trades_prices(&result, output)?; } ClobCommand::Market { condition_id } => { - let result = client.market(&condition_id).await?; + let result = unauth.market(&condition_id).await?; print_clob_market(&result, output)?; } ClobCommand::Markets { cursor } => { - let result = client.markets(cursor).await?; + let result = unauth.markets(cursor).await?; print_clob_markets(&result, output)?; } ClobCommand::SamplingMarkets { cursor } => { - let result = client.sampling_markets(cursor).await?; + let result = unauth.sampling_markets(cursor).await?; print_clob_markets(&result, output)?; } ClobCommand::SimplifiedMarkets { cursor } => { - let result = client.simplified_markets(cursor).await?; + let result = unauth.simplified_markets(cursor).await?; print_simplified_markets(&result, output)?; } ClobCommand::SamplingSimpMarkets { cursor } => { - let result = client.sampling_simplified_markets(cursor).await?; + let result = unauth.sampling_simplified_markets(cursor).await?; print_simplified_markets(&result, output)?; } ClobCommand::TickSize { token_id } => { - let result = client.tick_size(parse_token_id(&token_id)?).await?; + let result = unauth.tick_size(parse_token_id(&token_id)?).await?; print_tick_size(&result, output)?; } ClobCommand::FeeRate { token_id } => { - let result = client.fee_rate_bps(parse_token_id(&token_id)?).await?; + let result = unauth.fee_rate_bps(parse_token_id(&token_id)?).await?; print_fee_rate(&result, output)?; } ClobCommand::NegRisk { token_id } => { - let result = client.neg_risk(parse_token_id(&token_id)?).await?; + let result = unauth.neg_risk(parse_token_id(&token_id)?).await?; print_neg_risk(&result, output)?; } @@ -682,81 +646,21 @@ async fn execute_read(command: ClobCommand, output: &OutputFormat) -> Result<()> .time_range(TimeRange::from_interval(Interval::from(interval))) .maybe_fidelity(fidelity) .build(); - let result = client.price_history(&request).await?; + let result = unauth.price_history(&request).await?; print_price_history(&result, output)?; } ClobCommand::Time => { - let result = client.server_time().await?; + let result = unauth.server_time().await?; print_server_time(result, output)?; } ClobCommand::Geoblock => { - let result = client.check_geoblock().await?; + let result = unauth.check_geoblock().await?; print_geoblock(&result, output)?; } - // 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(()) -} - -async fn execute_trade( - command: ClobCommand, - output: &OutputFormat, - 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 request = OrdersRequest::builder() - .maybe_market(market) - .maybe_asset_id(asset) - .build(); - let result = client.orders(&request, cursor).await?; - print_orders(&result, output)?; - } - - ClobCommand::Order { order_id } => { - let result = client.order(&order_id).await?; - print_order_detail(&result, output)?; - } + // ── Authenticated trading commands (need signer for order signing) ── ClobCommand::CreateOrder { token, @@ -766,6 +670,9 @@ 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 = @@ -793,6 +700,9 @@ 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(); @@ -837,6 +747,9 @@ 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); @@ -859,23 +772,49 @@ async fn execute_trade( print_post_order_result(&result, output)?; } + // ── Authenticated trading commands (no signer needed) ─────────── + + 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) + .build(); + let result = client.orders(&request, cursor).await?; + print_orders(&result, output)?; + } + + 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)?; + } + 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) @@ -889,6 +828,7 @@ 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) @@ -898,6 +838,7 @@ async fn execute_trade( } ClobCommand::Balance { asset_type, token } => { + let client = auth::authenticated_clob_client(private_key, signature_type).await?; let is_collateral = matches!(asset_type, CliAssetType::Collateral); let request = BalanceAllowanceRequest::builder() .asset_type(AssetType::from(asset_type)) @@ -908,6 +849,7 @@ 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) @@ -922,11 +864,13 @@ 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() @@ -941,58 +885,10 @@ async fn execute_trade( } } - // 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(()) -} + // ── Authenticated reward commands ──────────────────────────────── -async fn execute_rewards( - command: ClobCommand, - output: &OutputFormat, - 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?; @@ -1000,6 +896,7 @@ 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?; @@ -1007,6 +904,7 @@ 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(); @@ -1017,11 +915,13 @@ 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)?; } @@ -1030,148 +930,49 @@ 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)?; } - // 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(()) -} + // ── Account management commands ────────────────────────────────── -async fn execute_account( - command: ClobCommand, - output: &OutputFormat, - 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?; + ClobCommand::CreateApiKey => { + let signer = auth::resolve_signer(private_key)?; + let result = unauth.create_or_derive_api_key(&signer, None).await?; + print_create_api_key(&result, output)?; + } - 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::AccountStatus => { + let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client.closed_only_mode().await?; print_account_status(&result, output)?; } - - // 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 08f275e..e1b2016 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -1,5 +1,5 @@ +use crate::output::OutputFormat; use crate::output::comments::{print_comment_detail, print_comments_table}; -use crate::output::{OutputFormat, print_json}; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{ @@ -81,8 +81,15 @@ pub enum EntityType { Series, } - -super::enum_from!(EntityType => ParentEntityType { Event, Market, Series }); +impl From for ParentEntityType { + fn from(v: EntityType) -> Self { + match v { + EntityType::Event => ParentEntityType::Event, + EntityType::Market => ParentEntityType::Market, + EntityType::Series => ParentEntityType::Series, + } + } +} pub async fn execute( client: &gamma::Client, @@ -108,11 +115,7 @@ pub async fn execute( .build(); let comments = client.comments(&request).await?; - - match output { - OutputFormat::Table => print_comments_table(&comments), - OutputFormat::Json => print_json(&comments)?, - } + print_comments_table(&comments, &output)?; } CommentsCommand::Get { id } => { @@ -123,10 +126,7 @@ pub async fn execute( anyhow::bail!("Comment not found"); }; - match output { - OutputFormat::Table => print_comment_detail(comment), - OutputFormat::Json => print_json(&comment)?, - } + print_comment_detail(comment, &output)?; } CommentsCommand::ByUser { @@ -145,11 +145,7 @@ pub async fn execute( .build(); let comments = client.comments_by_user_address(&request).await?; - - match output { - OutputFormat::Table => print_comments_table(&comments), - OutputFormat::Json => print_json(&comments)?, - } + print_comments_table(&comments, &output)?; } } diff --git a/src/commands/ctf.rs b/src/commands/ctf.rs index d3c63f8..5d06ea5 100644 --- a/src/commands/ctf.rs +++ b/src/commands/ctf.rs @@ -13,9 +13,7 @@ use crate::auth; use crate::output::OutputFormat; 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; +use super::{USDC_ADDRESS_STR, USDC_DECIMALS}; #[derive(Args)] pub struct CtfArgs { @@ -121,7 +119,8 @@ pub enum CtfCommand { } fn usdc_to_raw(val: Decimal) -> Result { - let raw = val * USDC_DECIMALS; + let multiplier = Decimal::from(10u64.pow(USDC_DECIMALS)); + let raw = val * multiplier; anyhow::ensure!( raw.fract().is_zero(), "Amount {val} exceeds USDC precision (max 6 decimal places)" diff --git a/src/commands/data.rs b/src/commands/data.rs index eb51f3a..c39ae1a 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -164,7 +164,16 @@ pub enum TimePeriod { All, } -super::enum_from!(TimePeriod => polymarket_client_sdk::data::types::TimePeriod { Day, Week, Month, All }); +impl From for polymarket_client_sdk::data::types::TimePeriod { + fn from(v: TimePeriod) -> Self { + match v { + TimePeriod::Day => Self::Day, + TimePeriod::Week => Self::Week, + TimePeriod::Month => Self::Month, + TimePeriod::All => Self::All, + } + } +} #[derive(Clone, Debug, clap::ValueEnum)] pub enum OrderBy { @@ -172,38 +181,17 @@ pub enum OrderBy { 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 { - // User-focused queries (positions, trades, activity, value) - DataCommand::Positions { .. } - | DataCommand::ClosedPositions { .. } - | DataCommand::Value { .. } - | DataCommand::Traded { .. } - | DataCommand::Trades { .. } - | DataCommand::Activity { .. } => execute_user(client, args.command, &output).await, - - // Market-focused queries (holders, open interest, volume) - DataCommand::Holders { .. } - | DataCommand::OpenInterest { .. } - | DataCommand::Volume { .. } => execute_market(client, args.command, &output).await, - - // Leaderboard queries - DataCommand::Leaderboard { .. } - | DataCommand::BuilderLeaderboard { .. } - | DataCommand::BuilderVolume { .. } => { - execute_leaderboard(client, args.command, &output).await +impl From for polymarket_client_sdk::data::types::LeaderboardOrderBy { + fn from(v: OrderBy) -> Self { + match v { + OrderBy::Pnl => Self::Pnl, + OrderBy::Vol => Self::Vol, } } } -async fn execute_user( - client: &data::Client, - command: DataCommand, - output: &OutputFormat, -) -> Result<()> { - match command { +pub async fn execute(client: &data::Client, args: DataArgs, output: OutputFormat) -> Result<()> { + match args.command { DataCommand::Positions { address, limit, @@ -216,7 +204,7 @@ async fn execute_user( .build(); let positions = client.positions(&request).await?; - print_positions(&positions, output)?; + print_positions(&positions, &output)?; } DataCommand::ClosedPositions { @@ -231,21 +219,21 @@ async fn execute_user( .build(); let positions = client.closed_positions(&request).await?; - print_closed_positions(&positions, output)?; + print_closed_positions(&positions, &output)?; } DataCommand::Value { address } => { let request = ValueRequest::builder().user(address).build(); let values = client.value(&request).await?; - print_value(&values, output)?; + print_value(&values, &output)?; } DataCommand::Traded { address } => { let request = TradedRequest::builder().user(address).build(); let traded = client.traded(&request).await?; - print_traded(&traded, output)?; + print_traded(&traded, &output)?; } DataCommand::Trades { @@ -260,7 +248,7 @@ async fn execute_user( .build(); let trades = client.trades(&request).await?; - print_trades(&trades, output)?; + print_trades(&trades, &output)?; } DataCommand::Activity { @@ -275,28 +263,9 @@ async fn execute_user( .build(); let activity = client.activity(&request).await?; - print_activity(&activity, output)?; + print_activity(&activity, &output)?; } - DataCommand::Holders { .. } - | DataCommand::OpenInterest { .. } - | DataCommand::Volume { .. } - | DataCommand::Leaderboard { .. } - | DataCommand::BuilderLeaderboard { .. } - | DataCommand::BuilderVolume { .. } => { - unreachable!("execute() routes market/leaderboard commands to other handlers") - } - } - - Ok(()) -} - -async fn execute_market( - client: &data::Client, - command: DataCommand, - output: &OutputFormat, -) -> Result<()> { - match command { DataCommand::Holders { market, limit } => { let request = HoldersRequest::builder() .markets(vec![market]) @@ -304,44 +273,22 @@ async fn execute_market( .build(); let holders = client.holders(&request).await?; - print_holders(&holders, output)?; + print_holders(&holders, &output)?; } DataCommand::OpenInterest { market } => { let request = OpenInterestRequest::builder().markets(vec![market]).build(); let oi = client.open_interest(&request).await?; - print_open_interest(&oi, output)?; + print_open_interest(&oi, &output)?; } DataCommand::Volume { id } => { let request = LiveVolumeRequest::builder().id(id).build(); let volume = client.live_volume(&request).await?; - print_live_volume(&volume, output)?; + print_live_volume(&volume, &output)?; } - 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(()) -} - -async fn execute_leaderboard( - client: &data::Client, - command: DataCommand, - output: &OutputFormat, -) -> Result<()> { - match command { DataCommand::Leaderboard { period, order_by, @@ -356,7 +303,7 @@ async fn execute_leaderboard( .build(); let entries = client.leaderboard(&request).await?; - print_leaderboard(&entries, output)?; + print_leaderboard(&entries, &output)?; } DataCommand::BuilderLeaderboard { @@ -371,7 +318,7 @@ async fn execute_leaderboard( .build(); let entries = client.builder_leaderboard(&request).await?; - print_builder_leaderboard(&entries, output)?; + print_builder_leaderboard(&entries, &output)?; } DataCommand::BuilderVolume { period } => { @@ -380,19 +327,7 @@ async fn execute_leaderboard( .build(); let entries = client.builder_volume(&request).await?; - print_builder_volume(&entries, output)?; - } - - 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") + print_builder_volume(&entries, &output)?; } } diff --git a/src/commands/events.rs b/src/commands/events.rs index a888524..56adf2a 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -6,9 +6,9 @@ use polymarket_client_sdk::gamma::{ }; use super::is_numeric_id; +use crate::output::OutputFormat; use crate::output::events::{print_event_detail, print_events_table}; use crate::output::tags::print_tags_table; -use crate::output::{OutputFormat, print_json}; #[derive(Args)] pub struct EventsArgs { @@ -81,15 +81,12 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .maybe_offset(offset) .ascending(ascending) .maybe_tag_slug(tag) + // EventsRequest::order is Vec; into_iter on Option yields 0 or 1 items. .order(order.into_iter().collect()) .build(); let events = client.events(&request).await?; - - match output { - OutputFormat::Table => print_events_table(&events), - OutputFormat::Json => print_json(&events)?, - } + print_events_table(&events, &output)?; } EventsCommand::Get { id } => { @@ -102,20 +99,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor client.event_by_slug(&req).await? }; - match output { - OutputFormat::Table => print_event_detail(&event), - OutputFormat::Json => print_json(&event)?, - } + print_event_detail(&event, &output)?; } EventsCommand::Tags { id } => { let req = EventTagsRequest::builder().id(id).build(); let tags = client.event_tags(&req).await?; - match output { - OutputFormat::Table => print_tags_table(&tags), - OutputFormat::Json => print_json(&tags)?, - } + print_tags_table(&tags, &output)?; } } diff --git a/src/commands/markets.rs b/src/commands/markets.rs index bb0f311..404ae0b 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -12,9 +12,9 @@ use polymarket_client_sdk::gamma::{ }; use super::is_numeric_id; +use crate::output::OutputFormat; use crate::output::markets::{print_market_detail, print_markets_table}; use crate::output::tags::print_tags_table; -use crate::output::{OutputFormat, print_json}; #[derive(Args)] pub struct MarketsArgs { @@ -99,11 +99,7 @@ pub async fn execute( .build(); let markets = client.markets(&request).await?; - - match output { - OutputFormat::Table => print_markets_table(&markets), - OutputFormat::Json => print_json(&markets)?, - } + print_markets_table(&markets, &output)?; } MarketsCommand::Get { id } => { @@ -116,10 +112,7 @@ pub async fn execute( client.market_by_slug(&req).await? }; - match output { - OutputFormat::Table => print_market_detail(&market), - OutputFormat::Json => print_json(&market)?, - } + print_market_detail(&market, &output)?; } MarketsCommand::Search { query, limit } => { @@ -137,20 +130,14 @@ pub async fn execute( .flat_map(|e| e.markets.unwrap_or_default()) .collect(); - match output { - OutputFormat::Table => print_markets_table(&markets), - OutputFormat::Json => print_json(&markets)?, - } + print_markets_table(&markets, &output)?; } MarketsCommand::Tags { id } => { let req = MarketTagsRequest::builder().id(id).build(); let tags = client.market_tags(&req).await?; - match output { - OutputFormat::Table => print_tags_table(&tags), - OutputFormat::Json => print_json(&tags)?, - } + print_tags_table(&tags, &output)?; } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c4ee0ac..d4c985b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,36 +1,23 @@ -/// Polygon USDC contract address (shared across ctf and approve commands). -pub const USDC_ADDRESS_STR: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; - -pub mod approve; -pub mod bridge; -pub mod clob; -pub mod comments; -pub mod ctf; -pub mod data; -pub mod events; -pub mod markets; -pub mod profiles; -pub mod series; -pub mod setup; -pub mod sports; -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 { +pub(crate) const USDC_ADDRESS_STR: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +pub(crate) const USDC_DECIMALS: u32 = 6; + +pub(crate) mod approve; +pub(crate) mod bridge; +pub(crate) mod clob; +pub(crate) mod comments; +pub(crate) mod ctf; +pub(crate) mod data; +pub(crate) mod events; +pub(crate) mod markets; +pub(crate) mod profiles; +pub(crate) mod series; +pub(crate) mod setup; +pub(crate) mod sports; +pub(crate) mod tags; +pub(crate) mod upgrade; +pub(crate) mod wallet; + +pub(crate) fn is_numeric_id(id: &str) -> bool { id.parse::().is_ok() } diff --git a/src/commands/profiles.rs b/src/commands/profiles.rs index f3964fc..5d456c6 100644 --- a/src/commands/profiles.rs +++ b/src/commands/profiles.rs @@ -1,5 +1,5 @@ +use crate::output::OutputFormat; 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}; @@ -30,10 +30,7 @@ pub async fn execute( let req = PublicProfileRequest::builder().address(address).build(); let profile = client.public_profile(&req).await?; - match output { - OutputFormat::Table => print_profile_detail(&profile), - OutputFormat::Json => print_json(&profile)?, - } + print_profile_detail(&profile, &output)?; } } diff --git a/src/commands/series.rs b/src/commands/series.rs index e8df5b8..c0c29dd 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -5,8 +5,8 @@ use polymarket_client_sdk::gamma::{ types::request::{SeriesByIdRequest, SeriesListRequest}, }; +use crate::output::OutputFormat; use crate::output::series::{print_series_detail, print_series_table}; -use crate::output::{OutputFormat, print_json}; #[derive(Args)] pub struct SeriesArgs { @@ -64,21 +64,14 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .build(); let series = client.series(&request).await?; - - match output { - OutputFormat::Table => print_series_table(&series), - OutputFormat::Json => print_json(&series)?, - } + print_series_table(&series, &output)?; } SeriesCommand::Get { id } => { let req = SeriesByIdRequest::builder().id(id).build(); let series = client.series_by_id(&req).await?; - match output { - OutputFormat::Table => print_series_detail(&series), - OutputFormat::Json => print_json(&series)?, - } + print_series_detail(&series, &output)?; } } diff --git a/src/commands/setup.rs b/src/commands/setup.rs index e9b8c47..c0d32be 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -84,7 +84,7 @@ pub fn execute() -> Result<()> { step_header(1, total, "Wallet"); let address = if config::config_exists() { - let (key, source) = config::resolve_key(None); + let (key, source) = config::resolve_key(None)?; if let Some(k) = &key && let Ok(signer) = LocalSigner::from_str(k) { diff --git a/src/commands/sports.rs b/src/commands/sports.rs index c0ad9de..e075eb2 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -2,8 +2,8 @@ use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{self, types::request::TeamsRequest}; +use crate::output::OutputFormat; use crate::output::sports::{print_sport_types, print_sports_table, print_teams_table}; -use crate::output::{OutputFormat, print_json}; #[derive(Args)] pub struct SportsArgs { @@ -47,20 +47,12 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor match args.command { SportsCommand::List => { let sports = client.sports().await?; - - match output { - OutputFormat::Table => print_sports_table(&sports), - OutputFormat::Json => print_json(&sports)?, - } + print_sports_table(&sports, &output)?; } SportsCommand::MarketTypes => { let types = client.sports_market_types().await?; - - match output { - OutputFormat::Table => print_sport_types(&types), - OutputFormat::Json => print_json(&types)?, - } + print_sport_types(&types, &output)?; } SportsCommand::Teams { @@ -79,11 +71,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .build(); let teams = client.teams(&request).await?; - - match output { - OutputFormat::Table => print_teams_table(&teams), - OutputFormat::Json => print_json(&teams)?, - } + print_teams_table(&teams, &output)?; } } diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 0742900..474350c 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -9,8 +9,8 @@ use polymarket_client_sdk::gamma::{ }; use super::is_numeric_id; +use crate::output::OutputFormat; use crate::output::tags::{print_related_tags_table, print_tag_detail, print_tags_table}; -use crate::output::{OutputFormat, print_json}; #[derive(Args)] pub struct TagsArgs { @@ -76,11 +76,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma .build(); let tags = client.tags(&request).await?; - - match output { - OutputFormat::Table => print_tags_table(&tags), - OutputFormat::Json => print_json(&tags)?, - } + print_tags_table(&tags, &output)?; } TagsCommand::Get { id } => { @@ -93,10 +89,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.tag_by_slug(&req).await? }; - match output { - OutputFormat::Table => print_tag_detail(&tag), - OutputFormat::Json => print_json(&tag)?, - } + print_tag_detail(&tag, &output)?; } TagsCommand::Related { id, omit_empty } => { @@ -115,10 +108,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.related_tags_by_slug(&req).await? }; - match output { - OutputFormat::Table => print_related_tags_table(&related), - OutputFormat::Json => print_json(&related)?, - } + print_related_tags_table(&related, &output)?; } TagsCommand::RelatedTags { id, omit_empty } => { @@ -137,10 +127,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.tags_related_to_tag_by_slug(&req).await? }; - match output { - OutputFormat::Table => print_tags_table(&tags), - OutputFormat::Json => print_json(&tags)?, - } + print_tags_table(&tags, &output)?; } } diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index a28cc1c..b85fd61 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -53,7 +53,7 @@ pub enum WalletCommand { pub fn execute( args: WalletArgs, - output: &OutputFormat, + output: OutputFormat, private_key_flag: Option<&str>, ) -> Result<()> { match args.command { @@ -82,7 +82,6 @@ fn guard_overwrite(force: bool) -> Result<()> { Ok(()) } -/// 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); @@ -93,7 +92,7 @@ pub(crate) fn signer_key_hex(signer: &PrivateKeySigner) -> String { hex } -fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Result<()> { +fn cmd_create(output: OutputFormat, force: bool, signature_type: &str) -> Result<()> { guard_overwrite(force)?; let signer = LocalSigner::random().with_chain_id(Some(POLYGON)); @@ -132,7 +131,7 @@ fn cmd_create(output: &OutputFormat, force: bool, signature_type: &str) -> Resul Ok(()) } -fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &str) -> Result<()> { +fn cmd_import(key: &str, output: OutputFormat, force: bool, signature_type: &str) -> Result<()> { guard_overwrite(force)?; let signer = LocalSigner::from_str(key) @@ -170,8 +169,8 @@ fn cmd_import(key: &str, output: &OutputFormat, force: bool, signature_type: &st Ok(()) } -fn cmd_address(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> { - let (key, _) = config::resolve_key(private_key_flag); +fn cmd_address(output: OutputFormat, private_key_flag: Option<&str>) -> Result<()> { + let (key, _) = config::resolve_key(private_key_flag)?; let key = key.ok_or_else(|| anyhow::anyhow!("{}", config::NO_WALLET_MSG))?; let signer = LocalSigner::from_str(&key).context("Invalid private key")?; @@ -188,8 +187,8 @@ fn cmd_address(output: &OutputFormat, private_key_flag: Option<&str>) -> Result< Ok(()) } -fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> { - let (key, source) = config::resolve_key(private_key_flag); +fn cmd_show(output: OutputFormat, private_key_flag: Option<&str>) -> Result<()> { + let (key, source) = config::resolve_key(private_key_flag)?; let signer = key.as_deref().and_then(|k| LocalSigner::from_str(k).ok()); let address = signer.as_ref().map(|s| s.address().to_string()); let proxy_addr = signer @@ -197,7 +196,7 @@ fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> .and_then(|s| derive_proxy_wallet(s.address(), POLYGON)) .map(|a| a.to_string()); - let sig_type = config::resolve_signature_type(None); + let sig_type = config::resolve_signature_type(None)?; let config_path = config::config_path()?; match output { @@ -230,7 +229,7 @@ fn cmd_show(output: &OutputFormat, private_key_flag: Option<&str>) -> Result<()> Ok(()) } -fn cmd_reset(output: &OutputFormat, force: bool) -> Result<()> { +fn cmd_reset(output: OutputFormat, force: bool) -> Result<()> { if !config::config_exists() { match output { OutputFormat::Table => println!("Nothing to reset. No config found."), diff --git a/src/config.rs b/src/config.rs index d2f5395..cf3a830 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize}; const ENV_VAR: &str = "POLYMARKET_PRIVATE_KEY"; const SIG_TYPE_ENV_VAR: &str = "POLYMARKET_SIGNATURE_TYPE"; -pub const DEFAULT_SIGNATURE_TYPE: &str = "proxy"; +pub(crate) const DEFAULT_SIGNATURE_TYPE: &str = "proxy"; -pub const NO_WALLET_MSG: &str = +pub(crate) const NO_WALLET_MSG: &str = "No wallet configured. Run `polymarket wallet create` or `polymarket wallet import `"; #[derive(Serialize, Deserialize)] -pub struct Config { +pub(crate) struct Config { pub private_key: String, pub chain_id: u64, #[serde(default = "default_signature_type")] @@ -23,7 +23,7 @@ fn default_signature_type() -> String { DEFAULT_SIGNATURE_TYPE.to_string() } -pub enum KeySource { +pub(crate) enum KeySource { Flag, EnvVar, ConfigFile, @@ -62,26 +62,38 @@ pub fn delete_config() -> Result<()> { Ok(()) } -pub fn load_config() -> Option { - let path = config_path().ok()?; - let data = fs::read_to_string(path).ok()?; - serde_json::from_str(&data).ok() +/// Load config from disk. Returns `Ok(None)` if no config file exists, +/// or `Err` if the file exists but can't be read or parsed. +pub fn load_config() -> Result> { + let path = config_path()?; + let data = match fs::read_to_string(&path) { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => { + return Err( + anyhow::anyhow!(e).context(format!("Failed to read {}", path.display())) + ) + } + }; + let config = serde_json::from_str(&data) + .context(format!("Invalid JSON in config file {}", path.display()))?; + Ok(Some(config)) } /// Priority: CLI flag > env var > config file > default ("proxy"). -pub fn resolve_signature_type(cli_flag: Option<&str>) -> String { +pub fn resolve_signature_type(cli_flag: Option<&str>) -> Result { if let Some(st) = cli_flag { - return st.to_string(); + return Ok(st.to_string()); } if let Ok(st) = std::env::var(SIG_TYPE_ENV_VAR) && !st.is_empty() { - return st; + return Ok(st); } - if let Some(config) = load_config() { - return config.signature_type; + if let Some(config) = load_config()? { + return Ok(config.signature_type); } - DEFAULT_SIGNATURE_TYPE.to_string() + Ok(DEFAULT_SIGNATURE_TYPE.to_string()) } pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> { @@ -126,19 +138,19 @@ pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> } /// Priority: CLI flag > env var > config file. -pub fn resolve_key(cli_flag: Option<&str>) -> (Option, KeySource) { +pub fn resolve_key(cli_flag: Option<&str>) -> Result<(Option, KeySource)> { if let Some(key) = cli_flag { - return (Some(key.to_string()), KeySource::Flag); + return Ok((Some(key.to_string()), KeySource::Flag)); } if let Ok(key) = std::env::var(ENV_VAR) && !key.is_empty() { - return (Some(key), KeySource::EnvVar); + return Ok((Some(key), KeySource::EnvVar)); } - if let Some(config) = load_config() { - return (Some(config.private_key), KeySource::ConfigFile); + if let Some(config) = load_config()? { + return Ok((Some(config.private_key), KeySource::ConfigFile)); } - (None, KeySource::None) + Ok((None, KeySource::None)) } #[cfg(test)] @@ -161,7 +173,7 @@ mod tests { fn resolve_key_flag_overrides_env() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { set(ENV_VAR, "env_key") }; - let (key, source) = resolve_key(Some("flag_key")); + let (key, source) = resolve_key(Some("flag_key")).unwrap(); assert_eq!(key.unwrap(), "flag_key"); assert!(matches!(source, KeySource::Flag)); unsafe { unset(ENV_VAR) }; @@ -171,7 +183,7 @@ mod tests { fn resolve_key_env_var_returns_env_value() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { set(ENV_VAR, "env_key_value") }; - let (key, source) = resolve_key(None); + let (key, source) = resolve_key(None).unwrap(); assert_eq!(key.unwrap(), "env_key_value"); assert!(matches!(source, KeySource::EnvVar)); unsafe { unset(ENV_VAR) }; @@ -181,7 +193,7 @@ mod tests { fn resolve_key_skips_empty_env_var() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { set(ENV_VAR, "") }; - let (_, source) = resolve_key(None); + let (_, source) = resolve_key(None).unwrap(); assert!(!matches!(source, KeySource::EnvVar)); unsafe { unset(ENV_VAR) }; } @@ -190,7 +202,10 @@ mod tests { fn resolve_sig_type_flag_overrides_env() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { set(SIG_TYPE_ENV_VAR, "eoa") }; - assert_eq!(resolve_signature_type(Some("gnosis-safe")), "gnosis-safe"); + assert_eq!( + resolve_signature_type(Some("gnosis-safe")).unwrap(), + "gnosis-safe" + ); unsafe { unset(SIG_TYPE_ENV_VAR) }; } @@ -198,7 +213,7 @@ mod tests { fn resolve_sig_type_env_var_returns_env_value() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { set(SIG_TYPE_ENV_VAR, "eoa") }; - assert_eq!(resolve_signature_type(None), "eoa"); + assert_eq!(resolve_signature_type(None).unwrap(), "eoa"); unsafe { unset(SIG_TYPE_ENV_VAR) }; } @@ -206,7 +221,7 @@ mod tests { fn resolve_sig_type_without_env_returns_nonempty() { let _lock = ENV_LOCK.lock().unwrap(); unsafe { unset(SIG_TYPE_ENV_VAR) }; - let result = resolve_signature_type(None); + let result = resolve_signature_type(None).unwrap(); assert!(!result.is_empty()); } } diff --git a/src/main.rs b/src/main.rs index 05c9349..2abb55f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,7 +114,7 @@ pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { 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::wallet::execute(args, cli.output, cli.private_key.as_deref()) } Commands::Upgrade => commands::upgrade::execute(), Commands::Status => { diff --git a/src/output/approve.rs b/src/output/approve.rs index 618b897..ec133e6 100644 --- a/src/output/approve.rs +++ b/src/output/approve.rs @@ -1,6 +1,3 @@ -#![allow(clippy::exhaustive_enums, reason = "Generated by sol! macro")] -#![allow(clippy::exhaustive_structs, reason = "Generated by sol! macro")] - use alloy::primitives::U256; use anyhow::Result; use tabled::Tabled; diff --git a/src/output/bridge.rs b/src/output/bridge.rs index 8c03e6d..0fa82a6 100644 --- a/src/output/bridge.rs +++ b/src/output/bridge.rs @@ -1,5 +1,3 @@ -#![allow(clippy::items_after_statements)] - use polymarket_client_sdk::bridge::types::{ DepositResponse, DepositTransactionStatus, StatusResponse, SupportedAssetsResponse, }; @@ -7,7 +5,7 @@ use serde_json::json; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{OutputFormat, detail_field, format_decimal, print_detail_table}; +use super::{NONE, OutputFormat, detail_field, format_decimal, print_detail_table}; pub fn print_deposit(response: &DepositResponse, output: &OutputFormat) -> anyhow::Result<()> { match output { @@ -142,7 +140,7 @@ pub fn print_status(response: &StatusResponse, output: &OutputFormat) -> anyhow: tx_hash: tx .tx_hash .as_deref() - .map_or_else(|| "—".into(), |h| super::truncate(h, 14)), + .map_or_else(|| NONE.into(), |h| super::truncate(h, 14)), }) .collect(); let table = Table::new(rows).with(Style::rounded()).to_string(); diff --git a/src/output/clob/account.rs b/src/output/clob/account.rs index 596be78..dd170a9 100644 --- a/src/output/clob/account.rs +++ b/src/output/clob/account.rs @@ -53,15 +53,12 @@ pub fn print_geoblock(result: &GeoblockResponse, output: &OutputFormat) -> anyho 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 divisor = Decimal::from(10u64.pow(crate::commands::USDC_DECIMALS)); let human_balance = result.balance / divisor; match output { OutputFormat::Table => { diff --git a/src/output/clob/books.rs b/src/output/clob/books.rs index d8a00b1..b387db4 100644 --- a/src/output/clob/books.rs +++ b/src/output/clob/books.rs @@ -5,31 +5,7 @@ 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()), - }) -} +use crate::output::{NONE, OutputFormat, truncate}; pub fn print_order_book( result: &OrderBookSummaryResponse, @@ -43,7 +19,7 @@ pub fn print_order_book( "Last Trade: {}", result .last_trade_price - .map_or("—".into(), |p| p.to_string()) + .map_or(NONE.into(), |p| p.to_string()) ); println!(); @@ -90,7 +66,7 @@ pub fn print_order_book( } } OutputFormat::Json => { - crate::output::print_json(&order_book_to_json(result))?; + crate::output::print_json(result)?; } } Ok(()) @@ -114,8 +90,7 @@ pub fn print_order_books( } } OutputFormat::Json => { - let data: Vec<_> = result.iter().map(order_book_to_json).collect(); - crate::output::print_json(&data)?; + crate::output::print_json(result)?; } } Ok(()) diff --git a/src/output/clob/markets.rs b/src/output/clob/markets.rs index b98251c..46ac62d 100644 --- a/src/output/clob/markets.rs +++ b/src/output/clob/markets.rs @@ -7,7 +7,7 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::END_CURSOR; -use crate::output::{OutputFormat, truncate}; +use crate::output::{NONE, OutputFormat, truncate}; pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyhow::Result<()> { match output { @@ -18,7 +18,7 @@ pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyh ["Slug".into(), result.market_slug.clone()], [ "Condition ID".into(), - result.condition_id.map_or("—".into(), |c| c.to_string()), + result.condition_id.map_or(NONE.into(), |c| c.to_string()), ], ["Active".into(), result.active.to_string()], ["Closed".into(), result.closed.to_string()], @@ -34,7 +34,7 @@ pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyh ["Neg Risk".into(), result.neg_risk.to_string()], [ "End Date".into(), - result.end_date_iso.map_or("—".into(), |d| d.to_rfc3339()), + result.end_date_iso.map_or(NONE.into(), |d| d.to_rfc3339()), ], ]; for token in &result.tokens { @@ -128,7 +128,7 @@ pub fn print_simplified_markets( .map(|m| Row { condition_id: m .condition_id - .map_or("—".into(), |c| truncate(&c.to_string(), 14)), + .map_or(NONE.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(), diff --git a/src/output/clob/mod.rs b/src/output/clob/mod.rs index 83f3c3d..6651db3 100644 --- a/src/output/clob/mod.rs +++ b/src/output/clob/mod.rs @@ -1,5 +1,3 @@ -#![allow(clippy::items_after_statements)] - mod account; mod books; mod markets; @@ -9,7 +7,6 @@ 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::{ diff --git a/src/output/comments.rs b/src/output/comments.rs index 85a5188..72d0c36 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::{NONE, detail_field, format_date, print_detail_table, truncate}; +use super::{NONE, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate}; #[derive(Tabled)] struct CommentRow { @@ -42,17 +42,26 @@ fn comment_to_row(c: &Comment) -> CommentRow { } } -pub fn print_comments_table(comments: &[Comment]) { - if comments.is_empty() { - println!("No comments found."); - return; +pub fn print_comments_table(comments: &[Comment], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if comments.is_empty() { + println!("No comments found."); + return Ok(()); + } + let rows: Vec = comments.iter().map(comment_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(comments)?, } - let rows: Vec = comments.iter().map(comment_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } -pub fn print_comment_detail(c: &Comment) { +pub fn print_comment_detail(c: &Comment, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(c); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "ID", c.id.clone()); @@ -111,4 +120,5 @@ pub fn print_comment_detail(c: &Comment) { ); print_detail_table(rows); + Ok(()) } diff --git a/src/output/data.rs b/src/output/data.rs index 9f46955..ac31f67 100644 --- a/src/output/data.rs +++ b/src/output/data.rs @@ -1,5 +1,3 @@ -#![allow(clippy::items_after_statements)] - use polymarket_client_sdk::data::types::response::{ Activity, BuilderLeaderboardEntry, BuilderVolumeEntry, ClosedPosition, LiveVolume, Market, MetaHolder, OpenInterest, Position, Trade, Traded, TraderLeaderboardEntry, Value, @@ -8,7 +6,7 @@ use serde_json::json; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{OutputFormat, format_decimal, truncate}; +use super::{NONE, OutputFormat, format_decimal, truncate}; fn format_market(m: &Market) -> String { match m { @@ -272,7 +270,7 @@ pub fn print_activity(activity: &[Activity], output: &OutputFormat) -> anyhow::R .iter() .map(|a| Row { activity_type: a.activity_type.to_string(), - title: truncate(a.title.as_deref().unwrap_or("—"), 35), + title: truncate(a.title.as_deref().unwrap_or(NONE), 35), size: format!("{:.2}", a.size), usdc_size: format_decimal(a.usdc_size), tx: truncate(&a.transaction_hash.to_string(), 14), @@ -329,7 +327,7 @@ pub fn print_holders(meta_holders: &[MetaHolder], output: &OutputFormat) -> anyh .name .as_deref() .or(h.pseudonym.as_deref()) - .unwrap_or("—") + .unwrap_or(NONE) .into(), amount: format_decimal(h.amount), outcome_index: h.outcome_index.to_string(), @@ -471,7 +469,7 @@ pub fn print_leaderboard( .iter() .map(|e| Row { rank: e.rank.to_string(), - trader: truncate(e.user_name.as_deref().unwrap_or("—"), 20), + trader: truncate(e.user_name.as_deref().unwrap_or(NONE), 20), pnl: format_decimal(e.pnl), volume: format_decimal(e.vol), }) diff --git a/src/output/events.rs b/src/output/events.rs index 387d081..c423575 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -3,7 +3,8 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, + NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + print_detail_table, print_json, truncate, }; #[derive(Tabled)] @@ -36,18 +37,27 @@ fn event_to_row(e: &Event) -> EventRow { } } -pub fn print_events_table(events: &[Event]) { - if events.is_empty() { - println!("No events found."); - return; +pub fn print_events_table(events: &[Event], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if events.is_empty() { + println!("No events found."); + return Ok(()); + } + let rows: Vec = events.iter().map(event_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(events)?, } - let rows: Vec = events.iter().map(event_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } #[allow(clippy::too_many_lines)] -pub fn print_event_detail(e: &Event) { +pub fn print_event_detail(e: &Event, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(e); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "ID", e.id.clone()); @@ -159,6 +169,7 @@ pub fn print_event_detail(e: &Event) { ); print_detail_table(rows); + Ok(()) } #[cfg(test)] diff --git a/src/output/markets.rs b/src/output/markets.rs index 7324310..ca549d0 100644 --- a/src/output/markets.rs +++ b/src/output/markets.rs @@ -4,7 +4,8 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, + NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + print_detail_table, print_json, truncate, }; #[derive(Tabled)] @@ -41,17 +42,26 @@ fn market_to_row(m: &Market) -> MarketRow { } } -pub fn print_markets_table(markets: &[Market]) { - if markets.is_empty() { - println!("No markets found."); - return; +pub fn print_markets(markets: &[Market], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if markets.is_empty() { + println!("No markets found."); + return Ok(()); + } + let rows: Vec = markets.iter().map(market_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(markets)?, } - let rows: Vec = markets.iter().map(market_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } -pub fn print_market_detail(m: &Market) { +pub fn print_market(m: &Market, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(m); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "ID", m.id.clone()); @@ -154,6 +164,7 @@ pub fn print_market_detail(m: &Market) { ); print_detail_table(rows); + Ok(()) } #[cfg(test)] diff --git a/src/output/mod.rs b/src/output/mod.rs index 14a81b0..6d040bf 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,15 +1,15 @@ -pub mod approve; -pub mod bridge; -pub mod clob; -pub mod comments; -pub mod ctf; -pub mod data; -pub mod events; -pub mod markets; -pub mod profiles; -pub mod series; -pub mod sports; -pub mod tags; +pub(crate) mod approve; +pub(crate) mod bridge; +pub(crate) mod clob; +pub(crate) mod comments; +pub(crate) mod ctf; +pub(crate) mod data; +pub(crate) mod events; +pub(crate) mod markets; +pub(crate) mod profiles; +pub(crate) mod series; +pub(crate) mod sports; +pub(crate) mod tags; use chrono::{DateTime, Utc}; use polymarket_client_sdk::types::Decimal; @@ -18,11 +18,10 @@ 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 = "—"; +pub(crate) const NONE: &str = "—"; #[derive(Clone, Copy, Debug, clap::ValueEnum)] -pub enum OutputFormat { +pub(crate) enum OutputFormat { Table, Json, } @@ -38,12 +37,14 @@ pub(crate) fn truncate(s: &str, max: usize) -> 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) - } else if f >= 1_000.0 { - format!("${:.1}K", f / 1_000.0) + let abs = f.abs(); + let sign = if f < 0.0 { "-" } else { "" }; + if abs >= 1_000_000.0 { + format!("{sign}${:.1}M", abs / 1_000_000.0) + } else if abs >= 1_000.0 { + format!("{sign}${:.1}K", abs / 1_000.0) } else { - format!("${f:.2}") + format!("{sign}${abs:.2}") } } @@ -61,12 +62,11 @@ pub(crate) fn active_status(closed: Option, active: Option) -> &'sta } } -pub(crate) fn print_json(data: &impl serde::Serialize) -> anyhow::Result<()> { +pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(data)?); Ok(()) } -/// Print an error in the appropriate format for the current output mode. pub(crate) fn print_error(error: &anyhow::Error, format: OutputFormat) { match format { OutputFormat::Json => { @@ -173,7 +173,12 @@ mod tests { #[test] fn format_decimal_negative() { - assert_eq!(format_decimal(dec!(-500)), "$-500.00"); + assert_eq!(format_decimal(dec!(-500)), "-$500.00"); + } + + #[test] + fn format_decimal_negative_thousands() { + assert_eq!(format_decimal(dec!(-1_500)), "-$1.5K"); } #[test] diff --git a/src/output/profiles.rs b/src/output/profiles.rs index f5d4348..20feb06 100644 --- a/src/output/profiles.rs +++ b/src/output/profiles.rs @@ -1,8 +1,11 @@ use polymarket_client_sdk::gamma::types::response::PublicProfile; -use super::{detail_field, print_detail_table}; +use super::{OutputFormat, detail_field, print_detail_table, print_json}; -pub fn print_profile_detail(p: &PublicProfile) { +pub fn print_profile_detail(p: &PublicProfile, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(p); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "Name", p.name.clone().unwrap_or_default()); @@ -38,4 +41,5 @@ pub fn print_profile_detail(p: &PublicProfile) { ); print_detail_table(rows); + Ok(()) } diff --git a/src/output/series.rs b/src/output/series.rs index 3d2a5ac..0c27f24 100644 --- a/src/output/series.rs +++ b/src/output/series.rs @@ -3,7 +3,8 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, active_status, detail_field, format_date, format_decimal, print_detail_table, truncate, + NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + print_detail_table, print_json, truncate, }; #[derive(Tabled)] @@ -30,17 +31,26 @@ fn series_to_row(s: &Series) -> SeriesRow { } } -pub fn print_series_table(series: &[Series]) { - if series.is_empty() { - println!("No series found."); - return; +pub fn print_series_table(series: &[Series], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if series.is_empty() { + println!("No series found."); + return Ok(()); + } + let rows: Vec = series.iter().map(series_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(series)?, } - let rows: Vec = series.iter().map(series_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } -pub fn print_series_detail(s: &Series) { +pub fn print_series_detail(s: &Series, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(s); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "ID", s.id.clone()); @@ -107,4 +117,5 @@ pub fn print_series_detail(s: &Series) { ); print_detail_table(rows); + Ok(()) } diff --git a/src/output/sports.rs b/src/output/sports.rs index 41d0305..c9cce7b 100644 --- a/src/output/sports.rs +++ b/src/output/sports.rs @@ -4,7 +4,7 @@ use polymarket_client_sdk::gamma::types::response::{ use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::truncate; +use super::{NONE, OutputFormat, print_json, truncate}; #[derive(Tabled)] struct SportRow { @@ -27,24 +27,36 @@ fn sport_to_row(s: &SportsMetadata) -> SportRow { } } -pub fn print_sports_table(sports: &[SportsMetadata]) { - if sports.is_empty() { - println!("No sports found."); - return; +pub fn print_sports_table(sports: &[SportsMetadata], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if sports.is_empty() { + println!("No sports found."); + return Ok(()); + } + let rows: Vec = sports.iter().map(sport_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(sports)?, } - let rows: Vec = sports.iter().map(sport_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } -pub fn print_sport_types(types: &SportsMarketTypesResponse) { - if types.market_types.is_empty() { - println!("No market types found."); - return; +pub fn print_sport_types(types: &SportsMarketTypesResponse, output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if types.market_types.is_empty() { + println!("No market types found."); + return Ok(()); + } + let rows: Vec<[String; 1]> = types.market_types.iter().map(|t| [t.clone()]).collect(); + let table = Table::from_iter(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(types)?, } - let rows: Vec<[String; 1]> = types.market_types.iter().map(|t| [t.clone()]).collect(); - let table = Table::from_iter(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } #[derive(Tabled)] @@ -64,19 +76,25 @@ struct TeamRow { fn team_to_row(t: &Team) -> TeamRow { TeamRow { id: t.id.to_string(), - name: t.name.as_deref().unwrap_or("—").into(), - league: t.league.as_deref().unwrap_or("—").into(), - record: t.record.as_deref().unwrap_or("—").into(), - abbreviation: t.abbreviation.as_deref().unwrap_or("—").into(), + name: t.name.as_deref().unwrap_or(NONE).into(), + league: t.league.as_deref().unwrap_or(NONE).into(), + record: t.record.as_deref().unwrap_or(NONE).into(), + abbreviation: t.abbreviation.as_deref().unwrap_or(NONE).into(), } } -pub fn print_teams_table(teams: &[Team]) { - if teams.is_empty() { - println!("No teams found."); - return; +pub fn print_teams_table(teams: &[Team], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if teams.is_empty() { + println!("No teams found."); + return Ok(()); + } + let rows: Vec = teams.iter().map(team_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(teams)?, } - let rows: Vec = teams.iter().map(team_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } diff --git a/src/output/tags.rs b/src/output/tags.rs index 40d7b77..dafda68 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::{NONE, detail_field, format_date, print_detail_table, truncate}; +use super::{NONE, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate}; #[derive(Tabled)] struct TagRow { @@ -25,14 +25,20 @@ fn tag_to_row(t: &Tag) -> TagRow { } } -pub fn print_tags_table(tags: &[Tag]) { - if tags.is_empty() { - println!("No tags found."); - return; +pub fn print_tags_table(tags: &[Tag], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if tags.is_empty() { + println!("No tags found."); + return Ok(()); + } + let rows: Vec = tags.iter().map(tag_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(tags)?, } - let rows: Vec = tags.iter().map(tag_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } #[derive(Tabled)] @@ -56,18 +62,27 @@ fn related_tag_to_row(r: &RelatedTag) -> RelatedTagRow { } } -pub fn print_related_tags_table(tags: &[RelatedTag]) { - if tags.is_empty() { - println!("No related tags found."); - return; +pub fn print_related_tags_table(tags: &[RelatedTag], output: &OutputFormat) -> anyhow::Result<()> { + match output { + OutputFormat::Table => { + if tags.is_empty() { + println!("No related tags found."); + return Ok(()); + } + let rows: Vec = tags.iter().map(related_tag_to_row).collect(); + let table = Table::new(rows).with(Style::rounded()).to_string(); + println!("{table}"); + } + OutputFormat::Json => print_json(tags)?, } - let rows: Vec = tags.iter().map(related_tag_to_row).collect(); - let table = Table::new(rows).with(Style::rounded()).to_string(); - println!("{table}"); + Ok(()) } #[allow(clippy::vec_init_then_push)] -pub fn print_tag_detail(t: &Tag) { +pub fn print_tag_detail(t: &Tag, output: &OutputFormat) -> anyhow::Result<()> { + if matches!(output, OutputFormat::Json) { + return print_json(t); + } let mut rows: Vec<[String; 2]> = Vec::new(); detail_field!(rows, "ID", t.id.clone()); @@ -100,4 +115,5 @@ pub fn print_tag_detail(t: &Tag) { ); print_detail_table(rows); + Ok(()) } From ed8fdd80e713d74a088e64f2904a9910224cc451 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 13:23:56 +0530 Subject: [PATCH 09/13] rename output functions to match singular/plural convention --- src/commands/bridge.rs | 5 +++-- src/commands/comments.rs | 11 ++++++----- src/commands/data.rs | 13 +++++++------ src/commands/events.rs | 10 +++++----- src/commands/markets.rs | 12 ++++++------ src/commands/profiles.rs | 7 ++++--- src/commands/series.rs | 6 +++--- src/commands/sports.rs | 6 +++--- src/commands/tags.rs | 10 +++++----- src/output/comments.rs | 4 ++-- src/output/events.rs | 4 ++-- src/output/profiles.rs | 2 +- src/output/series.rs | 4 ++-- src/output/sports.rs | 4 ++-- src/output/tags.rs | 6 +++--- 15 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/commands/bridge.rs b/src/commands/bridge.rs index 20ab200..9c9d859 100644 --- a/src/commands/bridge.rs +++ b/src/commands/bridge.rs @@ -1,5 +1,3 @@ -use crate::output::OutputFormat; -use crate::output::bridge::{print_deposit, print_status, print_supported_assets}; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::bridge::{ @@ -7,6 +5,9 @@ use polymarket_client_sdk::bridge::{ types::{DepositRequest, StatusRequest}, }; +use crate::output::OutputFormat; +use crate::output::bridge::{print_deposit, print_status, print_supported_assets}; + #[derive(Args)] pub struct BridgeArgs { #[command(subcommand)] diff --git a/src/commands/comments.rs b/src/commands/comments.rs index e1b2016..4003911 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -1,5 +1,3 @@ -use crate::output::OutputFormat; -use crate::output::comments::{print_comment_detail, print_comments_table}; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{ @@ -10,6 +8,9 @@ use polymarket_client_sdk::gamma::{ }, }; +use crate::output::OutputFormat; +use crate::output::comments::{print_comment, print_comments}; + #[derive(Args)] pub struct CommentsArgs { #[command(subcommand)] @@ -115,7 +116,7 @@ pub async fn execute( .build(); let comments = client.comments(&request).await?; - print_comments_table(&comments, &output)?; + print_comments(&comments, &output)?; } CommentsCommand::Get { id } => { @@ -126,7 +127,7 @@ pub async fn execute( anyhow::bail!("Comment not found"); }; - print_comment_detail(comment, &output)?; + print_comment(comment, &output)?; } CommentsCommand::ByUser { @@ -145,7 +146,7 @@ pub async fn execute( .build(); let comments = client.comments_by_user_address(&request).await?; - print_comments_table(&comments, &output)?; + print_comments(&comments, &output)?; } } diff --git a/src/commands/data.rs b/src/commands/data.rs index c39ae1a..ac92d44 100644 --- a/src/commands/data.rs +++ b/src/commands/data.rs @@ -1,9 +1,3 @@ -use crate::output::OutputFormat; -use crate::output::data::{ - print_activity, print_builder_leaderboard, print_builder_volume, print_closed_positions, - print_holders, print_leaderboard, print_live_volume, print_open_interest, print_positions, - print_traded, print_trades, print_value, -}; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::data::{ @@ -16,6 +10,13 @@ use polymarket_client_sdk::data::{ }; 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, + print_holders, print_leaderboard, print_live_volume, print_open_interest, print_positions, + print_traded, print_trades, print_value, +}; + #[derive(Args)] pub struct DataArgs { #[command(subcommand)] diff --git a/src/commands/events.rs b/src/commands/events.rs index 56adf2a..d001210 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -7,8 +7,8 @@ use polymarket_client_sdk::gamma::{ use super::is_numeric_id; use crate::output::OutputFormat; -use crate::output::events::{print_event_detail, print_events_table}; -use crate::output::tags::print_tags_table; +use crate::output::events::{print_event, print_events}; +use crate::output::tags::print_tags; #[derive(Args)] pub struct EventsArgs { @@ -86,7 +86,7 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .build(); let events = client.events(&request).await?; - print_events_table(&events, &output)?; + print_events(&events, &output)?; } EventsCommand::Get { id } => { @@ -99,14 +99,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor client.event_by_slug(&req).await? }; - print_event_detail(&event, &output)?; + print_event(&event, &output)?; } EventsCommand::Tags { id } => { let req = EventTagsRequest::builder().id(id).build(); let tags = client.event_tags(&req).await?; - print_tags_table(&tags, &output)?; + print_tags(&tags, &output)?; } } diff --git a/src/commands/markets.rs b/src/commands/markets.rs index 404ae0b..2544d18 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -13,8 +13,8 @@ use polymarket_client_sdk::gamma::{ use super::is_numeric_id; use crate::output::OutputFormat; -use crate::output::markets::{print_market_detail, print_markets_table}; -use crate::output::tags::print_tags_table; +use crate::output::markets::{print_market, print_markets}; +use crate::output::tags::print_tags; #[derive(Args)] pub struct MarketsArgs { @@ -99,7 +99,7 @@ pub async fn execute( .build(); let markets = client.markets(&request).await?; - print_markets_table(&markets, &output)?; + print_markets(&markets, &output)?; } MarketsCommand::Get { id } => { @@ -112,7 +112,7 @@ pub async fn execute( client.market_by_slug(&req).await? }; - print_market_detail(&market, &output)?; + print_market(&market, &output)?; } MarketsCommand::Search { query, limit } => { @@ -130,14 +130,14 @@ pub async fn execute( .flat_map(|e| e.markets.unwrap_or_default()) .collect(); - print_markets_table(&markets, &output)?; + print_markets(&markets, &output)?; } MarketsCommand::Tags { id } => { let req = MarketTagsRequest::builder().id(id).build(); let tags = client.market_tags(&req).await?; - print_tags_table(&tags, &output)?; + print_tags(&tags, &output)?; } } diff --git a/src/commands/profiles.rs b/src/commands/profiles.rs index 5d456c6..7c95dfa 100644 --- a/src/commands/profiles.rs +++ b/src/commands/profiles.rs @@ -1,10 +1,11 @@ -use crate::output::OutputFormat; -use crate::output::profiles::print_profile_detail; use anyhow::Result; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{self, types::request::PublicProfileRequest}; use polymarket_client_sdk::types::Address; +use crate::output::OutputFormat; +use crate::output::profiles::print_profile; + #[derive(Args)] pub struct ProfilesArgs { #[command(subcommand)] @@ -30,7 +31,7 @@ pub async fn execute( let req = PublicProfileRequest::builder().address(address).build(); let profile = client.public_profile(&req).await?; - print_profile_detail(&profile, &output)?; + print_profile(&profile, &output)?; } } diff --git a/src/commands/series.rs b/src/commands/series.rs index c0c29dd..ff80408 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -6,7 +6,7 @@ use polymarket_client_sdk::gamma::{ }; use crate::output::OutputFormat; -use crate::output::series::{print_series_detail, print_series_table}; +use crate::output::series::{print_series_item, print_series}; #[derive(Args)] pub struct SeriesArgs { @@ -64,14 +64,14 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .build(); let series = client.series(&request).await?; - print_series_table(&series, &output)?; + print_series(&series, &output)?; } SeriesCommand::Get { id } => { let req = SeriesByIdRequest::builder().id(id).build(); let series = client.series_by_id(&req).await?; - print_series_detail(&series, &output)?; + print_series_item(&series, &output)?; } } diff --git a/src/commands/sports.rs b/src/commands/sports.rs index e075eb2..0782fd1 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -3,7 +3,7 @@ use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{self, types::request::TeamsRequest}; use crate::output::OutputFormat; -use crate::output::sports::{print_sport_types, print_sports_table, print_teams_table}; +use crate::output::sports::{print_sport_types, print_sports, print_teams}; #[derive(Args)] pub struct SportsArgs { @@ -47,7 +47,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor match args.command { SportsCommand::List => { let sports = client.sports().await?; - print_sports_table(&sports, &output)?; + print_sports(&sports, &output)?; } SportsCommand::MarketTypes => { @@ -71,7 +71,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .build(); let teams = client.teams(&request).await?; - print_teams_table(&teams, &output)?; + print_teams(&teams, &output)?; } } diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 474350c..64aae6a 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -10,7 +10,7 @@ use polymarket_client_sdk::gamma::{ use super::is_numeric_id; use crate::output::OutputFormat; -use crate::output::tags::{print_related_tags_table, print_tag_detail, print_tags_table}; +use crate::output::tags::{print_related_tags, print_tag, print_tags}; #[derive(Args)] pub struct TagsArgs { @@ -76,7 +76,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma .build(); let tags = client.tags(&request).await?; - print_tags_table(&tags, &output)?; + print_tags(&tags, &output)?; } TagsCommand::Get { id } => { @@ -89,7 +89,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.tag_by_slug(&req).await? }; - print_tag_detail(&tag, &output)?; + print_tag(&tag, &output)?; } TagsCommand::Related { id, omit_empty } => { @@ -108,7 +108,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.related_tags_by_slug(&req).await? }; - print_related_tags_table(&related, &output)?; + print_related_tags(&related, &output)?; } TagsCommand::RelatedTags { id, omit_empty } => { @@ -127,7 +127,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma client.tags_related_to_tag_by_slug(&req).await? }; - print_tags_table(&tags, &output)?; + print_tags(&tags, &output)?; } } diff --git a/src/output/comments.rs b/src/output/comments.rs index 72d0c36..44b216a 100644 --- a/src/output/comments.rs +++ b/src/output/comments.rs @@ -42,7 +42,7 @@ fn comment_to_row(c: &Comment) -> CommentRow { } } -pub fn print_comments_table(comments: &[Comment], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_comments(comments: &[Comment], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if comments.is_empty() { @@ -58,7 +58,7 @@ pub fn print_comments_table(comments: &[Comment], output: &OutputFormat) -> anyh Ok(()) } -pub fn print_comment_detail(c: &Comment, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_comment(c: &Comment, output: &OutputFormat) -> anyhow::Result<()> { if matches!(output, OutputFormat::Json) { return print_json(c); } diff --git a/src/output/events.rs b/src/output/events.rs index c423575..b0f837b 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -37,7 +37,7 @@ fn event_to_row(e: &Event) -> EventRow { } } -pub fn print_events_table(events: &[Event], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_events(events: &[Event], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if events.is_empty() { @@ -54,7 +54,7 @@ pub fn print_events_table(events: &[Event], output: &OutputFormat) -> anyhow::Re } #[allow(clippy::too_many_lines)] -pub fn print_event_detail(e: &Event, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_event(e: &Event, output: &OutputFormat) -> anyhow::Result<()> { if matches!(output, OutputFormat::Json) { return print_json(e); } diff --git a/src/output/profiles.rs b/src/output/profiles.rs index 20feb06..6cf8c70 100644 --- a/src/output/profiles.rs +++ b/src/output/profiles.rs @@ -2,7 +2,7 @@ use polymarket_client_sdk::gamma::types::response::PublicProfile; use super::{OutputFormat, detail_field, print_detail_table, print_json}; -pub fn print_profile_detail(p: &PublicProfile, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_profile(p: &PublicProfile, output: &OutputFormat) -> anyhow::Result<()> { if matches!(output, OutputFormat::Json) { return print_json(p); } diff --git a/src/output/series.rs b/src/output/series.rs index 0c27f24..1c3b238 100644 --- a/src/output/series.rs +++ b/src/output/series.rs @@ -31,7 +31,7 @@ fn series_to_row(s: &Series) -> SeriesRow { } } -pub fn print_series_table(series: &[Series], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_series(series: &[Series], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if series.is_empty() { @@ -47,7 +47,7 @@ pub fn print_series_table(series: &[Series], output: &OutputFormat) -> anyhow::R Ok(()) } -pub fn print_series_detail(s: &Series, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_series_item(s: &Series, output: &OutputFormat) -> anyhow::Result<()> { if matches!(output, OutputFormat::Json) { return print_json(s); } diff --git a/src/output/sports.rs b/src/output/sports.rs index c9cce7b..e5895f0 100644 --- a/src/output/sports.rs +++ b/src/output/sports.rs @@ -27,7 +27,7 @@ fn sport_to_row(s: &SportsMetadata) -> SportRow { } } -pub fn print_sports_table(sports: &[SportsMetadata], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_sports(sports: &[SportsMetadata], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if sports.is_empty() { @@ -83,7 +83,7 @@ fn team_to_row(t: &Team) -> TeamRow { } } -pub fn print_teams_table(teams: &[Team], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_teams(teams: &[Team], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if teams.is_empty() { diff --git a/src/output/tags.rs b/src/output/tags.rs index dafda68..ddd1e32 100644 --- a/src/output/tags.rs +++ b/src/output/tags.rs @@ -25,7 +25,7 @@ fn tag_to_row(t: &Tag) -> TagRow { } } -pub fn print_tags_table(tags: &[Tag], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_tags(tags: &[Tag], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if tags.is_empty() { @@ -62,7 +62,7 @@ fn related_tag_to_row(r: &RelatedTag) -> RelatedTagRow { } } -pub fn print_related_tags_table(tags: &[RelatedTag], output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_related_tags(tags: &[RelatedTag], output: &OutputFormat) -> anyhow::Result<()> { match output { OutputFormat::Table => { if tags.is_empty() { @@ -79,7 +79,7 @@ pub fn print_related_tags_table(tags: &[RelatedTag], output: &OutputFormat) -> a } #[allow(clippy::vec_init_then_push)] -pub fn print_tag_detail(t: &Tag, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_tag(t: &Tag, output: &OutputFormat) -> anyhow::Result<()> { if matches!(output, OutputFormat::Json) { return print_json(t); } From bacc51d3497854f7839000683cf7e48efe45be3a Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 22:35:54 +0530 Subject: [PATCH 10/13] revert back to .maybe_ascending --- src/commands/comments.rs | 4 ++-- src/commands/events.rs | 2 +- src/commands/markets.rs | 2 +- src/commands/series.rs | 2 +- src/commands/sports.rs | 2 +- src/commands/tags.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/comments.rs b/src/commands/comments.rs index 4003911..8dc4c52 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -112,7 +112,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .build(); let comments = client.comments(&request).await?; @@ -142,7 +142,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .build(); let comments = client.comments_by_user_address(&request).await?; diff --git a/src/commands/events.rs b/src/commands/events.rs index d001210..6d895ab 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -79,7 +79,7 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .limit(limit) .maybe_closed(resolved_closed) .maybe_offset(offset) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .maybe_tag_slug(tag) // EventsRequest::order is Vec; into_iter on Option yields 0 or 1 items. .order(order.into_iter().collect()) diff --git a/src/commands/markets.rs b/src/commands/markets.rs index 2544d18..d816860 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -95,7 +95,7 @@ pub async fn execute( .maybe_closed(resolved_closed) .maybe_offset(offset) .maybe_order(order) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .build(); let markets = client.markets(&request).await?; diff --git a/src/commands/series.rs b/src/commands/series.rs index ff80408..ddec624 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -59,7 +59,7 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .maybe_closed(closed) .build(); diff --git a/src/commands/sports.rs b/src/commands/sports.rs index 0782fd1..852ef2c 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -66,7 +66,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .league(league.into_iter().collect()) .build(); diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 64aae6a..82b7ce7 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -72,7 +72,7 @@ pub async fn execute(client: &gamma::Client, args: TagsArgs, output: OutputForma let request = TagsRequest::builder() .limit(limit) .maybe_offset(offset) - .ascending(ascending) + .maybe_ascending(ascending.then_some(true)) .build(); let tags = client.tags(&request).await?; From c44ae8c986bf1d611e6f68e89833fff58f3d6913 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 22:46:19 +0530 Subject: [PATCH 11/13] replace none with dash --- src/commands/clob.rs | 5 ----- src/commands/series.rs | 2 +- src/config.rs | 4 +--- src/output/bridge.rs | 4 ++-- src/output/clob/books.rs | 4 ++-- src/output/clob/markets.rs | 8 ++++---- src/output/comments.rs | 14 ++++++++------ src/output/data.rs | 8 ++++---- src/output/events.rs | 10 +++++----- src/output/markets.rs | 10 +++++----- src/output/mod.rs | 2 +- src/output/series.rs | 10 +++++----- src/output/sports.rs | 15 +++++++++------ src/output/tags.rs | 16 +++++++++------- 14 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/commands/clob.rs b/src/commands/clob.rs index 878a576..9ee089f 100644 --- a/src/commands/clob.rs +++ b/src/commands/clob.rs @@ -498,7 +498,6 @@ pub async fn execute( match args.command { // ── Unauthenticated read commands ──────────────────────────────── - ClobCommand::Ok => { let result = unauth.ok().await?; print_ok(&result, output)?; @@ -661,7 +660,6 @@ pub async fn execute( } // ── Authenticated trading commands (need signer for order signing) ── - ClobCommand::CreateOrder { token, side, @@ -773,7 +771,6 @@ pub async fn execute( } // ── Authenticated trading commands (no signer needed) ─────────── - ClobCommand::Orders { market, asset, @@ -886,7 +883,6 @@ pub async fn execute( } // ── Authenticated reward commands ──────────────────────────────── - ClobCommand::Rewards { date, cursor } => { let client = auth::authenticated_clob_client(private_key, signature_type).await?; let result = client @@ -949,7 +945,6 @@ pub async fn execute( } // ── Account management commands ────────────────────────────────── - ClobCommand::CreateApiKey => { let signer = auth::resolve_signer(private_key)?; let result = unauth.create_or_derive_api_key(&signer, None).await?; diff --git a/src/commands/series.rs b/src/commands/series.rs index ddec624..c916f2e 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -6,7 +6,7 @@ use polymarket_client_sdk::gamma::{ }; use crate::output::OutputFormat; -use crate::output::series::{print_series_item, print_series}; +use crate::output::series::{print_series, print_series_item}; #[derive(Args)] pub struct SeriesArgs { diff --git a/src/config.rs b/src/config.rs index cf3a830..60c0179 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,9 +70,7 @@ pub fn load_config() -> Result> { Ok(d) => d, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(e) => { - return Err( - anyhow::anyhow!(e).context(format!("Failed to read {}", path.display())) - ) + return Err(anyhow::anyhow!(e).context(format!("Failed to read {}", path.display()))); } }; let config = serde_json::from_str(&data) diff --git a/src/output/bridge.rs b/src/output/bridge.rs index 0fa82a6..7944912 100644 --- a/src/output/bridge.rs +++ b/src/output/bridge.rs @@ -5,7 +5,7 @@ use serde_json::json; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, OutputFormat, detail_field, format_decimal, print_detail_table}; +use super::{DASH, OutputFormat, detail_field, format_decimal, print_detail_table}; pub fn print_deposit(response: &DepositResponse, output: &OutputFormat) -> anyhow::Result<()> { match output { @@ -140,7 +140,7 @@ pub fn print_status(response: &StatusResponse, output: &OutputFormat) -> anyhow: tx_hash: tx .tx_hash .as_deref() - .map_or_else(|| NONE.into(), |h| super::truncate(h, 14)), + .map_or_else(|| DASH.into(), |h| super::truncate(h, 14)), }) .collect(); let table = Table::new(rows).with(Style::rounded()).to_string(); diff --git a/src/output/clob/books.rs b/src/output/clob/books.rs index b387db4..71a322b 100644 --- a/src/output/clob/books.rs +++ b/src/output/clob/books.rs @@ -5,7 +5,7 @@ use serde_json::json; use tabled::settings::Style; use tabled::{Table, Tabled}; -use crate::output::{NONE, OutputFormat, truncate}; +use crate::output::{DASH, OutputFormat, truncate}; pub fn print_order_book( result: &OrderBookSummaryResponse, @@ -19,7 +19,7 @@ pub fn print_order_book( "Last Trade: {}", result .last_trade_price - .map_or(NONE.into(), |p| p.to_string()) + .map_or(DASH.into(), |p| p.to_string()) ); println!(); diff --git a/src/output/clob/markets.rs b/src/output/clob/markets.rs index 46ac62d..2251149 100644 --- a/src/output/clob/markets.rs +++ b/src/output/clob/markets.rs @@ -7,7 +7,7 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::END_CURSOR; -use crate::output::{NONE, OutputFormat, truncate}; +use crate::output::{DASH, OutputFormat, truncate}; pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyhow::Result<()> { match output { @@ -18,7 +18,7 @@ pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyh ["Slug".into(), result.market_slug.clone()], [ "Condition ID".into(), - result.condition_id.map_or(NONE.into(), |c| c.to_string()), + result.condition_id.map_or(DASH.into(), |c| c.to_string()), ], ["Active".into(), result.active.to_string()], ["Closed".into(), result.closed.to_string()], @@ -34,7 +34,7 @@ pub fn print_clob_market(result: &MarketResponse, output: &OutputFormat) -> anyh ["Neg Risk".into(), result.neg_risk.to_string()], [ "End Date".into(), - result.end_date_iso.map_or(NONE.into(), |d| d.to_rfc3339()), + result.end_date_iso.map_or(DASH.into(), |d| d.to_rfc3339()), ], ]; for token in &result.tokens { @@ -128,7 +128,7 @@ pub fn print_simplified_markets( .map(|m| Row { condition_id: m .condition_id - .map_or(NONE.into(), |c| truncate(&c.to_string(), 14)), + .map_or(DASH.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(), diff --git a/src/output/comments.rs b/src/output/comments.rs index 44b216a..4204afb 100644 --- a/src/output/comments.rs +++ b/src/output/comments.rs @@ -2,7 +2,9 @@ use polymarket_client_sdk::gamma::types::response::Comment; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate}; +use super::{ + DASH, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate, +}; #[derive(Tabled)] struct CommentRow { @@ -24,21 +26,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(|| NONE.into()) + .unwrap_or_else(|| DASH.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(NONE), 60), + body: truncate(c.body.as_deref().unwrap_or(DASH), 60), reactions: c .reaction_count - .map_or_else(|| NONE.into(), |n| n.to_string()), + .map_or_else(|| DASH.into(), |n| n.to_string()), created: c .created_at .as_ref() - .map_or_else(|| NONE.into(), format_date), + .map_or_else(|| DASH.into(), format_date), } } @@ -101,7 +103,7 @@ pub fn print_comment(c: &Comment, output: &OutputFormat) -> anyhow::Result<()> { rows, "Reactions", c.reaction_count - .map_or_else(|| NONE.into(), |n| n.to_string()) + .map_or_else(|| DASH.into(), |n| n.to_string()) ); detail_field!( rows, diff --git a/src/output/data.rs b/src/output/data.rs index ac31f67..2b4b7c0 100644 --- a/src/output/data.rs +++ b/src/output/data.rs @@ -6,7 +6,7 @@ use serde_json::json; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, OutputFormat, format_decimal, truncate}; +use super::{DASH, OutputFormat, format_decimal, truncate}; fn format_market(m: &Market) -> String { match m { @@ -270,7 +270,7 @@ pub fn print_activity(activity: &[Activity], output: &OutputFormat) -> anyhow::R .iter() .map(|a| Row { activity_type: a.activity_type.to_string(), - title: truncate(a.title.as_deref().unwrap_or(NONE), 35), + title: truncate(a.title.as_deref().unwrap_or(DASH), 35), size: format!("{:.2}", a.size), usdc_size: format_decimal(a.usdc_size), tx: truncate(&a.transaction_hash.to_string(), 14), @@ -327,7 +327,7 @@ pub fn print_holders(meta_holders: &[MetaHolder], output: &OutputFormat) -> anyh .name .as_deref() .or(h.pseudonym.as_deref()) - .unwrap_or(NONE) + .unwrap_or(DASH) .into(), amount: format_decimal(h.amount), outcome_index: h.outcome_index.to_string(), @@ -469,7 +469,7 @@ pub fn print_leaderboard( .iter() .map(|e| Row { rank: e.rank.to_string(), - trader: truncate(e.user_name.as_deref().unwrap_or(NONE), 20), + trader: truncate(e.user_name.as_deref().unwrap_or(DASH), 20), pnl: format_decimal(e.pnl), volume: format_decimal(e.vol), }) diff --git a/src/output/events.rs b/src/output/events.rs index b0f837b..5b4def2 100644 --- a/src/output/events.rs +++ b/src/output/events.rs @@ -3,7 +3,7 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + DASH, OutputFormat, active_status, detail_field, format_date, format_decimal, print_detail_table, print_json, truncate, }; @@ -22,17 +22,17 @@ struct EventRow { } fn event_to_row(e: &Event) -> EventRow { - let title = e.title.as_deref().unwrap_or(NONE); + let title = e.title.as_deref().unwrap_or(DASH); let market_count = e .markets .as_ref() - .map_or_else(|| NONE.into(), |m| m.len().to_string()); + .map_or_else(|| DASH.into(), |m| m.len().to_string()); EventRow { title: truncate(title, 60), market_count, - volume: e.volume.map_or_else(|| NONE.into(), format_decimal), - liquidity: e.liquidity.map_or_else(|| NONE.into(), format_decimal), + volume: e.volume.map_or_else(|| DASH.into(), format_decimal), + liquidity: e.liquidity.map_or_else(|| DASH.into(), format_decimal), status: active_status(e.closed, e.active).into(), } } diff --git a/src/output/markets.rs b/src/output/markets.rs index ca549d0..b9a3588 100644 --- a/src/output/markets.rs +++ b/src/output/markets.rs @@ -4,7 +4,7 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + DASH, OutputFormat, active_status, detail_field, format_date, format_decimal, print_detail_table, print_json, truncate, }; @@ -23,21 +23,21 @@ struct MarketRow { } fn market_to_row(m: &Market) -> MarketRow { - let question = m.question.as_deref().unwrap_or(NONE); + let question = m.question.as_deref().unwrap_or(DASH); let price_yes = m .outcome_prices .as_ref() .and_then(|p| p.first()) .map_or_else( - || NONE.into(), + || DASH.into(), |p| format!("{:.2}¢", p * Decimal::from(100)), ); MarketRow { question: truncate(question, 60), price_yes, - volume: m.volume_num.map_or_else(|| NONE.into(), format_decimal), - liquidity: m.liquidity_num.map_or_else(|| NONE.into(), format_decimal), + volume: m.volume_num.map_or_else(|| DASH.into(), format_decimal), + liquidity: m.liquidity_num.map_or_else(|| DASH.into(), format_decimal), status: active_status(m.closed, m.active).into(), } } diff --git a/src/output/mod.rs b/src/output/mod.rs index 6d040bf..05de836 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -18,7 +18,7 @@ use tabled::Table; use tabled::settings::object::Columns; use tabled::settings::{Modify, Style, Width}; -pub(crate) const NONE: &str = "—"; +pub(crate) const DASH: &str = "—"; #[derive(Clone, Copy, Debug, clap::ValueEnum)] pub(crate) enum OutputFormat { diff --git a/src/output/series.rs b/src/output/series.rs index 1c3b238..99f1cfb 100644 --- a/src/output/series.rs +++ b/src/output/series.rs @@ -3,7 +3,7 @@ use tabled::settings::Style; use tabled::{Table, Tabled}; use super::{ - NONE, OutputFormat, active_status, detail_field, format_date, format_decimal, + DASH, OutputFormat, active_status, detail_field, format_date, format_decimal, print_detail_table, print_json, truncate, }; @@ -23,10 +23,10 @@ struct SeriesRow { fn series_to_row(s: &Series) -> SeriesRow { SeriesRow { - 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), + title: truncate(s.title.as_deref().unwrap_or(DASH), 50), + series_type: s.series_type.as_deref().unwrap_or(DASH).into(), + volume: s.volume.map_or_else(|| DASH.into(), format_decimal), + liquidity: s.liquidity.map_or_else(|| DASH.into(), format_decimal), status: active_status(s.closed, s.active).into(), } } diff --git a/src/output/sports.rs b/src/output/sports.rs index e5895f0..0eae756 100644 --- a/src/output/sports.rs +++ b/src/output/sports.rs @@ -4,7 +4,7 @@ use polymarket_client_sdk::gamma::types::response::{ use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, OutputFormat, print_json, truncate}; +use super::{DASH, OutputFormat, print_json, truncate}; #[derive(Tabled)] struct SportRow { @@ -43,7 +43,10 @@ pub fn print_sports(sports: &[SportsMetadata], output: &OutputFormat) -> anyhow: Ok(()) } -pub fn print_sport_types(types: &SportsMarketTypesResponse, output: &OutputFormat) -> anyhow::Result<()> { +pub fn print_sport_types( + types: &SportsMarketTypesResponse, + output: &OutputFormat, +) -> anyhow::Result<()> { match output { OutputFormat::Table => { if types.market_types.is_empty() { @@ -76,10 +79,10 @@ struct TeamRow { fn team_to_row(t: &Team) -> TeamRow { TeamRow { id: t.id.to_string(), - name: t.name.as_deref().unwrap_or(NONE).into(), - league: t.league.as_deref().unwrap_or(NONE).into(), - record: t.record.as_deref().unwrap_or(NONE).into(), - abbreviation: t.abbreviation.as_deref().unwrap_or(NONE).into(), + name: t.name.as_deref().unwrap_or(DASH).into(), + league: t.league.as_deref().unwrap_or(DASH).into(), + record: t.record.as_deref().unwrap_or(DASH).into(), + abbreviation: t.abbreviation.as_deref().unwrap_or(DASH).into(), } } diff --git a/src/output/tags.rs b/src/output/tags.rs index ddd1e32..3c57f55 100644 --- a/src/output/tags.rs +++ b/src/output/tags.rs @@ -2,7 +2,9 @@ use polymarket_client_sdk::gamma::types::response::{RelatedTag, Tag}; use tabled::settings::Style; use tabled::{Table, Tabled}; -use super::{NONE, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate}; +use super::{ + DASH, OutputFormat, detail_field, format_date, print_detail_table, print_json, truncate, +}; #[derive(Tabled)] struct TagRow { @@ -19,9 +21,9 @@ struct TagRow { fn tag_to_row(t: &Tag) -> TagRow { TagRow { id: truncate(&t.id, 20), - 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()), + label: t.label.as_deref().unwrap_or(DASH).into(), + slug: t.slug.as_deref().unwrap_or(DASH).into(), + carousel: t.is_carousel.map_or_else(|| DASH.into(), |v| v.to_string()), } } @@ -56,9 +58,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(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()), + tag_id: r.tag_id.as_deref().unwrap_or(DASH).into(), + related_tag_id: r.related_tag_id.as_deref().unwrap_or(DASH).into(), + rank: r.rank.map_or_else(|| DASH.into(), |v| v.to_string()), } } From 47f8386cbae5d88ba27a7272d67303efee1bb44c Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Mon, 9 Mar 2026 23:20:08 +0530 Subject: [PATCH 12/13] replace signer_key_hex with inline format! --- src/commands/setup.rs | 5 ++--- src/commands/wallet.rs | 16 ++-------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/commands/setup.rs b/src/commands/setup.rs index c0d32be..2e4a66d 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -6,7 +6,6 @@ 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::signer_key_hex; use crate::config; fn print_banner() { @@ -117,12 +116,12 @@ fn setup_wallet() -> Result
{ let signer = LocalSigner::from_str(&key) .context("Invalid private key")? .with_chain_id(Some(POLYGON)); - let hex = signer_key_hex(&signer); + let hex = format!("{:#x}", signer.to_bytes()); (signer.address(), hex) } else { let signer = LocalSigner::random().with_chain_id(Some(POLYGON)); let address = signer.address(); - let hex = signer_key_hex(&signer); + let hex = format!("{:#x}", signer.to_bytes()); (address, hex) }; diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index b85fd61..d54a63b 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -1,7 +1,5 @@ -use std::fmt::Write as _; use std::str::FromStr; -use alloy::signers::local::PrivateKeySigner; use anyhow::{Context, Result, bail}; use clap::{Args, Subcommand}; use polymarket_client_sdk::auth::LocalSigner; @@ -82,22 +80,12 @@ fn guard_overwrite(force: bool) -> Result<()> { Ok(()) } -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<()> { guard_overwrite(force)?; let signer = LocalSigner::random().with_chain_id(Some(POLYGON)); let address = signer.address(); - let key_hex = signer_key_hex(&signer); + let key_hex = format!("{:#x}", signer.to_bytes()); config::save_wallet(&key_hex, POLYGON, signature_type)?; let config_path = config::config_path()?; @@ -138,7 +126,7 @@ fn cmd_import(key: &str, output: OutputFormat, force: bool, signature_type: &str .context("Invalid private key")? .with_chain_id(Some(POLYGON)); let address = signer.address(); - let key_hex = signer_key_hex(&signer); + let key_hex = format!("{:#x}", signer.to_bytes()); config::save_wallet(&key_hex, POLYGON, signature_type)?; let config_path = config::config_path()?; From eb44c20846d609f37d40cbb5c5a18ee5eb65ed79 Mon Sep 17 00:00:00 2001 From: Suhail Kakar Date: Tue, 10 Mar 2026 06:13:15 +0530 Subject: [PATCH 13/13] replace maybe_ascending with ascending --- src/commands/comments.rs | 4 ++-- src/commands/events.rs | 2 +- src/commands/markets.rs | 2 +- src/commands/series.rs | 2 +- src/commands/sports.rs | 2 +- src/commands/tags.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/comments.rs b/src/commands/comments.rs index 8dc4c52..4003911 100644 --- a/src/commands/comments.rs +++ b/src/commands/comments.rs @@ -112,7 +112,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending.then_some(true)) + .ascending(ascending) .build(); let comments = client.comments(&request).await?; @@ -142,7 +142,7 @@ pub async fn execute( .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending.then_some(true)) + .ascending(ascending) .build(); let comments = client.comments_by_user_address(&request).await?; diff --git a/src/commands/events.rs b/src/commands/events.rs index 6d895ab..d001210 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -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(ascending.then_some(true)) + .ascending(ascending) .maybe_tag_slug(tag) // EventsRequest::order is Vec; into_iter on Option yields 0 or 1 items. .order(order.into_iter().collect()) diff --git a/src/commands/markets.rs b/src/commands/markets.rs index d816860..2544d18 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -95,7 +95,7 @@ pub async fn execute( .maybe_closed(resolved_closed) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending.then_some(true)) + .ascending(ascending) .build(); let markets = client.markets(&request).await?; diff --git a/src/commands/series.rs b/src/commands/series.rs index c916f2e..fc5e884 100644 --- a/src/commands/series.rs +++ b/src/commands/series.rs @@ -59,7 +59,7 @@ pub async fn execute(client: &gamma::Client, args: SeriesArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending.then_some(true)) + .ascending(ascending) .maybe_closed(closed) .build(); diff --git a/src/commands/sports.rs b/src/commands/sports.rs index 852ef2c..0782fd1 100644 --- a/src/commands/sports.rs +++ b/src/commands/sports.rs @@ -66,7 +66,7 @@ pub async fn execute(client: &gamma::Client, args: SportsArgs, output: OutputFor .limit(limit) .maybe_offset(offset) .maybe_order(order) - .maybe_ascending(ascending.then_some(true)) + .ascending(ascending) .league(league.into_iter().collect()) .build(); diff --git a/src/commands/tags.rs b/src/commands/tags.rs index 82b7ce7..64aae6a 100644 --- a/src/commands/tags.rs +++ b/src/commands/tags.rs @@ -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(ascending.then_some(true)) + .ascending(ascending) .build(); let tags = client.tags(&request).await?;