polynode 0.7.1

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
//! Position management — build transactions for split, merge, and convert on Polymarket.
//!
//! These functions return pre-built [`TransactionRequest`] objects containing the
//! contract address and ABI-encoded calldata. Submit them via the Polymarket relayer
//! (gasless for Safe wallets) or directly on-chain.
//!
//! # Examples
//!
//! ```rust,no_run
//! use polynode::trading::position_management::*;
//!
//! // Split $100 into YES + NO tokens on a neg-risk market
//! let tx = build_split_txn("0xabc...conditionId", 100.0, true);
//! println!("Submit to {} with data {}", tx.to, tx.data);
//!
//! // Convert NO positions on outcomes 0 and 1
//! let tx = build_convert_txn("0xdef...marketId", &[0, 1], 50.0);
//! ```

use super::constants::{CTF, NEG_RISK_ADAPTER, USDC};
use super::types::TransactionRequest;

/// Build a split transaction: USDC → YES + NO outcome tokens.
///
/// Routes to NegRiskAdapter for neg-risk markets, CTF for standard markets.
/// `amount` is in USDC (e.g. 100.0 = $100).
pub fn build_split_txn(condition_id: &str, amount: f64, neg_risk: bool) -> TransactionRequest {
    let amount_raw = (amount * 1_000_000.0) as u64;

    if neg_risk {
        // NegRiskAdapter.splitPosition(bytes32 conditionId, uint256 amount)
        let mut data = vec![0xa3, 0xd7, 0xda, 0x1d]; // selector
        data.extend_from_slice(&decode_bytes32(condition_id));
        data.extend_from_slice(&pad_u256(amount_raw));
        TransactionRequest {
            to: NEG_RISK_ADAPTER.to_string(),
            data: format!("0x{}", hex::encode(&data)),
            value: "0".to_string(),
        }
    } else {
        // CTF.splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)
        let mut data = vec![0x72, 0x10, 0x85, 0x03]; // selector
        data.extend_from_slice(&pad_address(USDC));
        data.extend_from_slice(&[0u8; 32]); // parentCollectionId = bytes32(0)
        data.extend_from_slice(&decode_bytes32(condition_id));
        // partition = [1, 2] as dynamic array
        data.extend_from_slice(&pad_u256(160)); // offset to partition array (5 * 32 bytes)
        data.extend_from_slice(&pad_u256(amount_raw));
        // partition array: length=2, values=[1, 2]
        data.extend_from_slice(&pad_u256(2));
        data.extend_from_slice(&pad_u256(1));
        data.extend_from_slice(&pad_u256(2));
        TransactionRequest {
            to: CTF.to_string(),
            data: format!("0x{}", hex::encode(&data)),
            value: "0".to_string(),
        }
    }
}

/// Build a merge transaction: YES + NO outcome tokens → USDC.
///
/// Routes to NegRiskAdapter for neg-risk markets, CTF for standard markets.
pub fn build_merge_txn(condition_id: &str, amount: f64, neg_risk: bool) -> TransactionRequest {
    let amount_raw = (amount * 1_000_000.0) as u64;

    if neg_risk {
        // NegRiskAdapter.mergePositions(bytes32 conditionId, uint256 amount)
        let mut data = vec![0x5d, 0x03, 0xf4, 0x53]; // selector
        data.extend_from_slice(&decode_bytes32(condition_id));
        data.extend_from_slice(&pad_u256(amount_raw));
        TransactionRequest {
            to: NEG_RISK_ADAPTER.to_string(),
            data: format!("0x{}", hex::encode(&data)),
            value: "0".to_string(),
        }
    } else {
        // CTF.mergePositions(address, bytes32, bytes32, uint256[], uint256)
        let mut data = vec![0xca, 0xd9, 0x44, 0x0e]; // selector
        data.extend_from_slice(&pad_address(USDC));
        data.extend_from_slice(&[0u8; 32]);
        data.extend_from_slice(&decode_bytes32(condition_id));
        data.extend_from_slice(&pad_u256(160));
        data.extend_from_slice(&pad_u256(amount_raw));
        data.extend_from_slice(&pad_u256(2));
        data.extend_from_slice(&pad_u256(1));
        data.extend_from_slice(&pad_u256(2));
        TransactionRequest {
            to: CTF.to_string(),
            data: format!("0x{}", hex::encode(&data)),
            value: "0".to_string(),
        }
    }
}

/// Build a convert transaction: NO positions → USDC + YES on complementary outcomes.
///
/// Only works on neg-risk multi-outcome markets.
/// `outcome_indices` specifies which outcomes' NOs to convert (e.g. `&[0, 1]`).
pub fn build_convert_txn(market_id: &str, outcome_indices: &[u32], amount: f64) -> TransactionRequest {
    let amount_raw = (amount * 1_000_000.0) as u64;

    // Compute indexSet bitmask from outcome indices
    let mut index_set: u128 = 0;
    for &idx in outcome_indices {
        index_set |= 1u128 << idx;
    }

    // NegRiskAdapter.convertPositions(bytes32 marketId, uint256 indexSet, uint256 amount)
    let mut data = vec![0xc6, 0x47, 0x48, 0xc4]; // selector
    data.extend_from_slice(&decode_bytes32(market_id));
    data.extend_from_slice(&pad_u128(index_set));
    data.extend_from_slice(&pad_u256(amount_raw));

    TransactionRequest {
        to: NEG_RISK_ADAPTER.to_string(),
        data: format!("0x{}", hex::encode(&data)),
        value: "0".to_string(),
    }
}

// ── Helpers ──

fn decode_bytes32(hex_str: &str) -> [u8; 32] {
    let s = hex_str.strip_prefix("0x").unwrap_or(hex_str);
    let mut out = [0u8; 32];
    let bytes = hex::decode(s).unwrap_or_default();
    let start = 32usize.saturating_sub(bytes.len());
    out[start..].copy_from_slice(&bytes[..bytes.len().min(32)]);
    out
}

fn pad_address(addr: &str) -> [u8; 32] {
    let s = addr.strip_prefix("0x").unwrap_or(addr);
    let mut out = [0u8; 32];
    let bytes = hex::decode(s).unwrap_or_default();
    let start = 32 - bytes.len().min(20);
    out[start..start + bytes.len().min(20)].copy_from_slice(&bytes[..bytes.len().min(20)]);
    out
}

fn pad_u256(val: u64) -> [u8; 32] {
    let mut out = [0u8; 32];
    out[24..32].copy_from_slice(&val.to_be_bytes());
    out
}

fn pad_u128(val: u128) -> [u8; 32] {
    let mut out = [0u8; 32];
    out[16..32].copy_from_slice(&val.to_be_bytes());
    out
}