OrdersAdapter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { randomUUID } from 'crypto'
  2. export class PacificaOrdersAdapter {
  3. constructor(client) {
  4. this.client = client
  5. }
  6. normalizeSymbol(input) {
  7. if (!input) return input
  8. const s = String(input).toUpperCase()
  9. if (s.includes('-')) return s.split('-')[0]
  10. if (s.endsWith('USDT')) return s.replace('USDT', '')
  11. if (s.endsWith('USD')) return s.replace('USD', '')
  12. return s
  13. }
  14. async createMarketOrder(payload) {
  15. // For market orders, only use required fields per API docs
  16. const marketPayload = {
  17. account: payload.account,
  18. symbol: payload.symbol,
  19. amount: payload.amount,
  20. side: payload.side,
  21. reduceOnly: payload.reduceOnly,
  22. slippagePercent: payload.slippagePercent || '0.5',
  23. }
  24. return await this.sendSigned(this.client.endpoints.orderCreateMarket, 'create_market_order', marketPayload)
  25. }
  26. async createLimitOrder(payload) {
  27. return await this.sendSigned(this.client.endpoints.orderCreateLimit, 'create_order', payload)
  28. }
  29. async createStopOrder(payload) {
  30. return await this.sendSigned(this.client.endpoints.orderCreateStop, 'create_stop_order', payload)
  31. }
  32. async cancelOrder(payload) {
  33. return await this.sendSigned(this.client.endpoints.orderCancel, 'cancel_order', payload)
  34. }
  35. async cancelAll(payload) {
  36. const account = this.client.requireAccount()
  37. const data = {
  38. all_symbols: payload.allSymbols ?? !payload.symbol,
  39. exclude_reduce_only: payload.excludeReduceOnly ?? false,
  40. symbol: this.normalizeSymbol(payload.symbol),
  41. }
  42. const { signature, timestamp, expiryWindow } = await this.client.signOperation('cancel_all_orders', data, 30000)
  43. const body = { account, ...data, signature, timestamp, expiry_window: expiryWindow }
  44. return await this.client.post(this.client.endpoints.orderCancelAll, body, { skipHeaderSig: true })
  45. }
  46. async openOrders(symbol, account) {
  47. const query = { account: this.client.requireAccount(account) }
  48. if (symbol) query.symbol = symbol
  49. return await this.client.get(this.client.endpoints.openOrders, query)
  50. }
  51. async batch(actions) {
  52. const signedActions = await Promise.all(
  53. actions.map(async action => {
  54. if (action.type === 'Create') {
  55. const raw = action.data
  56. // Normalize incoming fields from external callers (reduce_only -> reduceOnly)
  57. const payload = {
  58. account: raw.account,
  59. symbol: raw.symbol,
  60. amount: raw.amount,
  61. side: raw.side,
  62. reduceOnly: raw.reduceOnly !== undefined ? raw.reduceOnly : raw.reduce_only ?? false,
  63. clientOrderId: raw.clientOrderId ?? raw.client_order_id,
  64. tif: raw.tif,
  65. price: raw.price,
  66. slippagePercent: raw.slippagePercent ?? raw.slippage_percent,
  67. takeProfit: raw.takeProfit,
  68. stopLoss: raw.stopLoss,
  69. agentWallet: raw.agentWallet ?? raw.agent_wallet,
  70. expiryWindow: raw.expiryWindow,
  71. }
  72. const transformed = this.transformCreatePayload(
  73. payload,
  74. this.client.cfg.agentPrivateKey ? this.client.requireAgentWallet(payload.agentWallet) : undefined,
  75. )
  76. const dataToSign = this.pickOrderSignFields('create_order', transformed)
  77. const { signature, timestamp, expiryWindow } = await this.client.signOperation(
  78. 'create_order',
  79. dataToSign,
  80. payload.expiryWindow ?? 30000,
  81. )
  82. return { type: 'Create', data: { ...transformed, signature, timestamp, expiry_window: expiryWindow } }
  83. }
  84. const cp = action.data
  85. const transformedCancel = this.transformCancelPayload(cp)
  86. const cancelSign = this.pickCancelSignFields(transformedCancel)
  87. const { signature, timestamp, expiryWindow } = await this.client.signOperation(
  88. 'cancel_order',
  89. cancelSign,
  90. cp.expiryWindow ?? 30000,
  91. )
  92. return { type: 'Cancel', data: { ...transformedCancel, signature, timestamp, expiry_window: expiryWindow } }
  93. }),
  94. )
  95. return await this.client.post(this.client.endpoints.orderBatch, { actions: signedActions }, { skipHeaderSig: true })
  96. }
  97. transformCreatePayload(payload, agentWallet, isMarketOrder = false) {
  98. const clientOrderId = payload.clientOrderId || randomUUID()
  99. // For market orders, use minimal required fields per API error messages
  100. if (isMarketOrder) {
  101. return {
  102. account: payload.account,
  103. symbol: this.normalizeSymbol(payload.symbol),
  104. amount: payload.amount,
  105. side: payload.side,
  106. reduce_only: payload.reduceOnly,
  107. slippage_percent: payload.slippagePercent || '1.0',
  108. agent_wallet: agentWallet ?? payload.agentWallet,
  109. }
  110. }
  111. // For limit orders and others, include all fields
  112. const takeProfit = payload.takeProfit
  113. ? {
  114. stop_price: payload.takeProfit.stopPrice,
  115. limit_price: payload.takeProfit.limitPrice,
  116. client_order_id: payload.takeProfit.clientOrderId,
  117. }
  118. : undefined
  119. const stopLoss = payload.stopLoss
  120. ? {
  121. stop_price: payload.stopLoss.stopPrice,
  122. limit_price: payload.stopLoss.limitPrice,
  123. client_order_id: payload.stopLoss.clientOrderId,
  124. }
  125. : undefined
  126. return {
  127. account: payload.account,
  128. symbol: this.normalizeSymbol(payload.symbol),
  129. amount: payload.amount,
  130. side: payload.side,
  131. reduce_only: payload.reduceOnly,
  132. client_order_id: clientOrderId,
  133. tif: payload.tif ?? 'GTC',
  134. price: payload.price,
  135. slippage_percent: payload.slippagePercent,
  136. take_profit: takeProfit,
  137. stop_loss: stopLoss,
  138. agent_wallet: agentWallet ?? payload.agentWallet,
  139. }
  140. }
  141. async buildSignedPayload(operation, data, expiryWindow = 30000, useAgent = false) {
  142. const signer = useAgent
  143. ? this.client.signOperationWithAgent.bind(this.client)
  144. : this.client.signOperation.bind(this.client)
  145. const { signature, timestamp, expiryWindow: ew } = await signer(operation, data, expiryWindow)
  146. return {
  147. ...data,
  148. signature,
  149. timestamp,
  150. expiry_window: ew,
  151. }
  152. }
  153. async sendSigned(path, operation, payload) {
  154. const isOrder = operation.startsWith('create_')
  155. const useAgent = isOrder && !!this.client.cfg.agentPrivateKey
  156. if (operation === 'cancel_order') {
  157. const transformed = this.transformCancelPayload(payload)
  158. const expiryWindow = payload.expiryWindow ?? 30000
  159. const dataToSign = this.pickCancelSignFields(transformed)
  160. const {
  161. signature,
  162. timestamp,
  163. expiryWindow: ew,
  164. } = await this.client.signOperation(operation, dataToSign, expiryWindow)
  165. const finalBody = { ...transformed, signature, timestamp, expiry_window: ew }
  166. if (process.env.PACIFICA_DEBUG === '1') {
  167. // eslint-disable-next-line no-console
  168. console.log('[pacifica.debug] op=', operation)
  169. // eslint-disable-next-line no-console
  170. console.log('[pacifica.debug] sign_data=', JSON.stringify(dataToSign))
  171. // eslint-disable-next-line no-console
  172. console.log(
  173. '[pacifica.debug] final_body=',
  174. JSON.stringify({ ...finalBody, signature: `b58(${String(finalBody.signature).slice(0, 8)}...)` }),
  175. )
  176. }
  177. return await this.client.post(path, finalBody, { skipHeaderSig: true })
  178. }
  179. // create_* flow: sign ONLY the operation fields per docs, NOT account/agent_wallet
  180. const transformed =
  181. operation === 'create_stop_order'
  182. ? (() => {
  183. const p = payload
  184. const clientId = p.clientOrderId || randomUUID()
  185. return {
  186. account: p.account,
  187. symbol: this.normalizeSymbol(p.symbol),
  188. side: p.side,
  189. reduce_only: p.reduceOnly,
  190. stop_order: {
  191. stop_price: p.stopLoss?.stopPrice,
  192. limit_price: p.stopLoss?.limitPrice,
  193. client_order_id: clientId,
  194. amount: p.amount,
  195. },
  196. agent_wallet: useAgent ? this.client.requireAgentWallet(p.agentWallet) : p.agentWallet,
  197. }
  198. })()
  199. : this.transformCreatePayload(
  200. payload,
  201. useAgent ? this.client.requireAgentWallet(payload.agentWallet) : undefined,
  202. operation === 'create_market_order',
  203. )
  204. const expiryWindow = payload.expiryWindow ?? 5000
  205. const dataToSign = this.pickOrderSignFields(operation, transformed)
  206. const signer = useAgent
  207. ? this.client.signOperationWithAgent.bind(this.client)
  208. : this.client.signOperation.bind(this.client)
  209. const { signature, timestamp, expiryWindow: ew } = await signer(operation, dataToSign, expiryWindow)
  210. const finalBody = {
  211. ...transformed,
  212. timestamp,
  213. expiry_window: ew,
  214. signature,
  215. }
  216. if (process.env.PACIFICA_DEBUG === '1') {
  217. // eslint-disable-next-line no-console
  218. console.log('[pacifica.debug] op=', operation)
  219. // eslint-disable-next-line no-console
  220. console.log('[pacifica.debug] sign_data=', JSON.stringify(dataToSign))
  221. // eslint-disable-next-line no-console
  222. console.log(
  223. '[pacifica.debug] final_body=',
  224. JSON.stringify({ ...finalBody, signature: `b58(${String(finalBody.signature).slice(0, 8)}...)` }),
  225. )
  226. }
  227. return await this.client.post(path, finalBody, { skipHeaderSig: true })
  228. }
  229. pickOrderSignFields(operation, transformed) {
  230. const base = {
  231. symbol: transformed.symbol,
  232. reduce_only: transformed.reduce_only,
  233. side: transformed.side,
  234. }
  235. if (operation === 'create_market_order') {
  236. // Market order fields per API requirements
  237. base.amount = transformed.amount
  238. if (transformed.slippage_percent !== undefined) {
  239. base.slippage_percent = transformed.slippage_percent
  240. }
  241. return base
  242. }
  243. // For other orders
  244. base.amount = transformed.amount || transformed.quantity
  245. // Add client_order_id for other order types
  246. base.client_order_id = transformed.client_order_id
  247. if (operation === 'create_stop_order') {
  248. base.side = transformed.side
  249. base.reduce_only = transformed.reduce_only
  250. base.stop_order = {
  251. stop_price: transformed.stop_order?.stop_price,
  252. limit_price: transformed.stop_order?.limit_price,
  253. amount: transformed.stop_order?.amount,
  254. client_order_id: transformed.stop_order?.client_order_id,
  255. }
  256. return base
  257. }
  258. if (operation === 'create_stop_order') {
  259. if (transformed.stop_price !== undefined) base.stop_price = transformed.stop_price
  260. if (transformed.limit_price !== undefined) base.limit_price = transformed.limit_price
  261. return base
  262. }
  263. // create_order (limit)
  264. if (transformed.tif !== undefined) base.tif = transformed.tif
  265. if (transformed.price !== undefined) base.price = transformed.price
  266. return base
  267. }
  268. transformCancelPayload(payload) {
  269. if (!payload.orderId && !payload.clientOrderId) {
  270. throw new Error('PacificaOrdersAdapter: orderId or clientOrderId required')
  271. }
  272. return {
  273. account: payload.account,
  274. symbol: this.normalizeSymbol(payload.symbol),
  275. order_id: payload.orderId,
  276. client_order_id: payload.clientOrderId,
  277. }
  278. }
  279. pickCancelSignFields(transformed) {
  280. const base = { symbol: transformed.symbol }
  281. if (transformed.order_id !== undefined && transformed.order_id !== null) base.order_id = transformed.order_id
  282. else if (transformed.client_order_id) base.client_order_id = transformed.client_order_id
  283. return base
  284. }
  285. }