Add marketplace endpoints

This commit is contained in:
Arnaud 2024-08-15 12:08:15 +02:00
parent 8bd91eb41b
commit 34bf6008ce
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
17 changed files with 1245 additions and 0 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

2
.npmignore Normal file
View File

@ -0,0 +1,2 @@
src
.editorconfig

170
README.md Normal file
View File

@ -0,0 +1,170 @@
# Codex SDK
The Codex SDK provides an API for interacting with the Codex decentralized storage network.
## Import
```js
import { Codex } from "@codex/sdk-js";
```
or
```js
const { Codex } = require("@codex/sdk-js");
```
## How to use
To create a Codex instance, provide the REST API url to interact with the Codex client:
```js
const codex = new Codex("http://localhost:3000")
```
### Error handling
The SDK provides a type called `SafeValue` for error handling instead of throwing errors. It is inspired by Go's "error as value" concept.
If the value represents an error, `error` is true and `data` will contain the error.
If the value is not an error, `error` is false and `data` will contain the requested data.
The error type is a [CodexError](./src/errors/errors.ts#L15) which can be error object of 3 types:
* `error`: Object containing the error message
* `api`: Object containing the api error message and the status code
* `validation`: Object containing the error message and a field `errors` of type [ValidationError](./src/errors/errors.ts#L3) containing the error message for each fields.
Example:
```js
const slots = await codex.marketplace.activeSlots();
if (slots.error) {
// Do something to handle the error in slots.data
return
}
// Access the slots within slots.data.
```
### Marketplace
#### activeSlots()
Returns active slots.
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L86)[]>
Example:
```js
const slots = await codex.marketplace.activeSlots();
```
#### activeSlot(slotId)
Returns active slot with id {slotId} for the host.
- slotId (string, required)
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L86)[]>
Example:
```js
const slotId= "AB9........"
const slot = await codex.marketplace.activeSlot(slotId);
```
#### availabilities
Returns storage that is for sale.
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L100)>
Example:
```js
const availabilities = await codex.marketplace.availabilities();
```
#### createAvailability
Offers storage for sale.
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L133), required)
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L124)[]>
Example:
```js
const response = await codex.marketplace.createAvailability({
maxCollateral: 1,
totalSize: 3000,
minPrice: 100,
duration: 100,
});
```
#### reservations
Return list of reservations for ongoing Storage Requests that the node hosts.
- availabilityId (string, required)
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L152)[]>
Example:
```js
const reservations = await codex.marketplace.reservations("Ox...");
```
#### createStorageRequest
Creates a new Request for storage
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L182), required)
- returns Promise<[CodexCreateStorageRequestResponse](./src/marketplace/types.ts#L195)[]>
Example:
```js
const request = await codex.marketplace.createStorageRequest({
duration: 3000,
reward: 100,
proofProbability: 1,
nodes: 1,
tolerance: 0,
collateral: 100,
expiry: 3000
});
```
#### purchaseIds
Returns list of purchase IDs
- returns Promise<string[]>
Example:
```js
const ids = await codex.marketplace.purchaseIds();
```
#### purchaseDetail
Returns list of purchase IDs
- purchaseId (string, required)
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L168)[]>
Example:
```js
const purchaseId = "Ox........"
const purchase = await codex.marketplace.purchaseDetail(purchaseId);
```

64
package-lock.json generated Normal file
View File

@ -0,0 +1,64 @@
{
"name": "@codex/sdk-js",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex/sdk-js",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.32.35"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@tsconfig/strictest": "^2.0.5",
"typescript": "^5.5.4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@faker-js/faker": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.32.35",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.35.tgz",
"integrity": "sha512-Ul3YyOTU++to8cgNkttakC0dWvpERr6RYoHO2W47DLbFvrwBDJUY31B1sImH6JZSYc4Kt4PyHtoPNu+vL2r2dA=="
},
"node_modules/@tsconfig/strictest": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.5.tgz",
"integrity": "sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==",
"dev": true
},
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@codex/sdk-js",
"version": "0.0.1",
"description": "Codex SDK to interact with the Codex decentralized storage network.",
"main": "index.js",
"scripts": {
"prepack": "npm run build",
"prebuild": "rm -Rf dist/*",
"build": "tsc",
"compile": "tsc --noEmit",
"pretest": "npm run build",
"test": "node --test",
"watch": "tsc --watch"
},
"keywords": [
"Codex",
"Javascript",
"SDK",
"storage"
],
"files": [
"dist/**"
],
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"author": "Codex team",
"readme": "README.md",
"license": "MIT",
"engines": {
"node": ">=20"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@tsconfig/strictest": "^2.0.5",
"typescript": "^5.5.4"
},
"dependencies": {
"@sinclair/typebox": "^0.32.35"
}
}

5
src/api/config.ts Normal file
View File

@ -0,0 +1,5 @@
export const Api = {
config: {
prefix: "/api/codex/v1"
}
}

19
src/disk/disk.ts Normal file
View File

@ -0,0 +1,19 @@
import { 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
}
}
}
}

51
src/errors/errors.ts Normal file
View File

@ -0,0 +1,51 @@
import { ValueErrorIterator } from "@sinclair/typebox/build/cjs/errors"
type ValidationError = {
path: string
message: string
}
/**
* The CodexError which can be error object of 3 types:
* `error`: Object containing the error message
* `api`: Object containing the api error message and the status code
* `validation`: Object containing the error message and a field `errors` of type ValidationError
* containing the error message for each fields.
*/
export type CodexError = {
type: "error"
message: string
} | {
type: "api"
message: string
status: number
} | {
type: "validation"
message: string
errors: ValidationError[]
}
export const CodexValidationErrors = {
map(iterator: ValueErrorIterator) {
let error
const errors = []
while (error = iterator.First()) {
errors.push({
path: error.path,
message: error.message
})
}
return errors
}
}
// export class CodexError extends Error {
// readonly status: number | null
// constructor(message: string, status: number | null = null) {
// super(message)
// this.status = status
// }
// }

View File

@ -0,0 +1,88 @@
import assert from "assert";
import { describe, it } from "node:test";
import { Fetch } from '../fetch-safe/fetch-safe';
class MockResponse implements Response {
headers: Headers = new Headers()
ok: boolean;
redirected = false
status: number;
statusText = "";
type = "basic" as "basic"
url = ""
body = null;
bodyUsed = false;
_text: string
constructor(ok: boolean, status: number, text: string) {
this.ok = ok
this.status = status
this._text = text
}
clone(): Response {
throw new Error("Method not implemented.");
}
arrayBuffer(): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
blob(): Promise<Blob> {
throw new Error("Method not implemented.");
}
formData(): Promise<FormData> {
throw new Error("Method not implemented.");
}
json(): Promise<any> {
return Promise.resolve(JSON.parse(this._text))
}
text(): Promise<string> {
return Promise.resolve(this._text)
}
}
describe("fetch", () => {
it("returns an error when the http call failed", async (t) => {
global.fetch = t.mock.fn(() =>
Promise.resolve(new MockResponse(false, 500, "error")),
);
const result = await Fetch.safe("http://localhost:3000/some-url", { method: "GET" })
const error = {
type: "api",
message: "error",
status: 500
}
assert.deepStrictEqual(result, { error: true, data: error });
});
it("returns an error when the json parsing failed", async (t) => {
global.fetch = t.mock.fn(() =>
Promise.resolve(new MockResponse(true, 200, "")),
);
const result = await Fetch.safe("http://localhost:3000/some-url", { method: "GET" })
const error = {
type: "error",
message: "Unexpected end of JSON input"
}
assert.deepStrictEqual(result, { error: true, data: error });
});
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" }))),
);
const result = await Fetch.safe("http://localhost:3000/some-url", { method: "GET" })
assert.deepStrictEqual(result, { error: false, data: { hello: "world" } });
});
});

View File

@ -0,0 +1,34 @@
import { SafeValue } from "../values/values"
export const Fetch = {
async safe<T extends Object>(url: string, init: RequestInit): Promise<SafeValue<T>> {
const res = await fetch(url, init)
if (!res.ok) {
const message = await res.text()
return {
error: true,
data: {
type: "api",
message,
status: res.status
}
}
}
try {
const json = await res.json()
return { error: false, data: json }
} catch (e) {
return {
error: true,
data: {
type: "error",
message: e instanceof Error ? e.message : "JSON parsing error :" + e
}
}
}
}
}

18
src/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { Disk } from "./disk/disk";
import { Marketplace } from "./marketplace/marketplace";
export * from "./fetch-safe/fetch-safe";
export * from "./marketplace/types";
export class Codex {
readonly url: string
readonly marketplace: Marketplace
readonly disk: Disk
constructor(url: string) {
this.url = url
this.marketplace = new Marketplace(url)
this.disk = new Disk(url)
}
}

View File

@ -0,0 +1,347 @@
import { faker } from "@faker-js/faker";
import assert from "assert";
import { describe, it } from "node:test";
import { Fetch } from "../fetch-safe/fetch-safe";
import { Marketplace } from "./marketplace";
// function createSlot() {
// 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 })
// }
// }
function createStorageRequest() {
return {
cid: faker.string.alphanumeric(64),
duration: faker.number.int({ min: 1 }),
reward: faker.number.int(),
proofProbability: faker.number.int(),
nodes: faker.number.int(),
tolerance: faker.number.int(),
expiry: faker.number.int({ min: 1 }),
collateral: faker.number.int()
}
}
function missingNumberValidationError(field: string) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: [
{
path: "/" + field,
message: "Expected required property"
},
{
path: "/" + field,
message: "Expected number"
}
]
}
}
}
function missingStringValidationError(field: string) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: [
{
path: "/" + field,
message: "Expected required property"
},
{
path: "/" + field,
message: "Expected string"
}
]
}
}
}
function mistypeNumberValidationError(field: string) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: [
{
path: "/" + field,
message: "Expected number"
},
]
}
}
}
function minNumberValidationError(field: string, min: number) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: [
{
path: "/" + field,
message: "Expected number to be greater or equal to " + min
}
]
}
}
}
function createAvailability() {
return {
"id": faker.finance.ethereumAddress(),
"availabilityId": faker.finance.ethereumAddress(),
"size": faker.number.int({ min: 3000, max: 300000 }),
"requestId": faker.finance.ethereumAddress(),
"slotIndex": faker.number.int({ min: 0, max: 9 })
}
}
describe("marketplace", () => {
const marketplace = new Marketplace("http://localhost:3000")
it("returns an error when trying to create an availability without total size", async () => {
const response = await marketplace.createAvailability({
duration: 3000,
maxCollateral: 1,
minPrice: 100,
} as any)
assert.deepStrictEqual(response, missingNumberValidationError("totalSize"));
});
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,
minPrice: 100,
totalSize: "abc"
} as any)
assert.deepStrictEqual(response, mistypeNumberValidationError("totalSize"));
});
it("returns an error when trying to create an availability with zero total size", async () => {
const response = await marketplace.createAvailability({
duration: 3000,
maxCollateral: 1,
minPrice: 100,
totalSize: 0
})
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1))
});
it("returns an error when trying to create an availability without duration", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
maxCollateral: 1,
minPrice: 100,
} as any)
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
it("returns an error when trying to create an availability with zero duration", async () => {
const response = await marketplace.createAvailability({
duration: 0,
maxCollateral: 1,
minPrice: 100,
totalSize: 3000
})
assert.deepStrictEqual(response, minNumberValidationError("duration", 1))
});
it("returns an error when trying to create an availability without min price", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
maxCollateral: 1,
duration: 100,
} as any)
assert.deepStrictEqual(response, missingNumberValidationError("minPrice"));
});
it("returns an error when trying to create an availability without max collateral", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
minPrice: 100,
duration: 100,
} as any)
assert.deepStrictEqual(response, missingNumberValidationError("maxCollateral"));
});
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 }),
);
const response = await marketplace.createAvailability({
maxCollateral: 1,
totalSize: 3000,
minPrice: 100,
duration: 100,
hello: "world"
} as any)
assert.deepStrictEqual(response, { error: false, data });
});
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 }),
);
const response = await marketplace.createAvailability({
maxCollateral: 1,
totalSize: 3000,
minPrice: 100,
duration: 100,
})
assert.deepStrictEqual(response, { error: false, data });
});
it("returns an error when trying to update an availability without id", async () => {
const response = await marketplace.updateAvailability({
} as any)
assert.deepStrictEqual(response, missingStringValidationError("id"));
});
it("returns an error when trying to update an availability with zero total size", async () => {
const response = await marketplace.updateAvailability({
id: faker.string.alphanumeric(64),
totalSize: 0
})
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1))
});
it("returns an error when trying to update an availability with zero duration", async () => {
const response = await marketplace.updateAvailability({
id: faker.string.alphanumeric(64),
duration: 0
})
assert.deepStrictEqual(response, minNumberValidationError("duration", 1))
});
it("returns a response when the update availability succeed", async (t) => {
const data = createAvailability()
t.mock.method(Fetch, "safe", () =>
Promise.resolve({ error: false, data }),
);
const response = await marketplace.updateAvailability({
id: faker.string.alphanumeric(64),
totalSize: 3000,
})
assert.deepStrictEqual(response, { error: false, data });
});
it("returns an error when trying to create a storage request without cid", async () => {
const { cid, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingStringValidationError("cid"));
});
it("returns an error when trying to create a storage request without duration", async () => {
const { duration, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
it("returns an error when trying to create a storage request with zero duration", async () => {
const { duration, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest({ ...rest, duration: 0 })
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
});
it("returns an error when trying to create a storage request without reward", async () => {
const { reward, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingNumberValidationError("reward"));
});
it("returns an error when trying to create a storage request without proof probability", async () => {
const { proofProbability, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingNumberValidationError("proofProbability"));
});
it("returns an error when trying to create a storage request without expiry", async () => {
const { expiry, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingNumberValidationError("expiry"));
});
it("returns an error when trying to create a storage request with zero expiry", async () => {
const { expiry, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest({ ...rest, expiry: 0 })
assert.deepStrictEqual(response, minNumberValidationError("expiry", 1));
});
it("returns an error when trying to create a storage request without collateral", async () => {
const { collateral, ...rest } = createStorageRequest()
const response = await marketplace.createStorageRequest(rest as any)
assert.deepStrictEqual(response, missingNumberValidationError("collateral"));
});
});

View File

@ -0,0 +1,173 @@
import { Value } from '@sinclair/typebox/value'
import { Api } from "../api/config"
import { CodexValidationErrors } from '../errors/errors'
import { Fetch } from "../fetch-safe/fetch-safe"
import { SafeValue } from "../values/values"
import { CodexAvailability, CodexAvailabilityCreateResponse, CodexCreateAvailabilityInput, CodexCreateStorageRequestInput, CodexCreateStorageRequestResponse, CodexPurchase, CodexReservation, CodexSlot, CodexUpdateAvailabilityInput } from "./types"
export class Marketplace {
readonly url: string
constructor(url: string) {
this.url = url
}
/**
* Returns active slots
*/
async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
const url = this.url + Api.config.prefix + "/sales/slots"
return Fetch.safe<CodexSlot[]>(url, {
method: "GET"
})
}
/**
* Returns active slot with id {slotId} for the host
*/
async activeSlot(slotId: string): Promise<SafeValue<CodexSlot>> {
const url = this.url + Api.config.prefix + "/sales/slots/" + slotId
return Fetch.safe<CodexSlot>(url, {
method: "GET"
})
}
/**
* Returns storage that is for sale
*/
async availabilities(): Promise<SafeValue<CodexAvailability[]>> {
const url = this.url + Api.config.prefix + "/sales/availability"
return Fetch.safe<CodexAvailability[]>(url, {
method: "GET"
})
}
/**
* Offers storage for sale
*/
async createAvailability(input: CodexCreateAvailabilityInput)
: Promise<SafeValue<CodexAvailabilityCreateResponse>> {
const cleaned = Value.Clean(CodexCreateAvailabilityInput, input)
if (!Value.Check(CodexCreateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexCreateAvailabilityInput, cleaned)
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator)
}
}
}
const url = this.url + Api.config.prefix + "/sales/availability"
return Fetch.safe<CodexAvailabilityCreateResponse>(url, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify(cleaned)
})
}
/**
* The new parameters will be only considered for new requests.
* Existing Requests linked to this Availability will continue as is.
*/
async updateAvailability(input: CodexUpdateAvailabilityInput): Promise<SafeValue<CodexAvailability>> {
const cleaned = Value.Clean(CodexUpdateAvailabilityInput, input)
if (!Value.Check(CodexUpdateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexUpdateAvailabilityInput, cleaned)
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator)
}
}
}
const url = this.url + Api.config.prefix + "/sales/availability/" + cleaned.id
return Fetch.safe<CodexAvailability>(url, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify(cleaned)
})
}
/**
* Return's list of Reservations for ongoing Storage Requests that the node hosts.
*/
async reservations(availabilityId: string): Promise<SafeValue<CodexReservation[]>> {
const url = this.url + Api.config.prefix + `/sales/availability/${availabilityId}/reservations`
return Fetch.safe<CodexReservation[]>(url, {
method: "GET"
})
}
/**
* Returns list of purchase IDs
*/
async purchaseIds(): Promise<SafeValue<string[]>> {
const url = this.url + Api.config.prefix + `/storage/purchases`
return Fetch.safe<string[]>(url, {
method: "GET"
})
}
/**
* Returns purchase details
*/
async purchaseDetail(purchaseId: string): Promise<SafeValue<CodexPurchase>> {
const url = this.url + Api.config.prefix + `/storage/purchases/` + purchaseId
return Fetch.safe<CodexPurchase>(url, {
method: "GET"
})
}
/**
* Creates a new request for storage.
*/
async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<CodexCreateStorageRequestResponse>> {
const cleaned = Value.Clean(CodexCreateStorageRequestInput, input)
if (!Value.Check(CodexCreateStorageRequestInput, cleaned)) {
const iterator = Value.Errors(CodexCreateStorageRequestInput, cleaned)
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator)
}
}
}
const { cid, ...body } = cleaned
const url = this.url + Api.config.prefix + "/storage/request/" + cid
return Fetch.safe<CodexCreateStorageRequestResponse>(url, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify(body)
})
}
}

195
src/marketplace/types.ts Normal file
View File

@ -0,0 +1,195 @@
import { Static, Type } from "@sinclair/typebox"
export type CodexStorageRequest = {
id: string
/**
* Address of Ethereum address
*/
client: string
ask: {
/**
* Number of slots that the tequest want to have the content spread over.
*/
slots: number
/**
* Amount of storage per slot (in bytes) as decimal string.
*/
slotSize: string
/**
* The duration of the storage request in seconds.
*/
duration: string
/**
* How often storage proofs are required as decimal string (in periods).
*/
proofProbability: string
/**
* The maximum amount of tokens paid per second per slot to hosts
* the client is willing to pay.
*/
reward: 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
*/
totalSize: string
/**
* Maximum time the storage should be sold for (in seconds)
*/
duration: string
/**
* Minimum price to be paid (in amount of tokens)
*/
minPrice: string
/**
* Maximum collateral user is willing to pay per filled Slot (in amount of tokens)
*/
maxCollateral: string
}
export type CodexAvailabilityCreateResponse = CodexAvailability & {
id: string
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: string
}
export const CodexCreateAvailabilityInput = Type.Object({
totalSize: Type.Number({ minimum: 1 }),
duration: Type.Number({ minimum: 1 }),
minPrice: Type.Number(),
maxCollateral: Type.Number()
})
export type CodexCreateAvailabilityInput = Static<typeof CodexCreateAvailabilityInput>
export const CodexUpdateAvailabilityInput = Type.Object({
id: Type.String(),
totalSize: Type.Optional(Type.Number({ minimum: 1 })),
duration: Type.Optional(Type.Number({ minimum: 1 })),
minPrice: Type.Optional(Type.Number()),
maxCollateral: Type.Optional(Type.Number())
})
export type CodexUpdateAvailabilityInput = Static<typeof CodexUpdateAvailabilityInput>
export type CodexReservation = {
id: string
availabilityId: string
requestId: string
/**
* Size in bytes
*/
size: string
/**
* Slot Index as hexadecimal string
*/
slotIndex: string
}
export type CodexPurchase = {
/**
* Description of the request's state
*/
state: string
/**
* If request failed, then here is presented the error message
*/
error: string
request: CodexStorageRequest
}
export const CodexCreateStorageRequestInput = Type.Object({
cid: Type.String(),
duration: Type.Number({ minimum: 1 }),
reward: Type.Number(),
proofProbability: Type.Number(),
nodes: Type.Optional(Type.Number({ default: 1 })),
tolerance: Type.Optional(Type.Number({ default: 0 })),
expiry: Type.Number({ minimum: 1 }),
collateral: Type.Number()
})
export type CodexCreateStorageRequestInput = Static<typeof CodexCreateStorageRequestInput>
export type CodexCreateStorageRequestResponse = Omit<CodexCreateStorageRequestInput, "cid">

10
src/values/values.ts Normal file
View File

@ -0,0 +1,10 @@
import { CodexError } from "../errors/errors";
/**
* SafeValue is a type used for error handling instead of throwing errors.
* It is inspired by Go's "error as value" concept.
* If the value represents an error, `error` is true and `data` will contain the error.
* If the value is not an error, `error` is false and `data` will contain the requested data.
*/
export type SafeValue<T> = { error: false, data: T } | { error: true, data: CodexError }

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"include": [
"src"
],
"compilerOptions": {
"declaration": true,
"removeComments": false,
"lib": [
"ES2020",
"DOM",
],
"outDir": "./dist",
}
}