Skip to content

Commit

Permalink
client: add frames module
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysu committed Mar 25, 2024
1 parent 4183f68 commit 89a6b63
Show file tree
Hide file tree
Showing 24 changed files with 1,160 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-emus-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lens-protocol/client": minor
---

**feat:** added Frames module
2 changes: 1 addition & 1 deletion examples/node/scripts/authentication/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function main() {
console.log(`Is LensClient authenticated? `, await client.authentication.isAuthenticated());
console.log(`Authenticated profileId: `, profileId);
console.log(`Access token: `, accessToken);
console.log(`Is access token valid? `, await client.authentication.verify(accessToken));
console.log(`Is access token valid? `, await client.authentication.verify({ accessToken }));
}

main();
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function main() {
console.log(`Is LensClient authenticated? `, await client.authentication.isAuthenticated());
console.log(`Authenticated wallet: `, walletAddress);
console.log(`Access token: `, accessToken);
console.log(`Is access token valid? `, await client.authentication.verify(accessToken));
console.log(`Is access token valid? `, await client.authentication.verify({ accessToken }));
}

main();
35 changes: 35 additions & 0 deletions examples/node/scripts/authentication/validateIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { LensClient, development } from '@lens-protocol/client';

import { setupWallet } from '../shared/setupWallet';

async function main() {
const client = new LensClient({
environment: development,
});

const wallet = setupWallet();
const address = await wallet.getAddress();

const managedProfiles = await client.wallet.profilesManaged({ for: wallet.address });

if (managedProfiles.items.length === 0) {
throw new Error(`You don't manage any profiles, create one first`);
}

const { id, text } = await client.authentication.generateChallenge({
signedBy: address,
for: managedProfiles.items[0].id,
});

const signature = await wallet.signMessage(text);

await client.authentication.authenticate({ id, signature });

const identityTokenResult = await client.authentication.getIdentityToken();
const identityToken = identityTokenResult.unwrap();

console.log(`Identity token: `, identityToken);
console.log(`Is identity token valid? `, await client.authentication.verify({ identityToken }));
}

main();
26 changes: 26 additions & 0 deletions examples/node/scripts/frames/createFrameTypedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FramesEip721TypedDataSpec, LensClient, development } from '@lens-protocol/client';

async function main() {
const client = new LensClient({
environment: development,
});

const deadline = new Date();
deadline.setMinutes(deadline.getMinutes() + 30);

const result = await client.frames.createFrameTypedData({
actionResponse: '0x0000000000000000000000000000000000000000',
buttonIndex: 2,
deadline: deadline.getTime(),
inputText: 'Hello, World!',
profileId: '0x01',
pubId: '0x01-0x01',
specVersion: FramesEip721TypedDataSpec.OnePointOnePointOne,
state: '{"counter":1,"idempotency_key":"431b8b38-eb4d-455b"}',
url: 'https://mylensframe.xyz',
});

console.log(`Result: `, result);
}

main();
31 changes: 31 additions & 0 deletions examples/node/scripts/frames/signFrameAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FramesEip721TypedDataSpec } from '@lens-protocol/client';

import { getAuthenticatedClient } from '../shared/getAuthenticatedClient';
import { setupWallet } from '../shared/setupWallet';

async function main() {
const wallet = setupWallet();
const client = await getAuthenticatedClient(wallet);

const result = await client.frames.signFrameAction({
actionResponse: '0x0000000000000000000000000000000000000000',
buttonIndex: 2,
inputText: 'Hello, World!',
profileId: '0x01',
pubId: '0x01-0x01',
specVersion: FramesEip721TypedDataSpec.OnePointOnePointOne,
state: '{"counter":1,"idempotency_key":"431b8b38-eb4d-455b"}',
url: 'https://mylensframe.xyz',
});

if (result.isFailure()) {
console.error(result.error); // CredentialsExpiredError or NotAuthenticatedError
process.exit(1);
}

const data = result.value;

console.log(`Result: `, data);
}

main();
47 changes: 47 additions & 0 deletions examples/node/scripts/frames/verifyFrameSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FrameVerifySignatureResult, FramesEip721TypedDataSpec } from '@lens-protocol/client';

import { getAuthenticatedClient } from '../shared/getAuthenticatedClient';
import { setupWallet } from '../shared/setupWallet';

async function main() {
const wallet = setupWallet();
const client = await getAuthenticatedClient(wallet);
const profileId = await client.authentication.getProfileId();

if (!profileId) {
throw new Error('Profile not authenticated');
}

const identityTokenResult = await client.authentication.getIdentityToken();
const identityToken = identityTokenResult.unwrap();

// get signature
const result = await client.frames.signFrameAction({
actionResponse: '0x0000000000000000000000000000000000000000',
buttonIndex: 2,
inputText: 'Hello, World!',
profileId: profileId,
pubId: '0x01-0x01',
specVersion: FramesEip721TypedDataSpec.OnePointOnePointOne,
state: '{"counter":1,"idempotency_key":"431b8b38-eb4d-455b"}',
url: 'https://mylensframe.xyz',
});

if (result.isFailure()) {
console.error(result.error); // CredentialsExpiredError or NotAuthenticatedError
process.exit(1);
}

const data = result.value;

// verify
const verifyResult = await client.frames.verifyFrameSignature({
identityToken,
signature: data.signature,
signedTypedData: data.signedTypedData,
});

console.log(`Is signature valid? `, verifyResult === FrameVerifySignatureResult.Verified);
}

main();
8 changes: 8 additions & 0 deletions packages/client/src/LensClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { QueryParams } from './queryParams';
import {
Explore,
Feed,
Frames,
Handle,
Invites,
Modules,
Expand Down Expand Up @@ -108,6 +109,13 @@ export class LensClient {
return new Feed(this.context, this._authentication);
}

/**
* The Frames module
*/
get frames(): Frames {
return new Frames(this.context, this._authentication);
}

/**
* The Handle module
*/
Expand Down
34 changes: 31 additions & 3 deletions packages/client/src/authentication/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ChallengeRequest,
RevokeAuthenticationRequest,
SignedAuthChallenge,
VerifyRequest,
WalletAuthenticationToProfileAuthenticationRequest,
} from '../graphql/types.generated';
import { buildAuthorizationHeader } from '../helpers/buildAuthorizationHeader';
Expand All @@ -39,7 +40,7 @@ export class Authentication implements IAuthentication {
}

async authenticateWith({ refreshToken }: { refreshToken: string }): Promise<void> {
const credentials = new Credentials(undefined, refreshToken);
const credentials = new Credentials(undefined, undefined, refreshToken);
await this.credentials.set(credentials);
}

Expand All @@ -59,8 +60,10 @@ export class Authentication implements IAuthentication {
});
}

async verify(accessToken: string): Promise<boolean> {
return this.api.verify(accessToken);
async verify(request: string | VerifyRequest): Promise<boolean> {
const correctRequest = typeof request === 'string' ? { accessToken: request } : request;

return this.api.verify(correctRequest);
}

async isAuthenticated(): Promise<boolean> {
Expand Down Expand Up @@ -109,6 +112,31 @@ export class Authentication implements IAuthentication {
return failure(new CredentialsExpiredError());
}

async getIdentityToken(): PromiseResult<string, CredentialsExpiredError | NotAuthenticatedError> {
const credentials = await this.credentials.get();

if (!credentials) {
return failure(new NotAuthenticatedError());
}

if (!credentials.shouldRefresh() && credentials.identityToken) {
return success(credentials.identityToken);
}

if (credentials.canRefresh()) {
const newCredentials = await this.api.refresh(credentials.refreshToken);
await this.credentials.set(newCredentials);

if (!newCredentials.identityToken) {
return failure(new CredentialsExpiredError());
}

return success(newCredentials.identityToken);
}

return failure(new CredentialsExpiredError());
}

async getProfileId(): Promise<string | null> {
const result = await this.getCredentials();

Expand Down
26 changes: 22 additions & 4 deletions packages/client/src/authentication/IAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ChallengeRequest,
RevokeAuthenticationRequest,
SignedAuthChallenge,
VerifyRequest,
WalletAuthenticationToProfileAuthenticationRequest,
} from '../graphql/types.generated';
import type { PaginatedResult } from '../helpers/buildPaginatedQueryResult';
Expand Down Expand Up @@ -55,12 +56,22 @@ export interface IAuthentication {
): PromiseResult<void, CredentialsExpiredError | NotAuthenticatedError>;

/**
* Verify that the access token is signed by the server and the user.
* Check the validity of the provided token.
*
* @param accessToken - The access token to verify
* @returns Whether the access token is valid
* @deprecated Use with {@link VerifyRequest} instead.
*
* @param request - Access token as a string
* @returns Whether the provided token is valid
*/
verify(request: string): Promise<boolean>;

/**
* Check the validity of the provided token.
*
* @param request - Verify request
* @returns Whether the provided token is valid
*/
verify(accessToken: string): Promise<boolean>;
verify(request: VerifyRequest): Promise<boolean>;

/**
* Check if the user is authenticated. If the credentials are expired, try to refresh them.
Expand All @@ -76,6 +87,13 @@ export interface IAuthentication {
*/
getAccessToken(): PromiseResult<string, CredentialsExpiredError | NotAuthenticatedError>;

/**
* Get the identity token. If it expired, try to refresh it.
*
* @returns A Result with the identity token or possible error scenarios
*/
getIdentityToken(): PromiseResult<string, CredentialsExpiredError | NotAuthenticatedError>;

/**
* Get the authenticated profile id.
*
Expand Down
21 changes: 11 additions & 10 deletions packages/client/src/authentication/adapters/AuthenticationApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ChallengeRequest,
RevokeAuthenticationRequest,
SignedAuthChallenge,
VerifyRequest,
WalletAuthenticationToProfileAuthenticationRequest,
} from '../../graphql/types.generated';
import {
Expand Down Expand Up @@ -33,27 +34,27 @@ export class AuthenticationApi {
return result.data.result;
}

async verify(accessToken: string): Promise<boolean> {
const result = await this.sdk.AuthVerify({ request: { accessToken } });
async verify(request: VerifyRequest): Promise<boolean> {
const result = await this.sdk.AuthVerify({ request });

return result.data.result;
}

async authenticate(request: SignedAuthChallenge): Promise<Credentials> {
const result = await this.sdk.AuthAuthenticate({ request });
const { accessToken, refreshToken } = result.data.result;
const { accessToken, identityToken, refreshToken } = result.data.result;

const credentials = new Credentials(accessToken, refreshToken);
const credentials = new Credentials(accessToken, identityToken, refreshToken);
credentials.checkClock();

return credentials;
}

async refresh(refreshToken: string): Promise<Credentials> {
const result = await this.sdk.AuthRefresh({ request: { refreshToken } });
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = result.data.result;
async refresh(currentRefreshToken: string): Promise<Credentials> {
const result = await this.sdk.AuthRefresh({ request: { refreshToken: currentRefreshToken } });
const { accessToken, identityToken, refreshToken } = result.data.result;

const credentials = new Credentials(newAccessToken, newRefreshToken);
const credentials = new Credentials(accessToken, identityToken, refreshToken);
credentials.checkClock();

return credentials;
Expand All @@ -64,9 +65,9 @@ export class AuthenticationApi {
headers?: Record<string, string>,
): Promise<Credentials> {
const result = await this.sdk.WalletAuthenticationToProfileAuthentication({ request }, headers);
const { accessToken, refreshToken } = result.data.result;
const { accessToken, identityToken, refreshToken } = result.data.result;

const credentials = new Credentials(accessToken, refreshToken);
const credentials = new Credentials(accessToken, identityToken, refreshToken);

return credentials;
}
Expand Down
14 changes: 12 additions & 2 deletions packages/client/src/authentication/adapters/Credentials.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const profileSession = {
refreshToken:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjB4NTYiLCJldm1BZGRyZXNzIjoiMHgzZkM0N2NkRGNGZDU5ZGNlMjA2OTRiNTc1QUZjMUQ5NDE4Njc3NWIwIiwicm9sZSI6InByb2ZpbGVfcmVmcmVzaCIsImF1dGhvcml6YXRpb25JZCI6IjA0NWM2N2I2LWIzNTEtNDVjOS1hNWE1LWM0YWQ5ODg5ZDYyYyIsImlhdCI6MTcwMTM1MzA5NiwiZXhwIjoxNzAxOTU3ODk2fQ.i2kzT4I6VBTuZvjly0TEdGN_YsuBaTDopMQU4_398kA',
refreshTokenExp: 1701957896000,
identityToken: '',
};

const walletSession = {
Expand All @@ -18,14 +19,23 @@ const walletSession = {
refreshToken:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjB4YTU2NTNlODhEOWMzNTIzODdkZURkQzc5YmNmOTlmMGFkYTYyZTljNiIsInJvbGUiOiJ3YWxsZXRfcmVmcmVzaCIsImF1dGhvcml6YXRpb25JZCI6IjMxMWVkNzNkLTZjYjMtNDRmZi05NzdmLTJjMmM1MjI4YWJiNCIsImlhdCI6MTcwMTM1NTA3NywiZXhwIjoxNzAxOTU5ODc3fQ.WTUpWsH-Fvv8U4WIwL_Sk6cpHvRSGY_vdBsy1IQrCmM',
refreshTokenExp: 1701959877000,
identityToken: '',
};

const buildProfileCredentials = () => {
return new Credentials(profileSession.accessToken, profileSession.refreshToken);
return new Credentials(
profileSession.accessToken,
profileSession.identityToken,
profileSession.refreshToken,
);
};

const buildWalletCredentials = () => {
return new Credentials(walletSession.accessToken, walletSession.refreshToken);
return new Credentials(
walletSession.accessToken,
walletSession.identityToken,
walletSession.refreshToken,
);
};

describe(`Given the ${Credentials.name} class`, () => {
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/authentication/adapters/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function isProfileJwtContent(decodedJwt: DecodedJwt): decodedJwt is ProfileJwtPa
export class Credentials {
constructor(
readonly accessToken: string | undefined,
readonly identityToken: string | undefined,
readonly refreshToken: string,
) {}

Expand Down
Loading

0 comments on commit 89a6b63

Please sign in to comment.