@catalyst-team/poly-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/.env +0 -0
  2. package/README.md +803 -0
  3. package/dist/__tests__/clob-api.test.d.ts +5 -0
  4. package/dist/__tests__/clob-api.test.d.ts.map +1 -0
  5. package/dist/__tests__/clob-api.test.js +240 -0
  6. package/dist/__tests__/clob-api.test.js.map +1 -0
  7. package/dist/__tests__/integration/bridge-client.integration.test.d.ts +11 -0
  8. package/dist/__tests__/integration/bridge-client.integration.test.d.ts.map +1 -0
  9. package/dist/__tests__/integration/bridge-client.integration.test.js +260 -0
  10. package/dist/__tests__/integration/bridge-client.integration.test.js.map +1 -0
  11. package/dist/__tests__/integration/clob-api.integration.test.d.ts +13 -0
  12. package/dist/__tests__/integration/clob-api.integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/integration/clob-api.integration.test.js +170 -0
  14. package/dist/__tests__/integration/clob-api.integration.test.js.map +1 -0
  15. package/dist/__tests__/integration/ctf-client.integration.test.d.ts +17 -0
  16. package/dist/__tests__/integration/ctf-client.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/integration/ctf-client.integration.test.js +234 -0
  18. package/dist/__tests__/integration/ctf-client.integration.test.js.map +1 -0
  19. package/dist/__tests__/integration/data-api.integration.test.d.ts +9 -0
  20. package/dist/__tests__/integration/data-api.integration.test.d.ts.map +1 -0
  21. package/dist/__tests__/integration/data-api.integration.test.js +161 -0
  22. package/dist/__tests__/integration/data-api.integration.test.js.map +1 -0
  23. package/dist/__tests__/integration/gamma-api.integration.test.d.ts +9 -0
  24. package/dist/__tests__/integration/gamma-api.integration.test.d.ts.map +1 -0
  25. package/dist/__tests__/integration/gamma-api.integration.test.js +170 -0
  26. package/dist/__tests__/integration/gamma-api.integration.test.js.map +1 -0
  27. package/dist/__tests__/test-utils.d.ts +92 -0
  28. package/dist/__tests__/test-utils.d.ts.map +1 -0
  29. package/dist/__tests__/test-utils.js +143 -0
  30. package/dist/__tests__/test-utils.js.map +1 -0
  31. package/dist/clients/bridge-client.d.ts +388 -0
  32. package/dist/clients/bridge-client.d.ts.map +1 -0
  33. package/dist/clients/bridge-client.js +587 -0
  34. package/dist/clients/bridge-client.js.map +1 -0
  35. package/dist/clients/clob-api.d.ts +318 -0
  36. package/dist/clients/clob-api.d.ts.map +1 -0
  37. package/dist/clients/clob-api.js +388 -0
  38. package/dist/clients/clob-api.js.map +1 -0
  39. package/dist/clients/ctf-client.d.ts +473 -0
  40. package/dist/clients/ctf-client.d.ts.map +1 -0
  41. package/dist/clients/ctf-client.js +915 -0
  42. package/dist/clients/ctf-client.js.map +1 -0
  43. package/dist/clients/data-api.d.ts +134 -0
  44. package/dist/clients/data-api.d.ts.map +1 -0
  45. package/dist/clients/data-api.js +265 -0
  46. package/dist/clients/data-api.js.map +1 -0
  47. package/dist/clients/gamma-api.d.ts +401 -0
  48. package/dist/clients/gamma-api.d.ts.map +1 -0
  49. package/dist/clients/gamma-api.js +352 -0
  50. package/dist/clients/gamma-api.js.map +1 -0
  51. package/dist/clients/trading-client.d.ts +252 -0
  52. package/dist/clients/trading-client.d.ts.map +1 -0
  53. package/dist/clients/trading-client.js +543 -0
  54. package/dist/clients/trading-client.js.map +1 -0
  55. package/dist/clients/websocket-manager.d.ts +100 -0
  56. package/dist/clients/websocket-manager.d.ts.map +1 -0
  57. package/dist/clients/websocket-manager.js +193 -0
  58. package/dist/clients/websocket-manager.js.map +1 -0
  59. package/dist/core/cache-adapter-bridge.d.ts +36 -0
  60. package/dist/core/cache-adapter-bridge.d.ts.map +1 -0
  61. package/dist/core/cache-adapter-bridge.js +81 -0
  62. package/dist/core/cache-adapter-bridge.js.map +1 -0
  63. package/dist/core/cache.d.ts +40 -0
  64. package/dist/core/cache.d.ts.map +1 -0
  65. package/dist/core/cache.js +71 -0
  66. package/dist/core/cache.js.map +1 -0
  67. package/dist/core/errors.d.ts +38 -0
  68. package/dist/core/errors.d.ts.map +1 -0
  69. package/dist/core/errors.js +84 -0
  70. package/dist/core/errors.js.map +1 -0
  71. package/dist/core/rate-limiter.d.ts +31 -0
  72. package/dist/core/rate-limiter.d.ts.map +1 -0
  73. package/dist/core/rate-limiter.js +70 -0
  74. package/dist/core/rate-limiter.js.map +1 -0
  75. package/dist/core/types.d.ts +314 -0
  76. package/dist/core/types.d.ts.map +1 -0
  77. package/dist/core/types.js +19 -0
  78. package/dist/core/types.js.map +1 -0
  79. package/dist/core/unified-cache.d.ts +63 -0
  80. package/dist/core/unified-cache.d.ts.map +1 -0
  81. package/dist/core/unified-cache.js +114 -0
  82. package/dist/core/unified-cache.js.map +1 -0
  83. package/dist/index.d.ts +94 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +258 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/mcp/errors.d.ts +33 -0
  88. package/dist/mcp/errors.d.ts.map +1 -0
  89. package/dist/mcp/errors.js +86 -0
  90. package/dist/mcp/errors.js.map +1 -0
  91. package/dist/mcp/index.d.ts +62 -0
  92. package/dist/mcp/index.d.ts.map +1 -0
  93. package/dist/mcp/index.js +173 -0
  94. package/dist/mcp/index.js.map +1 -0
  95. package/dist/mcp/server.d.ts +17 -0
  96. package/dist/mcp/server.d.ts.map +1 -0
  97. package/dist/mcp/server.js +155 -0
  98. package/dist/mcp/server.js.map +1 -0
  99. package/dist/mcp/tools/guide.d.ts +12 -0
  100. package/dist/mcp/tools/guide.d.ts.map +1 -0
  101. package/dist/mcp/tools/guide.js +801 -0
  102. package/dist/mcp/tools/guide.js.map +1 -0
  103. package/dist/mcp/tools/index.d.ts +11 -0
  104. package/dist/mcp/tools/index.d.ts.map +1 -0
  105. package/dist/mcp/tools/index.js +27 -0
  106. package/dist/mcp/tools/index.js.map +1 -0
  107. package/dist/mcp/tools/market.d.ts +11 -0
  108. package/dist/mcp/tools/market.d.ts.map +1 -0
  109. package/dist/mcp/tools/market.js +314 -0
  110. package/dist/mcp/tools/market.js.map +1 -0
  111. package/dist/mcp/tools/order.d.ts +10 -0
  112. package/dist/mcp/tools/order.d.ts.map +1 -0
  113. package/dist/mcp/tools/order.js +258 -0
  114. package/dist/mcp/tools/order.js.map +1 -0
  115. package/dist/mcp/tools/trade.d.ts +38 -0
  116. package/dist/mcp/tools/trade.d.ts.map +1 -0
  117. package/dist/mcp/tools/trade.js +314 -0
  118. package/dist/mcp/tools/trade.js.map +1 -0
  119. package/dist/mcp/tools/trader.d.ts +11 -0
  120. package/dist/mcp/tools/trader.d.ts.map +1 -0
  121. package/dist/mcp/tools/trader.js +277 -0
  122. package/dist/mcp/tools/trader.js.map +1 -0
  123. package/dist/mcp/tools/wallet.d.ts +274 -0
  124. package/dist/mcp/tools/wallet.d.ts.map +1 -0
  125. package/dist/mcp/tools/wallet.js +579 -0
  126. package/dist/mcp/tools/wallet.js.map +1 -0
  127. package/dist/mcp/types.d.ts +413 -0
  128. package/dist/mcp/types.d.ts.map +1 -0
  129. package/dist/mcp/types.js +5 -0
  130. package/dist/mcp/types.js.map +1 -0
  131. package/dist/services/authorization-service.d.ts +97 -0
  132. package/dist/services/authorization-service.d.ts.map +1 -0
  133. package/dist/services/authorization-service.js +279 -0
  134. package/dist/services/authorization-service.js.map +1 -0
  135. package/dist/services/market-service.d.ts +108 -0
  136. package/dist/services/market-service.d.ts.map +1 -0
  137. package/dist/services/market-service.js +458 -0
  138. package/dist/services/market-service.js.map +1 -0
  139. package/dist/services/realtime-service.d.ts +82 -0
  140. package/dist/services/realtime-service.d.ts.map +1 -0
  141. package/dist/services/realtime-service.js +150 -0
  142. package/dist/services/realtime-service.js.map +1 -0
  143. package/dist/services/swap-service.d.ts +217 -0
  144. package/dist/services/swap-service.d.ts.map +1 -0
  145. package/dist/services/swap-service.js +695 -0
  146. package/dist/services/swap-service.js.map +1 -0
  147. package/dist/services/wallet-service.d.ts +94 -0
  148. package/dist/services/wallet-service.d.ts.map +1 -0
  149. package/dist/services/wallet-service.js +173 -0
  150. package/dist/services/wallet-service.js.map +1 -0
  151. package/dist/utils/price-utils.d.ts +153 -0
  152. package/dist/utils/price-utils.d.ts.map +1 -0
  153. package/dist/utils/price-utils.js +236 -0
  154. package/dist/utils/price-utils.js.map +1 -0
  155. package/docs/00-design.md +760 -0
  156. package/docs/01-mcp.md +2041 -0
  157. package/docs/02-API.md +1148 -0
  158. package/docs/e2e/01-trader-tools.md +159 -0
  159. package/docs/e2e/02-market-tools.md +180 -0
  160. package/docs/e2e/03-order-tools.md +166 -0
  161. package/docs/e2e/04-wallet-tools.md +224 -0
  162. package/docs/e2e/05-trading-tools.md +327 -0
  163. package/docs/e2e/06-integration-scenarios.md +481 -0
  164. package/docs/e2e/coordinator.md +376 -0
  165. package/examples/01-basic-usage.ts +68 -0
  166. package/examples/02-smart-money.ts +95 -0
  167. package/examples/03-market-analysis.ts +108 -0
  168. package/examples/04-kline-aggregation.ts +158 -0
  169. package/examples/05-follow-wallet-strategy.ts +156 -0
  170. package/examples/06-services-demo.ts +124 -0
  171. package/examples/07-realtime-websocket.ts +117 -0
  172. package/examples/08-trading-orders.ts +278 -0
  173. package/examples/09-rewards-tracking.ts +187 -0
  174. package/examples/10-ctf-operations.ts +336 -0
  175. package/examples/11-live-arbitrage-scan.ts +221 -0
  176. package/examples/12-trending-arb-monitor.ts +406 -0
  177. package/examples/README.md +179 -0
  178. package/package.json +62 -0
  179. package/scripts/README.md +163 -0
  180. package/scripts/approvals/approve-erc1155.ts +129 -0
  181. package/scripts/approvals/approve-neg-risk-erc1155.ts +149 -0
  182. package/scripts/approvals/approve-neg-risk.ts +102 -0
  183. package/scripts/approvals/check-all-allowances.ts +150 -0
  184. package/scripts/approvals/check-allowance.ts +129 -0
  185. package/scripts/approvals/check-ctf-approval.ts +158 -0
  186. package/scripts/datas/001-report.md +486 -0
  187. package/scripts/datas/clone-modal-screenshot.png +0 -0
  188. package/scripts/deposit/deposit-native-usdc.ts +179 -0
  189. package/scripts/deposit/deposit-usdc.ts +155 -0
  190. package/scripts/deposit/swap-usdc-to-usdce.ts +375 -0
  191. package/scripts/research/research-markets.ts +166 -0
  192. package/scripts/trading/check-orders.ts +50 -0
  193. package/scripts/trading/sell-nvidia-positions.ts +206 -0
  194. package/scripts/trading/test-order.ts +172 -0
  195. package/scripts/truth.md +440 -0
  196. package/scripts/verify/test-approve-trading.ts +98 -0
  197. package/scripts/verify/test-provider-fix.ts +43 -0
  198. package/scripts/verify/test-search-mcp.ts +113 -0
  199. package/scripts/verify/verify-all-apis.ts +160 -0
  200. package/scripts/wallet/check-wallet-balances.ts +75 -0
  201. package/scripts/wallet/test-wallet-operations.ts +191 -0
  202. package/scripts/wallet/verify-wallet-tools.ts +124 -0
  203. package/src/__tests__/clob-api.test.ts +301 -0
  204. package/src/__tests__/integration/bridge-client.integration.test.ts +314 -0
  205. package/src/__tests__/integration/clob-api.integration.test.ts +218 -0
  206. package/src/__tests__/integration/ctf-client.integration.test.ts +331 -0
  207. package/src/__tests__/integration/data-api.integration.test.ts +194 -0
  208. package/src/__tests__/integration/gamma-api.integration.test.ts +206 -0
  209. package/src/__tests__/test-utils.ts +170 -0
  210. package/src/clients/bridge-client.ts +841 -0
  211. package/src/clients/clob-api.ts +629 -0
  212. package/src/clients/ctf-client.ts +1216 -0
  213. package/src/clients/data-api.ts +469 -0
  214. package/src/clients/gamma-api.ts +597 -0
  215. package/src/clients/trading-client.ts +749 -0
  216. package/src/clients/websocket-manager.ts +267 -0
  217. package/src/core/cache-adapter-bridge.ts +94 -0
  218. package/src/core/cache.ts +85 -0
  219. package/src/core/errors.ts +117 -0
  220. package/src/core/rate-limiter.ts +74 -0
  221. package/src/core/types.ts +360 -0
  222. package/src/core/unified-cache.ts +153 -0
  223. package/src/index.ts +455 -0
  224. package/src/mcp/README.md +380 -0
  225. package/src/mcp/errors.ts +124 -0
  226. package/src/mcp/index.ts +309 -0
  227. package/src/mcp/server.ts +183 -0
  228. package/src/mcp/tools/guide.ts +821 -0
  229. package/src/mcp/tools/index.ts +73 -0
  230. package/src/mcp/tools/market.ts +363 -0
  231. package/src/mcp/tools/order.ts +326 -0
  232. package/src/mcp/tools/trade.ts +417 -0
  233. package/src/mcp/tools/trader.ts +322 -0
  234. package/src/mcp/tools/wallet.ts +683 -0
  235. package/src/mcp/types.ts +472 -0
  236. package/src/services/authorization-service.ts +357 -0
  237. package/src/services/market-service.ts +544 -0
  238. package/src/services/realtime-service.ts +196 -0
  239. package/src/services/swap-service.ts +896 -0
  240. package/src/services/wallet-service.ts +259 -0
  241. package/src/utils/price-utils.ts +307 -0
  242. package/tsconfig.json +8 -0
  243. package/vitest.config.ts +19 -0
  244. package/vitest.integration.config.ts +18 -0
@@ -0,0 +1,1216 @@
1
+ /**
2
+ * CTF (Conditional Token Framework) Client
3
+ *
4
+ * Provides on-chain operations for Polymarket's conditional tokens:
5
+ * - Split: USDC → YES + NO token pair
6
+ * - Merge: YES + NO → USDC
7
+ * - Redeem: Winning tokens → USDC (after market resolution)
8
+ *
9
+ * ⚠️ CRITICAL: Polymarket CTF uses USDC.e (bridged), NOT native USDC!
10
+ *
11
+ * | Token | Address | CTF Compatible |
12
+ * |---------------|--------------------------------------------|-----------------
13
+ * | USDC.e | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 | ✅ Yes |
14
+ * | Native USDC | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | ❌ No |
15
+ *
16
+ * Common Mistake:
17
+ * - Your wallet has native USDC but CTF operations fail
18
+ * - Solution: Use SwapService.transferUsdcE() or swap native USDC to USDC.e
19
+ *
20
+ * Based on: docs/01-product-research/06-poly-sdk/05-ctf-integration-plan.md
21
+ *
22
+ * Contract: Gnosis Conditional Tokens on Polygon
23
+ * https://docs.polymarket.com/developers/CTF/overview
24
+ */
25
+
26
+ import { ethers, Contract, Wallet, BigNumber } from 'ethers';
27
+
28
+ // ===== Contract Addresses (Polygon Mainnet) =====
29
+
30
+ export const CTF_CONTRACT = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
31
+
32
+ /**
33
+ * USDC.e (Bridged USDC) - The ONLY USDC accepted by Polymarket CTF
34
+ *
35
+ * ⚠️ WARNING: This is NOT native USDC (0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359)
36
+ *
37
+ * If your wallet has native USDC but CTF operations fail with "Insufficient USDC balance",
38
+ * you need to swap your native USDC to USDC.e first using:
39
+ * - SwapService.swap('USDC', 'USDC_E', amount)
40
+ * - Or transfer USDC.e using SwapService.transferUsdcE()
41
+ */
42
+ export const USDC_CONTRACT = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
43
+
44
+ /** Native USDC on Polygon - NOT compatible with CTF */
45
+ export const NATIVE_USDC_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
46
+
47
+ export const NEG_RISK_CTF_EXCHANGE = '0xC5d563A36AE78145C45a50134d48A1215220f80a';
48
+ export const NEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296';
49
+
50
+ // USDC.e uses 6 decimals
51
+ export const USDC_DECIMALS = 6;
52
+
53
+ // ===== ABIs =====
54
+
55
+ const CTF_ABI = [
56
+ // Split: USDC → YES + NO
57
+ 'function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount) external',
58
+ // Merge: YES + NO → USDC
59
+ 'function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount) external',
60
+ // Redeem: Winning tokens → USDC
61
+ 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets) external',
62
+ // Balance query
63
+ 'function balanceOf(address account, uint256 positionId) view returns (uint256)',
64
+ // Check if condition is resolved
65
+ 'function payoutNumerators(bytes32 conditionId, uint256 outcomeIndex) view returns (uint256)',
66
+ 'function payoutDenominator(bytes32 conditionId) view returns (uint256)',
67
+ ];
68
+
69
+ const ERC20_ABI = [
70
+ 'function approve(address spender, uint256 amount) returns (bool)',
71
+ 'function allowance(address owner, address spender) view returns (uint256)',
72
+ 'function balanceOf(address account) view returns (uint256)',
73
+ 'function decimals() view returns (uint8)',
74
+ ];
75
+
76
+ // ===== Types =====
77
+
78
+ export interface CTFConfig {
79
+ /** Private key for signing transactions */
80
+ privateKey: string;
81
+ /** RPC URL (default: Polygon mainnet) */
82
+ rpcUrl?: string;
83
+ /** Chain ID (default: 137 for Polygon) */
84
+ chainId?: number;
85
+ /** Gas price multiplier (default: 1.2) */
86
+ gasPriceMultiplier?: number;
87
+ /** Transaction confirmation blocks (default: 1) */
88
+ confirmations?: number;
89
+ /** Transaction timeout in ms (default: 60000) */
90
+ txTimeout?: number;
91
+ }
92
+
93
+ export interface GasEstimate {
94
+ /** Estimated gas units */
95
+ gasUnits: string;
96
+ /** Gas price in gwei */
97
+ gasPriceGwei: string;
98
+ /** Estimated cost in MATIC */
99
+ costMatic: string;
100
+ /** Estimated cost in USDC (at current MATIC price) */
101
+ costUsdc: string;
102
+ /** MATIC/USDC price used */
103
+ maticPrice: number;
104
+ }
105
+
106
+ export interface TransactionStatus {
107
+ txHash: string;
108
+ status: 'pending' | 'confirmed' | 'failed' | 'reverted';
109
+ confirmations: number;
110
+ blockNumber?: number;
111
+ gasUsed?: string;
112
+ effectiveGasPrice?: string;
113
+ errorReason?: string;
114
+ }
115
+
116
+ /** Common revert reasons */
117
+ export enum RevertReason {
118
+ INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
119
+ INSUFFICIENT_ALLOWANCE = 'INSUFFICIENT_ALLOWANCE',
120
+ CONDITION_NOT_RESOLVED = 'CONDITION_NOT_RESOLVED',
121
+ INVALID_PARTITION = 'INVALID_PARTITION',
122
+ INVALID_CONDITION = 'INVALID_CONDITION',
123
+ EXECUTION_REVERTED = 'EXECUTION_REVERTED',
124
+ TIMEOUT = 'TIMEOUT',
125
+ UNKNOWN = 'UNKNOWN',
126
+ }
127
+
128
+ export interface SplitResult {
129
+ success: boolean;
130
+ txHash: string;
131
+ amount: string;
132
+ yesTokens: string;
133
+ noTokens: string;
134
+ gasUsed?: string;
135
+ }
136
+
137
+ export interface MergeResult {
138
+ success: boolean;
139
+ txHash: string;
140
+ amount: string;
141
+ usdcReceived: string;
142
+ gasUsed?: string;
143
+ }
144
+
145
+ export interface RedeemResult {
146
+ success: boolean;
147
+ txHash: string;
148
+ outcome: 'YES' | 'NO';
149
+ tokensRedeemed: string;
150
+ usdcReceived: string;
151
+ gasUsed?: string;
152
+ }
153
+
154
+ export interface PositionBalance {
155
+ conditionId: string;
156
+ yesBalance: string;
157
+ noBalance: string;
158
+ yesPositionId: string;
159
+ noPositionId: string;
160
+ }
161
+
162
+ export interface TokenIds {
163
+ yesTokenId: string;
164
+ noTokenId: string;
165
+ }
166
+
167
+ export interface MarketResolution {
168
+ conditionId: string;
169
+ isResolved: boolean;
170
+ winningOutcome?: 'YES' | 'NO';
171
+ payoutNumerators: [number, number];
172
+ payoutDenominator: number;
173
+ }
174
+
175
+ // ===== CTF Client =====
176
+
177
+ // Default MATIC price (updated via getMaticPrice)
178
+ const DEFAULT_MATIC_PRICE = 0.50;
179
+
180
+ export class CTFClient {
181
+ private provider: ethers.providers.JsonRpcProvider;
182
+ private wallet: Wallet;
183
+ private ctfContract: Contract;
184
+ private usdcContract: Contract;
185
+ private gasPriceMultiplier: number;
186
+ private confirmations: number;
187
+ private txTimeout: number;
188
+ private cachedMaticPrice: number = DEFAULT_MATIC_PRICE;
189
+ private maticPriceLastUpdated: number = 0;
190
+
191
+ constructor(config: CTFConfig) {
192
+ const rpcUrl = config.rpcUrl || 'https://polygon-rpc.com';
193
+ this.provider = new ethers.providers.JsonRpcProvider(rpcUrl);
194
+ this.wallet = new Wallet(config.privateKey, this.provider);
195
+ this.ctfContract = new Contract(CTF_CONTRACT, CTF_ABI, this.wallet);
196
+ this.usdcContract = new Contract(USDC_CONTRACT, ERC20_ABI, this.wallet);
197
+ this.gasPriceMultiplier = config.gasPriceMultiplier || 1.2;
198
+ this.confirmations = config.confirmations || 1;
199
+ this.txTimeout = config.txTimeout || 60000;
200
+ }
201
+
202
+ /**
203
+ * Get wallet address
204
+ */
205
+ getAddress(): string {
206
+ return this.wallet.address;
207
+ }
208
+
209
+ /**
210
+ * Get USDC.e (bridged USDC) balance - the token used by Polymarket CTF
211
+ *
212
+ * ⚠️ Note: This returns USDC.e balance, NOT native USDC balance.
213
+ * Polymarket CTF only accepts USDC.e (0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174).
214
+ *
215
+ * Common issue: Your wallet shows USDC balance but this returns 0
216
+ * - This means you have native USDC, not USDC.e
217
+ * - Use SwapService.swap('USDC', 'USDC_E', amount) to convert
218
+ */
219
+ async getUsdcBalance(): Promise<string> {
220
+ const balance = await this.usdcContract.balanceOf(this.wallet.address);
221
+ return ethers.utils.formatUnits(balance, USDC_DECIMALS);
222
+ }
223
+
224
+ /**
225
+ * Get native USDC balance (for comparison/debugging)
226
+ *
227
+ * This is NOT the token used by CTF. Use getUsdcBalance() for CTF operations.
228
+ */
229
+ async getNativeUsdcBalance(): Promise<string> {
230
+ const nativeUsdcContract = new Contract(NATIVE_USDC_CONTRACT, ERC20_ABI, this.provider);
231
+ const balance = await nativeUsdcContract.balanceOf(this.wallet.address);
232
+ return ethers.utils.formatUnits(balance, USDC_DECIMALS);
233
+ }
234
+
235
+ /**
236
+ * Check if wallet is ready for CTF trading operations
237
+ *
238
+ * Verifies:
239
+ * - Has sufficient USDC.e (not native USDC)
240
+ * - Has MATIC for gas fees
241
+ *
242
+ * @param amount - Minimum USDC.e amount needed (e.g., "100" for 100 USDC.e)
243
+ * @returns Ready status with balances and suggestions
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const status = await ctf.checkReadyForCTF('100');
248
+ * if (!status.ready) {
249
+ * console.log(status.suggestion);
250
+ * // "You have 50 native USDC but 0 USDC.e. Swap native USDC to USDC.e first."
251
+ * }
252
+ * ```
253
+ */
254
+ async checkReadyForCTF(amount: string): Promise<{
255
+ ready: boolean;
256
+ usdcEBalance: string;
257
+ nativeUsdcBalance: string;
258
+ maticBalance: string;
259
+ suggestion?: string;
260
+ }> {
261
+ const [usdcE, nativeUsdc, matic] = await Promise.all([
262
+ this.getUsdcBalance(),
263
+ this.getNativeUsdcBalance(),
264
+ this.provider.getBalance(this.wallet.address),
265
+ ]);
266
+
267
+ const usdcEBalance = parseFloat(usdcE);
268
+ const nativeUsdcBalance = parseFloat(nativeUsdc);
269
+ const maticBalance = parseFloat(ethers.utils.formatEther(matic));
270
+ const amountNeeded = parseFloat(amount);
271
+
272
+ const result = {
273
+ ready: false,
274
+ usdcEBalance: usdcE,
275
+ nativeUsdcBalance: nativeUsdc,
276
+ maticBalance: ethers.utils.formatEther(matic),
277
+ suggestion: undefined as string | undefined,
278
+ };
279
+
280
+ // Check MATIC for gas
281
+ if (maticBalance < 0.01) {
282
+ result.suggestion = `Insufficient MATIC for gas fees. Have: ${maticBalance.toFixed(4)} MATIC, need at least 0.01 MATIC.`;
283
+ return result;
284
+ }
285
+
286
+ // Check USDC.e balance
287
+ if (usdcEBalance < amountNeeded) {
288
+ if (nativeUsdcBalance >= amountNeeded) {
289
+ result.suggestion = `You have ${nativeUsdcBalance.toFixed(2)} native USDC but only ${usdcEBalance.toFixed(2)} USDC.e. ` +
290
+ `Polymarket CTF requires USDC.e. Use SwapService.swap('USDC', 'USDC_E', '${amount}') to convert.`;
291
+ } else if (nativeUsdcBalance > 0) {
292
+ result.suggestion = `Insufficient USDC.e. Have: ${usdcEBalance.toFixed(2)} USDC.e + ${nativeUsdcBalance.toFixed(2)} native USDC, need: ${amount} USDC.e. ` +
293
+ `Swap all native USDC to USDC.e, then add more funds.`;
294
+ } else {
295
+ result.suggestion = `Insufficient USDC.e. Have: ${usdcEBalance.toFixed(2)} USDC.e, need: ${amount} USDC.e.`;
296
+ }
297
+ return result;
298
+ }
299
+
300
+ result.ready = true;
301
+ return result;
302
+ }
303
+
304
+ /**
305
+ * Split USDC into YES + NO tokens
306
+ *
307
+ * @param conditionId - Market condition ID
308
+ * @param amount - USDC amount (e.g., "100" for 100 USDC)
309
+ * @returns SplitResult with transaction details
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * const result = await ctf.split(conditionId, "100");
314
+ * console.log(`Split ${result.amount} USDC into tokens`);
315
+ * console.log(`TX: ${result.txHash}`);
316
+ * ```
317
+ */
318
+ async split(conditionId: string, amount: string): Promise<SplitResult> {
319
+ const amountWei = ethers.utils.parseUnits(amount, USDC_DECIMALS);
320
+
321
+ // 1. Check USDC balance
322
+ const balance = await this.usdcContract.balanceOf(this.wallet.address);
323
+ if (balance.lt(amountWei)) {
324
+ throw new Error(`Insufficient USDC balance. Have: ${ethers.utils.formatUnits(balance, USDC_DECIMALS)}, Need: ${amount}`);
325
+ }
326
+
327
+ // 2. Check and approve USDC if needed
328
+ const allowance = await this.usdcContract.allowance(this.wallet.address, CTF_CONTRACT);
329
+ if (allowance.lt(amountWei)) {
330
+ const approveTx = await this.usdcContract.approve(
331
+ CTF_CONTRACT,
332
+ ethers.constants.MaxUint256,
333
+ await this.getGasOptions()
334
+ );
335
+ await approveTx.wait();
336
+ }
337
+
338
+ // 3. Execute split
339
+ // Partition [1, 2] represents [YES, NO] outcomes
340
+ const tx = await this.ctfContract.splitPosition(
341
+ USDC_CONTRACT,
342
+ ethers.constants.HashZero, // parentCollectionId = 0 for Polymarket
343
+ conditionId,
344
+ [1, 2], // partition for YES/NO
345
+ amountWei,
346
+ await this.getGasOptions()
347
+ );
348
+
349
+ const receipt = await tx.wait();
350
+
351
+ return {
352
+ success: true,
353
+ txHash: receipt.transactionHash,
354
+ amount,
355
+ yesTokens: amount, // 1:1 split
356
+ noTokens: amount,
357
+ gasUsed: receipt.gasUsed.toString(),
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Merge YES + NO tokens back to USDC
363
+ *
364
+ * @param conditionId - Market condition ID
365
+ * @param amount - Number of token pairs to merge (e.g., "100" for 100 YES + 100 NO)
366
+ * @returns MergeResult with transaction details
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * // After buying 100 YES and 100 NO via TradingClient
371
+ * const result = await ctf.merge(conditionId, "100");
372
+ * console.log(`Received ${result.usdcReceived} USDC`);
373
+ * ```
374
+ */
375
+ async merge(conditionId: string, amount: string): Promise<MergeResult> {
376
+ const amountWei = ethers.utils.parseUnits(amount, USDC_DECIMALS);
377
+
378
+ // Check token balances
379
+ const balances = await this.getPositionBalance(conditionId);
380
+ const yesBalance = ethers.utils.parseUnits(balances.yesBalance, USDC_DECIMALS);
381
+ const noBalance = ethers.utils.parseUnits(balances.noBalance, USDC_DECIMALS);
382
+
383
+ if (yesBalance.lt(amountWei) || noBalance.lt(amountWei)) {
384
+ throw new Error(
385
+ `Insufficient token balance. Need ${amount} of each. Have: YES=${balances.yesBalance}, NO=${balances.noBalance}`
386
+ );
387
+ }
388
+
389
+ // Execute merge
390
+ const tx = await this.ctfContract.mergePositions(
391
+ USDC_CONTRACT,
392
+ ethers.constants.HashZero,
393
+ conditionId,
394
+ [1, 2],
395
+ amountWei,
396
+ await this.getGasOptions()
397
+ );
398
+
399
+ const receipt = await tx.wait();
400
+
401
+ return {
402
+ success: true,
403
+ txHash: receipt.transactionHash,
404
+ amount,
405
+ usdcReceived: amount, // 1:1 merge
406
+ gasUsed: receipt.gasUsed.toString(),
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Merge YES and NO tokens back into USDC using explicit token IDs
412
+ *
413
+ * This method uses the provided token IDs for balance checking, which is
414
+ * necessary when working with Polymarket CLOB markets where token IDs
415
+ * don't match the calculated position IDs.
416
+ *
417
+ * @param conditionId - Market condition ID
418
+ * @param tokenIds - Token IDs from CLOB API
419
+ * @param amount - Amount of tokens to merge
420
+ * @returns MergeResult with transaction details
421
+ */
422
+ async mergeByTokenIds(conditionId: string, tokenIds: TokenIds, amount: string): Promise<MergeResult> {
423
+ const amountWei = ethers.utils.parseUnits(amount, USDC_DECIMALS);
424
+
425
+ // Check token balances using the provided token IDs
426
+ const balances = await this.getPositionBalanceByTokenIds(conditionId, tokenIds);
427
+ const yesBalance = ethers.utils.parseUnits(balances.yesBalance, USDC_DECIMALS);
428
+ const noBalance = ethers.utils.parseUnits(balances.noBalance, USDC_DECIMALS);
429
+
430
+ if (yesBalance.lt(amountWei) || noBalance.lt(amountWei)) {
431
+ throw new Error(
432
+ `Insufficient token balance. Need ${amount} of each. Have: YES=${balances.yesBalance}, NO=${balances.noBalance}`
433
+ );
434
+ }
435
+
436
+ // Execute merge
437
+ const tx = await this.ctfContract.mergePositions(
438
+ USDC_CONTRACT,
439
+ ethers.constants.HashZero,
440
+ conditionId,
441
+ [1, 2],
442
+ amountWei,
443
+ await this.getGasOptions()
444
+ );
445
+
446
+ const receipt = await tx.wait();
447
+
448
+ return {
449
+ success: true,
450
+ txHash: receipt.transactionHash,
451
+ amount,
452
+ usdcReceived: amount, // 1:1 merge
453
+ gasUsed: receipt.gasUsed.toString(),
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Redeem winning tokens after market resolution (Standard CTF)
459
+ *
460
+ * ⚠️ IMPORTANT: This method uses standard CTF position ID calculation.
461
+ * It is ONLY suitable for:
462
+ * - Standard Gnosis CTF markets (non-Polymarket)
463
+ * - Markets where position IDs are calculated from conditionId using standard formula
464
+ * - Direct CTF contract interactions without CLOB
465
+ *
466
+ * ❌ DO NOT USE for Polymarket CLOB markets!
467
+ * Polymarket uses custom token IDs that differ from standard CTF position IDs.
468
+ * For Polymarket, use `redeemByTokenIds()` instead.
469
+ *
470
+ * Position ID calculation: keccak256(collectionId, conditionId, indexSet)
471
+ * - This formula may NOT match Polymarket's token IDs
472
+ *
473
+ * @param conditionId - Market condition ID
474
+ * @param outcome - 'YES' or 'NO' (optional, auto-detects if not provided)
475
+ * @returns RedeemResult with transaction details
476
+ *
477
+ * @example
478
+ * ```typescript
479
+ * // For standard CTF markets (NOT Polymarket)
480
+ * const result = await ctf.redeem(conditionId);
481
+ * console.log(`Redeemed ${result.tokensRedeemed} ${result.outcome} tokens`);
482
+ * ```
483
+ *
484
+ * @see redeemByTokenIds - Use this for Polymarket CLOB markets
485
+ */
486
+ async redeem(conditionId: string, outcome?: 'YES' | 'NO'): Promise<RedeemResult> {
487
+ // Check resolution status
488
+ const resolution = await this.getMarketResolution(conditionId);
489
+ if (!resolution.isResolved) {
490
+ throw new Error('Market is not resolved yet');
491
+ }
492
+
493
+ // Auto-detect outcome if not provided
494
+ const winningOutcome = outcome || resolution.winningOutcome;
495
+ if (!winningOutcome) {
496
+ throw new Error('Could not determine winning outcome');
497
+ }
498
+
499
+ // Get token balance
500
+ const balances = await this.getPositionBalance(conditionId);
501
+ const tokenBalance = winningOutcome === 'YES' ? balances.yesBalance : balances.noBalance;
502
+
503
+ if (parseFloat(tokenBalance) === 0) {
504
+ throw new Error(`No ${winningOutcome} tokens to redeem`);
505
+ }
506
+
507
+ // indexSets: [1] for YES, [2] for NO
508
+ const indexSets = winningOutcome === 'YES' ? [1] : [2];
509
+
510
+ const tx = await this.ctfContract.redeemPositions(
511
+ USDC_CONTRACT,
512
+ ethers.constants.HashZero,
513
+ conditionId,
514
+ indexSets,
515
+ await this.getGasOptions()
516
+ );
517
+
518
+ const receipt = await tx.wait();
519
+
520
+ return {
521
+ success: true,
522
+ txHash: receipt.transactionHash,
523
+ outcome: winningOutcome,
524
+ tokensRedeemed: tokenBalance,
525
+ usdcReceived: tokenBalance, // 1:1 for winning outcome
526
+ gasUsed: receipt.gasUsed.toString(),
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Redeem winning tokens using Polymarket token IDs (Polymarket CLOB)
532
+ *
533
+ * ✅ USE THIS for Polymarket CLOB markets!
534
+ *
535
+ * Polymarket uses custom token IDs that are different from standard CTF position IDs.
536
+ * These token IDs are provided by the CLOB API and must be used for:
537
+ * - Querying balances (getPositionBalanceByTokenIds)
538
+ * - Redeeming positions (this method)
539
+ * - Trading via CLOB API
540
+ *
541
+ * Why Polymarket token IDs differ:
542
+ * - Polymarket wraps CTF positions into ERC-1155 tokens with custom IDs
543
+ * - The token IDs from CLOB API (e.g., "25064375...") are NOT the same as
544
+ * calculated position IDs from keccak256(collectionId, conditionId, indexSet)
545
+ *
546
+ * @param conditionId - The condition ID of the market
547
+ * @param tokenIds - The Polymarket token IDs for YES and NO outcomes (from CLOB API)
548
+ * @param outcome - Optional: which outcome to redeem ('YES' or 'NO'). Auto-detects if not provided.
549
+ * @returns RedeemResult with transaction details
550
+ *
551
+ * @example
552
+ * ```typescript
553
+ * // For Polymarket CLOB markets
554
+ * const tokenIds = {
555
+ * yesTokenId: '25064375110792967023484002819116042931016336431092144471807003884255851454283',
556
+ * noTokenId: '98190367690492181203391990709979106077460946443309150166954079213761598385827',
557
+ * };
558
+ * const result = await ctf.redeemByTokenIds(conditionId, tokenIds);
559
+ * console.log(`Redeemed ${result.tokensRedeemed} ${result.outcome} tokens`);
560
+ * console.log(`Received ${result.usdcReceived} USDC`);
561
+ * ```
562
+ *
563
+ * @see redeem - Only use for standard CTF markets (non-Polymarket)
564
+ */
565
+ async redeemByTokenIds(
566
+ conditionId: string,
567
+ tokenIds: TokenIds,
568
+ outcome?: 'YES' | 'NO'
569
+ ): Promise<RedeemResult> {
570
+ // Check resolution status
571
+ const resolution = await this.getMarketResolution(conditionId);
572
+ if (!resolution.isResolved) {
573
+ throw new Error('Market is not resolved yet');
574
+ }
575
+
576
+ // Auto-detect outcome if not provided
577
+ const winningOutcome = outcome || resolution.winningOutcome;
578
+ if (!winningOutcome) {
579
+ throw new Error('Could not determine winning outcome');
580
+ }
581
+
582
+ // Get token balance using Polymarket token IDs
583
+ const balances = await this.getPositionBalanceByTokenIds(conditionId, tokenIds);
584
+ const tokenBalance = winningOutcome === 'YES' ? balances.yesBalance : balances.noBalance;
585
+
586
+ if (parseFloat(tokenBalance) === 0) {
587
+ throw new Error(`No ${winningOutcome} tokens to redeem`);
588
+ }
589
+
590
+ // indexSets: [1] for YES, [2] for NO
591
+ const indexSets = winningOutcome === 'YES' ? [1] : [2];
592
+
593
+ const tx = await this.ctfContract.redeemPositions(
594
+ USDC_CONTRACT,
595
+ ethers.constants.HashZero,
596
+ conditionId,
597
+ indexSets,
598
+ await this.getGasOptions()
599
+ );
600
+
601
+ const receipt = await tx.wait();
602
+
603
+ return {
604
+ success: true,
605
+ txHash: receipt.transactionHash,
606
+ outcome: winningOutcome,
607
+ tokensRedeemed: tokenBalance,
608
+ usdcReceived: tokenBalance, // 1:1 for winning outcome
609
+ gasUsed: receipt.gasUsed.toString(),
610
+ };
611
+ }
612
+
613
+ /**
614
+ * Get token balances for a market using calculated position IDs
615
+ *
616
+ * NOTE: This method calculates position IDs from conditionId, which may not match
617
+ * the token IDs used by Polymarket's CLOB API. For accurate balances when working
618
+ * with CLOB markets, use getPositionBalanceByTokenIds() with the token IDs from
619
+ * the CLOB API.
620
+ *
621
+ * @deprecated Use getPositionBalanceByTokenIds for CLOB markets
622
+ */
623
+ async getPositionBalance(conditionId: string): Promise<PositionBalance> {
624
+ const yesPositionId = this.calculatePositionId(conditionId, 1);
625
+ const noPositionId = this.calculatePositionId(conditionId, 2);
626
+
627
+ const [yesBalance, noBalance] = await Promise.all([
628
+ this.ctfContract.balanceOf(this.wallet.address, yesPositionId),
629
+ this.ctfContract.balanceOf(this.wallet.address, noPositionId),
630
+ ]);
631
+
632
+ return {
633
+ conditionId,
634
+ yesBalance: ethers.utils.formatUnits(yesBalance, USDC_DECIMALS),
635
+ noBalance: ethers.utils.formatUnits(noBalance, USDC_DECIMALS),
636
+ yesPositionId,
637
+ noPositionId,
638
+ };
639
+ }
640
+
641
+ /**
642
+ * Get token balances using CLOB API token IDs
643
+ *
644
+ * This is the recommended method for checking balances when working with
645
+ * Polymarket CLOB markets. The token IDs should be obtained from the CLOB API
646
+ * (e.g., from ClobApiClient.getMarket()).
647
+ *
648
+ * @param conditionId - Market condition ID (for reference)
649
+ * @param tokenIds - Token IDs from CLOB API { yesTokenId, noTokenId }
650
+ * @returns PositionBalance with accurate balances
651
+ *
652
+ * @example
653
+ * ```typescript
654
+ * // Get token IDs from CLOB API
655
+ * const market = await clobApi.getMarket(conditionId);
656
+ * const tokenIds = {
657
+ * yesTokenId: market.tokens[0].tokenId,
658
+ * noTokenId: market.tokens[1].tokenId,
659
+ * };
660
+ *
661
+ * // Check balances
662
+ * const balance = await ctf.getPositionBalanceByTokenIds(conditionId, tokenIds);
663
+ * console.log(`YES: ${balance.yesBalance}, NO: ${balance.noBalance}`);
664
+ * ```
665
+ */
666
+ async getPositionBalanceByTokenIds(
667
+ conditionId: string,
668
+ tokenIds: TokenIds
669
+ ): Promise<PositionBalance> {
670
+ const [yesBalance, noBalance] = await Promise.all([
671
+ this.ctfContract.balanceOf(this.wallet.address, tokenIds.yesTokenId),
672
+ this.ctfContract.balanceOf(this.wallet.address, tokenIds.noTokenId),
673
+ ]);
674
+
675
+ return {
676
+ conditionId,
677
+ yesBalance: ethers.utils.formatUnits(yesBalance, USDC_DECIMALS),
678
+ noBalance: ethers.utils.formatUnits(noBalance, USDC_DECIMALS),
679
+ yesPositionId: tokenIds.yesTokenId,
680
+ noPositionId: tokenIds.noTokenId,
681
+ };
682
+ }
683
+
684
+ /**
685
+ * Check if a market is resolved and get payout info
686
+ */
687
+ async getMarketResolution(conditionId: string): Promise<MarketResolution> {
688
+ const [yesNumerator, noNumerator, denominator] = await Promise.all([
689
+ this.ctfContract.payoutNumerators(conditionId, 0),
690
+ this.ctfContract.payoutNumerators(conditionId, 1),
691
+ this.ctfContract.payoutDenominator(conditionId),
692
+ ]);
693
+
694
+ const isResolved = denominator.gt(0);
695
+ let winningOutcome: 'YES' | 'NO' | undefined;
696
+
697
+ if (isResolved) {
698
+ if (yesNumerator.gt(0) && noNumerator.eq(0)) {
699
+ winningOutcome = 'YES';
700
+ } else if (noNumerator.gt(0) && yesNumerator.eq(0)) {
701
+ winningOutcome = 'NO';
702
+ }
703
+ // If both are non-zero, it's a split resolution (rare)
704
+ }
705
+
706
+ return {
707
+ conditionId,
708
+ isResolved,
709
+ winningOutcome,
710
+ payoutNumerators: [yesNumerator.toNumber(), noNumerator.toNumber()],
711
+ payoutDenominator: denominator.toNumber(),
712
+ };
713
+ }
714
+
715
+ /**
716
+ * Estimate gas for split operation
717
+ */
718
+ async estimateSplitGas(conditionId: string, amount: string): Promise<string> {
719
+ const amountWei = ethers.utils.parseUnits(amount, USDC_DECIMALS);
720
+ try {
721
+ const gas = await this.ctfContract.estimateGas.splitPosition(
722
+ USDC_CONTRACT,
723
+ ethers.constants.HashZero,
724
+ conditionId,
725
+ [1, 2],
726
+ amountWei
727
+ );
728
+ return gas.toString();
729
+ } catch {
730
+ // Default estimate if call fails (e.g., insufficient balance)
731
+ return '250000';
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Estimate gas for merge operation
737
+ */
738
+ async estimateMergeGas(conditionId: string, amount: string): Promise<string> {
739
+ const amountWei = ethers.utils.parseUnits(amount, USDC_DECIMALS);
740
+ try {
741
+ const gas = await this.ctfContract.estimateGas.mergePositions(
742
+ USDC_CONTRACT,
743
+ ethers.constants.HashZero,
744
+ conditionId,
745
+ [1, 2],
746
+ amountWei
747
+ );
748
+ return gas.toString();
749
+ } catch {
750
+ return '200000';
751
+ }
752
+ }
753
+
754
+ // ===== Gas Estimation (Phase 3) =====
755
+
756
+ /**
757
+ * Get detailed gas estimate for a split operation
758
+ */
759
+ async getDetailedSplitGasEstimate(conditionId: string, amount: string): Promise<GasEstimate> {
760
+ const gasUnits = await this.estimateSplitGas(conditionId, amount);
761
+ return this.calculateGasCost(gasUnits);
762
+ }
763
+
764
+ /**
765
+ * Get detailed gas estimate for a merge operation
766
+ */
767
+ async getDetailedMergeGasEstimate(conditionId: string, amount: string): Promise<GasEstimate> {
768
+ const gasUnits = await this.estimateMergeGas(conditionId, amount);
769
+ return this.calculateGasCost(gasUnits);
770
+ }
771
+
772
+ /**
773
+ * Get current gas price info
774
+ */
775
+ async getGasPrice(): Promise<{ gwei: string; wei: string }> {
776
+ const gasPrice = await this.provider.getGasPrice();
777
+ return {
778
+ gwei: ethers.utils.formatUnits(gasPrice, 'gwei'),
779
+ wei: gasPrice.toString(),
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Get or refresh MATIC price (cached for 5 minutes)
785
+ */
786
+ async getMaticPrice(): Promise<number> {
787
+ const now = Date.now();
788
+ const cacheAge = now - this.maticPriceLastUpdated;
789
+
790
+ // Use cache if less than 5 minutes old
791
+ if (cacheAge < 5 * 60 * 1000 && this.maticPriceLastUpdated > 0) {
792
+ return this.cachedMaticPrice;
793
+ }
794
+
795
+ // In production, this would fetch from an oracle or price feed
796
+ // For now, we return a reasonable estimate
797
+ // Could integrate with Chainlink price feeds or CoinGecko API
798
+ this.cachedMaticPrice = DEFAULT_MATIC_PRICE;
799
+ this.maticPriceLastUpdated = now;
800
+
801
+ return this.cachedMaticPrice;
802
+ }
803
+
804
+ /**
805
+ * Set MATIC price manually (for testing or when external price is available)
806
+ */
807
+ setMaticPrice(price: number): void {
808
+ this.cachedMaticPrice = price;
809
+ this.maticPriceLastUpdated = Date.now();
810
+ }
811
+
812
+ // ===== Transaction Monitoring (Phase 3) =====
813
+
814
+ /**
815
+ * Get transaction status with detailed info
816
+ */
817
+ async getTransactionStatus(txHash: string): Promise<TransactionStatus> {
818
+ try {
819
+ const receipt = await this.provider.getTransactionReceipt(txHash);
820
+
821
+ if (!receipt) {
822
+ // Transaction is pending
823
+ const tx = await this.provider.getTransaction(txHash);
824
+ if (!tx) {
825
+ return {
826
+ txHash,
827
+ status: 'failed',
828
+ confirmations: 0,
829
+ errorReason: 'Transaction not found',
830
+ };
831
+ }
832
+ return {
833
+ txHash,
834
+ status: 'pending',
835
+ confirmations: 0,
836
+ };
837
+ }
838
+
839
+ const currentBlock = await this.provider.getBlockNumber();
840
+ const confirmations = currentBlock - receipt.blockNumber + 1;
841
+
842
+ if (receipt.status === 0) {
843
+ // Transaction reverted
844
+ const reason = await this.getRevertReason(txHash);
845
+ return {
846
+ txHash,
847
+ status: 'reverted',
848
+ confirmations,
849
+ blockNumber: receipt.blockNumber,
850
+ gasUsed: receipt.gasUsed.toString(),
851
+ effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
852
+ errorReason: reason,
853
+ };
854
+ }
855
+
856
+ return {
857
+ txHash,
858
+ status: 'confirmed',
859
+ confirmations,
860
+ blockNumber: receipt.blockNumber,
861
+ gasUsed: receipt.gasUsed.toString(),
862
+ effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
863
+ };
864
+ } catch (error) {
865
+ return {
866
+ txHash,
867
+ status: 'failed',
868
+ confirmations: 0,
869
+ errorReason: error instanceof Error ? error.message : 'Unknown error',
870
+ };
871
+ }
872
+ }
873
+
874
+ /**
875
+ * Wait for transaction confirmation with timeout
876
+ */
877
+ async waitForTransaction(txHash: string, confirmations?: number): Promise<TransactionStatus> {
878
+ const targetConfirmations = confirmations ?? this.confirmations;
879
+ const startTime = Date.now();
880
+
881
+ while (Date.now() - startTime < this.txTimeout) {
882
+ const status = await this.getTransactionStatus(txHash);
883
+
884
+ if (status.status === 'reverted' || status.status === 'failed') {
885
+ return status;
886
+ }
887
+
888
+ if (status.status === 'confirmed' && status.confirmations >= targetConfirmations) {
889
+ return status;
890
+ }
891
+
892
+ // Wait 2 seconds before checking again
893
+ await new Promise(resolve => setTimeout(resolve, 2000));
894
+ }
895
+
896
+ return {
897
+ txHash,
898
+ status: 'pending',
899
+ confirmations: 0,
900
+ errorReason: `Timeout after ${this.txTimeout}ms`,
901
+ };
902
+ }
903
+
904
+ /**
905
+ * Parse revert reason from transaction
906
+ */
907
+ async getRevertReason(txHash: string): Promise<string> {
908
+ try {
909
+ const tx = await this.provider.getTransaction(txHash);
910
+ if (!tx) return RevertReason.UNKNOWN;
911
+
912
+ const receipt = await this.provider.getTransactionReceipt(txHash);
913
+ if (!receipt || receipt.status !== 0) return RevertReason.UNKNOWN;
914
+
915
+ // Try to call the transaction to get the revert reason
916
+ try {
917
+ await this.provider.call(tx as ethers.providers.TransactionRequest, tx.blockNumber);
918
+ return RevertReason.UNKNOWN;
919
+ } catch (error: unknown) {
920
+ const err = error as { reason?: string; message?: string; data?: string };
921
+ if (err.reason) return err.reason;
922
+ if (err.message) {
923
+ // Parse common error messages
924
+ if (err.message.includes('insufficient balance')) {
925
+ return RevertReason.INSUFFICIENT_BALANCE;
926
+ }
927
+ if (err.message.includes('allowance')) {
928
+ return RevertReason.INSUFFICIENT_ALLOWANCE;
929
+ }
930
+ if (err.message.includes('condition not resolved')) {
931
+ return RevertReason.CONDITION_NOT_RESOLVED;
932
+ }
933
+ return err.message;
934
+ }
935
+ return RevertReason.EXECUTION_REVERTED;
936
+ }
937
+ } catch {
938
+ return RevertReason.UNKNOWN;
939
+ }
940
+ }
941
+
942
+ // ===== Position Tracking (Phase 3) =====
943
+
944
+ /**
945
+ * Get all positions for the wallet across multiple markets
946
+ */
947
+ async getAllPositions(conditionIds: string[]): Promise<PositionBalance[]> {
948
+ const positions: PositionBalance[] = [];
949
+
950
+ for (const conditionId of conditionIds) {
951
+ try {
952
+ const balance = await this.getPositionBalance(conditionId);
953
+ // Only include non-zero balances
954
+ if (parseFloat(balance.yesBalance) > 0 || parseFloat(balance.noBalance) > 0) {
955
+ positions.push(balance);
956
+ }
957
+ } catch {
958
+ // Skip errors for individual markets
959
+ }
960
+ }
961
+
962
+ return positions;
963
+ }
964
+
965
+ /**
966
+ * Check if wallet has sufficient tokens for merge
967
+ *
968
+ * @deprecated Use canMergeWithTokenIds for CLOB markets
969
+ */
970
+ async canMerge(conditionId: string, amount: string): Promise<{ canMerge: boolean; reason?: string }> {
971
+ try {
972
+ const balances = await this.getPositionBalance(conditionId);
973
+ return this.checkMergeBalance(balances, amount);
974
+ } catch (error) {
975
+ return {
976
+ canMerge: false,
977
+ reason: error instanceof Error ? error.message : 'Failed to check balances'
978
+ };
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Check if wallet has sufficient tokens for merge using CLOB token IDs
984
+ *
985
+ * @param conditionId - Market condition ID
986
+ * @param tokenIds - Token IDs from CLOB API
987
+ * @param amount - Amount to merge
988
+ */
989
+ async canMergeWithTokenIds(
990
+ conditionId: string,
991
+ tokenIds: TokenIds,
992
+ amount: string
993
+ ): Promise<{ canMerge: boolean; reason?: string }> {
994
+ try {
995
+ const balances = await this.getPositionBalanceByTokenIds(conditionId, tokenIds);
996
+ return this.checkMergeBalance(balances, amount);
997
+ } catch (error) {
998
+ return {
999
+ canMerge: false,
1000
+ reason: error instanceof Error ? error.message : 'Failed to check balances'
1001
+ };
1002
+ }
1003
+ }
1004
+
1005
+ private checkMergeBalance(
1006
+ balances: PositionBalance,
1007
+ amount: string
1008
+ ): { canMerge: boolean; reason?: string } {
1009
+ const amountNum = parseFloat(amount);
1010
+ const yesBalance = parseFloat(balances.yesBalance);
1011
+ const noBalance = parseFloat(balances.noBalance);
1012
+
1013
+ if (yesBalance < amountNum) {
1014
+ return {
1015
+ canMerge: false,
1016
+ reason: `Insufficient YES tokens. Have: ${yesBalance}, Need: ${amountNum}`
1017
+ };
1018
+ }
1019
+ if (noBalance < amountNum) {
1020
+ return {
1021
+ canMerge: false,
1022
+ reason: `Insufficient NO tokens. Have: ${noBalance}, Need: ${amountNum}`
1023
+ };
1024
+ }
1025
+
1026
+ return { canMerge: true };
1027
+ }
1028
+
1029
+ /**
1030
+ * Check if wallet has sufficient USDC for split
1031
+ */
1032
+ async canSplit(amount: string): Promise<{ canSplit: boolean; reason?: string }> {
1033
+ try {
1034
+ const balance = await this.getUsdcBalance();
1035
+ const balanceNum = parseFloat(balance);
1036
+ const amountNum = parseFloat(amount);
1037
+
1038
+ if (balanceNum < amountNum) {
1039
+ return {
1040
+ canSplit: false,
1041
+ reason: `Insufficient USDC. Have: ${balance}, Need: ${amount}`
1042
+ };
1043
+ }
1044
+
1045
+ return { canSplit: true };
1046
+ } catch (error) {
1047
+ return {
1048
+ canSplit: false,
1049
+ reason: error instanceof Error ? error.message : 'Failed to check balance'
1050
+ };
1051
+ }
1052
+ }
1053
+
1054
+ /**
1055
+ * Get total portfolio value across positions
1056
+ */
1057
+ async getPortfolioValue(positions: PositionBalance[], prices: Map<string, { yes: number; no: number }>): Promise<{
1058
+ totalValue: number;
1059
+ breakdown: Array<{
1060
+ conditionId: string;
1061
+ yesValue: number;
1062
+ noValue: number;
1063
+ totalValue: number;
1064
+ }>;
1065
+ }> {
1066
+ let totalValue = 0;
1067
+ const breakdown: Array<{
1068
+ conditionId: string;
1069
+ yesValue: number;
1070
+ noValue: number;
1071
+ totalValue: number;
1072
+ }> = [];
1073
+
1074
+ for (const position of positions) {
1075
+ const price = prices.get(position.conditionId);
1076
+ if (!price) continue;
1077
+
1078
+ const yesValue = parseFloat(position.yesBalance) * price.yes;
1079
+ const noValue = parseFloat(position.noBalance) * price.no;
1080
+ const positionValue = yesValue + noValue;
1081
+
1082
+ totalValue += positionValue;
1083
+ breakdown.push({
1084
+ conditionId: position.conditionId,
1085
+ yesValue,
1086
+ noValue,
1087
+ totalValue: positionValue,
1088
+ });
1089
+ }
1090
+
1091
+ return { totalValue, breakdown };
1092
+ }
1093
+
1094
+ // ===== Private Helpers =====
1095
+
1096
+ /**
1097
+ * Calculate position ID for a given outcome (INTERNAL USE ONLY)
1098
+ *
1099
+ * ⚠️ WARNING: This calculation does NOT produce correct Polymarket token IDs!
1100
+ *
1101
+ * Polymarket uses custom token IDs that differ from standard CTF position ID calculation.
1102
+ * The token IDs from CLOB API (e.g., "104173557214744537570424345347209544585775842950109756851652855913015295701992")
1103
+ * are NOT the same as what this function calculates.
1104
+ *
1105
+ * For Polymarket CLOB markets, ALWAYS:
1106
+ * 1. Get token IDs from CLOB API: https://clob.polymarket.com/markets/{conditionId}
1107
+ * 2. Use getPositionBalanceByTokenIds() instead of getPositionBalance()
1108
+ * 3. Use mergeByTokenIds() instead of merge()
1109
+ * 4. Use redeemByTokenIds() instead of redeem()
1110
+ *
1111
+ * This method is kept for potential non-Polymarket CTF markets only.
1112
+ *
1113
+ * @deprecated Use CLOB API token IDs for Polymarket markets
1114
+ */
1115
+ private calculatePositionId(conditionId: string, indexSet: number): string {
1116
+ // Collection ID - must use solidityPack (abi.encodePacked) to match CTF contract
1117
+ const collectionId = ethers.utils.keccak256(
1118
+ ethers.utils.solidityPack(
1119
+ ['bytes32', 'bytes32', 'uint256'],
1120
+ [ethers.constants.HashZero, conditionId, indexSet]
1121
+ )
1122
+ );
1123
+
1124
+ // Position ID - must use solidityPack (abi.encodePacked) to match CTF contract
1125
+ const positionId = ethers.utils.keccak256(
1126
+ ethers.utils.solidityPack(
1127
+ ['address', 'bytes32'],
1128
+ [USDC_CONTRACT, collectionId]
1129
+ )
1130
+ );
1131
+
1132
+ return positionId;
1133
+ }
1134
+
1135
+ /**
1136
+ * Get gas options for Polygon network using EIP-1559
1137
+ *
1138
+ * Polygon requires higher priority fees than default ethers.js estimates.
1139
+ * Uses minimum 30 gwei priority fee to ensure transactions don't get stuck.
1140
+ */
1141
+ private async getGasOptions(): Promise<{
1142
+ maxPriorityFeePerGas: BigNumber;
1143
+ maxFeePerGas: BigNumber;
1144
+ }> {
1145
+ const feeData = await this.provider.getFeeData();
1146
+ const baseFee = feeData.lastBaseFeePerGas || feeData.gasPrice || ethers.utils.parseUnits('100', 'gwei');
1147
+
1148
+ // Minimum 30 gwei priority fee for Polygon
1149
+ const minPriorityFee = ethers.utils.parseUnits('30', 'gwei');
1150
+ const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas && feeData.maxPriorityFeePerGas.gt(minPriorityFee)
1151
+ ? feeData.maxPriorityFeePerGas
1152
+ : minPriorityFee;
1153
+
1154
+ // Apply multiplier to base fee and add priority fee
1155
+ const adjustedBaseFee = baseFee.mul(Math.floor(this.gasPriceMultiplier * 100)).div(100);
1156
+ const maxFeePerGas = adjustedBaseFee.add(maxPriorityFeePerGas);
1157
+
1158
+ return { maxPriorityFeePerGas, maxFeePerGas };
1159
+ }
1160
+
1161
+ /**
1162
+ * Calculate gas cost from gas units
1163
+ */
1164
+ private async calculateGasCost(gasUnits: string): Promise<GasEstimate> {
1165
+ const gasOptions = await this.getGasOptions();
1166
+ const effectiveGasPrice = gasOptions.maxFeePerGas;
1167
+
1168
+ const gasUnitsNum = BigNumber.from(gasUnits);
1169
+ const costWei = gasUnitsNum.mul(effectiveGasPrice);
1170
+ const costMatic = parseFloat(ethers.utils.formatEther(costWei));
1171
+
1172
+ const maticPrice = await this.getMaticPrice();
1173
+ const costUsdc = costMatic * maticPrice;
1174
+
1175
+ return {
1176
+ gasUnits,
1177
+ gasPriceGwei: ethers.utils.formatUnits(effectiveGasPrice, 'gwei'),
1178
+ costMatic: costMatic.toFixed(6),
1179
+ costUsdc: costUsdc.toFixed(4),
1180
+ maticPrice,
1181
+ };
1182
+ }
1183
+ }
1184
+
1185
+ // ===== Utility Functions =====
1186
+
1187
+ /**
1188
+ * Calculate condition ID from oracle, question ID, and outcome count
1189
+ * This is rarely needed as Polymarket provides conditionId directly
1190
+ */
1191
+ export function calculateConditionId(
1192
+ oracle: string,
1193
+ questionId: string,
1194
+ outcomeSlotCount: number = 2
1195
+ ): string {
1196
+ return ethers.utils.keccak256(
1197
+ ethers.utils.defaultAbiCoder.encode(
1198
+ ['address', 'bytes32', 'uint256'],
1199
+ [oracle, questionId, outcomeSlotCount]
1200
+ )
1201
+ );
1202
+ }
1203
+
1204
+ /**
1205
+ * Parse USDC amount to BigNumber (6 decimals)
1206
+ */
1207
+ export function parseUsdc(amount: string): BigNumber {
1208
+ return ethers.utils.parseUnits(amount, USDC_DECIMALS);
1209
+ }
1210
+
1211
+ /**
1212
+ * Format BigNumber to USDC string (6 decimals)
1213
+ */
1214
+ export function formatUsdc(amount: BigNumber): string {
1215
+ return ethers.utils.formatUnits(amount, USDC_DECIMALS);
1216
+ }