Skip to content

Commit 50d1b27

Browse files
authored
Merge pull request #1044 from lens-protocol/cesare/T-23413-sdk-support-me-query
feat: add support for me query
2 parents e077132 + c78ff93 commit 50d1b27

File tree

8 files changed

+187
-39
lines changed

8 files changed

+187
-39
lines changed

packages/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"devDependencies": {
7979
"@lens-network/sdk": "canary",
8080
"@lens-protocol/metadata": "next",
81+
"@lens-protocol/storage-node-client": "next",
8182
"ethers": "^6.13.4",
8283
"msw": "^2.7.0",
8384
"tsup": "^8.3.5",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { beforeAll, describe, expect, it } from 'vitest';
2+
3+
import { type Account, assertTypename } from '@lens-protocol/graphql';
4+
import * as metadata from '@lens-protocol/metadata';
5+
import { assertOk, never, uri } from '@lens-protocol/types';
6+
import { loginAsOnboardingUser, signerWallet, storageClient } from '../../testing-utils';
7+
import type { SessionClient } from '../clients';
8+
import { handleWith } from '../viem';
9+
import {
10+
createAccountWithUsername,
11+
enableSignless,
12+
fetchAccount,
13+
setAccountMetadata,
14+
} from './account';
15+
import { fetchMeDetails } from './authentication';
16+
17+
const walletClient = signerWallet();
18+
describe('Given a new Lens Account', () => {
19+
let newAccount: Account;
20+
let sessionClient: SessionClient;
21+
22+
beforeAll(async () => {
23+
const initialMetadata = metadata.account({
24+
name: 'John Doe',
25+
});
26+
const result = await loginAsOnboardingUser().andThen((sessionClient) =>
27+
createAccountWithUsername(sessionClient, {
28+
username: { localName: `testname${Date.now()}` },
29+
metadataUri: uri(`data:application/json,${JSON.stringify(initialMetadata)}`), // empty at first
30+
})
31+
.andThen(handleWith(walletClient))
32+
.andThen(sessionClient.waitForTransaction)
33+
.andThen((txHash) => fetchAccount(sessionClient, { txHash }))
34+
.andThen((account) => {
35+
newAccount = account ?? never('Account not found');
36+
return sessionClient.switchAccount({
37+
account: newAccount.address,
38+
});
39+
}),
40+
);
41+
42+
assertOk(result);
43+
44+
sessionClient = result.value;
45+
});
46+
47+
describe(`When invoking the '${enableSignless.name}' action`, () => {
48+
beforeAll(async () => {
49+
const result = await enableSignless(sessionClient)
50+
.andThen(handleWith(walletClient))
51+
.andThen(sessionClient.waitForTransaction);
52+
assertOk(result);
53+
});
54+
it(`Then it should be reflected in the '${fetchMeDetails.name}' action result`, async () => {
55+
const result = await fetchMeDetails(sessionClient);
56+
57+
assertOk(result);
58+
expect(result.value).toMatchObject({
59+
isSignless: true,
60+
});
61+
});
62+
63+
it('Then it should be possible to perform social operations in a signless fashion (e.g., updating Account metadata)', async () => {
64+
const updated = metadata.account({
65+
name: 'Bruce Wayne',
66+
});
67+
const resource = await storageClient.uploadAsJson(updated);
68+
69+
const result = await setAccountMetadata(sessionClient, {
70+
metadataUri: resource.uri,
71+
});
72+
73+
assertOk(result);
74+
75+
assertTypename(result.value, 'SetAccountMetadataResponse');
76+
await sessionClient.waitForTransaction(result.value.hash);
77+
78+
const account = await fetchAccount(sessionClient, { address: newAccount.address }).unwrapOr(
79+
null,
80+
);
81+
82+
expect(account).toMatchObject({
83+
metadata: {
84+
name: 'Bruce Wayne',
85+
},
86+
});
87+
});
88+
});
89+
});

packages/client/src/actions/authentication.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
AuthenticatedSession,
33
AuthenticatedSessionsRequest,
4+
MeResult,
45
Paginated,
56
RefreshRequest,
67
RefreshResult,
@@ -13,6 +14,7 @@ import {
1314
AuthenticatedSessionsQuery,
1415
CurrentSessionQuery,
1516
LegacyRolloverRefreshMutation,
17+
MeQuery,
1618
RefreshMutation,
1719
RevokeAuthenticationMutation,
1820
SwitchAccountMutation,
@@ -142,3 +144,15 @@ export function switchAccount(
142144
): ResultAsync<SwitchAccountResult, UnauthenticatedError | UnexpectedError> {
143145
return client.mutation(SwitchAccountMutation, { request });
144146
}
147+
148+
/**
149+
* Retrieve the details of the authenticated Account.
150+
*
151+
* @param client - The session client for the authenticated Account.
152+
* @returns The details of the authenticated Account.
153+
*/
154+
export function fetchMeDetails(
155+
client: SessionClient,
156+
): ResultAsync<MeResult, UnauthenticatedError | UnexpectedError> {
157+
return client.query(MeQuery, {});
158+
}

packages/client/src/actions/onboarding.test.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,36 @@ describe('Given an onboarding user', () => {
2020
let newAccount: Account | null = null;
2121

2222
// Login as onboarding user
23-
const sessionClient = await loginAsOnboardingUser()
24-
.andThen((sessionClient) =>
25-
// Create an account with username
26-
createAccountWithUsername(sessionClient, {
27-
username: { localName: `testname${Date.now()}` },
28-
metadataUri: uri(`data:application/json,${JSON.stringify(metadata)}`),
23+
const result = await loginAsOnboardingUser().andThen((sessionClient) =>
24+
// Create an account with username
25+
createAccountWithUsername(sessionClient, {
26+
username: { localName: `testname${Date.now()}` },
27+
metadataUri: uri(`data:application/json,${JSON.stringify(metadata)}`),
28+
})
29+
// Sign if necessary
30+
.andThen(handleWith(walletClient))
31+
32+
// Wait for the transaction to be mined
33+
.andThen(sessionClient.waitForTransaction)
34+
35+
// Fetch the account
36+
.andThen((txHash) => fetchAccount(sessionClient, { txHash }))
37+
38+
.andTee((account) => {
39+
newAccount = account ?? never('Account not found');
2940
})
30-
// Sign if necessary
31-
.andThen(handleWith(walletClient))
3241

33-
// Wait for the transaction to be mined
34-
.andThen(sessionClient.waitForTransaction)
35-
36-
// Fetch the account
37-
.andThen((txHash) => fetchAccount(sessionClient, { txHash }))
38-
39-
.andTee((account) => {
40-
newAccount = account ?? never('Account not found');
41-
})
42-
43-
// Switch to the newly created account
44-
.andThen((account) =>
45-
sessionClient.switchAccount({
46-
account: account?.address ?? never('Account not found'),
47-
}),
48-
),
49-
)
50-
.match(
51-
(value) => value,
52-
(error) => {
53-
throw error;
54-
},
55-
);
56-
57-
const user = await sessionClient.getAuthenticatedUser();
58-
assertOk(user);
59-
60-
expect(user.value).toMatchObject({
42+
// Switch to the newly created account
43+
.andThen((account) =>
44+
sessionClient.switchAccount({
45+
account: account?.address ?? never('Account not found'),
46+
}),
47+
),
48+
);
49+
assertOk(result);
50+
51+
const user = await result.value.getAuthenticatedUser().unwrapOr(null);
52+
expect(user).toMatchObject({
6153
role: Role.AccountOwner,
6254
account: newAccount!.address.toLowerCase(),
6355
owner: signer.toLowerCase(),

packages/client/testing-utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { chains } from '@lens-network/sdk/viem';
2+
import { StorageClient, testnet as storageEnv } from '@lens-protocol/storage-node-client';
23
import { evmAddress } from '@lens-protocol/types';
34
import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem';
45
import { privateKeyToAccount } from 'viem/accounts';
5-
import { GraphQLErrorCode, PublicClient, testnet } from './src';
6+
7+
import { GraphQLErrorCode, PublicClient, testnet as apiEnv } from './src';
68

79
const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY);
810
const account = evmAddress(import.meta.env.TEST_ACCOUNT);
@@ -12,7 +14,7 @@ export const signer = evmAddress(pk.address);
1214

1315
export function createPublicClient() {
1416
return PublicClient.create({
15-
environment: testnet,
17+
environment: apiEnv,
1618
origin: 'http://example.com',
1719
});
1820
}
@@ -69,3 +71,5 @@ export function createGraphQLErrorObject(code: GraphQLErrorCode) {
6971
},
7072
};
7173
}
74+
75+
export const storageClient = StorageClient.create(storageEnv);

packages/graphql/src/authentication.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FragmentOf } from 'gql.tada';
2-
import { PaginatedResultInfoFragment } from './fragments';
2+
import { AccountAvailableFragment, PaginatedResultInfoFragment } from './fragments';
33
import { type RequestOf, graphql } from './graphql';
44

55
const AuthenticationChallengeFragment = graphql(
@@ -197,3 +197,31 @@ export const SwitchAccountMutation = graphql(
197197
[SwitchAccountResultFragment],
198198
);
199199
export type SwitchAccountRequest = RequestOf<typeof SwitchAccountMutation>;
200+
201+
const MeResultFragment = graphql(
202+
`fragment MeResult on MeResult {
203+
appLoggedIn
204+
isSignless
205+
isSponsored
206+
limit {
207+
allowance
208+
allowanceLeft
209+
allowanceUsed
210+
window
211+
}
212+
loggedInAs {
213+
...AccountAvailable
214+
}
215+
}`,
216+
[AccountAvailableFragment],
217+
);
218+
export type MeResult = FragmentOf<typeof MeResultFragment>;
219+
220+
export const MeQuery = graphql(
221+
`query Me {
222+
value: me {
223+
...MeResult
224+
}
225+
}`,
226+
[MeResultFragment],
227+
);

packages/graphql/src/graphql.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
UsernameValue,
2222
Void,
2323
} from '@lens-protocol/types';
24+
import { InvariantError } from '@lens-protocol/types';
2425
import {
2526
type DocumentDecoration,
2627
type FragmentOf,
@@ -251,3 +252,14 @@ export type DynamicFragmentOf<
251252
> = Document extends DynamicFragmentDocument<infer In, infer StaticNodes>
252253
? FragmentOf<FragmentDocumentFrom<In, FragmentDocumentForEach<[...DynamicNodes, ...StaticNodes]>>>
253254
: never;
255+
256+
export function assertTypename<Typename extends string>(
257+
node: AnyGqlNode,
258+
typename: Typename,
259+
): asserts node is AnyGqlNode<Typename> {
260+
if (node.__typename !== typename) {
261+
throw new InvariantError(
262+
`Expected node to have typename "${typename}", but got "${node.__typename}"`,
263+
);
264+
}
265+
}

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)