Skip to main content

polymarket_client/secure/
wallet.rs

1//! On-chain wallet operations (CTF split/merge/redeem).
2
3use std::str::FromStr as _;
4
5use alloy::providers::ProviderBuilder;
6use polymarket_client_sdk_v2::ctf::types::{
7    MergePositionsRequest as SdkMergeRequest, RedeemPositionsRequest as SdkRedeemRequest,
8    SplitPositionRequest as SdkSplitRequest,
9};
10use polymarket_client_sdk_v2::ctf::Client as CtfClient;
11use polymarket_client_sdk_v2::types::{Address, B256, U256};
12use polymarket_client_sdk_v2::POLYGON;
13
14use crate::error::{user_input, UserInputError};
15use crate::secure::secure_client::SecureClient;
16
17#[derive(Debug, thiserror::Error, Clone)]
18pub enum WalletOperationError {
19    #[error(transparent)]
20    UserInput(#[from] UserInputError),
21    #[error("on-chain error: {0}")]
22    OnChain(String),
23}
24
25#[derive(Clone, Debug)]
26pub struct TransactionOutcome {
27    pub transaction_hash: String,
28    pub block_number: u64,
29}
30
31#[derive(Clone, Debug)]
32pub struct SplitPositionRequest {
33    pub condition_id: String,
34    /// Amount in collateral base units (USDC has 6 decimals).
35    pub amount: u128,
36}
37
38#[derive(Clone, Debug)]
39pub struct MergePositionsRequest {
40    pub condition_id: String,
41    pub amount: u128,
42}
43
44#[derive(Clone, Debug, Default)]
45pub struct RedeemPositionsRequest {
46    pub condition_id: String,
47    /// Index sets to redeem. Empty uses the binary market default.
48    pub index_sets: Vec<u64>,
49}
50
51impl SecureClient {
52    pub async fn split_position(
53        &self,
54        request: SplitPositionRequest,
55    ) -> Result<TransactionOutcome, WalletOperationError> {
56        validate_amount(request.amount)?;
57        let client = self.ctf_client().await?;
58        let condition_id = parse_condition_id(&request.condition_id)?;
59        let collateral = collateral_token(self)?;
60
61        let sdk_request = SdkSplitRequest::for_binary_market(
62            collateral,
63            condition_id,
64            U256::from(request.amount),
65        );
66
67        let response = client
68            .split_position(&sdk_request)
69            .await
70            .map_err(|e| WalletOperationError::OnChain(e.to_string()))?;
71
72        Ok(TransactionOutcome {
73            transaction_hash: response.transaction_hash.to_string(),
74            block_number: response.block_number,
75        })
76    }
77
78    pub async fn merge_positions(
79        &self,
80        request: MergePositionsRequest,
81    ) -> Result<TransactionOutcome, WalletOperationError> {
82        validate_amount(request.amount)?;
83        let client = self.ctf_client().await?;
84        let condition_id = parse_condition_id(&request.condition_id)?;
85        let collateral = collateral_token(self)?;
86
87        let sdk_request = SdkMergeRequest::for_binary_market(
88            collateral,
89            condition_id,
90            U256::from(request.amount),
91        );
92
93        let response = client
94            .merge_positions(&sdk_request)
95            .await
96            .map_err(|e| WalletOperationError::OnChain(e.to_string()))?;
97
98        Ok(TransactionOutcome {
99            transaction_hash: response.transaction_hash.to_string(),
100            block_number: response.block_number,
101        })
102    }
103
104    pub async fn redeem_positions(
105        &self,
106        request: RedeemPositionsRequest,
107    ) -> Result<TransactionOutcome, WalletOperationError> {
108        let client = self.ctf_client().await?;
109        let condition_id = parse_condition_id(&request.condition_id)?;
110        let collateral = collateral_token(self)?;
111
112        let sdk_request = SdkRedeemRequest::for_binary_market(collateral, condition_id);
113
114        let response = client
115            .redeem_positions(&sdk_request)
116            .await
117            .map_err(|e| WalletOperationError::OnChain(e.to_string()))?;
118
119        Ok(TransactionOutcome {
120            transaction_hash: response.transaction_hash.to_string(),
121            block_number: response.block_number,
122        })
123    }
124
125    async fn ctf_client(
126        &self,
127    ) -> Result<CtfClient<impl alloy::providers::Provider + Clone>, WalletOperationError> {
128        let provider = ProviderBuilder::new()
129            .wallet(self.signer.clone())
130            .connect(self.environment().rpc)
131            .await
132            .map_err(|e| WalletOperationError::OnChain(e.to_string()))?;
133
134        CtfClient::new(provider, POLYGON).map_err(|e| WalletOperationError::OnChain(e.to_string()))
135    }
136}
137
138fn collateral_token(client: &SecureClient) -> Result<Address, WalletOperationError> {
139    Address::from_str(client.environment().collateral_token.as_str()).map_err(|e| {
140        WalletOperationError::UserInput(user_input(format!("invalid collateral token: {e}")))
141    })
142}
143
144fn parse_condition_id(value: &str) -> Result<B256, UserInputError> {
145    B256::from_str(value).map_err(|e| user_input(format!("invalid condition_id: {e}")))
146}
147
148fn validate_amount(amount: u128) -> Result<(), UserInputError> {
149    if amount == 0 {
150        return Err(user_input("amount must be greater than zero"));
151    }
152    Ok(())
153}