@nadohq/mobile-client 0.64.0 → 0.66.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 (225) hide show
  1. package/README.md +124 -7
  2. package/dist/MobileClient-M4oC4lrX.cjs +1101 -0
  3. package/dist/MobileClient-M4oC4lrX.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +306 -10
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +306 -10
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +544 -5
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-Dd3c6P_Z.d.ts → MobileServerFailureError-C9iqNsxI.d.ts} +2 -3
  12. package/dist/MobileServerFailureError-C9iqNsxI.d.ts.map +1 -0
  13. package/dist/{MobileServerFailureError-Bpa_WA9f.d.cts → MobileServerFailureError-MtS2reel.d.cts} +2 -3
  14. package/dist/MobileServerFailureError-MtS2reel.d.cts.map +1 -0
  15. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.cts +15 -0
  16. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.cts.map +1 -0
  17. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.ts +15 -0
  18. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.ts.map +1 -0
  19. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts +15 -0
  20. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts.map +1 -0
  21. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts +15 -0
  22. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts.map +1 -0
  23. package/dist/{buildSignedMobileRequest-Cb1T74xV.d.ts → buildSignedMobileRequest-BR-KJhIF.d.ts} +2 -2
  24. package/dist/{buildSignedMobileRequest-Cb1T74xV.d.ts.map → buildSignedMobileRequest-BR-KJhIF.d.ts.map} +1 -1
  25. package/dist/{buildSignedMobileRequest-NHbMAPzd.d.cts → buildSignedMobileRequest-DyKCK34Y.d.cts} +2 -2
  26. package/dist/{buildSignedMobileRequest-NHbMAPzd.d.cts.map → buildSignedMobileRequest-DyKCK34Y.d.cts.map} +1 -1
  27. package/dist/{canonicalize-BBYGh17w.d.ts → canonicalize-Bv6UIAbR.d.ts} +2 -2
  28. package/dist/canonicalize-Bv6UIAbR.d.ts.map +1 -0
  29. package/dist/{canonicalize-BF_SiNvO.d.cts → canonicalize-CWh3XZzj.d.cts} +2 -2
  30. package/dist/canonicalize-CWh3XZzj.d.cts.map +1 -0
  31. package/dist/clientTypes-BUn5_OzK.d.ts +1665 -0
  32. package/dist/clientTypes-BUn5_OzK.d.ts.map +1 -0
  33. package/dist/clientTypes-DOpUcu80.d.cts +1665 -0
  34. package/dist/clientTypes-DOpUcu80.d.cts.map +1 -0
  35. package/dist/dataMappers.cjs +691 -14
  36. package/dist/dataMappers.cjs.map +1 -1
  37. package/dist/dataMappers.d.cts +254 -8
  38. package/dist/dataMappers.d.cts.map +1 -1
  39. package/dist/dataMappers.d.ts +254 -8
  40. package/dist/dataMappers.d.ts.map +1 -1
  41. package/dist/dataMappers.js +651 -15
  42. package/dist/dataMappers.js.map +1 -1
  43. package/dist/{eip712MethodByType-BSx11xG-.d.cts → eip712MethodByType-CJ6BoayT.d.cts} +2 -2
  44. package/dist/eip712MethodByType-CJ6BoayT.d.cts.map +1 -0
  45. package/dist/{eip712MethodByType-DkiAjCLs.d.ts → eip712MethodByType-Dx4mvgni.d.ts} +2 -2
  46. package/dist/eip712MethodByType-Dx4mvgni.d.ts.map +1 -0
  47. package/dist/{feed-DTJlsg8W.d.ts → feed-DErOLaoY.d.cts} +4 -2
  48. package/dist/feed-DErOLaoY.d.cts.map +1 -0
  49. package/dist/{feed-DTJlsg8W.d.cts → feed-DErOLaoY.d.ts} +4 -2
  50. package/dist/feed-DErOLaoY.d.ts.map +1 -0
  51. package/dist/index.cjs +9 -2
  52. package/dist/index.d.cts +15 -16
  53. package/dist/index.d.ts +15 -16
  54. package/dist/index.js +5 -3
  55. package/dist/{mobileErrorCodes-3LtreeZM.d.ts → mobileErrorCodes-vD6rbFuF.d.cts} +25 -6
  56. package/dist/mobileErrorCodes-vD6rbFuF.d.cts.map +1 -0
  57. package/dist/{mobileErrorCodes-3LtreeZM.d.cts → mobileErrorCodes-vD6rbFuF.d.ts} +25 -6
  58. package/dist/mobileErrorCodes-vD6rbFuF.d.ts.map +1 -0
  59. package/dist/{payloadHash-BOWGjqdb.d.cts → payloadHash-CdkF2Ocr.d.cts} +2 -2
  60. package/dist/payloadHash-CdkF2Ocr.d.cts.map +1 -0
  61. package/dist/{payloadHash-Bzf4CtjV.d.ts → payloadHash-HD29K77M.d.ts} +2 -2
  62. package/dist/payloadHash-HD29K77M.d.ts.map +1 -0
  63. package/dist/serverExecuteTypes-5JyyIXRK.d.ts +178 -0
  64. package/dist/serverExecuteTypes-5JyyIXRK.d.ts.map +1 -0
  65. package/dist/serverExecuteTypes-CHrQ9Rtq.d.cts +178 -0
  66. package/dist/serverExecuteTypes-CHrQ9Rtq.d.cts.map +1 -0
  67. package/dist/serverModelTypes-CGUZCi0y.d.cts +938 -0
  68. package/dist/serverModelTypes-CGUZCi0y.d.cts.map +1 -0
  69. package/dist/serverModelTypes-CGUZCi0y.d.ts +938 -0
  70. package/dist/serverModelTypes-CGUZCi0y.d.ts.map +1 -0
  71. package/dist/{serverResponseGuards-BU1Xv_tG.d.ts → serverResponseGuards-Bvw8ltYJ.d.ts} +2 -2
  72. package/dist/{serverResponseGuards-BU1Xv_tG.d.ts.map → serverResponseGuards-Bvw8ltYJ.d.ts.map} +1 -1
  73. package/dist/{serverResponseGuards-Dkdok0Sm.d.cts → serverResponseGuards-i1njcYKN.d.cts} +2 -2
  74. package/dist/{serverResponseGuards-Dkdok0Sm.d.cts.map → serverResponseGuards-i1njcYKN.d.cts.map} +1 -1
  75. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  76. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  77. package/dist/signing/canonicalize.cjs +86 -0
  78. package/dist/signing/canonicalize.cjs.map +1 -1
  79. package/dist/signing/canonicalize.d.cts +1 -1
  80. package/dist/signing/canonicalize.d.ts +1 -1
  81. package/dist/signing/canonicalize.js +86 -0
  82. package/dist/signing/canonicalize.js.map +1 -1
  83. package/dist/signing/eip712MethodByType.cjs +12 -0
  84. package/dist/signing/eip712MethodByType.cjs.map +1 -1
  85. package/dist/signing/eip712MethodByType.d.cts +1 -1
  86. package/dist/signing/eip712MethodByType.d.ts +1 -1
  87. package/dist/signing/eip712MethodByType.js +12 -0
  88. package/dist/signing/eip712MethodByType.js.map +1 -1
  89. package/dist/signing/index.d.cts +5 -5
  90. package/dist/signing/index.d.ts +5 -5
  91. package/dist/signing/payloadHash.cjs +1 -1
  92. package/dist/signing/payloadHash.d.cts +1 -1
  93. package/dist/signing/payloadHash.d.ts +1 -1
  94. package/dist/signing/payloadHash.js +1 -1
  95. package/dist/signing/types.d.cts +1 -1
  96. package/dist/signing/types.d.ts +1 -1
  97. package/dist/types/MobileServerFailureError.d.cts +1 -1
  98. package/dist/types/MobileServerFailureError.d.ts +1 -1
  99. package/dist/types/MobileUnsupportedFeedItemError.cjs +19 -0
  100. package/dist/types/MobileUnsupportedFeedItemError.cjs.map +1 -0
  101. package/dist/types/MobileUnsupportedFeedItemError.d.cts +2 -0
  102. package/dist/types/MobileUnsupportedFeedItemError.d.ts +2 -0
  103. package/dist/types/MobileUnsupportedFeedItemError.js +18 -0
  104. package/dist/types/MobileUnsupportedFeedItemError.js.map +1 -0
  105. package/dist/types/MobileUnsupportedNotificationRouteError.cjs +19 -0
  106. package/dist/types/MobileUnsupportedNotificationRouteError.cjs.map +1 -0
  107. package/dist/types/MobileUnsupportedNotificationRouteError.d.cts +2 -0
  108. package/dist/types/MobileUnsupportedNotificationRouteError.d.ts +2 -0
  109. package/dist/types/MobileUnsupportedNotificationRouteError.js +18 -0
  110. package/dist/types/MobileUnsupportedNotificationRouteError.js.map +1 -0
  111. package/dist/types/clientTypes.d.cts +2 -2
  112. package/dist/types/clientTypes.d.ts +2 -2
  113. package/dist/types/index.cjs +6 -0
  114. package/dist/types/index.d.cts +9 -9
  115. package/dist/types/index.d.ts +9 -9
  116. package/dist/types/index.js +4 -2
  117. package/dist/types/mobileErrorCodes.cjs +39 -1
  118. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  119. package/dist/types/mobileErrorCodes.d.cts +1 -1
  120. package/dist/types/mobileErrorCodes.d.ts +1 -1
  121. package/dist/types/mobileErrorCodes.js +39 -1
  122. package/dist/types/mobileErrorCodes.js.map +1 -1
  123. package/dist/types/serverBaseTypes.d.cts +1 -1
  124. package/dist/types/serverBaseTypes.d.ts +1 -1
  125. package/dist/types/serverExecuteTypes.d.cts +2 -2
  126. package/dist/types/serverExecuteTypes.d.ts +2 -2
  127. package/dist/types/serverModelTypes.cjs +15 -0
  128. package/dist/types/serverModelTypes.cjs.map +1 -1
  129. package/dist/types/serverModelTypes.d.cts +2 -2
  130. package/dist/types/serverModelTypes.d.ts +2 -2
  131. package/dist/types/serverModelTypes.js +14 -1
  132. package/dist/types/serverModelTypes.js.map +1 -1
  133. package/dist/types/serverQueryTypes.d.cts +2 -2
  134. package/dist/types/serverQueryTypes.d.ts +2 -2
  135. package/dist/types/serverRequestTypes.d.cts +2 -2
  136. package/dist/types/serverRequestTypes.d.ts +2 -2
  137. package/dist/types-C9YE4EIt.d.cts +794 -0
  138. package/dist/types-C9YE4EIt.d.cts.map +1 -0
  139. package/dist/types-Cwy1y_9x.d.ts +794 -0
  140. package/dist/types-Cwy1y_9x.d.ts.map +1 -0
  141. package/dist/utils/buildMobileFeedImageMultipart.cjs +31 -0
  142. package/dist/utils/buildMobileFeedImageMultipart.cjs.map +1 -0
  143. package/dist/utils/buildMobileFeedImageMultipart.d.cts +15 -0
  144. package/dist/utils/buildMobileFeedImageMultipart.d.cts.map +1 -0
  145. package/dist/utils/buildMobileFeedImageMultipart.d.ts +15 -0
  146. package/dist/utils/buildMobileFeedImageMultipart.d.ts.map +1 -0
  147. package/dist/utils/buildMobileFeedImageMultipart.js +30 -0
  148. package/dist/utils/buildMobileFeedImageMultipart.js.map +1 -0
  149. package/dist/utils/feed.cjs +3 -0
  150. package/dist/utils/feed.cjs.map +1 -1
  151. package/dist/utils/feed.d.cts +2 -2
  152. package/dist/utils/feed.d.ts +2 -2
  153. package/dist/utils/feed.js +3 -1
  154. package/dist/utils/feed.js.map +1 -1
  155. package/dist/utils/index.cjs +2 -1
  156. package/dist/utils/index.d.cts +3 -3
  157. package/dist/utils/index.d.ts +3 -3
  158. package/dist/utils/index.js +2 -2
  159. package/dist/utils/serverResponseGuards.d.cts +1 -1
  160. package/dist/utils/serverResponseGuards.d.ts +1 -1
  161. package/package.json +3 -3
  162. package/src/MobileClient.ts +789 -7
  163. package/src/dataMappers.test.ts +848 -0
  164. package/src/dataMappers.ts +924 -17
  165. package/src/feedImages.test.ts +309 -0
  166. package/src/notifications.test.ts +950 -0
  167. package/src/signing/canonicalize.ts +119 -0
  168. package/src/signing/eip712MethodByType.ts +15 -0
  169. package/src/signing/signing.test.ts +362 -0
  170. package/src/signing/types.ts +108 -13
  171. package/src/socialFeed.test.ts +38 -2
  172. package/src/types/MobileUnsupportedFeedItemError.ts +18 -0
  173. package/src/types/MobileUnsupportedNotificationRouteError.ts +16 -0
  174. package/src/types/clientTypes.ts +1012 -34
  175. package/src/types/index.ts +2 -0
  176. package/src/types/mobileErrorCodes.ts +38 -0
  177. package/src/types/serverExecuteTypes.ts +112 -3
  178. package/src/types/serverModelTypes.ts +581 -28
  179. package/src/types/serverQueryTypes.ts +212 -5
  180. package/src/types/serverRequestTypes.ts +213 -13
  181. package/src/utils/buildMobileFeedImageMultipart.test.ts +45 -0
  182. package/src/utils/buildMobileFeedImageMultipart.ts +36 -0
  183. package/src/utils/feed.ts +3 -0
  184. package/dist/MobileClient-aDxiM_Kf.cjs +0 -562
  185. package/dist/MobileClient-aDxiM_Kf.cjs.map +0 -1
  186. package/dist/MobileServerFailureError-Bpa_WA9f.d.cts.map +0 -1
  187. package/dist/MobileServerFailureError-Dd3c6P_Z.d.ts.map +0 -1
  188. package/dist/canonicalize-BBYGh17w.d.ts.map +0 -1
  189. package/dist/canonicalize-BF_SiNvO.d.cts.map +0 -1
  190. package/dist/clientTypes-BWFKo9oE.d.cts +0 -844
  191. package/dist/clientTypes-BWFKo9oE.d.cts.map +0 -1
  192. package/dist/clientTypes-DCTiUV0H.d.ts +0 -844
  193. package/dist/clientTypes-DCTiUV0H.d.ts.map +0 -1
  194. package/dist/eip712MethodByType-BSx11xG-.d.cts.map +0 -1
  195. package/dist/eip712MethodByType-DkiAjCLs.d.ts.map +0 -1
  196. package/dist/feed-DTJlsg8W.d.cts.map +0 -1
  197. package/dist/feed-DTJlsg8W.d.ts.map +0 -1
  198. package/dist/mobileErrorCodes-3LtreeZM.d.cts.map +0 -1
  199. package/dist/mobileErrorCodes-3LtreeZM.d.ts.map +0 -1
  200. package/dist/payloadHash-BOWGjqdb.d.cts.map +0 -1
  201. package/dist/payloadHash-Bzf4CtjV.d.ts.map +0 -1
  202. package/dist/serverBaseTypes-DWf3iGWy.d.ts +0 -25
  203. package/dist/serverBaseTypes-DWf3iGWy.d.ts.map +0 -1
  204. package/dist/serverBaseTypes-KREDe2M9.d.cts +0 -25
  205. package/dist/serverBaseTypes-KREDe2M9.d.cts.map +0 -1
  206. package/dist/serverExecuteTypes-DVRa6ifu.d.cts +0 -89
  207. package/dist/serverExecuteTypes-DVRa6ifu.d.cts.map +0 -1
  208. package/dist/serverExecuteTypes-DlYZCp1f.d.ts +0 -89
  209. package/dist/serverExecuteTypes-DlYZCp1f.d.ts.map +0 -1
  210. package/dist/serverModelTypes-DAqEyYtB.d.cts +0 -508
  211. package/dist/serverModelTypes-DAqEyYtB.d.cts.map +0 -1
  212. package/dist/serverModelTypes-DAqEyYtB.d.ts +0 -508
  213. package/dist/serverModelTypes-DAqEyYtB.d.ts.map +0 -1
  214. package/dist/serverQueryTypes-BFdwq0Z2.d.ts +0 -171
  215. package/dist/serverQueryTypes-BFdwq0Z2.d.ts.map +0 -1
  216. package/dist/serverQueryTypes-DNdMpmDr.d.cts +0 -171
  217. package/dist/serverQueryTypes-DNdMpmDr.d.cts.map +0 -1
  218. package/dist/serverRequestTypes-BNA7FCY6.d.ts +0 -200
  219. package/dist/serverRequestTypes-BNA7FCY6.d.ts.map +0 -1
  220. package/dist/serverRequestTypes-CxDRFKfv.d.cts +0 -200
  221. package/dist/serverRequestTypes-CxDRFKfv.d.cts.map +0 -1
  222. package/dist/types-BCr2G5gS.d.cts +0 -84
  223. package/dist/types-BCr2G5gS.d.cts.map +0 -1
  224. package/dist/types-C18k-9Vy.d.ts +0 -84
  225. package/dist/types-C18k-9Vy.d.ts.map +0 -1
@@ -0,0 +1,848 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import {
3
+ mapMobileSocialFeedActionCandidate,
4
+ mapMobileSocialFeedCommentsPage,
5
+ mapMobileSocialFeedDrawingMutationResult,
6
+ mapMobileSocialFeedDrawingResult,
7
+ mapMobileSocialFeedItem,
8
+ mapMobileSocialFeedItemPatch,
9
+ mapMobileSocialFeedItemResponse,
10
+ mapMobileSocialFeedMentionToServer,
11
+ mapMobileSocialFeedPage,
12
+ mapMobileSocialFeedPositionAction,
13
+ mapMobileSocialFeedPositionChart,
14
+ mapMobileSocialFeedPositionHistory,
15
+ mapMobileSocialFeedPositionPatch,
16
+ mapMobileSocialFeedPresence,
17
+ mapMobileSocialFeedTradeCard,
18
+ } from './dataMappers';
19
+ import { MobileUnsupportedFeedItemError } from './types/MobileUnsupportedFeedItemError';
20
+ import {
21
+ MobileServerSocialFeedItem,
22
+ MobileServerSocialFeedPost,
23
+ MobileServerSocialFeedReply,
24
+ } from './types/serverModelTypes';
25
+ import { MobileServerSocialFeedPositionHistoryResponse } from './types/serverQueryTypes';
26
+
27
+ const post: MobileServerSocialFeedPost = {
28
+ id: '88888888-8888-4888-8888-888888888888',
29
+ kind: 'post',
30
+ has_analysis: true,
31
+ author: {
32
+ subaccount:
33
+ '0x1111111111111111111111111111111111111111000000000000000000000000',
34
+ username: 'alice',
35
+ display_name: 'Alice',
36
+ avatar_url: null,
37
+ is_following: false,
38
+ trader_badge: null,
39
+ trader_data_status: 'unavailable',
40
+ },
41
+ created_at_ms: 1791072000000,
42
+ display_at_ms: 1791072000000,
43
+ version: '1',
44
+ market: {
45
+ product_id: 2,
46
+ symbol: 'BTC-PERP',
47
+ logo_url: null,
48
+ category: 'crypto',
49
+ source_categories: ['crypto'],
50
+ },
51
+ capabilities: {
52
+ can_like: true,
53
+ can_comment: true,
54
+ can_record_view: true,
55
+ },
56
+ images: [],
57
+ text: 'BTC 回踩后继续持有多仓。',
58
+ mentions: [],
59
+ position: {
60
+ position_id: '22222222-2222-4222-8222-222222222222',
61
+ product_id: 2,
62
+ market: {
63
+ product_id: 2,
64
+ symbol: 'BTC-PERP',
65
+ logo_url: null,
66
+ category: 'crypto',
67
+ source_categories: ['crypto'],
68
+ },
69
+ display_basis: 'current_position',
70
+ direction: 'long',
71
+ margin_mode: 'cross',
72
+ state: 'open',
73
+ entry_price_x18: '100000000000000000000000',
74
+ quantity_x18: '100000000000000000',
75
+ notional_x18: '10200000000000000000000',
76
+ leverage: {
77
+ value: null,
78
+ basis: 'not_applicable',
79
+ },
80
+ leverage_at_ms: null,
81
+ pnl: {
82
+ amount_x18: '200000000000000000000',
83
+ basis: 'unrealized_excl_fees_funding',
84
+ mode: 'live',
85
+ as_of_ms: 1791072060000,
86
+ quality: 'ready',
87
+ },
88
+ snapshot_at_ms: null,
89
+ state_as_of_ms: 1791072060000,
90
+ position_version: '3',
91
+ history_quality: 'complete',
92
+ },
93
+ interaction: {
94
+ like_count: 0,
95
+ comment_count: 0,
96
+ view_count: 0,
97
+ liked_by_me: false,
98
+ },
99
+ comment_preview: {
100
+ items: [],
101
+ total_count: 0,
102
+ next_cursor: null,
103
+ },
104
+ drawing: {
105
+ data: {
106
+ example: 'frontend-defined drawing document',
107
+ },
108
+ created_at_ms: 1791072000000,
109
+ updated_at_ms: 1791072000000,
110
+ },
111
+ };
112
+ const reply: MobileServerSocialFeedReply = {
113
+ id: '33333333-3333-4333-8333-333333333333',
114
+ kind: 'reply',
115
+ has_analysis: false,
116
+ author: {
117
+ subaccount:
118
+ '0x2222222222222222222222222222222222222222000000000000000000000000',
119
+ username: 'bob',
120
+ display_name: 'Bob',
121
+ avatar_url: null,
122
+ is_following: false,
123
+ trader_badge: null,
124
+ trader_data_status: 'unavailable',
125
+ },
126
+ created_at_ms: 1791072120000,
127
+ display_at_ms: 1791072120000,
128
+ version: '1',
129
+ market: {
130
+ product_id: 2,
131
+ symbol: 'BTC-PERP',
132
+ logo_url: null,
133
+ category: 'crypto',
134
+ source_categories: ['crypto'],
135
+ },
136
+ capabilities: {
137
+ can_like: false,
138
+ can_comment: true,
139
+ can_record_view: false,
140
+ },
141
+ images: [],
142
+ text: '我会继续观察支撑位。',
143
+ mentions: [],
144
+ root_item_id: '11111111-1111-4111-8111-111111111111',
145
+ reply_to_author: null,
146
+ attached_position: null,
147
+ attachment_status: 'none',
148
+ original: {
149
+ id: '11111111-1111-4111-8111-111111111111',
150
+ author: {
151
+ subaccount:
152
+ '0x1111111111111111111111111111111111111111000000000000000000000000',
153
+ username: 'alice',
154
+ display_name: 'Alice',
155
+ avatar_url: null,
156
+ is_following: false,
157
+ trader_badge: null,
158
+ trader_data_status: 'unavailable',
159
+ },
160
+ text_excerpt: 'BTC 回踩后继续持有多仓。',
161
+ position: {
162
+ position_id: '22222222-2222-4222-8222-222222222222',
163
+ market: {
164
+ product_id: 2,
165
+ symbol: 'BTC-PERP',
166
+ logo_url: null,
167
+ category: 'crypto',
168
+ source_categories: ['crypto'],
169
+ },
170
+ direction: 'long',
171
+ state: 'open',
172
+ notional_x18: '10200000000000000000000',
173
+ },
174
+ has_analysis: false,
175
+ },
176
+ interaction: {
177
+ like_count: 0,
178
+ comment_count: 0,
179
+ view_count: 0,
180
+ liked_by_me: false,
181
+ },
182
+ drawing: null,
183
+ };
184
+ const history: MobileServerSocialFeedPositionHistoryResponse = {
185
+ position: {
186
+ position_id: '22222222-2222-4222-8222-222222222222',
187
+ product_id: 2,
188
+ market: {
189
+ product_id: 2,
190
+ symbol: 'BTC-PERP',
191
+ logo_url: null,
192
+ category: 'crypto',
193
+ source_categories: ['crypto'],
194
+ },
195
+ display_basis: 'current_position',
196
+ direction: 'long',
197
+ margin_mode: 'cross',
198
+ state: 'open',
199
+ entry_price_x18: '100000000000000000000000',
200
+ quantity_x18: '100000000000000000',
201
+ notional_x18: '10200000000000000000000',
202
+ leverage: {
203
+ value: null,
204
+ basis: 'not_applicable',
205
+ },
206
+ leverage_at_ms: null,
207
+ pnl: {
208
+ amount_x18: '200000000000000000000',
209
+ basis: 'unrealized_excl_fees_funding',
210
+ mode: 'live',
211
+ as_of_ms: 1791072060000,
212
+ quality: 'ready',
213
+ },
214
+ snapshot_at_ms: null,
215
+ state_as_of_ms: 1791072060000,
216
+ position_version: '3',
217
+ history_quality: 'complete',
218
+ estimated_liquidation_price_x18: null,
219
+ funding_pnl_x18: '0',
220
+ liquidation_price_quality: 'unavailable',
221
+ liquidation_price_as_of_ms: null,
222
+ funding_quality: 'ready',
223
+ funding_as_of_ms: 1791072060000,
224
+ },
225
+ summary: {
226
+ action_count: 1,
227
+ take_count: 1,
228
+ },
229
+ timeline: [
230
+ {
231
+ kind: 'action',
232
+ action: {
233
+ id: '44444444-4444-4444-8444-444444444444',
234
+ position_id: '22222222-2222-4222-8222-222222222222',
235
+ action: 'opened',
236
+ direction: 'long',
237
+ margin_mode: 'cross',
238
+ quantity_before_x18: '0',
239
+ quantity_after_x18: '100000000000000000',
240
+ executed_quantity_x18: '100000000000000000',
241
+ executed_notional_x18: '10000000000000000000000',
242
+ average_price_x18: '100000000000000000000000',
243
+ entry_price_before_x18: null,
244
+ realized_pnl_delta_x18: '0',
245
+ leverage: {
246
+ value: null,
247
+ basis: 'not_applicable',
248
+ },
249
+ leverage_at_ms: null,
250
+ first_event_at_ms: 1791072000000,
251
+ last_event_at_ms: 1791072000000,
252
+ version: '1',
253
+ liquidation_oracle_price_x18: null,
254
+ liquidation_notional_x18: null,
255
+ },
256
+ related_item_id: '11111111-1111-4111-8111-111111111111',
257
+ related_take: {
258
+ id: '11111111-1111-4111-8111-111111111111',
259
+ kind: 'trade_card',
260
+ has_analysis: false,
261
+ author: {
262
+ subaccount:
263
+ '0x1111111111111111111111111111111111111111000000000000000000000000',
264
+ username: 'alice',
265
+ display_name: 'Alice',
266
+ avatar_url: null,
267
+ is_following: false,
268
+ trader_badge: null,
269
+ trader_data_status: 'unavailable',
270
+ },
271
+ created_at_ms: 1791072000000,
272
+ display_at_ms: 1791072000000,
273
+ version: '1',
274
+ market: {
275
+ product_id: 2,
276
+ symbol: 'BTC-PERP',
277
+ logo_url: null,
278
+ category: 'crypto',
279
+ source_categories: ['crypto'],
280
+ },
281
+ capabilities: {
282
+ can_like: true,
283
+ can_comment: true,
284
+ can_record_view: true,
285
+ },
286
+ images: [],
287
+ text: '持有这一仓位,关注后续价格变化。',
288
+ mentions: [],
289
+ position: {
290
+ position_id: '22222222-2222-4222-8222-222222222222',
291
+ product_id: 2,
292
+ market: {
293
+ product_id: 2,
294
+ symbol: 'BTC-PERP',
295
+ logo_url: null,
296
+ category: 'crypto',
297
+ source_categories: ['crypto'],
298
+ },
299
+ display_basis: 'current_position',
300
+ direction: 'long',
301
+ margin_mode: 'cross',
302
+ state: 'open',
303
+ entry_price_x18: '100000000000000000000000',
304
+ quantity_x18: '100000000000000000',
305
+ notional_x18: '10200000000000000000000',
306
+ leverage: {
307
+ value: null,
308
+ basis: 'not_applicable',
309
+ },
310
+ leverage_at_ms: null,
311
+ pnl: {
312
+ amount_x18: '200000000000000000000',
313
+ basis: 'unrealized_excl_fees_funding',
314
+ mode: 'live',
315
+ as_of_ms: 1791072060000,
316
+ quality: 'ready',
317
+ },
318
+ snapshot_at_ms: null,
319
+ state_as_of_ms: 1791072060000,
320
+ position_version: '3',
321
+ history_quality: 'complete',
322
+ },
323
+ interaction: {
324
+ like_count: 12,
325
+ comment_count: 0,
326
+ view_count: 80,
327
+ liked_by_me: false,
328
+ },
329
+ comment_preview: {
330
+ items: [],
331
+ total_count: 0,
332
+ next_cursor: null,
333
+ },
334
+ drawing: null,
335
+ action: 'opened',
336
+ },
337
+ },
338
+ ],
339
+ next_cursor: null,
340
+ history_quality: 'complete',
341
+ trading_context: {
342
+ product_id: 2,
343
+ suggested_direction: 'long',
344
+ viewer_position_id: '22222222-2222-4222-8222-222222222222',
345
+ can_reduce: true,
346
+ resolution: 'unique',
347
+ candidates: [
348
+ {
349
+ position_id: '22222222-2222-4222-8222-222222222222',
350
+ identity_subaccount:
351
+ '0x1111111111111111111111111111111111111111000000000000000000000000',
352
+ trading_subaccount:
353
+ '0x1111111111111111111111111111111111111111000000000000000000000000',
354
+ product_id: 2,
355
+ margin_mode: 'cross',
356
+ iso_subaccount_name: null,
357
+ direction: 'long',
358
+ state_as_of_ms: 1791072060000,
359
+ },
360
+ ],
361
+ },
362
+ read_session: {
363
+ session_id: '66666666-6666-4666-8666-666666666666',
364
+ mode: 'position',
365
+ expires_at_ms: 1791072900000,
366
+ update_cursor: null,
367
+ viewer_token: 'viewer_example_opaque',
368
+ view_tracking: {
369
+ mode: 'detail_visit',
370
+ },
371
+ },
372
+ };
373
+
374
+ describe('[mobile-client]: content mapping', () => {
375
+ it('rejects unsupported feed item kinds with the typed error', () => {
376
+ const server = {
377
+ kind: 'future_kind',
378
+ } as unknown as MobileServerSocialFeedItem;
379
+ expect(() => mapMobileSocialFeedItem(server)).toThrow(
380
+ MobileUnsupportedFeedItemError,
381
+ );
382
+ expect(() => mapMobileSocialFeedItem(server)).toThrow('future_kind');
383
+ });
384
+
385
+ it.each([
386
+ { label: 'timeline entry', entry: { kind: 'future_kind' } },
387
+ {
388
+ label: 'history take',
389
+ entry: { kind: 'take', item: { kind: 'future_kind' } },
390
+ },
391
+ ])(
392
+ 'rejects unsupported $label kinds before accessing their payload',
393
+ ({ entry }) => {
394
+ const server = {
395
+ ...history,
396
+ timeline: [entry],
397
+ } as unknown as MobileServerSocialFeedPositionHistoryResponse;
398
+ expect(() => mapMobileSocialFeedPositionHistory(server)).toThrow(
399
+ MobileUnsupportedFeedItemError,
400
+ );
401
+ expect(() => mapMobileSocialFeedPositionHistory(server)).toThrow(
402
+ 'future_kind',
403
+ );
404
+ },
405
+ );
406
+
407
+ it('maps shared post and trade card fields consistently, including history takes', () => {
408
+ const serverCard = {
409
+ ...post,
410
+ kind: 'trade_card',
411
+ action: 'opened',
412
+ } as const;
413
+ const mappedPost = mapMobileSocialFeedItem(post);
414
+ const mappedCard = mapMobileSocialFeedTradeCard(serverCard);
415
+ expect(mappedCard).toEqual({
416
+ ...mappedPost,
417
+ kind: 'trade_card',
418
+ action: 'opened',
419
+ });
420
+ const mappedHistory = mapMobileSocialFeedPositionHistory({
421
+ ...history,
422
+ timeline: [
423
+ { kind: 'take', item: post },
424
+ { kind: 'take', item: serverCard },
425
+ ],
426
+ });
427
+ expect(mappedHistory.timeline).toEqual([
428
+ { kind: 'take', item: mappedPost },
429
+ { kind: 'take', item: mappedCard },
430
+ ]);
431
+ });
432
+
433
+ it.each([
434
+ 'position_closed',
435
+ 'already_published',
436
+ 'unavailable',
437
+ null,
438
+ ] as const)('preserves action publication reason %s', (reason) => {
439
+ const candidate = mapMobileSocialFeedActionCandidate({
440
+ action_id: 'action',
441
+ position_id: post.position.position_id,
442
+ action: 'opened',
443
+ can_publish: reason === null,
444
+ unavailable_reason: reason,
445
+ });
446
+ expect(candidate.unavailableReason).toBe(reason);
447
+ expect(candidate.canPublish).toBe(reason === null);
448
+ });
449
+
450
+ it('maps all supported item kinds without dropping items or page metadata', () => {
451
+ const items: MobileServerSocialFeedItem[] = [
452
+ {
453
+ ...post,
454
+ id: '11111111-1111-4111-8111-111111111111',
455
+ kind: 'trade_card',
456
+ action: 'opened',
457
+ },
458
+ {
459
+ ...post,
460
+ id: '44444444-4444-4444-8444-444444444444',
461
+ kind: 'trade_row',
462
+ action: 'increased',
463
+ position_id: post.position.position_id,
464
+ direction: 'long',
465
+ margin_mode: 'cross',
466
+ leverage: { value: null, basis: 'not_applicable' },
467
+ leverage_at_ms: null,
468
+ trade_count: 2,
469
+ quantity_x18: '100000000000000000',
470
+ notional_x18: '10200000000000000000000',
471
+ average_price_x18: '102000000000000000000000',
472
+ },
473
+ post,
474
+ reply,
475
+ ];
476
+ const page = mapMobileSocialFeedPage({
477
+ items,
478
+ read_session: { ...history.read_session, mode: 'feed' },
479
+ next_cursor: 'NextCursor',
480
+ watermark: 'Watermark',
481
+ });
482
+
483
+ expect(page.items.map(({ id, kind }) => ({ id, kind }))).toEqual(
484
+ items.map(({ id, kind }) => ({ id, kind })),
485
+ );
486
+ expect(page.items[0]).toMatchObject({
487
+ kind: 'trade_card',
488
+ action: 'opened',
489
+ });
490
+ expect(page.items[1]).toMatchObject({ kind: 'trade_row', tradeCount: 2 });
491
+ expect(page.items[2]).toMatchObject({ kind: 'post', text: post.text });
492
+ expect(page.items[3]).toMatchObject({
493
+ kind: 'reply',
494
+ rootItemId: reply.root_item_id,
495
+ });
496
+ expect(page.nextCursor).toBe('NextCursor');
497
+ expect(page.watermark).toBe('Watermark');
498
+ expect(page.readSession).toMatchObject({
499
+ mode: 'feed',
500
+ sessionId: history.read_session.session_id,
501
+ viewerToken: history.read_session.viewer_token,
502
+ });
503
+ });
504
+
505
+ it('maps empty image arrays in feeds, item details, and comments', () => {
506
+ const serverReply = { ...reply };
507
+ const serverPost = {
508
+ ...post,
509
+ comment_preview: {
510
+ ...post.comment_preview,
511
+ items: [serverReply],
512
+ },
513
+ };
514
+ const serverCard = {
515
+ ...serverPost,
516
+ kind: 'trade_card',
517
+ action: 'opened',
518
+ } as const;
519
+ const items: MobileServerSocialFeedItem[] = [
520
+ serverPost,
521
+ serverCard,
522
+ serverReply,
523
+ ];
524
+ const page = mapMobileSocialFeedPage({
525
+ items,
526
+ next_cursor: null,
527
+ watermark: 'Watermark',
528
+ read_session: history.read_session,
529
+ });
530
+ expect(page.items).toMatchObject([
531
+ {
532
+ kind: 'post',
533
+ images: [],
534
+ commentPreview: { items: [{ images: [] }] },
535
+ },
536
+ {
537
+ kind: 'trade_card',
538
+ images: [],
539
+ commentPreview: { items: [{ images: [] }] },
540
+ },
541
+ { kind: 'reply', images: [] },
542
+ ]);
543
+ for (const item of items) {
544
+ expect(
545
+ mapMobileSocialFeedItemResponse({
546
+ item,
547
+ read_session: history.read_session,
548
+ }).item,
549
+ ).toMatchObject({ kind: item.kind, images: [] });
550
+ }
551
+ const comments = mapMobileSocialFeedCommentsPage({
552
+ comments: [serverReply],
553
+ total_count: 1,
554
+ next_cursor: null,
555
+ previous_cursor: null,
556
+ anchor_reply_id: null,
557
+ read_session: history.read_session,
558
+ });
559
+ expect(comments.comments).toMatchObject([{ kind: 'reply', images: [] }]);
560
+ });
561
+
562
+ it('maps feed images and detail-visit read sessions', () => {
563
+ const mapped = mapMobileSocialFeedItem({
564
+ ...post,
565
+ images: [
566
+ {
567
+ image_id: '88888888-8888-4888-8888-888888888888',
568
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/standard',
569
+ width: 1024,
570
+ height: 256,
571
+ content_type: 'image/png',
572
+ byte_size: 123456,
573
+ thumbnail: {
574
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/thumbnail',
575
+ width: 512,
576
+ height: 128,
577
+ },
578
+ },
579
+ ],
580
+ });
581
+ expect(mapped.kind).toBe('post');
582
+ if (mapped.kind !== 'post') throw new Error('Expected post');
583
+ expect(mapped.images).toEqual([
584
+ {
585
+ imageId: '88888888-8888-4888-8888-888888888888',
586
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/standard',
587
+ width: 1024,
588
+ height: 256,
589
+ contentType: 'image/png',
590
+ byteSize: 123456,
591
+ thumbnail: {
592
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/thumbnail',
593
+ width: 512,
594
+ height: 128,
595
+ },
596
+ },
597
+ ]);
598
+ expect(
599
+ mapMobileSocialFeedPage({
600
+ items: [],
601
+ next_cursor: null,
602
+ watermark: 'w',
603
+ read_session: history.read_session,
604
+ }).readSession.viewTracking,
605
+ ).toEqual({ mode: 'detail_visit' });
606
+ });
607
+
608
+ it('maps posts, drawings, reply previews, and nullable badges from the documented response', () => {
609
+ const mapped = mapMobileSocialFeedItem({
610
+ ...post,
611
+ comment_preview: { items: [reply], total_count: 1, next_cursor: null },
612
+ });
613
+ expect(mapped.kind).toBe('post');
614
+ if (mapped.kind !== 'post') throw new Error('Expected post');
615
+ expect(mapped.text).toBe(post.text);
616
+ expect(mapped.drawing?.data).toEqual(post.drawing?.data);
617
+ expect(mapped.author.traderBadge).toBeNull();
618
+ expect(mapped.position.quantity.toFixed()).toBe(post.position.quantity_x18);
619
+ expect(mapped.position.notional.toFixed()).toBe(post.position.notional_x18);
620
+ expect(mapped.commentPreview.items[0].kind).toBe('reply');
621
+ expect(mapped.commentPreview.items[0].rootItemId).toBe(reply.root_item_id);
622
+ expect(
623
+ mapped.commentPreview.items[0].original.author.traderBadge,
624
+ ).toBeNull();
625
+ expect(mapped.commentPreview.items[0].attachedPosition).toBeNull();
626
+ expect(mapped.commentPreview.items[0].attachmentStatus).toBe('none');
627
+ expect(mapped.commentPreview.items[0]).not.toHaveProperty('replyToItemId');
628
+ });
629
+
630
+ it.each([false, 0, { lines: [] }])(
631
+ 'preserves valid drawing data %p rather than treating it as missing',
632
+ (data) => {
633
+ const result = mapMobileSocialFeedDrawingResult({
634
+ item_id: post.id,
635
+ item_version: '9',
636
+ drawing: { data, created_at_ms: 10, updated_at_ms: 20 },
637
+ });
638
+ expect(result.drawing).toEqual({
639
+ data,
640
+ createdAtMs: 10,
641
+ updatedAtMs: 20,
642
+ });
643
+ },
644
+ );
645
+
646
+ it.each([false, 0, null, { lines: [] }])(
647
+ 'maps saved drawing mutations with non-null drawings and data %p',
648
+ (data) => {
649
+ const result = mapMobileSocialFeedDrawingMutationResult({
650
+ item_id: post.id,
651
+ item_version: '9',
652
+ drawing: { data, created_at_ms: 10, updated_at_ms: 20 },
653
+ });
654
+ expect(result.itemId).toBe(post.id);
655
+ expect(result.itemVersion).toBe('9');
656
+ expect(result.drawing.data).toEqual(data);
657
+ expect(result.drawing).toEqual({
658
+ data,
659
+ createdAtMs: 10,
660
+ updatedAtMs: 20,
661
+ });
662
+ },
663
+ );
664
+
665
+ it('distinguishes missing drawings and explicit attachment clearing from omitted patches', () => {
666
+ expect(
667
+ mapMobileSocialFeedDrawingResult({
668
+ item_id: post.id,
669
+ item_version: '1',
670
+ drawing: null,
671
+ }).drawing,
672
+ ).toBeNull();
673
+ const patch = {
674
+ item_id: reply.id,
675
+ version: '2',
676
+ author: reply.author,
677
+ display_at_ms: 22,
678
+ };
679
+ expect(mapMobileSocialFeedItemPatch(patch)).not.toHaveProperty(
680
+ 'attachedPosition',
681
+ );
682
+ expect(
683
+ mapMobileSocialFeedItemPatch({
684
+ ...patch,
685
+ attached_position: null,
686
+ attachment_status: 'unavailable',
687
+ }),
688
+ ).toMatchObject({
689
+ attachedPosition: null,
690
+ attachmentStatus: 'unavailable',
691
+ });
692
+ const attached = mapMobileSocialFeedItemPatch({
693
+ ...patch,
694
+ attached_position: post.position,
695
+ attachment_status: 'visible',
696
+ });
697
+ expect(attached.attachedPosition?.positionId).toBe(
698
+ post.position.position_id,
699
+ );
700
+ expect(attached.attachedPosition?.quantity.toFixed()).toBe(
701
+ post.position.quantity_x18,
702
+ );
703
+ });
704
+
705
+ it('maps a position session, related takes, history metrics, and trading targets', () => {
706
+ const mapped = mapMobileSocialFeedPositionHistory(history);
707
+ expect(mapped.readSession.mode).toBe('position');
708
+ expect(mapped.readSession.viewerToken).toBe(
709
+ history.read_session.viewer_token,
710
+ );
711
+ expect(mapped.summary).toEqual({
712
+ actionCount: history.summary.action_count,
713
+ takeCount: history.summary.take_count,
714
+ });
715
+ expect(mapped.tradingContext.viewerPositionId).toBe(
716
+ history.trading_context.viewer_position_id,
717
+ );
718
+ expect(mapped.timeline[0].kind).toBe('action');
719
+ expect(mapped.position.fundingPnl?.toFixed() ?? null).toBe(
720
+ history.position.funding_pnl_x18,
721
+ );
722
+ const patch = mapMobileSocialFeedPositionPatch({
723
+ ...post.position,
724
+ current_position: {
725
+ estimated_liquidation_price_x18: '2500123456789012345678',
726
+ liquidation_price_quality: 'ready',
727
+ liquidation_price_as_of_ms: 42,
728
+ funding_pnl_x18: '-9007199254740993000001',
729
+ funding_quality: 'ready',
730
+ funding_as_of_ms: 42,
731
+ },
732
+ });
733
+ expect(patch.currentPosition?.estimatedLiquidationPrice?.toFixed()).toBe(
734
+ '2500.123456789012345678',
735
+ );
736
+ expect(patch.currentPosition?.fundingPnl?.toFixed()).toBe(
737
+ '-9007199254740993000001',
738
+ );
739
+ expect(mapMobileSocialFeedPositionPatch(post.position)).not.toHaveProperty(
740
+ 'currentPosition',
741
+ );
742
+ });
743
+
744
+ it('keeps execution, Oracle, and realized PnL independently nullable and precise', () => {
745
+ const entry = history.timeline[0];
746
+ if (entry.kind !== 'action') throw new Error('Expected history action');
747
+ const mapped = mapMobileSocialFeedPositionAction({
748
+ ...entry.action,
749
+ action: 'liquidated',
750
+ executed_quantity_x18: '1234567890123456789012345',
751
+ executed_notional_x18: null,
752
+ average_price_x18: '2500123456789012345678',
753
+ entry_price_before_x18: null,
754
+ liquidation_oracle_price_x18: null,
755
+ liquidation_notional_x18: null,
756
+ realized_pnl_delta_x18: '-9007199254740993000001',
757
+ });
758
+ expect(mapped.executedQuantity.toFixed()).toBe('1234567890123456789012345');
759
+ expect(mapped.executedNotional).toBeNull();
760
+ expect(mapped.averagePrice?.toFixed()).toBe('2500.123456789012345678');
761
+ expect(mapped.entryPriceBefore).toBeNull();
762
+ expect(mapped.liquidationOraclePrice).toBeNull();
763
+ expect(mapped.liquidationNotional).toBeNull();
764
+ expect(mapped.realizedPnlDelta?.toFixed()).toBe('-9007199254740993000001');
765
+ });
766
+
767
+ it('converts candle prices but retains volume in x18 and markers with unknown prices', () => {
768
+ const mapped = mapMobileSocialFeedPositionChart({
769
+ candles: [
770
+ {
771
+ timestamp_ms: 42,
772
+ open_x18: '1000000000000000000',
773
+ high_x18: '2000000000000000000',
774
+ low_x18: '500000000000000000',
775
+ close_x18: '1500000000000000000',
776
+ volume_x18: '1234567890123456789012345',
777
+ },
778
+ ],
779
+ action_markers: [
780
+ {
781
+ action_id: post.id,
782
+ timestamp_ms: 42,
783
+ price_x18: null,
784
+ action: 'liquidated',
785
+ direction: 'long',
786
+ price_basis: 'liquidation_oracle',
787
+ },
788
+ ],
789
+ estimated_liquidation_line: null,
790
+ next_before_ms: null,
791
+ as_of_ms: 42,
792
+ data_quality: 'partial',
793
+ });
794
+ expect(mapped.candles[0].open.toFixed()).toBe('1');
795
+ expect(mapped.candles[0].high.toFixed()).toBe('2');
796
+ expect(mapped.candles[0].low.toFixed()).toBe('0.5');
797
+ expect(mapped.candles[0].close.toFixed()).toBe('1.5');
798
+ expect(mapped.candles[0].volume.toFixed()).toBe(
799
+ '1234567890123456789012345',
800
+ );
801
+ expect(mapped.actionMarkers[0].price).toBeNull();
802
+ expect(mapped.actionMarkers[0].priceBasis).toBe('liquidation_oracle');
803
+ expect(mapped.estimatedLiquidationLine).toBeNull();
804
+ });
805
+
806
+ it('maps comment cursors and anchor without inventing an observation timestamp', () => {
807
+ const result = mapMobileSocialFeedCommentsPage({
808
+ comments: [reply],
809
+ next_cursor: 'NextOpaque',
810
+ previous_cursor: 'PreviousOpaque',
811
+ total_count: 8,
812
+ anchor_reply_id: reply.id,
813
+ read_session: history.read_session,
814
+ });
815
+ expect(result.nextCursor).toBe('NextOpaque');
816
+ expect(result.previousCursor).toBe('PreviousOpaque');
817
+ expect(result.anchorReplyId).toBe(reply.id);
818
+ expect(result).not.toHaveProperty('asOfMs');
819
+ });
820
+
821
+ it('maps presence lease metadata without assuming an as_of_ms field', () => {
822
+ const result = mapMobileSocialFeedPresence({
823
+ watcher_count: 1,
824
+ watcher_preview: [reply.author],
825
+ heartbeat_interval_seconds: 15,
826
+ expires_in_seconds: 45,
827
+ });
828
+ expect(result.watcherCount).toBe(1);
829
+ expect(result.watcherPreview[0].traderBadge).toBeNull();
830
+ expect(result.heartbeatIntervalSeconds).toBe(15);
831
+ expect(result.expiresInSeconds).toBe(45);
832
+ expect(result).not.toHaveProperty('asOfMs');
833
+ });
834
+
835
+ it('keeps mention labels and code-point offsets intact', () => {
836
+ const result = mapMobileSocialFeedMentionToServer({
837
+ subaccount: {
838
+ subaccountOwner: '0x1111111111111111111111111111111111111111',
839
+ subaccountName: 'default',
840
+ },
841
+ start: 2,
842
+ end: 8,
843
+ label: '@Alice',
844
+ });
845
+ expect(result).toMatchObject({ start: 2, end: 8, label: '@Alice' });
846
+ expect(result.subaccount).toMatch(/^0x[0-9a-f]{64}$/);
847
+ });
848
+ });