Skip to content
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
18 changes: 18 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: "Setup"
description: "Setup repo and install dependencies"

runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install Dependencies
shell: bash
run: pnpm install --frozen-lockfile
19 changes: 19 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Pull Request
on:
workflow_dispatch:
pull_request:
branches: [main]

defaults:
run:
working-directory: ./typescript

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
verify:
name: Verify
uses: ./.github/workflows/verify.yml
secrets: inherit
62 changes: 62 additions & 0 deletions .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Canary Release

on:
push:
branches: [main]

defaults:
run:
working-directory: ./typescript

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
verify:
name: Verify
uses: ./.github/workflows/verify.yml
secrets: inherit

publish:
name: Publish Snapshot
runs-on: ubuntu-latest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runs-on: [self-hosted, linux, x64, lens, "staging"] -> this will use our own runners

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the same on the other workflows

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any specific reason? nothing particurlarly resource demanding in this workflow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@absis I gave it a try, but the jobs wait forever for the runner to be allocated. Is it possible this setup is only for the lens-protocol org and not the lens-network?

needs: verify

steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "./typescript/.nvmrc"
cache: "pnpm"
cache-dependency-path: "./typescript/pnpm-lock.yaml"

- name: Install Dependencies
shell: bash
run: pnpm install --frozen-lockfile

- name: Configure NPM Auth Token
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NODE_AUTH_TOKEN }}" >> ~/.npmrc

- name: Create Snapshot
id: create_snapshot
run: pnpm changeset version --snapshot next || echo "No unreleased changesets found"

- name: Check for Changeset
id: check_changeset
run: echo "changeset_exists=$(grep -q 'No unreleased changesets found' <<< '${{ steps.create_snapshot.outputs.stdout }}' && echo false || echo true)" >> $GITHUB_ENV

- name: Build
if: env.changeset_exists == 'true'
run: pnpm build

- name: Publish Snapshot
if: env.changeset_exists == 'true'
run: pnpm changeset publish --snapshot --tag next --no-git-tag
60 changes: 60 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: "Verify"
on:
workflow_call:
workflow_dispatch:

defaults:
run:
working-directory: ./typescript

jobs:
lint:
name: Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest

- name: Run Biome
run: biome ci .

test:
name: Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "./typescript/.nvmrc"
cache: "pnpm"
cache-dependency-path: "./typescript/pnpm-lock.yaml"

- name: Install Dependencies
shell: bash
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Setup Environment Variables
shell: bash
run: |
echo "PRIVATE_KEY=${{ secrets.PRIVATE_KEY }}" >> .env
echo "ACCOUNT=${{ vars.ACCOUNT }}" >> .env
echo "ADDRESS=${{ vars.ADDRESS }}" >> .env

- name: Run Tests
run: pnpm test
2 changes: 2 additions & 0 deletions typescript/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
PRIVATE_KEY=
ADDRESS=
ACCOUNT=
4 changes: 3 additions & 1 deletion typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,11 @@ and populate the `PRIVATE_KEY` environment variable:

```bash filename=".env"
PRIVATE_KEY=0x…
ACCOUNT=0x…
ADDRESS=
```

with the private key of a Lens Account owner (needed by Lens Account ACL tests).
with the details of a Lens Account owner (needed by Lens Account ACL tests).

Install the dependencies:

Expand Down
7 changes: 5 additions & 2 deletions typescript/src/AuthorizationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class AuthorizationService {
const challenge = await this.requestChallenge(action, storageKey);

const signature = await signer.signMessage({
message: challenge.message
message: challenge.message,
});

const { challenge_cid } = await this.submitSignedChallenge({
Expand All @@ -45,7 +45,10 @@ export class AuthorizationService {
};
}

private async requestChallenge(action: 'delete' | 'edit', storageKey: string): Promise<Challenge> {
private async requestChallenge(
action: 'delete' | 'edit',
storageKey: string,
): Promise<Challenge> {
const response = await fetch(`${this.env.backend}/challenge/new`, {
method: 'POST',
headers: {
Expand Down
20 changes: 8 additions & 12 deletions typescript/src/StorageClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { privateKeyToAccount } from 'viem/accounts';
import { describe, expect, it } from 'vitest';
import { StorageClient } from './StorageClient';
import { staging } from './environments';
import { testnet } from './environments';
import type { Resource } from './types';
import { lensAccountOnly, never, walletOnly } from './utils';

Expand All @@ -21,7 +21,7 @@ async function assertFileExist(url: string) {
}

describe(`Given an instance of the '${StorageClient.name}'`, () => {
const client = StorageClient.create(staging);
const client = StorageClient.create(testnet);

describe('When testing single file uploads', () => {
it('Then it should create the expected resource', async () => {
Expand Down Expand Up @@ -93,18 +93,16 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => {

describe('When testing file editing with Lens Accounts', () => {
it('Then it should allow editing according to the specified ACL', async () => {
const acl = lensAccountOnly("0x6982508145454Ce325dDbE47a25d4ec3d2311933");
const acl = lensAccountOnly(import.meta.env.ACCOUNT);
const resource = await client.uploadFile(file1, { acl });

await expect(
client.editFile(resource.uri, file2, signer, { acl })
).resolves.toBe(true);
await expect(client.editFile(resource.uri, file2, signer, { acl })).resolves.toBe(true);
});
});

describe('When testing file deletion with Lens Accounts', () => {
it('Then it should allow deletion according to the specified ACL', async () => {
const acl = lensAccountOnly("0x6982508145454Ce325dDbE47a25d4ec3d2311933");
const acl = lensAccountOnly(import.meta.env.ACCOUNT);
const resource = await client.uploadFile(file1, { acl });

await expect(client.delete(resource.uri, signer)).resolves.toBe(true);
Expand All @@ -113,18 +111,16 @@ describe(`Given an instance of the '${StorageClient.name}'`, () => {

describe('When testing file editing with Wallet Addresses', () => {
it('Then it should allow editing according to the specified ACL', async () => {
const acl = walletOnly("0x24d1017aE28A0DD8dd8B4544B7B60E11D5E196eC");
const acl = walletOnly(import.meta.env.ADDRESS);
const resource = await client.uploadFile(file1, { acl });

await expect(
client.editFile(resource.uri, file2, signer, { acl })
).resolves.toBe(true);
await expect(client.editFile(resource.uri, file2, signer, { acl })).resolves.toBe(true);
});
});

describe('When testing file deletion with Wallet Addresses', () => {
it('Then it should allow deletion according to the specified ACL', async () => {
const acl = walletOnly("0x24d1017aE28A0DD8dd8B4544B7B60E11D5E196eC");
const acl = walletOnly(import.meta.env.ADDRESS);
const resource = await client.uploadFile(file1, { acl });

await expect(client.delete(resource.uri, signer)).resolves.toBe(true);
Expand Down
41 changes: 18 additions & 23 deletions typescript/src/StorageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
MultipartEntriesBuilder,
type MultipartEntry,
createMultipartStream,
extractStorageKey as extractStorageKey,
extractStorageKey,
invariant,
parseResource,
resourceFrom,
} from './utils';

Expand Down Expand Up @@ -44,11 +45,8 @@ export class StorageClient {
* @returns The {@link Resource} to the uploaded file
*/
async uploadFile(file: File, options: UploadFileOptions = {}): Promise<Resource> {
const [resource] = await this.requestStorageKeys();
const gatewayUrl = resource.gatewayUrl;
const uri = resource.uri;

const builder = MultipartEntriesBuilder.from([resource.storageKey]).withFile(file);
const [resource] = await this.allocateStorage(1);
const builder = MultipartEntriesBuilder.from([resource]).withFile(file);

if (options.acl) {
builder.withAclTemplate(options.acl, this.env);
Expand All @@ -62,7 +60,7 @@ export class StorageClient {
throw await StorageClientError.fromResponse(response);
}

return resourceFrom(resource.storageKey, gatewayUrl, uri);
return resource;
}

/**
Expand All @@ -78,34 +76,30 @@ export class StorageClient {
options: UploadFolderOptions = {},
): Promise<UploadFolderResponse> {
const needsIndex = 'index' in options && !!options.index;
const [folderResource, ...fileResources] = await this.requestStorageKeys(
const [folderResource, ...fileResources] = await this.allocateStorage(
files.length + (needsIndex ? 2 : 1),
);
const gatewayUrl = folderResource.gatewayUrl;
const uri = folderResource.uri;
const folderHash = folderResource.storageKey;
const fileHashes = fileResources.map(f => f.storageKey);

const builder = MultipartEntriesBuilder.from(fileHashes).withFiles(Array.from(files));
const builder = MultipartEntriesBuilder.from(fileResources).withFiles(Array.from(files));

if (options.index) {
builder.withIndexFile(options.index, gatewayUrl, uri);
builder.withIndexFile(options.index);
}

if (options.acl) {
builder.withAclTemplate(options.acl, this.env);
}

const entries = builder.build();
const response = await this.create(folderHash, entries);
const response = await this.create(folderResource.storageKey, entries);

if (!response.ok) {
throw await StorageClientError.fromResponse(response);
}

return {
folder: resourceFrom(folderHash, gatewayUrl, uri),
files: fileResources.map(fr => resourceFrom(fr.storageKey, fr.gatewayUrl, fr.uri)),
folder: folderResource,
files: fileResources,
};
}

Expand Down Expand Up @@ -149,20 +143,22 @@ export class StorageClient {
* @throws {@link StorageClientError} if editing the file fails
* @throws {@link AuthorizationError} if not authorized to edit the file
* @param storageKeyOrUri - The `lens://…` URI or storage key
* @param file - The file to replace the existing file with
* @param newFile - The file to replace the existing file with
* @param signer - The signer to use for the edit
* @param options - Any additional options for the edit
*/
async editFile(
storageKeyOrUri: string,
file: File,
newFile: File,
signer: Signer,
options: EditFileOptions = {},
): Promise<boolean> {
const storageKey = extractStorageKey(storageKeyOrUri);
const authorization = await this.authorization.authorize('edit', storageKey, signer);

const builder = MultipartEntriesBuilder.from([storageKey]).withFile(file);
const builder = MultipartEntriesBuilder.from([resourceFrom(storageKey, this.env)]).withFile(
newFile,
);

if (options.acl) {
builder.withAclTemplate(options.acl, this.env);
Expand All @@ -174,7 +170,7 @@ export class StorageClient {
return response.ok;
}

private async requestStorageKeys(amount = 1): Promise<[Resource, ...Resource[]]> {
private async allocateStorage(amount: number): Promise<[Resource, ...Resource[]]> {
invariant(amount > 0, 'Amount must be greater than 0');
const response = await fetch(`${this.env.backend}/link/new?amount=${amount}`, {
method: 'POST',
Expand All @@ -185,8 +181,7 @@ export class StorageClient {
}

const data = await response.json();
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
return data.map((entry: any) => resourceFrom(entry.storage_key, entry.gateway_url, entry.uri));
return data.map(parseResource);
}

private async create(storageKey: string, entries: readonly MultipartEntry[]): Promise<Response> {
Expand Down
Loading
Loading