Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for me query #1044

Merged
merged 1 commit into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
89 changes: 89 additions & 0 deletions packages/client/src/actions/accountManager.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});
});
});
});
14 changes: 14 additions & 0 deletions packages/client/src/actions/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AuthenticatedSession,
AuthenticatedSessionsRequest,
MeResult,
Paginated,
RefreshRequest,
RefreshResult,
Expand All @@ -13,6 +14,7 @@ import {
AuthenticatedSessionsQuery,
CurrentSessionQuery,
LegacyRolloverRefreshMutation,
MeQuery,
RefreshMutation,
RevokeAuthenticationMutation,
SwitchAccountMutation,
Expand Down Expand Up @@ -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, {});
}
64 changes: 28 additions & 36 deletions packages/client/src/actions/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 6 additions & 2 deletions packages/client/testing-utils.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -12,7 +14,7 @@ export const signer = evmAddress(pk.address);

export function createPublicClient() {
return PublicClient.create({
environment: testnet,
environment: apiEnv,
origin: 'http://example.com',
});
}
Expand Down Expand Up @@ -69,3 +71,5 @@ export function createGraphQLErrorObject(code: GraphQLErrorCode) {
},
};
}

export const storageClient = StorageClient.create(storageEnv);
30 changes: 29 additions & 1 deletion packages/graphql/src/authentication.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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],
);
12 changes: 12 additions & 0 deletions packages/graphql/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
UsernameValue,
Void,
} from '@lens-protocol/types';
import { InvariantError } from '@lens-protocol/types';
import {
type DocumentDecoration,
type FragmentOf,
Expand Down Expand Up @@ -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}"`,
);
}
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading