@megatao/sdk 1.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 (228) hide show
  1. package/.env.example +37 -0
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +199 -0
  4. package/bin/alf +4 -0
  5. package/cli/README.md +198 -0
  6. package/cli/TEST_MANUAL.md +577 -0
  7. package/cli/commands/account.ts +545 -0
  8. package/cli/commands/funding.ts +481 -0
  9. package/cli/commands/liquidation.ts +523 -0
  10. package/cli/commands/market.ts +590 -0
  11. package/cli/commands/orders.ts +395 -0
  12. package/cli/commands/position.ts +1085 -0
  13. package/cli/commands/shared/positionUtils.ts +239 -0
  14. package/cli/commands/trading.ts +483 -0
  15. package/cli/commands/utils.ts +281 -0
  16. package/cli/commands/vault.ts +522 -0
  17. package/cli/index.ts +169 -0
  18. package/cli/interactive.ts +530 -0
  19. package/cli/utils/client.ts +457 -0
  20. package/cli/utils/config.ts +226 -0
  21. package/cli/utils/display.ts +258 -0
  22. package/cli/utils/index.ts +10 -0
  23. package/cli/utils/prompts.ts +364 -0
  24. package/config.example.json +23 -0
  25. package/dist/AlphaFuturesClient.d.ts +36 -0
  26. package/dist/AlphaFuturesClient.d.ts.map +1 -0
  27. package/dist/AlphaFuturesClient.js +116 -0
  28. package/dist/AlphaFuturesClient.js.map +1 -0
  29. package/dist/abi/Alpha.json +5987 -0
  30. package/dist/abi/abis.d.ts +319 -0
  31. package/dist/abi/abis.d.ts.map +1 -0
  32. package/dist/abi/abis.js +128 -0
  33. package/dist/abi/abis.js.map +1 -0
  34. package/dist/abi/index.d.ts +11 -0
  35. package/dist/abi/index.d.ts.map +1 -0
  36. package/dist/abi/index.js +15 -0
  37. package/dist/abi/index.js.map +1 -0
  38. package/dist/config/contracts.config.d.ts +70 -0
  39. package/dist/config/contracts.config.d.ts.map +1 -0
  40. package/dist/config/contracts.config.js +137 -0
  41. package/dist/config/contracts.config.js.map +1 -0
  42. package/dist/config/environments/alpha.config.d.ts +17 -0
  43. package/dist/config/environments/alpha.config.d.ts.map +1 -0
  44. package/dist/config/environments/alpha.config.js +140 -0
  45. package/dist/config/environments/alpha.config.js.map +1 -0
  46. package/dist/config/environments/beta.config.d.ts +16 -0
  47. package/dist/config/environments/beta.config.d.ts.map +1 -0
  48. package/dist/config/environments/beta.config.js +131 -0
  49. package/dist/config/environments/beta.config.js.map +1 -0
  50. package/dist/config/environments/dev.config.d.ts +13 -0
  51. package/dist/config/environments/dev.config.d.ts.map +1 -0
  52. package/dist/config/environments/dev.config.js +123 -0
  53. package/dist/config/environments/dev.config.js.map +1 -0
  54. package/dist/config/environments/index.d.ts +48 -0
  55. package/dist/config/environments/index.d.ts.map +1 -0
  56. package/dist/config/environments/index.js +81 -0
  57. package/dist/config/environments/index.js.map +1 -0
  58. package/dist/config/environments/localhost.config.d.ts +16 -0
  59. package/dist/config/environments/localhost.config.d.ts.map +1 -0
  60. package/dist/config/environments/localhost.config.js +152 -0
  61. package/dist/config/environments/localhost.config.js.map +1 -0
  62. package/dist/config/environments/prod.config.d.ts +20 -0
  63. package/dist/config/environments/prod.config.d.ts.map +1 -0
  64. package/dist/config/environments/prod.config.js +143 -0
  65. package/dist/config/environments/prod.config.js.map +1 -0
  66. package/dist/config/index.d.ts +7 -0
  67. package/dist/config/index.d.ts.map +1 -0
  68. package/dist/config/index.js +41 -0
  69. package/dist/config/index.js.map +1 -0
  70. package/dist/constants/assets.d.ts +76 -0
  71. package/dist/constants/assets.d.ts.map +1 -0
  72. package/dist/constants/assets.js +277 -0
  73. package/dist/constants/assets.js.map +1 -0
  74. package/dist/constants/contracts.d.ts +41 -0
  75. package/dist/constants/contracts.d.ts.map +1 -0
  76. package/dist/constants/contracts.js +57 -0
  77. package/dist/constants/contracts.js.map +1 -0
  78. package/dist/constants/index.d.ts +36 -0
  79. package/dist/constants/index.d.ts.map +1 -0
  80. package/dist/constants/index.js +75 -0
  81. package/dist/constants/index.js.map +1 -0
  82. package/dist/constants/networks.d.ts +32 -0
  83. package/dist/constants/networks.d.ts.map +1 -0
  84. package/dist/constants/networks.js +174 -0
  85. package/dist/constants/networks.js.map +1 -0
  86. package/dist/contracts/index.d.ts +5 -0
  87. package/dist/contracts/index.d.ts.map +1 -0
  88. package/dist/contracts/index.js +21 -0
  89. package/dist/contracts/index.js.map +1 -0
  90. package/dist/contracts/viem/AlphaViem.d.ts +518 -0
  91. package/dist/contracts/viem/AlphaViem.d.ts.map +1 -0
  92. package/dist/contracts/viem/AlphaViem.js +1287 -0
  93. package/dist/contracts/viem/AlphaViem.js.map +1 -0
  94. package/dist/contracts/viem/PriceOracleViem.d.ts +71 -0
  95. package/dist/contracts/viem/PriceOracleViem.d.ts.map +1 -0
  96. package/dist/contracts/viem/PriceOracleViem.js +212 -0
  97. package/dist/contracts/viem/PriceOracleViem.js.map +1 -0
  98. package/dist/contracts/viem/index.d.ts +9 -0
  99. package/dist/contracts/viem/index.d.ts.map +1 -0
  100. package/dist/contracts/viem/index.js +17 -0
  101. package/dist/contracts/viem/index.js.map +1 -0
  102. package/dist/errors/index.d.ts +44 -0
  103. package/dist/errors/index.d.ts.map +1 -0
  104. package/dist/errors/index.js +83 -0
  105. package/dist/errors/index.js.map +1 -0
  106. package/dist/index.d.ts +19 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +60 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/types/alpha.d.ts +299 -0
  111. package/dist/types/alpha.d.ts.map +1 -0
  112. package/dist/types/alpha.js +6 -0
  113. package/dist/types/alpha.js.map +1 -0
  114. package/dist/types/client.d.ts +24 -0
  115. package/dist/types/client.d.ts.map +1 -0
  116. package/dist/types/client.js +13 -0
  117. package/dist/types/client.js.map +1 -0
  118. package/dist/types/contracts.d.ts +48 -0
  119. package/dist/types/contracts.d.ts.map +1 -0
  120. package/dist/types/contracts.js +6 -0
  121. package/dist/types/contracts.js.map +1 -0
  122. package/dist/types/funding.d.ts +27 -0
  123. package/dist/types/funding.d.ts.map +1 -0
  124. package/dist/types/funding.js +6 -0
  125. package/dist/types/funding.js.map +1 -0
  126. package/dist/types/index.d.ts +92 -0
  127. package/dist/types/index.d.ts.map +1 -0
  128. package/dist/types/index.js +47 -0
  129. package/dist/types/index.js.map +1 -0
  130. package/dist/types/liquidation.d.ts +20 -0
  131. package/dist/types/liquidation.d.ts.map +1 -0
  132. package/dist/types/liquidation.js +6 -0
  133. package/dist/types/liquidation.js.map +1 -0
  134. package/dist/types/margin.d.ts +29 -0
  135. package/dist/types/margin.d.ts.map +1 -0
  136. package/dist/types/margin.js +6 -0
  137. package/dist/types/margin.js.map +1 -0
  138. package/dist/types/oracle.d.ts +21 -0
  139. package/dist/types/oracle.d.ts.map +1 -0
  140. package/dist/types/oracle.js +6 -0
  141. package/dist/types/oracle.js.map +1 -0
  142. package/dist/types/positions.d.ts +43 -0
  143. package/dist/types/positions.d.ts.map +1 -0
  144. package/dist/types/positions.js +13 -0
  145. package/dist/types/positions.js.map +1 -0
  146. package/dist/utils/calculations.d.ts +84 -0
  147. package/dist/utils/calculations.d.ts.map +1 -0
  148. package/dist/utils/calculations.js +155 -0
  149. package/dist/utils/calculations.js.map +1 -0
  150. package/dist/utils/errors.d.ts +24 -0
  151. package/dist/utils/errors.d.ts.map +1 -0
  152. package/dist/utils/errors.js +129 -0
  153. package/dist/utils/errors.js.map +1 -0
  154. package/dist/utils/events.d.ts +40 -0
  155. package/dist/utils/events.d.ts.map +1 -0
  156. package/dist/utils/events.js +73 -0
  157. package/dist/utils/events.js.map +1 -0
  158. package/dist/utils/format.d.ts +40 -0
  159. package/dist/utils/format.d.ts.map +1 -0
  160. package/dist/utils/format.js +86 -0
  161. package/dist/utils/format.js.map +1 -0
  162. package/dist/utils/index.d.ts +10 -0
  163. package/dist/utils/index.d.ts.map +1 -0
  164. package/dist/utils/index.js +26 -0
  165. package/dist/utils/index.js.map +1 -0
  166. package/dist/utils/network.d.ts +52 -0
  167. package/dist/utils/network.d.ts.map +1 -0
  168. package/dist/utils/network.js +192 -0
  169. package/dist/utils/network.js.map +1 -0
  170. package/dist/utils/positionCalculations.d.ts +145 -0
  171. package/dist/utils/positionCalculations.d.ts.map +1 -0
  172. package/dist/utils/positionCalculations.js +278 -0
  173. package/dist/utils/positionCalculations.js.map +1 -0
  174. package/dist/utils/validation.d.ts +28 -0
  175. package/dist/utils/validation.d.ts.map +1 -0
  176. package/dist/utils/validation.js +68 -0
  177. package/dist/utils/validation.js.map +1 -0
  178. package/docs/README.md +40 -0
  179. package/docs/api/API.md +831 -0
  180. package/docs/guides/GETTING_STARTED.md +316 -0
  181. package/docs/guides/TRADING_GUIDE.md +677 -0
  182. package/docs/integration/INTEGRATION_GUIDE.md +1679 -0
  183. package/docs/integration/VIEM_INTEGRATION.md +294 -0
  184. package/docs/reference/CLI_QUICK_REFERENCE.md +197 -0
  185. package/docs/reference/TROUBLESHOOTING.md +922 -0
  186. package/package.json +113 -0
  187. package/src/AlphaFuturesClient.ts +158 -0
  188. package/src/abi/.gitkeep +1 -0
  189. package/src/abi/Alpha.json +5987 -0
  190. package/src/abi/README.md +99 -0
  191. package/src/abi/abis.ts +131 -0
  192. package/src/abi/index.ts +13 -0
  193. package/src/config/contracts.config.ts +186 -0
  194. package/src/config/environments/alpha.config.ts +139 -0
  195. package/src/config/environments/beta.config.ts +130 -0
  196. package/src/config/environments/dev.config.ts +122 -0
  197. package/src/config/environments/index.ts +87 -0
  198. package/src/config/environments/localhost.config.ts +153 -0
  199. package/src/config/environments/prod.config.ts +142 -0
  200. package/src/config/index.ts +29 -0
  201. package/src/constants/assets.ts +299 -0
  202. package/src/constants/contracts.ts +64 -0
  203. package/src/constants/index.ts +69 -0
  204. package/src/constants/networks.ts +182 -0
  205. package/src/contracts/index.ts +5 -0
  206. package/src/contracts/viem/AlphaViem.ts +1615 -0
  207. package/src/contracts/viem/PriceOracleViem.ts +272 -0
  208. package/src/contracts/viem/index.ts +11 -0
  209. package/src/errors/index.ts +87 -0
  210. package/src/index.ts +59 -0
  211. package/src/types/VIEM_TYPES_README.md +70 -0
  212. package/src/types/alpha.ts +358 -0
  213. package/src/types/client.ts +27 -0
  214. package/src/types/contracts.ts +74 -0
  215. package/src/types/funding.ts +31 -0
  216. package/src/types/index.ts +108 -0
  217. package/src/types/liquidation.ts +23 -0
  218. package/src/types/margin.ts +34 -0
  219. package/src/types/oracle.ts +24 -0
  220. package/src/types/positions.ts +48 -0
  221. package/src/utils/calculations.ts +175 -0
  222. package/src/utils/errors.ts +147 -0
  223. package/src/utils/events.ts +98 -0
  224. package/src/utils/format.ts +84 -0
  225. package/src/utils/index.ts +10 -0
  226. package/src/utils/network.ts +212 -0
  227. package/src/utils/positionCalculations.ts +317 -0
  228. package/src/utils/validation.ts +76 -0
@@ -0,0 +1,1085 @@
1
+ /**
2
+ * Position Commands - Manage trading positions
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { parseEther } from 'viem';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import Table from 'cli-table3';
10
+ import { AlphaFuturesClient } from '../../src';
11
+ import { formatUSD, formatTAO, formatPercentage } from '../../src/utils';
12
+ import { calculateMaintenanceMargin } from '../../src/utils/calculations';
13
+ import { getClient, handleError, validateMarketAddresses, getMarketAddress } from '../utils/client';
14
+ import type { Address } from 'viem';
15
+ import { confirmAction, selectFromList, validateDirection } from '../utils/prompts';
16
+ import { displayPositionDetails, calculatePnL } from '../utils/display';
17
+ import { detectParadigm, openPositionFromCollateral } from './shared/positionUtils';
18
+
19
+ /**
20
+ * Validates position ID format and provides helpful error message
21
+ * @param positionId Position ID to validate
22
+ * @throws Error with helpful message if format is incorrect
23
+ */
24
+ function validatePositionIdFormat(positionId: string): void {
25
+ if (!positionId) {
26
+ throw new Error(`Position ID cannot be empty.
27
+
28
+ Expected format: 32 hex characters after 0x (bytes16)
29
+ Example: 0x00000000000000000000000000000001`);
30
+ }
31
+
32
+ const hex = positionId.startsWith('0x') ? positionId.slice(2) : positionId;
33
+
34
+ // Check if contains only hex characters
35
+ if (!/^[0-9a-fA-F]*$/.test(hex)) {
36
+ throw new Error(`Invalid position ID format: "${positionId}"
37
+
38
+ Position ID must contain only hexadecimal characters (0-9, a-f, A-F).
39
+ Expected format: 32 hex characters after 0x (bytes16)
40
+ Example: 0x00000000000000000000000000000001`);
41
+ }
42
+
43
+ // Warn if too long (would be truncated)
44
+ if (hex.length > 32) {
45
+ console.warn(
46
+ chalk.yellow(`āš ļø Position ID is longer than 32 hex characters and will be truncated.
47
+ Given: ${positionId}
48
+ Will use: 0x${hex.slice(0, 32)}`),
49
+ );
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Convert position ID to proper bytes16 format (32 hex characters)
55
+ * @param positionId Raw position ID (could be short like 0x123 or full bytes16)
56
+ * @returns Properly formatted bytes16 position ID
57
+ */
58
+ function normalizePositionId(positionId: string): `0x${string}` {
59
+ // Validate format first
60
+ validatePositionIdFormat(positionId);
61
+
62
+ // Remove 0x prefix if present
63
+ const hex = positionId.startsWith('0x') ? positionId.slice(2) : positionId;
64
+
65
+ // Pad to 32 characters (16 bytes) or truncate if too long
66
+ const normalizedHex = hex.length > 32 ? hex.slice(0, 32) : hex.padEnd(32, '0');
67
+
68
+ return `0x${normalizedHex}` as `0x${string}`;
69
+ }
70
+
71
+ export function positionCommands(program: Command) {
72
+ const position = program.command('position').alias('pos').description('Manage trading positions');
73
+
74
+ // Open position command
75
+ position
76
+ .command('open')
77
+ .description(
78
+ `Open a new position
79
+
80
+ Two modes available:
81
+
82
+ 1. Collateral-First (NEW - Recommended):
83
+ $ alpha-futures position open --asset BITMIND --direction long --collateral 100 --leverage 3.5
84
+
85
+ 2. Size-First (Legacy - Backward Compatible):
86
+ $ alpha-futures position open --asset BITMIND --direction long --size 30 --margin 10
87
+ `,
88
+ )
89
+ .requiredOption('-a, --asset <asset>', 'Asset to trade (e.g., BITMIND)')
90
+ .requiredOption('-d, --direction <direction>', 'Direction: long or short')
91
+ .option('-c, --collateral <amount>', 'Collateral amount in USD (e.g., 100 for $100) [NEW]')
92
+ .option('-s, --size <size>', 'Position size in USD [LEGACY]')
93
+ .option('-m, --margin <margin>', 'Margin amount in TAO [LEGACY]')
94
+ .option(
95
+ '-l, --leverage <leverage>',
96
+ 'Leverage multiplier (supports decimals, e.g., 3.5)',
97
+ parseFloat,
98
+ )
99
+ .option('-y, --yes', 'Skip confirmation')
100
+ .action(async (options) => {
101
+ const spinner = ora('Preparing position...').start();
102
+
103
+ try {
104
+ const client = await getClient(program.opts());
105
+ const userAddress = await client.getSignerAddress();
106
+ const alpha = client.getAlpha();
107
+
108
+ // Detect paradigm
109
+ const paradigm = detectParadigm({
110
+ size: options.size,
111
+ margin: options.margin,
112
+ collateral: options.collateral,
113
+ leverage: options.leverage,
114
+ });
115
+
116
+ if (paradigm === 'invalid') {
117
+ spinner.stop();
118
+ if (options.collateral && (options.size || options.margin)) {
119
+ console.error(chalk.red('āŒ Cannot mix paradigms! Use either:'));
120
+ console.error(chalk.cyan(' • Collateral-First: --collateral and --leverage'));
121
+ console.error(chalk.cyan(' • Size-First: --size and --margin'));
122
+ process.exit(1);
123
+ } else {
124
+ console.error(chalk.red('āŒ Missing required options! Use either:'));
125
+ console.error(chalk.cyan(' • Collateral-First: --collateral and --leverage'));
126
+ console.error(chalk.cyan(' • Size-First: --size and --margin (or --leverage)'));
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ // Parse common inputs
132
+ const asset = options.asset.toUpperCase();
133
+
134
+ // Validate direction with strict checking
135
+ let direction: 'long' | 'short';
136
+ try {
137
+ direction = validateDirection(options.direction);
138
+ } catch (error: any) {
139
+ spinner.fail(error.message);
140
+ return;
141
+ }
142
+
143
+ const isLong = direction === 'long';
144
+
145
+ // Get market data
146
+ const marketAddress = getMarketAddress(asset);
147
+ const currentPrice = await client.oracle.getPrice(marketAddress);
148
+
149
+ spinner.stop();
150
+
151
+ // Execute based on paradigm
152
+ if (paradigm === 'collateral-first') {
153
+ // NEW PARADIGM
154
+ console.log(chalk.bold('šŸ“Š Using Collateral-First mode'));
155
+
156
+ const collateral = parseFloat(options.collateral);
157
+ const leverage = options.leverage || 3; // Default 3x
158
+
159
+ // Validate leverage precision (0.1x increments)
160
+ const leverageDecimal = leverage % 0.1;
161
+ if (leverageDecimal > 0.001 && leverageDecimal < 0.099) {
162
+ // Allow some floating point error
163
+ console.error(
164
+ chalk.red('āŒ Leverage must be in 0.1x increments (e.g., 1.5, 2.3, 10.0)'),
165
+ );
166
+ process.exit(1);
167
+ }
168
+
169
+ // Validate leverage range
170
+ if (leverage < 1 || leverage > 30) {
171
+ console.error(chalk.red('āŒ Leverage must be between 1x and 30x'));
172
+ process.exit(1);
173
+ }
174
+
175
+ // Check available balance
176
+ const collateralWei = parseEther(collateral.toString());
177
+ const available = await alpha.getAvailableMargin(userAddress);
178
+ if (collateralWei > available) {
179
+ console.error(chalk.red('āŒ Insufficient available balance'));
180
+ console.log(` Required: ${formatTAO(collateralWei)} TAO`);
181
+ console.log(` Available: ${formatTAO(available)} TAO`);
182
+ process.exit(1);
183
+ }
184
+
185
+ // Display position preview
186
+ console.log(chalk.bold('\nšŸ“Š Position Preview:'));
187
+ console.log(` Asset: ${chalk.cyan(asset)}`);
188
+ console.log(` Direction: ${isLong ? chalk.green('LONG ↗') : chalk.red('SHORT ā†˜')}`);
189
+ console.log(` Current Price: ${formatUSD(currentPrice)}`);
190
+ console.log(` Collateral: $${collateral}`);
191
+ console.log(` Leverage: ${chalk.yellow(leverage.toFixed(1) + 'x')}`);
192
+ console.log(` Position Value: $${(collateral * leverage).toFixed(2)}`);
193
+
194
+ // Calculate and display fees
195
+ const positionValueUSD = parseEther((collateral * leverage).toString());
196
+ const tradingFee = (positionValueUSD * 10n) / 10000n; // 0.1%
197
+ console.log(` Trading Fee: ${formatUSD(tradingFee)} (0.1%)`);
198
+
199
+ // Show liquidation price estimate
200
+ const liquidationPrice = isLong
201
+ ? (currentPrice * (100n - 2000n)) / 100n // 20% below for longs
202
+ : (currentPrice * (100n + 2000n)) / 100n; // 20% above for shorts
203
+ console.log(` Est. Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
204
+
205
+ // Confirm action
206
+ if (!options.yes) {
207
+ const confirmed = await confirmAction('Open this position?');
208
+ if (!confirmed) {
209
+ console.log(chalk.yellow('Position opening cancelled'));
210
+ return;
211
+ }
212
+ }
213
+
214
+ spinner.start('Opening position...');
215
+
216
+ const hash = await openPositionFromCollateral(
217
+ alpha,
218
+ marketAddress,
219
+ isLong,
220
+ collateral,
221
+ leverage,
222
+ currentPrice,
223
+ );
224
+
225
+ spinner.text = 'Waiting for confirmation...';
226
+ const publicClient = client.getPublicClient();
227
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
228
+
229
+ spinner.succeed('Position opened successfully!');
230
+ console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
231
+ console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
232
+
233
+ // Parse position opened event
234
+ const positionEvent = alpha.parsePositionOpenedEvent(receipt);
235
+ if (positionEvent) {
236
+ console.log(chalk.green(`\nāœ“ Position ID: ${positionEvent.positionId}`));
237
+ }
238
+ } else {
239
+ // LEGACY PARADIGM
240
+ console.log(chalk.bold('šŸ“Š Using Size-First mode (legacy)'));
241
+
242
+ const positionSizeUSD = parseEther(options.size); // USD amount
243
+ const marginAmount = parseEther(options.margin);
244
+
245
+ // Calculate leverage
246
+ const leverage = options.leverage
247
+ ? parseFloat(options.leverage)
248
+ : Number(positionSizeUSD) / Number(marginAmount);
249
+
250
+ // Validate leverage
251
+ if (leverage > 30 || leverage < 1) {
252
+ console.error(chalk.red('āŒ Invalid leverage: must be between 1x and 30x'));
253
+ process.exit(1);
254
+ }
255
+
256
+ // Check available balance
257
+ const available = await alpha.getAvailableMargin(userAddress);
258
+ if (marginAmount > available) {
259
+ console.error(chalk.red('āŒ Insufficient available balance'));
260
+ console.log(` Required: ${formatTAO(marginAmount)} TAO`);
261
+ console.log(` Available: ${formatTAO(available)} TAO`);
262
+ process.exit(1);
263
+ }
264
+
265
+ // Convert USD position size to token amount
266
+ const positionSize = (positionSizeUSD * parseEther('1')) / currentPrice;
267
+
268
+ // Display position preview
269
+ console.log(chalk.bold('\nšŸ“Š Position Preview:'));
270
+ console.log(` Asset: ${chalk.cyan(asset)}`);
271
+ console.log(` Direction: ${isLong ? chalk.green('LONG ↗') : chalk.red('SHORT ā†˜')}`);
272
+ console.log(` Current Price: ${formatUSD(currentPrice)}`);
273
+ console.log(
274
+ ` Position Size: ${formatUSD(positionSizeUSD)} (${formatUSD(positionSize)} tokens)`,
275
+ );
276
+ console.log(` Margin: ${formatTAO(marginAmount)} TAO`);
277
+ console.log(` Leverage: ${chalk.yellow(leverage.toFixed(2) + 'x')}`);
278
+
279
+ // Calculate and display fees
280
+ const tradingFee = (positionSizeUSD * 10n) / 10000n; // 0.1% of USD notional
281
+ console.log(` Trading Fee: ${formatUSD(tradingFee)} (0.1%)`);
282
+
283
+ // Show liquidation price
284
+ const liquidationPrice = isLong
285
+ ? (currentPrice * (100n - 2000n)) / 100n // 20% below for longs
286
+ : (currentPrice * (100n + 2000n)) / 100n; // 20% above for shorts
287
+ console.log(` Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
288
+
289
+ // Confirm action
290
+ if (!options.yes) {
291
+ const confirmed = await confirmAction('Open this position?');
292
+ if (!confirmed) {
293
+ console.log(chalk.yellow('Position opening cancelled'));
294
+ return;
295
+ }
296
+ }
297
+
298
+ spinner.start('Opening position...');
299
+ const leverageBigInt = parseEther(leverage.toString()); // Convert to PRECISION (1e18)
300
+ const maxSlippage = 100n; // 1% max slippage
301
+
302
+ const hash = await alpha.openMarketPosition(
303
+ marketAddress,
304
+ isLong,
305
+ marginAmount,
306
+ leverageBigInt,
307
+ maxSlippage,
308
+ );
309
+
310
+ spinner.text = 'Waiting for confirmation...';
311
+ const publicClient = client.getPublicClient();
312
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
313
+
314
+ spinner.succeed('Position opened successfully!');
315
+ console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
316
+ console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
317
+
318
+ // Parse position opened event
319
+ const positionEvent = alpha.parsePositionOpenedEvent(receipt);
320
+ if (positionEvent) {
321
+ console.log(chalk.green(`\nāœ“ Position ID: ${positionEvent.positionId}`));
322
+ }
323
+ }
324
+ } catch (error) {
325
+ spinner.fail('Failed to open position');
326
+ handleError(error, program.opts());
327
+ }
328
+ });
329
+
330
+ // Close position command
331
+ position
332
+ .command('close <positionId>')
333
+ .description('Close an existing position')
334
+ .option('-s, --size <size>', 'Size to close (partial close)')
335
+ .option('-p, --percent <percent>', 'Percentage to close (e.g., 50 for 50%)')
336
+ .option('-y, --yes', 'Skip confirmation')
337
+ .addHelpText(
338
+ 'after',
339
+ `
340
+ Examples:
341
+ $ alpha-futures position close 0x00000000000000000000000000000001
342
+ $ alpha-futures position close 0x12345000000000000000000000000000 --percent 50
343
+ $ alpha-futures position close 0xabcd1234000000000000000000000000 --size 500 --yes
344
+
345
+ Note: Position ID must be in bytes16 format (32 hex characters after 0x).
346
+ Use "alpha-futures position find" to get your position IDs.`,
347
+ )
348
+ .action(async (positionId, options) => {
349
+ const spinner = ora('Loading position...').start();
350
+
351
+ try {
352
+ const client = await getClient(program.opts());
353
+ const alpha = client.getAlpha();
354
+
355
+ // Get position details - position ID needs to be converted to bytes16 format
356
+ const normalizedPositionId = normalizePositionId(positionId);
357
+ const position = await alpha.getPosition(normalizedPositionId);
358
+
359
+ // Note: Monolith positions don't have isActive field, we check size > 0
360
+ if (position.notionalValue === 0n) {
361
+ spinner.fail('Position is not active');
362
+ return;
363
+ }
364
+
365
+ // Calculate close size
366
+ let closeSize: bigint;
367
+ if (options.percent) {
368
+ const percent = BigInt(Math.floor(parseFloat(options.percent) * 100));
369
+ closeSize = (position.notionalValue * percent) / 10000n;
370
+ } else if (options.size) {
371
+ closeSize = parseEther(options.size);
372
+ } else {
373
+ // Close entire position - pass 0 to smart contract for full close
374
+ closeSize = 0n;
375
+ }
376
+
377
+ // Validate close size (only if not closing full position)
378
+ if (closeSize > 0n && closeSize > position.notionalValue) {
379
+ spinner.fail('Close size exceeds position size');
380
+ return;
381
+ }
382
+
383
+ spinner.stop();
384
+
385
+ // Get current price - derive market address from position or use environment lookup
386
+ // For now, using BITMIND as default since most tests use BITMIND
387
+ const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
388
+ const currentPrice = await client.oracle.getPrice(marketAddress);
389
+ // Use position.notionalValue for P&L calculation when closing full position
390
+ const sizeForPnl = closeSize === 0n ? position.notionalValue : closeSize;
391
+ const pnl = calculatePnL(position, currentPrice, sizeForPnl);
392
+
393
+ // Display close preview
394
+ console.log(chalk.bold('\nšŸ”’ Position Close Preview:'));
395
+ displayPositionDetails(position, currentPrice);
396
+
397
+ console.log(chalk.bold('\nClose Details:'));
398
+ const displaySize = closeSize === 0n ? position.notionalValue : closeSize;
399
+ const closePercent =
400
+ position.notionalValue > 0n
401
+ ? ((Number(displaySize) * 100) / Number(position.notionalValue)).toFixed(1)
402
+ : '100.0';
403
+ console.log(` Size to Close: ${formatUSD(displaySize)} (${closePercent}%)`);
404
+ console.log(
405
+ ` P&L: ${pnl >= 0 ? chalk.green('+' + formatUSD(pnl)) : chalk.red(formatUSD(pnl))}`,
406
+ );
407
+
408
+ // Confirm action
409
+ if (!options.yes) {
410
+ const confirmed = await confirmAction('Close this position?');
411
+ if (!confirmed) {
412
+ console.log(chalk.yellow('Position close cancelled'));
413
+ return;
414
+ }
415
+ }
416
+
417
+ spinner.start('Closing position...');
418
+ const hash = await alpha.closePosition(normalizedPositionId, closeSize);
419
+
420
+ spinner.text = 'Waiting for confirmation...';
421
+ const publicClient = client.getPublicClient();
422
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
423
+
424
+ spinner.succeed('Position closed successfully!');
425
+ console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
426
+ console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
427
+
428
+ // Parse position closed event
429
+ const positionEvent = alpha.parsePositionClosedEvent(receipt);
430
+ if (positionEvent) {
431
+ const realizedPnL = positionEvent.pnl;
432
+ console.log(
433
+ chalk.bold(
434
+ `\nšŸ’° Realized P&L: ${realizedPnL >= 0 ? chalk.green('+' + formatTAO(realizedPnL) + ' TAO') : chalk.red(formatTAO(realizedPnL) + ' TAO')}`,
435
+ ),
436
+ );
437
+ console.log(
438
+ chalk.cyan(`šŸ’³ P&L ${realizedPnL >= 0 ? 'credited to' : 'debited from'} TAO wallet`),
439
+ );
440
+
441
+ // Show updated TAO wallet balance
442
+ try {
443
+ const userAddress = await client.getSignerAddress();
444
+ const publicClient = client.getPublicClient();
445
+ const taoTokenAddress = client.getCollateralTokenAddress();
446
+ if (taoTokenAddress) {
447
+ const updatedTaoBalance = (await publicClient.readContract({
448
+ address: taoTokenAddress,
449
+ abi: [
450
+ {
451
+ name: 'balanceOf',
452
+ type: 'function',
453
+ stateMutability: 'view',
454
+ inputs: [{ name: 'account', type: 'address' }],
455
+ outputs: [{ name: '', type: 'uint256' }],
456
+ },
457
+ ],
458
+ functionName: 'balanceOf',
459
+ args: [userAddress],
460
+ })) as bigint;
461
+ console.log(
462
+ chalk.gray(`šŸ’³ Updated TAO wallet balance: ${formatTAO(updatedTaoBalance)} TAO`),
463
+ );
464
+ }
465
+ } catch (error) {
466
+ // Ignore error fetching updated balance
467
+ }
468
+ }
469
+ } catch (error) {
470
+ spinner.fail('Failed to close position');
471
+ handleError(error, program.opts());
472
+ }
473
+ });
474
+
475
+ // List positions command
476
+ position
477
+ .command('list [address]')
478
+ .alias('ls')
479
+ .description('List all positions')
480
+ .option('-a, --active', 'Show only active positions')
481
+ .option('-c, --closed', 'Show only closed positions')
482
+ .option('-s, --sort <field>', 'Sort by: size, pnl, margin, age', 'size')
483
+ .action(async (address, options) => {
484
+ const spinner = ora('Loading positions...').start();
485
+
486
+ try {
487
+ const client = await getClient(program.opts());
488
+ const alpha = client.getAlpha();
489
+ const userAddress = address || (await client.getSignerAddress());
490
+
491
+ if (!userAddress) {
492
+ spinner.fail('No address provided');
493
+ return;
494
+ }
495
+
496
+ // Get all position IDs
497
+ const positionIds = await alpha.getUserPositions(userAddress);
498
+
499
+ if (positionIds.length === 0) {
500
+ spinner.stop();
501
+ console.log(chalk.yellow('\nNo positions found'));
502
+ return;
503
+ }
504
+
505
+ // Fetch position details
506
+ const positions = await Promise.all(positionIds.map((id) => alpha.getPosition(id)));
507
+
508
+ // Filter positions - alpha positions use notionalValue > 0 to indicate active
509
+ let filteredPositions = positions;
510
+ if (options.active) {
511
+ filteredPositions = positions.filter((p) => p.notionalValue > 0n);
512
+ } else if (options.closed) {
513
+ filteredPositions = positions.filter((p) => p.notionalValue === 0n);
514
+ }
515
+
516
+ spinner.stop();
517
+
518
+ if (filteredPositions.length === 0) {
519
+ console.log(chalk.yellow('\nNo matching positions found'));
520
+ return;
521
+ }
522
+
523
+ // Get current prices for P&L calculation
524
+ // Use BITMIND as default market for testing
525
+ const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
526
+ const currentPrice = await client.oracle.getPrice(marketAddress);
527
+ const priceMap = new Map<string, bigint>();
528
+ priceMap.set('ALPHA', currentPrice);
529
+
530
+ // Create positions table
531
+ console.log(chalk.bold(`\nšŸ“Š Positions for ${chalk.cyan(userAddress)}:`));
532
+
533
+ const table = new Table({
534
+ head: ['ID', 'Asset', 'Direction', 'Size', 'Margin', 'Entry', 'Current', 'P&L', 'Status'],
535
+ colWidths: [6, 8, 10, 15, 15, 12, 12, 15, 8],
536
+ });
537
+
538
+ // Sort positions
539
+ const sortedPositions = [...filteredPositions].sort((a, b) => {
540
+ switch (options.sort) {
541
+ case 'size':
542
+ return Number(b.notionalValue - a.notionalValue);
543
+ case 'margin':
544
+ return Number(b.margin - a.margin);
545
+ case 'age':
546
+ return Number(b.lastUpdated - a.lastUpdated);
547
+ case 'pnl':
548
+ if (a.notionalValue === 0n || b.notionalValue === 0n) return 0;
549
+ const pnlA = calculatePnL(a, currentPrice, a.notionalValue);
550
+ const pnlB = calculatePnL(b, currentPrice, b.notionalValue);
551
+ return Number(pnlB - pnlA);
552
+ default:
553
+ return 0;
554
+ }
555
+ });
556
+
557
+ let totalPnL = 0n;
558
+ let totalMargin = 0n;
559
+
560
+ for (const pos of sortedPositions) {
561
+ const isActive = pos.notionalValue > 0n;
562
+ const pnl = isActive ? calculatePnL(pos, currentPrice, pos.notionalValue) : 0n;
563
+
564
+ if (isActive) {
565
+ totalPnL += pnl;
566
+ totalMargin += pos.margin;
567
+ }
568
+
569
+ const directionIcon = pos.isLong ? chalk.green('LONG ↗') : chalk.red('SHORT ā†˜');
570
+ const pnlDisplay = !isActive
571
+ ? '-'
572
+ : pnl >= 0
573
+ ? chalk.green('+' + formatUSD(pnl))
574
+ : chalk.red(formatUSD(pnl));
575
+ const statusDisplay = isActive ? chalk.green('Active') : chalk.gray('Closed');
576
+
577
+ // Calculate USD value of position: notionalValue represents the position's notional value
578
+ const positionValueUSD = pos.notionalValue;
579
+
580
+ table.push([
581
+ positionIds.indexOf(positionIds.find((id) => id === positionIds[0]) || positionIds[0]) +
582
+ 1, // Simple index for now
583
+ 'ALPHA', // Placeholder asset
584
+ directionIcon,
585
+ formatUSD(positionValueUSD),
586
+ formatTAO(pos.margin),
587
+ formatUSD(pos.entryPrice),
588
+ isActive ? formatUSD(currentPrice) : '-',
589
+ pnlDisplay,
590
+ statusDisplay,
591
+ ]);
592
+ }
593
+
594
+ console.log(table.toString());
595
+
596
+ // Show summary for active positions
597
+ const activePositions = filteredPositions.filter((p) => p.notionalValue > 0n);
598
+ if (activePositions.length > 0) {
599
+ console.log(chalk.bold('\nSummary:'));
600
+ console.log(` Active Positions: ${activePositions.length}`);
601
+ console.log(` Total Margin: ${formatTAO(totalMargin)} TAO`);
602
+ console.log(
603
+ ` Total P&L: ${totalPnL >= 0 ? chalk.green('+' + formatUSD(totalPnL)) : chalk.red(formatUSD(totalPnL))}`,
604
+ );
605
+ }
606
+ } catch (error) {
607
+ spinner.fail('Failed to load positions');
608
+ handleError(error, program.opts());
609
+ }
610
+ });
611
+
612
+ // Position info command
613
+ position
614
+ .command('info <positionId>')
615
+ .description('Get detailed information about a position')
616
+ .option('-w, --watch', 'Watch position in real-time')
617
+ .addHelpText(
618
+ 'after',
619
+ `
620
+ Examples:
621
+ $ alpha-futures position info 0x00000000000000000000000000000001
622
+ $ alpha-futures position info 0x12345000000000000000000000000000 --watch
623
+
624
+ Note: Position ID must be in bytes16 format (32 hex characters after 0x).
625
+ Use "alpha-futures position find" to get your position IDs.`,
626
+ )
627
+ .action(async (positionId, options) => {
628
+ const spinner = ora('Loading position...').start();
629
+
630
+ try {
631
+ const client = await getClient(program.opts());
632
+ const alpha = client.getAlpha();
633
+
634
+ const displayInfo = async () => {
635
+ const normalizedPositionId = normalizePositionId(positionId);
636
+ const position = await alpha.getPosition(normalizedPositionId);
637
+ // Use BITMIND as default market for testing
638
+ const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
639
+ const currentPrice = await client.oracle.getPrice(marketAddress);
640
+
641
+ if (!options.watch) spinner.stop();
642
+
643
+ console.clear();
644
+ console.log(chalk.bold(`\nšŸ“Š Position #${positionId} Details:`));
645
+
646
+ displayPositionDetails(position, currentPrice);
647
+
648
+ if (position.notionalValue > 0n) {
649
+ // Calculate additional metrics
650
+ const pnl = calculatePnL(position, currentPrice, position.notionalValue);
651
+ const pnlPercent = (Number(pnl) * 100) / Number(position.margin);
652
+ const marginRatio = await alpha.getMarginRatio(normalizedPositionId);
653
+
654
+ console.log(chalk.bold('\nšŸ’° Financial Metrics:'));
655
+ console.log(
656
+ ` Unrealized P&L: ${pnl >= 0 ? chalk.green('+' + formatUSD(pnl)) : chalk.red(formatUSD(pnl))}`,
657
+ );
658
+ console.log(
659
+ ` P&L %: ${pnlPercent >= 0 ? chalk.green('+' + pnlPercent.toFixed(2) + '%') : chalk.red(pnlPercent.toFixed(2) + '%')}`,
660
+ );
661
+ console.log(` Margin Ratio: ${formatPercentage(marginRatio)}`);
662
+
663
+ // Use liquidation price from position structure
664
+ const liquidationPrice = position.liquidationPrice;
665
+
666
+ const distanceToLiquidation = position.isLong
667
+ ? ((Number(currentPrice) - Number(liquidationPrice)) / Number(currentPrice)) * 100
668
+ : ((Number(liquidationPrice) - Number(currentPrice)) / Number(currentPrice)) * 100;
669
+
670
+ console.log(` Liquidation Price: ${chalk.red(formatUSD(liquidationPrice))}`);
671
+ console.log(` Distance to Liquidation: ${distanceToLiquidation.toFixed(2)}%`);
672
+
673
+ // Show funding rate impact - use BITMIND market address
674
+ const fundingRate = await alpha.getFundingRate(marketAddress);
675
+ const fundingDirection = position.isLong
676
+ ? fundingRate > 0n
677
+ ? 'Pay'
678
+ : 'Receive'
679
+ : fundingRate > 0n
680
+ ? 'Receive'
681
+ : 'Pay';
682
+
683
+ console.log(chalk.bold('\nšŸ“ˆ Funding Rate:'));
684
+ console.log(` Current Rate: ${formatPercentage(fundingRate)} per 8h`);
685
+ console.log(` Direction: ${fundingDirection}`);
686
+ }
687
+
688
+ if (program.opts().json) {
689
+ console.log(
690
+ '\n' +
691
+ JSON.stringify(
692
+ {
693
+ position: {
694
+ size: position.notionalValue.toString(),
695
+ margin: position.margin.toString(),
696
+ entryPrice: position.entryPrice.toString(),
697
+ isLong: position.isLong,
698
+ lastUpdated: position.lastUpdated.toString(),
699
+ fundingIndex: position.fundingIndex.toString(),
700
+ leverage: position.leverage.toString(),
701
+ liquidationPrice: position.liquidationPrice.toString(),
702
+ },
703
+ currentPrice: currentPrice.toString(),
704
+ },
705
+ null,
706
+ 2,
707
+ ),
708
+ );
709
+ }
710
+ };
711
+
712
+ await displayInfo();
713
+
714
+ if (options.watch) {
715
+ // Update every 5 seconds
716
+ setInterval(displayInfo, 5000);
717
+
718
+ // Handle exit
719
+ process.on('SIGINT', () => {
720
+ console.log(chalk.yellow('\n\nStopped watching'));
721
+ process.exit(0);
722
+ });
723
+ }
724
+ } catch (error) {
725
+ spinner.fail('Failed to load position');
726
+ handleError(error, program.opts());
727
+ }
728
+ });
729
+
730
+ // Position ID lookup command
731
+ position
732
+ .command('id <trader> [asset]')
733
+ .description('Get position ID for a trader and asset')
734
+ .action(async (trader, asset) => {
735
+ const spinner = ora('Looking up position ID...').start();
736
+
737
+ try {
738
+ const client = await getClient(program.opts());
739
+ const alpha = client.getAlpha();
740
+
741
+ const targetAsset = asset || 'BITMIND';
742
+ const marketAddress = getMarketAddress(targetAsset);
743
+
744
+ // Check if position exists
745
+ const hasPosition = await alpha.hasPosition(trader as Address, marketAddress);
746
+
747
+ if (!hasPosition) {
748
+ spinner.stop();
749
+ console.log(chalk.yellow(`\nNo active position found`));
750
+ console.log(` Trader: ${chalk.cyan(trader)}`);
751
+ console.log(` Asset: ${chalk.cyan(targetAsset.toUpperCase())}`);
752
+ return;
753
+ }
754
+
755
+ // Get position ID
756
+ const positionId = await alpha.getPositionId(trader as Address, marketAddress);
757
+
758
+ spinner.stop();
759
+
760
+ console.log(chalk.bold('\nšŸ” Position ID Lookup:'));
761
+ console.log(` Trader: ${chalk.cyan(trader)}`);
762
+ console.log(` Asset: ${chalk.cyan(targetAsset.toUpperCase())}`);
763
+ console.log(` Market: ${chalk.gray(marketAddress)}`);
764
+ console.log(` Position ID: ${chalk.green(positionId)}`);
765
+
766
+ // Get basic position info
767
+ try {
768
+ const position = await alpha.getPosition(positionId);
769
+ console.log(chalk.bold('\nšŸ“Š Quick Info:'));
770
+ console.log(` Size: ${formatUSD(position.notionalValue)}`);
771
+ console.log(
772
+ ` Direction: ${position.isLong ? chalk.green('LONG ↗') : chalk.red('SHORT ā†˜')}`,
773
+ );
774
+ console.log(` Margin: ${formatTAO(position.margin)} TAO`);
775
+ console.log(` Entry Price: ${formatUSD(position.entryPrice)}`);
776
+
777
+ console.log(
778
+ chalk.gray(
779
+ '\nšŸ’” Use "alpha-futures position info ' + positionId + '" for detailed information',
780
+ ),
781
+ );
782
+ } catch (error) {
783
+ console.log(chalk.yellow('\nāš ļø Could not fetch position details'));
784
+ }
785
+ } catch (error) {
786
+ spinner.fail('Failed to lookup position ID');
787
+ handleError(error, program.opts());
788
+ }
789
+ });
790
+
791
+ // Find positions command
792
+ position
793
+ .command('find [trader]')
794
+ .description('Find all position IDs for a trader')
795
+ .option('-a, --asset <asset>', 'Filter by specific asset')
796
+ .action(async (trader, options) => {
797
+ const spinner = ora('Finding positions...').start();
798
+
799
+ try {
800
+ const client = await getClient(program.opts());
801
+ const alpha = client.getAlpha();
802
+ const walletClient = client.getWalletClient();
803
+
804
+ // Use provided trader or wallet address
805
+ const userAddress = trader || walletClient?.account?.address;
806
+
807
+ if (!userAddress) {
808
+ spinner.fail('No trader address provided and no wallet client available');
809
+ return;
810
+ }
811
+
812
+ if (options.asset) {
813
+ // Find position for specific asset
814
+ const marketAddress = getMarketAddress(options.asset);
815
+
816
+ const hasPosition = await alpha.hasPosition(userAddress as Address, marketAddress);
817
+
818
+ if (hasPosition) {
819
+ const positionId = await alpha.getPositionId(userAddress as Address, marketAddress);
820
+ const position = await alpha.getPosition(positionId);
821
+
822
+ spinner.stop();
823
+
824
+ console.log(
825
+ chalk.bold(`\nšŸŽÆ Position Found: ${chalk.cyan(options.asset.toUpperCase())}`),
826
+ );
827
+ console.log(` Trader: ${chalk.cyan(userAddress)}`);
828
+ console.log(` Position ID: ${chalk.green(positionId)}`);
829
+ console.log(` Size: ${formatUSD(position.notionalValue)}`);
830
+ console.log(
831
+ ` Direction: ${position.isLong ? chalk.green('LONG ↗') : chalk.red('SHORT ā†˜')}`,
832
+ );
833
+ console.log(
834
+ ` Status: ${position.notionalValue > 0n ? chalk.green('Active') : chalk.gray('Closed')}`,
835
+ );
836
+ } else {
837
+ spinner.stop();
838
+ console.log(chalk.yellow(`\nNo position found for ${options.asset.toUpperCase()}`));
839
+ }
840
+ } else {
841
+ // Find all positions for trader
842
+ const positionIds = await alpha.getUserPositions(userAddress as Address);
843
+
844
+ if (positionIds.length === 0) {
845
+ spinner.stop();
846
+ console.log(chalk.yellow(`\nNo positions found for trader: ${userAddress}`));
847
+ return;
848
+ }
849
+
850
+ spinner.stop();
851
+
852
+ console.log(chalk.bold(`\nšŸ“‹ All Positions: ${chalk.cyan(userAddress)}`));
853
+ console.log(` Total Positions: ${positionIds.length}`);
854
+
855
+ const table = new Table({
856
+ head: ['#', 'Position ID', 'Size', 'Direction', 'Status'],
857
+ colWidths: [5, 42, 15, 12, 10],
858
+ });
859
+
860
+ for (let i = 0; i < positionIds.length; i++) {
861
+ try {
862
+ const position = await alpha.getPosition(positionIds[i]);
863
+ const status =
864
+ position.notionalValue > 0n ? chalk.green('Active') : chalk.gray('Closed');
865
+ const direction = position.isLong ? chalk.green('LONG') : chalk.red('SHORT');
866
+
867
+ table.push([
868
+ i + 1,
869
+ positionIds[i],
870
+ formatUSD(position.notionalValue),
871
+ direction,
872
+ status,
873
+ ]);
874
+ } catch (error) {
875
+ table.push([
876
+ i + 1,
877
+ positionIds[i],
878
+ chalk.red('Error'),
879
+ chalk.red('Error'),
880
+ chalk.red('Error'),
881
+ ]);
882
+ }
883
+ }
884
+
885
+ console.log(table.toString());
886
+ console.log(
887
+ chalk.gray(
888
+ '\nšŸ’” Use "alpha-futures position info <positionId>" for detailed information',
889
+ ),
890
+ );
891
+ }
892
+ } catch (error) {
893
+ spinner.fail('Failed to find positions');
894
+ handleError(error, program.opts());
895
+ }
896
+ });
897
+
898
+ // Modify position command
899
+ position
900
+ .command('modify <positionId>')
901
+ .description('Modify an existing position (add/remove margin)')
902
+ .option('-a, --add <amount>', 'Add margin to position')
903
+ .option('-r, --remove <amount>', 'Remove margin from position')
904
+ .option('-y, --yes', 'Skip confirmation')
905
+ .addHelpText(
906
+ 'after',
907
+ `
908
+ Examples:
909
+ $ alpha-futures position modify 0x00000000000000000000000000000001 --add 100
910
+ $ alpha-futures position modify 0x12345000000000000000000000000000 --remove 50 --yes
911
+
912
+ Note: Position ID must be in bytes16 format (32 hex characters after 0x).
913
+ Use "alpha-futures position find" to get your position IDs.`,
914
+ )
915
+ .action(async (positionId, options) => {
916
+ if (!options.add && !options.remove) {
917
+ console.error(chalk.red('āŒ Must specify either --add or --remove'));
918
+ return;
919
+ }
920
+
921
+ const spinner = ora('Loading position...').start();
922
+
923
+ try {
924
+ const client = await getClient(program.opts());
925
+ const alpha = client.getAlpha();
926
+ const normalizedPositionId = normalizePositionId(positionId);
927
+ const position = await alpha.getPosition(normalizedPositionId);
928
+
929
+ if (position.notionalValue === 0n) {
930
+ spinner.fail('Position is not active');
931
+ return;
932
+ }
933
+
934
+ spinner.stop();
935
+
936
+ // Use BITMIND as default market for testing
937
+ const marketAddress = getMarketAddress('BITMIND'); // Default to BITMIND for testing
938
+ const currentPrice = await client.oracle.getPrice(marketAddress);
939
+
940
+ console.log(chalk.bold('\nšŸ“Š Current Position:'));
941
+ displayPositionDetails(position, currentPrice);
942
+
943
+ if (options.add) {
944
+ const addAmount = parseEther(options.add);
945
+
946
+ // Check available balance first
947
+ const userAddress = await client.getSignerAddress();
948
+ const availableMargin = await alpha.getAvailableMargin(userAddress);
949
+ if (addAmount > availableMargin) {
950
+ console.error(chalk.red('āŒ Insufficient available margin'));
951
+ console.log(` Required: ${formatTAO(addAmount)} TAO`);
952
+ console.log(` Available: ${formatTAO(availableMargin)} TAO`);
953
+ console.log(
954
+ chalk.yellow('šŸ’” Use "alpha-futures account deposit <amount>" to deposit more TAO'),
955
+ );
956
+ return;
957
+ }
958
+
959
+ const newMargin = position.margin + addAmount;
960
+ const newLeverage =
961
+ position.notionalValue > 0n ? Number(position.notionalValue) / Number(newMargin) : 0;
962
+
963
+ console.log(chalk.bold('\nāž• Add Margin:'));
964
+ console.log(` Amount to Add: ${formatTAO(addAmount)} TAO`);
965
+ console.log(` New Margin: ${formatTAO(newMargin)} TAO`);
966
+ console.log(` New Leverage: ${newLeverage.toFixed(2)}x`);
967
+
968
+ if (!options.yes && !(await confirmAction('Add margin to position?'))) {
969
+ console.log(chalk.yellow('Cancelled'));
970
+ return;
971
+ }
972
+
973
+ spinner.start('Adding margin...');
974
+ try {
975
+ const hash = await alpha.addMargin(normalizedPositionId, addAmount);
976
+ spinner.text = 'Waiting for confirmation...';
977
+ const publicClient = client.getPublicClient();
978
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
979
+ spinner.succeed('Margin added successfully!');
980
+ console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
981
+ console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
982
+ } catch (error: any) {
983
+ spinner.fail('Failed to add margin');
984
+ if (error.message?.includes('insufficient funds')) {
985
+ console.error(chalk.red('\nāŒ Insufficient TAO balance'));
986
+ console.log(
987
+ chalk.yellow(
988
+ 'šŸ’” Make sure you have deposited enough TAO using "alpha-futures account deposit <amount>"',
989
+ ),
990
+ );
991
+ } else if (error.message?.includes('revert')) {
992
+ console.error(chalk.red('\nāŒ Contract reverted the transaction'));
993
+ console.log(chalk.gray('Reason: ' + (error.reason || error.message)));
994
+ } else {
995
+ console.error(chalk.red('\nāŒ Error: ' + error.message));
996
+ }
997
+ return;
998
+ }
999
+ } else if (options.remove) {
1000
+ const removeAmount = parseEther(options.remove);
1001
+
1002
+ // Validate remove amount
1003
+ if (removeAmount > position.margin) {
1004
+ console.error(chalk.red('āŒ Cannot remove more than current margin'));
1005
+ console.log(` Current margin: ${formatTAO(position.margin)} TAO`);
1006
+ console.log(` Requested removal: ${formatTAO(removeAmount)} TAO`);
1007
+ return;
1008
+ }
1009
+
1010
+ const newMargin = position.margin - removeAmount;
1011
+
1012
+ // Simple maintenance margin check: ensure position doesn't exceed max leverage (30x)
1013
+ // Minimum margin = position size / max leverage
1014
+ const maxLeverage = 30n;
1015
+ const minMarginForMaxLeverage = position.notionalValue / maxLeverage;
1016
+
1017
+ // Also ensure we don't go below 20% margin ratio (maintenance margin)
1018
+ // For a position, minimum margin ā‰ˆ position value * 0.2 / current_price
1019
+ // Simplified: position.notionalValue already includes leverage, so min = position.notionalValue / 5 (20%)
1020
+ const maintenanceMarginSimple = position.notionalValue / 5n; // 20% maintenance
1021
+ const minMargin =
1022
+ minMarginForMaxLeverage > maintenanceMarginSimple
1023
+ ? minMarginForMaxLeverage
1024
+ : maintenanceMarginSimple;
1025
+
1026
+ if (newMargin < minMargin) {
1027
+ console.error(chalk.red('āŒ Cannot remove that much margin'));
1028
+ console.log(` Current margin: ${formatTAO(position.margin)} TAO`);
1029
+ console.log(` Requested removal: ${formatTAO(removeAmount)} TAO`);
1030
+ console.log(` New margin would be: ${formatTAO(newMargin)} TAO`);
1031
+ console.log(` Minimum required margin: ${formatTAO(minMargin)} TAO`);
1032
+ console.log(` Maximum removable: ${formatTAO(position.margin - minMargin)} TAO`);
1033
+ console.log(chalk.gray(' (Based on 30x max leverage and 20% maintenance margin)'));
1034
+ return;
1035
+ }
1036
+
1037
+ const newLeverage =
1038
+ position.notionalValue > 0n ? Number(position.notionalValue) / Number(newMargin) : 0;
1039
+
1040
+ // The margin validation above already handles max leverage, so this is just for display
1041
+
1042
+ console.log(chalk.bold('\nāž– Remove Margin:'));
1043
+ console.log(` Amount to Remove: ${formatTAO(removeAmount)} TAO`);
1044
+ console.log(` New Margin: ${formatTAO(newMargin)} TAO`);
1045
+ console.log(` New Leverage: ${newLeverage.toFixed(2)}x`);
1046
+ console.log(` Required Margin: ${formatTAO(minMargin)} TAO`);
1047
+
1048
+ if (!options.yes && !(await confirmAction('Remove margin from position?'))) {
1049
+ console.log(chalk.yellow('Cancelled'));
1050
+ return;
1051
+ }
1052
+
1053
+ spinner.start('Removing margin...');
1054
+ try {
1055
+ const hash = await alpha.removeMargin(normalizedPositionId, removeAmount);
1056
+ spinner.text = 'Waiting for confirmation...';
1057
+ const publicClient = client.getPublicClient();
1058
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
1059
+ spinner.succeed('Margin removed successfully!');
1060
+ console.log(chalk.gray(` Transaction: ${receipt.transactionHash}`));
1061
+ console.log(chalk.gray(` Gas used: ${receipt.gasUsed.toString()}`));
1062
+ } catch (error: any) {
1063
+ spinner.fail('Failed to remove margin');
1064
+ if (error.message?.includes('insufficient margin')) {
1065
+ console.error(chalk.red('\nāŒ Insufficient margin for removal'));
1066
+ console.log(
1067
+ chalk.yellow(
1068
+ 'šŸ’” The contract rejected the removal. Position may be too close to liquidation.',
1069
+ ),
1070
+ );
1071
+ } else if (error.message?.includes('revert')) {
1072
+ console.error(chalk.red('\nāŒ Contract reverted the transaction'));
1073
+ console.log(chalk.gray('Reason: ' + (error.reason || error.message)));
1074
+ } else {
1075
+ console.error(chalk.red('\nāŒ Error: ' + error.message));
1076
+ }
1077
+ return;
1078
+ }
1079
+ }
1080
+ } catch (error) {
1081
+ spinner.fail('Failed to modify position');
1082
+ handleError(error, program.opts());
1083
+ }
1084
+ });
1085
+ }