nautilus_polymarket/positions/
calldata.rs1use alloy::{primitives::Bytes, sol, sol_types::SolCall};
19use alloy_primitives::{Address, B256, U256};
20use rust_decimal::Decimal;
21
22use super::amounts::pusd_to_base_units;
23use crate::{
24 http::error::Result,
25 signing::eip712::{
26 CTF_COLLATERAL_ADAPTER, NEG_RISK_CTF_COLLATERAL_ADAPTER, POLYMARKET_COLLATERAL_TOKEN,
27 parse_bytes32,
28 },
29};
30
31sol! {
32 function splitPosition(
33 address collateralToken,
34 bytes32 parentCollectionId,
35 bytes32 conditionId,
36 uint256[] partition,
37 uint256 amount
38 );
39
40 function mergePositions(
41 address collateralToken,
42 bytes32 parentCollectionId,
43 bytes32 conditionId,
44 uint256[] partition,
45 uint256 amount
46 );
47
48 function redeemPositions(
49 address collateralToken,
50 bytes32 parentCollectionId,
51 bytes32 conditionId,
52 uint256[] indexSets
53 );
54}
55
56const PARENT_COLLECTION_ID: B256 = B256::ZERO;
57const BINARY_INDEX_SETS: [U256; 2] = [
58 U256::from_limbs([1, 0, 0, 0]),
59 U256::from_limbs([2, 0, 0, 0]),
60];
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct PositionCall {
65 pub target: Address,
67 pub data: Bytes,
69}
70
71#[must_use]
73pub const fn collateral_adapter(neg_risk: bool) -> Address {
74 if neg_risk {
75 NEG_RISK_CTF_COLLATERAL_ADAPTER
76 } else {
77 CTF_COLLATERAL_ADAPTER
78 }
79}
80
81pub fn encode_split_position(
88 condition_id: &str,
89 amount: Decimal,
90 neg_risk: bool,
91) -> Result<PositionCall> {
92 let condition_id = parse_condition_id(condition_id)?;
93 let amount = pusd_to_base_units(amount)?;
94 Ok(PositionCall {
95 target: collateral_adapter(neg_risk),
96 data: Bytes::from(
97 splitPositionCall {
98 collateralToken: POLYMARKET_COLLATERAL_TOKEN,
99 parentCollectionId: PARENT_COLLECTION_ID,
100 conditionId: condition_id,
101 partition: BINARY_INDEX_SETS.to_vec(),
102 amount,
103 }
104 .abi_encode(),
105 ),
106 })
107}
108
109pub fn encode_merge_positions(
116 condition_id: &str,
117 amount: Decimal,
118 neg_risk: bool,
119) -> Result<PositionCall> {
120 let condition_id = parse_condition_id(condition_id)?;
121 let amount = pusd_to_base_units(amount)?;
122 Ok(PositionCall {
123 target: collateral_adapter(neg_risk),
124 data: Bytes::from(
125 mergePositionsCall {
126 collateralToken: POLYMARKET_COLLATERAL_TOKEN,
127 parentCollectionId: PARENT_COLLECTION_ID,
128 conditionId: condition_id,
129 partition: BINARY_INDEX_SETS.to_vec(),
130 amount,
131 }
132 .abi_encode(),
133 ),
134 })
135}
136
137pub fn encode_redeem_positions(condition_id: &str, neg_risk: bool) -> Result<PositionCall> {
143 let condition_id = parse_condition_id(condition_id)?;
144 Ok(PositionCall {
145 target: collateral_adapter(neg_risk),
146 data: Bytes::from(
147 redeemPositionsCall {
148 collateralToken: POLYMARKET_COLLATERAL_TOKEN,
149 parentCollectionId: PARENT_COLLECTION_ID,
150 conditionId: condition_id,
151 indexSets: BINARY_INDEX_SETS.to_vec(),
152 }
153 .abi_encode(),
154 ),
155 })
156}
157
158fn parse_condition_id(condition_id: &str) -> Result<B256> {
159 parse_bytes32(condition_id, "condition_id")
160}
161
162#[cfg(test)]
163mod tests {
164 use alloy_primitives::keccak256;
165 use rstest::rstest;
166 use rust_decimal_macros::dec;
167
168 use super::*;
169
170 const CONDITION_ID: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";
171
172 fn selector(signature: &str) -> [u8; 4] {
173 let hash = keccak256(signature.as_bytes());
174 [hash[0], hash[1], hash[2], hash[3]]
175 }
176
177 #[rstest]
178 fn test_collateral_adapter_targets() {
179 assert_eq!(collateral_adapter(false), CTF_COLLATERAL_ADAPTER);
180 assert_eq!(collateral_adapter(true), NEG_RISK_CTF_COLLATERAL_ADAPTER);
181 assert_ne!(CTF_COLLATERAL_ADAPTER, NEG_RISK_CTF_COLLATERAL_ADAPTER);
182 }
183
184 #[rstest]
185 fn test_encode_split_standard_fixture() {
186 let call = encode_split_position(CONDITION_ID, dec!(1), false).unwrap();
187 assert_eq!(call.target, CTF_COLLATERAL_ADAPTER);
188 assert_eq!(
189 &call.data[..4],
190 selector("splitPosition(address,bytes32,bytes32,uint256[],uint256)")
191 );
192 assert_eq!(
193 format!("0x{}", alloy_primitives::hex::encode(&call.data)),
194 concat!(
195 "0x72ce4275",
196 "000000000000000000000000c011a7e12a19f7b1f670d46f03b03f3342e82dfb",
197 "0000000000000000000000000000000000000000000000000000000000000000",
198 "1111111111111111111111111111111111111111111111111111111111111111",
199 "00000000000000000000000000000000000000000000000000000000000000a0",
200 "00000000000000000000000000000000000000000000000000000000000f4240",
201 "0000000000000000000000000000000000000000000000000000000000000002",
202 "0000000000000000000000000000000000000000000000000000000000000001",
203 "0000000000000000000000000000000000000000000000000000000000000002",
204 )
205 );
206 }
207
208 #[rstest]
209 fn test_encode_merge_neg_risk_fixture() {
210 let call = encode_merge_positions(CONDITION_ID, dec!(1), true).unwrap();
211 assert_eq!(call.target, NEG_RISK_CTF_COLLATERAL_ADAPTER);
212 assert_eq!(
213 &call.data[..4],
214 selector("mergePositions(address,bytes32,bytes32,uint256[],uint256)")
215 );
216 assert_eq!(
217 format!("0x{}", alloy_primitives::hex::encode(&call.data)),
218 concat!(
219 "0x9e7212ad",
220 "000000000000000000000000c011a7e12a19f7b1f670d46f03b03f3342e82dfb",
221 "0000000000000000000000000000000000000000000000000000000000000000",
222 "1111111111111111111111111111111111111111111111111111111111111111",
223 "00000000000000000000000000000000000000000000000000000000000000a0",
224 "00000000000000000000000000000000000000000000000000000000000f4240",
225 "0000000000000000000000000000000000000000000000000000000000000002",
226 "0000000000000000000000000000000000000000000000000000000000000001",
227 "0000000000000000000000000000000000000000000000000000000000000002",
228 )
229 );
230 }
231
232 #[rstest]
233 fn test_encode_redeem_standard_and_neg_risk_share_calldata() {
234 let standard = encode_redeem_positions(CONDITION_ID, false).unwrap();
235 let neg_risk = encode_redeem_positions(CONDITION_ID, true).unwrap();
236 assert_eq!(standard.target, CTF_COLLATERAL_ADAPTER);
237 assert_eq!(neg_risk.target, NEG_RISK_CTF_COLLATERAL_ADAPTER);
238 assert_eq!(standard.data, neg_risk.data);
239 assert_eq!(
240 &standard.data[..4],
241 selector("redeemPositions(address,bytes32,bytes32,uint256[])")
242 );
243 assert_eq!(
244 format!("0x{}", alloy_primitives::hex::encode(&standard.data)),
245 concat!(
246 "0x01b7037c",
247 "000000000000000000000000c011a7e12a19f7b1f670d46f03b03f3342e82dfb",
248 "0000000000000000000000000000000000000000000000000000000000000000",
249 "1111111111111111111111111111111111111111111111111111111111111111",
250 "0000000000000000000000000000000000000000000000000000000000000080",
251 "0000000000000000000000000000000000000000000000000000000000000002",
252 "0000000000000000000000000000000000000000000000000000000000000001",
253 "0000000000000000000000000000000000000000000000000000000000000002",
254 )
255 );
256 }
257
258 #[rstest]
259 fn test_encode_rejects_invalid_condition_id() {
260 let err = encode_split_position("not-a-condition", dec!(1), false).unwrap_err();
261 assert!(err.to_string().contains("Invalid condition_id"));
262 }
263}