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