From c78ff935be1559749c57fcb6d634dee338ddc734 Mon Sep 17 00:00:00 2001 From: Cesare Naldi <naldi.cesare@gmail.com> Date: Fri, 27 Dec 2024 10:53:31 +0100 Subject: [PATCH] feat: add support for me query --- packages/client/package.json | 1 + .../client/src/actions/accountManager.test.ts | 89 +++++++++++++++++++ packages/client/src/actions/authentication.ts | 14 +++ .../client/src/actions/onboarding.test.ts | 64 ++++++------- packages/client/testing-utils.ts | 8 +- packages/graphql/src/authentication.ts | 30 ++++++- packages/graphql/src/graphql.ts | 12 +++ pnpm-lock.yaml | 8 ++ 8 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 packages/client/src/actions/accountManager.test.ts diff --git a/packages/client/package.json b/packages/client/package.json index f2a5eb7bca..cde9f34177 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -78,6 +78,7 @@ "devDependencies": { "@lens-network/sdk": "canary", "@lens-protocol/metadata": "next", + "@lens-protocol/storage-node-client": "next", "ethers": "^6.13.4", "msw": "^2.7.0", "tsup": "^8.3.5", diff --git a/packages/client/src/actions/accountManager.test.ts b/packages/client/src/actions/accountManager.test.ts new file mode 100644 index 0000000000..34eadb323b --- /dev/null +++ b/packages/client/src/actions/accountManager.test.ts @@ -0,0 +1,89 @@ +import { beforeAll, describe, expect, it } from 'vitest'; + +import { type Account, assertTypename } from '@lens-protocol/graphql'; +import * as metadata from '@lens-protocol/metadata'; +import { assertOk, never, uri } from '@lens-protocol/types'; +import { loginAsOnboardingUser, signerWallet, storageClient } from '../../testing-utils'; +import type { SessionClient } from '../clients'; +import { handleWith } from '../viem'; +import { + createAccountWithUsername, + enableSignless, + fetchAccount, + setAccountMetadata, +} from './account'; +import { fetchMeDetails } from './authentication'; + +const walletClient = signerWallet(); +describe('Given a new Lens Account', () => { + let newAccount: Account; + let sessionClient: SessionClient; + + beforeAll(async () => { + const initialMetadata = metadata.account({ + name: 'John Doe', + }); + const result = await loginAsOnboardingUser().andThen((sessionClient) => + createAccountWithUsername(sessionClient, { + username: { localName: `testname${Date.now()}` }, + metadataUri: uri(`data:application/json,${JSON.stringify(initialMetadata)}`), // empty at first + }) + .andThen(handleWith(walletClient)) + .andThen(sessionClient.waitForTransaction) + .andThen((txHash) => fetchAccount(sessionClient, { txHash })) + .andThen((account) => { + newAccount = account ?? never('Account not found'); + return sessionClient.switchAccount({ + account: newAccount.address, + }); + }), + ); + + assertOk(result); + + sessionClient = result.value; + }); + + describe(`When invoking the '${enableSignless.name}' action`, () => { + beforeAll(async () => { + const result = await enableSignless(sessionClient) + .andThen(handleWith(walletClient)) + .andThen(sessionClient.waitForTransaction); + assertOk(result); + }); + it(`Then it should be reflected in the '${fetchMeDetails.name}' action result`, async () => { + const result = await fetchMeDetails(sessionClient); + + assertOk(result); + expect(result.value).toMatchObject({ + isSignless: true, + }); + }); + + it('Then it should be possible to perform social operations in a signless fashion (e.g., updating Account metadata)', async () => { + const updated = metadata.account({ + name: 'Bruce Wayne', + }); + const resource = await storageClient.uploadAsJson(updated); + + const result = await setAccountMetadata(sessionClient, { + metadataUri: resource.uri, + }); + + assertOk(result); + + assertTypename(result.value, 'SetAccountMetadataResponse'); + await sessionClient.waitForTransaction(result.value.hash); + + const account = await fetchAccount(sessionClient, { address: newAccount.address }).unwrapOr( + null, + ); + + expect(account).toMatchObject({ + metadata: { + name: 'Bruce Wayne', + }, + }); + }); + }); +}); diff --git a/packages/client/src/actions/authentication.ts b/packages/client/src/actions/authentication.ts index f5095e9514..9c6ea179c1 100644 --- a/packages/client/src/actions/authentication.ts +++ b/packages/client/src/actions/authentication.ts @@ -1,6 +1,7 @@ import type { AuthenticatedSession, AuthenticatedSessionsRequest, + MeResult, Paginated, RefreshRequest, RefreshResult, @@ -13,6 +14,7 @@ import { AuthenticatedSessionsQuery, CurrentSessionQuery, LegacyRolloverRefreshMutation, + MeQuery, RefreshMutation, RevokeAuthenticationMutation, SwitchAccountMutation, @@ -142,3 +144,15 @@ export function switchAccount( ): ResultAsync<SwitchAccountResult, UnauthenticatedError | UnexpectedError> { return client.mutation(SwitchAccountMutation, { request }); } + +/** + * Retrieve the details of the authenticated Account. + * + * @param client - The session client for the authenticated Account. + * @returns The details of the authenticated Account. + */ +export function fetchMeDetails( + client: SessionClient, +): ResultAsync<MeResult, UnauthenticatedError | UnexpectedError> { + return client.query(MeQuery, {}); +} diff --git a/packages/client/src/actions/onboarding.test.ts b/packages/client/src/actions/onboarding.test.ts index 18703067c7..645a40571d 100644 --- a/packages/client/src/actions/onboarding.test.ts +++ b/packages/client/src/actions/onboarding.test.ts @@ -20,44 +20,36 @@ describe('Given an onboarding user', () => { let newAccount: Account | null = null; // Login as onboarding user - const sessionClient = await loginAsOnboardingUser() - .andThen((sessionClient) => - // Create an account with username - createAccountWithUsername(sessionClient, { - username: { localName: `testname${Date.now()}` }, - metadataUri: uri(`data:application/json,${JSON.stringify(metadata)}`), + const result = await loginAsOnboardingUser().andThen((sessionClient) => + // Create an account with username + createAccountWithUsername(sessionClient, { + username: { localName: `testname${Date.now()}` }, + metadataUri: uri(`data:application/json,${JSON.stringify(metadata)}`), + }) + // Sign if necessary + .andThen(handleWith(walletClient)) + + // Wait for the transaction to be mined + .andThen(sessionClient.waitForTransaction) + + // Fetch the account + .andThen((txHash) => fetchAccount(sessionClient, { txHash })) + + .andTee((account) => { + newAccount = account ?? never('Account not found'); }) - // Sign if necessary - .andThen(handleWith(walletClient)) - // Wait for the transaction to be mined - .andThen(sessionClient.waitForTransaction) - - // Fetch the account - .andThen((txHash) => fetchAccount(sessionClient, { txHash })) - - .andTee((account) => { - newAccount = account ?? never('Account not found'); - }) - - // Switch to the newly created account - .andThen((account) => - sessionClient.switchAccount({ - account: account?.address ?? never('Account not found'), - }), - ), - ) - .match( - (value) => value, - (error) => { - throw error; - }, - ); - - const user = await sessionClient.getAuthenticatedUser(); - assertOk(user); - - expect(user.value).toMatchObject({ + // Switch to the newly created account + .andThen((account) => + sessionClient.switchAccount({ + account: account?.address ?? never('Account not found'), + }), + ), + ); + assertOk(result); + + const user = await result.value.getAuthenticatedUser().unwrapOr(null); + expect(user).toMatchObject({ role: Role.AccountOwner, account: newAccount!.address.toLowerCase(), owner: signer.toLowerCase(), diff --git a/packages/client/testing-utils.ts b/packages/client/testing-utils.ts index 4ef04a9d09..0f7bfe8a23 100644 --- a/packages/client/testing-utils.ts +++ b/packages/client/testing-utils.ts @@ -1,8 +1,10 @@ import { chains } from '@lens-network/sdk/viem'; +import { StorageClient, testnet as storageEnv } from '@lens-protocol/storage-node-client'; import { evmAddress } from '@lens-protocol/types'; import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { GraphQLErrorCode, PublicClient, testnet } from './src'; + +import { GraphQLErrorCode, PublicClient, testnet as apiEnv } from './src'; const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY); const account = evmAddress(import.meta.env.TEST_ACCOUNT); @@ -12,7 +14,7 @@ export const signer = evmAddress(pk.address); export function createPublicClient() { return PublicClient.create({ - environment: testnet, + environment: apiEnv, origin: 'http://example.com', }); } @@ -69,3 +71,5 @@ export function createGraphQLErrorObject(code: GraphQLErrorCode) { }, }; } + +export const storageClient = StorageClient.create(storageEnv); diff --git a/packages/graphql/src/authentication.ts b/packages/graphql/src/authentication.ts index ec7ace3cc7..32a3ceb8bc 100644 --- a/packages/graphql/src/authentication.ts +++ b/packages/graphql/src/authentication.ts @@ -1,5 +1,5 @@ import type { FragmentOf } from 'gql.tada'; -import { PaginatedResultInfoFragment } from './fragments'; +import { AccountAvailableFragment, PaginatedResultInfoFragment } from './fragments'; import { type RequestOf, graphql } from './graphql'; const AuthenticationChallengeFragment = graphql( @@ -197,3 +197,31 @@ export const SwitchAccountMutation = graphql( [SwitchAccountResultFragment], ); export type SwitchAccountRequest = RequestOf<typeof SwitchAccountMutation>; + +const MeResultFragment = graphql( + `fragment MeResult on MeResult { + appLoggedIn + isSignless + isSponsored + limit { + allowance + allowanceLeft + allowanceUsed + window + } + loggedInAs { + ...AccountAvailable + } + }`, + [AccountAvailableFragment], +); +export type MeResult = FragmentOf<typeof MeResultFragment>; + +export const MeQuery = graphql( + `query Me { + value: me { + ...MeResult + } + }`, + [MeResultFragment], +); diff --git a/packages/graphql/src/graphql.ts b/packages/graphql/src/graphql.ts index ea3ccfb508..299cbdd0af 100644 --- a/packages/graphql/src/graphql.ts +++ b/packages/graphql/src/graphql.ts @@ -21,6 +21,7 @@ import type { UsernameValue, Void, } from '@lens-protocol/types'; +import { InvariantError } from '@lens-protocol/types'; import { type DocumentDecoration, type FragmentOf, @@ -251,3 +252,14 @@ export type DynamicFragmentOf< > = Document extends DynamicFragmentDocument<infer In, infer StaticNodes> ? FragmentOf<FragmentDocumentFrom<In, FragmentDocumentForEach<[...DynamicNodes, ...StaticNodes]>>> : never; + +export function assertTypename<Typename extends string>( + node: AnyGqlNode, + typename: Typename, +): asserts node is AnyGqlNode<Typename> { + if (node.__typename !== typename) { + throw new InvariantError( + `Expected node to have typename "${typename}", but got "${node.__typename}"`, + ); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dc26b605b..45d60d048f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: '@lens-protocol/metadata': specifier: next version: 2.0.0-next.2(zod@3.23.8) + '@lens-protocol/storage-node-client': + specifier: next + version: 0.0.0-next-20241217195719 ethers: specifier: ^6.13.4 version: 6.13.4 @@ -887,6 +890,9 @@ packages: zod: optional: true + '@lens-protocol/storage-node-client@0.0.0-next-20241217195719': + resolution: {integrity: sha512-jWonPI26JViuAN54rjhhpC1jw9bkSmjnY7xqYUEmwdza/P6Noh5GXQLaHvyi9QgrQ7RO8Iw7ObgcTK2oHD84MA==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -3497,6 +3503,8 @@ snapshots: optionalDependencies: zod: 3.23.8 + '@lens-protocol/storage-node-client@0.0.0-next-20241217195719': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.25.9