@nadohq/mobile-client 0.35.0 → 0.37.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 (189) hide show
  1. package/README.md +19 -6
  2. package/dist/{MobileClient-D5TNTnLx.cjs → MobileClient-C9S_o_eC.cjs} +82 -12
  3. package/dist/MobileClient-C9S_o_eC.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +64 -16
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +64 -16
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +82 -12
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-CPFjwUBx.d.cts → MobileServerFailureError-CVK3aLOi.d.cts} +3 -3
  12. package/dist/{MobileServerFailureError-CPFjwUBx.d.cts.map → MobileServerFailureError-CVK3aLOi.d.cts.map} +1 -1
  13. package/dist/{MobileServerFailureError-Terv2Wzy.d.ts → MobileServerFailureError-D-76n3pS.d.ts} +3 -3
  14. package/dist/{MobileServerFailureError-Terv2Wzy.d.ts.map → MobileServerFailureError-D-76n3pS.d.ts.map} +1 -1
  15. package/dist/{buildSignedMobileRequest-BScnCGPF.d.ts → buildSignedMobileRequest-42meR4yV.d.ts} +2 -2
  16. package/dist/{buildSignedMobileRequest-BScnCGPF.d.ts.map → buildSignedMobileRequest-42meR4yV.d.ts.map} +1 -1
  17. package/dist/{buildSignedMobileRequest-CXfnhc-4.d.cts → buildSignedMobileRequest-BDxji7Vy.d.cts} +2 -2
  18. package/dist/{buildSignedMobileRequest-CXfnhc-4.d.cts.map → buildSignedMobileRequest-BDxji7Vy.d.cts.map} +1 -1
  19. package/dist/{canonicalize-CQ-pMYIt.d.ts → canonicalize-BebHhHcF.d.ts} +2 -2
  20. package/dist/canonicalize-BebHhHcF.d.ts.map +1 -0
  21. package/dist/{canonicalize-hzhboB-C.d.cts → canonicalize-CIDJLLgg.d.cts} +2 -2
  22. package/dist/canonicalize-CIDJLLgg.d.cts.map +1 -0
  23. package/dist/{clientTypes-BwakovWk.d.cts → clientTypes-2I3x4TTt.d.ts} +142 -14
  24. package/dist/clientTypes-2I3x4TTt.d.ts.map +1 -0
  25. package/dist/{clientTypes-D-o1_2Yc.d.ts → clientTypes-tLld8E6J.d.cts} +142 -14
  26. package/dist/clientTypes-tLld8E6J.d.cts.map +1 -0
  27. package/dist/dataMappers.cjs +65 -6
  28. package/dist/dataMappers.cjs.map +1 -1
  29. package/dist/dataMappers.d.cts +21 -5
  30. package/dist/dataMappers.d.cts.map +1 -1
  31. package/dist/dataMappers.d.ts +21 -5
  32. package/dist/dataMappers.d.ts.map +1 -1
  33. package/dist/dataMappers.js +62 -7
  34. package/dist/dataMappers.js.map +1 -1
  35. package/dist/{eip712MethodByType-3aiJkrHj.d.cts → eip712MethodByType-2-vyN5SU.d.cts} +2 -2
  36. package/dist/eip712MethodByType-2-vyN5SU.d.cts.map +1 -0
  37. package/dist/{eip712MethodByType-zNvZukU7.d.ts → eip712MethodByType-CAVpvZoS.d.ts} +2 -2
  38. package/dist/eip712MethodByType-CAVpvZoS.d.ts.map +1 -0
  39. package/dist/follow-Bh3UmXMc.d.cts +23 -0
  40. package/dist/follow-Bh3UmXMc.d.cts.map +1 -0
  41. package/dist/follow-DjkQm1vR.d.ts +23 -0
  42. package/dist/follow-DjkQm1vR.d.ts.map +1 -0
  43. package/dist/index.cjs +6 -1
  44. package/dist/index.d.cts +17 -16
  45. package/dist/index.d.ts +17 -16
  46. package/dist/index.js +2 -1
  47. package/dist/{mobileErrorCodes-CuQ5lyBV.d.ts → mobileErrorCodes-B3eR6t4b.d.ts} +8 -2
  48. package/dist/mobileErrorCodes-B3eR6t4b.d.ts.map +1 -0
  49. package/dist/{mobileErrorCodes-D_SyBm0k.d.cts → mobileErrorCodes-DkzBW4v3.d.cts} +8 -2
  50. package/dist/mobileErrorCodes-DkzBW4v3.d.cts.map +1 -0
  51. package/dist/{patterns-B5gi7OfV.d.ts → patterns-C-aUAnKl.d.ts} +1 -1
  52. package/dist/patterns-C-aUAnKl.d.ts.map +1 -0
  53. package/dist/{patterns-Db6fgz8i.d.cts → patterns-DS8P5LH0.d.cts} +1 -1
  54. package/dist/patterns-DS8P5LH0.d.cts.map +1 -0
  55. package/dist/{payloadHash-DVkEt-Rg.d.cts → payloadHash-BVPYKdk4.d.cts} +2 -2
  56. package/dist/payloadHash-BVPYKdk4.d.cts.map +1 -0
  57. package/dist/{payloadHash-BxL26uwq.d.ts → payloadHash-DMqE5aMo.d.ts} +2 -2
  58. package/dist/payloadHash-DMqE5aMo.d.ts.map +1 -0
  59. package/dist/{serverBaseTypes-C8KjE9Fm.d.cts → serverBaseTypes-BybrVE6L.d.cts} +2 -2
  60. package/dist/{serverBaseTypes-C8KjE9Fm.d.cts.map → serverBaseTypes-BybrVE6L.d.cts.map} +1 -1
  61. package/dist/{serverBaseTypes-B_KHFDab.d.ts → serverBaseTypes-DPB1vEvv.d.ts} +2 -2
  62. package/dist/{serverBaseTypes-B_KHFDab.d.ts.map → serverBaseTypes-DPB1vEvv.d.ts.map} +1 -1
  63. package/dist/serverExecuteTypes-DoR0InCd.d.cts +50 -0
  64. package/dist/serverExecuteTypes-DoR0InCd.d.cts.map +1 -0
  65. package/dist/serverExecuteTypes-ih4Xbr3J.d.ts +50 -0
  66. package/dist/serverExecuteTypes-ih4Xbr3J.d.ts.map +1 -0
  67. package/dist/{serverModelTypes-_cX99seo.d.ts → serverModelTypes-DV-eARP8.d.ts} +54 -10
  68. package/dist/serverModelTypes-DV-eARP8.d.ts.map +1 -0
  69. package/dist/{serverModelTypes-CwgibK-l.d.cts → serverModelTypes-KSm0CAcF.d.cts} +54 -10
  70. package/dist/serverModelTypes-KSm0CAcF.d.cts.map +1 -0
  71. package/dist/{serverQueryTypes-NIrPm2Sl.d.ts → serverQueryTypes-B9OOpiJB.d.cts} +32 -9
  72. package/dist/serverQueryTypes-B9OOpiJB.d.cts.map +1 -0
  73. package/dist/{serverQueryTypes-DwE1iJ9f.d.cts → serverQueryTypes-LygPrZn7.d.ts} +32 -9
  74. package/dist/serverQueryTypes-LygPrZn7.d.ts.map +1 -0
  75. package/dist/serverRequestTypes-BDXIPjtw.d.ts +101 -0
  76. package/dist/serverRequestTypes-BDXIPjtw.d.ts.map +1 -0
  77. package/dist/serverRequestTypes-q4xTJHk0.d.cts +101 -0
  78. package/dist/serverRequestTypes-q4xTJHk0.d.cts.map +1 -0
  79. package/dist/{serverResponseGuards-Cep5s7j5.d.ts → serverResponseGuards-BcdkKV30.d.ts} +2 -2
  80. package/dist/{serverResponseGuards-Cep5s7j5.d.ts.map → serverResponseGuards-BcdkKV30.d.ts.map} +1 -1
  81. package/dist/{serverResponseGuards-Dxt9_aGh.d.cts → serverResponseGuards-k6xK1bZ1.d.cts} +2 -2
  82. package/dist/{serverResponseGuards-Dxt9_aGh.d.cts.map → serverResponseGuards-k6xK1bZ1.d.cts.map} +1 -1
  83. package/dist/signing/buildSignedMobileRequest.cjs +1 -1
  84. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  85. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  86. package/dist/signing/canonicalize.cjs +17 -0
  87. package/dist/signing/canonicalize.cjs.map +1 -1
  88. package/dist/signing/canonicalize.d.cts +1 -1
  89. package/dist/signing/canonicalize.d.ts +1 -1
  90. package/dist/signing/canonicalize.js +17 -0
  91. package/dist/signing/canonicalize.js.map +1 -1
  92. package/dist/signing/eip712MethodByType.cjs +4 -1
  93. package/dist/signing/eip712MethodByType.cjs.map +1 -1
  94. package/dist/signing/eip712MethodByType.d.cts +1 -1
  95. package/dist/signing/eip712MethodByType.d.ts +1 -1
  96. package/dist/signing/eip712MethodByType.js +4 -1
  97. package/dist/signing/eip712MethodByType.js.map +1 -1
  98. package/dist/signing/index.d.cts +6 -6
  99. package/dist/signing/index.d.ts +6 -6
  100. package/dist/signing/payloadHash.cjs +1 -1
  101. package/dist/signing/payloadHash.d.cts +1 -1
  102. package/dist/signing/payloadHash.d.ts +1 -1
  103. package/dist/signing/types.d.cts +2 -2
  104. package/dist/signing/types.d.ts +2 -2
  105. package/dist/types/MobileServerFailureError.cjs +1 -1
  106. package/dist/types/MobileServerFailureError.d.cts +1 -1
  107. package/dist/types/MobileServerFailureError.d.ts +1 -1
  108. package/dist/types/clientTypes.d.cts +2 -2
  109. package/dist/types/clientTypes.d.ts +2 -2
  110. package/dist/types/index.d.cts +9 -9
  111. package/dist/types/index.d.ts +9 -9
  112. package/dist/types/mobileErrorCodes.cjs +13 -1
  113. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  114. package/dist/types/mobileErrorCodes.d.cts +1 -1
  115. package/dist/types/mobileErrorCodes.d.ts +1 -1
  116. package/dist/types/mobileErrorCodes.js +12 -0
  117. package/dist/types/mobileErrorCodes.js.map +1 -1
  118. package/dist/types/serverBaseTypes.d.cts +1 -1
  119. package/dist/types/serverBaseTypes.d.ts +1 -1
  120. package/dist/types/serverExecuteTypes.d.cts +2 -2
  121. package/dist/types/serverExecuteTypes.d.ts +2 -2
  122. package/dist/types/serverModelTypes.cjs.map +1 -1
  123. package/dist/types/serverModelTypes.d.cts +2 -2
  124. package/dist/types/serverModelTypes.d.ts +2 -2
  125. package/dist/types/serverModelTypes.js.map +1 -1
  126. package/dist/types/serverQueryTypes.d.cts +2 -2
  127. package/dist/types/serverQueryTypes.d.ts +2 -2
  128. package/dist/types/serverRequestTypes.d.cts +2 -2
  129. package/dist/types/serverRequestTypes.d.ts +2 -2
  130. package/dist/{types-47IPsDyA.d.cts → types-B8lDIhBP.d.ts} +30 -7
  131. package/dist/types-B8lDIhBP.d.ts.map +1 -0
  132. package/dist/{types-D5kFJzEX.d.ts → types-CClB00EN.d.cts} +30 -7
  133. package/dist/types-CClB00EN.d.cts.map +1 -0
  134. package/dist/utils/follow.cjs +28 -0
  135. package/dist/utils/follow.cjs.map +1 -0
  136. package/dist/utils/follow.d.cts +2 -0
  137. package/dist/utils/follow.d.ts +2 -0
  138. package/dist/utils/follow.js +24 -0
  139. package/dist/utils/follow.js.map +1 -0
  140. package/dist/utils/index.cjs +5 -0
  141. package/dist/utils/index.d.cts +4 -3
  142. package/dist/utils/index.d.ts +4 -3
  143. package/dist/utils/index.js +2 -1
  144. package/dist/utils/patterns.d.cts +1 -1
  145. package/dist/utils/patterns.d.ts +1 -1
  146. package/dist/utils/serverResponseGuards.d.cts +1 -1
  147. package/dist/utils/serverResponseGuards.d.ts +1 -1
  148. package/package.json +3 -3
  149. package/src/MobileClient.ts +138 -21
  150. package/src/dataMappers.ts +89 -5
  151. package/src/signing/canonicalize.ts +20 -0
  152. package/src/signing/eip712MethodByType.ts +3 -0
  153. package/src/signing/signing.test.ts +58 -4
  154. package/src/signing/types.ts +34 -1
  155. package/src/types/clientTypes.ts +148 -11
  156. package/src/types/mobileErrorCodes.ts +12 -0
  157. package/src/types/serverExecuteTypes.ts +40 -2
  158. package/src/types/serverModelTypes.ts +55 -8
  159. package/src/types/serverQueryTypes.ts +35 -4
  160. package/src/types/serverRequestTypes.ts +41 -6
  161. package/src/utils/follow.ts +22 -0
  162. package/src/utils/index.ts +1 -0
  163. package/dist/MobileClient-D5TNTnLx.cjs.map +0 -1
  164. package/dist/canonicalize-CQ-pMYIt.d.ts.map +0 -1
  165. package/dist/canonicalize-hzhboB-C.d.cts.map +0 -1
  166. package/dist/clientTypes-BwakovWk.d.cts.map +0 -1
  167. package/dist/clientTypes-D-o1_2Yc.d.ts.map +0 -1
  168. package/dist/eip712MethodByType-3aiJkrHj.d.cts.map +0 -1
  169. package/dist/eip712MethodByType-zNvZukU7.d.ts.map +0 -1
  170. package/dist/mobileErrorCodes-CuQ5lyBV.d.ts.map +0 -1
  171. package/dist/mobileErrorCodes-D_SyBm0k.d.cts.map +0 -1
  172. package/dist/patterns-B5gi7OfV.d.ts.map +0 -1
  173. package/dist/patterns-Db6fgz8i.d.cts.map +0 -1
  174. package/dist/payloadHash-BxL26uwq.d.ts.map +0 -1
  175. package/dist/payloadHash-DVkEt-Rg.d.cts.map +0 -1
  176. package/dist/serverExecuteTypes-Cb5v7y0A.d.cts +0 -19
  177. package/dist/serverExecuteTypes-Cb5v7y0A.d.cts.map +0 -1
  178. package/dist/serverExecuteTypes-GFjZj_DC.d.ts +0 -19
  179. package/dist/serverExecuteTypes-GFjZj_DC.d.ts.map +0 -1
  180. package/dist/serverModelTypes-CwgibK-l.d.cts.map +0 -1
  181. package/dist/serverModelTypes-_cX99seo.d.ts.map +0 -1
  182. package/dist/serverQueryTypes-DwE1iJ9f.d.cts.map +0 -1
  183. package/dist/serverQueryTypes-NIrPm2Sl.d.ts.map +0 -1
  184. package/dist/serverRequestTypes-516zXnQ6.d.ts +0 -72
  185. package/dist/serverRequestTypes-516zXnQ6.d.ts.map +0 -1
  186. package/dist/serverRequestTypes-GAtVYIda.d.cts +0 -72
  187. package/dist/serverRequestTypes-GAtVYIda.d.cts.map +0 -1
  188. package/dist/types-47IPsDyA.d.cts.map +0 -1
  189. package/dist/types-D5kFJzEX.d.ts.map +0 -1
@@ -19,8 +19,8 @@ const FIXED_PRIVATE_KEY =
19
19
  '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907' as const;
20
20
  const FIXED_VERIFYING_ADDR = '0x0000000000000000000000000000000000000001';
21
21
 
22
- // `set_username`, `set_private_mode`, and `register_expo_token` mirror the backend's pinned fixtures in
23
- // mobile/src/api/types.rs, so a divergence here is a real wire-format break rather than a stale expectation.
22
+ // Every hash here mirrors the backend's pinned fixtures in mobile/src/api/types.rs, so a divergence is a
23
+ // real wire-format break rather than a stale expectation.
24
24
  const PINNED_HASHES = {
25
25
  set_username:
26
26
  '0xb4a55d9eb0be4e5c9457761c0c34c75cf39f7c07bd170700d8dd5e9c7de4a659',
@@ -28,8 +28,20 @@ const PINNED_HASHES = {
28
28
  '0x4d12a06234d751e6ddcc01d8f70836bb5b7e207e573641d1efb5aaf2b0f30d10',
29
29
  register_expo_token:
30
30
  '0x1b9471afc9bde66f9bffd576d3326420e1bade4e16afa3a73ddc65f27155611c',
31
+ set_follow_true:
32
+ '0x1ff8529b7c1a0111313eed98536e591555a3bcab51133776ce30c6ef32b559b3',
33
+ set_follow_false:
34
+ '0xec9e9e8f32ac4fcf3015f8b7f20c338623ff85db0488703c7fb688519a46e2e5',
35
+ followers:
36
+ '0x44f3cd2c991196cf949a25cebe0e16a4e5e8a14846d35751f3931de3c8ffcabd',
37
+ following:
38
+ '0xaddabad62abf1919dcd4250eee20359970cbfb59f4c81536551462f8918e5ddd',
31
39
  } as const;
32
40
 
41
+ // Every follow fixture on the backend targets this subaccount.
42
+ const FIXTURE_TARGET_SUBACCOUNT =
43
+ '0x1111111111111111111111111111111111111111111111111111111111111111' as const;
44
+
33
45
  describe('[mobile-client]: signing (offline)', () => {
34
46
  describe('pinned payload hashes', () => {
35
47
  it('set_username', () => {
@@ -61,6 +73,48 @@ describe('[mobile-client]: signing (offline)', () => {
61
73
  const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
62
74
  expect(hash).toBe(PINNED_HASHES.register_expo_token);
63
75
  });
76
+
77
+ it('set_follow (follow)', () => {
78
+ const inner: MobileSignedInner = {
79
+ type: 'set_follow',
80
+ subaccount: FIXTURE_TARGET_SUBACCOUNT,
81
+ is_following: true,
82
+ };
83
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
84
+ expect(hash).toBe(PINNED_HASHES.set_follow_true);
85
+ });
86
+
87
+ it('set_follow (unfollow)', () => {
88
+ const inner: MobileSignedInner = {
89
+ type: 'set_follow',
90
+ subaccount: FIXTURE_TARGET_SUBACCOUNT,
91
+ is_following: false,
92
+ };
93
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
94
+ expect(hash).toBe(PINNED_HASHES.set_follow_false);
95
+ });
96
+
97
+ it('followers', () => {
98
+ const inner: MobileSignedInner = {
99
+ type: 'followers',
100
+ subaccount: FIXTURE_TARGET_SUBACCOUNT,
101
+ cursor: null,
102
+ limit: 50,
103
+ };
104
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
105
+ expect(hash).toBe(PINNED_HASHES.followers);
106
+ });
107
+
108
+ it('following', () => {
109
+ const inner: MobileSignedInner = {
110
+ type: 'following',
111
+ subaccount: FIXTURE_TARGET_SUBACCOUNT,
112
+ cursor: null,
113
+ limit: 50,
114
+ };
115
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
116
+ expect(hash).toBe(PINNED_HASHES.following);
117
+ });
64
118
  });
65
119
 
66
120
  it('canonicalizes msgpack key order regardless of input key order', () => {
@@ -76,8 +130,8 @@ describe('[mobile-client]: signing (offline)', () => {
76
130
  });
77
131
 
78
132
  it('canonicalizes multi-field key order regardless of input key order', () => {
79
- // register_expo_token is the only signed payload with more than one field, so it is where a caller can
80
- // actually get the order wrong; canonicalizeMobileInner must rebuild it in the backend's declaration order.
133
+ // register_expo_token has the most fields of any signed payload, so it is where a caller is most likely
134
+ // to get the order wrong; canonicalizeMobileInner must rebuild it in the backend's declaration order.
81
135
  const outOfOrderInner = {
82
136
  app_version: '1.2.3',
83
137
  locale: 'en-GB',
@@ -11,6 +11,11 @@ import { MobileNotificationPlatform } from '../types/serverModelTypes';
11
11
  * declares its structs — `type` first, then the remaining fields — or the hash won't match and signature
12
12
  * verification fails. This type only describes the shape; the authoritative runtime key order is enforced by
13
13
  * {@link canonicalizeMobileInner}, which is the source of truth. Keep the field order here in sync with it.
14
+ *
15
+ * Every bytes32 field must be lowercase hex. The backend decodes them case-insensitively but hashes its own
16
+ * re-serialization, which is always lowercase, so a mixed-case value is accepted on the wire and then fails
17
+ * authentication against a hash the client never reproduced. Build them with `subaccountToHex`, which
18
+ * lowercases; unlike bytes32, string fields such as `display_name` are hashed as-is and keep their casing.
14
19
  */
15
20
  export type MobileSignedInner =
16
21
  | { type: 'set_username'; display_name: string }
@@ -23,14 +28,42 @@ export type MobileSignedInner =
23
28
  // backend, which serializes `Option::None` as nil under the same key.
24
29
  locale: string | null;
25
30
  app_version: string | null;
31
+ }
32
+ | {
33
+ type: 'set_follow';
34
+ subaccount: Hex;
35
+ /** `true` follows the target, `false` unfollows it. Both directions are idempotent. */
36
+ is_following: boolean;
37
+ }
38
+ | {
39
+ type: 'followers';
40
+ subaccount: Hex;
41
+ /** `null` requests the first page; otherwise an opaque cursor from the previous page. */
42
+ cursor: string | null;
43
+ /** `null` requests the backend default of 25; otherwise 1–50. */
44
+ limit: number | null;
45
+ }
46
+ | {
47
+ type: 'following';
48
+ subaccount: Hex;
49
+ /** `null` requests the first page; otherwise an opaque cursor from the previous page. */
50
+ cursor: string | null;
51
+ /** `null` requests the backend default of 25; otherwise 1–50. */
52
+ limit: number | null;
26
53
  };
27
54
 
55
+ /**
56
+ * The single {@link MobileSignedInner} member carrying the given `type` tag.
57
+ */
58
+ export type MobileSignedInnerByType<T extends MobileSignedInner['type']> =
59
+ Extract<MobileSignedInner, { type: T }>;
60
+
28
61
  /**
29
62
  * Type-specific payload fields for a signed inner request of a given `type` (the inner payload without its
30
63
  * `type` tag). Derived from {@link MobileSignedInner} so the two never drift apart.
31
64
  */
32
65
  export type MobileSignedInnerParams<T extends MobileSignedInner['type']> = Omit<
33
- Extract<MobileSignedInner, { type: T }>,
66
+ MobileSignedInnerByType<T>,
34
67
  'type'
35
68
  >;
36
69
 
@@ -9,7 +9,11 @@ import {
9
9
 
10
10
  /**
11
11
  * A subaccount's public profile, as returned by an unsigned profile lookup. Private Mode hides the account's
12
- * activity, not its profile, so `privateMode` is exposed here too.
12
+ * activity, not its profile, so `privateMode` is exposed here too. A subaccount with no identity row still
13
+ * yields a profile, with `null` names and `privateMode: false`.
14
+ *
15
+ * The last three fields are `undefined` unless the matching {@link MobileProfilesInclude} asked for them —
16
+ * absent means "not requested" rather than "none".
13
17
  */
14
18
  export interface MobilePublicProfile {
15
19
  subaccount: Hex;
@@ -18,6 +22,80 @@ export interface MobilePublicProfile {
18
22
  /** User-facing name as claimed, or `null` if no username has been claimed. */
19
23
  displayName: string | null;
20
24
  privateMode: boolean;
25
+ /**
26
+ * Exact count at query time rather than a cached counter, including unnamed and private accounts — Private
27
+ * Mode hides activity, not relationships. Requires the `followCounts` include.
28
+ */
29
+ followerCount?: number;
30
+ followingCount?: number;
31
+ /** Requires the `followSummary` include, and is relative to that include's `viewAs`. */
32
+ followSummary?: MobileFollowSummary;
33
+ }
34
+
35
+ /**
36
+ * The identity fields the backend attaches to a subaccount wherever it surfaces one alongside other data.
37
+ * Resolved at read time, so a rename shows up on the next read without any change to the surrounding record.
38
+ */
39
+ export interface MobileIdentitySummary {
40
+ subaccount: Hex;
41
+ /** `null` when the account has not claimed a username; render a local fallback label from `subaccount`. */
42
+ username: string | null;
43
+ /** `null` when the account has not claimed a username; render a local fallback label from `subaccount`. */
44
+ displayName: string | null;
45
+ /** Reserved for a future avatar source; `null` until one is implemented. */
46
+ avatarUrl: string | null;
47
+ }
48
+
49
+ /**
50
+ * A viewer's relationship with one Profile, plus the Followed By preview: accounts the viewer follows that
51
+ * also follow that Profile. Every preview entry is therefore already familiar to the viewer, which is why it
52
+ * carries no `isFollowing` of its own.
53
+ */
54
+ export interface MobileFollowSummary {
55
+ /** The direct `viewer -> viewed Profile` relationship. */
56
+ isFollowing: boolean;
57
+ /** Exact size of the two-edge intersection, independent of how many previews came back. */
58
+ followedByCount: number;
59
+ /**
60
+ * Newest `preview -> viewed Profile` relationship first, with a bytes32 tie-break. Capped at
61
+ * `MOBILE_FOLLOWED_BY_PREVIEW_LIMIT` (2) by the backend and not client-configurable.
62
+ */
63
+ followedBy: MobileIdentitySummary[];
64
+ }
65
+
66
+ /**
67
+ * One row of a Followers or Following page.
68
+ */
69
+ export interface MobileFollowListAccount {
70
+ identity: MobileIdentitySummary;
71
+ /**
72
+ * Whether the *Viewer* follows this account — not the listed relationship that put it in the page. The
73
+ * Viewer can appear in another Profile's list, and does not follow themself, so their own row is `false`.
74
+ */
75
+ isFollowing: boolean;
76
+ /** This account's own follower count. */
77
+ followerCount: number;
78
+ }
79
+
80
+ /**
81
+ * A page of Followers or Following. Pagination is live rather than snapshot: a Follow or Unfollow between
82
+ * pages can move an account, so an account can appear twice or not at all across one session. Deduplicate by
83
+ * {@link MobileIdentitySummary.subaccount} and restart from `cursor: undefined` on refresh.
84
+ */
85
+ export interface MobileFollowListPage {
86
+ accounts: MobileFollowListAccount[];
87
+ /** `null` means the list is complete. */
88
+ nextCursor: string | null;
89
+ }
90
+
91
+ /**
92
+ * Post-commit state of a follow or unfollow. The mutation and its count share one database transaction, so
93
+ * these values are authoritative — apply them rather than re-reading.
94
+ */
95
+ export interface MobileFollowMutationResult {
96
+ isFollowing: boolean;
97
+ /** The *target's* follower count after the mutation, not the sender's. */
98
+ followerCount: number;
21
99
  }
22
100
 
23
101
  /**
@@ -43,16 +121,9 @@ export interface MobileFeedMargin {
43
121
  * (a rename can change an already-fetched trade on the next read). Amounts are display-oriented human-unit
44
122
  * numbers — do not use them for accounting, order construction, or exact threshold decisions.
45
123
  */
46
- export interface MobileFeedTrade {
124
+ export interface MobileFeedTrade extends MobileIdentitySummary {
47
125
  /** Order digest this trade is keyed by; the stable id for reconciling live pagination. */
48
126
  orderDigest: Hex;
49
- subaccount: Hex;
50
- /** `null` when the trader has not claimed a username; render a local fallback label from `subaccount`. */
51
- username: string | null;
52
- /** `null` when the trader has not claimed a username; render a local fallback label from `subaccount`. */
53
- displayName: string | null;
54
- /** Reserved for a future avatar source; `null` until one is implemented. */
55
- avatarUrl: string | null;
56
127
  productId: number;
57
128
  /** Executed quantity in human units. */
58
129
  quantity: number;
@@ -155,9 +226,31 @@ export interface GetMobileUsernameAvailabilityParams {
155
226
  }
156
227
 
157
228
  /**
158
- * Params for {@link MobileClient.getPublicProfile}.
229
+ * Opt-in extras for a profiles lookup. Each one adds fields that are otherwise absent from every returned
230
+ * profile, and costs extra rate-limit weight per profile, so ask only for what the UI shows.
159
231
  */
160
- export type GetMobilePublicProfileParams = Subaccount;
232
+ export interface MobileProfilesInclude {
233
+ /** Populates `followerCount` and `followingCount`. */
234
+ followCounts?: boolean;
235
+ /**
236
+ * Populates `followSummary`, relative to `viewAs`. Unlike the signed follow list reads, this viewer
237
+ * identity is an unauthenticated claim — the route takes no signature — so do not treat the result as
238
+ * proof of who is asking.
239
+ */
240
+ followSummary?: { viewAs: Subaccount };
241
+ }
242
+
243
+ /**
244
+ * Params for {@link MobileClient.getProfiles}.
245
+ */
246
+ export interface GetMobileProfilesParams {
247
+ /**
248
+ * 1–`MOBILE_PROFILES_MAX_BATCH_SIZE` (25) distinct subaccounts. Duplicates and an empty list are rejected
249
+ * rather than deduplicated, and results come back in this exact order.
250
+ */
251
+ subaccounts: Subaccount[];
252
+ include?: MobileProfilesInclude;
253
+ }
161
254
 
162
255
  /**
163
256
  * Params for {@link MobileClient.getFeed}.
@@ -192,6 +285,50 @@ export interface MobileSetPrivateModeParams extends MobileSignedRequestParams {
192
285
  privateMode: boolean;
193
286
  }
194
287
 
288
+ /**
289
+ * Common params for the signed follow requests. The subaccount carried by {@link MobileSignedRequestParams}
290
+ * is the signing Viewer (the Follower, for mutations); `target` is the other party.
291
+ */
292
+ export interface MobileFollowRequestParams extends MobileSignedRequestParams {
293
+ /** The Profile being followed, unfollowed, or read. Must not be the signing subaccount. */
294
+ target: Subaccount;
295
+ }
296
+
297
+ /**
298
+ * Params for {@link MobileClient.setFollow}.
299
+ */
300
+ export interface MobileSetFollowParams extends MobileFollowRequestParams {
301
+ /** `true` follows the target, `false` unfollows it. Both directions are idempotent. */
302
+ isFollowing: boolean;
303
+ }
304
+
305
+ /**
306
+ * Common params for a Followers or Following page.
307
+ */
308
+ export interface GetMobileFollowListParams extends MobileFollowRequestParams {
309
+ /**
310
+ * Opaque cursor from the previous page's {@link MobileFollowListPage.nextCursor}. Omit for the first page.
311
+ * A cursor is bound to its Viewer, Profile, and list direction — reusing it elsewhere fails with
312
+ * `INVALID_FOLLOW_CURSOR`.
313
+ */
314
+ cursor?: string;
315
+ /**
316
+ * Page size, 1–`MOBILE_FOLLOW_LIST_MAX_PAGE_SIZE` (50); the backend defaults to
317
+ * `MOBILE_FOLLOW_LIST_DEFAULT_PAGE_SIZE` (25) when omitted.
318
+ */
319
+ limit?: number;
320
+ }
321
+
322
+ /**
323
+ * Params for {@link MobileClient.getFollowers}.
324
+ */
325
+ export type GetMobileFollowersParams = GetMobileFollowListParams;
326
+
327
+ /**
328
+ * Params for {@link MobileClient.getFollowing}.
329
+ */
330
+ export type GetMobileFollowingParams = GetMobileFollowListParams;
331
+
195
332
  /**
196
333
  * Params for {@link MobileClient.registerExpoToken}.
197
334
  */
@@ -13,6 +13,18 @@ export const MOBILE_ERROR_CODES = {
13
13
  /** Another identity write for the subaccount committed with a later nonce. Re-read before retrying. */
14
14
  STALE_IDENTITY_UPDATE: 6105,
15
15
  PROFILE_NOT_FOUND: 6106,
16
+ /** Profiles lookup is empty, holds duplicates, or exceeds the batch maximum. Do not retry unchanged. */
17
+ INVALID_PROFILES_REQUEST: 6107,
18
+ /** Follow mutation names the same source and target, or an isolated participant. Do not retry unchanged. */
19
+ INVALID_FOLLOW_TARGET: 6110,
20
+ /** Signed follower is not yet in the canonical Query DB subaccount index. Retry after index catch-up. */
21
+ FOLLOWER_NOT_ELIGIBLE: 6111,
22
+ /** Follow target is not yet in the canonical Query DB subaccount index. Refresh after a new record. */
23
+ FOLLOWING_NOT_FOUND: 6112,
24
+ /** Follow list cursor is malformed or bound to a different list request. Restart from `cursor: null`. */
25
+ INVALID_FOLLOW_CURSOR: 6113,
26
+ /** A follow list or followed-by limit is outside its accepted range. Fix it; do not retry unchanged. */
27
+ INVALID_FOLLOW_LIMIT: 6114,
16
28
  INVALID_EXPO_TOKEN: 6200,
17
29
  INVALID_DEVICE_METADATA: 6201,
18
30
  INVALID_PREFERENCES: 6202,
@@ -3,6 +3,7 @@ import {
3
3
  MobileServerSuccessResponse,
4
4
  } from './serverBaseTypes';
5
5
  import {
6
+ MobileServerExecuteRequestType,
6
7
  MobileServerPublicExecuteRequestByType,
7
8
  MobileServerPublicExecuteRequestType,
8
9
  } from './serverRequestTypes';
@@ -21,8 +22,45 @@ export type MobileServerPublicExecuteRequest<
21
22
  }[T];
22
23
 
23
24
  /**
24
- * Discriminated `execute` result union. Mobile execute routes return no payload beyond the success
25
- * discriminant, so the success arm is the base success envelope; failures use the uniform failure envelope.
25
+ * Payload the `set_follow` execute returns alongside the success envelope. The mutation and the count commit
26
+ * in one database transaction, so these are the authoritative post-mutation values — prefer them over
27
+ * re-reading, and note that overlapping toggles for the same pair can respond out of arrival order.
28
+ */
29
+ export interface MobileServerFollowMutationResponse {
30
+ is_following: boolean;
31
+ /** The *target's* follower count after the mutation, not the sender's. */
32
+ follower_count: number;
33
+ }
34
+
35
+ /**
36
+ * Payload of an execute route that returns nothing beyond the success discriminant. Spelled `Record<never,
37
+ * never>` rather than `Record<string, never>` because the latter's index signature would collapse `status` to
38
+ * `never` when intersected with the success envelope.
39
+ */
40
+ export type MobileServerEmptyExecuteResponse = Record<never, never>;
41
+
42
+ /**
43
+ * Success payloads for each signed `execute`, keyed by request `type`. Most execute routes carry no payload
44
+ * beyond the success discriminant and so map to an empty object; `set_follow` returns its post-commit
45
+ * relationship state.
46
+ */
47
+ export interface MobileServerExecuteResponseByType {
48
+ set_username: MobileServerEmptyExecuteResponse;
49
+ set_private_mode: MobileServerEmptyExecuteResponse;
50
+ register_expo_token: MobileServerEmptyExecuteResponse;
51
+ set_follow: MobileServerFollowMutationResponse;
52
+ }
53
+
54
+ /**
55
+ * Full success response for a signed `execute`: the {@link MobileServerSuccessResponse} envelope with the
56
+ * route's payload inlined alongside `status`, matching how the query routes inline theirs.
57
+ */
58
+ export type MobileServerExecuteSuccessResponse<
59
+ T extends MobileServerExecuteRequestType = MobileServerExecuteRequestType,
60
+ > = MobileServerSuccessResponse & MobileServerExecuteResponseByType[T];
61
+
62
+ /**
63
+ * Discriminated `execute` result union used at the transport boundary, before the envelope is narrowed.
26
64
  */
27
65
  export type MobileServerExecuteResult =
28
66
  | MobileServerSuccessResponse
@@ -18,14 +18,66 @@ export type MobileNotificationCategory =
18
18
  | 'announcement';
19
19
 
20
20
  /**
21
- * Server-side public profile shape (snake_case). Name fields are `null` until the subaccount claims a
22
- * username.
21
+ * Server-side identity summary (snake_case): the read-time identity fields the backend attaches to a
22
+ * subaccount wherever it surfaces one alongside other data. Name fields are `null` until the subaccount
23
+ * claims a username, and a rename shows up on the next read without any change to the surrounding record.
24
+ */
25
+ export interface MobileServerIdentitySummary {
26
+ subaccount: Hex;
27
+ username: string | null;
28
+ display_name: string | null;
29
+ /** Reserved for a future avatar source; `null` until one is implemented. */
30
+ avatar_url: string | null;
31
+ }
32
+
33
+ /**
34
+ * Server-side follow summary (snake_case), nested in a `profiles` entry when the `follow_summary` include is
35
+ * requested. `is_following` is the direct `view_as -> this profile` relationship; every `followed_by` entry
36
+ * satisfies both `view_as -> entry` and `entry -> this profile`, so each one is already familiar to the
37
+ * viewer and carries no `is_following` of its own.
38
+ */
39
+ export interface MobileServerFollowSummary {
40
+ is_following: boolean;
41
+ /** Exact size of the two-edge intersection, independent of how many previews came back. */
42
+ followed_by_count: number;
43
+ /** Capped at `MOBILE_FOLLOWED_BY_PREVIEW_LIMIT` (2) by the backend; not client-configurable. */
44
+ followed_by: MobileServerIdentitySummary[];
45
+ }
46
+
47
+ /**
48
+ * Server-side public profile shape (snake_case), shared by the singular `profile` and batched `profiles`
49
+ * queries. Name fields are `null` until the subaccount claims a username; a subaccount with no identity row
50
+ * still yields an entry, with `null` names and `private_mode: false`.
51
+ *
52
+ * The three optional fields are omitted from the wire entirely unless the matching `profiles` include asked
53
+ * for them, which is why they are optional rather than nullable — absent means "not requested", not "none".
54
+ * The singular `profile` query never returns any of them.
23
55
  */
24
56
  export interface MobileServerProfile {
25
57
  subaccount: Hex;
26
58
  username: string | null;
27
59
  display_name: string | null;
28
60
  private_mode: boolean;
61
+ /**
62
+ * Exact count at query time — the backend counts rather than reading a cached counter. Unnamed and private
63
+ * accounts are included: Private Mode hides activity, not relationships. Requires `follow_counts`.
64
+ */
65
+ follower_count?: number;
66
+ following_count?: number;
67
+ /** Requires the `follow_summary` include, and is relative to that include's `view_as`. */
68
+ follow_summary?: MobileServerFollowSummary;
69
+ }
70
+
71
+ /**
72
+ * Server-side row of a Followers or Following page (snake_case). `is_following` describes the *Viewer's*
73
+ * relationship to this account, not the listed relationship that put it in the page, so it is `false` on the
74
+ * Viewer's own row.
75
+ */
76
+ export interface MobileServerFollowListAccount {
77
+ identity: MobileServerIdentitySummary;
78
+ is_following: boolean;
79
+ /** The listed account's own follower count, exact at query time. */
80
+ follower_count: number;
29
81
  }
30
82
 
31
83
  /**
@@ -97,14 +149,9 @@ export interface MobileServerFeedMargin {
97
149
  * trader's current identity. Amounts are display-oriented human-unit JSON numbers — do not use them for
98
150
  * accounting, order construction, or exact threshold decisions.
99
151
  */
100
- export interface MobileServerFeedTrade {
152
+ export interface MobileServerFeedTrade extends MobileServerIdentitySummary {
101
153
  /** Order digest this row is keyed by; the stable id clients use to reconcile live pagination. */
102
154
  order_digest: Hex;
103
- subaccount: Hex;
104
- username: string | null;
105
- display_name: string | null;
106
- /** Reserved for a future avatar source; `null` until one is implemented. */
107
- avatar_url: string | null;
108
155
  product_id: number;
109
156
  /** Executed quantity in human units. */
110
157
  quantity: number;
@@ -3,12 +3,14 @@ import { MobileServerSuccessResponse } from './serverBaseTypes';
3
3
  import {
4
4
  MobileNotificationPlatform,
5
5
  MobileServerFeedTrade,
6
+ MobileServerFollowListAccount,
6
7
  MobileServerNotificationPreferences,
7
8
  MobileServerProfile,
8
9
  } from './serverModelTypes';
9
10
  import {
10
11
  MobileServerPublicQueryRequestByType,
11
12
  MobileServerPublicQueryRequestType,
13
+ MobileServerSignedQueryRequestType,
12
14
  } from './serverRequestTypes';
13
15
 
14
16
  /**
@@ -32,10 +34,11 @@ export interface MobileServerUsernameAvailabilityResponse {
32
34
  }
33
35
 
34
36
  /**
35
- * Payload of the `profile` public query success response.
37
+ * Payload of the `profiles` public query success response. One entry per requested subaccount, in the exact
38
+ * order they were requested, so callers can correlate positionally as well as by `subaccount`.
36
39
  */
37
- export interface MobileServerProfileResponse {
38
- profile: MobileServerProfile;
40
+ export interface MobileServerProfilesResponse {
41
+ profiles: MobileServerProfile[];
39
42
  }
40
43
 
41
44
  /**
@@ -79,7 +82,7 @@ export interface MobileServerRegisteredWalletResponse {
79
82
  */
80
83
  export interface MobileServerPublicQueryResponseByType {
81
84
  username_availability: MobileServerUsernameAvailabilityResponse;
82
- profile: MobileServerProfileResponse;
85
+ profiles: MobileServerProfilesResponse;
83
86
  feed: MobileServerFeedResponse;
84
87
  notification_preferences: MobileServerNotificationPreferencesResponse;
85
88
  registered_wallet: MobileServerRegisteredWalletResponse;
@@ -94,3 +97,31 @@ export type MobileServerPublicQuerySuccessResponse<
94
97
  T extends MobileServerPublicQueryRequestType =
95
98
  MobileServerPublicQueryRequestType,
96
99
  > = MobileServerSuccessResponse & MobileServerPublicQueryResponseByType[T];
100
+
101
+ /**
102
+ * Payload of the `followers` and `following` signed query success responses. Both list directions share one
103
+ * shape; only the relationship that selected the rows differs.
104
+ */
105
+ export interface MobileServerFollowListResponse {
106
+ accounts: MobileServerFollowListAccount[];
107
+ /** `null` means the list is complete. */
108
+ next_cursor: string | null;
109
+ }
110
+
111
+ /**
112
+ * Success payloads for each signed `query`, keyed by request `type` — the signed counterpart to
113
+ * {@link MobileServerPublicQueryResponseByType}.
114
+ */
115
+ export interface MobileServerSignedQueryResponseByType {
116
+ followers: MobileServerFollowListResponse;
117
+ following: MobileServerFollowListResponse;
118
+ }
119
+
120
+ /**
121
+ * Full success response for a signed `query`: the {@link MobileServerSuccessResponse} envelope with the
122
+ * query's payload inlined alongside `status`.
123
+ */
124
+ export type MobileServerSignedQuerySuccessResponse<
125
+ T extends MobileServerSignedQueryRequestType =
126
+ MobileServerSignedQueryRequestType,
127
+ > = MobileServerSuccessResponse & MobileServerSignedQueryResponseByType[T];
@@ -2,12 +2,34 @@ import { Hex } from 'viem';
2
2
  import { MobileSignedInner } from '../signing/types';
3
3
  import { MobileServerNotificationPreferences } from './serverModelTypes';
4
4
 
5
+ /**
6
+ * Opt-in extras for a `profiles` query. Each one costs extra rate-limit weight per profile and adds fields
7
+ * that are otherwise absent from the response, so the backend gates them behind an explicit request rather
8
+ * than always resolving the follow graph on a public route.
9
+ */
10
+ export interface MobileServerProfilesInclude {
11
+ /** Adds `follower_count` and `following_count` to every entry. Omitted or `false` leaves both absent. */
12
+ follow_counts?: boolean;
13
+ /**
14
+ * Adds `follow_summary` to every entry, relative to the `view_as` subaccount. Unlike the signed follow
15
+ * list reads, `view_as` is an unauthenticated claim on this public route.
16
+ */
17
+ follow_summary?: { view_as: Hex };
18
+ }
19
+
5
20
  /**
6
21
  * Params for each unsigned `public_query`, keyed by request `type`.
7
22
  */
8
23
  export interface MobileServerPublicQueryRequestByType {
9
24
  username_availability: { display_name: string };
10
- profile: { subaccount: Hex };
25
+ profiles: {
26
+ /**
27
+ * 1–`MOBILE_PROFILES_MAX_BATCH_SIZE` (25) distinct subaccounts. Results come back in this exact order,
28
+ * one entry per request slot.
29
+ */
30
+ subaccounts: Hex[];
31
+ include?: MobileServerProfilesInclude;
32
+ };
11
33
  feed: {
12
34
  /**
13
35
  * Minimum notional as a whole-dollar JSON integer (NOT an x18 string), at least $1,000. Omitted or
@@ -52,18 +74,31 @@ export interface MobileServerPublicExecuteRequestByType {
52
74
  export type MobileServerPublicExecuteRequestType =
53
75
  keyof MobileServerPublicExecuteRequestByType;
54
76
 
77
+ /**
78
+ * Discriminant `type` values for signed `query` requests: the read-only subset of {@link MobileSignedInner}
79
+ * tags. Unlike the public queries, these carry no separate params map — a signed request body *is* its inner
80
+ * payload flattened with the envelope, so {@link MobileSignedInner} is the single source of truth for both
81
+ * the fields and their significant order.
82
+ */
83
+ export type MobileServerSignedQueryRequestType = 'followers' | 'following';
84
+
55
85
  /**
56
86
  * Discriminant `type` values for signed `execute` requests: the write subset of {@link MobileSignedInner}
57
- * tags. With no signed queries remaining, every signed inner type is an execute.
87
+ * tags, i.e. everything that is not a signed query.
58
88
  */
59
- export type MobileServerExecuteRequestType = MobileSignedInner['type'];
89
+ export type MobileServerExecuteRequestType = Exclude<
90
+ MobileSignedInner['type'],
91
+ MobileServerSignedQueryRequestType
92
+ >;
60
93
 
61
94
  /**
62
95
  * Wire `request_type` the backend echoes on failure envelopes, prefixed by the route that produced it:
63
- * `public_query_*` for unsigned queries, `public_execute_*` for unsigned writes, and `execute_*` for signed
64
- * writes (e.g. `execute_set_username`).
96
+ * `public_query_*` for unsigned queries, `public_execute_*` for unsigned writes, `execute_*` for signed
97
+ * writes (e.g. `execute_set_username`), and `query_*` for signed reads (e.g. `query_followers`). The prefix
98
+ * names the route, so it never carries the `mobile:` prefix that the EIP-712 method does.
65
99
  */
66
100
  export type MobileServerRequestType =
67
101
  | `public_query_${MobileServerPublicQueryRequestType}`
68
102
  | `public_execute_${MobileServerPublicExecuteRequestType}`
69
- | `execute_${MobileServerExecuteRequestType}`;
103
+ | `execute_${MobileServerExecuteRequestType}`
104
+ | `query_${MobileServerSignedQueryRequestType}`;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Maximum Followers/Following page size the backend accepts. Requests outside 1–50 fail with
3
+ * `INVALID_FOLLOW_LIMIT`.
4
+ */
5
+ export const MOBILE_FOLLOW_LIST_MAX_PAGE_SIZE = 50;
6
+
7
+ /**
8
+ * Page size the backend uses when `limit` is omitted from a Followers or Following request.
9
+ */
10
+ export const MOBILE_FOLLOW_LIST_DEFAULT_PAGE_SIZE = 25;
11
+
12
+ /**
13
+ * Number of Followed By preview identities a follow summary carries. Fixed by the backend rather than
14
+ * requested, so use it to size the preview UI; `followedByCount` still reports the full intersection.
15
+ */
16
+ export const MOBILE_FOLLOWED_BY_PREVIEW_LIMIT = 2;
17
+
18
+ /**
19
+ * Maximum number of subaccounts a single profiles lookup accepts. An empty list, a duplicate, or more than
20
+ * this many fail with `INVALID_PROFILES_REQUEST`.
21
+ */
22
+ export const MOBILE_PROFILES_MAX_BATCH_SIZE = 25;
@@ -1,3 +1,4 @@
1
1
  export * from './feed';
2
+ export * from './follow';
2
3
  export * from './patterns';
3
4
  export * from './serverResponseGuards';