Skip to content

Commit

Permalink
feat(api): availabilities patch endpoint (#692)
Browse files Browse the repository at this point in the history
* feat(api): availabilities patch and reservations endpoints

* test: fixing tests and writing

* test: test reservations endpoint

* chore: feedback implementation

* chore: feedback implementation

* test: fix integration tests
  • Loading branch information
AuHau committed Mar 21, 2024
1 parent 43f0bf1 commit de1714e
Show file tree
Hide file tree
Showing 18 changed files with 603 additions and 130 deletions.
115 changes: 108 additions & 7 deletions codex/rest/api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import ../contracts
import ../manifest
import ../streams/asyncstreamwrapper
import ../stores
import ../utils/options

import ./coders
import ./json
Expand Down Expand Up @@ -253,9 +254,10 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi(
MethodPost,
"/api/codex/v1/sales/availability") do () -> RestApiResponse:
## Add available storage to sell
## Add available storage to sell.
## Every time Availability's offer finishes, its capacity is returned to the availability.
##
## size - size of available storage in bytes
## totalSize - size of available storage in bytes
## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimum price to be paid (in amount of tokens)
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens)
Expand All @@ -271,24 +273,122 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =

let reservations = contracts.sales.context.reservations

if not reservations.hasAvailable(restAv.size.truncate(uint)):
if restAv.totalSize == 0:
return RestApiResponse.error(Http400, "Total size must be larger then zero")

if not reservations.hasAvailable(restAv.totalSize.truncate(uint)):
return RestApiResponse.error(Http422, "Not enough storage quota")

without availability =? (
await reservations.createAvailability(
restAv.size,
restAv.totalSize,
restAv.duration,
restAv.minPrice,
restAv.maxCollateral)
), error:
return RestApiResponse.error(Http500, error.msg)

return RestApiResponse.response(availability.toJson,
Http201,
contentType="application/json")
except CatchableError as exc:
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500)

router.rawApi(
MethodPatch,
"/api/codex/v1/sales/availability/{id}") do (id: AvailabilityId) -> RestApiResponse:
## Updates Availability.
## The new parameters will be only considered for new requests.
## Existing Requests linked to this Availability will continue as is.
##
## totalSize - size of available storage in bytes. When decreasing the size, then lower limit is the currently `totalSize - freeSize`.
## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimum price to be paid (in amount of tokens)
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens)

try:
without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Sales unavailable")

without id =? id.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
without keyId =? id.key.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)

let
body = await request.getBody()
reservations = contracts.sales.context.reservations

type OptRestAvailability = Optionalize(RestAvailability)
without restAv =? OptRestAvailability.fromJson(body), error:
return RestApiResponse.error(Http400, error.msg)

without availability =? (await reservations.get(keyId, Availability)), error:
if error of NotExistsError:
return RestApiResponse.error(Http404, "Availability not found")

return RestApiResponse.error(Http500, error.msg)

if isSome restAv.freeSize:
return RestApiResponse.error(Http400, "Updating freeSize is not allowed")

if size =? restAv.totalSize:
# we don't allow lowering the totalSize bellow currently utilized size
if size < (availability.totalSize - availability.freeSize):
return RestApiResponse.error(Http400, "New totalSize must be larger then current totalSize - freeSize, which is currently: " & $(availability.totalSize - availability.freeSize))

availability.freeSize += size - availability.totalSize
availability.totalSize = size

if duration =? restAv.duration:
availability.duration = duration

if minPrice =? restAv.minPrice:
availability.minPrice = minPrice

if maxCollateral =? restAv.maxCollateral:
availability.maxCollateral = maxCollateral

if err =? (await reservations.update(availability)).errorOption:
return RestApiResponse.error(Http500, err.msg)

return RestApiResponse.response(Http200)
except CatchableError as exc:
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500)

router.rawApi(
MethodGet,
"/api/codex/v1/sales/availability/{id}/reservations") do (id: AvailabilityId) -> RestApiResponse:
## Gets Availability's reservations.

try:
without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Sales unavailable")

without id =? id.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
without keyId =? id.key.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)

let reservations = contracts.sales.context.reservations

if error =? (await reservations.get(keyId, Availability)).errorOption:
if error of NotExistsError:
return RestApiResponse.error(Http404, "Availability not found")
else:
return RestApiResponse.error(Http500, error.msg)

without availabilitysReservations =? (await reservations.all(Reservation, id)), err:
return RestApiResponse.error(Http500, err.msg)

# TODO: Expand this structure with information about the linked StorageRequest not only RequestID
return RestApiResponse.response(availabilitysReservations.toJson, contentType="application/json")
except CatchableError as exc:
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500)

proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi(
MethodPost,
Expand Down Expand Up @@ -329,10 +429,11 @@ proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
return RestApiResponse.error(Http500)

if expiry <= node.clock.now.u256:
return RestApiResponse.error(Http400, "Expiry needs to be in future")
return RestApiResponse.error(Http400, "Expiry needs to be in future. Now: " & $node.clock.now)

if expiry > node.clock.now.u256 + params.duration:
return RestApiResponse.error(Http400, "Expiry has to be before the request's end (now + duration)")
let expiryLimit = node.clock.now.u256 + params.duration
if expiry > expiryLimit:
return RestApiResponse.error(Http400, "Expiry has to be before the request's end (now + duration). Limit: " & $expiryLimit)

without purchaseId =? await node.requestStorage(
cid,
Expand Down
2 changes: 1 addition & 1 deletion codex/rest/coders.nim
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ proc decodeString*(_: type array[32, byte],
except ValueError as e:
err e.msg.cstring

proc decodeString*[T: PurchaseId | RequestId | Nonce | SlotId](_: type T,
proc decodeString*[T: PurchaseId | RequestId | Nonce | SlotId | AvailabilityId](_: type T,
value: string): Result[T, cstring] =
array[32, byte].decodeString(value).map(id => T(id))

Expand Down
3 changes: 2 additions & 1 deletion codex/rest/json.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ type
error* {.serialize.}: ?string

RestAvailability* = object
size* {.serialize.}: UInt256
totalSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256
minPrice* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256
freeSize* {.serialize.}: ?UInt256

RestSalesAgent* = object
state* {.serialize.}: string
Expand Down

0 comments on commit de1714e

Please sign in to comment.