@nadohq/mobile-client 0.65.0 → 0.67.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 (211) hide show
  1. package/README.md +202 -10
  2. package/dist/{MobileClient-CfVZXTcz.cjs → MobileClient-vuI5wlB_.cjs} +303 -21
  3. package/dist/MobileClient-vuI5wlB_.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +175 -20
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +175 -20
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +303 -21
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-BZVwlsMg.d.ts → MobileServerFailureError-BeCCNShj.d.cts} +2 -3
  12. package/dist/MobileServerFailureError-BeCCNShj.d.cts.map +1 -0
  13. package/dist/{MobileServerFailureError-CbtK8kD4.d.cts → MobileServerFailureError-hHv28weI.d.ts} +2 -3
  14. package/dist/MobileServerFailureError-hHv28weI.d.ts.map +1 -0
  15. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts +15 -0
  16. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts.map +1 -0
  17. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts +15 -0
  18. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts.map +1 -0
  19. package/dist/{buildSignedMobileRequest-C1v0flCR.d.cts → buildSignedMobileRequest-DcEMKU_4.d.cts} +2 -2
  20. package/dist/{buildSignedMobileRequest-C1v0flCR.d.cts.map → buildSignedMobileRequest-DcEMKU_4.d.cts.map} +1 -1
  21. package/dist/{buildSignedMobileRequest-UeeWdYGU.d.ts → buildSignedMobileRequest-DlTMvoXR.d.ts} +2 -2
  22. package/dist/{buildSignedMobileRequest-UeeWdYGU.d.ts.map → buildSignedMobileRequest-DlTMvoXR.d.ts.map} +1 -1
  23. package/dist/{canonicalize-5eeZrkzQ.d.ts → canonicalize-BOZlflI3.d.ts} +2 -2
  24. package/dist/canonicalize-BOZlflI3.d.ts.map +1 -0
  25. package/dist/{canonicalize-CBaZprTo.d.cts → canonicalize-BvCozzvJ.d.cts} +2 -2
  26. package/dist/canonicalize-BvCozzvJ.d.cts.map +1 -0
  27. package/dist/{clientTypes-_mEgzmNs.d.cts → clientTypes-KpHlCDGZ.d.cts} +341 -38
  28. package/dist/clientTypes-KpHlCDGZ.d.cts.map +1 -0
  29. package/dist/{clientTypes-C9dJufxk.d.ts → clientTypes-XRI41VdC.d.ts} +341 -38
  30. package/dist/clientTypes-XRI41VdC.d.ts.map +1 -0
  31. package/dist/dataMappers.cjs +257 -20
  32. package/dist/dataMappers.cjs.map +1 -1
  33. package/dist/dataMappers.d.cts +98 -15
  34. package/dist/dataMappers.d.cts.map +1 -1
  35. package/dist/dataMappers.d.ts +98 -15
  36. package/dist/dataMappers.d.ts.map +1 -1
  37. package/dist/dataMappers.js +242 -20
  38. package/dist/dataMappers.js.map +1 -1
  39. package/dist/{eip712MethodByType-D8jLoxDu.d.cts → eip712MethodByType-DyAmzH7b.d.cts} +2 -2
  40. package/dist/eip712MethodByType-DyAmzH7b.d.cts.map +1 -0
  41. package/dist/{eip712MethodByType-DcUh8h-l.d.ts → eip712MethodByType-qSSAF6i2.d.ts} +2 -2
  42. package/dist/eip712MethodByType-qSSAF6i2.d.ts.map +1 -0
  43. package/dist/feed-BrgkFqP3.d.cts +23 -0
  44. package/dist/feed-BrgkFqP3.d.cts.map +1 -0
  45. package/dist/feed-DcbhLoo1.d.ts +23 -0
  46. package/dist/feed-DcbhLoo1.d.ts.map +1 -0
  47. package/dist/index-BVVgDSdq.d.cts +1 -0
  48. package/dist/index-BVVgDSdq.d.ts +1 -0
  49. package/dist/index.cjs +8 -2
  50. package/dist/index.d.cts +14 -16
  51. package/dist/index.d.ts +14 -16
  52. package/dist/index.js +4 -3
  53. package/dist/{mobileErrorCodes-CmjYEIYm.d.ts → mobileErrorCodes-CdwX6QTO.d.cts} +8 -3
  54. package/dist/mobileErrorCodes-CdwX6QTO.d.cts.map +1 -0
  55. package/dist/{mobileErrorCodes-CmjYEIYm.d.cts → mobileErrorCodes-CdwX6QTO.d.ts} +8 -3
  56. package/dist/mobileErrorCodes-CdwX6QTO.d.ts.map +1 -0
  57. package/dist/{payloadHash-DhHmgRqL.d.cts → payloadHash-BgGSZock.d.cts} +2 -2
  58. package/dist/payloadHash-BgGSZock.d.cts.map +1 -0
  59. package/dist/{payloadHash-E1_8fpLW.d.ts → payloadHash-Bi6upCLQ.d.ts} +2 -2
  60. package/dist/payloadHash-Bi6upCLQ.d.ts.map +1 -0
  61. package/dist/{serverExecuteTypes-BwWGspOg.d.ts → serverExecuteTypes-DuCWe_H_.d.cts} +23 -14
  62. package/dist/serverExecuteTypes-DuCWe_H_.d.cts.map +1 -0
  63. package/dist/{serverExecuteTypes-B-OJok19.d.cts → serverExecuteTypes-usJ6HMJv.d.ts} +23 -14
  64. package/dist/serverExecuteTypes-usJ6HMJv.d.ts.map +1 -0
  65. package/dist/{serverModelTypes-CiA-dy1R.d.ts → serverModelTypes-CTHMNIQS.d.cts} +170 -13
  66. package/dist/serverModelTypes-CTHMNIQS.d.cts.map +1 -0
  67. package/dist/{serverModelTypes-CiA-dy1R.d.cts → serverModelTypes-CTHMNIQS.d.ts} +170 -13
  68. package/dist/serverModelTypes-CTHMNIQS.d.ts.map +1 -0
  69. package/dist/{serverResponseGuards-CShiXpHT.d.ts → serverResponseGuards-CzmldAad.d.cts} +2 -2
  70. package/dist/{serverResponseGuards-BW69PS7O.d.cts.map → serverResponseGuards-CzmldAad.d.cts.map} +1 -1
  71. package/dist/{serverResponseGuards-BW69PS7O.d.cts → serverResponseGuards-Dvkbjp2_.d.ts} +2 -2
  72. package/dist/{serverResponseGuards-CShiXpHT.d.ts.map → serverResponseGuards-Dvkbjp2_.d.ts.map} +1 -1
  73. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  74. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  75. package/dist/signing/canonicalize.cjs +53 -2
  76. package/dist/signing/canonicalize.cjs.map +1 -1
  77. package/dist/signing/canonicalize.d.cts +1 -1
  78. package/dist/signing/canonicalize.d.ts +1 -1
  79. package/dist/signing/canonicalize.js +53 -2
  80. package/dist/signing/canonicalize.js.map +1 -1
  81. package/dist/signing/eip712MethodByType.cjs +7 -0
  82. package/dist/signing/eip712MethodByType.cjs.map +1 -1
  83. package/dist/signing/eip712MethodByType.d.cts +1 -1
  84. package/dist/signing/eip712MethodByType.d.ts +1 -1
  85. package/dist/signing/eip712MethodByType.js +7 -0
  86. package/dist/signing/eip712MethodByType.js.map +1 -1
  87. package/dist/signing/index.d.cts +5 -5
  88. package/dist/signing/index.d.ts +5 -5
  89. package/dist/signing/payloadHash.cjs +1 -1
  90. package/dist/signing/payloadHash.d.cts +1 -1
  91. package/dist/signing/payloadHash.d.ts +1 -1
  92. package/dist/signing/payloadHash.js +1 -1
  93. package/dist/signing/types.d.cts +1 -1
  94. package/dist/signing/types.d.ts +1 -1
  95. package/dist/types/MobileServerFailureError.d.cts +1 -1
  96. package/dist/types/MobileServerFailureError.d.ts +1 -1
  97. package/dist/types/MobileUnsupportedNotificationRouteError.cjs +19 -0
  98. package/dist/types/MobileUnsupportedNotificationRouteError.cjs.map +1 -0
  99. package/dist/types/MobileUnsupportedNotificationRouteError.d.cts +2 -0
  100. package/dist/types/MobileUnsupportedNotificationRouteError.d.ts +2 -0
  101. package/dist/types/MobileUnsupportedNotificationRouteError.js +18 -0
  102. package/dist/types/MobileUnsupportedNotificationRouteError.js.map +1 -0
  103. package/dist/types/clientTypes.d.cts +2 -2
  104. package/dist/types/clientTypes.d.ts +2 -2
  105. package/dist/types/index.cjs +4 -0
  106. package/dist/types/index.d.cts +8 -9
  107. package/dist/types/index.d.ts +8 -9
  108. package/dist/types/index.js +3 -2
  109. package/dist/types/mobileErrorCodes.cjs +11 -1
  110. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  111. package/dist/types/mobileErrorCodes.d.cts +1 -1
  112. package/dist/types/mobileErrorCodes.d.ts +1 -1
  113. package/dist/types/mobileErrorCodes.js +11 -1
  114. package/dist/types/mobileErrorCodes.js.map +1 -1
  115. package/dist/types/serverBaseTypes.d.cts +1 -1
  116. package/dist/types/serverBaseTypes.d.ts +1 -1
  117. package/dist/types/serverExecuteTypes.d.cts +2 -2
  118. package/dist/types/serverExecuteTypes.d.ts +2 -2
  119. package/dist/types/serverModelTypes.cjs +15 -0
  120. package/dist/types/serverModelTypes.cjs.map +1 -1
  121. package/dist/types/serverModelTypes.d.cts +2 -2
  122. package/dist/types/serverModelTypes.d.ts +2 -2
  123. package/dist/types/serverModelTypes.js +14 -1
  124. package/dist/types/serverModelTypes.js.map +1 -1
  125. package/dist/types/serverQueryTypes.d.cts +2 -2
  126. package/dist/types/serverQueryTypes.d.ts +2 -2
  127. package/dist/types/serverRequestTypes.d.cts +2 -2
  128. package/dist/types/serverRequestTypes.d.ts +2 -2
  129. package/dist/types-CByl_NyJ.d.ts +825 -0
  130. package/dist/types-CByl_NyJ.d.ts.map +1 -0
  131. package/dist/types-CR5iaKR4.d.cts +825 -0
  132. package/dist/types-CR5iaKR4.d.cts.map +1 -0
  133. package/dist/utils/buildMobileFeedImageMultipart.cjs +31 -0
  134. package/dist/utils/buildMobileFeedImageMultipart.cjs.map +1 -0
  135. package/dist/utils/buildMobileFeedImageMultipart.d.cts +15 -0
  136. package/dist/utils/buildMobileFeedImageMultipart.d.cts.map +1 -0
  137. package/dist/utils/buildMobileFeedImageMultipart.d.ts +15 -0
  138. package/dist/utils/buildMobileFeedImageMultipart.d.ts.map +1 -0
  139. package/dist/utils/buildMobileFeedImageMultipart.js +30 -0
  140. package/dist/utils/buildMobileFeedImageMultipart.js.map +1 -0
  141. package/dist/utils/feed.cjs +12 -0
  142. package/dist/utils/feed.cjs.map +1 -1
  143. package/dist/utils/feed.d.cts +2 -2
  144. package/dist/utils/feed.d.ts +2 -2
  145. package/dist/utils/feed.js +11 -1
  146. package/dist/utils/feed.js.map +1 -1
  147. package/dist/utils/index.cjs +3 -1
  148. package/dist/utils/index.d.cts +3 -3
  149. package/dist/utils/index.d.ts +3 -3
  150. package/dist/utils/index.js +2 -2
  151. package/dist/utils/serverResponseGuards.d.cts +1 -1
  152. package/dist/utils/serverResponseGuards.d.ts +1 -1
  153. package/package.json +3 -3
  154. package/src/MobileClient.ts +444 -19
  155. package/src/dataMappers.test.ts +511 -6
  156. package/src/dataMappers.ts +325 -24
  157. package/src/feedImages.test.ts +309 -0
  158. package/src/notifications.test.ts +950 -0
  159. package/src/signing/canonicalize.ts +72 -0
  160. package/src/signing/eip712MethodByType.ts +10 -0
  161. package/src/signing/signing.test.ts +67 -1
  162. package/src/signing/types.ts +55 -9
  163. package/src/socialFeed.test.ts +63 -2
  164. package/src/types/MobileUnsupportedNotificationRouteError.ts +16 -0
  165. package/src/types/clientTypes.ts +427 -36
  166. package/src/types/index.ts +1 -0
  167. package/src/types/mobileErrorCodes.ts +10 -0
  168. package/src/types/serverExecuteTypes.ts +22 -10
  169. package/src/types/serverModelTypes.ts +216 -11
  170. package/src/types/serverQueryTypes.ts +106 -9
  171. package/src/types/serverRequestTypes.ts +114 -17
  172. package/src/utils/buildMobileFeedImageMultipart.test.ts +45 -0
  173. package/src/utils/buildMobileFeedImageMultipart.ts +36 -0
  174. package/src/utils/feed.ts +16 -0
  175. package/dist/MobileClient-CfVZXTcz.cjs.map +0 -1
  176. package/dist/MobileServerFailureError-BZVwlsMg.d.ts.map +0 -1
  177. package/dist/MobileServerFailureError-CbtK8kD4.d.cts.map +0 -1
  178. package/dist/canonicalize-5eeZrkzQ.d.ts.map +0 -1
  179. package/dist/canonicalize-CBaZprTo.d.cts.map +0 -1
  180. package/dist/clientTypes-C9dJufxk.d.ts.map +0 -1
  181. package/dist/clientTypes-_mEgzmNs.d.cts.map +0 -1
  182. package/dist/eip712MethodByType-D8jLoxDu.d.cts.map +0 -1
  183. package/dist/eip712MethodByType-DcUh8h-l.d.ts.map +0 -1
  184. package/dist/feed-DTJlsg8W.d.cts +0 -14
  185. package/dist/feed-DTJlsg8W.d.cts.map +0 -1
  186. package/dist/feed-DTJlsg8W.d.ts +0 -14
  187. package/dist/feed-DTJlsg8W.d.ts.map +0 -1
  188. package/dist/mobileErrorCodes-CmjYEIYm.d.cts.map +0 -1
  189. package/dist/mobileErrorCodes-CmjYEIYm.d.ts.map +0 -1
  190. package/dist/payloadHash-DhHmgRqL.d.cts.map +0 -1
  191. package/dist/payloadHash-E1_8fpLW.d.ts.map +0 -1
  192. package/dist/serverBaseTypes-B5J1_TyC.d.cts +0 -25
  193. package/dist/serverBaseTypes-B5J1_TyC.d.cts.map +0 -1
  194. package/dist/serverBaseTypes-DnLzsdm-.d.ts +0 -25
  195. package/dist/serverBaseTypes-DnLzsdm-.d.ts.map +0 -1
  196. package/dist/serverExecuteTypes-B-OJok19.d.cts.map +0 -1
  197. package/dist/serverExecuteTypes-BwWGspOg.d.ts.map +0 -1
  198. package/dist/serverModelTypes-CiA-dy1R.d.cts.map +0 -1
  199. package/dist/serverModelTypes-CiA-dy1R.d.ts.map +0 -1
  200. package/dist/serverQueryTypes-CJcjLB7n.d.ts +0 -283
  201. package/dist/serverQueryTypes-CJcjLB7n.d.ts.map +0 -1
  202. package/dist/serverQueryTypes-V1f9yE7L.d.cts +0 -283
  203. package/dist/serverQueryTypes-V1f9yE7L.d.cts.map +0 -1
  204. package/dist/serverRequestTypes-CRlkNSu5.d.cts +0 -270
  205. package/dist/serverRequestTypes-CRlkNSu5.d.cts.map +0 -1
  206. package/dist/serverRequestTypes-DZoOhV2o.d.ts +0 -270
  207. package/dist/serverRequestTypes-DZoOhV2o.d.ts.map +0 -1
  208. package/dist/types-BiRmH5Iv.d.cts +0 -108
  209. package/dist/types-BiRmH5Iv.d.cts.map +0 -1
  210. package/dist/types-DoJuq0Ij.d.ts +0 -108
  211. package/dist/types-DoJuq0Ij.d.ts.map +0 -1
@@ -0,0 +1,309 @@
1
+ import { afterEach, describe, expect, it, jest } from '@jest/globals';
2
+ import { createWalletClient, http, keccak256, stringToBytes } from 'viem';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { ink } from 'viem/chains';
5
+ import { MobileClient } from './MobileClient';
6
+ import {
7
+ MOBILE_ERROR_CODES,
8
+ MobileServerFailureError,
9
+ MobileServerFailureResponse,
10
+ MobileServerSocialFeedImage,
11
+ } from './types';
12
+ import { MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES } from './utils/feed';
13
+
14
+ const imagePath = '/v1/mobile/feed/images/test/standard';
15
+ const session = { sessionId: 'Session', viewerToken: 'Viewer' };
16
+ const imageBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 127, 128, 255]);
17
+
18
+ function mockImageClient(data: ArrayBuffer | Uint8Array, status = 200) {
19
+ const client = new MobileClient({ url: 'https://mobile.example/mobile/v1' });
20
+ client.axiosInstance.defaults.adapter = (config) => {
21
+ expect(config.url).toBe(
22
+ 'https://mobile.example/mobile/v1/feed/images/test/standard',
23
+ );
24
+ expect(config.method).toBe('get');
25
+ expect(config.responseType).toBe('arraybuffer');
26
+ expect(config.headers.get('x-feed-session-id')).toBe(session.sessionId);
27
+ expect(config.headers.get('x-feed-viewer-token')).toBe(session.viewerToken);
28
+ return Promise.resolve({
29
+ data,
30
+ status,
31
+ statusText: 'mock',
32
+ headers: {},
33
+ config,
34
+ });
35
+ };
36
+ return client;
37
+ }
38
+
39
+ describe('[mobile-client]: feed image reads', () => {
40
+ afterEach(() => {
41
+ jest.restoreAllMocks();
42
+ });
43
+
44
+ it.each([
45
+ '/v1/mobile/feed/images/test/standard',
46
+ '/feed/images/test/standard',
47
+ '/v1/mobile/feed/comment-images/test/thumbnail',
48
+ ])('exposes a native image request without fetching bytes: %s', (path) => {
49
+ const client = new MobileClient({
50
+ url: 'https://mobile.example/mobile/v1',
51
+ });
52
+ const get = jest.spyOn(client.axiosInstance, 'get');
53
+ expect(client.getFeedImageRequest({ ...session, path })).toEqual({
54
+ url: client.resolveFeedImageUrl(path),
55
+ headers: {
56
+ 'x-feed-session-id': session.sessionId,
57
+ 'x-feed-viewer-token': session.viewerToken,
58
+ },
59
+ });
60
+ expect(get).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it.each(['uint8array', 'arraybuffer'] as const)(
64
+ 'preserves successful image bytes from %s responses',
65
+ async (format) => {
66
+ const data =
67
+ format === 'uint8array' ? imageBytes : imageBytes.slice().buffer;
68
+ const client = mockImageClient(data);
69
+ const getRequest = jest.spyOn(client, 'getFeedImageRequest');
70
+ await expect(
71
+ client.getFeedImageBytes({ ...session, path: imagePath }),
72
+ ).resolves.toEqual(imageBytes);
73
+ expect(getRequest).toHaveBeenCalledWith({ ...session, path: imagePath });
74
+ },
75
+ );
76
+
77
+ describe.each(['uint8array', 'arraybuffer'] as const)(
78
+ '%s error responses',
79
+ (format) => {
80
+ it.each([
81
+ [401, MOBILE_ERROR_CODES.SOCIAL_FEED_SESSION_EXPIRED, 'feed_image'],
82
+ [403, MOBILE_ERROR_CODES.SOCIAL_FEED_SESSION_MISMATCH, 'feed_image'],
83
+ [404, MOBILE_ERROR_CODES.SOCIAL_FEED_CONTENT_UNAVAILABLE, 'feed_image'],
84
+ [
85
+ 404,
86
+ MOBILE_ERROR_CODES.SOCIAL_FEED_CONTENT_UNAVAILABLE,
87
+ 'feed_comment_image',
88
+ ],
89
+ ] as const)(
90
+ 'preserves the failure envelope for HTTP %i',
91
+ async (status, errorCode, requestType) => {
92
+ const failure: MobileServerFailureResponse = {
93
+ status: 'failure',
94
+ error: 'Image unavailable: \u56fe\u7247\u4e0d\u53ef\u7528',
95
+ error_code: errorCode,
96
+ request_type: requestType,
97
+ };
98
+ const encoded = stringToBytes(JSON.stringify(failure));
99
+ const data =
100
+ format === 'uint8array' ? encoded : Uint8Array.from(encoded).buffer;
101
+ const client = mockImageClient(data, status);
102
+ const result = client.getFeedImageBytes({
103
+ ...session,
104
+ path: imagePath,
105
+ });
106
+ await expect(result).rejects.toBeInstanceOf(MobileServerFailureError);
107
+ await expect(result).rejects.toMatchObject({
108
+ errorCode,
109
+ requestType,
110
+ httpStatus: status,
111
+ responseData: failure,
112
+ });
113
+ },
114
+ );
115
+ },
116
+ );
117
+
118
+ it.each([
119
+ '',
120
+ '<html>Bad gateway</html>',
121
+ '{invalid',
122
+ '{"message":"unavailable"}',
123
+ ])('keeps non-envelope failures as transport errors: %s', async (body) => {
124
+ const client = mockImageClient(stringToBytes(body), 502);
125
+ const result = client.getFeedImageBytes({ ...session, path: imagePath });
126
+ await expect(result).rejects.not.toBeInstanceOf(MobileServerFailureError);
127
+ await expect(result).rejects.toThrow(
128
+ 'Unexpected response from mobile service: 502',
129
+ );
130
+ });
131
+
132
+ it.each([
133
+ '/v1/mobile/feed/images/test/standard',
134
+ '/feed/images/test/standard',
135
+ ])('resolves the metadata path %s', (path) => {
136
+ const client = new MobileClient({
137
+ url: 'https://mobile.example/mobile/v1',
138
+ });
139
+ expect(client.resolveFeedImageUrl(path)).toBe(
140
+ 'https://mobile.example/mobile/v1/feed/images/test/standard',
141
+ );
142
+ });
143
+ });
144
+
145
+ describe('[mobile-client]: feed image uploads', () => {
146
+ it.each([
147
+ ['createFeedPostImageUpload', 'create_feed_post_image_uploads'],
148
+ ['createFeedCommentImageUpload', 'create_feed_comment_image_uploads'],
149
+ ] as const)('sends signed multipart bytes for %s', async (method, type) => {
150
+ const account = privateKeyToAccount(
151
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907',
152
+ );
153
+ const client = new MobileClient({
154
+ url: 'https://mobile.example/mobile/v1',
155
+ walletClient: createWalletClient({
156
+ account,
157
+ chain: ink,
158
+ transport: http(),
159
+ }),
160
+ });
161
+ const image: MobileServerSocialFeedImage = {
162
+ image_id: '88888888-8888-4888-8888-888888888888',
163
+ url: imagePath,
164
+ width: 1,
165
+ height: 1,
166
+ content_type: 'image/jpeg',
167
+ byte_size: imageBytes.length,
168
+ thumbnail: {
169
+ url: '/v1/mobile/feed/images/test/thumbnail',
170
+ width: 1,
171
+ height: 1,
172
+ },
173
+ };
174
+ client.axiosInstance.defaults.adapter = (config) => {
175
+ expect(config.url).toBe('https://mobile.example/mobile/v1/execute');
176
+ expect(config.method).toBe('post');
177
+ expect(config.data).toBeInstanceOf(ArrayBuffer);
178
+ const body = new Uint8Array(config.data as ArrayBuffer);
179
+ const text = String.fromCharCode(...body);
180
+ const contentType = config.headers.get('Content-Type');
181
+ if (typeof contentType !== 'string')
182
+ throw new Error('Expected multipart Content-Type');
183
+ expect(contentType).toMatch(/^multipart\/form-data; boundary=/);
184
+ const boundary = contentType.split('boundary=')[1];
185
+ const payloadStart = text.indexOf('\r\n\r\n') + 4;
186
+ const payloadEnd = text.indexOf(`\r\n--${boundary}`, payloadStart);
187
+ const payload = JSON.parse(
188
+ text.slice(payloadStart, payloadEnd),
189
+ ) as Record<string, unknown>;
190
+ expect(payload).toMatchObject({
191
+ type,
192
+ content_hash: keccak256(imageBytes),
193
+ });
194
+ expect(payload.signature).toEqual(expect.stringMatching(/^0x[\da-f]+$/));
195
+ const fileHeader = text.indexOf('name="file"; filename="image"');
196
+ expect(fileHeader).toBeGreaterThan(payloadEnd);
197
+ const fileStart = text.indexOf('\r\n\r\n', fileHeader) + 4;
198
+ expect(
199
+ new Uint8Array(body.subarray(fileStart, fileStart + imageBytes.length)),
200
+ ).toEqual(imageBytes);
201
+ expect(text.slice(fileStart + imageBytes.length)).toBe(
202
+ `\r\n--${boundary}--\r\n`,
203
+ );
204
+ return Promise.resolve({
205
+ data: { status: 'success', image },
206
+ status: 200,
207
+ statusText: 'OK',
208
+ headers: {},
209
+ config,
210
+ });
211
+ };
212
+ await expect(
213
+ client[method]({
214
+ subaccountOwner: account.address,
215
+ subaccountName: 'default',
216
+ chainId: ink.id,
217
+ verifyingAddr: '0x0000000000000000000000000000000000000001',
218
+ fileBytes: imageBytes,
219
+ }),
220
+ ).resolves.toEqual({
221
+ imageId: image.image_id,
222
+ url: imagePath,
223
+ width: 1,
224
+ height: 1,
225
+ contentType: 'image/jpeg',
226
+ byteSize: imageBytes.length,
227
+ thumbnail: image.thumbnail,
228
+ });
229
+ });
230
+ });
231
+
232
+ describe.each([
233
+ 'createFeedPostImageUpload',
234
+ 'createFeedCommentImageUpload',
235
+ ] as const)('[mobile-client]: %s request size', (method) => {
236
+ afterEach(() => {
237
+ jest.restoreAllMocks();
238
+ });
239
+
240
+ it.each(
241
+ [-1, 0, 1].flatMap((offset) =>
242
+ [1n, undefined].map((nonce) => ({ offset, nonce })),
243
+ ),
244
+ )(
245
+ 'checks the complete body at offset $offset with nonce $nonce',
246
+ async ({ offset, nonce }) => {
247
+ const account = privateKeyToAccount(
248
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907',
249
+ );
250
+ const walletClient = createWalletClient({
251
+ account,
252
+ chain: ink,
253
+ transport: http(),
254
+ });
255
+ const sign = jest.spyOn(walletClient, 'signTypedData');
256
+ const client = new MobileClient({
257
+ url: 'https://mobile.example/mobile/v1',
258
+ walletClient,
259
+ });
260
+ const params = {
261
+ subaccountOwner: account.address,
262
+ subaccountName: 'default',
263
+ chainId: ink.id,
264
+ verifyingAddr: '0x0000000000000000000000000000000000000001',
265
+ nonce,
266
+ } as const;
267
+ const post = jest.spyOn(client.axiosInstance, 'post').mockResolvedValue({
268
+ data: {
269
+ status: 'success',
270
+ image: {
271
+ image_id: 'test',
272
+ url: imagePath,
273
+ width: 1,
274
+ height: 1,
275
+ content_type: 'image/jpeg',
276
+ byte_size: 0,
277
+ thumbnail: { url: imagePath, width: 1, height: 1 },
278
+ },
279
+ },
280
+ status: 200,
281
+ });
282
+ await client[method]({ ...params, fileBytes: new Uint8Array() });
283
+ const emptyBody: unknown = post.mock.calls[0][1];
284
+ if (!(emptyBody instanceof Uint8Array))
285
+ throw new Error('Expected multipart bytes');
286
+ post.mockClear();
287
+ sign.mockClear();
288
+ const fileBytes = new Uint8Array(
289
+ MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES - emptyBody.byteLength + offset,
290
+ );
291
+ const result = client[method]({ ...params, fileBytes });
292
+ if (offset > 0) {
293
+ await expect(result).rejects.toBeInstanceOf(RangeError);
294
+ await expect(result).rejects.toThrow('2097152 bytes (2 MiB)');
295
+ expect(post).not.toHaveBeenCalled();
296
+ expect(sign).not.toHaveBeenCalled();
297
+ } else {
298
+ await expect(result).resolves.toMatchObject({ imageId: 'test' });
299
+ expect(sign).toHaveBeenCalledTimes(1);
300
+ expect(post).toHaveBeenCalledTimes(1);
301
+ const body: unknown = post.mock.calls[0][1];
302
+ expect(body).toHaveProperty(
303
+ 'byteLength',
304
+ MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES + offset,
305
+ );
306
+ }
307
+ },
308
+ );
309
+ });