@nadohq/mobile-client 0.27.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 (204) hide show
  1. package/README.md +73 -0
  2. package/dist/MobileClient-9JSAzblH.cjs +218 -0
  3. package/dist/MobileClient-9JSAzblH.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +4 -0
  5. package/dist/MobileClient.d.cts +114 -0
  6. package/dist/MobileClient.d.cts.map +1 -0
  7. package/dist/MobileClient.d.ts +114 -0
  8. package/dist/MobileClient.d.ts.map +1 -0
  9. package/dist/MobileClient.js +185 -0
  10. package/dist/MobileClient.js.map +1 -0
  11. package/dist/MobileServerFailureError-Belp_yef.d.cts +25 -0
  12. package/dist/MobileServerFailureError-Belp_yef.d.cts.map +1 -0
  13. package/dist/MobileServerFailureError-DqM0T6CB.d.ts +25 -0
  14. package/dist/MobileServerFailureError-DqM0T6CB.d.ts.map +1 -0
  15. package/dist/buildSignedMobileRequest-BsbBkU0K.d.cts +14 -0
  16. package/dist/buildSignedMobileRequest-BsbBkU0K.d.cts.map +1 -0
  17. package/dist/buildSignedMobileRequest-CDRsRNHm.d.ts +14 -0
  18. package/dist/buildSignedMobileRequest-CDRsRNHm.d.ts.map +1 -0
  19. package/dist/canonicalize-Dm7sXwPg.d.cts +14 -0
  20. package/dist/canonicalize-Dm7sXwPg.d.cts.map +1 -0
  21. package/dist/canonicalize-PX6dTIdF.d.ts +14 -0
  22. package/dist/canonicalize-PX6dTIdF.d.ts.map +1 -0
  23. package/dist/clientTypes-DWW2N66c.d.ts +168 -0
  24. package/dist/clientTypes-DWW2N66c.d.ts.map +1 -0
  25. package/dist/clientTypes-Et0PUa2d.d.cts +168 -0
  26. package/dist/clientTypes-Et0PUa2d.d.cts.map +1 -0
  27. package/dist/dataMappers.cjs +89 -0
  28. package/dist/dataMappers.cjs.map +1 -0
  29. package/dist/dataMappers.d.cts +27 -0
  30. package/dist/dataMappers.d.cts.map +1 -0
  31. package/dist/dataMappers.d.ts +27 -0
  32. package/dist/dataMappers.d.ts.map +1 -0
  33. package/dist/dataMappers.js +84 -0
  34. package/dist/dataMappers.js.map +1 -0
  35. package/dist/eip712MethodByType-DJgXal9r.d.ts +10 -0
  36. package/dist/eip712MethodByType-DJgXal9r.d.ts.map +1 -0
  37. package/dist/eip712MethodByType-DLh9MRsD.d.cts +10 -0
  38. package/dist/eip712MethodByType-DLh9MRsD.d.cts.map +1 -0
  39. package/dist/endpoints.cjs +14 -0
  40. package/dist/endpoints.cjs.map +1 -0
  41. package/dist/endpoints.d.cts +10 -0
  42. package/dist/endpoints.d.cts.map +1 -0
  43. package/dist/endpoints.d.ts +10 -0
  44. package/dist/endpoints.d.ts.map +1 -0
  45. package/dist/endpoints.js +13 -0
  46. package/dist/endpoints.js.map +1 -0
  47. package/dist/index.cjs +27 -0
  48. package/dist/index.d.cts +18 -0
  49. package/dist/index.d.ts +18 -0
  50. package/dist/index.js +15 -0
  51. package/dist/mobileErrorCodes-D8KzJXbU.d.cts +29 -0
  52. package/dist/mobileErrorCodes-D8KzJXbU.d.cts.map +1 -0
  53. package/dist/mobileErrorCodes-DM09jnNe.d.ts +29 -0
  54. package/dist/mobileErrorCodes-DM09jnNe.d.ts.map +1 -0
  55. package/dist/nonce-DCOrXXkQ.d.ts +15 -0
  56. package/dist/nonce-DCOrXXkQ.d.ts.map +1 -0
  57. package/dist/nonce-zGyisRlj.d.cts +15 -0
  58. package/dist/nonce-zGyisRlj.d.cts.map +1 -0
  59. package/dist/patterns-B9J2ldpx.d.cts +8 -0
  60. package/dist/patterns-B9J2ldpx.d.cts.map +1 -0
  61. package/dist/patterns-BbxH3_VA.d.ts +8 -0
  62. package/dist/patterns-BbxH3_VA.d.ts.map +1 -0
  63. package/dist/payloadHash-Bmhzs78N.d.ts +13 -0
  64. package/dist/payloadHash-Bmhzs78N.d.ts.map +1 -0
  65. package/dist/payloadHash-nj8WuLUP.d.cts +13 -0
  66. package/dist/payloadHash-nj8WuLUP.d.cts.map +1 -0
  67. package/dist/serverBaseTypes-BicXMrZV.d.cts +43 -0
  68. package/dist/serverBaseTypes-BicXMrZV.d.cts.map +1 -0
  69. package/dist/serverBaseTypes-ewbo8l4R.d.ts +43 -0
  70. package/dist/serverBaseTypes-ewbo8l4R.d.ts.map +1 -0
  71. package/dist/serverExecuteTypes-CXnMjKnt.d.ts +11 -0
  72. package/dist/serverExecuteTypes-CXnMjKnt.d.ts.map +1 -0
  73. package/dist/serverExecuteTypes-DKYK1V7C.d.cts +11 -0
  74. package/dist/serverExecuteTypes-DKYK1V7C.d.cts.map +1 -0
  75. package/dist/serverModelTypes-CQgfqgW3.d.cts +69 -0
  76. package/dist/serverModelTypes-CQgfqgW3.d.cts.map +1 -0
  77. package/dist/serverModelTypes-vEPhHIit.d.ts +69 -0
  78. package/dist/serverModelTypes-vEPhHIit.d.ts.map +1 -0
  79. package/dist/serverQueryTypes-BI-2TD8F.d.ts +53 -0
  80. package/dist/serverQueryTypes-BI-2TD8F.d.ts.map +1 -0
  81. package/dist/serverQueryTypes-D7Yharse.d.cts +53 -0
  82. package/dist/serverQueryTypes-D7Yharse.d.cts.map +1 -0
  83. package/dist/serverResponseGuards-C_vZIgGl.d.ts +16 -0
  84. package/dist/serverResponseGuards-C_vZIgGl.d.ts.map +1 -0
  85. package/dist/serverResponseGuards-DjCNL3CR.d.cts +16 -0
  86. package/dist/serverResponseGuards-DjCNL3CR.d.cts.map +1 -0
  87. package/dist/signing/buildSignedMobileRequest.cjs +47 -0
  88. package/dist/signing/buildSignedMobileRequest.cjs.map +1 -0
  89. package/dist/signing/buildSignedMobileRequest.d.cts +2 -0
  90. package/dist/signing/buildSignedMobileRequest.d.ts +2 -0
  91. package/dist/signing/buildSignedMobileRequest.js +45 -0
  92. package/dist/signing/buildSignedMobileRequest.js.map +1 -0
  93. package/dist/signing/canonicalize.cjs +68 -0
  94. package/dist/signing/canonicalize.cjs.map +1 -0
  95. package/dist/signing/canonicalize.d.cts +2 -0
  96. package/dist/signing/canonicalize.d.ts +2 -0
  97. package/dist/signing/canonicalize.js +67 -0
  98. package/dist/signing/canonicalize.js.map +1 -0
  99. package/dist/signing/eip712MethodByType.cjs +20 -0
  100. package/dist/signing/eip712MethodByType.cjs.map +1 -0
  101. package/dist/signing/eip712MethodByType.d.cts +2 -0
  102. package/dist/signing/eip712MethodByType.d.ts +2 -0
  103. package/dist/signing/eip712MethodByType.js +19 -0
  104. package/dist/signing/eip712MethodByType.js.map +1 -0
  105. package/dist/signing/index.cjs +11 -0
  106. package/dist/signing/index.d.cts +7 -0
  107. package/dist/signing/index.d.ts +7 -0
  108. package/dist/signing/index.js +6 -0
  109. package/dist/signing/nonce.cjs +19 -0
  110. package/dist/signing/nonce.cjs.map +1 -0
  111. package/dist/signing/nonce.d.cts +2 -0
  112. package/dist/signing/nonce.d.ts +2 -0
  113. package/dist/signing/nonce.js +18 -0
  114. package/dist/signing/nonce.js.map +1 -0
  115. package/dist/signing/payloadHash.cjs +17 -0
  116. package/dist/signing/payloadHash.cjs.map +1 -0
  117. package/dist/signing/payloadHash.d.cts +2 -0
  118. package/dist/signing/payloadHash.d.ts +2 -0
  119. package/dist/signing/payloadHash.js +15 -0
  120. package/dist/signing/payloadHash.js.map +1 -0
  121. package/dist/signing/types.cjs +0 -0
  122. package/dist/signing/types.d.cts +2 -0
  123. package/dist/signing/types.d.ts +2 -0
  124. package/dist/signing/types.js +1 -0
  125. package/dist/types/MobileServerFailureError.cjs +17 -0
  126. package/dist/types/MobileServerFailureError.cjs.map +1 -0
  127. package/dist/types/MobileServerFailureError.d.cts +2 -0
  128. package/dist/types/MobileServerFailureError.d.ts +2 -0
  129. package/dist/types/MobileServerFailureError.js +16 -0
  130. package/dist/types/MobileServerFailureError.js.map +1 -0
  131. package/dist/types/clientTypes.cjs +0 -0
  132. package/dist/types/clientTypes.d.cts +2 -0
  133. package/dist/types/clientTypes.d.ts +2 -0
  134. package/dist/types/clientTypes.js +1 -0
  135. package/dist/types/index.cjs +5 -0
  136. package/dist/types/index.d.cts +8 -0
  137. package/dist/types/index.d.ts +8 -0
  138. package/dist/types/index.js +3 -0
  139. package/dist/types/mobileErrorCodes.cjs +27 -0
  140. package/dist/types/mobileErrorCodes.cjs.map +1 -0
  141. package/dist/types/mobileErrorCodes.d.cts +2 -0
  142. package/dist/types/mobileErrorCodes.d.ts +2 -0
  143. package/dist/types/mobileErrorCodes.js +26 -0
  144. package/dist/types/mobileErrorCodes.js.map +1 -0
  145. package/dist/types/serverBaseTypes.cjs +0 -0
  146. package/dist/types/serverBaseTypes.d.cts +2 -0
  147. package/dist/types/serverBaseTypes.d.ts +2 -0
  148. package/dist/types/serverBaseTypes.js +1 -0
  149. package/dist/types/serverExecuteTypes.cjs +0 -0
  150. package/dist/types/serverExecuteTypes.d.cts +2 -0
  151. package/dist/types/serverExecuteTypes.d.ts +2 -0
  152. package/dist/types/serverExecuteTypes.js +1 -0
  153. package/dist/types/serverModelTypes.cjs +0 -0
  154. package/dist/types/serverModelTypes.d.cts +2 -0
  155. package/dist/types/serverModelTypes.d.ts +2 -0
  156. package/dist/types/serverModelTypes.js +1 -0
  157. package/dist/types/serverQueryTypes.cjs +0 -0
  158. package/dist/types/serverQueryTypes.d.cts +2 -0
  159. package/dist/types/serverQueryTypes.d.ts +2 -0
  160. package/dist/types/serverQueryTypes.js +1 -0
  161. package/dist/types-B76Pn7Sy.d.ts +73 -0
  162. package/dist/types-B76Pn7Sy.d.ts.map +1 -0
  163. package/dist/types-uCD4BNZR.d.cts +73 -0
  164. package/dist/types-uCD4BNZR.d.cts.map +1 -0
  165. package/dist/utils/index.cjs +6 -0
  166. package/dist/utils/index.d.cts +3 -0
  167. package/dist/utils/index.d.ts +3 -0
  168. package/dist/utils/index.js +3 -0
  169. package/dist/utils/patterns.cjs +10 -0
  170. package/dist/utils/patterns.cjs.map +1 -0
  171. package/dist/utils/patterns.d.cts +2 -0
  172. package/dist/utils/patterns.d.ts +2 -0
  173. package/dist/utils/patterns.js +9 -0
  174. package/dist/utils/patterns.js.map +1 -0
  175. package/dist/utils/serverResponseGuards.cjs +19 -0
  176. package/dist/utils/serverResponseGuards.cjs.map +1 -0
  177. package/dist/utils/serverResponseGuards.d.cts +2 -0
  178. package/dist/utils/serverResponseGuards.d.ts +2 -0
  179. package/dist/utils/serverResponseGuards.js +17 -0
  180. package/dist/utils/serverResponseGuards.js.map +1 -0
  181. package/package.json +58 -0
  182. package/src/MobileClient.ts +395 -0
  183. package/src/dataMappers.ts +106 -0
  184. package/src/endpoints.ts +10 -0
  185. package/src/index.ts +5 -0
  186. package/src/signing/buildSignedMobileRequest.ts +58 -0
  187. package/src/signing/canonicalize.ts +69 -0
  188. package/src/signing/eip712MethodByType.ts +19 -0
  189. package/src/signing/index.ts +6 -0
  190. package/src/signing/nonce.ts +16 -0
  191. package/src/signing/payloadHash.ts +12 -0
  192. package/src/signing/signing.test.ts +224 -0
  193. package/src/signing/types.ts +71 -0
  194. package/src/types/MobileServerFailureError.ts +18 -0
  195. package/src/types/clientTypes.ts +182 -0
  196. package/src/types/index.ts +7 -0
  197. package/src/types/mobileErrorCodes.ts +27 -0
  198. package/src/types/serverBaseTypes.ts +43 -0
  199. package/src/types/serverExecuteTypes.ts +12 -0
  200. package/src/types/serverModelTypes.ts +74 -0
  201. package/src/types/serverQueryTypes.ts +74 -0
  202. package/src/utils/index.ts +2 -0
  203. package/src/utils/patterns.ts +4 -0
  204. package/src/utils/serverResponseGuards.ts +27 -0
@@ -0,0 +1,12 @@
1
+ import { encode } from '@msgpack/msgpack';
2
+ import { Hex, keccak256 } from 'viem';
3
+ import { MobileSignedInner } from './types';
4
+
5
+ /**
6
+ * Computes the EIP-712 `payloadHash` for a canonicalized inner payload: `keccak256(msgpack(inner))`.
7
+ *
8
+ * @param inner - A canonicalized inner payload; callers must run {@link canonicalizeMobileInner} first.
9
+ */
10
+ export function getMobilePayloadHash(inner: MobileSignedInner): Hex {
11
+ return keccak256(encode(inner));
12
+ }
@@ -0,0 +1,224 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import {
3
+ getNadoEIP712Domain,
4
+ getNadoEIP712PrimaryType,
5
+ getNadoEIP712Types,
6
+ subaccountToHex,
7
+ } from '@nadohq/shared';
8
+ import { createWalletClient, http, recoverTypedDataAddress } from 'viem';
9
+ import { privateKeyToAccount } from 'viem/accounts';
10
+ import { ink } from 'viem/chains';
11
+ import { buildSignedMobileRequest } from './buildSignedMobileRequest';
12
+ import { canonicalizeMobileInner } from './canonicalize';
13
+ import { getMobileNonce } from './nonce';
14
+ import { getMobilePayloadHash } from './payloadHash';
15
+ import { MobileSignedInner } from './types';
16
+
17
+ // Fixed test key — these tests are offline (no network I/O) and only exercise local signing/hashing logic.
18
+ const FIXED_PRIVATE_KEY =
19
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907' as const;
20
+ const FIXED_VERIFYING_ADDR = '0x0000000000000000000000000000000000000001';
21
+
22
+ const PINNED_HASHES = {
23
+ claim_username:
24
+ '0xcfbe9e0a546f43f6d68f86b38cf260ffe079af1915800c0e6df8724ddb6f2dff',
25
+ update_username:
26
+ '0x92c3831d30de8206f9ee9d4cf29ed2a2fb1612a3b818e2e46121fc1d5eb4edb6',
27
+ set_private_mode:
28
+ '0x4d12a06234d751e6ddcc01d8f70836bb5b7e207e573641d1efb5aaf2b0f30d10',
29
+ self_identity:
30
+ '0x10e94c4502cade0a0b4d7469717bcc1d266fc6a3b7635236fa1d3600b58c9954',
31
+ // The three execute hashes below mirror the backend's pinned fixtures in mobile/src/api/types.rs.
32
+ register_expo_token:
33
+ '0x1b9471afc9bde66f9bffd576d3326420e1bade4e16afa3a73ddc65f27155611c',
34
+ unregister_expo_token:
35
+ '0xd131071d0898c97496f3d41dfc05f30a5f82c95e550944c4fba47419ddd0ebf3',
36
+ update_preferences:
37
+ '0x071b8ac6f3d2518267dd238c8c96e15e07e0552aede3ba32383706bb05726f83',
38
+ notification_preferences:
39
+ '0x3694dda0e5732b36eb3b60da6b147ec4e35520fc54529610c69d19d722d4fe97',
40
+ registered_devices:
41
+ '0xe77b42e32d27f054f86d5ed52c9aac7b3eed857eb6209ee883c92f01b84d3334',
42
+ } as const;
43
+
44
+ describe('[mobile-client]: signing (offline)', () => {
45
+ describe('pinned payload hashes', () => {
46
+ it('claim_username', () => {
47
+ const inner: MobileSignedInner = {
48
+ type: 'claim_username',
49
+ display_name: 'Alice.One',
50
+ };
51
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
52
+ expect(hash).toBe(PINNED_HASHES.claim_username);
53
+ });
54
+
55
+ it('update_username', () => {
56
+ const inner: MobileSignedInner = {
57
+ type: 'update_username',
58
+ display_name: 'Alice.Two',
59
+ };
60
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
61
+ expect(hash).toBe(PINNED_HASHES.update_username);
62
+ });
63
+
64
+ it('set_private_mode', () => {
65
+ const inner: MobileSignedInner = {
66
+ type: 'set_private_mode',
67
+ private_mode: true,
68
+ };
69
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
70
+ expect(hash).toBe(PINNED_HASHES.set_private_mode);
71
+ });
72
+
73
+ it('self_identity', () => {
74
+ const inner: MobileSignedInner = { type: 'self_identity' };
75
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
76
+ expect(hash).toBe(PINNED_HASHES.self_identity);
77
+ });
78
+
79
+ it('register_expo_token', () => {
80
+ const inner: MobileSignedInner = {
81
+ type: 'register_expo_token',
82
+ expo_token: 'ExponentPushToken[abcdef1234567890abcdef]',
83
+ platform: 'ios',
84
+ locale: 'en-GB',
85
+ app_version: '1.2.3',
86
+ };
87
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
88
+ expect(hash).toBe(PINNED_HASHES.register_expo_token);
89
+ });
90
+
91
+ it('unregister_expo_token', () => {
92
+ const inner: MobileSignedInner = {
93
+ type: 'unregister_expo_token',
94
+ expo_token: 'ExponentPushToken[abcdef1234567890abcdef]',
95
+ };
96
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
97
+ expect(hash).toBe(PINNED_HASHES.unregister_expo_token);
98
+ });
99
+
100
+ it('update_preferences', () => {
101
+ const inner: MobileSignedInner = {
102
+ type: 'update_preferences',
103
+ preferences: {
104
+ schema_version: 1,
105
+ categories: [{ category: 'order_fill', enabled: true, scopes: [] }],
106
+ },
107
+ };
108
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
109
+ expect(hash).toBe(PINNED_HASHES.update_preferences);
110
+ });
111
+
112
+ it('notification_preferences', () => {
113
+ const inner: MobileSignedInner = { type: 'notification_preferences' };
114
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
115
+ expect(hash).toBe(PINNED_HASHES.notification_preferences);
116
+ });
117
+
118
+ it('registered_devices', () => {
119
+ const inner: MobileSignedInner = { type: 'registered_devices' };
120
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
121
+ expect(hash).toBe(PINNED_HASHES.registered_devices);
122
+ });
123
+ });
124
+
125
+ it('canonicalizes msgpack key order regardless of input key order', () => {
126
+ // Deliberately construct the inner payload with `type` last — canonicalizeMobileInner must rebuild it
127
+ // with `type` first so the msgpack encoding (and therefore the payload hash) is deterministic.
128
+ const outOfOrderInner = {
129
+ private_mode: true,
130
+ type: 'set_private_mode',
131
+ } as MobileSignedInner;
132
+
133
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(outOfOrderInner));
134
+ expect(hash).toBe(PINNED_HASHES.set_private_mode);
135
+ });
136
+
137
+ it('canonicalizes nested preference key order regardless of input key order', () => {
138
+ // Nested preference objects must also be rebuilt in the backend's struct declaration order
139
+ // (schema_version, categories; category, enabled, scopes) for the msgpack hash to be deterministic.
140
+ const outOfOrderInner = {
141
+ preferences: {
142
+ categories: [{ scopes: [], enabled: true, category: 'order_fill' }],
143
+ schema_version: 1,
144
+ },
145
+ type: 'update_preferences',
146
+ } as unknown as MobileSignedInner;
147
+
148
+ const hash = getMobilePayloadHash(canonicalizeMobileInner(outOfOrderInner));
149
+ expect(hash).toBe(PINNED_HASHES.update_preferences);
150
+ });
151
+
152
+ describe('nonce generation', () => {
153
+ it('encodes a ~30s-ahead receive deadline as a bigint', () => {
154
+ const before = Date.now();
155
+ const nonce = getMobileNonce();
156
+ const after = Date.now();
157
+
158
+ expect(typeof nonce).toBe('bigint');
159
+
160
+ const deadlineMs = nonce / 1_000_000n;
161
+ expect(deadlineMs >= BigInt(before + 29_000)).toBe(true);
162
+ expect(deadlineMs <= BigInt(after + 30_000)).toBe(true);
163
+ });
164
+
165
+ it('produces distinct nonces on successive calls', () => {
166
+ const a = getMobileNonce();
167
+ const b = getMobileNonce();
168
+ expect(a).not.toBe(b);
169
+ });
170
+ });
171
+
172
+ it('signs a request that recovers to the expected signer with a correctly flattened body', async () => {
173
+ const account = privateKeyToAccount(FIXED_PRIVATE_KEY);
174
+ const walletClient = createWalletClient({
175
+ account,
176
+ chain: ink,
177
+ transport: http(),
178
+ });
179
+
180
+ const subaccountOwner = account.address;
181
+ const subaccountName = 'default';
182
+ const chainId = ink.id;
183
+ const verifyingAddr = FIXED_VERIFYING_ADDR;
184
+ const nonce = 30_000_000_000_000n;
185
+
186
+ const signedRequest = await buildSignedMobileRequest({
187
+ walletClient,
188
+ subaccountOwner,
189
+ subaccountName,
190
+ chainId,
191
+ verifyingAddr,
192
+ inner: { type: 'set_private_mode', private_mode: true },
193
+ nonce,
194
+ });
195
+
196
+ expect(signedRequest.type).toBe('set_private_mode');
197
+ expect(signedRequest.private_mode).toBe(true);
198
+ expect(signedRequest.nonce).toBe(nonce.toString());
199
+
200
+ const expectedSender = subaccountToHex({
201
+ subaccountOwner,
202
+ subaccountName,
203
+ }).toLowerCase();
204
+ expect(signedRequest.sender).toBe(expectedSender);
205
+
206
+ const payloadHash = getMobilePayloadHash(
207
+ canonicalizeMobileInner({ type: 'set_private_mode', private_mode: true }),
208
+ );
209
+ const recoveredAddress = await recoverTypedDataAddress({
210
+ domain: getNadoEIP712Domain(verifyingAddr, chainId),
211
+ types: getNadoEIP712Types('nado_authentication'),
212
+ primaryType: getNadoEIP712PrimaryType('nado_authentication'),
213
+ message: {
214
+ method: 'mobile:execute_set_private_mode',
215
+ sender: signedRequest.sender,
216
+ payloadHash,
217
+ nonce,
218
+ },
219
+ signature: signedRequest.signature,
220
+ });
221
+
222
+ expect(recoveredAddress.toLowerCase()).toBe(account.address.toLowerCase());
223
+ });
224
+ });
@@ -0,0 +1,71 @@
1
+ import { WalletClientWithAccount } from '@nadohq/shared';
2
+ import { Hex } from 'viem';
3
+ import { MobileSignedRequestParams } from '../types/clientTypes';
4
+ import {
5
+ MobileNotificationPlatform,
6
+ MobileServerNotificationPreferences,
7
+ } from '../types/serverModelTypes';
8
+
9
+ /**
10
+ * Unsigned inner payloads that can be authenticated against the mobile service API.
11
+ *
12
+ * Key order is significant: the payload hash is `keccak256(msgpack(inner))`, and msgpack encodes object keys
13
+ * in insertion order, so every field of every sub-type must be serialized in the exact order the backend
14
+ * declares its structs — `type` first, then the remaining fields — or the hash won't match and signature
15
+ * verification fails. This type only describes the shape; the authoritative runtime key order is enforced by
16
+ * {@link canonicalizeMobileInner}, which is the source of truth. Keep the field order here in sync with it.
17
+ */
18
+ export type MobileSignedInner =
19
+ | { type: 'claim_username'; display_name: string }
20
+ | { type: 'update_username'; display_name: string }
21
+ | { type: 'set_private_mode'; private_mode: boolean }
22
+ | { type: 'self_identity' }
23
+ | {
24
+ type: 'register_expo_token';
25
+ expo_token: string;
26
+ platform: MobileNotificationPlatform;
27
+ // Optional on the wire, but always present (null when unset) so the msgpack payload hash matches the
28
+ // backend, which serializes `Option::None` as nil under the same key.
29
+ locale: string | null;
30
+ app_version: string | null;
31
+ }
32
+ | { type: 'unregister_expo_token'; expo_token: string }
33
+ | {
34
+ type: 'update_preferences';
35
+ preferences: MobileServerNotificationPreferences;
36
+ }
37
+ | { type: 'notification_preferences' }
38
+ | { type: 'registered_devices' };
39
+
40
+ /**
41
+ * Type-specific payload fields for a signed inner request of a given `type` (the inner payload without its
42
+ * `type` tag). Derived from {@link MobileSignedInner} so the two never drift apart.
43
+ */
44
+ export type MobileSignedInnerParams<T extends MobileSignedInner['type']> = Omit<
45
+ Extract<MobileSignedInner, { type: T }>,
46
+ 'type'
47
+ >;
48
+
49
+ /**
50
+ * A signed mobile service API request body: the inner payload flattened with its signature, sender, and
51
+ * nonce (unlike the engine client, which wraps params under a `{ [type]: params }` key).
52
+ */
53
+ export type MobileSignedRequest<
54
+ T extends MobileSignedInner = MobileSignedInner,
55
+ > = T & {
56
+ signature: Hex;
57
+ sender: Hex;
58
+ nonce: string;
59
+ };
60
+
61
+ /**
62
+ * Params for {@link buildSignedMobileRequest}.
63
+ */
64
+ export interface BuildSignedMobileRequestParams extends MobileSignedRequestParams {
65
+ walletClient: WalletClientWithAccount;
66
+ inner: MobileSignedInner;
67
+ /**
68
+ * Overrides the generated nonce; mainly useful for tests that pin the signed payload.
69
+ */
70
+ nonce?: bigint;
71
+ }
@@ -0,0 +1,18 @@
1
+ import { MobileServerFailureResponse } from './serverBaseTypes';
2
+
3
+ /**
4
+ * Error thrown when the mobile service API returns a failure envelope, either as a non-2xx HTTP response
5
+ * or a 2xx response with `status: 'failure'`.
6
+ */
7
+ export class MobileServerFailureError extends Error {
8
+ constructor(
9
+ readonly responseData: MobileServerFailureResponse,
10
+ /**
11
+ * HTTP status code of the response, e.g. `404` for `PROFILE_NOT_FOUND`. Unlike the engine, the mobile
12
+ * backend maps domain failures onto HTTP statuses.
13
+ */
14
+ readonly httpStatus: number,
15
+ ) {
16
+ super(`${responseData.error_code}: ${responseData.error}`);
17
+ }
18
+ }
@@ -0,0 +1,182 @@
1
+ import { SignatureParams, Subaccount } from '@nadohq/shared';
2
+ import { Hex } from 'viem';
3
+ import {
4
+ MobileNotificationCategory,
5
+ MobileNotificationPlatform,
6
+ } from './serverModelTypes';
7
+
8
+ /**
9
+ * A subaccount's claimed identity on the mobile service API.
10
+ *
11
+ * `username` and `displayName` are two representations of the same claimed name: `displayName` is what the
12
+ * user entered (preserving casing, e.g. `Alice.One`), while `username` is the canonical, lowercased handle
13
+ * derived from it (e.g. `alice.one`). `username` is the unique key used for profile lookups
14
+ * ({@link MobilePublicProfile}); `displayName` is only for presentation.
15
+ */
16
+ export interface MobileIdentity {
17
+ subaccount: Hex;
18
+ /**
19
+ * Canonical, lowercased handle derived from {@link MobileIdentity.displayName}, unique per identity and
20
+ * used for profile lookups.
21
+ */
22
+ username: string;
23
+ /**
24
+ * User-facing name as claimed, preserving original casing. Validated against `MOBILE_DISPLAY_NAME_PATTERN`.
25
+ */
26
+ displayName: string;
27
+ privateMode: boolean;
28
+ }
29
+
30
+ /**
31
+ * A subaccount's public profile, as returned by an unsigned profile lookup.
32
+ */
33
+ export interface MobilePublicProfile {
34
+ subaccount: Hex;
35
+ username: string;
36
+ displayName: string;
37
+ }
38
+
39
+ /**
40
+ * Result of a username availability check.
41
+ */
42
+ export interface MobileUsernameAvailability {
43
+ username: string;
44
+ available: boolean;
45
+ }
46
+
47
+ /**
48
+ * Scope limiting a notification category preference. Scopes are part of the wire format but rejected by the
49
+ * backend for the MVP — `scopes` must be empty.
50
+ */
51
+ export type MobileNotificationPreferenceScope =
52
+ | { type: 'subaccount'; subaccount: Hex }
53
+ | { type: 'product'; productId: number };
54
+
55
+ /**
56
+ * A per-category push notification preference.
57
+ */
58
+ export interface MobileNotificationCategoryPreference {
59
+ category: MobileNotificationCategory;
60
+ enabled: boolean;
61
+ scopes: MobileNotificationPreferenceScope[];
62
+ }
63
+
64
+ /**
65
+ * A wallet's push notification preferences. The backend requires exactly one entry per known category and
66
+ * `schemaVersion` of 1.
67
+ */
68
+ export interface MobileNotificationPreferences {
69
+ /** Only `1` is accepted by the backend for the MVP. */
70
+ schemaVersion: 1;
71
+ categories: MobileNotificationCategoryPreference[];
72
+ }
73
+
74
+ /**
75
+ * A device registered for push notifications.
76
+ */
77
+ export interface MobileRegisteredDevice {
78
+ /** Push platform the device registered under. */
79
+ platform: MobileNotificationPlatform;
80
+ /** BCP-47 locale tag reported at registration, or `null` if none was provided. */
81
+ locale: string | null;
82
+ /** App version string reported at registration, or `null` if none was provided. */
83
+ appVersion: string | null;
84
+ /**
85
+ * First 8 hex chars of `keccak256` over the bracket-stripped Expo token — a stable, non-reversible
86
+ * identifier for the device's push token. The raw token is never returned by the backend.
87
+ */
88
+ tokenFingerprintPrefix: string;
89
+ /**
90
+ * Unix timestamp (milliseconds) of the device's last registration refresh.
91
+ */
92
+ lastSeenAt: number;
93
+ }
94
+
95
+ /**
96
+ * Common params for signed requests that authenticate as a given subaccount.
97
+ */
98
+ export interface MobileSignedRequestParams
99
+ extends Subaccount, SignatureParams {}
100
+
101
+ /**
102
+ * Params for {@link MobileClient.getUsernameAvailability}.
103
+ */
104
+ export interface GetMobileUsernameAvailabilityParams {
105
+ displayName: string;
106
+ }
107
+
108
+ /**
109
+ * Params for {@link MobileClient.getPublicProfile}.
110
+ */
111
+ export interface GetMobilePublicProfileParams {
112
+ username: string;
113
+ }
114
+
115
+ /**
116
+ * Params for {@link MobileClient.getSelfIdentity}.
117
+ */
118
+ export type GetMobileSelfIdentityParams = MobileSignedRequestParams;
119
+
120
+ /**
121
+ * Params for {@link MobileClient.claimUsername}.
122
+ */
123
+ export interface MobileClaimUsernameParams extends MobileSignedRequestParams {
124
+ displayName: string;
125
+ }
126
+
127
+ /**
128
+ * Params for {@link MobileClient.updateUsername}.
129
+ */
130
+ export interface MobileUpdateUsernameParams extends MobileSignedRequestParams {
131
+ displayName: string;
132
+ }
133
+
134
+ /**
135
+ * Params for {@link MobileClient.setPrivateMode}.
136
+ */
137
+ export interface MobileSetPrivateModeParams extends MobileSignedRequestParams {
138
+ privateMode: boolean;
139
+ }
140
+
141
+ /**
142
+ * Params for {@link MobileClient.registerExpoToken}.
143
+ */
144
+ export interface MobileRegisterExpoTokenParams extends MobileSignedRequestParams {
145
+ /**
146
+ * Expo push token, e.g. `ExponentPushToken[...]`.
147
+ */
148
+ expoToken: string;
149
+ platform: MobileNotificationPlatform;
150
+ /**
151
+ * BCP-47 locale tag, max 35 chars.
152
+ */
153
+ locale?: string;
154
+ /**
155
+ * App version string, max 64 chars.
156
+ */
157
+ appVersion?: string;
158
+ }
159
+
160
+ /**
161
+ * Params for {@link MobileClient.unregisterExpoToken}.
162
+ */
163
+ export interface MobileUnregisterExpoTokenParams extends MobileSignedRequestParams {
164
+ expoToken: string;
165
+ }
166
+
167
+ /**
168
+ * Params for {@link MobileClient.updateNotificationPreferences}.
169
+ */
170
+ export interface MobileUpdateNotificationPreferencesParams extends MobileSignedRequestParams {
171
+ preferences: MobileNotificationPreferences;
172
+ }
173
+
174
+ /**
175
+ * Params for {@link MobileClient.getNotificationPreferences}.
176
+ */
177
+ export type GetMobileNotificationPreferencesParams = MobileSignedRequestParams;
178
+
179
+ /**
180
+ * Params for {@link MobileClient.getRegisteredDevices}.
181
+ */
182
+ export type GetMobileRegisteredDevicesParams = MobileSignedRequestParams;
@@ -0,0 +1,7 @@
1
+ export * from './clientTypes';
2
+ export * from './mobileErrorCodes';
3
+ export * from './MobileServerFailureError';
4
+ export * from './serverBaseTypes';
5
+ export * from './serverExecuteTypes';
6
+ export * from './serverModelTypes';
7
+ export * from './serverQueryTypes';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Numeric error codes returned by the mobile service API. Identity codes (61xx) and notification codes
3
+ * (62xx) are mobile-specific; the remaining codes are shared cross-service codes from the backend's common
4
+ * error enum.
5
+ */
6
+ export const MOBILE_ERROR_CODES = {
7
+ NOT_IMPLEMENTED: 4001,
8
+ INVALID_SIGNER: 2028,
9
+ SERVICE_UNAVAILABLE: 1006,
10
+ INVALID_DISPLAY_NAME: 6100,
11
+ USERNAME_UNAVAILABLE: 6101,
12
+ IDENTITY_ALREADY_CLAIMED: 6102,
13
+ IDENTITY_REQUIRED: 6103,
14
+ INVALID_IDENTITY_TARGET: 6104,
15
+ STALE_IDENTITY_UPDATE: 6105,
16
+ PROFILE_NOT_FOUND: 6106,
17
+ INVALID_EXPO_TOKEN: 6200,
18
+ INVALID_DEVICE_METADATA: 6201,
19
+ INVALID_PREFERENCES: 6202,
20
+ INTERNAL_ERROR: 5000,
21
+ } as const;
22
+
23
+ /**
24
+ * Union of all known mobile service API error codes.
25
+ */
26
+ export type MobileErrorCode =
27
+ (typeof MOBILE_ERROR_CODES)[keyof typeof MOBILE_ERROR_CODES];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Params for each unsigned `public_query`, keyed by request `type`.
3
+ */
4
+ export interface MobileServerPublicQueryParamsByType {
5
+ username_availability: { display_name: string };
6
+ profile: { username: string };
7
+ }
8
+
9
+ /**
10
+ * Discriminant `type` values for unsigned `public_query` requests.
11
+ */
12
+ export type MobileServerPublicQueryType =
13
+ keyof MobileServerPublicQueryParamsByType;
14
+
15
+ /**
16
+ * Wire `request_type` the backend echoes on failure envelopes, prefixed by route (`public_query_*`,
17
+ * `query_*`, `execute_*`, e.g. `execute_claim_username`). Public-query types are enumerated; signed
18
+ * query/execute names are kept as a `${prefix}_${string}` template to avoid duplicating the signed-type
19
+ * tags declared in `signing.ts`.
20
+ */
21
+ export type MobileServerRequestType =
22
+ | `public_query_${MobileServerPublicQueryType}`
23
+ | `query_${string}`
24
+ | `execute_${string}`;
25
+
26
+ /**
27
+ * Successful mobile service API envelope, discriminated on `status`. Payload fields are inlined alongside
28
+ * `status` rather than nested under a `data` key, matching the mobile backend's wire format.
29
+ */
30
+ export type MobileServerSuccessResponse<TData extends object = object> = {
31
+ status: 'success';
32
+ } & TData;
33
+
34
+ /**
35
+ * Failure envelope returned by any mobile service API route. A response missing this envelope shape
36
+ * (e.g. a malformed body, or a non-JSON response) is a transport-level error, not a domain error.
37
+ */
38
+ export interface MobileServerFailureResponse {
39
+ status: 'failure';
40
+ error: string;
41
+ error_code: number;
42
+ request_type: MobileServerRequestType;
43
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ MobileServerFailureResponse,
3
+ MobileServerSuccessResponse,
4
+ } from './serverBaseTypes';
5
+
6
+ /**
7
+ * Discriminated `execute` result union. Mobile execute routes return no payload beyond the success
8
+ * discriminant, so the success arm is the base success envelope; failures use the uniform failure envelope.
9
+ */
10
+ export type MobileServerExecuteResult =
11
+ | MobileServerSuccessResponse
12
+ | MobileServerFailureResponse;
@@ -0,0 +1,74 @@
1
+ import { Hex } from 'viem';
2
+
3
+ /**
4
+ * Platform of a device registered for push notifications.
5
+ */
6
+ export type MobileNotificationPlatform = 'ios' | 'android';
7
+
8
+ /**
9
+ * Category of push notification.
10
+ */
11
+ export type MobileNotificationCategory =
12
+ | 'order_fill'
13
+ | 'order_update'
14
+ | 'liquidation'
15
+ | 'funding'
16
+ | 'product_listing'
17
+ | 'announcement';
18
+
19
+ /**
20
+ * Server-side public profile shape (snake_case).
21
+ */
22
+ export interface MobileServerProfile {
23
+ subaccount: Hex;
24
+ username: string;
25
+ display_name: string;
26
+ }
27
+
28
+ /**
29
+ * Server-side identity shape (snake_case) returned by the signed `self_identity` query.
30
+ */
31
+ export interface MobileServerIdentity {
32
+ subaccount: Hex;
33
+ username: string;
34
+ display_name: string;
35
+ private_mode: boolean;
36
+ }
37
+
38
+ /**
39
+ * Server-side scope limiting a notification category preference. Scopes are rejected by the backend for the
40
+ * MVP (`scopes` must be empty) but are part of the wire format.
41
+ */
42
+ export type MobileServerNotificationPreferenceScope =
43
+ | { type: 'subaccount'; subaccount: Hex }
44
+ | { type: 'product'; product_id: number };
45
+
46
+ /**
47
+ * Server-side per-category notification preference (snake_case).
48
+ */
49
+ export interface MobileServerNotificationCategoryPreference {
50
+ category: MobileNotificationCategory;
51
+ enabled: boolean;
52
+ scopes: MobileServerNotificationPreferenceScope[];
53
+ }
54
+
55
+ /**
56
+ * Server-side notification preferences shape (snake_case), used in both the `update_preferences` execute and
57
+ * the `notification_preferences` query response.
58
+ */
59
+ export interface MobileServerNotificationPreferences {
60
+ /** Only `1` is accepted by the backend for the MVP. */
61
+ schema_version: 1;
62
+ categories: MobileServerNotificationCategoryPreference[];
63
+ }
64
+
65
+ /**
66
+ * Server-side registered push device shape (snake_case).
67
+ */
68
+ export interface MobileServerRegisteredDevice {
69
+ platform: MobileNotificationPlatform;
70
+ locale: string | null;
71
+ app_version: string | null;
72
+ token_fingerprint_prefix: string;
73
+ last_seen_at: number;
74
+ }