Skip to content

Commit

Permalink
New GatedClient
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarenaldi committed Sep 21, 2023
1 parent 5e475fe commit a1771b5
Show file tree
Hide file tree
Showing 35 changed files with 7,973 additions and 828 deletions.
68 changes: 68 additions & 0 deletions packages/gated-content/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
overwrite: true
hooks:
afterAllFileWrite:
- prettier --write
- eslint --fix

watch: false
config:
gqlImport: graphql-tag
avoidOptionals:
field: false
strictScalars: true
scalars:
AppId: string
BlockchainData: string
BroadcastId: string
ChainId: string
ChallengeId: string
ContentEncryptionKey: string
CreateHandle: string
Cursor: string
DateTime: string
EncryptableDateTime: string
EncryptableMarkdown: string
EncryptableString: string
EncryptableTxHash: string
EncryptableURI: string
EncryptedPath: string
EncryptedValue: string
Ens: string
EvmAddress: string
Handle: string
ImageSizeTransform: string
IpfsCid: string
Jwt: string
LimitScalar: number
Locale: string
Markdown: string
MimeType: string
MomokaId: string
MomokaProof: string
NftGalleryId: string
NftGalleryName: string
Nonce: string
OnchainPublicationId: string
PoapEventId: string
ProfileId: string
PublicationId: string
Signature: string
TokenId: string
TxHash: string
TxId: string
UnixTimestamp: string
URI: string
URL: string
UUID: string
Void: string

schema:
# - https://api-v2-mumbai.lens.dev/graphql
- http://localhost:4000/graphql

generates:
src/graphql/generated.ts:
plugins:
- typescript:
nonOptionalTypename: true
immutableTypes: true
21 changes: 13 additions & 8 deletions packages/gated-content/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lens-protocol/gated-content",
"version": "0.3.3",
"version": "0.3.3-next.3",
"description": "Token-gated content for the Lens Protocol",
"main": "dist/lens-protocol-gated-content.cjs.js",
"module": "dist/lens-protocol-gated-content.esm.js",
Expand Down Expand Up @@ -34,6 +34,7 @@
"build": "preconstruct build",
"eslint:fix": "pnpm run eslint --fix",
"eslint": "eslint src",
"generate": "graphql-codegen",
"lint": "pnpm run prettier && pnpm run eslint && pnpm run tsc",
"lint:fix": "pnpm run prettier:fix && pnpm run eslint:fix && pnpm run tsc",
"prettier:fix": "prettier --write .",
Expand All @@ -44,10 +45,7 @@
},
"license": "MIT",
"dependencies": {
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@lens-protocol/api-bindings": "workspace:*",
"@lens-protocol/domain": "workspace:*",
"@lens-protocol/metadata": "0.1.0-alpha.13",
"@lens-protocol/shared-kernel": "workspace:*",
"@lens-protocol/storage": "workspace:*",
"@lit-protocol/constants": "2.1.62",
Expand All @@ -56,42 +54,49 @@
"@lit-protocol/node-client": "^2.1.62",
"@lit-protocol/types": "2.1.62",
"siwe": "^1.1.6",
"traverse": "^0.6.7",
"tslib": "^2.5.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@ethersproject/wallet": "^5.7.0",
"@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "3.1.0",
"@graphql-codegen/typescript": "3.0.1",
"@jest/globals": "^29.4.3",
"@lens-protocol/eslint-config": "workspace:*",
"@lens-protocol/prettier-config": "workspace:*",
"@lens-protocol/tsconfig": "workspace:*",
"@types/jest": "29.5.3",
"@types/jest-when": "^3.5.2",
"@types/node": "^18.15.11",
"@types/traverse": "^0.6.32",
"eslint": "^8.34.0",
"ethers": "^5.7.2",
"graphql": "^16.6.0",
"jest": "^29.3.1",
"jest-mock-extended": "^3.0.1",
"jest-when": "^3.5.2",
"prettier": "^2.8.4",
"ts-jest": "^29.0.5",
"typescript": "^4.9.5",
"wait-for-expect": "^3.0.2",
"zod": "^3.20.6"
"zod": "^3.22.0"
},
"peerDependencies": {
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@ethersproject/wallet": "^5.7.0",
"ethers": "^5.7.2",
"zod": "^3.20.6"
"zod": "^3.22.0"
},
"prettier": "@lens-protocol/prettier-config",
"babel": {
Expand Down
5 changes: 5 additions & 0 deletions packages/gated-content/src/CannotDecryptError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CausedError, IEquatableError } from '@lens-protocol/shared-kernel';

export class CannotDecryptError extends CausedError implements IEquatableError {
name = 'CannotDecryptError' as const;
}
137 changes: 84 additions & 53 deletions packages/gated-content/src/GatedClient.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
/* eslint-disable no-case-declarations */
import { verifyMessage } from '@ethersproject/wallet';
import * as raw from '@lens-protocol/metadata';
import {
PublicationMetadata,
GatedPublicationMetadata,
EncryptionProvider,
ContentEncryptionKey,
AccessCondition,
EncryptionParamsOutput,
OmitTypename,
RootConditionOutput,
} from '@lens-protocol/api-bindings';
import { PromiseResult, success } from '@lens-protocol/shared-kernel';
assertError,
assertNever,
failure,
PromiseResult,
success,
} from '@lens-protocol/shared-kernel';
import { IStorage, IStorageProvider } from '@lens-protocol/storage';
import { NodeClient } from '@lit-protocol/node-client';
import { JsonEncryptionRetrieveRequest, JsonSaveEncryptionKeyRequest } from '@lit-protocol/types';
import { Signer, utils } from 'ethers';
import { SiweMessage } from 'siwe';

import { AuthSig, createAuthStorage } from './AuthStorage';
import { CannotDecryptError } from './CannotDecryptError';
import { ICipher, IEncryptionProvider } from './IEncryptionProvider';
import { transform } from './conditions';
import { transformFromGql, transformFromRaw } from './conditions';
import {
EncryptedPublicationMetadata,
PublicationMetadataDecryptor,
PublicationMetadataEncryptor,
LegacyPublicationMetadataDecryptor,
} from './encryption';
import { EnvironmentConfig } from './environments';
import * as gql from './graphql';

/**
* The LIT Protocol authentication configuration
Expand All @@ -34,6 +34,11 @@ export type AuthenticationConfig = {
uri: string;
};

export type Signer = {
getAddress(): Promise<string>;
signMessage(message: string): Promise<string>;
};

export type GatedClientConfig = {
authentication: AuthenticationConfig;
environment: EnvironmentConfig;
Expand All @@ -53,8 +58,6 @@ function uint8arrayToHexString(buffer: Uint8Array): string {
return buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
}

export type EncryptionParams = OmitTypename<EncryptionParamsOutput>;

export class GatedClient {
private readonly authentication: AuthenticationConfig;

Expand Down Expand Up @@ -83,10 +86,10 @@ export class GatedClient {
this.litClient = new NodeClient({ debug: false });
}

async encryptPublication(
metadata: PublicationMetadata,
accessCondition: AccessCondition,
): PromiseResult<GatedPublicationMetadata, never> {
async encryptPublicationMetadata<T extends raw.PublicationMetadata>(
metadata: T,
accessCondition: raw.AccessCondition,
): PromiseResult<T, never> {
await this.ensureLitConnection();

const cipher = await this.encryptionProvider.createCipher();
Expand All @@ -95,39 +98,70 @@ export class GatedClient {

const enc = new PublicationMetadataEncryptor(cipher);

const { obfuscated, encryptedFields } = await enc.encrypt(metadata);
const { encrypted, paths } = await enc.encrypt(metadata);

const encryptedMetadata: GatedPublicationMetadata = {
...obfuscated,
encryptionParams: {
providerSpecificParams: {
return success({
...encrypted,
lens: {
...encrypted.lens,
encryptedWith: {
encryptionKey,
accessCondition,
provider: raw.EncryptionProvider.LIT_PROTOCOL,
encryptedPaths: paths,
},
encryptionProvider: EncryptionProvider.LitProtocol,
accessCondition,
encryptedFields,
},
};

return success(encryptedMetadata);
});
}

async decryptPublication<T extends EncryptedPublicationMetadata, P extends EncryptionParams>(
encrypted: T,
using: P,
): PromiseResult<T, never> {
async decryptPublicationMetadataFragment<T extends gql.EncryptedFragmentOfAnyPublicationMetadata>(
encryptedMetadata: T,
): PromiseResult<T, CannotDecryptError> {
await this.ensureLitConnection();

const cipher = await this.retrieveEncryptionKey(
using.providerSpecificParams.encryptionKey,
using.accessCondition,
);
try {
const decrypted = await this.decrypt(encryptedMetadata);
return success(decrypted);
} catch (error) {
assertError(error);

const enc = new PublicationMetadataDecryptor(cipher);
return failure(new CannotDecryptError('Cannot decrypt the metadata', { cause: error }));
}
}

const decrypted = await enc.decrypt(encrypted, using.encryptedFields);
private async decrypt<T extends gql.EncryptedFragmentOfAnyPublicationMetadata>(
encryptedMetadata: T,
): Promise<T> {
const cipher = await this.retrieveCipher(
encryptedMetadata.encryptedWith.encryptionKey,
encryptedMetadata.encryptedWith.accessCondition,
);

switch (encryptedMetadata.__typename) {
case 'LegacyPublicationMetadata':
return new LegacyPublicationMetadataDecryptor(cipher).decrypt(
encryptedMetadata,
) as Promise<T>;

case 'ArticleMetadataV3':
case 'AudioMetadataV3':
case 'CheckingInMetadataV3':
case 'EmbedMetadataV3':
case 'EventMetadataV3':
case 'ImageMetadataV3':
case 'LinkMetadataV3':
case 'LiveStreamMetadataV3':
case 'MintMetadataV3':
case 'SpaceMetadataV3':
case 'StoryMetadataV3':
case 'TextOnlyMetadataV3':
case 'ThreeDMetadataV3':
case 'TransactionMetadataV3':
case 'VideoMetadataV3':
return new PublicationMetadataDecryptor(cipher).decrypt(encryptedMetadata) as Promise<T>;
}

return success(decrypted);
assertNever(encryptedMetadata, `Not supported metadata type`);
}

private async getOrCreateAuthSig(): Promise<AuthSig> {
Expand All @@ -142,7 +176,7 @@ export class GatedClient {

const signature = await this.signer.signMessage(messageToSign);

const recoveredAddress = utils.verifyMessage(messageToSign, signature);
const recoveredAddress = verifyMessage(messageToSign, signature);

const newAuthSig = {
sig: signature,
Expand All @@ -164,8 +198,8 @@ export class GatedClient {

private async saveEncryptionKey(
cipher: ICipher,
accessCondition: AccessCondition,
): Promise<ContentEncryptionKey> {
accessCondition: raw.AccessCondition,
): Promise<raw.LitEncryptionKey> {
const symmetricKey = await cipher.exportKey();

const authSig = await this.getOrCreateAuthSig();
Expand All @@ -174,26 +208,23 @@ export class GatedClient {
authSig,
chain: this.environment.chainName,
symmetricKey,
unifiedAccessControlConditions: transform(
accessCondition as RootConditionOutput,
this.environment,
),
unifiedAccessControlConditions: transformFromRaw(accessCondition, this.environment),
});

return uint8arrayToHexString(encryptedSymmetricKey);
return raw.toLitEncryptionKey(uint8arrayToHexString(encryptedSymmetricKey));
}

private async retrieveEncryptionKey(
encryptedEncryptionKey: ContentEncryptionKey,
accessCondition: RootConditionOutput,
private async retrieveCipher(
encryptedEncryptionKey: gql.Scalars['ContentEncryptionKey'],
accessCondition: gql.AccessCondition,
): Promise<ICipher> {
const authSig = await this.getOrCreateAuthSig();

const encryptionKey = await this.litClient.getEncryptionKey({
authSig,
chain: this.environment.chainName,
toDecrypt: encryptedEncryptionKey,
unifiedAccessControlConditions: transform(accessCondition, this.environment),
unifiedAccessControlConditions: transformFromGql(accessCondition, this.environment),
});

return this.encryptionProvider.importCipher(encryptionKey);
Expand Down
Loading

0 comments on commit a1771b5

Please sign in to comment.