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: workflow_dispatch:
env: env:
codex_version: v0.1.9 codex_version: v0.2.0
circuit_version: v0.1.9 circuit_version: v0.2.0
marketplace_address: "0xAB03b6a58C5262f530D54146DA2a552B1C0F7648" marketplace_address: "0xfFaF679D5Cbfdd5Dbc9Be61C616ed115DFb597ed"
eth_provider: "https://rpc.testnet.codex.storage" 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_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }}
VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }} VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }}
jobs: jobs:
@ -36,7 +37,7 @@ jobs:
key: ${{ env.circuit_version }}-circuits key: ${{ env.circuit_version }}-circuits
- name: Download circuits - name: Download circuits
if: steps.circuits-cache-restore.outputs.cache-hit != 'true' # if: steps.circuits-cache-restore.outputs.cache-hit != 'true'
run: | run: |
mkdir -p datadir/circuits mkdir -p datadir/circuits
chmod 700 datadir chmod 700 datadir
@ -85,13 +86,13 @@ jobs:
chmod 600 eth.key chmod 600 eth.key
# Run # 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 sleep 15
- name: Check Codex API - name: Check Codex API
run: | 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; } [[ $? -eq 0 ]] && { echo "Codex node is up"; } || { echo "Please check Codex node"; exit 1; }
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -1,121 +1,143 @@
import test, { expect } from "@playwright/test"; import test, { expect } from "@playwright/test";
import { Bytes } from "../src/utils/bytes" import { Bytes } from "../src/utils/bytes";
import { GB } from "../src/utils/constants" import { GB } from "../src/utils/constants";
test('create an availability', async ({ page }) => { test("create an availability", async ({ page }) => {
await page.goto('/dashboard/availabilities'); await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click(); await page.locator(".availability-edit button").first().click();
await page.getByLabel('Total size').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("Total size").fill(value.toFixed(1));
await page.getByLabel('Duration').click(); await page.getByLabel("Duration").click();
await page.getByLabel('Duration').fill('30'); await page.getByLabel("Duration").fill("30");
await page.getByLabel('Min price').click(); await page.getByLabel("Min price").click();
await page.getByLabel('Min price').fill('5'); await page.getByLabel("Min price").fill("5");
await page.getByLabel('Max collateral').click(); await page.getByLabel("Total collateral").click();
await page.getByLabel('Max collateral').fill('30'); await page.getByLabel("Total collateral").fill("30");
await page.getByLabel('Min price').fill('5'); await page.getByLabel("Min price").fill("5");
await page.getByLabel('Nickname').click(); await page.getByLabel("Nickname").click();
await page.getByLabel('Nickname').fill('test'); await page.getByLabel("Nickname").fill("test");
await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible(); await expect(page.getByText("Confirm your new sale")).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible(); await expect(page.getByText("Success", { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible(); await expect(
}) page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()
).toBeVisible();
});
test('availability navigation buttons', async ({ page }) => { test("availability navigation buttons", async ({ page }) => {
await page.goto('/dashboard/availabilities'); await page.goto("/dashboard/availabilities");
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click(); await page.locator(".availability-edit button").first().click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible() await expect(page.locator(".stepper-number-done")).not.toBeVisible();
await expect(page.locator('.step--active')).toBeVisible() await expect(page.locator(".step--active")).toBeVisible();
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); await expect(
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); page.locator("footer .button--outline").first()
await page.getByLabel('Total size').click(); ).not.toHaveAttribute("disabled");
await page.getByLabel('Total size').fill('19'); await page.getByLabel("Total size").click();
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); await page.getByLabel("Total size").fill("");
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled"); await page.getByLabel("Duration").click();
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 expect(page.locator("footer .button--primary")).toHaveAttribute(
await page.goto('/dashboard/availabilities'); "disabled",
await page.waitForTimeout(500); { timeout: 3000 }
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("Total size").click();
await page.getByLabel('Duration').click(); await page.getByLabel("Total size").fill("0.5");
await page.getByLabel('Duration').fill("3"); await page.getByRole("button", { name: "Next" }).click();
await page.getByRole('combobox').nth(1).selectOption('months'); 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.getByLabel('Min price').click(); test("create an availability with changing the duration to months", async ({
await page.getByLabel('Min price').fill('5'); page,
await page.getByLabel('Max collateral').click(); }) => {
await page.getByLabel('Max collateral').fill('30'); await page.goto("/dashboard/availabilities");
await page.getByLabel('Min price').fill('5'); await page.waitForTimeout(500);
await page.getByLabel('Nickname').click(); await page.locator(".availability-edit button").first().click();
await page.getByLabel('Nickname').fill('test'); await page.getByLabel("Total size").click();
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();
})
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");
test('create an availability after checking max size and invalid input', async ({ page }) => { await page.getByLabel("Min price").click();
await page.goto('/dashboard/availabilities'); await page.getByLabel("Min price").fill("5");
await page.waitForTimeout(500); await page.getByLabel("Total collateral").click();
await page.locator('.availability-edit button').first().click(); await page.getByLabel("Total collateral").fill("30");
await page.getByLabel('Total size').click(); 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");
await page.waitForTimeout(500);
await page.locator(".availability-edit button").first().click();
await page.getByLabel("Total size").click();
await page.getByLabel('Total size').fill("9999"); await page.getByLabel("Total size").fill("9999");
await expect(page.getByLabel('Total size')).toHaveAttribute("aria-invalid"); await expect(page.getByLabel("Total size")).toHaveAttribute("aria-invalid");
await page.getByText("Use max size").click() await page.getByText("Use max size").click();
await expect(page.getByLabel('Total size')).not.toHaveAttribute("aria-invalid"); await expect(page.getByLabel("Total size")).not.toHaveAttribute(
"aria-invalid"
);
const value = (Math.random() * 0.5); const value = 0.2;
await page.getByLabel('Total size').fill(value.toFixed(1)); await page.getByLabel("Total size").fill(value.toString());
await page.getByLabel('Duration').click(); await page.getByLabel("Duration").click();
await page.getByLabel('Duration').fill('30'); await page.getByLabel("Duration").fill("30");
await page.getByLabel('Min price').click(); await page.getByLabel("Min price").click();
await page.getByLabel('Min price').fill('5'); await page.getByLabel("Min price").fill("5");
await page.getByLabel('Max collateral').click(); await page.getByLabel("Total collateral").click();
await page.getByLabel('Max collateral').fill('30'); await page.getByLabel("Total collateral").fill("30");
await page.getByLabel('Min price').fill('5'); await page.getByLabel("Min price").fill("5");
await page.getByLabel('Nickname').click(); await page.getByLabel("Nickname").click();
await page.getByLabel('Nickname').fill('test'); await page.getByLabel("Nickname").fill("test");
await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible(); await expect(page.getByText("Confirm your new sale")).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible(); await expect(page.getByText("Success", { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click(); await page.getByRole("button", { name: "Finish" }).click();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible(); 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 { test, expect } from "@playwright/test";
import path, { dirname } from 'path'; import path, { dirname } from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); 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 // https://github.com/microsoft/playwright/issues/13037
test.skip(browserName.toLowerCase() !== 'chromium', test.skip(
`Test only for chromium!`); browserName.toLowerCase() !== "chromium",
`Test only for chromium!`
);
await page.goto('/dashboard'); await page.goto("/dashboard");
await page.locator('div').getByTestId("upload").setInputFiles([ await page
path.join(__dirname, "assets", 'chatgpt.jpg'), .locator("div")
]); .getByTestId("upload")
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); .setInputFiles([path.join(__dirname, "assets", "chatgpt.jpg")]);
await page.locator('.file-cell button').first().click(); await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); await page.locator(".file-cell button").first().click();
const cid = await handle.jsonValue() 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 page1Promise = page.waitForEvent('popup');
const downloadPromise = page.waitForEvent('download'); const downloadPromise = page.waitForEvent("download");
await page.locator('.download-input + button').click(); await page.locator(".download-input + button").click();
// const page1 = await page1Promise; // const page1 = await page1Promise;
const download = await downloadPromise; 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.describe("onboarding", () => {
test('onboarding steps', async ({ page, browserName }) => { test("onboarding steps", async ({ page, browserName }) => {
await page.context().setOffline(false) await page.context().setOffline(false);
await page.goto('/'); 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 expect(
await page.locator('.navigation').click(); page.getByText(
await expect(page.locator('.navigation')).toHaveAttribute("aria-disabled"); "Codex is a durable, decentralised data storage protocol, created so the world community can preserve its most important knowledge without risk of censorship."
await page.getByLabel('Preferred name').fill('Arnaud'); )
await expect(page.locator('.navigation')).not.toHaveAttribute("aria-disabled"); ).toBeVisible();
await page.locator('.navigation').click(); 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 // Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).not.toBeVisible() await expect(
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).toBeVisible() 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 // Port forwarding
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible() await expect(
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible() 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 // Codex node
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible() await expect(
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible() 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 // 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-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)).toBeVisible();
// Can be simulated with File -> Work offline // Can be simulated with File -> Work offline
if (browserName.toLowerCase() !== 'firefox') { if (browserName.toLowerCase() !== "firefox") {
await page.context().setOffline(true) await page.context().setOffline(true);
// Network // Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).toBeVisible() await expect(
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).not.toBeVisible() 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 }) => { 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 }) => { test("does not display undefined when delete the url value", async ({
await page.goto('/onboarding-checks'); page,
await page.locator('#url').focus() }) => {
await page.goto("/onboarding-checks");
await page.locator("#url").focus();
for (let i = 0; i < "http://localhost:8080".length; i++) { 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")).toHaveValue("");
await expect(page.locator('#url')).toHaveAttribute("aria-invalid") await expect(page.locator("#url")).toHaveAttribute("aria-invalid");
await expect(page.locator('.refresh svg')).toHaveAttribute("color", "#494949") await expect(page.locator(".refresh svg")).toHaveAttribute(
}); "color",
"#494949"
);
});

View File

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

2432
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"type": "git", "type": "git",
"url": "https://github.com/codex-storage/codex-marketplace-ui" "url": "https://github.com/codex-storage/codex-marketplace-ui"
}, },
"version": "0.0.13", "version": "0.0.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1 --port 5173", "dev": "vite --host 127.0.0.1 --port 5173",
@ -27,7 +27,7 @@
], ],
"dependencies": { "dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.51", "@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/browser": "^8.32.0",
"@sentry/react": "^8.31.0", "@sentry/react": "^8.31.0",
"@tanstack/react-query": "^5.51.15", "@tanstack/react-query": "^5.51.15",
@ -60,9 +60,9 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.7", "vite": "^6.1.0",
"vite-plugin-svgr": "^4.3.0", "vite-plugin-svgr": "^4.3.0",
"vitest": "^2.1.4" "vitest": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=18" "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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@ -7,33 +7,33 @@ const __dirname = dirname(__filename);
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
*/ */
import dotenv from 'dotenv'; import dotenv from "dotenv";
import path, { dirname } from 'path'; import path, { dirname } from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
dotenv.config({ path: path.resolve(__dirname, '.env.ci') }); dotenv.config({ path: path.resolve(__dirname, ".env.ci") });
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: "./e2e",
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* 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. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* 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 */ /* 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", screenshot: "only-on-failure",
}, },
@ -41,18 +41,18 @@ export default defineConfig({
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{ {
name: 'chromium', name: "chromium",
use: { ...devices['Desktop Chrome'] }, use: { ...devices["Desktop Chrome"] },
}, },
{ {
name: 'firefox', name: "firefox",
use: { ...devices['Desktop Firefox'] }, use: { ...devices["Desktop Firefox"] },
}, },
{ {
name: 'webkit', name: "webkit",
use: { ...devices['Desktop Safari'] }, use: { ...devices["Desktop Safari"] },
}, },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
@ -78,8 +78,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
webServer: { webServer: {
command: 'npm run preview', command: "npm run preview",
url: 'http://127.0.0.1:5173', url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}); });

View File

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

View File

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

View File

@ -184,32 +184,35 @@ export function AvailabilityForm({
<div className="row gap"> <div className="row gap">
<div className="group"> <div className="group">
<Input <Input
id="minPrice" id="minPricePerBytePerSecond"
name="minPrice" name="minPricePerBytePerSecond"
type="number" type="number"
label="Min price" label="Min price per byte per second"
min={0} min={0}
onChange={onInputChange} 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> <InfoIcon></InfoIcon>
</Tooltip> </Tooltip>
</div> </div>
<div className="group"> <div className="group">
<Input <Input
id="maxCollateral" id="totalCollateral"
name="maxCollateral" name="totalCollateral"
type="number" type="number"
label="Max collateral" label="Total collateral"
min={0} min={0}
onChange={onInputChange} onChange={onInputChange}
value={availability.maxCollateral.toString()} value={availability.totalCollateral.toString()}
/> />
<Tooltip <Tooltip
message={ 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> <InfoIcon></InfoIcon>
</Tooltip> </Tooltip>

View File

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

View File

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

View File

@ -5,191 +5,229 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { AvailabilityState } from "./types"; import { AvailabilityState } from "./types";
describe("files", () => { describe("files", () => {
it("sorts by id", async () => { it("sorts by id", async () => {
const a = { const a = {
id: "a", id: "a",
totalSize: 0, totalSize: 0,
duration: 0, duration: 0,
minPrice: 0, freeSize: 0,
maxCollateral: 0, minPricePerBytePerSecond: 0,
name: "", totalCollateral: 0,
slots: [] totalRemainingCollateral: 0,
} name: "",
const b = { slots: [],
id: "b", };
totalSize: 0, const b = {
duration: 0, id: "b",
minPrice: 0, totalSize: 0,
maxCollateral: 0, freeSize: 0,
name: "", duration: 0,
slots: [] minPricePerBytePerSecond: 0,
} totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
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]); 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]); assert.deepEqual(ascSorted, [a, b]);
}); });
it("sorts by size", async () => { it("sorts by size", async () => {
const a = { const a = {
id: "", id: "",
totalSize: 1, totalSize: 1,
duration: 0, freeSize: 0,
minPrice: 0, duration: 0,
maxCollateral: 0, minPricePerBytePerSecond: 0,
name: "", totalCollateral: 0,
slots: [] totalRemainingCollateral: 0,
} name: "",
const b = { slots: [],
id: "", };
totalSize: 2, const b = {
duration: 0, id: "",
minPrice: 0, totalSize: 2,
maxCollateral: 0, freeSize: 0,
name: "", duration: 0,
slots: [] minPricePerBytePerSecond: 0,
} totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
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]); 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]); assert.deepEqual(ascSorted, [a, b]);
}); });
it("sorts by duration", async () => { it("sorts by duration", async () => {
const a = { const a = {
id: "", id: "",
totalSize: 0, totalSize: 0,
duration: 1, freeSize: 0,
minPrice: 0, duration: 1,
maxCollateral: 0, minPricePerBytePerSecond: 0,
name: "", totalCollateral: 0,
slots: [] totalRemainingCollateral: 0,
} name: "",
const b = { slots: [],
id: "", };
totalSize: 0, const b = {
duration: 2, id: "",
minPrice: 0, totalSize: 0,
maxCollateral: 0, freeSize: 0,
name: "", duration: 2,
slots: [] minPricePerBytePerSecond: 0,
} totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
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]); 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]); assert.deepEqual(ascSorted, [a, b]);
}); });
it("sorts by price", async () => { it("sorts by price", async () => {
const a = { const a = {
id: "", id: "",
totalSize: 0, totalSize: 0,
duration: 0, freeSize: 0,
minPrice: 1, duration: 0,
maxCollateral: 0, minPricePerBytePerSecond: 1,
name: "", totalCollateral: 0,
slots: [] totalRemainingCollateral: 0,
} name: "",
const b = { slots: [],
id: "", };
totalSize: 0, const b = {
duration: 0, id: "",
minPrice: 2, freeSize: 0,
maxCollateral: 0, totalSize: 0,
name: "", duration: 0,
slots: [] minPricePerBytePerSecond: 2,
} totalCollateral: 0,
totalRemainingCollateral: 0,
name: "",
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]); 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]); assert.deepEqual(ascSorted, [a, b]);
}); });
it("sorts by collateral", async () => { it("sorts by collateral", async () => {
const a = { const a = {
id: "", id: "",
totalSize: 0, totalSize: 0,
duration: 0, freeSize: 0,
minPrice: 0, duration: 0,
maxCollateral: 1, minPricePerBytePerSecond: 0,
name: "", totalCollateral: 0,
slots: [] totalRemainingCollateral: 1,
} name: "",
const b = { slots: [],
id: "", };
totalSize: 0, const b = {
duration: 0, id: "",
minPrice: 0, totalSize: 0,
maxCollateral: 2, freeSize: 0,
name: "", duration: 0,
slots: [] minPricePerBytePerSecond: 0,
} totalCollateral: 0,
totalRemainingCollateral: 2,
name: "",
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]); 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]); assert.deepEqual(ascSorted, [a, b]);
}); });
it("returns the number of bytes per unit", async () => { it("returns the number of bytes per unit", async () => {
assert.deepEqual(AvailabilityUtils.toUnit(GB, "gb"), 1); assert.deepEqual(AvailabilityUtils.toUnit(GB, "gb"), 1);
assert.deepEqual(AvailabilityUtils.toUnit(TB, "tb"), 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,
};
assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB - 1);
});
it("returns the max value possible for an availability", async () => { it("checks the availability max value", async () => {
const space: CodexNodeSpace = { const availability = {
quotaMaxBytes: 8 * GB, totalSizeUnit: "gb",
quotaReservedBytes: 2 * GB, totalSize: 1,
quotaUsedBytes: GB, } as AvailabilityState;
totalBlocks: 0
}
assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB - 1);
})
it("checks the availability max value", async () => { assert.deepEqual(AvailabilityUtils.isValid(availability, GB * 2), true);
const availability = { assert.deepEqual(
totalSizeUnit: "gb", AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB),
totalSize: 1 false
} as AvailabilityState );
assert.deepEqual(
AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB),
false
);
});
assert.deepEqual(AvailabilityUtils.isValid(availability, GB * 2), true); it("toggles item in array", async () => {
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB), false); const array: string[] = [];
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB), false); assert.deepEqual(AvailabilityUtils.toggle(array, "1"), ["1"]);
}) assert.deepEqual(
AvailabilityUtils.toggle(AvailabilityUtils.toggle(array, "1"), "1"),
it("toggles item in array", async () => { []
const array: string[] = [] );
assert.deepEqual(AvailabilityUtils.toggle(array, "1"), ["1"]); });
assert.deepEqual(AvailabilityUtils.toggle(AvailabilityUtils.toggle(array, "1"), "1"), []); });
})
})

View File

@ -1,93 +1,91 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components" import { TabSortState } from "@codex-storage/marketplace-ui-components";
import { AvailabilityState, AvailabilityWithSlots } from "./types" import { AvailabilityState, AvailabilityWithSlots } from "./types";
import { GB, TB } from "../../utils/constants"; import { GB, TB } from "../../utils/constants";
import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { CodexNodeSpace } from "@codex-storage/sdk-js";
export const AvailabilityUtils = { export const AvailabilityUtils = {
sortById: (state: TabSortState) => sortById:
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => { (state: TabSortState) =>
(a: AvailabilityWithSlots, b: AvailabilityWithSlots) => {
return state === "desc" return state === "desc"
? b.id ? b.id.toLocaleLowerCase().localeCompare(a.id.toLocaleLowerCase())
.toLocaleLowerCase() : a.id.toLocaleLowerCase().localeCompare(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
,
toUnit(bytes: number, unit: "gb" | "tb") {
return bytes / this.unitValue(unit || "gb")
}, },
maxValue(space: CodexNodeSpace) { sortBySize:
// Remove 1 byte to allow to create an availability with the max space possible (state: TabSortState) =>
return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes - 1 (a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
}, state === "desc" ? b.totalSize - a.totalSize : a.totalSize - b.totalSize,
unitValue(unit: "gb" | "tb") { sortByDuration:
return unit === "tb" ? TB : GB (state: TabSortState) =>
}, (a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
isValid: ( state === "desc" ? b.duration - a.duration : a.duration - b.duration,
availability: AvailabilityState, sortByPrice:
max: number (state: TabSortState) =>
) => availability.totalSize > 0 && availability.totalSize * AvailabilityUtils.unitValue(availability.totalSizeUnit) <= max (a: AvailabilityWithSlots, b: AvailabilityWithSlots) =>
, state === "desc"
toggle: <T>(arr: Array<T>, value: T) => ? b.minPricePerBytePerSecond - a.minPricePerBytePerSecond
arr.includes(value) ? arr.filter(i => i !== value) : [...arr, value], : 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");
},
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
);
},
unitValue(unit: "gb" | "tb") {
return unit === "tb" ? TB : GB;
},
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],
availabilityColors: [ availabilityColors: [
"#34A0FFFF", "#34A0FFFF",
"#34A0FFEE", "#34A0FFEE",
"#34A0FFDD", "#34A0FFDD",
"#34A0FFCC", "#34A0FFCC",
"#34A0FFBB", "#34A0FFBB",
"#34A0FFAA", "#34A0FFAA",
"#34A0FF99", "#34A0FF99",
"#34A0FF88", "#34A0FF88",
"#34A0FF77", "#34A0FF77",
"#34A0FF66", "#34A0FF66",
"#34A0FF55", "#34A0FF55",
"#34A0FF44", "#34A0FF44",
"#34A0FF33", "#34A0FF33",
"#34A0FF22", "#34A0FF22",
"#34A0FF11", "#34A0FF11",
"#34A0FF00", "#34A0FF00",
], ],
slotColors: [ slotColors: [
"#D2493CFF", "#D2493CFF",
"#D2493CEE", "#D2493CEE",
"#D2493CDD", "#D2493CDD",
"#D2493CCC", "#D2493CCC",
"#D2493CBB", "#D2493CBB",
"#D2493CAA", "#D2493CAA",
"#D2493C99", "#D2493C99",
"#D2493C88", "#D2493C88",
"#D2493C77", "#D2493C77",
"#D2493C66", "#D2493C66",
"#D2493C55", "#D2493C55",
"#D2493C44", "#D2493C44",
"#D2493C33", "#D2493C33",
"#D2493C22", "#D2493C22",
"#D2493C11", "#D2493C11",
"#D2493C00", "#D2493C00",
], ],
} };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,75 +20,95 @@ export const FilesUtils = {
return !!type && type.startsWith("video"); return !!type && type.startsWith("video");
}, },
isArchive(mimetype: string | null) { isArchive(mimetype: string | null) {
return !!mimetype && archiveMimetypes.includes(mimetype) return !!mimetype && archiveMimetypes.includes(mimetype);
}, },
type(mimetype: string | null) { type(mimetype: string | null) {
if (FilesUtils.isArchive(mimetype)) { if (FilesUtils.isArchive(mimetype)) {
return "archive" return "archive";
} }
if (FilesUtils.isVideo(mimetype)) { if (FilesUtils.isVideo(mimetype)) {
return "video" return "video";
} }
if (FilesUtils.isImage(mimetype)) { if (FilesUtils.isImage(mimetype)) {
return "image" return "image";
} }
return "document" return "document";
}, },
sortByName: (state: TabSortState) => sortByName:
(a: CodexDataContent, b: CodexDataContent) => { (state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) => {
const { manifest: { filename: afilename } } = a const {
const { manifest: { filename: bfilename } } = b manifest: { filename: afilename },
} = a;
const {
manifest: { filename: bfilename },
} = b;
return state === "desc" return state === "desc"
? (bfilename || "") ? (bfilename || "")
.toLocaleLowerCase() .toLocaleLowerCase()
.localeCompare((afilename || "").toLocaleLowerCase()) .localeCompare((afilename || "").toLocaleLowerCase())
: (afilename || "") : (afilename || "")
.toLocaleLowerCase() .toLocaleLowerCase()
.localeCompare((bfilename || "").toLocaleLowerCase()) .localeCompare((bfilename || "").toLocaleLowerCase());
}, },
sortBySize: (state: TabSortState) => sortBySize:
(a: CodexDataContent, b: CodexDataContent) => state === "desc" (state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) =>
? b.manifest.datasetSize - a.manifest.datasetSize state === "desc"
: a.manifest.datasetSize - b.manifest.datasetSize ? b.manifest.datasetSize - a.manifest.datasetSize
, : a.manifest.datasetSize - b.manifest.datasetSize,
sortByDate: (state: TabSortState) => sortByDate:
(a: CodexDataContent, b: CodexDataContent) => state === "desc" (state: TabSortState) => (a: CodexDataContent, b: CodexDataContent) => {
? new Date(b.manifest.uploadedAt).getTime() - const aUploadedAt = FilesUtils.getUploadedAt(a.cid);
new Date(a.manifest.uploadedAt).getTime() const bUploadedAt = FilesUtils.getUploadedAt(b.cid);
: new Date(a.manifest.uploadedAt).getTime() -
new Date(b.manifest.uploadedAt).getTime() return state === "desc"
, ? new Date(bUploadedAt).getTime() - new Date(aUploadedAt).getTime()
removeCidFromFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] { : new Date(aUploadedAt).getTime() - new Date(bUploadedAt).getTime();
},
removeCidFromFolder(
folders: [string, string[]][],
folder: string,
cid: string
): [string, string[]][] {
return folders.map(([name, files]) => return folders.map(([name, files]) =>
name === folder name === folder ? [name, files.filter((id) => id !== cid)] : [name, files]
? [name, files.filter((id) => id !== cid)] );
: [name, files]
)
}, },
addCidToFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] { addCidToFolder(
folders: [string, string[]][],
folder: string,
cid: string
): [string, string[]][] {
return folders.map(([name, files]) => return folders.map(([name, files]) =>
name === folder ? [name, [...files, cid]] : [name, files] name === folder ? [name, [...files, cid]] : [name, files]
) );
}, },
exists(folders: [string, string[]][], name: string) { 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.filter((f) => f !== filter) filters.includes(filter)
: [...filters, filter], ? filters.filter((f) => f !== filter)
listInFolder(files: CodexDataContent[], folders: [string, string[]][], index: number) { : [...filters, filter],
listInFolder(
files: CodexDataContent[],
folders: [string, string[]][],
index: number
) {
return index === 0 return index === 0
? files ? files
: files.filter((file) => folders[index - 1][1].includes(file.cid)); : files.filter((file) => folders[index - 1][1].includes(file.cid));
}, },
applyFilters(files: CodexDataContent[], filters: string[]) { applyFilters(files: CodexDataContent[], filters: string[]) {
return files.filter( 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) { formatDate(date: number) {
if (!date) { if (!date) {
@ -99,7 +119,14 @@ export const FilesUtils = {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short", timeStyle: "short",
}).format(new Date(date * 1000)); }).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 = { export type CodexFileMetadata = {

View File

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

View File

@ -11,6 +11,17 @@ export function ManifestFetch() {
const { refetch } = useQuery({ const { refetch } = useQuery({
queryFn: () => { 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() return CodexSdk.data()
.fetchManifest(cid) .fetchManifest(cid)
.then((s) => { .then((s) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -282,7 +282,7 @@ export function StorageRequestReview({
onChange={onAvailabilityChange} onChange={onAvailabilityChange}
onValidation={isInvalidAvailability}></Commitment> onValidation={isInvalidAvailability}></Commitment>
<CardNumbers <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" id="collateral"
unit={"Collateral"} unit={"Collateral"}
value={storageRequest.collateral.toString()} value={storageRequest.collateral.toString()}
@ -290,13 +290,13 @@ export function StorageRequestReview({
onValidation={isInvalidNumber} onValidation={isInvalidNumber}
title="Penality tokens"></CardNumbers> title="Penality tokens"></CardNumbers>
<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" id="reward"
unit={"Reward"} unit={"Reward"}
value={storageRequest.reward.toString()} value={storageRequest.reward.toString()}
onChange={onRewardChange} onChange={onRewardChange}
onValidation={isInvalidNumber} onValidation={isInvalidNumber}
title="Reward tokens for hosts"></CardNumbers> title="Price per byte"></CardNumbers>
</div> </div>
<div className="row"> <div className="row">

View File

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

View File

@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { CodexSdk } from "../sdk/codex"; import { CodexSdk } from "../sdk/codex";
import { CodexDataResponse } from "@codex-storage/sdk-js"; import { CodexDataResponse } from "@codex-storage/sdk-js";
import { Promises } from "../utils/promises"; import { Promises } from "../utils/promises";
import { FilesUtils } from "../components/Files/files.utils";
export function useData() { export function useData() {
const { data = { content: [] } satisfies CodexDataResponse } = const { data = { content: [] } satisfies CodexDataResponse } =
@ -29,5 +30,8 @@ export function useData() {
throwOnError: true, 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 { HelpRoute } from "./routes/dashboard/help.tsx";
import { DisclaimerRoute } from "./routes/dashboard/disclaimer.tsx"; import { DisclaimerRoute } from "./routes/dashboard/disclaimer.tsx";
import { RouteErrorBoundary } from "./components/RouteErrorBoundary/RouteErrorBoundary.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) { if (import.meta.env.PROD && !import.meta.env.CI) {
Sentry.init({ Sentry.init({
@ -117,14 +118,36 @@ const queryClient = new QueryClient();
const rootElement = document.getElementById("root")!; const rootElement = document.getElementById("root")!;
if (rootElement) { if (rootElement) {
CodexSdk.load().then(() => { CodexSdk.load()
render( .then(() => {
<StrictMode> const queryString = window.location.search;
<QueryClientProvider client={queryClient}> if (queryString) {
<RouterProvider router={router} /> const urlParams = new URLSearchParams(queryString);
</QueryClientProvider> const param = urlParams.get("api-port");
</StrictMode>, if (param) {
rootElement 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}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>,
rootElement
);
});
} }

View File

@ -7,10 +7,7 @@ import {
import { CodexSdk as Sdk } from "./sdk/codex"; import { CodexSdk as Sdk } from "./sdk/codex";
import { WebStorage } from "./utils/web-storage"; import { WebStorage } from "./utils/web-storage";
class CodexDataMock extends CodexData { class CodexDataMock extends CodexData {}
}
class CodexMarketplaceMock extends CodexMarketplace { class CodexMarketplaceMock extends CodexMarketplace {
// override async purchases(): Promise<SafeValue<CodexPurchase[]>> { // override async purchases(): Promise<SafeValue<CodexPurchase[]>> {
@ -31,26 +28,29 @@ class CodexMarketplaceMock extends CodexMarketplace {
// } // }
/** /**
* Maintains a temporary link between the CID and the file metadata. * Maintains a temporary link between the CID and the file metadata.
* When the metadata is available in the manifest, the CID link * When the metadata is available in the manifest, the CID link
* should still be maintained, but the metadata should be retrieved * should still be maintained, but the metadata should be retrieved
* using a REST API call. * using a REST API call.
*/ */
override async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<string>> { override async createStorageRequest(
const res = await super.createStorageRequest(input) input: CodexCreateStorageRequestInput
): Promise<SafeValue<string>> {
console.info(input);
const res = await super.createStorageRequest(input);
if (res.error) { 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()) // await PurchaseDatesStorage.set(res.data, new Date().toJSON())
return res return res;
} }
// override createStorageRequest( // override createStorageRequest(
// input: CodexCreateStorageRequestInput // input: CodexCreateStorageRequestInput
// ): Promise<SafeValue<string>> { // ): Promise<SafeValue<string>> {
@ -139,5 +139,3 @@ export const CodexSdk = {
marketplace: () => new CodexMarketplaceMock(CodexSdk.url()), marketplace: () => new CodexMarketplaceMock(CodexSdk.url()),
data: () => new CodexDataMock(CodexSdk.url()), data: () => new CodexDataMock(CodexSdk.url()),
}; };

View File

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

View File

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