chore: generate types from open api (#14)

* Generate types from the open api file

* Update dependencies

* Update dependencies

* Update the openapi schema

* Fix merge

* Fix doc reference
This commit is contained in:
Arnaud 2025-04-02 17:13:40 +02:00 committed by GitHub
parent 921bb617ef
commit 3bf517d1bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 4021 additions and 456 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules node_modules
dist dist
*.tgz *.tgz
index.bundle.js

View File

@ -10,6 +10,14 @@ The SDK is currently under early development and the API can change at any time.
- Version 0.1.0 introduces [upload strategy](#upload) to support browser and Node JS. - Version 0.1.0 introduces [upload strategy](#upload) to support browser and Node JS.
## Types generation
The types are generated from the openapi.yaml using the commande:
```bash
npx openapi-typescript ./openapi.yaml -o src/openapi.ts --default-non-nullable false
```
## How to use ## How to use
### Sync api ### Sync api
@ -126,7 +134,7 @@ const marketplace = codex.marketplace;
Returns active slots. Returns active slots.
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]> - returns Promise<[CodexSlot](./src/marketplace/types.ts#L7)[]>
Example: Example:
@ -139,7 +147,7 @@ const slots = await marketplace.activeSlots();
Returns active slot with id {slotId} for the host. Returns active slot with id {slotId} for the host.
- slotId (string, required) - slotId (string, required)
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]> - returns Promise<[CodexSlotAgent](./src/marketplace/types.ts#L12)[]>
Example: Example:
@ -152,7 +160,7 @@ const slot = await marketplace.activeSlot(slotId);
Returns storage that is for sale. Returns storage that is for sale.
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L99)> - returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)>
Example: Example:
@ -164,8 +172,8 @@ const availabilities = await marketplace.availabilities();
Offers storage for sale. Offers storage for sale.
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L175), required) - input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L45), required)
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L186)[]> - returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)[]>
Example: Example:
@ -182,7 +190,7 @@ const response = await marketplace.createAvailability({
Updates availability. Updates availability.
- input ([CodexUpdateAvailabilityInput](./src/marketplace/types.ts#L186), required) - input ([CodexAvailabilityPatchInput](./src/marketplace/types.ts#L66), required)
- returns Promise<""> - returns Promise<"">
Example: Example:
@ -202,7 +210,7 @@ const response = await marketplace.updateAvailability({
Return list of reservations for ongoing Storage Requests that the node hosts. Return list of reservations for ongoing Storage Requests that the node hosts.
- availabilityId (string, required) - availabilityId (string, required)
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L198)[]> - returns Promise<[CodexReservation](./src/marketplace/types.ts#L83)[]>
Example: Example:
@ -214,7 +222,7 @@ const reservations = await marketplace.reservations("Ox...");
Creates a new Request for storage Creates a new Request for storage
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L230), required) - input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L120), required)
- returns Promise<string> - returns Promise<string>
Example: Example:
@ -248,7 +256,7 @@ const ids = await marketplace.purchaseIds();
Returns purchase details Returns purchase details
- purchaseId (string, required) - purchaseId (string, required)
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L214)[]> - returns Promise<[CodexPurchase](./src/marketplace/types.ts#L103)[]>
Example: Example:
@ -274,7 +282,7 @@ const data = codex.data;
Returns the manifest stored locally in node. Returns the manifest stored locally in node.
- returns Promise<[CodexDataResponse](./src/data/types.ts#L54)[]> - returns Promise<[CodexDataItem](./src/data/types.ts#L8)[]>
Example: Example:
@ -286,7 +294,7 @@ const cids = await data.cids();
Returns a summary of the storage space allocation of the node Returns a summary of the storage space allocation of the node
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L56)[]> - returns Promise<[CodexNodeSpace](./src/data/types.ts#L15)[]>
Example: Example:
@ -301,7 +309,7 @@ Upload a file in a streaming manner
#### Browser #### Browser
- stategy [BrowserUploadStategy](./src/data/browser-upload.ts#L5) - stategy [BrowserUploadStategy](./src/data/browser-upload.ts#L5)
- returns [UploadResponse](./src/data/types.ts#L80) - returns [UploadResponse](./src/data/types.ts#L17)
Example: Example:
@ -330,8 +338,8 @@ console.info("CID is", res.data);
#### Node #### Node
- stategy [NodeUploadStategy](./src/data/node-download.ts#L8) - stategy [NodeUploadStategy](./src/data/node-upload.ts#L9)
- returns [UploadResponse](./src/data/types.ts#L80) - returns [UploadResponse](./src/data/types.ts#L17)
Example: Example:
@ -354,7 +362,7 @@ console.info("CID is", res.data);
Download only the dataset manifest from the network to the local node if it's not available locally. Download only the dataset manifest from the network to the local node if it's not available locally.
- cid (string, required) - cid (string, required)
- returns [CodexManifest](./src/data/types.ts#L3) - returns [CodexManifest](./src/data/types.ts#L30)
Example: Example:
@ -410,7 +418,7 @@ const data = codex.debug;
Set log level at run time. Set log level at run time.
- level ([CodexLogLevel](./src/debug/types.ts#L3), required) - level ([CodexLogLevel](./src/debug/types.ts#L7), required)
- returns Promise<""> - returns Promise<"">
Example: Example:
@ -423,7 +431,7 @@ await debug.setLogLevel("DEBUG");
Gets node information Gets node information
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L15)> - returns Promise<[CodexDebugInfo](./src/debug/types.ts#L23)>
Example: Example:
@ -448,10 +456,44 @@ const node = codex.node;
Get Node's SPR Get Node's SPR
- returns Promise<[CodexSpr](./src/node/types.ts#L1)> - returns Promise<[CodexSpr](./src/node/types.ts#L11)>
Example: Example:
```js ```js
const spr = await node.spr(); const spr = await node.spr();
``` ```
By default, the response will be a json. You can use `text` option to get the string:
#### peeriD
Get Node's peer id
- returns Promise<[CodexPeerId](./src/node/types.ts#L25)>
Example:
```js
const peerId = await node.peerId();
```
By default, the response will be a json. You can use `text` option to get the string:
```js
const peerId = await node.peerId("text");
```
#### connect
Connect to a peer
- returns Promise<string>
Example:
```js
const peerId = "..."
const addrs = [...]
const spr = await node.connect(peerId, addrs);
```

View File

@ -10,7 +10,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@codex-storage/sdk-js": "../..", "@codex-storage/sdk-js": "../..",
"undici": "^7.6.0" "undici": "^7.7.0"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3" "prettier": "^3.5.3"
@ -24,11 +24,14 @@
"version": "0.1.1", "version": "0.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"valibot": "^0.32.0" "undici": "^7.7.0",
"valibot": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.13", "@types/node": "^22.13.17",
"oas-normalize": "^13.1.2",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsup": "^8.3.6", "tsup": "^8.3.6",
"typescript": "^5.8.2", "typescript": "^5.8.2",
@ -65,9 +68,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.6.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz",
"integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==", "integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20.18.1" "node": ">=20.18.1"

View File

@ -2,15 +2,13 @@
"name": "@codex-storage/sdk-js-update-node-example", "name": "@codex-storage/sdk-js-update-node-example",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {},
"build": "node esbuild.js"
},
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@codex-storage/sdk-js": "../..", "@codex-storage/sdk-js": "../..",
"undici": "^7.6.0" "undici": "^7.7.0"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3" "prettier": "^3.5.3"

1012
openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

1142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -77,16 +77,18 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.14", "@types/node": "^22.13.17",
"oas-normalize": "^13.1.2",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsup": "^8.3.6", "tsup": "^8.3.6",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vitest": "^3.1.1" "vitest": "^3.1.1"
}, },
"dependencies": { "dependencies": {
"valibot": "^0.32.0" "valibot": "^1.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"undici": "^7.0.0" "undici": "^7.7.0"
} }
} }

View File

@ -7,11 +7,14 @@ import {
import type { SafeValue } from "../values/values"; import type { SafeValue } from "../values/values";
import type { import type {
CodexDataResponse, CodexDataResponse,
CodexManifest,
CodexNodeSpace,
UploadStategy, UploadStategy,
NetworkDownloadResponse,
UploadResponse, UploadResponse,
CodexSpaceResponse,
CodexNodeSpace,
CodexDataNetworkResponse,
CodexNetworkDownload,
CodexManifest,
CodexDataItems,
} from "./types"; } from "./types";
type CodexDataOptions = { type CodexDataOptions = {
@ -34,7 +37,7 @@ export class CodexData {
* Lists manifest CIDs stored locally in node. * Lists manifest CIDs stored locally in node.
* TODO: remove the faker data part when the api is ready * TODO: remove the faker data part when the api is ready
*/ */
cids(): Promise<SafeValue<CodexDataResponse>> { cids(): Promise<SafeValue<CodexDataItems>> {
const url = this.url + Api.config.prefix + "/data"; const url = this.url + Api.config.prefix + "/data";
return Fetch.safeJson<CodexDataResponse>(url, { return Fetch.safeJson<CodexDataResponse>(url, {
@ -52,10 +55,10 @@ export class CodexData {
/** /**
* Gets a summary of the storage space allocation of the node. * Gets a summary of the storage space allocation of the node.
*/ */
space() { space(): Promise<SafeValue<CodexNodeSpace>> {
const url = this.url + Api.config.prefix + "/space"; const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexNodeSpace>(url, { return Fetch.safeJson<CodexSpaceResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -95,12 +98,10 @@ export class CodexData {
* Download a file from the network to the local node if it's not available locally. * Download a file from the network to the local node if it's not available locally.
* Note: Download is performed async. Call can return before download is completed. * Note: Download is performed async. Call can return before download is completed.
*/ */
async networkDownload( async networkDownload(cid: string): Promise<SafeValue<CodexNetworkDownload>> {
cid: string
): Promise<SafeValue<NetworkDownloadResponse>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`; const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson(url, { return Fetch.safeJson<CodexDataNetworkResponse>(url, {
method: "POST", method: "POST",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -123,7 +124,7 @@ export class CodexData {
* Download only the dataset manifest from the network to the local node * Download only the dataset manifest from the network to the local node
* if it's not available locally. * if it's not available locally.
*/ */
async fetchManifest(cid: string) { async fetchManifest(cid: string): Promise<SafeValue<CodexManifest>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network/manifest`; const url = this.url + Api.config.prefix + `/data/${cid}/network/manifest`;
return Fetch.safeJson<CodexManifest>(url, { return Fetch.safeJson<CodexManifest>(url, {

View File

@ -1,87 +1,33 @@
import type { components, paths } from "../openapi";
import type { FetchAuth } from "../fetch-safe/fetch-safe"; import type { FetchAuth } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values"; import type { SafeValue } from "../values/values";
export type CodexManifest = { export type CodexDataResponse =
/** paths["/data"]["get"]["responses"][200]["content"]["application/json"];
* "Root hash of the content"
*/
// rootHash: string;
/** export type CodexDataItem = components["schemas"]["DataItem"];
* Length of original content in bytes
*/
// originalBytes: number;
/** export type CodexDataItems = CodexDataResponse;
* Total size of all blocks
*/
datasetSize: number;
/** export type CodexSpaceResponse =
* "Size of blocks" paths["/space"]["get"]["responses"][200]["content"]["application/json"];
*/
blockSize: number;
/** export type CodexNodeSpace = CodexSpaceResponse;
* Indicates if content is protected by erasure-coding
*/
protected: boolean;
/**
* Root of the merkle tree
*/
treeCid: string;
/**
* Name of the name
*/
filename: string | null;
/**
* Mimetype
*/
mimetype: string | null;
};
export type CodexDataContent = {
/**
* Content Identifier as specified at https://github.com/multiformats/cid
*/
cid: string;
manifest: CodexManifest;
};
export type CodexDataResponse = { content: CodexDataContent[] };
export type CodexNodeSpace = {
/**
* Number of blocks stored by the node
*/
totalBlocks: number;
/**
* Maximum storage space used by the node
*/
quotaMaxBytes: number;
/**
* Amount of storage space currently in use
*/
quotaUsedBytes: number;
/**
* Amount of storage space reserved
*/
quotaReservedBytes: number;
};
export type UploadResponse = { export type UploadResponse = {
result: Promise<SafeValue<string>>; result: Promise<SafeValue<string>>;
abort: () => void; abort: () => void;
}; };
export type NetworkDownloadResponse = { cid: string; manifest: CodexManifest }; export type CodexDataNetworkResponse =
paths["/data/{cid}/network"]["post"]["responses"][200]["content"]["application/json"];
export type CodexNetworkDownload = components["schemas"]["DataItem"];
export type CodexFetchManifestResponse =
paths["/data/{cid}/network/manifest"]["get"]["responses"][200]["content"]["application/json"];
export type CodexManifest = CodexFetchManifestResponse;
export type UploadStategyOptions = { export type UploadStategyOptions = {
auth?: FetchAuth; auth?: FetchAuth;

View File

@ -6,7 +6,12 @@ import {
type FetchAuth, type FetchAuth,
} from "../fetch-safe/fetch-safe"; } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values"; import type { SafeValue } from "../values/values";
import { CodexLogLevel, type CodexDebugInfo } from "./types"; import {
CodexLogLevelInput,
type CodexDebugInfo,
type CodexInfoResponse,
type CodexLogLevel,
} from "./types";
import * as v from "valibot"; import * as v from "valibot";
type CodexDebugOptions = { type CodexDebugOptions = {
@ -28,8 +33,8 @@ export class CodexDebug {
/** /**
* Set log level at run time * Set log level at run time
*/ */
async setLogLevel(level: CodexLogLevel): Promise<SafeValue<"">> { async setLogLevel(level: CodexLogLevel): Promise<SafeValue<string>> {
const result = v.safeParse(CodexLogLevel, level); const result = v.safeParse(CodexLogLevelInput, level);
if (!result.success) { if (!result.success) {
return Promise.resolve({ return Promise.resolve({
@ -46,26 +51,20 @@ export class CodexDebug {
"/debug/chronicles/loglevel?level=" + "/debug/chronicles/loglevel?level=" +
level; level;
const res = await Fetch.safe(url, { return Fetch.safeText(url, {
method: "POST", method: "POST",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
body: "", body: "",
}); });
if (res.error) {
return res;
}
return { error: false, data: "" };
} }
/** /**
* Gets node information * Gets node information
*/ */
info() { info(): Promise<SafeValue<CodexDebugInfo>> {
const url = this.url + Api.config.prefix + `/debug/info`; const url = this.url + Api.config.prefix + `/debug/info`;
return Fetch.safeJson<CodexDebugInfo>(url, { return Fetch.safeJson<CodexInfoResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });

View File

@ -1,6 +1,13 @@
import * as v from "valibot"; import * as v from "valibot";
import type { paths } from "../openapi";
export const CodexLogLevel = v.picklist([ export type CodexLogLevelResponse =
paths["/debug/chronicles/loglevel"]["post"]["responses"][200]["content"];
export type CodexLogLevel =
paths["/debug/chronicles/loglevel"]["post"]["parameters"]["query"]["level"];
export const CodexLogLevelInput = v.picklist([
"TRACE", "TRACE",
"DEBUG", "DEBUG",
"INFO", "INFO",
@ -10,49 +17,7 @@ export const CodexLogLevel = v.picklist([
"FATAL", "FATAL",
]); ]);
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>; export type CodexInfoResponse =
paths["/debug/info"]["get"]["responses"][200]["content"]["application/json"];
export type CodexDebugInfo = { export type CodexDebugInfo = CodexInfoResponse;
/**
* Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/
*/
id: string;
/**
* Address of node as specified by the multi-address specification https://multiformats.io/multiaddr/
*/
addrs: string[];
announceAddresses: string[]
/**
* Path of the data repository where all nodes data are stored
*/
repo: string;
// Signed Peer Record (libp2p)
spr: string;
table: {
localNode: {
nodeId: string
peerId: string
record: string
address: string
seen: boolean
}
nodes: {
nodeId: string
peerId: string
record: string
address: string
seen: boolean
}[]
}
codex: {
version: string
revision: string
}
};

View File

@ -60,4 +60,14 @@ export const Fetch = {
return Promises.safe(() => res.data.json()); return Promises.safe(() => res.data.json());
}, },
async safeText(url: string, init: RequestInit): Promise<SafeValue<string>> {
const res = await this.safe(url, init);
if (res.error) {
return res;
}
return Promises.safe(() => res.data.text());
},
}; };

View File

@ -104,10 +104,12 @@ function minNumberValidationError(field: string, min: number) {
function createAvailability() { function createAvailability() {
return { return {
id: randomEthereumAddress(), id: randomEthereumAddress(),
totalSize: randomInt(0, 9).toString(), totalSize: randomInt(0, 9),
duration: randomInt(0, 9).toString(), duration: randomInt(0, 9),
minPrice: randomInt(0, 9).toString(), minPrice: randomInt(0, 9),
maxCollateral: randomInt(0, 9).toString(), minPricePerBytePerSecond: randomInt(0, 9),
totalCollateral: randomInt(0, 900),
totalRemainingCollateral: randomInt(0, 900),
}; };
} }
@ -210,7 +212,7 @@ describe("marketplace", () => {
}); });
it("returns a response when the request succeed", async () => { it("returns a response when the request succeed", async () => {
const data = { ...createAvailability(), freeSize: "1000" }; const data = { ...createAvailability(), freeSize: 1000 };
const spy = vi.spyOn(Fetch, "safeJson"); const spy = vi.spyOn(Fetch, "safeJson");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data })); spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
@ -228,16 +230,16 @@ describe("marketplace", () => {
}); });
it("returns a response when the create availability succeed", async () => { it("returns a response when the create availability succeed", async () => {
const data = { ...createAvailability(), freeSize: "1000" }; const data = { ...createAvailability(), freeSize: 1000 };
const spy = vi.spyOn(Fetch, "safeJson"); const spy = vi.spyOn(Fetch, "safeJson");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data })); spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
const response = await marketplace.createAvailability({ const response = await marketplace.createAvailability({
totalCollateral: 1, totalCollateral: data.totalCollateral,
totalSize: 3000, totalSize: data.totalSize,
minPricePerBytePerSecond: 100, minPricePerBytePerSecond: data.minPricePerBytePerSecond,
duration: 100, duration: data.duration,
}); });
assert.ok(!response.error); assert.ok(!response.error);

View File

@ -8,16 +8,27 @@ import {
} from "../fetch-safe/fetch-safe"; } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values"; import type { SafeValue } from "../values/values";
import { import {
type CodexAvailabilityResponse,
type CodexAvailability, type CodexAvailability,
type CodexSlot,
type CodexSlotAgent,
type CodexSlotResponse,
type CodexSlotAgentResponse,
type CodexAvailabilityWithoutTypes,
type CodexAvailabilityCreateResponse, type CodexAvailabilityCreateResponse,
type CodexAvailabilityDto, type CodexAvailabilityCreateBody,
CodexAvailabilityPatchInput,
type CodexReservationsResponse,
type CodexPurchaseIdsResponse,
type CodexPurchaseResponse,
type CodexPurchase,
type CodexStorageRequestCreateBody,
type CodexReservation,
type CodexPurchaseWithoutTypes,
} from "./types";
import {
CodexCreateAvailabilityInput, CodexCreateAvailabilityInput,
CodexCreateStorageRequestInput, CodexCreateStorageRequestInput,
type CodexPurchase,
type CodexReservation,
type CodexSlot,
type CodexStorageRequest,
CodexUpdateAvailabilityInput,
} from "./types"; } from "./types";
type CodexMarketplaceOptions = { type CodexMarketplaceOptions = {
@ -42,7 +53,7 @@ export class CodexMarketplace {
async activeSlots(): Promise<SafeValue<CodexSlot[]>> { async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
const url = this.url + Api.config.prefix + "/sales/slots"; const url = this.url + Api.config.prefix + "/sales/slots";
return Fetch.safeJson<CodexSlot[]>(url, { return Fetch.safeJson<CodexSlotResponse[]>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -51,22 +62,40 @@ export class CodexMarketplace {
/** /**
* Returns active slot with id {slotId} for the host * Returns active slot with id {slotId} for the host
*/ */
async activeSlot(slotId: string): Promise<SafeValue<CodexSlot>> { async activeSlot(slotId: string): Promise<SafeValue<CodexSlotAgent>> {
const url = this.url + Api.config.prefix + "/sales/slots/" + slotId; const url = this.url + Api.config.prefix + "/sales/slots/" + slotId;
return Fetch.safeJson<CodexSlot>(url, { return Fetch.safeJson<CodexSlotAgentResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
} }
private transformAvailability({
freeSize,
...a
}: CodexAvailabilityWithoutTypes) {
const availability: CodexAvailability = {
...a,
minPricePerBytePerSecond: parseInt(a.minPricePerBytePerSecond, 10),
totalCollateral: parseInt(a.totalCollateral, 10),
totalRemainingCollateral: parseInt(a.totalRemainingCollateral, 10),
};
if (freeSize) {
availability.freeSize = freeSize;
}
return availability;
}
/** /**
* Returns storage that is for sale * Returns storage that is for sale
*/ */
async availabilities(): Promise<SafeValue<CodexAvailability[]>> { async availabilities(): Promise<SafeValue<CodexAvailability[]>> {
const url = this.url + Api.config.prefix + "/sales/availability"; const url = this.url + Api.config.prefix + "/sales/availability";
const res = await Fetch.safeJson<CodexAvailabilityDto[]>(url, { const res = await Fetch.safeJson<CodexAvailabilityResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -77,15 +106,7 @@ export class CodexMarketplace {
return { return {
error: false, error: false,
data: res.data.map((a) => ({ data: res.data.map(this.transformAvailability),
id: a.id,
totalSize: parseInt(a.totalSize, 10),
freeSize: parseInt(a.freeSize, 10),
duration: parseInt(a.duration, 10),
minPricePerBytePerSecond: parseInt(a.minPricePerBytePerSecond, 10),
totalCollateral: parseInt(a.totalCollateral, 10),
totalRemainingCollateral: parseInt(a.totalRemainingCollateral, 10),
})),
}; };
} }
@ -94,7 +115,7 @@ export class CodexMarketplace {
*/ */
async createAvailability( async createAvailability(
input: CodexCreateAvailabilityInput input: CodexCreateAvailabilityInput
): Promise<SafeValue<CodexAvailabilityCreateResponse>> { ): Promise<SafeValue<CodexAvailability>> {
const result = v.safeParse(CodexCreateAvailabilityInput, input); const result = v.safeParse(CodexCreateAvailabilityInput, input);
if (!result.success) { if (!result.success) {
@ -108,17 +129,32 @@ export class CodexMarketplace {
const url = this.url + Api.config.prefix + "/sales/availability"; const url = this.url + Api.config.prefix + "/sales/availability";
const body = result.output; const body: CodexAvailabilityCreateBody = {
totalSize: result.output.totalSize,
duration: result.output.duration,
minPricePerBytePerSecond:
result.output.minPricePerBytePerSecond.toString(),
totalCollateral: result.output.totalCollateral.toString(),
};
if (result.output.enabled) {
body.enabled = result.output.enabled;
}
if (result.output.until) {
body.until = result.output.until;
}
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, { return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
method: "POST", method: "POST",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({ body: JSON.stringify(body),
totalSize: body.totalSize.toString(), }).then((result) => {
duration: body.duration.toString(), if (result.error) {
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(), return result;
totalCollateral: body.totalCollateral.toString(), }
}),
return { error: false, data: this.transformAvailability(result.data) };
}); });
} }
@ -127,9 +163,9 @@ export class CodexMarketplace {
* Existing Requests linked to this Availability will continue as is. * Existing Requests linked to this Availability will continue as is.
*/ */
async updateAvailability( async updateAvailability(
input: CodexUpdateAvailabilityInput input: CodexAvailabilityPatchInput
): Promise<SafeValue<"">> { ): Promise<SafeValue<"">> {
const result = v.safeParse(CodexUpdateAvailabilityInput, input); const result = v.safeParse(CodexAvailabilityPatchInput, input);
if (!result.success) { if (!result.success) {
return { return {
@ -143,17 +179,26 @@ export class CodexMarketplace {
const url = const url =
this.url + Api.config.prefix + "/sales/availability/" + result.output.id; this.url + Api.config.prefix + "/sales/availability/" + result.output.id;
const body = result.output; const body: CodexAvailabilityCreateBody = {
totalSize: result.output.totalSize,
duration: result.output.duration,
minPricePerBytePerSecond:
result.output.minPricePerBytePerSecond.toString(),
totalCollateral: result.output.totalCollateral.toString(),
};
if (result.output.enabled) {
body.enabled = result.output.enabled;
}
if (result.output.until) {
body.until = result.output.until;
}
const res = await Fetch.safe(url, { const res = await Fetch.safe(url, {
method: "PATCH", method: "PATCH",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({ body: JSON.stringify(body),
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
totalCollateral: body.totalCollateral.toString(),
}),
}); });
if (res.error) { if (res.error) {
@ -174,7 +219,7 @@ export class CodexMarketplace {
Api.config.prefix + Api.config.prefix +
`/sales/availability/${availabilityId}/reservations`; `/sales/availability/${availabilityId}/reservations`;
return Fetch.safeJson<CodexReservation[]>(url, { return Fetch.safeJson<CodexReservationsResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -183,22 +228,47 @@ export class CodexMarketplace {
/** /**
* Returns list of purchase IDs * Returns list of purchase IDs
*/ */
async purchaseIds(): Promise<SafeValue<string[]>> { async purchaseIds(): Promise<SafeValue<CodexPurchaseIdsResponse>> {
const url = this.url + Api.config.prefix + `/storage/purchases`; const url = this.url + Api.config.prefix + `/storage/purchases`;
return Fetch.safeJson<string[]>(url, { return Fetch.safeJson<CodexPurchaseIdsResponse>(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
} }
async purchases(): Promise<SafeValue<CodexPurchase[]>> { private transformPurchase(p: CodexPurchaseWithoutTypes): CodexPurchase {
const url = this.url + Api.config.prefix + `/storage/purchases`; const purchase: CodexPurchase = {
requestId: p.requestId,
state: p.state,
};
const res = await Fetch.safeJson<string[]>(url, { if (p.error) {
method: "GET", purchase.error = p.error;
headers: FetchAuthBuilder.build(this.auth), }
});
if (!p.request) {
return purchase;
}
return {
...purchase,
request: {
...p.request,
ask: {
...p.request.ask,
proofProbability: parseInt(p.request.ask.proofProbability, 10),
pricePerBytePerSecond: parseInt(
p.request.ask.pricePerBytePerSecond,
10
),
},
},
};
}
async purchases(): Promise<SafeValue<CodexPurchase[]>> {
const res = await this.purchaseIds();
if (res.error) { if (res.error) {
return res; return res;
@ -220,7 +290,6 @@ export class CodexMarketplace {
state: "error", state: "error",
error: p.data.message, error: p.data.message,
requestId: "", requestId: "",
request: {} as CodexStorageRequest,
} satisfies CodexPurchase) } satisfies CodexPurchase)
: p.data : p.data
), ),
@ -234,9 +303,15 @@ export class CodexMarketplace {
const url = const url =
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId; this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
return Fetch.safeJson<CodexPurchase>(url, { return Fetch.safeJson<CodexPurchaseResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
method: "GET",
}).then((res) => {
if (res.error) {
return res;
}
return { error: false, data: this.transformPurchase(res.data) };
}); });
} }
@ -269,24 +344,18 @@ export class CodexMarketplace {
} = result.output; } = result.output;
const url = this.url + Api.config.prefix + "/storage/request/" + cid; const url = this.url + Api.config.prefix + "/storage/request/" + cid;
const res = await Fetch.safe(url, { return Fetch.safeText(url, {
method: "POST", method: "POST",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({ body: JSON.stringify({
duration: duration.toString(), duration,
pricePerBytePerSecond: pricePerBytePerSecond.toString(), pricePerBytePerSecond: pricePerBytePerSecond.toString(),
proofProbability: proofProbability.toString(), proofProbability: proofProbability.toString(),
nodes, nodes,
collateralPerByte: collateralPerByte.toString(), collateralPerByte: collateralPerByte.toString(),
expiry: expiry.toString(), expiry,
tolerance, tolerance,
}), } satisfies CodexStorageRequestCreateBody),
}); });
if (res.error) {
return res;
}
return { error: false, data: await res.data.text() };
} }
} }

View File

@ -1,232 +1,122 @@
import type { components, paths } from "../openapi";
import * as v from "valibot"; import * as v from "valibot";
export type CodexStorageRequest = { export type CodexSlotResponse =
id: string; paths["/sales/slots"]["get"]["responses"][200]["content"]["application/json"];
/** export type CodexSlot = CodexSlotResponse;
* Address of Ethereum address
*/
client: string;
ask: { export type CodexSlotAgentResponse =
/** paths["/sales/slots/{slotId}"]["get"]["responses"][200]["content"]["application/json"];
* Number of slots that the tequest want to have the content spread over.
*/
slots: number;
/** export type CodexSlotAgent = CodexSlotAgentResponse;
* Amount of storage per slot (in bytes) as decimal string.
*/
slotSize: string;
/** export type CodexAvailabilityResponse =
* The duration of the storage request in seconds. paths["/sales/availability"]["get"]["responses"][200]["content"]["application/json"];
*/
duration: string;
/** export type CodexAvailabilityWithoutTypes =
* How often storage proofs are required as decimal string (in periods). components["schemas"]["SalesAvailabilityREAD"];
*/
proofProbability: string;
/** export type CodexAvailability = Omit<
* The amount of tokens paid per byte per second per slot to hosts the client is willing to pay CodexAvailabilityWithoutTypes,
*/ | "freeSize"
pricePerBytePerSecond: string; | "totalSize"
| "minPricePerBytePerSecond"
/** | "duration"
* Max slots that can be lost without data considered to be lost. | "totalCollateral"
*/ | "totalRemainingCollateral"
maxSlotLoss: number; > & {
}; freeSize?: number;
content: {
/**
* Unique Content ID
*/
cid: string;
/**
* Erasure code parameters
*/
// erasure: {
/**
* Total number of chunks generated by the erasure code process.
*/
// totalChunks: number;
// };
/**
* Parameters for Proof of Retrievability
*/
// por: {
// u: string;
// publicKey: string;
// name: string;
// };
};
/* Number as decimal string that represents expiry threshold in seconds from when the Request is submitted.
* When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided.
* The number of seconds can not be higher then the Request's duration itself.
*/
expiry: string;
/**
* Random data
*/
nonce: string;
};
/**
* A storage slot is a portion of a storage contract that needs to be fulfilled
* by a host. To initiate a contract, all the slots must be filled.
*/
export type CodexSlot = {
id: string;
request: CodexStorageRequest;
/**
* The slot index as hexadecimal string
*/
slotIndex: "string";
};
/**
* Storage availability for sell.
*/
export type CodexAvailability = {
id: string;
/**
* Size of available storage in bytes
*/
totalSize: number; totalSize: number;
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: number;
/**
* Maximum time the storage should be sold for (in seconds)
*/
duration: number; duration: number;
/**
* Minimal price per byte per second paid (in amount of tokens) for the
* hosted request's slot for the request's duration as decimal string
*/
minPricePerBytePerSecond: number; minPricePerBytePerSecond: number;
/**
* Total collateral (in amount of tokens) that can be used for matching requests
*/
totalCollateral: number; totalCollateral: number;
totalRemainingCollateral: number; totalRemainingCollateral: number;
}; };
/** export type CodexAvailabilityCreateResponse =
* Storage availability received from the api. paths["/sales/availability"]["post"]["responses"][201]["content"]["application/json"];
*/
export type CodexAvailabilityDto = {
id: string;
/** export type CodexAvailabilityCreateBody = Exclude<
* Size of available storage in bytes paths["/sales/availability"]["post"]["requestBody"],
*/ undefined
totalSize: string; >["content"]["application/json"];
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: string;
/**
* Maximum time the storage should be sold for (in seconds)
*/
duration: string;
/**
* Minimal price per byte per second paid (in amount of tokens) for the
* hosted request's slot for the request's duration as decimal string
*/
minPricePerBytePerSecond: string;
/**
* Total collateral (in amount of tokens) that can be used for matching requests
*/
totalCollateral: string;
totalRemainingCollateral: string;
};
export type CodexAvailabilityCreateResponse = CodexAvailability & {
id: string;
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: string;
};
export const CodexCreateAvailabilityInput = v.strictObject({ export const CodexCreateAvailabilityInput = v.strictObject({
totalSize: v.pipe(v.number(), v.minValue(1)), totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)), duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(), minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(), totalCollateral: v.number(),
enabled: v.optional(v.boolean()),
until: v.optional(v.number()),
}); });
export type CodexAvailabilityPatchResponse =
paths["/sales/availability/{id}"]["patch"]["responses"][204]["content"];
export type CodexAvailabilityPatchBody = Exclude<
paths["/sales/availability"]["post"]["requestBody"],
undefined
>["content"]["application/json"];
export type CodexCreateAvailabilityInput = v.InferOutput< export type CodexCreateAvailabilityInput = v.InferOutput<
typeof CodexCreateAvailabilityInput typeof CodexCreateAvailabilityInput
>; >;
export const CodexUpdateAvailabilityInput = v.strictObject({ export const CodexAvailabilityPatchInput = v.strictObject({
id: v.string(), id: v.string(),
totalSize: v.pipe(v.number(), v.minValue(1)), totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)), duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(), minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(), totalCollateral: v.number(),
enabled: v.optional(v.boolean()),
until: v.optional(v.number()),
}); });
export type CodexUpdateAvailabilityInput = v.InferOutput< export type CodexAvailabilityPatchInput = v.InferOutput<
typeof CodexUpdateAvailabilityInput typeof CodexAvailabilityPatchInput
>; >;
export type CodexReservation = { export type CodexReservationsResponse =
id: string; paths["/sales/availability/{id}/reservations"]["get"]["responses"][200]["content"]["application/json"];
availabilityId: string;
requestId: string;
/** export type CodexReservation = components["schemas"]["Reservation"];
* Size in bytes
*/
size: string;
/** export type CodexPurchaseIdsResponse =
* Slot Index as hexadecimal string paths["/storage/purchases"]["get"]["responses"][200]["content"]["application/json"];
*/
slotIndex: string; export type CodexPurchaseResponse =
paths["/storage/purchases/{id}"]["get"]["responses"][200]["content"]["application/json"];
export type CodexStorageAsk = Omit<
components["schemas"]["StorageAsk"],
"slotSize" | "duration" | "proofProbability" | "pricePerBytePerSecond"
> & {
slotSize: number;
duration: number;
proofProbability: number;
pricePerBytePerSecond: number;
}; };
export type CodexPurchase = { export type CodexPurchaseWithoutTypes = components["schemas"]["Purchase"];
/**
* Description of the request's state
*/
state: string;
/** export type CodexPurchase = Omit<
* If request failed, then here is presented the error message components["schemas"]["Purchase"],
*/ "request"
error: string; > & {
request?: Omit<components["schemas"]["StorageRequest"], "ask"> & {
request: CodexStorageRequest; ask: CodexStorageAsk;
};
requestId: string;
}; };
export type CodexStorageRequestResponse =
paths["/storage/request/{cid}"]["post"]["responses"][200]["content"]["text/plain"];
export type CodexStorageRequestCreateBody = Exclude<
paths["/storage/request/{cid}"]["post"]["requestBody"],
undefined
>["content"]["application/json"];
export const CodexCreateStorageRequestInput = v.strictObject({ export const CodexCreateStorageRequestInput = v.strictObject({
cid: v.string(), cid: v.string(),
duration: v.pipe(v.number(), v.minValue(1)), duration: v.pipe(v.number(), v.minValue(1)),

View File

@ -5,7 +5,14 @@ import {
type FetchAuth, type FetchAuth,
} from "../fetch-safe/fetch-safe"; } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values"; import type { SafeValue } from "../values/values";
import type { CodexSpr } from "./types"; import type {
CodexPeerId,
CodexPeerIdContentType,
CodexPeerIdJsonResponse,
CodexSpr,
CodexSprContentType,
CodexSprJsonResponse,
} from "./types";
type CodexNodeOptions = { type CodexNodeOptions = {
auth?: FetchAuth; auth?: FetchAuth;
@ -25,9 +32,8 @@ export class CodexNode {
/** /**
* Connect to a peer * Connect to a peer
* TODO check result
*/ */
connect(peerId: string, addrs: string[] = []) { connect(peerId: string, addrs: string[] = []): Promise<SafeValue<string>> {
const params = new URLSearchParams(); const params = new URLSearchParams();
for (const addr of addrs) { for (const addr of addrs) {
@ -37,7 +43,7 @@ export class CodexNode {
const url = const url =
this.url + Api.config.prefix + `/connect/${peerId}?` + params.toString(); this.url + Api.config.prefix + `/connect/${peerId}?` + params.toString();
return Fetch.safe(url, { return Fetch.safeText(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: FetchAuthBuilder.build(this.auth),
}); });
@ -46,25 +52,54 @@ export class CodexNode {
/** /**
* Get Node's SPR * Get Node's SPR
*/ */
async spr(): Promise<SafeValue<CodexSpr>> { async spr(
type: CodexSprContentType = "json"
): Promise<SafeValue<CodexSpr<CodexSprContentType>>> {
const url = this.url + Api.config.prefix + "/spr"; const url = this.url + Api.config.prefix + "/spr";
return Fetch.safeJson(url, { if (type === "json") {
return Fetch.safeJson<CodexSprJsonResponse>(url, {
method: "GET",
headers: {
...FetchAuthBuilder.build(this.auth),
"Content-Type": "application/json",
},
});
}
return Fetch.safeText(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: {
...FetchAuthBuilder.build(this.auth),
"Content-Type": "text/plain",
},
}); });
} }
/** /**
* Get Node's PeerID * Get Node's PeerID
* TODO check result
*/ */
peerId() { peerId(
type: CodexPeerIdContentType = "json"
): Promise<SafeValue<CodexPeerId<CodexPeerIdContentType>>> {
const url = this.url + Api.config.prefix + "/node/peerid"; const url = this.url + Api.config.prefix + "/node/peerid";
return Fetch.safe(url, { if (type === "json") {
return Fetch.safeJson<CodexPeerIdJsonResponse>(url, {
method: "GET",
headers: {
...FetchAuthBuilder.build(this.auth),
"Content-Type": "application/json",
},
});
}
return Fetch.safeText(url, {
method: "GET", method: "GET",
headers: FetchAuthBuilder.build(this.auth), headers: {
...FetchAuthBuilder.build(this.auth),
"Content-Type": "text/plain",
},
}); });
} }
} }

View File

@ -1,3 +1,29 @@
export type CodexSpr = { import type { paths } from "../openapi";
spr: string;
}; export type CodexSprTextResponse =
paths["/spr"]["get"]["responses"][200]["content"]["text/plain"];
export type CodexSprJsonResponse =
paths["/spr"]["get"]["responses"][200]["content"]["application/json"];
export type CodexSprContentType = "json" | "text";
export type CodexSpr<T extends CodexSprContentType> = T extends "json"
? CodexSprJsonResponse
: T extends "text"
? CodexSprTextResponse
: never;
export type CodexPeerIdTextResponse =
paths["/peerid"]["get"]["responses"][200]["content"]["text/plain"];
export type CodexPeerIdJsonResponse =
paths["/peerid"]["get"]["responses"][200]["content"]["application/json"];
export type CodexPeerIdContentType = "json" | "text";
export type CodexPeerId<T extends CodexPeerIdContentType> = T extends "json"
? CodexPeerIdJsonResponse
: T extends "text"
? CodexPeerIdTextResponse
: never;

1449
src/openapi.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"sourceMap": true "sourceMap": true,
"noUncheckedIndexedAccess": true
} }
} }