1use crate::constants::Addresses;
6use crate::errors::{Error, Result};
7use crate::types::ChainId;
8use alloy::primitives::{Address, Bytes, FixedBytes, U256};
9use alloy::providers::{Provider, ProviderBuilder};
10use alloy::signers::local::PrivateKeySigner;
11use alloy::sol;
12use alloy::sol_types::SolCall;
13use tracing::{debug, info};
14
15sol! {
18 #[sol(rpc)]
20 interface IConditionalTokens {
21 function splitPosition(
22 address collateralToken,
23 bytes32 parentCollectionId,
24 bytes32 conditionId,
25 uint256[] calldata partition,
26 uint256 amount
27 ) external;
28
29 function mergePositions(
30 address collateralToken,
31 bytes32 parentCollectionId,
32 bytes32 conditionId,
33 uint256[] calldata partition,
34 uint256 amount
35 ) external;
36 }
37}
38
39sol! {
40 #[sol(rpc)]
42 interface INegRiskAdapter {
43 #[allow(non_snake_case)]
44 function splitPosition(bytes32 conditionId, uint256 amount) external;
45
46 #[allow(non_snake_case)]
47 function mergePositions(bytes32 conditionId, uint256 amount) external;
48 }
49}
50
51sol! {
52 #[sol(rpc)]
54 interface IKernel {
55 function execute(bytes32 mode, bytes calldata executionCalldata) external payable returns (bytes memory);
56 }
57}
58
59sol! {
60 #[sol(rpc)]
62 interface IERC20 {
63 function approve(address spender, uint256 amount) external returns (bool);
64 function allowance(address owner, address spender) external view returns (uint256);
65 function balanceOf(address account) external view returns (uint256);
66 }
67}
68
69sol! {
70 #[sol(rpc)]
72 interface IERC1155 {
73 function setApprovalForAll(address operator, bool approved) external;
74 function isApprovedForAll(address account, address operator) external view returns (bool);
75 function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
76 function balanceOf(address account, uint256 id) external view returns (uint256);
77 }
78}
79
80const KERNEL_EXEC_MODE: [u8; 32] = [0u8; 32];
82
83#[derive(Debug, Clone)]
85pub struct SplitOptions {
86 pub condition_id: String,
88 pub amount: f64,
90 pub is_neg_risk: bool,
92 pub is_yield_bearing: bool,
94}
95
96pub struct OnchainClient {
98 chain_id: ChainId,
99 signer: PrivateKeySigner,
100 addresses: Addresses,
101 rpc_url: String,
102 predict_account: Option<Address>,
104}
105
106impl OnchainClient {
107 pub fn new(chain_id: ChainId, signer: PrivateKeySigner) -> Self {
109 let addresses = Addresses::for_chain(chain_id);
110 let rpc_url = match chain_id {
111 ChainId::BnbMainnet => "https://bsc-dataseed.bnbchain.org/".to_string(),
112 ChainId::BnbTestnet => "https://bsc-testnet-dataseed.bnbchain.org/".to_string(),
113 };
114
115 Self {
116 chain_id,
117 signer,
118 addresses,
119 rpc_url,
120 predict_account: None,
121 }
122 }
123
124 pub fn with_predict_account(
126 chain_id: ChainId,
127 signer: PrivateKeySigner,
128 predict_account: &str,
129 ) -> Result<Self> {
130 let mut client = Self::new(chain_id, signer);
131 client.predict_account = Some(
132 predict_account
133 .parse()
134 .map_err(|e| Error::Other(format!("Invalid predict account address: {}", e)))?,
135 );
136 Ok(client)
137 }
138
139 pub fn signer_address(&self) -> Address {
141 self.signer.address()
142 }
143
144 pub fn trading_address(&self) -> Address {
146 self.predict_account.unwrap_or_else(|| self.signer.address())
147 }
148
149 pub fn is_smart_wallet(&self) -> bool {
151 self.predict_account.is_some()
152 }
153
154 pub fn addresses(&self) -> &Addresses {
156 &self.addresses
157 }
158
159 pub async fn set_approvals(
169 &self,
170 is_neg_risk: bool,
171 is_yield_bearing: bool,
172 ) -> Result<()> {
173 let provider = ProviderBuilder::new()
174 .wallet(alloy::network::EthereumWallet::from(self.signer.clone()))
175 .connect_http(self.rpc_url.parse().unwrap());
176
177 let owner = self.trading_address();
178
179 let ct_address: Address = self.addresses.get_conditional_tokens(is_yield_bearing, is_neg_risk)
181 .parse().unwrap();
182 let exchange_address: Address = self.addresses.get_ctf_exchange(is_yield_bearing, is_neg_risk)
183 .parse().unwrap();
184
185 let ct = IERC1155::new(ct_address, provider.clone());
186 let is_approved = ct
187 .isApprovedForAll(owner, exchange_address)
188 .call()
189 .await
190 .map_err(|e| Error::Other(format!("Failed to check ERC-1155 approval: {}", e)))?;
191
192 if !is_approved {
193 info!("Setting ERC-1155 approval: {} → {}", ct_address, exchange_address);
194 let tx = ct
195 .setApprovalForAll(exchange_address, true)
196 .send()
197 .await
198 .map_err(|e| Error::Other(format!("Failed to send setApprovalForAll: {}", e)))?;
199 let receipt = tx
200 .get_receipt()
201 .await
202 .map_err(|e| Error::Other(format!("Failed to get approval receipt: {}", e)))?;
203 if !receipt.status() {
204 return Err(Error::Other(format!(
205 "setApprovalForAll reverted: {:?}", receipt.transaction_hash
206 )));
207 }
208 info!("ERC-1155 approval set: {:?}", receipt.transaction_hash);
209 } else {
210 debug!("ERC-1155 already approved: {} → {}", ct_address, exchange_address);
211 }
212
213 let usdt_address: Address = self.addresses.usdt.parse().unwrap();
215 let usdt = IERC20::new(usdt_address, provider.clone());
216 let allowance = usdt
217 .allowance(owner, exchange_address)
218 .call()
219 .await
220 .map_err(|e| Error::Other(format!("Failed to check USDT allowance: {}", e)))?;
221
222 if allowance < U256::from(1_000_000_000_000_000_000_000_u128) {
223 info!("Approving USDT for CTF Exchange: {}", exchange_address);
225 let tx = usdt
226 .approve(exchange_address, U256::MAX)
227 .send()
228 .await
229 .map_err(|e| Error::Other(format!("Failed to send USDT approval: {}", e)))?;
230 let receipt = tx
231 .get_receipt()
232 .await
233 .map_err(|e| Error::Other(format!("Failed to get USDT approval receipt: {}", e)))?;
234 if !receipt.status() {
235 return Err(Error::Other(format!(
236 "USDT approval reverted: {:?}", receipt.transaction_hash
237 )));
238 }
239 info!("USDT approval set: {:?}", receipt.transaction_hash);
240 } else {
241 debug!("USDT already approved for CTF Exchange");
242 }
243
244 if is_neg_risk {
246 let adapter_address: Address = if is_yield_bearing {
247 self.addresses.yield_bearing_neg_risk_adapter
248 } else {
249 self.addresses.neg_risk_adapter
250 }.parse().unwrap();
251
252 let is_adapter_approved = ct
253 .isApprovedForAll(owner, adapter_address)
254 .call()
255 .await
256 .map_err(|e| Error::Other(format!("Failed to check adapter approval: {}", e)))?;
257
258 if !is_adapter_approved {
259 info!("Setting ERC-1155 approval for Neg Risk Adapter: {}", adapter_address);
260 let tx = ct
261 .setApprovalForAll(adapter_address, true)
262 .send()
263 .await
264 .map_err(|e| Error::Other(format!("Failed to approve adapter: {}", e)))?;
265 let receipt = tx
266 .get_receipt()
267 .await
268 .map_err(|e| Error::Other(format!("Failed to get adapter approval receipt: {}", e)))?;
269 if !receipt.status() {
270 return Err(Error::Other(format!(
271 "Adapter approval reverted: {:?}", receipt.transaction_hash
272 )));
273 }
274 info!("Neg Risk Adapter approval set: {:?}", receipt.transaction_hash);
275 }
276 }
277
278 Ok(())
279 }
280
281 pub async fn split_positions(&self, options: SplitOptions) -> Result<String> {
289 info!(
290 "Splitting {} USDT for condition {} (neg_risk={}, yield_bearing={})",
291 options.amount, options.condition_id, options.is_neg_risk, options.is_yield_bearing
292 );
293
294 let amount_wei = U256::from((options.amount * 1e18) as u128);
296
297 let condition_id: FixedBytes<32> = options
299 .condition_id
300 .parse()
301 .map_err(|e| Error::Other(format!("Invalid condition ID: {}", e)))?;
302
303 let provider = ProviderBuilder::new()
305 .wallet(alloy::network::EthereumWallet::from(self.signer.clone()))
306 .connect_http(self.rpc_url.parse().unwrap());
307
308 self.ensure_usdt_approval(&provider, amount_wei, options.is_neg_risk, options.is_yield_bearing)
310 .await?;
311
312 let tx_hash = if self.is_smart_wallet() {
314 self.split_via_kernel(&provider, condition_id, amount_wei, &options)
315 .await?
316 } else {
317 self.split_direct(&provider, condition_id, amount_wei, &options)
318 .await?
319 };
320
321 info!("Split transaction submitted: {}", tx_hash);
322 Ok(tx_hash)
323 }
324
325 async fn ensure_usdt_approval<P: Provider + Clone>(
327 &self,
328 provider: &P,
329 amount: U256,
330 is_neg_risk: bool,
331 is_yield_bearing: bool,
332 ) -> Result<()> {
333 let usdt_address: Address = self.addresses.usdt.parse().unwrap();
334 let spender = self.get_target_contract(is_neg_risk, is_yield_bearing);
335 let owner = self.trading_address();
336
337 let usdt = IERC20::new(usdt_address, provider.clone());
338
339 let allowance = usdt
341 .allowance(owner, spender)
342 .call()
343 .await
344 .map_err(|e| Error::Other(format!("Failed to check allowance: {}", e)))?;
345
346 if allowance < amount {
347 info!("Approving USDT spend for {:?}", spender);
348
349 let approve_call = usdt.approve(spender, U256::MAX);
351
352 if self.is_smart_wallet() {
353 let encoded = approve_call.calldata().clone();
355 self.execute_via_kernel(provider, usdt_address, encoded)
356 .await?;
357 } else {
358 let tx = approve_call
360 .send()
361 .await
362 .map_err(|e| Error::Other(format!("Failed to send approval: {}", e)))?;
363 let receipt = tx
364 .get_receipt()
365 .await
366 .map_err(|e| Error::Other(format!("Failed to get approval receipt: {}", e)))?;
367
368 if !receipt.status() {
370 return Err(Error::Other(format!(
371 "Approval transaction reverted: {:?}",
372 receipt.transaction_hash
373 )));
374 }
375
376 debug!("Approval tx: {:?}", receipt.transaction_hash);
377 }
378 }
379
380 Ok(())
381 }
382
383 fn get_target_contract(&self, is_neg_risk: bool, is_yield_bearing: bool) -> Address {
385 let addr_str = if is_neg_risk {
386 if is_yield_bearing {
387 self.addresses.yield_bearing_neg_risk_adapter
388 } else {
389 self.addresses.neg_risk_adapter
390 }
391 } else if is_yield_bearing {
392 self.addresses.yield_bearing_conditional_tokens
393 } else {
394 self.addresses.conditional_tokens
395 };
396 addr_str.parse().unwrap()
397 }
398
399 async fn split_direct<P: Provider + Clone>(
401 &self,
402 provider: &P,
403 condition_id: FixedBytes<32>,
404 amount: U256,
405 options: &SplitOptions,
406 ) -> Result<String> {
407 let target = self.get_target_contract(options.is_neg_risk, options.is_yield_bearing);
408
409 if options.is_neg_risk {
410 let contract = INegRiskAdapter::new(target, provider.clone());
412 let tx = contract
413 .splitPosition(condition_id, amount)
414 .send()
415 .await
416 .map_err(|e| Error::Other(format!("Failed to send split tx: {}", e)))?;
417
418 let receipt = tx
419 .get_receipt()
420 .await
421 .map_err(|e| Error::Other(format!("Failed to get receipt: {}", e)))?;
422
423 if !receipt.status() {
425 return Err(Error::Other(format!(
426 "Transaction reverted: {:?}",
427 receipt.transaction_hash
428 )));
429 }
430
431 Ok(format!("{:?}", receipt.transaction_hash))
432 } else {
433 let contract = IConditionalTokens::new(target, provider.clone());
435 let usdt: Address = self.addresses.usdt.parse().unwrap();
436 let parent_collection = FixedBytes::<32>::ZERO;
437 let partition = vec![U256::from(1), U256::from(2)];
438
439 let tx = contract
440 .splitPosition(usdt, parent_collection, condition_id, partition, amount)
441 .send()
442 .await
443 .map_err(|e| Error::Other(format!("Failed to send split tx: {}", e)))?;
444
445 let receipt = tx
446 .get_receipt()
447 .await
448 .map_err(|e| Error::Other(format!("Failed to get receipt: {}", e)))?;
449
450 if !receipt.status() {
452 return Err(Error::Other(format!(
453 "Transaction reverted: {:?}",
454 receipt.transaction_hash
455 )));
456 }
457
458 Ok(format!("{:?}", receipt.transaction_hash))
459 }
460 }
461
462 async fn split_via_kernel<P: Provider + Clone>(
464 &self,
465 provider: &P,
466 condition_id: FixedBytes<32>,
467 amount: U256,
468 options: &SplitOptions,
469 ) -> Result<String> {
470 let target = self.get_target_contract(options.is_neg_risk, options.is_yield_bearing);
471
472 let calldata = if options.is_neg_risk {
474 let call = INegRiskAdapter::splitPositionCall {
476 conditionId: condition_id,
477 amount,
478 };
479 Bytes::from(call.abi_encode())
480 } else {
481 let usdt: Address = self.addresses.usdt.parse().unwrap();
483 let parent_collection = FixedBytes::<32>::ZERO;
484 let partition = vec![U256::from(1), U256::from(2)];
485
486 let call = IConditionalTokens::splitPositionCall {
487 collateralToken: usdt,
488 parentCollectionId: parent_collection,
489 conditionId: condition_id,
490 partition,
491 amount,
492 };
493 Bytes::from(call.abi_encode())
494 };
495
496 self.execute_via_kernel(provider, target, calldata).await
497 }
498
499 async fn execute_via_kernel<P: Provider + Clone>(
501 &self,
502 provider: &P,
503 target: Address,
504 calldata: Bytes,
505 ) -> Result<String> {
506 let predict_account = self
507 .predict_account
508 .ok_or_else(|| Error::Other("No predict account configured".to_string()))?;
509
510 let mut execution_calldata = Vec::new();
512 execution_calldata.extend_from_slice(target.as_slice());
513 execution_calldata.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
514 execution_calldata.extend_from_slice(&calldata);
515
516 let kernel = IKernel::new(predict_account, provider.clone());
517 let mode = FixedBytes::<32>::from(KERNEL_EXEC_MODE);
518
519 let tx = kernel
520 .execute(mode, Bytes::from(execution_calldata))
521 .send()
522 .await
523 .map_err(|e| Error::Other(format!("Failed to send kernel execute: {}", e)))?;
524
525 let receipt = tx
526 .get_receipt()
527 .await
528 .map_err(|e| Error::Other(format!("Failed to get receipt: {}", e)))?;
529
530 if !receipt.status() {
532 return Err(Error::Other(format!(
533 "Kernel execute reverted: {:?}",
534 receipt.transaction_hash
535 )));
536 }
537
538 Ok(format!("{:?}", receipt.transaction_hash))
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_create_onchain_client() {
548 let signer = PrivateKeySigner::random();
549 let client = OnchainClient::new(ChainId::BnbTestnet, signer);
550 assert!(!client.is_smart_wallet());
551 }
552
553 #[test]
554 fn test_create_smart_wallet_client() {
555 let signer = PrivateKeySigner::random();
556 let client = OnchainClient::with_predict_account(
557 ChainId::BnbTestnet,
558 signer,
559 "0x1234567890123456789012345678901234567890",
560 )
561 .unwrap();
562 assert!(client.is_smart_wallet());
563 }
564}