mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-01-02 13:33:10 +00:00
455 lines
14 KiB
Nim
455 lines
14 KiB
Nim
import std/strutils
|
|
|
|
from pkg/libp2p import Cid, `$`, init
|
|
import pkg/stint
|
|
import pkg/questionable/results
|
|
import pkg/chronos/apps/http/[httpserver, shttpserver, httpclient, httptable]
|
|
import pkg/codex/logutils
|
|
import pkg/codex/rest/json
|
|
import pkg/codex/purchasing
|
|
import pkg/codex/errors
|
|
import pkg/codex/sales/reservations
|
|
|
|
export purchasing, httptable, httpclient
|
|
|
|
type CodexClient* = ref object
|
|
baseurl: string
|
|
session: HttpSessionRef
|
|
|
|
type HasBlockResponse = object
|
|
has: bool
|
|
|
|
proc new*(_: type CodexClient, baseurl: string): CodexClient =
|
|
CodexClient(session: HttpSessionRef.new(), baseurl: baseurl)
|
|
|
|
proc close*(self: CodexClient): Future[void] {.async: (raises: []).} =
|
|
await self.session.closeWait()
|
|
|
|
proc request(
|
|
self: CodexClient,
|
|
httpMethod: httputils.HttpMethod,
|
|
url: string,
|
|
body: openArray[char] = [],
|
|
headers: openArray[HttpHeaderTuple] = [],
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
HttpClientRequestRef
|
|
.new(
|
|
self.session,
|
|
url,
|
|
httpMethod,
|
|
version = HttpVersion11,
|
|
flags = {},
|
|
maxResponseHeadersSize = HttpMaxHeadersSize,
|
|
headers = headers,
|
|
body = body.toOpenArrayByte(0, len(body) - 1),
|
|
).get
|
|
.send()
|
|
|
|
proc post*(
|
|
self: CodexClient,
|
|
url: string,
|
|
body: string = "",
|
|
headers: seq[HttpHeaderTuple] = @[],
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return self.request(MethodPost, url, headers = headers, body = body)
|
|
|
|
proc get(
|
|
self: CodexClient, url: string, headers: seq[HttpHeaderTuple] = @[]
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return self.request(MethodGet, url, headers = headers)
|
|
|
|
proc delete(
|
|
self: CodexClient, url: string, headers: seq[HttpHeaderTuple] = @[]
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return self.request(MethodDelete, url, headers = headers)
|
|
|
|
proc patch*(
|
|
self: CodexClient,
|
|
url: string,
|
|
body: string = "",
|
|
headers: seq[HttpHeaderTuple] = @[],
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return self.request(MethodPatch, url, headers = headers, body = body)
|
|
|
|
proc body*(
|
|
response: HttpClientResponseRef
|
|
): Future[string] {.async: (raises: [CancelledError, HttpError]).} =
|
|
return bytesToString (await response.getBodyBytes())
|
|
|
|
proc getContent(
|
|
client: CodexClient, url: string, headers: seq[HttpHeaderTuple] = @[]
|
|
): Future[string] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.get(url, headers)
|
|
return await response.body
|
|
|
|
proc info*(
|
|
client: CodexClient
|
|
): Future[?!JsonNode] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.get(client.baseurl & "/debug/info")
|
|
return JsonNode.parse(await response.body)
|
|
|
|
proc setLogLevel*(
|
|
client: CodexClient, level: string
|
|
): Future[void] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let
|
|
url = client.baseurl & "/debug/chronicles/loglevel?level=" & level
|
|
headers = @[("Content-Type", "text/plain")]
|
|
response = await client.post(url, headers = headers, body = "")
|
|
assert response.status == 200
|
|
|
|
proc uploadRaw*(
|
|
client: CodexClient, contents: string, headers: seq[HttpHeaderTuple] = @[]
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return client.post(client.baseurl & "/data", body = contents, headers = headers)
|
|
|
|
proc upload*(
|
|
client: CodexClient, contents: string
|
|
): Future[?!Cid] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.uploadRaw(contents)
|
|
assert response.status == 200
|
|
Cid.init(await response.body).mapFailure
|
|
|
|
proc upload*(
|
|
client: CodexClient, bytes: seq[byte]
|
|
): Future[?!Cid] {.async: (raw: true).} =
|
|
return client.upload(string.fromBytes(bytes))
|
|
|
|
proc downloadRaw*(
|
|
client: CodexClient, cid: string, local = false
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return
|
|
client.get(client.baseurl & "/data/" & cid & (if local: "" else: "/network/stream"))
|
|
|
|
proc downloadBytes*(
|
|
client: CodexClient, cid: Cid, local = false
|
|
): Future[?!seq[byte]] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.downloadRaw($cid, local = local)
|
|
|
|
if response.status != 200:
|
|
return failure($response.status)
|
|
|
|
success await response.getBodyBytes()
|
|
|
|
proc download*(
|
|
client: CodexClient, cid: Cid, local = false
|
|
): Future[?!string] {.async: (raises: [CancelledError, HttpError]).} =
|
|
without response =? await client.downloadBytes(cid, local = local), err:
|
|
return failure(err)
|
|
return success bytesToString(response)
|
|
|
|
proc downloadNoStream*(
|
|
client: CodexClient, cid: Cid
|
|
): Future[?!string] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.post(client.baseurl & "/data/" & $cid & "/network")
|
|
|
|
if response.status != 200:
|
|
return failure($response.status)
|
|
|
|
success await response.body
|
|
|
|
proc downloadManifestOnly*(
|
|
client: CodexClient, cid: Cid
|
|
): Future[?!string] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response =
|
|
await client.get(client.baseurl & "/data/" & $cid & "/network/manifest")
|
|
|
|
if response.status != 200:
|
|
return failure($response.status)
|
|
|
|
success await response.body
|
|
|
|
proc deleteRaw*(
|
|
client: CodexClient, cid: string
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return client.delete(client.baseurl & "/data/" & cid)
|
|
|
|
proc delete*(
|
|
client: CodexClient, cid: Cid
|
|
): Future[?!void] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.deleteRaw($cid)
|
|
|
|
if response.status != 204:
|
|
return failure($response.status)
|
|
|
|
success()
|
|
|
|
proc listRaw*(
|
|
client: CodexClient
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
return client.get(client.baseurl & "/data")
|
|
|
|
proc list*(
|
|
client: CodexClient
|
|
): Future[?!RestContentList] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.listRaw()
|
|
|
|
if response.status != 200:
|
|
return failure($response.status)
|
|
|
|
RestContentList.fromJson(await response.body)
|
|
|
|
proc space*(
|
|
client: CodexClient
|
|
): Future[?!RestRepoStore] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let url = client.baseurl & "/space"
|
|
let response = await client.get(url)
|
|
|
|
if response.status != 200:
|
|
return failure($response.status)
|
|
|
|
RestRepoStore.fromJson(await response.body)
|
|
|
|
proc requestStorageRaw*(
|
|
client: CodexClient,
|
|
cid: Cid,
|
|
duration: uint64,
|
|
pricePerBytePerSecond: UInt256,
|
|
proofProbability: UInt256,
|
|
collateralPerByte: UInt256,
|
|
expiry: uint64 = 0,
|
|
nodes: uint = 3,
|
|
tolerance: uint = 1,
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
## Call request storage REST endpoint
|
|
##
|
|
let url = client.baseurl & "/storage/request/" & $cid
|
|
let json =
|
|
%*{
|
|
"duration": duration,
|
|
"pricePerBytePerSecond": pricePerBytePerSecond,
|
|
"proofProbability": proofProbability,
|
|
"collateralPerByte": collateralPerByte,
|
|
"nodes": nodes,
|
|
"tolerance": tolerance,
|
|
}
|
|
|
|
if expiry != 0:
|
|
json["expiry"] = %($expiry)
|
|
|
|
return client.post(url, $json)
|
|
|
|
proc requestStorage*(
|
|
client: CodexClient,
|
|
cid: Cid,
|
|
duration: uint64,
|
|
pricePerBytePerSecond: UInt256,
|
|
proofProbability: UInt256,
|
|
expiry: uint64,
|
|
collateralPerByte: UInt256,
|
|
nodes: uint = 3,
|
|
tolerance: uint = 1,
|
|
): Future[?!PurchaseId] {.async: (raises: [CancelledError, HttpError]).} =
|
|
## Call request storage REST endpoint
|
|
##
|
|
let
|
|
response = await client.requestStorageRaw(
|
|
cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte, expiry,
|
|
nodes, tolerance,
|
|
)
|
|
body = await response.body
|
|
|
|
if response.status != 200:
|
|
doAssert(false, body)
|
|
PurchaseId.fromHex(body).catch
|
|
|
|
proc getPurchase*(
|
|
client: CodexClient, purchaseId: PurchaseId
|
|
): Future[?!RestPurchase] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let url = client.baseurl & "/storage/purchases/" & purchaseId.toHex
|
|
try:
|
|
let body = await client.getContent(url)
|
|
return RestPurchase.fromJson(body)
|
|
except CatchableError as e:
|
|
return failure e.msg
|
|
|
|
proc getSalesAgent*(
|
|
client: CodexClient, slotId: SlotId
|
|
): Future[?!RestSalesAgent] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let url = client.baseurl & "/sales/slots/" & slotId.toHex
|
|
try:
|
|
let body = await client.getContent(url)
|
|
return RestSalesAgent.fromJson(body)
|
|
except CatchableError as e:
|
|
return failure e.msg
|
|
|
|
proc postAvailabilityRaw*(
|
|
client: CodexClient,
|
|
totalSize, duration: uint64,
|
|
minPricePerBytePerSecond, totalCollateral: UInt256,
|
|
enabled: ?bool = bool.none,
|
|
until: ?SecondsSince1970 = SecondsSince1970.none,
|
|
): Future[HttpClientResponseRef] {.async: (raises: [CancelledError, HttpError]).} =
|
|
## Post sales availability endpoint
|
|
##
|
|
let url = client.baseurl & "/sales/availability"
|
|
let json =
|
|
%*{
|
|
"totalSize": totalSize,
|
|
"duration": duration,
|
|
"minPricePerBytePerSecond": minPricePerBytePerSecond,
|
|
"totalCollateral": totalCollateral,
|
|
"enabled": enabled,
|
|
"until": until,
|
|
}
|
|
return await client.post(url, $json)
|
|
|
|
proc postAvailability*(
|
|
client: CodexClient,
|
|
totalSize, duration: uint64,
|
|
minPricePerBytePerSecond, totalCollateral: UInt256,
|
|
enabled: ?bool = bool.none,
|
|
until: ?SecondsSince1970 = SecondsSince1970.none,
|
|
): Future[?!Availability] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.postAvailabilityRaw(
|
|
totalSize = totalSize,
|
|
duration = duration,
|
|
minPricePerBytePerSecond = minPricePerBytePerSecond,
|
|
totalCollateral = totalCollateral,
|
|
enabled = enabled,
|
|
until = until,
|
|
)
|
|
|
|
let body = await response.body
|
|
|
|
doAssert response.status == 201,
|
|
"expected 201 Created, got " & $response.status & ", body: " & body
|
|
Availability.fromJson(body)
|
|
|
|
proc patchAvailabilityRaw*(
|
|
client: CodexClient,
|
|
availabilityId: AvailabilityId,
|
|
totalSize, freeSize, duration: ?uint64 = uint64.none,
|
|
minPricePerBytePerSecond, totalCollateral: ?UInt256 = UInt256.none,
|
|
enabled: ?bool = bool.none,
|
|
until: ?SecondsSince1970 = SecondsSince1970.none,
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
## Updates availability
|
|
##
|
|
let url = client.baseurl & "/sales/availability/" & $availabilityId
|
|
|
|
# TODO: Optionalize macro does not keep `serialize` pragmas so we can't use `Optionalize(RestAvailability)` here.
|
|
var json = %*{}
|
|
|
|
if totalSize =? totalSize:
|
|
json["totalSize"] = %totalSize
|
|
|
|
if freeSize =? freeSize:
|
|
json["freeSize"] = %freeSize
|
|
|
|
if duration =? duration:
|
|
json["duration"] = %duration
|
|
|
|
if minPricePerBytePerSecond =? minPricePerBytePerSecond:
|
|
json["minPricePerBytePerSecond"] = %minPricePerBytePerSecond
|
|
|
|
if totalCollateral =? totalCollateral:
|
|
json["totalCollateral"] = %totalCollateral
|
|
|
|
if enabled =? enabled:
|
|
json["enabled"] = %enabled
|
|
|
|
if until =? until:
|
|
json["until"] = %until
|
|
|
|
client.patch(url, $json)
|
|
|
|
proc patchAvailability*(
|
|
client: CodexClient,
|
|
availabilityId: AvailabilityId,
|
|
totalSize, duration: ?uint64 = uint64.none,
|
|
minPricePerBytePerSecond, totalCollateral: ?UInt256 = UInt256.none,
|
|
enabled: ?bool = bool.none,
|
|
until: ?SecondsSince1970 = SecondsSince1970.none,
|
|
): Future[void] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let response = await client.patchAvailabilityRaw(
|
|
availabilityId,
|
|
totalSize = totalSize,
|
|
duration = duration,
|
|
minPricePerBytePerSecond = minPricePerBytePerSecond,
|
|
totalCollateral = totalCollateral,
|
|
enabled = enabled,
|
|
until = until,
|
|
)
|
|
doAssert response.status == 204, "expected No Content, got " & $response.status
|
|
|
|
proc getAvailabilities*(
|
|
client: CodexClient
|
|
): Future[?!seq[Availability]] {.async: (raises: [CancelledError, HttpError]).} =
|
|
## Call sales availability REST endpoint
|
|
let url = client.baseurl & "/sales/availability"
|
|
let body = await client.getContent(url)
|
|
seq[Availability].fromJson(body)
|
|
|
|
proc getAvailabilityReservations*(
|
|
client: CodexClient, availabilityId: AvailabilityId
|
|
): Future[?!seq[Reservation]] {.async: (raises: [CancelledError, HttpError]).} =
|
|
## Retrieves Availability's Reservations
|
|
let url = client.baseurl & "/sales/availability/" & $availabilityId & "/reservations"
|
|
let body = await client.getContent(url)
|
|
seq[Reservation].fromJson(body)
|
|
|
|
proc purchaseStateIs*(
|
|
client: CodexClient, id: PurchaseId, state: string
|
|
): Future[bool] {.async: (raises: [CancelledError, HttpError]).} =
|
|
(await client.getPurchase(id)).option .? state == some state
|
|
|
|
proc saleStateIs*(
|
|
client: CodexClient, id: SlotId, state: string
|
|
): Future[bool] {.async: (raises: [CancelledError, HttpError]).} =
|
|
(await client.getSalesAgent(id)).option .? state == some state
|
|
|
|
proc requestId*(
|
|
client: CodexClient, id: PurchaseId
|
|
): Future[?RequestId] {.async: (raises: [CancelledError, HttpError]).} =
|
|
return (await client.getPurchase(id)).option .? requestId
|
|
|
|
proc buildUrl*(client: CodexClient, path: string): string =
|
|
return client.baseurl & path
|
|
|
|
proc getSlots*(
|
|
client: CodexClient
|
|
): Future[?!seq[Slot]] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let url = client.baseurl & "/sales/slots"
|
|
let body = await client.getContent(url)
|
|
seq[Slot].fromJson(body)
|
|
|
|
proc hasBlock*(
|
|
client: CodexClient, cid: Cid
|
|
): Future[?!bool] {.async: (raises: [CancelledError, HttpError]).} =
|
|
let url = client.baseurl & "/data/" & $cid & "/exists"
|
|
let body = await client.getContent(url)
|
|
let response = HasBlockResponse.fromJson(body)
|
|
if response.isErr:
|
|
return failure "Failed to parse has block response"
|
|
return response.get.has.success
|
|
|
|
proc hasBlockRaw*(
|
|
client: CodexClient, cid: string
|
|
): Future[HttpClientResponseRef] {.
|
|
async: (raw: true, raises: [CancelledError, HttpError])
|
|
.} =
|
|
let url = client.baseurl & "/data/" & cid & "/exists"
|
|
return client.get(url)
|