Add marketplace endpoints
This commit is contained in:
parent
8bd91eb41b
commit
34bf6008ce
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
|
@ -0,0 +1,2 @@
|
|||
src
|
||||
.editorconfig
|
|
@ -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);
|
||||
```
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export const Api = {
|
||||
config: {
|
||||
prefix: "/api/codex/v1"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
// }
|
|
@ -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" } });
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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">
|
|
@ -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 }
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"removeComments": false,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
],
|
||||
"outDir": "./dist",
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue