Merge pull request #94 from codex-storage/releases/v0.0.14

Releases/v0.0.14
This commit is contained in:
Arnaud 2025-02-27 08:17:15 +01:00 committed by GitHub
commit d44a8dc95f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2171 additions and 1928 deletions

View File

@ -8,10 +8,11 @@ on:
workflow_dispatch:
env:
codex_version: v0.1.9
circuit_version: v0.1.9
marketplace_address: "0xAB03b6a58C5262f530D54146DA2a552B1C0F7648"
codex_version: v0.2.0
circuit_version: v0.2.0
marketplace_address: "0xfFaF679D5Cbfdd5Dbc9Be61C616ed115DFb597ed"
eth_provider: "https://rpc.testnet.codex.storage"
bootstrap_node: "spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P"
VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }}
VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }}
jobs:
@ -36,7 +37,7 @@ jobs:
key: ${{ env.circuit_version }}-circuits
- name: Download circuits
if: steps.circuits-cache-restore.outputs.cache-hit != 'true'
# if: steps.circuits-cache-restore.outputs.cache-hit != 'true'
run: |
mkdir -p datadir/circuits
chmod 700 datadir
@ -85,13 +86,13 @@ jobs:
chmod 600 eth.key
# Run
./codex-${codex_version}-${platform}-${architecture} --data-dir=datadir --api-cors-origin="*" persistence --eth-provider=${eth_provider} --eth-private-key=./eth.key --marketplace-address=${marketplace_address} prover --circuit-dir=./datadir/circuits &
./codex-${codex_version}-${platform}-${architecture} --data-dir=./datadir --bootstrap-node=${bootstrap_node} --nat=any --disc-port=8090 --api-cors-origin="*" persistence --eth-provider=${eth_provider} --eth-private-key=./eth.key --marketplace-address=${marketplace_address} &
sleep 15
- name: Check Codex API
run: |
curl --max-time 5 --fail localhost:8080/api/codex/v1/debug/info -w "\n"
curl --max-time 10 --fail localhost:8080/api/codex/v1/debug/info -w "\n"
[[ $? -eq 0 ]] && { echo "Codex node is up"; } || { echo "Please check Codex node"; exit 1; }
- uses: actions/checkout@v4

View File

@ -1,121 +1,143 @@
import test, { expect } from "@playwright/test";
import { Bytes } from "../src/utils/bytes"
import { GB } from "../src/utils/constants"
import { Bytes } from "../src/utils/bytes";
import { GB } from "../src/utils/constants";
test('create an availability', async ({ page }) => {
await page.goto('/dashboard/availabilities');
test("create an availability", async ({ page }) => {
await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.locator(".availability-edit button").first().click();
await page.getByLabel("Total size").click();
const value = (Math.random() * 0.5) + 0.1;
const value = Math.random() * 0.5 + 0.1;
await page.getByLabel('Total size').fill(value.toFixed(1));
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill('30');
await page.getByLabel('Min price').click();
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Max collateral').click();
await page.getByLabel('Max collateral').fill('30');
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Nickname').click();
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible();
})
await page.getByLabel("Total size").fill(value.toFixed(1));
await page.getByLabel("Duration").click();
await page.getByLabel("Duration").fill("30");
await page.getByLabel("Min price").click();
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Total collateral").click();
await page.getByLabel("Total collateral").fill("30");
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Nickname").click();
await page.getByLabel("Nickname").fill("test");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Confirm your new sale")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Success", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(
page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()
).toBeVisible();
});
test('availability navigation buttons', async ({ page }) => {
await page.goto('/dashboard/availabilities');
test("availability navigation buttons", async ({ page }) => {
await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('19');
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('0.5');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Back' }).click();
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.step--done')).toHaveCount(2)
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('.modal--open')).not.toBeVisible();
})
await page.locator(".availability-edit button").first().click();
await expect(page.locator(".stepper-number-done")).not.toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await expect(
page.locator("footer .button--outline").first()
).not.toHaveAttribute("disabled");
await page.getByLabel("Total size").click();
await page.getByLabel("Total size").fill("");
await page.getByLabel("Duration").click();
test('create an availability with changing the duration to months', async ({ page }) => {
await page.goto('/dashboard/availabilities');
await expect(page.locator("footer .button--primary")).toHaveAttribute(
"disabled",
{ timeout: 3000 }
);
await page.getByLabel("Total size").click();
await page.getByLabel("Total size").fill("0.5");
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.locator("footer .button--outline").first()
).not.toHaveAttribute("disabled");
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await expect(page.locator(".step--done")).toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await page.getByRole("button", { name: "Back" }).click();
await expect(page.locator(".step--done")).not.toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: "Next" }).click();
await expect(page.locator(".step--done")).toHaveCount(2);
await expect(page.locator(".step--active")).toBeVisible();
await expect(page.locator("footer .button--outline").first()).toHaveAttribute(
"disabled"
);
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.locator(".modal--open")).not.toBeVisible();
});
test("create an availability with changing the duration to months", async ({
page,
}) => {
await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.locator(".availability-edit button").first().click();
await page.getByLabel("Total size").click();
await page.getByLabel('Total size').fill("0.1");
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill("3");
await page.getByRole('combobox').nth(1).selectOption('months');
await page.getByLabel("Total size").fill("0.1");
await page.getByLabel("Duration").click();
await page.getByLabel("Duration").fill("3");
await page.getByRole("combobox").nth(1).selectOption("months");
await page.getByLabel('Min price').click();
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Max collateral').click();
await page.getByLabel('Max collateral').fill('30');
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Nickname').click();
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await page.getByLabel("Min price").click();
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Total collateral").click();
await page.getByLabel("Total collateral").fill("30");
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Nickname").click();
await page.getByLabel("Nickname").fill("test");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Confirm your new sale")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Success", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText("3 months").first()).toBeVisible();
})
});
test('create an availability after checking max size and invalid input', async ({ page }) => {
await page.goto('/dashboard/availabilities');
test("create an availability after checking max size and invalid input", async ({
page,
}) => {
await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.locator(".availability-edit button").first().click();
await page.getByLabel("Total size").click();
await page.getByLabel("Total size").fill("9999");
await expect(page.getByLabel("Total size")).toHaveAttribute("aria-invalid");
await page.getByLabel('Total size').fill("9999");
await expect(page.getByLabel('Total size')).toHaveAttribute("aria-invalid");
await page.getByText("Use max size").click();
await expect(page.getByLabel("Total size")).not.toHaveAttribute(
"aria-invalid"
);
await page.getByText("Use max size").click()
await expect(page.getByLabel('Total size')).not.toHaveAttribute("aria-invalid");
const value = 0.2;
await page.getByLabel("Total size").fill(value.toString());
const value = (Math.random() * 0.5);
await page.getByLabel('Total size').fill(value.toFixed(1));
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill('30');
await page.getByLabel('Min price').click();
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Max collateral').click();
await page.getByLabel('Max collateral').fill('30');
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Nickname').click();
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible();
})
await page.getByLabel("Duration").click();
await page.getByLabel("Duration").fill("30");
await page.getByLabel("Min price").click();
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Total collateral").click();
await page.getByLabel("Total collateral").fill("30");
await page.getByLabel("Min price").fill("5");
await page.getByLabel("Nickname").click();
await page.getByLabel("Nickname").fill("test");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Confirm your new sale")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Success", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(
page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()
).toBeVisible();
});

View File

@ -1,29 +1,34 @@
import { test, expect } from '@playwright/test';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { test, expect } from "@playwright/test";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
test('download a file', async ({ page, browserName }) => {
test("download a file", async ({ page, browserName }) => {
// https://github.com/microsoft/playwright/issues/13037
test.skip(browserName.toLowerCase() !== 'chromium',
`Test only for chromium!`);
test.skip(
browserName.toLowerCase() !== "chromium",
`Test only for chromium!`
);
await page.goto('/dashboard');
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await page.goto("/dashboard");
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await page.locator('.file-cell button').first().click();
const handle = await page.evaluateHandle(() => navigator.clipboard.readText());
const cid = await handle.jsonValue()
await page.locator(".file-cell button").first().click();
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText()
);
const cid = await handle.jsonValue();
await page.locator('.download-input input').fill(cid);
await page.locator(".download-input input").fill(cid);
// const page1Promise = page.waitForEvent('popup');
const downloadPromise = page.waitForEvent('download');
await page.locator('.download-input + button').click();
const downloadPromise = page.waitForEvent("download");
await page.locator(".download-input + button").click();
// const page1 = await page1Promise;
const download = await downloadPromise;
expect(await download.failure()).toBeNull()
expect(await download.failure()).toBeNull();
});

View File

@ -1,58 +1,85 @@
import { test, expect, } from '@playwright/test';
import { test, expect } from "@playwright/test";
test.describe('onboarding', () => {
test('onboarding steps', async ({ page, browserName }) => {
await page.context().setOffline(false)
await page.goto('/');
await expect(page.getByText("Codex is a durable, decentralised data storage protocol, created so the world community can preserve its most important knowledge without risk of censorship.")).toBeVisible()
await page.locator('.navigation').click();
await expect(page.locator('.navigation')).toHaveAttribute("aria-disabled");
await page.getByLabel('Preferred name').fill('Arnaud');
await expect(page.locator('.navigation')).not.toHaveAttribute("aria-disabled");
await page.locator('.navigation').click();
test.describe("onboarding", () => {
test("onboarding steps", async ({ page, browserName }) => {
await page.context().setOffline(false);
await page.goto("/");
await expect(
page.getByText(
"Codex is a durable, decentralised data storage protocol, created so the world community can preserve its most important knowledge without risk of censorship."
)
).toBeVisible();
await page.locator(".navigation").click();
await expect(page.locator(".navigation")).toHaveAttribute("aria-disabled");
await page.getByLabel("Preferred name").fill("Arnaud");
await expect(page.locator(".navigation")).not.toHaveAttribute(
"aria-disabled"
);
await page.locator(".navigation").click();
// Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).toBeVisible()
await expect(
page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")
).not.toBeVisible();
await expect(
page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")
).toBeVisible();
// Port forwarding
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible()
await expect(
page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")
).not.toBeVisible();
await expect(
page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")
).toBeVisible();
// Codex node
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible()
await expect(
page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")
).not.toBeVisible();
await expect(
page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")
).toBeVisible();
// Marketplace
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-warning")).toBeVisible({ timeout: 10_000 })
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).not.toBeVisible()
//await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-warning")).toBeVisible({ timeout: 10_000 })
await expect(page.locator(".health-checks ul li").nth(3)).toBeVisible();
// Can be simulated with File -> Work offline
if (browserName.toLowerCase() !== 'firefox') {
await page.context().setOffline(true)
if (browserName.toLowerCase() !== "firefox") {
await page.context().setOffline(true);
// Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).toBeVisible()
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).not.toBeVisible()
await expect(
page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")
).toBeVisible();
await expect(
page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")
).not.toBeVisible();
await page.context().setOffline(false)
await page.context().setOffline(false);
}
});
test.beforeEach(async ({ page }) => {
await page.context().setOffline(false)
await page.context().setOffline(false);
});
});
})
test('does not display undefined when delete the url value', async ({ page }) => {
await page.goto('/onboarding-checks');
await page.locator('#url').focus()
test("does not display undefined when delete the url value", async ({
page,
}) => {
await page.goto("/onboarding-checks");
await page.locator("#url").focus();
for (let i = 0; i < "http://localhost:8080".length; i++) {
await page.keyboard.press('Backspace');
await page.keyboard.press("Backspace");
}
await expect(page.locator('#url')).toHaveValue("");
await expect(page.locator('#url')).toHaveAttribute("aria-invalid")
await expect(page.locator('.refresh svg')).toHaveAttribute("color", "#494949")
await expect(page.locator("#url")).toHaveValue("");
await expect(page.locator("#url")).toHaveAttribute("aria-invalid");
await expect(page.locator(".refresh svg")).toHaveAttribute(
"color",
"#494949"
);
});

View File

@ -1,113 +1,158 @@
import test, { expect } from "@playwright/test";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { Times } from "../src/utils/times";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
test('create a storage request', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
test("create a storage request", async ({ page }) => {
await page.goto("/dashboard");
await page.locator("a").filter({ hasText: "Purchases" }).click();
await page.getByRole("button", { name: "Storage Request" }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await expect(page.getByText('Success, the CID has been')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Your request is being processed.')).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('No data.')).not.toBeVisible();
await expect(page.getByTestId('cell-pending').first()).toBeVisible();
})
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await expect(page.locator("#cid")).not.toBeEmpty();
await expect(page.getByText("Success, the CID has been")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.getByText("Your request is being processed.")
).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText("No data.")).not.toBeVisible();
await expect(page.getByTestId("cell-pending").first()).toBeVisible();
});
test('select a uploaded cid when creating a storage request', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.getByPlaceholder('CID').click();
await page.locator('.dropdown ul li').nth(1).click();
await expect(page.getByText('button[disabled]')).not.toBeVisible();
})
test("select a uploaded cid when creating a storage request", async ({
page,
}) => {
await page.goto("/dashboard");
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await page.locator("a").filter({ hasText: "Purchases" }).click();
await page.getByRole("button", { name: "Storage Request" }).click();
await page.getByPlaceholder("CID").click();
await page.locator(".dropdown ul li").nth(1).click();
await expect(page.getByText("button[disabled]")).not.toBeVisible();
});
test('storage request navigation buttons', async ({ page }) => {
await page.goto('/dashboard/purchases');
await page.getByRole('button', { name: 'Storage Request' }).click();
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Back' }).click();
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.step--done')).toHaveCount(2)
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('.modal--open')).not.toBeVisible();
})
test("storage request navigation buttons", async ({ page }) => {
await page.goto("/dashboard/purchases");
await page.getByRole("button", { name: "Storage Request" }).click();
await expect(page.locator(".step--done")).not.toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await expect(page.locator("footer .button--primary")).toHaveAttribute(
"disabled"
);
await expect(
page.locator("footer .button--outline").first()
).not.toHaveAttribute("disabled");
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await expect(
page.locator("footer .button--outline").first()
).not.toHaveAttribute("disabled");
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.locator("footer .button--outline").first()
).not.toHaveAttribute("disabled");
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await expect(page.locator(".step--done")).toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await page.getByRole("button", { name: "Back" }).click();
await expect(page.locator(".step--done")).not.toBeVisible();
await expect(page.locator(".step--active")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: "Next" }).click();
await expect(page.locator(".step--done")).toHaveCount(2);
await expect(page.locator(".step--active")).toBeVisible();
await expect(page.locator("footer .button--outline").first()).toHaveAttribute(
"disabled"
);
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.locator(".modal--open")).not.toBeVisible();
});
test('remove the CID when the file is deleted', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await page.locator('.button-icon--small').nth(1).click();
await expect(page.locator('#cid')).toBeEmpty()
})
test("remove the CID when the file is deleted", async ({ page }) => {
await page.goto("/dashboard");
await page.locator("a").filter({ hasText: "Purchases" }).click();
await page.getByRole("button", { name: "Storage Request" }).click();
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await expect(page.locator("#cid")).not.toBeEmpty();
await page.locator(".button-icon--small").nth(1).click();
await expect(page.locator("#cid")).toBeEmpty();
});
test('create a storage request by using decimal values', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
test("create a storage request by using decimal values", async ({ page }) => {
await page.goto("/dashboard");
await page.locator("a").filter({ hasText: "Settings" }).click();
await page.getByLabel("Address").fill("http://127.0.0.1:8080");
await page.locator(".refresh").click();
await page.locator("a").filter({ hasText: "Purchases" }).click();
await page.getByRole("button", { name: "Storage Request" }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await expect(page.getByText('Success, the CID has been')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await page
.locator("div")
.getByTestId("upload")
.setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await expect(page.locator("#cid")).not.toBeEmpty();
await expect(page.getByText("Success, the CID has been")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await page.getByLabel("Full period of the contract").fill("10")
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await page.getByLabel("Full period of the contract").fill("10");
await expect(page.locator("footer .button--primary")).toHaveAttribute(
"disabled"
);
await page.getByLabel("Full period of the contract").fill("1")
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByLabel("Full period of the contract").fill("1");
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await page.getByLabel("Full period of the contract").fill("0")
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await page.getByLabel("Full period of the contract").fill("0");
await expect(page.locator("footer .button--primary")).toHaveAttribute(
"disabled"
);
const value = (Math.random() * 7);
await page.getByLabel("Full period of the contract").fill(value.toFixed(1))
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
const days = 4;
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Your request is being processed.')).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('No data.')).not.toBeVisible();
await expect(page.getByText(value.toFixed(1) + " days").first()).toBeVisible();
})
await page.getByLabel("Full period of the contract").fill(days.toFixed(1));
await expect(page.locator("footer .button--primary")).not.toHaveAttribute(
"disabled"
);
await page.getByRole("button", { name: "Next" }).click();
await expect(
page.getByText("Your request is being processed.")
).toBeVisible();
await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText("No data.")).not.toBeVisible();
const oneDay = 24 * 60 * 60;
await expect(
page.getByText(Times.pretty(days * oneDay)).first()
).toBeVisible();
});
// test('create a storage request by using months', async ({ page }) => {
// await page.goto('/dashboard');

2422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"type": "git",
"url": "https://github.com/codex-storage/codex-marketplace-ui"
},
"version": "0.0.13",
"version": "0.0.14",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5173",
@ -27,7 +27,7 @@
],
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.51",
"@codex-storage/sdk-js": "^0.0.16",
"@codex-storage/sdk-js": "^0.0.22",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
"@tanstack/react-query": "^5.51.15",
@ -60,9 +60,9 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.5.4",
"vite": "^5.4.7",
"vite": "^6.1.0",
"vite-plugin-svgr": "^4.3.0",
"vitest": "^2.1.4"
"vitest": "^3.0.5"
},
"engines": {
"node": ">=18"

View File

@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from "@playwright/test";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -7,33 +7,33 @@ const __dirname = dirname(__filename);
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import dotenv from 'dotenv';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
dotenv.config({ path: path.resolve(__dirname, '.env.ci') });
import dotenv from "dotenv";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
dotenv.config({ path: path.resolve(__dirname, ".env.ci") });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
testDir: "./e2e",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:5173',
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: "on-first-retry",
screenshot: "only-on-failure",
},
@ -41,18 +41,18 @@ export default defineConfig({
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
@ -78,8 +78,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run preview',
url: 'http://127.0.0.1:5173',
command: "npm run preview",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

View File

@ -57,8 +57,8 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
["id", onSortById],
["total size", onSortBySize],
["duration", onSortByDuration],
["min price", onSortByPrice],
["max collateral", onSortByCollateral],
["min price per byte", onSortByPrice],
["remaining collateral", onSortByCollateral],
["actions"],
] satisfies [string, ((state: TabSortState) => void)?][];
@ -88,8 +88,8 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
<AvailabilityIdCell value={a} />,
<Cell>{Bytes.pretty(a.totalSize)}</Cell>,
<Cell>{Times.pretty(a.duration)}</Cell>,
<Cell>{a.minPrice.toString()}</Cell>,
<Cell>{a.maxCollateral.toString()}</Cell>,
<Cell>{a.minPricePerBytePerSecond.toString()}</Cell>,
<Cell>{a.totalRemainingCollateral.toString()}</Cell>,
<AvailabilityActionsCell availability={a} />,
]}></Row>

View File

@ -10,7 +10,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { AvailabilityConfirm } from "./AvailabilityConfirmation";
import { WebStorage } from "../../utils/web-storage";
import { AvailabilityState } from "./types";
import { STEPPER_DURATION } from "../../utils/constants";
import { GB, STEPPER_DURATION } from "../../utils/constants";
import { useAvailabilityMutation } from "./useAvailabilityMutation";
import { AvailabilitySuccess } from "./AvailabilitySuccess";
import { AvailabilityError } from "./AvailabilityError";
@ -30,8 +30,8 @@ const CONFIRM_STATE = 2;
const defaultAvailabilityData: AvailabilityState = {
totalSize: 0.5,
duration: 1,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalSizeUnit: "gb",
durationUnit: "days",
};
@ -143,6 +143,9 @@ export function AvailabilityEdit({
setAvailability({
...a,
totalSize: a.totalSize / GB,
totalSizeUnit: "gb",
duration: a.duration / Times.value(unit),
durationUnit: unit as "hours" | "days" | "months",
});

View File

@ -184,32 +184,35 @@ export function AvailabilityForm({
<div className="row gap">
<div className="group">
<Input
id="minPrice"
name="minPrice"
id="minPricePerBytePerSecond"
name="minPricePerBytePerSecond"
type="number"
label="Min price"
label="Min price per byte per second"
min={0}
onChange={onInputChange}
value={availability.minPrice.toString()}
value={availability.minPricePerBytePerSecond.toString()}
/>
<Tooltip message={"Minimum price to be paid (in amount of tokens)"}>
<Tooltip
message={
"inimal price per byte per second paid (in amount of tokens) for the hosted request's slot for the request's duration"
}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
<div className="group">
<Input
id="maxCollateral"
name="maxCollateral"
id="totalCollateral"
name="totalCollateral"
type="number"
label="Max collateral"
label="Total collateral"
min={0}
onChange={onInputChange}
value={availability.maxCollateral.toString()}
value={availability.totalCollateral.toString()}
/>
<Tooltip
message={
"Maximum collateral user is willing to pay per filled Slot (in amount of tokens)"
"Total collateral (in amount of tokens) that can be used for matching requests"
}>
<InfoIcon></InfoIcon>
</Tooltip>

View File

@ -22,7 +22,8 @@ export function AvailabilityIdCell({ value }: Props) {
</small>
<br />
<small className="text--light">
Max collateral {value.maxCollateral} | Min price {value.minPrice}
Collateral {value.totalCollateral} | Min price{" "}
{value.minPricePerBytePerSecond}
</small>
</div>
</div>

View File

@ -68,11 +68,11 @@ export function Sunburst({ availabilities, space }: Props) {
"Duration " +
Times.pretty(a.duration) +
"<br/>" +
"Max collateral " +
a.maxCollateral +
"Total remaining collateral " +
a.totalRemainingCollateral +
"<br/>" +
"Min price " +
a.minPrice +
"Min price per byte per second " +
a.minPricePerBytePerSecond +
"<br/>" +
"Size " +
Bytes.pretty(a.totalSize)

View File

@ -10,28 +10,32 @@ describe("files", () => {
id: "a",
totalSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 0,
freeSize: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const b = {
id: "b",
totalSize: 0,
freeSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const items = [a, b,]
const items = [a, b];
const descSorted = items.slice().sort(AvailabilityUtils.sortById("desc"))
const descSorted = items.slice().sort(AvailabilityUtils.sortById("desc"));
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(AvailabilityUtils.sortById("asc"))
const ascSorted = items.slice().sort(AvailabilityUtils.sortById("asc"));
assert.deepEqual(ascSorted, [a, b]);
});
@ -40,29 +44,33 @@ describe("files", () => {
const a = {
id: "",
totalSize: 1,
freeSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const b = {
id: "",
totalSize: 2,
freeSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const items = [a, b,]
const items = [a, b];
const descSorted = items.slice().sort(AvailabilityUtils.sortBySize("desc"))
const descSorted = items.slice().sort(AvailabilityUtils.sortBySize("desc"));
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(AvailabilityUtils.sortBySize("asc"))
const ascSorted = items.slice().sort(AvailabilityUtils.sortBySize("asc"));
assert.deepEqual(ascSorted, [a, b]);
});
@ -71,29 +79,37 @@ describe("files", () => {
const a = {
id: "",
totalSize: 0,
freeSize: 0,
duration: 1,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const b = {
id: "",
totalSize: 0,
freeSize: 0,
duration: 2,
minPrice: 0,
maxCollateral: 0,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const items = [a, b,]
const items = [a, b];
const descSorted = items.slice().sort(AvailabilityUtils.sortByDuration("desc"))
const descSorted = items
.slice()
.sort(AvailabilityUtils.sortByDuration("desc"));
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(AvailabilityUtils.sortByDuration("asc"))
const ascSorted = items
.slice()
.sort(AvailabilityUtils.sortByDuration("asc"));
assert.deepEqual(ascSorted, [a, b]);
});
@ -102,29 +118,35 @@ describe("files", () => {
const a = {
id: "",
totalSize: 0,
freeSize: 0,
duration: 0,
minPrice: 1,
maxCollateral: 0,
minPricePerBytePerSecond: 1,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const b = {
id: "",
freeSize: 0,
totalSize: 0,
duration: 0,
minPrice: 2,
maxCollateral: 0,
minPricePerBytePerSecond: 2,
totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
slots: []
}
slots: [],
};
const items = [a, b,]
const items = [a, b];
const descSorted = items.slice().sort(AvailabilityUtils.sortByPrice("desc"))
const descSorted = items
.slice()
.sort(AvailabilityUtils.sortByPrice("desc"));
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(AvailabilityUtils.sortByPrice("asc"))
const ascSorted = items.slice().sort(AvailabilityUtils.sortByPrice("asc"));
assert.deepEqual(ascSorted, [a, b]);
});
@ -133,29 +155,37 @@ describe("files", () => {
const a = {
id: "",
totalSize: 0,
freeSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 1,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 1,
name: "",
slots: []
}
slots: [],
};
const b = {
id: "",
totalSize: 0,
freeSize: 0,
duration: 0,
minPrice: 0,
maxCollateral: 2,
minPricePerBytePerSecond: 0,
totalCollateral: 0,
totalRemainingCollateral: 2,
name: "",
slots: []
}
slots: [],
};
const items = [a, b,]
const items = [a, b];
const descSorted = items.slice().sort(AvailabilityUtils.sortByCollateral("desc"))
const descSorted = items
.slice()
.sort(AvailabilityUtils.sortByCollateral("desc"));
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(AvailabilityUtils.sortByCollateral("asc"))
const ascSorted = items
.slice()
.sort(AvailabilityUtils.sortByCollateral("asc"));
assert.deepEqual(ascSorted, [a, b]);
});
@ -163,33 +193,41 @@ describe("files", () => {
it("returns the number of bytes per unit", async () => {
assert.deepEqual(AvailabilityUtils.toUnit(GB, "gb"), 1);
assert.deepEqual(AvailabilityUtils.toUnit(TB, "tb"), 1);
})
});
it("returns the max value possible for an availability", async () => {
const space: CodexNodeSpace = {
quotaMaxBytes: 8 * GB,
quotaReservedBytes: 2 * GB,
quotaUsedBytes: GB,
totalBlocks: 0
}
totalBlocks: 0,
};
assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB - 1);
})
});
it("checks the availability max value", async () => {
const availability = {
totalSizeUnit: "gb",
totalSize: 1
} as AvailabilityState
totalSize: 1,
} as AvailabilityState;
assert.deepEqual(AvailabilityUtils.isValid(availability, GB * 2), true);
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB), false);
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB), false);
})
assert.deepEqual(
AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB),
false
);
assert.deepEqual(
AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB),
false
);
});
it("toggles item in array", async () => {
const array: string[] = []
const array: string[] = [];
assert.deepEqual(AvailabilityUtils.toggle(array, "1"), ["1"]);
assert.deepEqual(AvailabilityUtils.toggle(AvailabilityUtils.toggle(array, "1"), "1"), []);
})
})
assert.deepEqual(
AvailabilityUtils.toggle(AvailabilityUtils.toggle(array, "1"), "1"),
[]
);
});
});

View File

@ -1,57 +1,55 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components"
import { AvailabilityState, AvailabilityWithSlots } from "./types"
import { TabSortState } from "@codex-storage/marketplace-ui-components";
import { AvailabilityState, AvailabilityWithSlots } from "./types";
import { GB, TB } from "../../utils/constants";
import { CodexNodeSpace } from "@codex-storage/sdk-js";
export const AvailabilityUtils = {
sortById: (state: TabSortState) =>
sortById:
(state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => {
return state === "desc"
? b.id
.toLocaleLowerCase()
.localeCompare(a.id.toLocaleLowerCase())
: a.id
.toLocaleLowerCase()
.localeCompare(b.id.toLocaleLowerCase())
? b.id.toLocaleLowerCase().localeCompare(a.id.toLocaleLowerCase())
: a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase());
},
sortBySize: (state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc"
? b.totalSize - a.totalSize
: a.totalSize - b.totalSize
,
sortByDuration: (state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc"
? b.duration - a.duration
: a.duration - b.duration
,
sortByPrice: (state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc"
? b.minPrice - a.minPrice
: a.minPrice - b.minPrice
,
sortByCollateral: (state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc"
? b.maxCollateral - a.maxCollateral
: a.maxCollateral - b.maxCollateral
,
sortBySize:
(state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
state === "desc" ? b.totalSize - a.totalSize : a.totalSize - b.totalSize,
sortByDuration:
(state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
state === "desc" ? b.duration - a.duration : a.duration - b.duration,
sortByPrice:
(state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
state === "desc"
? b.minPricePerBytePerSecond - a.minPricePerBytePerSecond
: a.minPricePerBytePerSecond - b.minPricePerBytePerSecond,
sortByCollateral:
(state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
state === "desc"
? b.totalRemainingCollateral - a.totalRemainingCollateral
: a.totalRemainingCollateral - b.totalRemainingCollateral,
toUnit(bytes: number, unit: "gb" | "tb") {
return bytes / this.unitValue(unit || "gb")
return bytes / this.unitValue(unit || "gb");
},
maxValue(space: CodexNodeSpace) {
// Remove 1 byte to allow to create an availability with the max space possible
return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes - 1
return (
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes - 1
);
},
unitValue(unit: "gb" | "tb") {
return unit === "tb" ? TB : GB
return unit === "tb" ? TB : GB;
},
isValid: (
availability: AvailabilityState,
max: number
) => availability.totalSize > 0 && availability.totalSize * AvailabilityUtils.unitValue(availability.totalSizeUnit) <= max
,
isValid: (availability: AvailabilityState, max: number) =>
availability.totalSize > 0 &&
availability.totalSize *
AvailabilityUtils.unitValue(availability.totalSizeUnit) <=
max,
toggle: <T>(arr: Array<T>, value: T) =>
arr.includes(value) ? arr.filter(i => i !== value) : [...arr, value],
arr.includes(value) ? arr.filter((i) => i !== value) : [...arr, value],
availabilityColors: [
"#34A0FFFF",
@ -90,4 +88,4 @@ export const AvailabilityUtils = {
"#D2493C11",
"#D2493C00",
],
}
};

View File

@ -14,8 +14,8 @@ export type AvailabilityState = {
totalSize: number;
duration: number;
durationUnit: "hours" | "days" | "months";
minPrice: number;
maxCollateral: number;
minPricePerBytePerSecond: number;
totalCollateral: number;
totalSizeUnit: "gb" | "tb";
name?: string;
};

View File

@ -12,7 +12,6 @@ import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js";
import { Times } from "../../utils/times";
import { AvailabilityUtils } from "./availability.utils";
export function useAvailabilityMutation(
dispatch: Dispatch<StepperAction>,
state: StepperState
@ -33,10 +32,17 @@ export function useAvailabilityMutation(
const fn: (
input: Omit<AvailabilityState, "totalSizeUnit" | "durationUnit">
) => Promise<"" | CodexAvailabilityCreateResponse> = input.id
? (input) =>
CodexSdk.marketplace()
.updateAvailability({ ...input, id: input.id || "" })
.then((s) => Promises.rejectOnError(s))
? (input) => {
return CodexSdk.marketplace()
.updateAvailability({
totalSize: input.totalSize,
duration: input.duration,
minPricePerBytePerSecond: input.minPricePerBytePerSecond,
totalCollateral: input.totalCollateral,
id: input.id || "",
})
.then((s) => Promises.rejectOnError(s));
}
: (input) =>
CodexSdk.marketplace()
.createAvailability(input)
@ -45,7 +51,9 @@ export function useAvailabilityMutation(
return fn({
...input,
duration: Times.value(durationUnit) * duration,
totalSize: Math.trunc(totalSize * AvailabilityUtils.unitValue(totalSizeUnit)),
totalSize: Math.trunc(
totalSize * AvailabilityUtils.unitValue(totalSizeUnit)
),
});
},
onSuccess: (res, body) => {
@ -56,7 +64,7 @@ export function useAvailabilityMutation(
WebStorage.delete("availability-step");
if (typeof res === "object" && body.name) {
WebStorage.availabilities.add(res.id, body.name)
WebStorage.availabilities.add(res.id, body.name);
}
setError(null);

View File

@ -7,6 +7,7 @@ import {
import "./FileCell.css";
import { WebStorage } from "../../utils/web-storage";
import { CodexDataContent } from "@codex-storage/sdk-js";
import { FilesUtils } from "../Files/files.utils";
type FileMetadata = {
mimetype: string | null;
@ -37,11 +38,10 @@ export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) {
const content = data.find((m) => m.cid === cid);
if (content) {
const {
filename = "-",
mimetype = "application/octet-stream",
uploadedAt = 0,
} = content.manifest;
const { filename = "-", mimetype = "application/octet-stream" } =
content.manifest;
const uploadedAt = FilesUtils.getUploadedAt(content.cid);
setMetadata({
filename,
mimetype,

View File

@ -117,7 +117,7 @@ export function FileDetails({ onClose, details }: Props) {
<p>Date:</p>
<p>
{FilesUtils.formatDate(
details.manifest.uploadedAt
FilesUtils.getUploadedAt(details.cid)
).toString()}
</p>
</li>

View File

@ -166,7 +166,7 @@ export function Files({ limit }: Props) {
<FileCell content={c}></FileCell>,
<Cell>{Bytes.pretty(c.manifest.datasetSize)}</Cell>,
<Cell>
{FilesUtils.formatDate(c.manifest.uploadedAt).toString()}
{FilesUtils.formatDate(FilesUtils.getUploadedAt(c.cid)).toString()}
</Cell>,
<FileActions
content={c}

View File

@ -20,27 +20,31 @@ export const FilesUtils = {
return !!type && type.startsWith("video");
},
isArchive(mimetype: string | null) {
return !!mimetype && archiveMimetypes.includes(mimetype)
return !!mimetype && archiveMimetypes.includes(mimetype);
},
type(mimetype: string | null) {
if (FilesUtils.isArchive(mimetype)) {
return "archive"
return "archive";
}
if (FilesUtils.isVideo(mimetype)) {
return "video"
return "video";
}
if (FilesUtils.isImage(mimetype)) {
return "image"
return "image";
}
return "document"
return "document";
},
sortByName: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => {
const { manifest: { filename: afilename } } = a
const { manifest: { filename: bfilename } } = b
sortByName:
(state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) => {
const {
manifest: { filename: afilename },
} = a;
const {
manifest: { filename: bfilename },
} = b;
return state === "desc"
? (bfilename || "")
@ -48,47 +52,63 @@ export const FilesUtils = {
.localeCompare((afilename || "").toLocaleLowerCase())
: (afilename || "")
.toLocaleLowerCase()
.localeCompare((bfilename || "").toLocaleLowerCase())
.localeCompare((bfilename || "").toLocaleLowerCase());
},
sortBySize: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => state === "desc"
sortBySize:
(state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) =>
state === "desc"
? b.manifest.datasetSize - a.manifest.datasetSize
: a.manifest.datasetSize - b.manifest.datasetSize
,
sortByDate: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => state === "desc"
? new Date(b.manifest.uploadedAt).getTime() -
new Date(a.manifest.uploadedAt).getTime()
: new Date(a.manifest.uploadedAt).getTime() -
new Date(b.manifest.uploadedAt).getTime()
,
removeCidFromFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
return folders.map(([name, files]) =>
name === folder
? [name, files.filter((id) => id !== cid)]
: [name, files]
)
: a.manifest.datasetSize - b.manifest.datasetSize,
sortByDate:
(state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) => {
const aUploadedAt = FilesUtils.getUploadedAt(a.cid);
const bUploadedAt = FilesUtils.getUploadedAt(b.cid);
return state === "desc"
? new Date(bUploadedAt).getTime() - new Date(aUploadedAt).getTime()
: new Date(aUploadedAt).getTime() - new Date(bUploadedAt).getTime();
},
addCidToFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
removeCidFromFolder(
folders: [string, string[]][],
folder: string,
cid: string
): [string, string[]][] {
return folders.map(([name, files]) =>
name === folder ? [name, files.filter((id) => id !== cid)] : [name, files]
);
},
addCidToFolder(
folders: [string, string[]][],
folder: string,
cid: string
): [string, string[]][] {
return folders.map(([name, files]) =>
name === folder ? [name, [...files, cid]] : [name, files]
)
);
},
exists(folders: [string, string[]][], name: string) {
return !!folders.find(([folder]) => folder === name)
return !!folders.find(([folder]) => folder === name);
},
toggleFilters: (filters: string[], filter: string) => filters.includes(filter)
toggleFilters: (filters: string[], filter: string) =>
filters.includes(filter)
? filters.filter((f) => f !== filter)
: [...filters, filter],
listInFolder(files: CodexDataContent[], folders: [string, string[]][], index: number) {
listInFolder(
files: CodexDataContent[],
folders: [string, string[]][],
index: number
) {
return index === 0
? files
: files.filter((file) => folders[index - 1][1].includes(file.cid));
},
applyFilters(files: CodexDataContent[], filters: string[]) {
return files.filter(
(file) => filters.length === 0 || filters.includes(this.type(file.manifest.mimetype))
)
(file) =>
filters.length === 0 ||
filters.includes(this.type(file.manifest.mimetype))
);
},
formatDate(date: number) {
if (!date) {
@ -99,7 +119,14 @@ export const FilesUtils = {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(date * 1000));
}
},
getUploadedAt(key: string) {
return parseInt(localStorage.getItem(key + "-uploadedAt") || "0", 10);
},
setUploadedAt(key: string, value: number) {
localStorage.setItem(key + "-uploadedAt", value.toString());
},
};
export type CodexFileMetadata = {

View File

@ -8,7 +8,6 @@ import { classnames } from "../../utils/classnames";
import "./HealthChecks.css";
import { CodexSdk } from "../../sdk/codex";
import { HealthCheckUtils } from "./health-check.utils";
import { PortForwardingUtil } from "../../hooks/port-forwarding.util";
import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
import DeviceIcon from "../../assets/icons/device.svg?react";
@ -21,7 +20,6 @@ type Props = {
};
const throwOnError = false;
const defaultPort = 8070;
export function HealthChecks({ online, onStepValid }: Props) {
const codex = useDebug(throwOnError);
@ -87,15 +85,6 @@ export function HealthChecks({ online, onStepValid }: Props) {
.then(() => codex.refetch());
};
let forwardingPortValue = defaultPort;
if (codex.isSuccess && codex.data) {
const port = PortForwardingUtil.getTcpPort(codex.data);
if (!port.error) {
forwardingPortValue = port.data;
}
}
return (
<div className="health-checks">
<div
@ -140,10 +129,7 @@ export function HealthChecks({ online, onStepValid }: Props) {
</div>
<p>
<li>
Port forwarding should be {forwardingPortValue} for TCP and 8090 by
default for UDP.
</li>
<li>Ensure that port forwarding is enabled for your settings.</li>
</p>
<ul>

View File

@ -11,6 +11,17 @@ export function ManifestFetch() {
const { refetch } = useQuery({
queryFn: () => {
CodexSdk.data()
.networkDownload(cid)
.then((s) => {
if (s.error === false) {
setCid("");
queryClient.invalidateQueries({ queryKey: ["cids"] });
console.info("Done");
}
return Promises.rejectOnError(s);
});
return CodexSdk.data()
.fetchManifest(cid)
.then((s) => {

View File

@ -80,7 +80,7 @@ export function PurchasesTable() {
["request id"],
["duration", onSortByDuration],
["slots"],
["reward", onSortByReward],
["price per byte", onSortByReward],
["proof probability"],
["state", onSortByState],
] satisfies [string, ((state: TabSortState) => void)?][];
@ -90,9 +90,8 @@ export function PurchasesTable() {
const rows = sorted.map((p, index) => {
const r = p.request;
const ask = p.request.ask;
const duration = parseInt(p.request.ask.duration, 10);
const pf = parseInt(p.request.ask.proofProbability, 10);
const duration = parseInt(ask.duration, 10);
const pf = parseInt(ask.proofProbability, 10);
return (
<Row
cells={[
@ -105,7 +104,7 @@ export function PurchasesTable() {
<TruncateCell value={r.id} />,
<Cell>{Times.pretty(duration)}</Cell>,
<Cell>{ask.slots.toString()}</Cell>,
<Cell>{ask.reward + " CDX"}</Cell>,
<Cell>{p.request.ask.pricePerBytePerSecond + " CDX"}</Cell>,
<Cell>{pf.toString()}</Cell>,
<CustomStateCellRender state={p.state} message={p.error} />,
]}></Row>

View File

@ -1,42 +1,42 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components"
import { CodexPurchase } from "@codex-storage/sdk-js"
import { TabSortState } from "@codex-storage/marketplace-ui-components";
import { CodexPurchase, CodexStorageRequest } from "@codex-storage/sdk-js";
export const PurchaseUtils = {
sortById: (state: TabSortState) =>
(a: CodexPurchase, b: CodexPurchase) => {
sortById: (state: TabSortState) => (a: CodexPurchase, b: CodexPurchase) => {
return state === "desc"
? b.requestId
.toLocaleLowerCase()
.localeCompare(a.requestId.toLocaleLowerCase())
: a.requestId
.toLocaleLowerCase()
.localeCompare(b.requestId.toLocaleLowerCase())
.localeCompare(b.requestId.toLocaleLowerCase());
},
sortByState: (state: TabSortState) =>
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
? b.state
.toLocaleLowerCase()
.localeCompare(a.state.toLocaleLowerCase())
: a.state
.toLocaleLowerCase()
.localeCompare(b.state.toLocaleLowerCase())
,
sortByDuration: (state: TabSortState) =>
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
sortByState: (state: TabSortState) => (a: CodexPurchase, b: CodexPurchase) =>
state === "desc"
? b.state.toLocaleLowerCase().localeCompare(a.state.toLocaleLowerCase())
: a.state.toLocaleLowerCase().localeCompare(b.state.toLocaleLowerCase()),
sortByDuration:
(state: TabSortState) => (a: CodexPurchase, b: CodexPurchase) =>
state === "desc"
? Number(b.request.ask.duration) - Number(a.request.ask.duration)
: Number(a.request.ask.duration) - Number(b.request.ask.duration)
,
sortByReward: (state: TabSortState) =>
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
? Number(b.request.ask.reward) - Number(a.request.ask.reward)
: Number(a.request.ask.reward) - Number(b.request.ask.reward)
,
sortByUploadedAt: (state: TabSortState, table: Record<string, number>) =>
: Number(a.request.ask.duration) - Number(b.request.ask.duration),
sortByReward:
(state: TabSortState) => (a: CodexPurchase, b: CodexPurchase) => {
const aPrice = parseInt(a.request.ask.pricePerBytePerSecond, 10);
const bPrice = parseInt(b.request.ask.pricePerBytePerSecond, 10);
return state === "desc" ? bPrice - aPrice : aPrice - bPrice;
},
sortByUploadedAt:
(state: TabSortState, table: Record<string, number>) =>
(a: CodexPurchase, b: CodexPurchase) => {
return state === "desc"
? (table[b.requestId] || 0) - (table[a.requestId] || 0)
: (table[a.requestId] || 0) - (table[b.requestId] || 0)
}
,
}
: (table[a.requestId] || 0) - (table[b.requestId] || 0);
},
calculatePrice(request: CodexStorageRequest) {
return (
parseInt(request.ask.slotSize, 10) *
parseInt(request.ask.pricePerBytePerSecond, 10)
);
},
};

View File

@ -83,12 +83,26 @@ export function StorageRequestCreate() {
WebStorage.set("storage-request-step", step);
if (step == CONFIRM_STATE) {
const { availability, availabilityUnit, expiration, ...rest } =
storageRequest;
const {
availability,
availabilityUnit,
expiration,
reward,
collateral,
proofProbability,
cid,
nodes,
tolerance,
} = storageRequest;
mutateAsync({
...rest,
duration: Math.trunc(availability * Times.value(availabilityUnit)),
pricePerBytePerSecond: reward,
proofProbability,
collateralPerByte: collateral,
expiry: expiration * 60,
cid,
nodes,
tolerance,
});
} else {
dispatch({

View File

@ -12,6 +12,7 @@ import { StorageRequestComponentProps } from "./types";
import { useQueryClient } from "@tanstack/react-query";
import ChooseCidIcon from "../../assets/icons/choose-cid.svg?react";
import UploadIcon from "../../assets/icons/upload.svg?react";
import { FilesUtils } from "../Files/files.utils";
export function StorageRequestFileChooser({
storageRequest,
@ -39,6 +40,8 @@ export function StorageRequestFileChooser({
};
const onSuccess = (data: string) => {
FilesUtils.setUploadedAt(data, Date.now() / 1000);
queryClient.invalidateQueries({ queryKey: ["cids"] });
onStorageRequestChange({ cid: data });

View File

@ -282,7 +282,7 @@ export function StorageRequestReview({
onChange={onAvailabilityChange}
onValidation={isInvalidAvailability}></Commitment>
<CardNumbers
helper="Represents how much collateral is asked from hosts if they don't fulfill the contract."
helper="Represents how much collateral is asked from hosts per byte if they don't fulfill the contract."
id="collateral"
unit={"Collateral"}
value={storageRequest.collateral.toString()}
@ -290,13 +290,13 @@ export function StorageRequestReview({
onValidation={isInvalidNumber}
title="Penality tokens"></CardNumbers>
<CardNumbers
helper="The maximum amount of tokens paid per second per slot to hosts the client is willing to pay."
helper="The maximum amount of tokens paid per second per byte to hosts the client is willing to pay."
id="reward"
unit={"Reward"}
value={storageRequest.reward.toString()}
onChange={onRewardChange}
onValidation={isInvalidNumber}
title="Reward tokens for hosts"></CardNumbers>
title="Price per byte"></CardNumbers>
</div>
<div className="row">

View File

@ -2,11 +2,13 @@ import { Upload } from "@codex-storage/marketplace-ui-components";
import { CodexSdk } from "../../sdk/codex";
import { useQueryClient } from "@tanstack/react-query";
import UploadIcon from "../../assets/icons/upload.svg?react";
import { FilesUtils } from "../Files/files.utils";
export function UploadCard() {
const queryClient = useQueryClient();
const onSuccess = () => {
const onSuccess = (cid: string) => {
FilesUtils.setUploadedAt(cid, Date.now() / 1000);
queryClient.invalidateQueries({ queryKey: ["cids"] });
};

View File

@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { CodexSdk } from "../sdk/codex";
import { CodexDataResponse } from "@codex-storage/sdk-js";
import { Promises } from "../utils/promises";
import { FilesUtils } from "../components/Files/files.utils";
export function useData() {
const { data = { content: [] } satisfies CodexDataResponse } =
@ -29,5 +30,8 @@ export function useData() {
throwOnError: true,
});
return data.content;
return data.content.map((c) => ({
...c,
uploadedAt: FilesUtils.getUploadedAt(c.cid),
}));
}

View File

@ -21,6 +21,7 @@ import { SettingsRoute } from "./routes/dashboard/settings.tsx";
import { HelpRoute } from "./routes/dashboard/help.tsx";
import { DisclaimerRoute } from "./routes/dashboard/disclaimer.tsx";
import { RouteErrorBoundary } from "./components/RouteErrorBoundary/RouteErrorBoundary.tsx";
import { HealthCheckUtils } from "./components/HealthChecks/health-check.utils.ts";
if (import.meta.env.PROD && !import.meta.env.CI) {
Sentry.init({
@ -117,7 +118,29 @@ const queryClient = new QueryClient();
const rootElement = document.getElementById("root")!;
if (rootElement) {
CodexSdk.load().then(() => {
CodexSdk.load()
.then(() => {
const queryString = window.location.search;
if (queryString) {
const urlParams = new URLSearchParams(queryString);
const param = urlParams.get("api-port");
if (param) {
const port = parseInt(param, 10);
if (!isNaN(port)) {
const address = HealthCheckUtils.removePort(CodexSdk.url());
const url = address + ":" + port;
if (HealthCheckUtils.isUrlInvalid(url)) {
return;
}
return CodexSdk.updateURL(url);
}
}
}
})
.then(() => {
render(
<StrictMode>
<QueryClientProvider client={queryClient}>

View File

@ -7,10 +7,7 @@ import {
import { CodexSdk as Sdk } from "./sdk/codex";
import { WebStorage } from "./utils/web-storage";
class CodexDataMock extends CodexData {
}
class CodexDataMock extends CodexData {}
class CodexMarketplaceMock extends CodexMarketplace {
// override async purchases(): Promise<SafeValue<CodexPurchase[]>> {
@ -36,21 +33,24 @@ class CodexMarketplaceMock extends CodexMarketplace {
* should still be maintained, but the metadata should be retrieved
* using a REST API call.
*/
override async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<string>> {
const res = await super.createStorageRequest(input)
override async createStorageRequest(
input: CodexCreateStorageRequestInput
): Promise<SafeValue<string>> {
console.info(input);
const res = await super.createStorageRequest(input);
if (res.error) {
return res
console.error(res.data);
return res;
}
await WebStorage.purchases.set("0x" + res.data, input.cid)
await WebStorage.purchases.set("0x" + res.data, input.cid);
// await PurchaseDatesStorage.set(res.data, new Date().toJSON())
return res
return res;
}
// override createStorageRequest(
// input: CodexCreateStorageRequestInput
// ): Promise<SafeValue<string>> {
@ -139,5 +139,3 @@ export const CodexSdk = {
marketplace: () => new CodexMarketplaceMock(CodexSdk.url()),
data: () => new CodexDataMock(CodexSdk.url()),
};

View File

@ -7,9 +7,9 @@ export type TimesUnit =
| "seconds";
const plural = (value: number, unit: TimesUnit) => {
const val = Number.isInteger(value) ? value : value.toFixed(1)
const val = Number.isInteger(value) ? value : value.toFixed(1);
return value > 1 ? val + ` ${unit}` : val + ` ${unit.slice(0, -1)}`;
}
};
export const Times = {
toSeconds(value: number, unit: TimesUnit) {
@ -73,24 +73,23 @@ export const Times = {
seconds /= 30;
if (value >= seconds) {
return "days"
return "days";
}
return "hours"
return "hours";
},
value(unit: "hours" | "days" | "months") {
switch (unit) {
case "months": {
return 30 * 24 * 60 * 60
return 30 * 24 * 60 * 60;
}
case "days": {
return 24 * 60 * 60
return 24 * 60 * 60;
}
default: {
return 60 * 60
return 60 * 60;
}
}
}
},
};

View File

@ -1,6 +1,5 @@
import { createStore, del, entries, get, set } from "idb-keyval";
export const WebStorage = {
set(key: string, value: unknown) {
return set(key, value);
@ -16,27 +15,27 @@ export const WebStorage = {
onBoarding: {
getStep() {
return parseInt(localStorage.getItem("onboarding-step") || "0", 10)
return parseInt(localStorage.getItem("onboarding-step") || "0", 10);
},
setStep(step: number) {
localStorage.setItem("onboarding-step", step.toString())
localStorage.setItem("onboarding-step", step.toString());
},
setDisplayName(displayName: string) {
localStorage.setItem("display-name", displayName)
localStorage.setItem("display-name", displayName);
},
getDisplayName() {
return localStorage.getItem("display-name") || ""
return localStorage.getItem("display-name") || "";
},
setEmoji(emoji: string) {
localStorage.setItem("emoji", emoji)
localStorage.setItem("emoji", emoji);
},
getEmoji() {
return localStorage.getItem("emoji") || "🤖"
return localStorage.getItem("emoji") || "🤖";
},
},
@ -48,33 +47,35 @@ export const WebStorage = {
},
async list(): Promise<[string, string[]][]> {
const items = await entries<string, string[]>(this.store) || []
const items = (await entries<string, string[]>(this.store)) || [];
if (items.length == 0) {
return [["Favorites", []]]
return [["Favorites", []]];
}
if (items[0][0] !== "Favorites") {
return [["Favorites", []], ...items]
return [["Favorites", []], ...items];
}
return items
return items;
},
delete(key: string) {
return del(key, this.store);
},
async addFile(folder: string, cid: string) {
const files = await get<string[]>(folder, this.store) || []
const files = (await get<string[]>(folder, this.store)) || [];
return set(folder, [...files, cid], this.store)
return set(folder, [...files, cid], this.store);
},
async deleteFile(folder: string, cid: string) {
const files = await get<string[]>(folder, this.store) || []
return set(folder, files.filter(item => item !== cid), this.store)
const files = (await get<string[]>(folder, this.store)) || [];
return set(
folder,
files.filter((item) => item !== cid),
this.store
);
},
},
@ -97,7 +98,6 @@ export const WebStorage = {
purchases: {
store: createStore("purchases", "purchases"),
async get(key: string) {
return get<string>(key, this.store);
},
@ -120,6 +120,6 @@ export const WebStorage = {
async set(key: string, date: string) {
return set(key, date, this.store);
},
}
}
},
},
};