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
dist
*.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.
## 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
### Sync api
@ -126,7 +134,7 @@ const marketplace = codex.marketplace;
Returns active slots.
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L7)[]>
Example:
@ -139,7 +147,7 @@ const slots = await marketplace.activeSlots();
Returns active slot with id {slotId} for the host.
- slotId (string, required)
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
- returns Promise<[CodexSlotAgent](./src/marketplace/types.ts#L12)[]>
Example:
@ -152,7 +160,7 @@ const slot = await marketplace.activeSlot(slotId);
Returns storage that is for sale.
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L99)>
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)>
Example:
@ -164,8 +172,8 @@ const availabilities = await marketplace.availabilities();
Offers storage for sale.
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L175), required)
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L186)[]>
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L45), required)
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)[]>
Example:
@ -182,7 +190,7 @@ const response = await marketplace.createAvailability({
Updates availability.
- input ([CodexUpdateAvailabilityInput](./src/marketplace/types.ts#L186), required)
- input ([CodexAvailabilityPatchInput](./src/marketplace/types.ts#L66), required)
- returns Promise<"">
Example:
@ -202,7 +210,7 @@ const response = await marketplace.updateAvailability({
Return list of reservations for ongoing Storage Requests that the node hosts.
- availabilityId (string, required)
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L198)[]>
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L83)[]>
Example:
@ -214,7 +222,7 @@ const reservations = await marketplace.reservations("Ox...");
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>
Example:
@ -248,7 +256,7 @@ const ids = await marketplace.purchaseIds();
Returns purchase details
- purchaseId (string, required)
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L214)[]>
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L103)[]>
Example:
@ -274,7 +282,7 @@ const data = codex.data;
Returns the manifest stored locally in node.
- returns Promise<[CodexDataResponse](./src/data/types.ts#L54)[]>
- returns Promise<[CodexDataItem](./src/data/types.ts#L8)[]>
Example:
@ -286,7 +294,7 @@ const cids = await data.cids();
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:
@ -301,7 +309,7 @@ Upload a file in a streaming manner
#### Browser
- stategy [BrowserUploadStategy](./src/data/browser-upload.ts#L5)
- returns [UploadResponse](./src/data/types.ts#L80)
- returns [UploadResponse](./src/data/types.ts#L17)
Example:
@ -330,8 +338,8 @@ console.info("CID is", res.data);
#### Node
- stategy [NodeUploadStategy](./src/data/node-download.ts#L8)
- returns [UploadResponse](./src/data/types.ts#L80)
- stategy [NodeUploadStategy](./src/data/node-upload.ts#L9)
- returns [UploadResponse](./src/data/types.ts#L17)
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.
- cid (string, required)
- returns [CodexManifest](./src/data/types.ts#L3)
- returns [CodexManifest](./src/data/types.ts#L30)
Example:
@ -410,7 +418,7 @@ const data = codex.debug;
Set log level at run time.
- level ([CodexLogLevel](./src/debug/types.ts#L3), required)
- level ([CodexLogLevel](./src/debug/types.ts#L7), required)
- returns Promise<"">
Example:
@ -423,7 +431,7 @@ await debug.setLogLevel("DEBUG");
Gets node information
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L15)>
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L23)>
Example:
@ -448,10 +456,44 @@ const node = codex.node;
Get Node's SPR
- returns Promise<[CodexSpr](./src/node/types.ts#L1)>
- returns Promise<[CodexSpr](./src/node/types.ts#L11)>
Example:
```js
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",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.6.0"
"undici": "^7.7.0"
},
"devDependencies": {
"prettier": "^3.5.3"
@ -24,11 +24,14 @@
"version": "0.1.1",
"license": "MIT",
"dependencies": {
"valibot": "^0.32.0"
"undici": "^7.7.0",
"valibot": "^1.0.0"
},
"devDependencies": {
"@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",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
@ -65,9 +68,9 @@
}
},
"node_modules/undici": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz",
"integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==",
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz",
"integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"

View File

@ -2,15 +2,13 @@
"name": "@codex-storage/sdk-js-update-node-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node esbuild.js"
},
"scripts": {},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.6.0"
"undici": "^7.7.0"
},
"devDependencies": {
"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": {
"@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",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.1.1"
},
"dependencies": {
"valibot": "^0.32.0"
"valibot": "^1.0.0"
},
"peerDependencies": {
"undici": "^7.0.0"
"undici": "^7.7.0"
}
}

View File

@ -7,11 +7,14 @@ import {
import type { SafeValue } from "../values/values";
import type {
CodexDataResponse,
CodexManifest,
CodexNodeSpace,
UploadStategy,
NetworkDownloadResponse,
UploadResponse,
CodexSpaceResponse,
CodexNodeSpace,
CodexDataNetworkResponse,
CodexNetworkDownload,
CodexManifest,
CodexDataItems,
} from "./types";
type CodexDataOptions = {
@ -34,7 +37,7 @@ export class CodexData {
* Lists manifest CIDs stored locally in node.
* 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";
return Fetch.safeJson<CodexDataResponse>(url, {
@ -52,10 +55,10 @@ export class CodexData {
/**
* Gets a summary of the storage space allocation of the node.
*/
space() {
space(): Promise<SafeValue<CodexNodeSpace>> {
const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexNodeSpace>(url, {
return Fetch.safeJson<CodexSpaceResponse>(url, {
method: "GET",
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.
* Note: Download is performed async. Call can return before download is completed.
*/
async networkDownload(
cid: string
): Promise<SafeValue<NetworkDownloadResponse>> {
async networkDownload(cid: string): Promise<SafeValue<CodexNetworkDownload>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson(url, {
return Fetch.safeJson<CodexDataNetworkResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
});
@ -123,7 +124,7 @@ export class CodexData {
* Download only the dataset manifest from the network to the local node
* 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`;
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 { SafeValue } from "../values/values";
export type CodexManifest = {
/**
* "Root hash of the content"
*/
// rootHash: string;
export type CodexDataResponse =
paths["/data"]["get"]["responses"][200]["content"]["application/json"];
/**
* Length of original content in bytes
*/
// originalBytes: number;
export type CodexDataItem = components["schemas"]["DataItem"];
/**
* Total size of all blocks
*/
datasetSize: number;
export type CodexDataItems = CodexDataResponse;
/**
* "Size of blocks"
*/
blockSize: number;
export type CodexSpaceResponse =
paths["/space"]["get"]["responses"][200]["content"]["application/json"];
/**
* 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 CodexNodeSpace = CodexSpaceResponse;
export type UploadResponse = {
result: Promise<SafeValue<string>>;
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 = {
auth?: FetchAuth;

View File

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

View File

@ -1,6 +1,13 @@
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",
"DEBUG",
"INFO",
@ -10,49 +17,7 @@ export const CodexLogLevel = v.picklist([
"FATAL",
]);
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>;
export type CodexInfoResponse =
paths["/debug/info"]["get"]["responses"][200]["content"]["application/json"];
export type CodexDebugInfo = {
/**
* 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
}
};
export type CodexDebugInfo = CodexInfoResponse;

View File

@ -60,4 +60,14 @@ export const Fetch = {
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() {
return {
id: randomEthereumAddress(),
totalSize: randomInt(0, 9).toString(),
duration: randomInt(0, 9).toString(),
minPrice: randomInt(0, 9).toString(),
maxCollateral: randomInt(0, 9).toString(),
totalSize: randomInt(0, 9),
duration: randomInt(0, 9),
minPrice: randomInt(0, 9),
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 () => {
const data = { ...createAvailability(), freeSize: "1000" };
const data = { ...createAvailability(), freeSize: 1000 };
const spy = vi.spyOn(Fetch, "safeJson");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
@ -228,16 +230,16 @@ describe("marketplace", () => {
});
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");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
const response = await marketplace.createAvailability({
totalCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
duration: 100,
totalCollateral: data.totalCollateral,
totalSize: data.totalSize,
minPricePerBytePerSecond: data.minPricePerBytePerSecond,
duration: data.duration,
});
assert.ok(!response.error);

View File

@ -8,16 +8,27 @@ import {
} from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
import {
type CodexAvailabilityResponse,
type CodexAvailability,
type CodexSlot,
type CodexSlotAgent,
type CodexSlotResponse,
type CodexSlotAgentResponse,
type CodexAvailabilityWithoutTypes,
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,
CodexCreateStorageRequestInput,
type CodexPurchase,
type CodexReservation,
type CodexSlot,
type CodexStorageRequest,
CodexUpdateAvailabilityInput,
} from "./types";
type CodexMarketplaceOptions = {
@ -42,7 +53,7 @@ export class CodexMarketplace {
async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
const url = this.url + Api.config.prefix + "/sales/slots";
return Fetch.safeJson<CodexSlot[]>(url, {
return Fetch.safeJson<CodexSlotResponse[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
@ -51,22 +62,40 @@ export class CodexMarketplace {
/**
* 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;
return Fetch.safeJson<CodexSlot>(url, {
return Fetch.safeJson<CodexSlotAgentResponse>(url, {
method: "GET",
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
*/
async availabilities(): Promise<SafeValue<CodexAvailability[]>> {
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",
headers: FetchAuthBuilder.build(this.auth),
});
@ -77,15 +106,7 @@ export class CodexMarketplace {
return {
error: false,
data: res.data.map((a) => ({
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),
})),
data: res.data.map(this.transformAvailability),
};
}
@ -94,7 +115,7 @@ export class CodexMarketplace {
*/
async createAvailability(
input: CodexCreateAvailabilityInput
): Promise<SafeValue<CodexAvailabilityCreateResponse>> {
): Promise<SafeValue<CodexAvailability>> {
const result = v.safeParse(CodexCreateAvailabilityInput, input);
if (!result.success) {
@ -108,17 +129,32 @@ export class CodexMarketplace {
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, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
totalCollateral: body.totalCollateral.toString(),
}),
body: JSON.stringify(body),
}).then((result) => {
if (result.error) {
return result;
}
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.
*/
async updateAvailability(
input: CodexUpdateAvailabilityInput
input: CodexAvailabilityPatchInput
): Promise<SafeValue<"">> {
const result = v.safeParse(CodexUpdateAvailabilityInput, input);
const result = v.safeParse(CodexAvailabilityPatchInput, input);
if (!result.success) {
return {
@ -143,17 +179,26 @@ export class CodexMarketplace {
const url =
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, {
method: "PATCH",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
totalCollateral: body.totalCollateral.toString(),
}),
body: JSON.stringify(body),
});
if (res.error) {
@ -174,7 +219,7 @@ export class CodexMarketplace {
Api.config.prefix +
`/sales/availability/${availabilityId}/reservations`;
return Fetch.safeJson<CodexReservation[]>(url, {
return Fetch.safeJson<CodexReservationsResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
@ -183,22 +228,47 @@ export class CodexMarketplace {
/**
* Returns list of purchase IDs
*/
async purchaseIds(): Promise<SafeValue<string[]>> {
async purchaseIds(): Promise<SafeValue<CodexPurchaseIdsResponse>> {
const url = this.url + Api.config.prefix + `/storage/purchases`;
return Fetch.safeJson<string[]>(url, {
return Fetch.safeJson<CodexPurchaseIdsResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
async purchases(): Promise<SafeValue<CodexPurchase[]>> {
const url = this.url + Api.config.prefix + `/storage/purchases`;
private transformPurchase(p: CodexPurchaseWithoutTypes): CodexPurchase {
const purchase: CodexPurchase = {
requestId: p.requestId,
state: p.state,
};
const res = await Fetch.safeJson<string[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
if (p.error) {
purchase.error = p.error;
}
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) {
return res;
@ -220,7 +290,6 @@ export class CodexMarketplace {
state: "error",
error: p.data.message,
requestId: "",
request: {} as CodexStorageRequest,
} satisfies CodexPurchase)
: p.data
),
@ -234,9 +303,15 @@ export class CodexMarketplace {
const url =
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
return Fetch.safeJson<CodexPurchase>(url, {
method: "GET",
return Fetch.safeJson<CodexPurchaseResponse>(url, {
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;
const url = this.url + Api.config.prefix + "/storage/request/" + cid;
const res = await Fetch.safe(url, {
return Fetch.safeText(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
duration: duration.toString(),
duration,
pricePerBytePerSecond: pricePerBytePerSecond.toString(),
proofProbability: proofProbability.toString(),
nodes,
collateralPerByte: collateralPerByte.toString(),
expiry: expiry.toString(),
expiry,
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";
export type CodexStorageRequest = {
id: string;
export type CodexSlotResponse =
paths["/sales/slots"]["get"]["responses"][200]["content"]["application/json"];
/**
* Address of Ethereum address
*/
client: string;
export type CodexSlot = CodexSlotResponse;
ask: {
/**
* Number of slots that the tequest want to have the content spread over.
*/
slots: number;
export type CodexSlotAgentResponse =
paths["/sales/slots/{slotId}"]["get"]["responses"][200]["content"]["application/json"];
/**
* Amount of storage per slot (in bytes) as decimal string.
*/
slotSize: string;
export type CodexSlotAgent = CodexSlotAgentResponse;
/**
* The duration of the storage request in seconds.
*/
duration: string;
export type CodexAvailabilityResponse =
paths["/sales/availability"]["get"]["responses"][200]["content"]["application/json"];
/**
* How often storage proofs are required as decimal string (in periods).
*/
proofProbability: string;
export type CodexAvailabilityWithoutTypes =
components["schemas"]["SalesAvailabilityREAD"];
/**
* The amount of tokens paid per byte per second per slot to hosts the client is willing to pay
*/
pricePerBytePerSecond: string;
/**
* Max slots that can be lost without data considered to be lost.
*/
maxSlotLoss: 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
*/
export type CodexAvailability = Omit<
CodexAvailabilityWithoutTypes,
| "freeSize"
| "totalSize"
| "minPricePerBytePerSecond"
| "duration"
| "totalCollateral"
| "totalRemainingCollateral"
> & {
freeSize?: 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;
/**
* 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;
/**
* Total collateral (in amount of tokens) that can be used for matching requests
*/
totalCollateral: number;
totalRemainingCollateral: number;
};
/**
* Storage availability received from the api.
*/
export type CodexAvailabilityDto = {
id: string;
export type CodexAvailabilityCreateResponse =
paths["/sales/availability"]["post"]["responses"][201]["content"]["application/json"];
/**
* Size of available storage in bytes
*/
totalSize: string;
/**
* 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 type CodexAvailabilityCreateBody = Exclude<
paths["/sales/availability"]["post"]["requestBody"],
undefined
>["content"]["application/json"];
export const CodexCreateAvailabilityInput = v.strictObject({
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: 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<
typeof CodexCreateAvailabilityInput
>;
export const CodexUpdateAvailabilityInput = v.strictObject({
export const CodexAvailabilityPatchInput = v.strictObject({
id: v.string(),
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(),
enabled: v.optional(v.boolean()),
until: v.optional(v.number()),
});
export type CodexUpdateAvailabilityInput = v.InferOutput<
typeof CodexUpdateAvailabilityInput
export type CodexAvailabilityPatchInput = v.InferOutput<
typeof CodexAvailabilityPatchInput
>;
export type CodexReservation = {
id: string;
availabilityId: string;
requestId: string;
export type CodexReservationsResponse =
paths["/sales/availability/{id}/reservations"]["get"]["responses"][200]["content"]["application/json"];
/**
* Size in bytes
*/
size: string;
export type CodexReservation = components["schemas"]["Reservation"];
/**
* Slot Index as hexadecimal string
*/
slotIndex: string;
export type CodexPurchaseIdsResponse =
paths["/storage/purchases"]["get"]["responses"][200]["content"]["application/json"];
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 = {
/**
* Description of the request's state
*/
state: string;
export type CodexPurchaseWithoutTypes = components["schemas"]["Purchase"];
/**
* If request failed, then here is presented the error message
*/
error: string;
request: CodexStorageRequest;
requestId: string;
export type CodexPurchase = Omit<
components["schemas"]["Purchase"],
"request"
> & {
request?: Omit<components["schemas"]["StorageRequest"], "ask"> & {
ask: CodexStorageAsk;
};
};
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({
cid: v.string(),
duration: v.pipe(v.number(), v.minValue(1)),

View File

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

View File

@ -1,3 +1,29 @@
export type CodexSpr = {
spr: string;
};
import type { paths } from "../openapi";
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",
"moduleResolution": "Bundler",
"verbatimModuleSyntax": true,
"sourceMap": true
"sourceMap": true,
"noUncheckedIndexedAccess": true
}
}