@nadohq/mobile-client 0.36.0 → 0.38.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.
- package/README.md +19 -6
- package/dist/{MobileClient-D5TNTnLx.cjs → MobileClient-wjTrqP9z.cjs} +75 -12
- package/dist/MobileClient-wjTrqP9z.cjs.map +1 -0
- package/dist/MobileClient.cjs +1 -1
- package/dist/MobileClient.d.cts +62 -16
- package/dist/MobileClient.d.cts.map +1 -1
- package/dist/MobileClient.d.ts +62 -16
- package/dist/MobileClient.d.ts.map +1 -1
- package/dist/MobileClient.js +75 -12
- package/dist/MobileClient.js.map +1 -1
- package/dist/{MobileServerFailureError-CPFjwUBx.d.cts → MobileServerFailureError-BvQYrxjx.d.cts} +3 -3
- package/dist/{MobileServerFailureError-CPFjwUBx.d.cts.map → MobileServerFailureError-BvQYrxjx.d.cts.map} +1 -1
- package/dist/{MobileServerFailureError-Terv2Wzy.d.ts → MobileServerFailureError-CrNpL_L4.d.ts} +3 -3
- package/dist/{MobileServerFailureError-Terv2Wzy.d.ts.map → MobileServerFailureError-CrNpL_L4.d.ts.map} +1 -1
- package/dist/{buildSignedMobileRequest-BScnCGPF.d.ts → buildSignedMobileRequest-BqLRFLKV.d.ts} +2 -2
- package/dist/{buildSignedMobileRequest-BScnCGPF.d.ts.map → buildSignedMobileRequest-BqLRFLKV.d.ts.map} +1 -1
- package/dist/{buildSignedMobileRequest-CXfnhc-4.d.cts → buildSignedMobileRequest-DkkwEqIu.d.cts} +2 -2
- package/dist/{buildSignedMobileRequest-CXfnhc-4.d.cts.map → buildSignedMobileRequest-DkkwEqIu.d.cts.map} +1 -1
- package/dist/{canonicalize-CQ-pMYIt.d.ts → canonicalize-BRhCpYm1.d.ts} +2 -2
- package/dist/canonicalize-BRhCpYm1.d.ts.map +1 -0
- package/dist/{canonicalize-hzhboB-C.d.cts → canonicalize-DtpaaA2B.d.cts} +2 -2
- package/dist/canonicalize-DtpaaA2B.d.cts.map +1 -0
- package/dist/{clientTypes-BwakovWk.d.cts → clientTypes-CB_itU4Z.d.ts} +147 -14
- package/dist/clientTypes-CB_itU4Z.d.ts.map +1 -0
- package/dist/{clientTypes-D-o1_2Yc.d.ts → clientTypes-CceWsQzi.d.cts} +147 -14
- package/dist/clientTypes-CceWsQzi.d.cts.map +1 -0
- package/dist/dataMappers.cjs +79 -6
- package/dist/dataMappers.cjs.map +1 -1
- package/dist/dataMappers.d.cts +27 -5
- package/dist/dataMappers.d.cts.map +1 -1
- package/dist/dataMappers.d.ts +27 -5
- package/dist/dataMappers.d.ts.map +1 -1
- package/dist/dataMappers.js +75 -7
- package/dist/dataMappers.js.map +1 -1
- package/dist/{eip712MethodByType-3aiJkrHj.d.cts → eip712MethodByType-1xUkvTIp.d.cts} +2 -2
- package/dist/eip712MethodByType-1xUkvTIp.d.cts.map +1 -0
- package/dist/{eip712MethodByType-zNvZukU7.d.ts → eip712MethodByType-CrqBJIrp.d.ts} +2 -2
- package/dist/eip712MethodByType-CrqBJIrp.d.ts.map +1 -0
- package/dist/follow-Bh3UmXMc.d.cts +23 -0
- package/dist/follow-Bh3UmXMc.d.cts.map +1 -0
- package/dist/follow-DjkQm1vR.d.ts +23 -0
- package/dist/follow-DjkQm1vR.d.ts.map +1 -0
- package/dist/index.cjs +6 -1
- package/dist/index.d.cts +17 -16
- package/dist/index.d.ts +17 -16
- package/dist/index.js +2 -1
- package/dist/{mobileErrorCodes-CuQ5lyBV.d.ts → mobileErrorCodes-B3eR6t4b.d.ts} +8 -2
- package/dist/mobileErrorCodes-B3eR6t4b.d.ts.map +1 -0
- package/dist/{mobileErrorCodes-D_SyBm0k.d.cts → mobileErrorCodes-DkzBW4v3.d.cts} +8 -2
- package/dist/mobileErrorCodes-DkzBW4v3.d.cts.map +1 -0
- package/dist/{patterns-B5gi7OfV.d.ts → patterns-C-aUAnKl.d.ts} +1 -1
- package/dist/patterns-C-aUAnKl.d.ts.map +1 -0
- package/dist/{patterns-Db6fgz8i.d.cts → patterns-DS8P5LH0.d.cts} +1 -1
- package/dist/patterns-DS8P5LH0.d.cts.map +1 -0
- package/dist/{payloadHash-DVkEt-Rg.d.cts → payloadHash-CIo8Hkh6.d.cts} +2 -2
- package/dist/payloadHash-CIo8Hkh6.d.cts.map +1 -0
- package/dist/{payloadHash-BxL26uwq.d.ts → payloadHash-Cv4WwZCZ.d.ts} +2 -2
- package/dist/payloadHash-Cv4WwZCZ.d.ts.map +1 -0
- package/dist/{serverBaseTypes-B_KHFDab.d.ts → serverBaseTypes-C2JGib--.d.ts} +2 -2
- package/dist/{serverBaseTypes-B_KHFDab.d.ts.map → serverBaseTypes-C2JGib--.d.ts.map} +1 -1
- package/dist/{serverBaseTypes-C8KjE9Fm.d.cts → serverBaseTypes-YkCmgmt1.d.cts} +2 -2
- package/dist/{serverBaseTypes-C8KjE9Fm.d.cts.map → serverBaseTypes-YkCmgmt1.d.cts.map} +1 -1
- package/dist/serverExecuteTypes-C2NoNEwu.d.ts +50 -0
- package/dist/serverExecuteTypes-C2NoNEwu.d.ts.map +1 -0
- package/dist/serverExecuteTypes-DPZbs9ym.d.cts +50 -0
- package/dist/serverExecuteTypes-DPZbs9ym.d.cts.map +1 -0
- package/dist/{serverModelTypes-_cX99seo.d.ts → serverModelTypes-CDcRlRtw.d.ts} +57 -10
- package/dist/serverModelTypes-CDcRlRtw.d.ts.map +1 -0
- package/dist/{serverModelTypes-CwgibK-l.d.cts → serverModelTypes-CUzqFA6x.d.cts} +57 -10
- package/dist/serverModelTypes-CUzqFA6x.d.cts.map +1 -0
- package/dist/{serverQueryTypes-DwE1iJ9f.d.cts → serverQueryTypes-B0xI1G39.d.ts} +21 -9
- package/dist/serverQueryTypes-B0xI1G39.d.ts.map +1 -0
- package/dist/{serverQueryTypes-NIrPm2Sl.d.ts → serverQueryTypes-RdcIqRnM.d.cts} +21 -9
- package/dist/serverQueryTypes-RdcIqRnM.d.cts.map +1 -0
- package/dist/serverRequestTypes-Ddws3yXc.d.cts +116 -0
- package/dist/serverRequestTypes-Ddws3yXc.d.cts.map +1 -0
- package/dist/serverRequestTypes-Dv9NR-hQ.d.ts +116 -0
- package/dist/serverRequestTypes-Dv9NR-hQ.d.ts.map +1 -0
- package/dist/{serverResponseGuards-Cep5s7j5.d.ts → serverResponseGuards-BjIzucYt.d.ts} +2 -2
- package/dist/{serverResponseGuards-Cep5s7j5.d.ts.map → serverResponseGuards-BjIzucYt.d.ts.map} +1 -1
- package/dist/{serverResponseGuards-Dxt9_aGh.d.cts → serverResponseGuards-Dg9WbqBA.d.cts} +2 -2
- package/dist/{serverResponseGuards-Dxt9_aGh.d.cts.map → serverResponseGuards-Dg9WbqBA.d.cts.map} +1 -1
- package/dist/signing/buildSignedMobileRequest.cjs +1 -1
- package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
- package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
- package/dist/signing/canonicalize.cjs +5 -0
- package/dist/signing/canonicalize.cjs.map +1 -1
- package/dist/signing/canonicalize.d.cts +1 -1
- package/dist/signing/canonicalize.d.ts +1 -1
- package/dist/signing/canonicalize.js +5 -0
- package/dist/signing/canonicalize.js.map +1 -1
- package/dist/signing/eip712MethodByType.cjs +2 -1
- package/dist/signing/eip712MethodByType.cjs.map +1 -1
- package/dist/signing/eip712MethodByType.d.cts +1 -1
- package/dist/signing/eip712MethodByType.d.ts +1 -1
- package/dist/signing/eip712MethodByType.js +2 -1
- package/dist/signing/eip712MethodByType.js.map +1 -1
- package/dist/signing/index.d.cts +6 -6
- package/dist/signing/index.d.ts +6 -6
- package/dist/signing/payloadHash.cjs +1 -1
- package/dist/signing/payloadHash.d.cts +1 -1
- package/dist/signing/payloadHash.d.ts +1 -1
- package/dist/signing/types.d.cts +2 -2
- package/dist/signing/types.d.ts +2 -2
- package/dist/types/MobileServerFailureError.cjs +1 -1
- package/dist/types/MobileServerFailureError.d.cts +1 -1
- package/dist/types/MobileServerFailureError.d.ts +1 -1
- package/dist/types/clientTypes.d.cts +2 -2
- package/dist/types/clientTypes.d.ts +2 -2
- package/dist/types/index.d.cts +9 -9
- package/dist/types/index.d.ts +9 -9
- package/dist/types/mobileErrorCodes.cjs +13 -1
- package/dist/types/mobileErrorCodes.cjs.map +1 -1
- package/dist/types/mobileErrorCodes.d.cts +1 -1
- package/dist/types/mobileErrorCodes.d.ts +1 -1
- package/dist/types/mobileErrorCodes.js +12 -0
- package/dist/types/mobileErrorCodes.js.map +1 -1
- package/dist/types/serverBaseTypes.d.cts +1 -1
- package/dist/types/serverBaseTypes.d.ts +1 -1
- package/dist/types/serverExecuteTypes.d.cts +2 -2
- package/dist/types/serverExecuteTypes.d.ts +2 -2
- package/dist/types/serverModelTypes.cjs.map +1 -1
- package/dist/types/serverModelTypes.d.cts +2 -2
- package/dist/types/serverModelTypes.d.ts +2 -2
- package/dist/types/serverModelTypes.js.map +1 -1
- package/dist/types/serverQueryTypes.d.cts +2 -2
- package/dist/types/serverQueryTypes.d.ts +2 -2
- package/dist/types/serverRequestTypes.d.cts +2 -2
- package/dist/types/serverRequestTypes.d.ts +2 -2
- package/dist/{types-47IPsDyA.d.cts → types-BIIvIzG2.d.ts} +20 -7
- package/dist/types-BIIvIzG2.d.ts.map +1 -0
- package/dist/{types-D5kFJzEX.d.ts → types-UmYaeq1C.d.cts} +20 -7
- package/dist/types-UmYaeq1C.d.cts.map +1 -0
- package/dist/utils/follow.cjs +28 -0
- package/dist/utils/follow.cjs.map +1 -0
- package/dist/utils/follow.d.cts +2 -0
- package/dist/utils/follow.d.ts +2 -0
- package/dist/utils/follow.js +24 -0
- package/dist/utils/follow.js.map +1 -0
- package/dist/utils/index.cjs +5 -0
- package/dist/utils/index.d.cts +4 -3
- package/dist/utils/index.d.ts +4 -3
- package/dist/utils/index.js +2 -1
- package/dist/utils/patterns.d.cts +1 -1
- package/dist/utils/patterns.d.ts +1 -1
- package/dist/utils/serverResponseGuards.d.cts +1 -1
- package/dist/utils/serverResponseGuards.d.ts +1 -1
- package/package.json +3 -3
- package/src/MobileClient.ts +117 -21
- package/src/dataMappers.ts +109 -5
- package/src/signing/canonicalize.ts +6 -0
- package/src/signing/eip712MethodByType.ts +1 -0
- package/src/signing/signing.test.ts +32 -4
- package/src/signing/types.ts +18 -1
- package/src/types/clientTypes.ts +152 -11
- package/src/types/mobileErrorCodes.ts +12 -0
- package/src/types/serverExecuteTypes.ts +40 -2
- package/src/types/serverModelTypes.ts +58 -8
- package/src/types/serverQueryTypes.ts +18 -4
- package/src/types/serverRequestTypes.ts +50 -4
- package/src/utils/follow.ts +22 -0
- package/src/utils/index.ts +1 -0
- package/dist/MobileClient-D5TNTnLx.cjs.map +0 -1
- package/dist/canonicalize-CQ-pMYIt.d.ts.map +0 -1
- package/dist/canonicalize-hzhboB-C.d.cts.map +0 -1
- package/dist/clientTypes-BwakovWk.d.cts.map +0 -1
- package/dist/clientTypes-D-o1_2Yc.d.ts.map +0 -1
- package/dist/eip712MethodByType-3aiJkrHj.d.cts.map +0 -1
- package/dist/eip712MethodByType-zNvZukU7.d.ts.map +0 -1
- package/dist/mobileErrorCodes-CuQ5lyBV.d.ts.map +0 -1
- package/dist/mobileErrorCodes-D_SyBm0k.d.cts.map +0 -1
- package/dist/patterns-B5gi7OfV.d.ts.map +0 -1
- package/dist/patterns-Db6fgz8i.d.cts.map +0 -1
- package/dist/payloadHash-BxL26uwq.d.ts.map +0 -1
- package/dist/payloadHash-DVkEt-Rg.d.cts.map +0 -1
- package/dist/serverExecuteTypes-Cb5v7y0A.d.cts +0 -19
- package/dist/serverExecuteTypes-Cb5v7y0A.d.cts.map +0 -1
- package/dist/serverExecuteTypes-GFjZj_DC.d.ts +0 -19
- package/dist/serverExecuteTypes-GFjZj_DC.d.ts.map +0 -1
- package/dist/serverModelTypes-CwgibK-l.d.cts.map +0 -1
- package/dist/serverModelTypes-_cX99seo.d.ts.map +0 -1
- package/dist/serverQueryTypes-DwE1iJ9f.d.cts.map +0 -1
- package/dist/serverQueryTypes-NIrPm2Sl.d.ts.map +0 -1
- package/dist/serverRequestTypes-516zXnQ6.d.ts +0 -72
- package/dist/serverRequestTypes-516zXnQ6.d.ts.map +0 -1
- package/dist/serverRequestTypes-GAtVYIda.d.cts +0 -72
- package/dist/serverRequestTypes-GAtVYIda.d.cts.map +0 -1
- package/dist/types-47IPsDyA.d.cts.map +0 -1
- package/dist/types-D5kFJzEX.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nadohq/mobile-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "HTTP client for the Nado mobile service API — username claims, profiles, privacy settings, and push notification devices & preferences",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"types": "./dist/index.d.ts",
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@msgpack/msgpack": "3.1.3",
|
|
45
|
-
"@nadohq/shared": "^0.
|
|
45
|
+
"@nadohq/shared": "^0.38.0",
|
|
46
46
|
"axios": "1.16.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"bignumber.js": "11.1.1",
|
|
55
55
|
"viem": "2.52.0"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "1a536b7f05b1b042ef257b290cdb8c83f48f4dd0"
|
|
58
58
|
}
|
package/src/MobileClient.ts
CHANGED
|
@@ -7,28 +7,38 @@ import {
|
|
|
7
7
|
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
|
8
8
|
import {
|
|
9
9
|
mapMobileFeedPage,
|
|
10
|
+
mapMobileFollowListPage,
|
|
11
|
+
mapMobileFollowListParamsToServer,
|
|
12
|
+
mapMobileFollowMutationResult,
|
|
10
13
|
mapMobileNotificationPreferences,
|
|
11
14
|
mapMobileNotificationPreferencesToServer,
|
|
15
|
+
mapMobileProfilesIncludeToServer,
|
|
12
16
|
mapMobilePublicProfile,
|
|
13
17
|
mapMobileRegisteredWallet,
|
|
14
18
|
} from './dataMappers';
|
|
15
19
|
import {
|
|
16
20
|
buildSignedMobileRequest,
|
|
17
21
|
MobileSignedInner,
|
|
22
|
+
MobileSignedInnerByType,
|
|
18
23
|
MobileSignedInnerParams,
|
|
19
24
|
MobileSignedRequest,
|
|
20
25
|
} from './signing';
|
|
21
26
|
import {
|
|
22
27
|
GetMobileFeedParams,
|
|
28
|
+
GetMobileFollowersParams,
|
|
29
|
+
GetMobileFollowingParams,
|
|
23
30
|
GetMobileNotificationPreferencesParams,
|
|
24
|
-
|
|
31
|
+
GetMobileProfilesParams,
|
|
25
32
|
GetMobileRegisteredWalletParams,
|
|
26
33
|
GetMobileUsernameAvailabilityParams,
|
|
27
34
|
MobileFeedPage,
|
|
35
|
+
MobileFollowListPage,
|
|
36
|
+
MobileFollowMutationResult,
|
|
28
37
|
MobileNotificationPreferences,
|
|
29
38
|
MobilePublicProfile,
|
|
30
39
|
MobileRegisteredWallet,
|
|
31
40
|
MobileRegisterExpoTokenParams,
|
|
41
|
+
MobileSetFollowParams,
|
|
32
42
|
MobileSetPrivateModeParams,
|
|
33
43
|
MobileSetUsernameParams,
|
|
34
44
|
MobileSignedRequestParams,
|
|
@@ -40,6 +50,7 @@ import { MobileServerFailureError } from './types/MobileServerFailureError';
|
|
|
40
50
|
import { MobileServerSuccessResponse } from './types/serverBaseTypes';
|
|
41
51
|
import {
|
|
42
52
|
MobileServerExecuteResult,
|
|
53
|
+
MobileServerExecuteSuccessResponse,
|
|
43
54
|
MobileServerPublicExecuteRequest,
|
|
44
55
|
} from './types/serverExecuteTypes';
|
|
45
56
|
import {
|
|
@@ -47,6 +58,7 @@ import {
|
|
|
47
58
|
MobileServerPublicQuerySuccessResponse,
|
|
48
59
|
} from './types/serverQueryTypes';
|
|
49
60
|
import {
|
|
61
|
+
MobileServerExecuteRequestType,
|
|
50
62
|
MobileServerPublicQueryRequestByType,
|
|
51
63
|
MobileServerPublicQueryRequestType,
|
|
52
64
|
} from './types/serverRequestTypes';
|
|
@@ -78,8 +90,8 @@ export interface MobileClientOpts {
|
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
81
|
-
* Client for the Nado mobile service API: usernames, public profile lookups, the global trade feed,
|
|
82
|
-
* settings, and push notification device/preference management.
|
|
93
|
+
* Client for the Nado mobile service API: usernames, public profile lookups, the global trade feed, the
|
|
94
|
+
* follower/following graph, privacy settings, and push notification device/preference management.
|
|
83
95
|
*/
|
|
84
96
|
export class MobileClient {
|
|
85
97
|
readonly opts: MobileClientOpts;
|
|
@@ -125,22 +137,73 @@ export class MobileClient {
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
/**
|
|
128
|
-
* Looks up
|
|
129
|
-
*
|
|
130
|
-
*
|
|
140
|
+
* Looks up public profiles, optionally with follower totals and a follow summary. Returns one profile per
|
|
141
|
+
* requested subaccount, in the requested order, so callers can correlate positionally. Every non-isolated
|
|
142
|
+
* subaccount resolves, with `username` and `displayName` `null` until a username is claimed. Private Mode
|
|
143
|
+
* does not hide the profile itself, only the account's activity.
|
|
131
144
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
145
|
+
* This is the only profile route, so use it for a single subaccount too. It is unsigned, which means
|
|
146
|
+
* `include.followSummary.viewAs` is an unauthenticated claim.
|
|
147
|
+
*
|
|
148
|
+
* @throws {MobileServerFailureError} With error code `INVALID_PROFILES_REQUEST` if `subaccounts` is empty,
|
|
149
|
+
* holds duplicates, or exceeds `MOBILE_PROFILES_MAX_BATCH_SIZE` (25), or `PROFILE_NOT_FOUND` if any
|
|
150
|
+
* requested subaccount — or the follow summary's `viewAs` — is in the engine-created isolated namespace.
|
|
151
|
+
* One isolated member fails the whole batch rather than being omitted.
|
|
152
|
+
*/
|
|
153
|
+
async getProfiles(
|
|
154
|
+
params: GetMobileProfilesParams,
|
|
155
|
+
): Promise<MobilePublicProfile[]> {
|
|
156
|
+
const body: MobileServerPublicQueryRequest<'profiles'> = {
|
|
157
|
+
type: 'profiles',
|
|
158
|
+
subaccounts: params.subaccounts.map(subaccountToHex),
|
|
159
|
+
include: params.include
|
|
160
|
+
? mapMobileProfilesIncludeToServer(params.include)
|
|
161
|
+
: undefined,
|
|
162
|
+
};
|
|
163
|
+
const data = await this.publicQuery(body);
|
|
164
|
+
return data.profiles.map(mapMobilePublicProfile);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Fetches a page of the accounts that follow the given Profile.
|
|
169
|
+
*
|
|
170
|
+
* Ordering depends on `viewAs`. With it, accounts the Viewer already follows come first, unfamiliar
|
|
171
|
+
* accounts second, and every row carries an `isFollowing`. Without it, the page is ordered by the listed
|
|
172
|
+
* relationship alone and no row carries an `isFollowing`. In both cases the newest listed relationship
|
|
173
|
+
* comes first within a group, with a bytes32 tie-break. The grouping is evaluated per request against the
|
|
174
|
+
* live graph, so following an account from inside the list moves it between groups on the next page —
|
|
175
|
+
* deduplicate by subaccount and do not re-sort locally.
|
|
176
|
+
*
|
|
177
|
+
* @throws {MobileServerFailureError} With error code `PROFILE_NOT_FOUND` if the viewed Profile or `viewAs`
|
|
178
|
+
* is isolated, `INVALID_FOLLOW_CURSOR` if the cursor is malformed or was issued for a different `viewAs`,
|
|
179
|
+
* Profile, or list direction (discard it and restart from the first page), or `INVALID_FOLLOW_LIMIT` if
|
|
180
|
+
* `limit` is outside 1–50.
|
|
181
|
+
*/
|
|
182
|
+
async getFollowers(
|
|
183
|
+
params: GetMobileFollowersParams,
|
|
184
|
+
): Promise<MobileFollowListPage> {
|
|
185
|
+
const body: MobileServerPublicQueryRequest<'followers'> = {
|
|
186
|
+
type: 'followers',
|
|
187
|
+
...mapMobileFollowListParamsToServer(params),
|
|
188
|
+
};
|
|
189
|
+
const data = await this.publicQuery(body);
|
|
190
|
+
return mapMobileFollowListPage(data);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Fetches a page of the accounts the given Profile follows. Ordering, cursor rules, and errors match
|
|
195
|
+
* {@link MobileClient.getFollowers}. When `viewAs` reads its own Following list every row is familiar, so
|
|
196
|
+
* every `isFollowing` is `true`.
|
|
134
197
|
*/
|
|
135
|
-
async
|
|
136
|
-
params:
|
|
137
|
-
): Promise<
|
|
138
|
-
const body: MobileServerPublicQueryRequest<'
|
|
139
|
-
type: '
|
|
140
|
-
|
|
198
|
+
async getFollowing(
|
|
199
|
+
params: GetMobileFollowingParams,
|
|
200
|
+
): Promise<MobileFollowListPage> {
|
|
201
|
+
const body: MobileServerPublicQueryRequest<'following'> = {
|
|
202
|
+
type: 'following',
|
|
203
|
+
...mapMobileFollowListParamsToServer(params),
|
|
141
204
|
};
|
|
142
205
|
const data = await this.publicQuery(body);
|
|
143
|
-
return
|
|
206
|
+
return mapMobileFollowListPage(data);
|
|
144
207
|
}
|
|
145
208
|
|
|
146
209
|
/**
|
|
@@ -314,15 +377,43 @@ export class MobileClient {
|
|
|
314
377
|
return this.execute(signedRequest);
|
|
315
378
|
}
|
|
316
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Sets whether the signing subaccount follows a Profile. Both directions are idempotent: following again
|
|
382
|
+
* keeps the original relationship creation time, so it does not move the account in recency order, and
|
|
383
|
+
* unfollowing a relationship that does not exist succeeds with `isFollowing: false`. Unfollowing deletes
|
|
384
|
+
* the relationship rather than deactivating it, so a later follow gets a new creation time and moves the
|
|
385
|
+
* account to the front of recency order.
|
|
386
|
+
*
|
|
387
|
+
* Only `isFollowing: true` is checked against the canonical Query DB subaccount index, so unfollowing still
|
|
388
|
+
* works while that index lags.
|
|
389
|
+
*
|
|
390
|
+
* @throws {MobileServerFailureError} With error code `INVALID_FOLLOW_TARGET` if the target is the sender or
|
|
391
|
+
* either party is isolated, `FOLLOWER_NOT_ELIGIBLE` if the sender is not yet in the canonical Query DB
|
|
392
|
+
* subaccount index, or `FOLLOWING_NOT_FOUND` if the target is not. Both index errors are transient for an
|
|
393
|
+
* account that is being recorded, so they are retryable after catch-up — unlike `INVALID_FOLLOW_TARGET`.
|
|
394
|
+
*/
|
|
395
|
+
async setFollow(
|
|
396
|
+
params: MobileSetFollowParams,
|
|
397
|
+
): Promise<MobileFollowMutationResult> {
|
|
398
|
+
const signedRequest = await this.getSignedRequest('set_follow', params, {
|
|
399
|
+
subaccount: subaccountToHex(params.target),
|
|
400
|
+
is_following: params.isFollowing,
|
|
401
|
+
});
|
|
402
|
+
const data = await this.execute(signedRequest);
|
|
403
|
+
return mapMobileFollowMutationResult(data);
|
|
404
|
+
}
|
|
405
|
+
|
|
317
406
|
/*
|
|
318
407
|
Base Fns
|
|
319
408
|
*/
|
|
320
409
|
|
|
410
|
+
// Returns the request narrowed to `type` so `execute` can infer its response from the request it is given,
|
|
411
|
+
// instead of being told the type a second time at the call site.
|
|
321
412
|
protected async getSignedRequest<T extends MobileSignedInner['type']>(
|
|
322
413
|
type: T,
|
|
323
414
|
params: MobileSignedRequestParams,
|
|
324
415
|
innerParams: MobileSignedInnerParams<T>,
|
|
325
|
-
): Promise<MobileSignedRequest
|
|
416
|
+
): Promise<MobileSignedRequest<MobileSignedInnerByType<T>>> {
|
|
326
417
|
// Use the linked signer if provided, otherwise use the default signer provided to the client
|
|
327
418
|
const walletClient =
|
|
328
419
|
this.opts.linkedSignerWalletClient ?? this.opts.walletClient;
|
|
@@ -331,7 +422,10 @@ export class MobileClient {
|
|
|
331
422
|
throw new WalletNotProvidedError();
|
|
332
423
|
}
|
|
333
424
|
|
|
334
|
-
const inner = {
|
|
425
|
+
const inner = {
|
|
426
|
+
type,
|
|
427
|
+
...innerParams,
|
|
428
|
+
} as unknown as MobileSignedInnerByType<T>;
|
|
335
429
|
return buildSignedMobileRequest({ ...params, walletClient, inner });
|
|
336
430
|
}
|
|
337
431
|
|
|
@@ -367,9 +461,11 @@ export class MobileClient {
|
|
|
367
461
|
return response.data as MobileServerSuccessResponse;
|
|
368
462
|
}
|
|
369
463
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
464
|
+
// `T` is inferred from the body's `type` rather than declared by the caller, for the same reason
|
|
465
|
+
// `publicQuery` spells its body out as an intersection: an indexed access is not an inference site.
|
|
466
|
+
protected async execute<T extends MobileServerExecuteRequestType>(
|
|
467
|
+
body: MobileSignedRequest & { type: T },
|
|
468
|
+
): Promise<MobileServerExecuteSuccessResponse<T>> {
|
|
373
469
|
const response = await this.axiosInstance.post<MobileServerExecuteResult>(
|
|
374
470
|
`${this.opts.url}/mobile/execute`,
|
|
375
471
|
body,
|
|
@@ -379,7 +475,7 @@ export class MobileClient {
|
|
|
379
475
|
this.checkServerStatus(response);
|
|
380
476
|
|
|
381
477
|
// checkServerStatus catches the failure result and throws the error, so the cast to the success response is acceptable here
|
|
382
|
-
return response.data as
|
|
478
|
+
return response.data as MobileServerExecuteSuccessResponse<T>;
|
|
383
479
|
}
|
|
384
480
|
|
|
385
481
|
/**
|
package/src/dataMappers.ts
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
|
+
import { subaccountToHex } from '@nadohq/shared';
|
|
1
2
|
import {
|
|
3
|
+
GetMobileFollowListParams,
|
|
2
4
|
MobileFeedPage,
|
|
3
5
|
MobileFeedTrade,
|
|
6
|
+
MobileFollowListPage,
|
|
7
|
+
MobileFollowMutationResult,
|
|
8
|
+
MobileFollowSummary,
|
|
9
|
+
MobileIdentitySummary,
|
|
4
10
|
MobileNotificationPreferenceScope,
|
|
5
11
|
MobileNotificationPreferences,
|
|
12
|
+
MobileProfilesInclude,
|
|
6
13
|
MobilePublicProfile,
|
|
7
14
|
MobileRegisteredWallet,
|
|
8
15
|
} from './types/clientTypes';
|
|
16
|
+
import { MobileServerFollowMutationResponse } from './types/serverExecuteTypes';
|
|
9
17
|
import {
|
|
10
18
|
MobileServerFeedTrade,
|
|
19
|
+
MobileServerFollowSummary,
|
|
20
|
+
MobileServerIdentitySummary,
|
|
11
21
|
MobileServerNotificationPreferenceScope,
|
|
12
22
|
MobileServerNotificationPreferences,
|
|
13
23
|
MobileServerProfile,
|
|
14
24
|
} from './types/serverModelTypes';
|
|
15
25
|
import {
|
|
16
26
|
MobileServerFeedResponse,
|
|
27
|
+
MobileServerFollowListResponse,
|
|
17
28
|
MobileServerRegisteredWalletResponse,
|
|
18
29
|
} from './types/serverQueryTypes';
|
|
30
|
+
import {
|
|
31
|
+
MobileServerFollowListRequest,
|
|
32
|
+
MobileServerProfilesInclude,
|
|
33
|
+
} from './types/serverRequestTypes';
|
|
19
34
|
|
|
20
35
|
/**
|
|
21
|
-
* Maps a server-side public profile (snake_case) to its client-side (camelCase) representation.
|
|
36
|
+
* Maps a server-side public profile (snake_case) to its client-side (camelCase) representation. The three
|
|
37
|
+
* include-gated fields stay `undefined` when the backend omitted them, rather than being coerced to `null`
|
|
38
|
+
* or zero, so callers can tell "not requested" from a real value.
|
|
22
39
|
*/
|
|
23
40
|
export function mapMobilePublicProfile(
|
|
24
41
|
server: MobileServerProfile,
|
|
@@ -28,6 +45,67 @@ export function mapMobilePublicProfile(
|
|
|
28
45
|
username: server.username,
|
|
29
46
|
displayName: server.display_name,
|
|
30
47
|
privateMode: server.private_mode,
|
|
48
|
+
followerCount: server.follower_count,
|
|
49
|
+
followingCount: server.following_count,
|
|
50
|
+
followSummary: server.follow_summary
|
|
51
|
+
? mapMobileFollowSummary(server.follow_summary)
|
|
52
|
+
: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maps a server-side identity summary (snake_case) to its client-side (camelCase) representation.
|
|
58
|
+
*/
|
|
59
|
+
function mapMobileIdentitySummary(
|
|
60
|
+
server: MobileServerIdentitySummary,
|
|
61
|
+
): MobileIdentitySummary {
|
|
62
|
+
return {
|
|
63
|
+
subaccount: server.subaccount,
|
|
64
|
+
username: server.username,
|
|
65
|
+
displayName: server.display_name,
|
|
66
|
+
avatarUrl: server.avatar_url,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Maps a server-side follow summary to its client-side representation.
|
|
72
|
+
*/
|
|
73
|
+
function mapMobileFollowSummary(
|
|
74
|
+
server: MobileServerFollowSummary,
|
|
75
|
+
): MobileFollowSummary {
|
|
76
|
+
return {
|
|
77
|
+
isFollowing: server.is_following,
|
|
78
|
+
followedByCount: server.followed_by_count,
|
|
79
|
+
followedBy: server.followed_by.map(mapMobileIdentitySummary),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Maps a server-side Followers or Following response to a client-side {@link MobileFollowListPage}. Rows
|
|
85
|
+
* keep `isFollowing` absent when the request named no `viewAs`, rather than defaulting it to `false`.
|
|
86
|
+
*/
|
|
87
|
+
export function mapMobileFollowListPage(
|
|
88
|
+
server: MobileServerFollowListResponse,
|
|
89
|
+
): MobileFollowListPage {
|
|
90
|
+
return {
|
|
91
|
+
accounts: server.accounts.map((account) => ({
|
|
92
|
+
identity: mapMobileIdentitySummary(account.identity),
|
|
93
|
+
isFollowing: account.is_following,
|
|
94
|
+
followerCount: account.follower_count,
|
|
95
|
+
})),
|
|
96
|
+
nextCursor: server.next_cursor,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Maps a server-side follow or unfollow response to its client-side representation.
|
|
102
|
+
*/
|
|
103
|
+
export function mapMobileFollowMutationResult(
|
|
104
|
+
server: MobileServerFollowMutationResponse,
|
|
105
|
+
): MobileFollowMutationResult {
|
|
106
|
+
return {
|
|
107
|
+
isFollowing: server.is_following,
|
|
108
|
+
followerCount: server.follower_count,
|
|
31
109
|
};
|
|
32
110
|
}
|
|
33
111
|
|
|
@@ -56,6 +134,35 @@ function mapMobileNotificationPreferenceScope(
|
|
|
56
134
|
return { type: 'product', productId: server.product_id };
|
|
57
135
|
}
|
|
58
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Maps client-side profiles includes (camelCase) to the server-side (snake_case) wire shape.
|
|
139
|
+
*/
|
|
140
|
+
export function mapMobileProfilesIncludeToServer(
|
|
141
|
+
include: MobileProfilesInclude,
|
|
142
|
+
): MobileServerProfilesInclude {
|
|
143
|
+
return {
|
|
144
|
+
follow_counts: include.followCounts,
|
|
145
|
+
follow_summary: include.followSummary
|
|
146
|
+
? { view_as: subaccountToHex(include.followSummary.viewAs) }
|
|
147
|
+
: undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Maps the client-side params of a Followers or Following page to the server-side wire shape. Both list
|
|
153
|
+
* directions take identical params and differ only in the `type` the caller dispatches on.
|
|
154
|
+
*/
|
|
155
|
+
export function mapMobileFollowListParamsToServer(
|
|
156
|
+
params: GetMobileFollowListParams,
|
|
157
|
+
): MobileServerFollowListRequest {
|
|
158
|
+
return {
|
|
159
|
+
subaccount: subaccountToHex(params.target),
|
|
160
|
+
view_as: params.viewAs ? subaccountToHex(params.viewAs) : undefined,
|
|
161
|
+
cursor: params.cursor,
|
|
162
|
+
limit: params.limit,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
59
166
|
/**
|
|
60
167
|
* Maps client-side notification preferences (camelCase) to the server-side (snake_case) wire shape.
|
|
61
168
|
*/
|
|
@@ -86,11 +193,8 @@ function mapMobileNotificationPreferenceScopeToServer(
|
|
|
86
193
|
*/
|
|
87
194
|
function mapMobileFeedTrade(server: MobileServerFeedTrade): MobileFeedTrade {
|
|
88
195
|
return {
|
|
196
|
+
...mapMobileIdentitySummary(server),
|
|
89
197
|
orderDigest: server.order_digest,
|
|
90
|
-
subaccount: server.subaccount,
|
|
91
|
-
username: server.username,
|
|
92
|
-
displayName: server.display_name,
|
|
93
|
-
avatarUrl: server.avatar_url,
|
|
94
198
|
productId: server.product_id,
|
|
95
199
|
quantity: server.quantity,
|
|
96
200
|
notional: server.notional,
|
|
@@ -23,6 +23,12 @@ export function canonicalizeMobileInner(
|
|
|
23
23
|
locale: inner.locale,
|
|
24
24
|
app_version: inner.app_version,
|
|
25
25
|
};
|
|
26
|
+
case 'set_follow':
|
|
27
|
+
return {
|
|
28
|
+
type: 'set_follow',
|
|
29
|
+
subaccount: inner.subaccount,
|
|
30
|
+
is_following: inner.is_following,
|
|
31
|
+
};
|
|
26
32
|
default: {
|
|
27
33
|
throw new Error(
|
|
28
34
|
`canonicalizeMobileInner: unhandled inner payload type: ${JSON.stringify(inner)}`,
|
|
@@ -19,8 +19,8 @@ const FIXED_PRIVATE_KEY =
|
|
|
19
19
|
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907' as const;
|
|
20
20
|
const FIXED_VERIFYING_ADDR = '0x0000000000000000000000000000000000000001';
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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,16 @@ 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',
|
|
31
35
|
} as const;
|
|
32
36
|
|
|
37
|
+
// Every follow fixture on the backend targets this subaccount.
|
|
38
|
+
const FIXTURE_TARGET_SUBACCOUNT =
|
|
39
|
+
'0x1111111111111111111111111111111111111111111111111111111111111111' as const;
|
|
40
|
+
|
|
33
41
|
describe('[mobile-client]: signing (offline)', () => {
|
|
34
42
|
describe('pinned payload hashes', () => {
|
|
35
43
|
it('set_username', () => {
|
|
@@ -61,6 +69,26 @@ describe('[mobile-client]: signing (offline)', () => {
|
|
|
61
69
|
const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
|
|
62
70
|
expect(hash).toBe(PINNED_HASHES.register_expo_token);
|
|
63
71
|
});
|
|
72
|
+
|
|
73
|
+
it('set_follow (follow)', () => {
|
|
74
|
+
const inner: MobileSignedInner = {
|
|
75
|
+
type: 'set_follow',
|
|
76
|
+
subaccount: FIXTURE_TARGET_SUBACCOUNT,
|
|
77
|
+
is_following: true,
|
|
78
|
+
};
|
|
79
|
+
const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
|
|
80
|
+
expect(hash).toBe(PINNED_HASHES.set_follow_true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('set_follow (unfollow)', () => {
|
|
84
|
+
const inner: MobileSignedInner = {
|
|
85
|
+
type: 'set_follow',
|
|
86
|
+
subaccount: FIXTURE_TARGET_SUBACCOUNT,
|
|
87
|
+
is_following: false,
|
|
88
|
+
};
|
|
89
|
+
const hash = getMobilePayloadHash(canonicalizeMobileInner(inner));
|
|
90
|
+
expect(hash).toBe(PINNED_HASHES.set_follow_false);
|
|
91
|
+
});
|
|
64
92
|
});
|
|
65
93
|
|
|
66
94
|
it('canonicalizes msgpack key order regardless of input key order', () => {
|
|
@@ -76,8 +104,8 @@ describe('[mobile-client]: signing (offline)', () => {
|
|
|
76
104
|
});
|
|
77
105
|
|
|
78
106
|
it('canonicalizes multi-field key order regardless of input key order', () => {
|
|
79
|
-
// register_expo_token
|
|
80
|
-
//
|
|
107
|
+
// register_expo_token has the most fields of any signed payload, so it is where a caller is most likely
|
|
108
|
+
// to get the order wrong; canonicalizeMobileInner must rebuild it in the backend's declaration order.
|
|
81
109
|
const outOfOrderInner = {
|
|
82
110
|
app_version: '1.2.3',
|
|
83
111
|
locale: 'en-GB',
|
package/src/signing/types.ts
CHANGED
|
@@ -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,26 @@ 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;
|
|
26
37
|
};
|
|
27
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The single {@link MobileSignedInner} member carrying the given `type` tag.
|
|
41
|
+
*/
|
|
42
|
+
export type MobileSignedInnerByType<T extends MobileSignedInner['type']> =
|
|
43
|
+
Extract<MobileSignedInner, { type: T }>;
|
|
44
|
+
|
|
28
45
|
/**
|
|
29
46
|
* Type-specific payload fields for a signed inner request of a given `type` (the inner payload without its
|
|
30
47
|
* `type` tag). Derived from {@link MobileSignedInner} so the two never drift apart.
|
|
31
48
|
*/
|
|
32
49
|
export type MobileSignedInnerParams<T extends MobileSignedInner['type']> = Omit<
|
|
33
|
-
|
|
50
|
+
MobileSignedInnerByType<T>,
|
|
34
51
|
'type'
|
|
35
52
|
>;
|
|
36
53
|
|