mirror of
https://github.com/logos-storage/logos-storage-js.git
synced 2026-01-02 13:33:07 +00:00
Add data and marketplace API endpoints
This commit is contained in:
parent
945582bf57
commit
f5dd224479
@ -4,6 +4,8 @@ The Codex SDK provides an API for interacting with the Codex decentralized stora
|
||||
|
||||
The SDK has a small bundle size and support tree shaking.
|
||||
|
||||
The SDK is currently under early development and the API can change at any time.
|
||||
|
||||
## Import
|
||||
|
||||
```js
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -19,7 +19,7 @@
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
|
||||
@ -7,12 +7,14 @@
|
||||
"url": "https://github.com/codex-storage/codex-js"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npm run build",
|
||||
"prepack": "tsup tsup src/index.ts --format esm,cjs --dts",
|
||||
"prebuild": "rm -Rf dist/*",
|
||||
"build": "tsup tsup src/index.ts --format esm,cjs --dts",
|
||||
"build": "tsc --p tsconfig.test.json",
|
||||
"compile": "tsc --noEmit",
|
||||
"pretest": "npm run build",
|
||||
"pretest:only": "npm run build",
|
||||
"test": "node --test",
|
||||
"test:only": "node --test --test-only",
|
||||
"watch": "tsc --watch",
|
||||
"format": "prettier --write ./src"
|
||||
},
|
||||
@ -44,7 +46,7 @@
|
||||
"readme": "README.md",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
|
||||
19
src/chunks/chunks.ts
Normal file
19
src/chunks/chunks.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export const Chunks = {
|
||||
async split(file: File) {
|
||||
const totalSize = file.size;
|
||||
const chunkSize = 1024 * 1024 * 3; // 10MB
|
||||
|
||||
const chunks = [] as Uint8Array[];
|
||||
const amountOfChunks = Math.ceil(totalSize / chunkSize);
|
||||
|
||||
for (let index = 0; index < amountOfChunks; index++) {
|
||||
const start = index * chunkSize;
|
||||
const end = (index + 1) * chunkSize;
|
||||
|
||||
const chunk = await file.slice(start, end).arrayBuffer();
|
||||
chunks.push(new Uint8Array(chunk));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
};
|
||||
197
src/data/data.ts
Normal file
197
src/data/data.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import { Api } from "../api/config";
|
||||
import { Chunks } from "../chunks/chunks";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import type { CodexDataResponse, CodexNodeSpace } from "./types";
|
||||
|
||||
type UploadResponse = {
|
||||
result: Promise<SafeValue<string>>;
|
||||
abort: () => void;
|
||||
};
|
||||
|
||||
export class Data {
|
||||
readonly url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists manifest CIDs stored locally in node.
|
||||
* TODO: remove the faker data part when the api is ready
|
||||
*/
|
||||
cids(): Promise<SafeValue<CodexDataResponse>> {
|
||||
const url = this.url + Api.config.prefix + "/data";
|
||||
|
||||
return Fetch.safeJson<CodexDataResponse>(url, {
|
||||
method: "GET",
|
||||
}).then((data) => {
|
||||
if (data.error) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/* const mimetypes = [
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"audio/mp3",
|
||||
"video/mp4",
|
||||
"application/pdf",
|
||||
"application/msdoc",
|
||||
"text/plain",
|
||||
];
|
||||
*/
|
||||
return {
|
||||
error: false,
|
||||
data: {
|
||||
content: data.data.content /*.map((content) => {
|
||||
const random = Math.trunc(Math.random() * (mimetypes.length - 1));
|
||||
const mimetype = mimetypes[random];
|
||||
const [, extension] = mimetype?.split("/") || [];
|
||||
const filename = Array(5)
|
||||
.fill("")
|
||||
.map((_) => ((Math.random() * 36) | 0).toString(36))
|
||||
.join("");
|
||||
|
||||
return {
|
||||
cid: content.cid,
|
||||
manifest: {
|
||||
...content.manifest,
|
||||
filename: `${filename}.${extension}`,
|
||||
mimetype: mimetype || "",
|
||||
uploadedAt: new Date().toJSON(),
|
||||
},
|
||||
};
|
||||
})*/,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of the storage space allocation of the node.
|
||||
*/
|
||||
space() {
|
||||
const url = this.url + Api.config.prefix + "/space";
|
||||
|
||||
return Fetch.safeJson<CodexNodeSpace>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file in a streaming manner.
|
||||
* Once completed, the file is stored in the node and can be retrieved by any node in the network using the returned CID.
|
||||
* XMLHttpRequest is used instead of fetch for this case, to obtain progress information.
|
||||
* A callback onProgress can be passed to receive upload progress data information.
|
||||
*/
|
||||
async upload(
|
||||
file: File,
|
||||
onProgress?: (loaded: number, total: number) => void
|
||||
): Promise<UploadResponse> {
|
||||
const url = this.url + Api.config.prefix + "/data";
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const promise = new Promise<SafeValue<string>>(async (resolve) => {
|
||||
xhr.upload.onprogress = (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
onProgress?.(evt.loaded, evt.total);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("POST", url, true);
|
||||
|
||||
await Chunks.split(file);
|
||||
|
||||
xhr.send(new Blob(await Chunks.split(file)));
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status != 200) {
|
||||
resolve({
|
||||
error: true,
|
||||
data: {
|
||||
code: xhr.status,
|
||||
message: xhr.responseText,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
resolve({ error: false, data: xhr.response });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
resolve({
|
||||
error: true,
|
||||
data: {
|
||||
message: "Something went wrong during the file upload.",
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// const promise = Fetch.safe(url, {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "text/plain" },
|
||||
// body: file.stream(),
|
||||
// // @ts-ignore
|
||||
// duplex: "half",
|
||||
// })
|
||||
// .then(async (res) => {
|
||||
// console.info(res);
|
||||
// return res.error
|
||||
// ? res
|
||||
// : { error: false as false, data: await res.data.text() };
|
||||
// })
|
||||
|
||||
return {
|
||||
result: promise,
|
||||
abort: () => {
|
||||
xhr.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from the local node in a streaming manner.
|
||||
* If the file is not available locally, a 404 is returned.
|
||||
* There result is a readable stream.
|
||||
*/
|
||||
async localDownload(cid: string) {
|
||||
const url = this.url + Api.config.prefix + "/data/" + cid;
|
||||
|
||||
const res = await Fetch.safe(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"content-type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return res.data.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from the network in a streaming manner.
|
||||
* If the file is not available locally, it will be retrieved from other nodes in the network if able.
|
||||
*/
|
||||
async networkDownload(cid: string) {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
|
||||
|
||||
const res = await Fetch.safe(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"content-type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return res.data.body;
|
||||
}
|
||||
}
|
||||
81
src/data/types.ts
Normal file
81
src/data/types.ts
Normal file
@ -0,0 +1,81 @@
|
||||
export type CodexManifest = {
|
||||
/**
|
||||
* "Root hash of the content"
|
||||
*/
|
||||
// rootHash: string;
|
||||
|
||||
/**
|
||||
* Length of original content in bytes
|
||||
*/
|
||||
// originalBytes: number;
|
||||
|
||||
/**
|
||||
* Total size of all blocks
|
||||
*/
|
||||
datasetSize: number;
|
||||
|
||||
/**
|
||||
* "Size of blocks"
|
||||
*/
|
||||
blockSize: number;
|
||||
|
||||
/**
|
||||
* Indicates if content is protected by erasure-coding
|
||||
*/
|
||||
protected: boolean;
|
||||
|
||||
/**
|
||||
* Root of the merkle tree
|
||||
*/
|
||||
treeCid: string;
|
||||
|
||||
/**
|
||||
* Name of the name (not implemeted yet)
|
||||
*/
|
||||
filename: string;
|
||||
|
||||
/**
|
||||
* Mimetype (not implemeted yet)
|
||||
*/
|
||||
mimetype: string;
|
||||
|
||||
/**
|
||||
* Date of upload (not implemented at)
|
||||
*/
|
||||
uploadedAt: string;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
40
src/debug/debug.test.ts
Normal file
40
src/debug/debug.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import { Debug } from "./debug";
|
||||
import type { CodexLogLevel } from "./types";
|
||||
|
||||
describe("debug", () => {
|
||||
const debug = new Debug("http://localhost:3000");
|
||||
|
||||
it("returns an error when trying to setup the log level with a bad value", async () => {
|
||||
const response = await debug.setLogLevel("TEST" as CodexLogLevel);
|
||||
|
||||
assert.deepStrictEqual(response, {
|
||||
error: true,
|
||||
data: {
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
expected:
|
||||
'"TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL"',
|
||||
message:
|
||||
'Invalid type: Expected "TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL" but received "TEST"',
|
||||
path: undefined,
|
||||
received: '"TEST"',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a success when trying to setup the log level with a correct value", async (t) => {
|
||||
t.mock.method(Fetch, "safe", () =>
|
||||
Promise.resolve({ error: false, data: true })
|
||||
);
|
||||
|
||||
const response = await debug.setLogLevel("ERROR");
|
||||
|
||||
assert.deepStrictEqual(response, { error: false, data: true });
|
||||
});
|
||||
});
|
||||
52
src/debug/debug.ts
Normal file
52
src/debug/debug.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Api } from "../api/config";
|
||||
import { CodexValibotIssuesMap } from "../errors/errors";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import { CodexLogLevel, type CodexDebugInfo } from "./types";
|
||||
import * as v from "valibot";
|
||||
|
||||
export class Debug {
|
||||
readonly url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set log level at run time
|
||||
*/
|
||||
setLogLevel(level: CodexLogLevel) {
|
||||
const result = v.safeParse(CodexLogLevel, level);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message: "Cannot validate the input",
|
||||
errors: CodexValibotIssuesMap(result.issues),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const url =
|
||||
this.url +
|
||||
Api.config.prefix +
|
||||
"/debug/chronicles/loglevel?level=" +
|
||||
level;
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "POST",
|
||||
body: "",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node information
|
||||
*/
|
||||
info() {
|
||||
const url = this.url + Api.config.prefix + `/debug/info`;
|
||||
|
||||
return Fetch.safeJson<CodexDebugInfo>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
}
|
||||
33
src/debug/types.ts
Normal file
33
src/debug/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as v from "valibot";
|
||||
|
||||
export const CodexLogLevel = v.picklist([
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"NOTICE",
|
||||
"WARN",
|
||||
"ERROR",
|
||||
"FATAL",
|
||||
]);
|
||||
|
||||
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>;
|
||||
|
||||
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[];
|
||||
|
||||
/**
|
||||
* Path of the data repository where all nodes data are stored
|
||||
*/
|
||||
repo: "string";
|
||||
|
||||
// Signed Peer Record (libp2p)
|
||||
spr: string;
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import type { SafeValue } from "../values/values";
|
||||
|
||||
export class Disk {
|
||||
readonly url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
async available(): Promise<SafeValue<{ full: number; used: number }>> {
|
||||
return {
|
||||
error: false,
|
||||
data: {
|
||||
full: 500,
|
||||
used: 200,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -25,5 +25,5 @@ export const CodexValibotIssuesMap = (issues: InferIssue<any>[]) =>
|
||||
expected: i.expected,
|
||||
received: i.received,
|
||||
message: i.message,
|
||||
path: i.path.map((item: { key: string }) => item.key).join("."),
|
||||
path: i.path?.map((item: { key: string }) => item.key).join("."),
|
||||
}));
|
||||
|
||||
@ -45,48 +45,45 @@ class MockResponse implements Response {
|
||||
}
|
||||
}
|
||||
|
||||
describe("fetch", () => {
|
||||
describe.only("fetch", () => {
|
||||
it("returns an error when the http call failed", async (t) => {
|
||||
global.fetch = t.mock.fn(() =>
|
||||
Promise.resolve(new MockResponse(false, 500, "error"))
|
||||
Promise.resolve(new MockResponse(false, 500, "error")),
|
||||
);
|
||||
|
||||
const result = await Fetch.safe("http://localhost:3000/some-url", {
|
||||
const result = await Fetch.safeJson("http://localhost:3000/some-url", {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const error = {
|
||||
type: "api",
|
||||
message: "error",
|
||||
status: 500,
|
||||
code: 500,
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(result, { error: true, data: error });
|
||||
});
|
||||
|
||||
it("returns an error when the json parsing failed", async (t) => {
|
||||
it.only("returns an error when the json parsing failed", async (t) => {
|
||||
global.fetch = t.mock.fn(() =>
|
||||
Promise.resolve(new MockResponse(true, 200, ""))
|
||||
Promise.resolve(new MockResponse(true, 200, "")),
|
||||
);
|
||||
|
||||
const result = await Fetch.safe("http://localhost:3000/some-url", {
|
||||
const result = await Fetch.safeJson("http://localhost:3000/some-url", {
|
||||
method: "GET",
|
||||
});
|
||||
const error = {
|
||||
type: "error",
|
||||
message: "Unexpected end of JSON input",
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(result, { error: true, data: error });
|
||||
assert.ok(result.error);
|
||||
assert.deepStrictEqual(result.data.message, "Unexpected end of JSON input");
|
||||
});
|
||||
|
||||
it("returns the data when the fetch succeed", async (t) => {
|
||||
global.fetch = t.mock.fn(() =>
|
||||
Promise.resolve(
|
||||
new MockResponse(true, 200, JSON.stringify({ hello: "world" }))
|
||||
)
|
||||
new MockResponse(true, 200, JSON.stringify({ hello: "world" })),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await Fetch.safe("http://localhost:3000/some-url", {
|
||||
const result = await Fetch.safeJson("http://localhost:3000/some-url", {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@ -1,37 +1,50 @@
|
||||
import { Promises } from "../promise-safe/promise-safe";
|
||||
import { type SafeValue } from "../values/values";
|
||||
|
||||
export const Fetch = {
|
||||
async safe<T extends Object>(
|
||||
async safe(url: string, init: RequestInit): Promise<SafeValue<Response>> {
|
||||
const res = await Promises.safe(() => fetch(url, init));
|
||||
|
||||
if (res.error) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message:
|
||||
"The connection with the Codex node seems to be broken. Please check your node is running.",
|
||||
code: 502,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.data.ok) {
|
||||
const message = await Promises.safe(() => res.data.text());
|
||||
|
||||
if (message.error) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message: message.data,
|
||||
code: res.data.status,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { error: false, data: res.data };
|
||||
},
|
||||
|
||||
async safeJson<T extends Object>(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
init: RequestInit
|
||||
): Promise<SafeValue<T>> {
|
||||
const res = await fetch(url, init);
|
||||
const res = await this.safe(url, init);
|
||||
|
||||
if (!res.ok) {
|
||||
const message = await res.text();
|
||||
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message,
|
||||
code: res.status,
|
||||
},
|
||||
};
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = await res.json();
|
||||
|
||||
return { error: false, data: json };
|
||||
} catch (e) {
|
||||
const opts = e instanceof Error && e.stack ? { stack: e.stack } : {};
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message: e instanceof Error ? e.message : "JSON parsing error :" + e,
|
||||
...opts,
|
||||
},
|
||||
};
|
||||
}
|
||||
return Promises.safe(() => res.data.json());
|
||||
},
|
||||
};
|
||||
|
||||
54
src/index.ts
54
src/index.ts
@ -1,18 +1,28 @@
|
||||
import { Disk } from "./disk/disk";
|
||||
import type { Data } from "./data/data";
|
||||
import type { Node } from "./node/node";
|
||||
import { Marketplace } from "./marketplace/marketplace";
|
||||
import type { Debug } from "./debug/debug";
|
||||
|
||||
export * from "./fetch-safe/fetch-safe";
|
||||
export * from "./marketplace/types";
|
||||
export * from "./debug/types";
|
||||
export * from "./data/types";
|
||||
export * from "./values/values";
|
||||
export * from "./errors/errors";
|
||||
|
||||
export class Codex {
|
||||
readonly url: string;
|
||||
private _marketplace: Marketplace | null;
|
||||
readonly disk: Disk;
|
||||
private _data: Data | null;
|
||||
private _node: Node | null;
|
||||
private _debug: Debug | null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this._marketplace = null;
|
||||
this.disk = new Disk(url);
|
||||
this._data = null;
|
||||
this._node = null;
|
||||
this._debug = null;
|
||||
}
|
||||
|
||||
async marketplace() {
|
||||
@ -24,6 +34,42 @@ export class Codex {
|
||||
|
||||
this._marketplace = new module.Marketplace(this.url);
|
||||
|
||||
return module.Marketplace;
|
||||
return this._marketplace;
|
||||
}
|
||||
|
||||
async data() {
|
||||
if (this._data) {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
const module = await import("./data/data");
|
||||
|
||||
this._data = new module.Data(this.url);
|
||||
|
||||
return this._data;
|
||||
}
|
||||
|
||||
async node() {
|
||||
if (this._node) {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
const module = await import("./node/node");
|
||||
|
||||
this._node = new module.Node(this.url);
|
||||
|
||||
return this._node;
|
||||
}
|
||||
|
||||
async debug() {
|
||||
if (this._debug) {
|
||||
return this._debug;
|
||||
}
|
||||
|
||||
const module = await import("./debug/debug");
|
||||
|
||||
this._debug = new module.Debug(this.url);
|
||||
|
||||
return this._debug;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,34 +5,34 @@ import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import { Marketplace } from "./marketplace";
|
||||
|
||||
// function createSlot() {
|
||||
// return {
|
||||
// "id": faker.string.alphanumeric(64),
|
||||
// "request": {
|
||||
// return {
|
||||
// "id": faker.string.alphanumeric(64),
|
||||
// "request": {
|
||||
|
||||
// "id": faker.string.alphanumeric(64),
|
||||
// "client": faker.finance.ethereumAddress(),
|
||||
// "ask":
|
||||
// {
|
||||
// "slots": faker.number.int({ min: 0, max: 9 }),
|
||||
// "slotSize": faker.number.float({ max: 10000 }).toString(),
|
||||
// "duration": faker.number.int({ max: 300000 }).toString(),
|
||||
// "proofProbability": faker.number.int({ max: 9 }),
|
||||
// "reward": faker.number.float({ max: 1000 }).toString(),
|
||||
// "maxSlotLoss": faker.number.int({ max: 9 })
|
||||
// },
|
||||
// "content": {
|
||||
// "cid": faker.string.alphanumeric(64),
|
||||
// "por": {
|
||||
// "u": faker.string.alphanumeric(16),
|
||||
// "publicKey": faker.string.alphanumeric(64),
|
||||
// "name": faker.string.alphanumeric(16)
|
||||
// }
|
||||
// },
|
||||
// "expiry": faker.number.int({ min: 2, max: 59 }) + " minutes",
|
||||
// "nonce": faker.string.alphanumeric(64)
|
||||
// },
|
||||
// "slotIndex": faker.number.int({ min: 0, max: 9 })
|
||||
// }
|
||||
// "id": faker.string.alphanumeric(64),
|
||||
// "client": faker.finance.ethereumAddress(),
|
||||
// "ask":
|
||||
// {
|
||||
// "slots": faker.number.int({ min: 0, max: 9 }),
|
||||
// "slotSize": faker.number.float({ max: 10000 }).toString(),
|
||||
// "duration": faker.number.int({ max: 300000 }).toString(),
|
||||
// "proofProbability": faker.number.int({ max: 9 }),
|
||||
// "reward": faker.number.float({ max: 1000 }).toString(),
|
||||
// "maxSlotLoss": faker.number.int({ max: 9 })
|
||||
// },
|
||||
// "content": {
|
||||
// "cid": faker.string.alphanumeric(64),
|
||||
// "por": {
|
||||
// "u": faker.string.alphanumeric(16),
|
||||
// "publicKey": faker.string.alphanumeric(64),
|
||||
// "name": faker.string.alphanumeric(16)
|
||||
// }
|
||||
// },
|
||||
// "expiry": faker.number.int({ min: 2, max: 59 }) + " minutes",
|
||||
// "nonce": faker.string.alphanumeric(64)
|
||||
// },
|
||||
// "slotIndex": faker.number.int({ min: 0, max: 9 })
|
||||
// }
|
||||
// }
|
||||
|
||||
function createStorageRequest() {
|
||||
@ -52,7 +52,6 @@ function missingNumberValidationError(field: string) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
type: "validation",
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
@ -70,7 +69,6 @@ function extraValidationError(field: string, value: unknown) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
type: "validation",
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
@ -88,7 +86,6 @@ function missingStringValidationError(field: string) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
type: "validation",
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
@ -106,7 +103,6 @@ function mistypeNumberValidationError(field: string, value: string) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
type: "validation",
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
@ -124,7 +120,6 @@ function minNumberValidationError(field: string, min: number) {
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
type: "validation",
|
||||
message: "Cannot validate the input",
|
||||
errors: [
|
||||
{
|
||||
@ -161,7 +156,7 @@ describe("marketplace", () => {
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("totalSize"));
|
||||
});
|
||||
|
||||
it.only("returns an error when trying to create an availability with an invalid number valid", async () => {
|
||||
it("returns an error when trying to create an availability with an invalid number valid", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
duration: 3000,
|
||||
maxCollateral: 1,
|
||||
@ -193,8 +188,6 @@ describe("marketplace", () => {
|
||||
minPrice: 100,
|
||||
} as any);
|
||||
|
||||
console.info(response.error);
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
|
||||
});
|
||||
|
||||
@ -247,7 +240,9 @@ describe("marketplace", () => {
|
||||
it("returns a response when the request succeed", async (t) => {
|
||||
const data = { ...createAvailability(), freeSize: 1000 };
|
||||
|
||||
t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data }));
|
||||
t.mock.method(Fetch, "safeJson", () =>
|
||||
Promise.resolve({ error: false, data })
|
||||
);
|
||||
|
||||
const response = await marketplace.createAvailability({
|
||||
maxCollateral: 1,
|
||||
@ -262,7 +257,9 @@ describe("marketplace", () => {
|
||||
it("returns a response when the create availability succeed", async (t) => {
|
||||
const data = { ...createAvailability(), freeSize: 1000 };
|
||||
|
||||
t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data }));
|
||||
t.mock.method(Fetch, "safeJson", () =>
|
||||
Promise.resolve({ error: false, data })
|
||||
);
|
||||
|
||||
const response = await marketplace.createAvailability({
|
||||
maxCollateral: 1,
|
||||
@ -301,7 +298,9 @@ describe("marketplace", () => {
|
||||
it("returns a response when the update availability succeed", async (t) => {
|
||||
const data = createAvailability();
|
||||
|
||||
t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data }));
|
||||
t.mock.method(Fetch, "safeJson", () =>
|
||||
Promise.resolve({ error: false, data })
|
||||
);
|
||||
|
||||
const response = await marketplace.updateAvailability({
|
||||
id: faker.string.alphanumeric(64),
|
||||
|
||||
@ -28,7 +28,7 @@ export class Marketplace {
|
||||
async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/slots";
|
||||
|
||||
return Fetch.safe<CodexSlot[]>(url, {
|
||||
return Fetch.safeJson<CodexSlot[]>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -39,7 +39,7 @@ export class Marketplace {
|
||||
async activeSlot(slotId: string): Promise<SafeValue<CodexSlot>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/slots/" + slotId;
|
||||
|
||||
return Fetch.safe<CodexSlot>(url, {
|
||||
return Fetch.safeJson<CodexSlot>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -50,7 +50,7 @@ export class Marketplace {
|
||||
async availabilities(): Promise<SafeValue<CodexAvailability[]>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/availability";
|
||||
|
||||
return Fetch.safe<CodexAvailability[]>(url, {
|
||||
return Fetch.safeJson<CodexAvailability[]>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -75,7 +75,7 @@ export class Marketplace {
|
||||
|
||||
const url = this.url + Api.config.prefix + "/sales/availability";
|
||||
|
||||
return Fetch.safe<CodexAvailabilityCreateResponse>(url, {
|
||||
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@ -106,7 +106,7 @@ export class Marketplace {
|
||||
const url =
|
||||
this.url + Api.config.prefix + "/sales/availability/" + result.output.id;
|
||||
|
||||
return Fetch.safe<CodexAvailability>(url, {
|
||||
return Fetch.safeJson<CodexAvailability>(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@ -126,7 +126,7 @@ export class Marketplace {
|
||||
Api.config.prefix +
|
||||
`/sales/availability/${availabilityId}/reservations`;
|
||||
|
||||
return Fetch.safe<CodexReservation[]>(url, {
|
||||
return Fetch.safeJson<CodexReservation[]>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -137,7 +137,7 @@ export class Marketplace {
|
||||
async purchaseIds(): Promise<SafeValue<string[]>> {
|
||||
const url = this.url + Api.config.prefix + `/storage/purchases`;
|
||||
|
||||
return Fetch.safe<string[]>(url, {
|
||||
return Fetch.safeJson<string[]>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -149,7 +149,7 @@ export class Marketplace {
|
||||
const url =
|
||||
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
|
||||
|
||||
return Fetch.safe<CodexPurchase>(url, {
|
||||
return Fetch.safeJson<CodexPurchase>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@ -175,7 +175,7 @@ export class Marketplace {
|
||||
const { cid, ...body } = result.output;
|
||||
const url = this.url + Api.config.prefix + "/storage/request/" + cid;
|
||||
|
||||
return Fetch.safe<CodexCreateStorageRequestResponse>(url, {
|
||||
return Fetch.safeJson<CodexCreateStorageRequestResponse>(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
|
||||
53
src/node/node.ts
Normal file
53
src/node/node.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Api } from "../api/config";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
|
||||
export class Node {
|
||||
readonly url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a peer
|
||||
* TODO check result
|
||||
*/
|
||||
connect(peerId: string, addrs: string[] = []) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
for (const addr of addrs) {
|
||||
params.append("addrs", addr);
|
||||
}
|
||||
|
||||
const url =
|
||||
this.url + Api.config.prefix + `/connect/${peerId}?` + addrs.toString();
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node's SPR
|
||||
* TODO check result
|
||||
*/
|
||||
spr() {
|
||||
const url = this.url + Api.config.prefix + "/spr";
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node's PeerID
|
||||
* TODO check result
|
||||
*/
|
||||
peerId() {
|
||||
const url = this.url + Api.config.prefix + "/node/peerid";
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
}
|
||||
21
src/promise-safe/promise-safe.test.ts
Normal file
21
src/promise-safe/promise-safe.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { Promises } from "./promise-safe";
|
||||
|
||||
describe("promise safe", () => {
|
||||
it("returns an error when the promise failed", async () => {
|
||||
const result = await Promises.safe(
|
||||
() => new Promise((_, reject) => reject("error")),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(result, { error: true, data: { message: "error" } });
|
||||
});
|
||||
|
||||
it("returns the value when the promise succeed", async () => {
|
||||
const result = await Promises.safe(
|
||||
() => new Promise((resolve) => resolve("ok")),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(result, { error: false, data: "ok" });
|
||||
});
|
||||
});
|
||||
21
src/promise-safe/promise-safe.ts
Normal file
21
src/promise-safe/promise-safe.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { SafeValue } from "../values/values";
|
||||
|
||||
export const Promises = {
|
||||
async safe<T>(promise: () => Promise<T>): Promise<SafeValue<T>> {
|
||||
try {
|
||||
const result = await promise();
|
||||
|
||||
return { error: false, data: result };
|
||||
} catch (e) {
|
||||
const opts = e instanceof Error && e.stack ? { stack: e.stack } : {};
|
||||
|
||||
return {
|
||||
error: true,
|
||||
data: {
|
||||
message: e instanceof Error ? e.message : "" + e,
|
||||
...opts,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -13,7 +13,6 @@
|
||||
"outDir": "./dist",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
},
|
||||
"buildOptions": {}
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
14
tsconfig.test.json
Normal file
14
tsconfig.test.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
],
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user