feat(api): availabilities patch endpoint (#692)

* 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
This commit is contained in:
Adam Uhlíř 2024-03-21 11:53:45 +01:00 committed by GitHub
parent 43f0bf1c1c
commit de1714ed06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 604 additions and 131 deletions

View File

@ -35,6 +35,7 @@ import ../contracts
import ../manifest import ../manifest
import ../streams/asyncstreamwrapper import ../streams/asyncstreamwrapper
import ../stores import ../stores
import ../utils/options
import ./coders import ./coders
import ./json import ./json
@ -253,9 +254,10 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi( router.rawApi(
MethodPost, MethodPost,
"/api/codex/v1/sales/availability") do () -> RestApiResponse: "/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) ## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimum price to be paid (in amount of tokens) ## 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) ## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens)
@ -271,12 +273,15 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
let reservations = contracts.sales.context.reservations 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") return RestApiResponse.error(Http422, "Not enough storage quota")
without availability =? ( without availability =? (
await reservations.createAvailability( await reservations.createAvailability(
restAv.size, restAv.totalSize,
restAv.duration, restAv.duration,
restAv.minPrice, restAv.minPrice,
restAv.maxCollateral) restAv.maxCollateral)
@ -284,11 +289,106 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
return RestApiResponse.error(Http500, error.msg) return RestApiResponse.error(Http500, error.msg)
return RestApiResponse.response(availability.toJson, return RestApiResponse.response(availability.toJson,
Http201,
contentType="application/json") contentType="application/json")
except CatchableError as exc: except CatchableError as exc:
trace "Excepting processing request", exc = exc.msg trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500) 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) = proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi( router.rawApi(
MethodPost, MethodPost,
@ -329,10 +429,11 @@ proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
return RestApiResponse.error(Http500) return RestApiResponse.error(Http500)
if expiry <= node.clock.now.u256: 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: let expiryLimit = node.clock.now.u256 + params.duration
return RestApiResponse.error(Http400, "Expiry has to be before the request's end (now + 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( without purchaseId =? await node.requestStorage(
cid, cid,

View File

@ -84,7 +84,7 @@ proc decodeString*(_: type array[32, byte],
except ValueError as e: except ValueError as e:
err e.msg.cstring 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] = value: string): Result[T, cstring] =
array[32, byte].decodeString(value).map(id => T(id)) array[32, byte].decodeString(value).map(id => T(id))

View File

@ -27,10 +27,11 @@ type
error* {.serialize.}: ?string error* {.serialize.}: ?string
RestAvailability* = object RestAvailability* = object
size* {.serialize.}: UInt256 totalSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
minPrice* {.serialize.}: UInt256 minPrice* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256 maxCollateral* {.serialize.}: UInt256
freeSize* {.serialize.}: ?UInt256
RestSalesAgent* = object RestSalesAgent* = object
state* {.serialize.}: string state* {.serialize.}: string

View File

@ -9,24 +9,27 @@
## ##
## +--------------------------------------+ ## +--------------------------------------+
## | RESERVATION | ## | RESERVATION |
## +--------------------------------------+ |--------------------------------------| ## +----------------------------------------+ |--------------------------------------|
## | AVAILABILITY | | ReservationId | id | PK | ## | AVAILABILITY | | ReservationId | id | PK |
## |--------------------------------------| |--------------------------------------| ## |----------------------------------------| |--------------------------------------|
## | AvailabilityId | id | PK |<-||-------o<-| AvailabilityId | availabilityId | FK | ## | AvailabilityId | id | PK |<-||-------o<-| AvailabilityId | availabilityId | FK |
## |--------------------------------------| |--------------------------------------| ## |----------------------------------------| |--------------------------------------|
## | UInt256 | size | | | UInt256 | size | | ## | UInt256 | totalSize | | | UInt256 | size | |
## |--------------------------------------| |--------------------------------------| ## |----------------------------------------| |--------------------------------------|
## | UInt256 | duration | | | SlotId | slotId | | ## | UInt256 | freeSize | | | SlotId | slotId | |
## |--------------------------------------| +--------------------------------------+ ## |----------------------------------------| +--------------------------------------+
## | UInt256 | minPrice | | ## | UInt256 | duration | |
## |--------------------------------------| ## |----------------------------------------|
## | UInt256 | maxCollateral | | ## | UInt256 | minPrice | |
## +--------------------------------------+ ## |----------------------------------------|
## | UInt256 | maxCollateral | |
## +----------------------------------------+
import pkg/upraises import pkg/upraises
push: {.upraises: [].} push: {.upraises: [].}
import std/typetraits import std/typetraits
import std/sequtils
import pkg/chronos import pkg/chronos
import pkg/datastore import pkg/datastore
import pkg/nimcrypto import pkg/nimcrypto
@ -35,7 +38,9 @@ import pkg/questionable/results
import pkg/stint import pkg/stint
import pkg/stew/byteutils import pkg/stew/byteutils
import ../logutils import ../logutils
import ../clock
import ../stores import ../stores
import ../market
import ../contracts/requests import ../contracts/requests
import ../utils/json import ../utils/json
@ -52,7 +57,8 @@ type
SomeStorableId = AvailabilityId | ReservationId SomeStorableId = AvailabilityId | ReservationId
Availability* = ref object Availability* = ref object
id* {.serialize.}: AvailabilityId id* {.serialize.}: AvailabilityId
size* {.serialize.}: UInt256 totalSize* {.serialize.}: UInt256
freeSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
minPrice* {.serialize.}: UInt256 minPrice* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256 maxCollateral* {.serialize.}: UInt256
@ -91,14 +97,15 @@ proc new*(T: type Reservations,
proc init*( proc init*(
_: type Availability, _: type Availability,
size: UInt256, totalSize: UInt256,
freeSize: UInt256,
duration: UInt256, duration: UInt256,
minPrice: UInt256, minPrice: UInt256,
maxCollateral: UInt256): Availability = maxCollateral: UInt256): Availability =
var id: array[32, byte] var id: array[32, byte]
doAssert randomBytes(id) == 32 doAssert randomBytes(id) == 32
Availability(id: AvailabilityId(id), size: size, duration: duration, minPrice: minPrice, maxCollateral: maxCollateral) Availability(id: AvailabilityId(id), totalSize:totalSize, freeSize: freeSize, duration: duration, minPrice: minPrice, maxCollateral: maxCollateral)
proc init*( proc init*(
_: type Reservation, _: type Reservation,
@ -118,17 +125,9 @@ func toArray(id: SomeStorableId): array[32, byte] =
proc `==`*(x, y: AvailabilityId): bool {.borrow.} proc `==`*(x, y: AvailabilityId): bool {.borrow.}
proc `==`*(x, y: ReservationId): bool {.borrow.} proc `==`*(x, y: ReservationId): bool {.borrow.}
proc `==`*(x, y: Reservation): bool = proc `==`*(x, y: Reservation): bool =
x.id == y.id and x.id == y.id
x.availabilityId == y.availabilityId and
x.size == y.size and
x.requestId == y.requestId and
x.slotIndex == y.slotIndex
proc `==`*(x, y: Availability): bool = proc `==`*(x, y: Availability): bool =
x.id == y.id and x.id == y.id
x.size == y.size and
x.duration == y.duration and
x.maxCollateral == y.maxCollateral and
x.minPrice == y.minPrice
proc `$`*(id: SomeStorableId): string = id.toArray.toHex proc `$`*(id: SomeStorableId): string = id.toArray.toHex
@ -198,11 +197,11 @@ proc get*(
return success obj return success obj
proc update( proc updateImpl(
self: Reservations, self: Reservations,
obj: SomeStorableObject): Future[?!void] {.async.} = obj: SomeStorableObject): Future[?!void] {.async.} =
trace "updating " & $(obj.type), id = obj.id, size = obj.size trace "updating " & $(obj.type), id = obj.id
without key =? obj.key, error: without key =? obj.key, error:
return failure(error) return failure(error)
@ -215,6 +214,39 @@ proc update(
return success() return success()
proc update*(
self: Reservations,
obj: Reservation): Future[?!void] {.async.} =
return await self.updateImpl(obj)
proc update*(
self: Reservations,
obj: Availability): Future[?!void] {.async.} =
without key =? obj.key, error:
return failure(error)
let getResult = await self.get(key, Availability)
if getResult.isOk:
let oldAvailability = !getResult
# Sizing of the availability changed, we need to adjust the repo reservation accordingly
if oldAvailability.totalSize != obj.totalSize:
if oldAvailability.totalSize < obj.totalSize: # storage added
if reserveErr =? (await self.repo.reserve((obj.totalSize - oldAvailability.totalSize).truncate(uint))).errorOption:
return failure(reserveErr.toErr(ReserveFailedError))
elif oldAvailability.totalSize > obj.totalSize: # storage removed
if reserveErr =? (await self.repo.release((oldAvailability.totalSize - obj.totalSize).truncate(uint))).errorOption:
return failure(reserveErr.toErr(ReleaseFailedError))
else:
let err = getResult.error()
if not (err of NotExistsError):
return failure(err)
return await self.updateImpl(obj)
proc delete( proc delete(
self: Reservations, self: Reservations,
key: Key): Future[?!void] {.async.} = key: Key): Future[?!void] {.async.} =
@ -258,7 +290,7 @@ proc deleteReservation*(
without var availability =? await self.get(availabilityKey, Availability), error: without var availability =? await self.get(availabilityKey, Availability), error:
return failure(error) return failure(error)
availability.size += reservation.size availability.freeSize += reservation.size
if updateErr =? (await self.update(availability)).errorOption: if updateErr =? (await self.update(availability)).errorOption:
return failure(updateErr) return failure(updateErr)
@ -278,9 +310,9 @@ proc createAvailability*(
trace "creating availability", size, duration, minPrice, maxCollateral trace "creating availability", size, duration, minPrice, maxCollateral
let availability = Availability.init( let availability = Availability.init(
size, duration, minPrice, maxCollateral size, size, duration, minPrice, maxCollateral
) )
let bytes = availability.size.truncate(uint) let bytes = availability.freeSize.truncate(uint)
if reserveErr =? (await self.repo.reserve(bytes)).errorOption: if reserveErr =? (await self.repo.reserve(bytes)).errorOption:
return failure(reserveErr.toErr(ReserveFailedError)) return failure(reserveErr.toErr(ReserveFailedError))
@ -324,7 +356,7 @@ proc createReservation*(
without var availability =? await self.get(availabilityKey, Availability), error: without var availability =? await self.get(availabilityKey, Availability), error:
return failure(error) return failure(error)
if availability.size < slotSize: if availability.freeSize < slotSize:
let error = newException( let error = newException(
BytesOutOfBoundsError, BytesOutOfBoundsError,
"trying to reserve an amount of bytes that is greater than the total size of the Availability") "trying to reserve an amount of bytes that is greater than the total size of the Availability")
@ -333,9 +365,9 @@ proc createReservation*(
if createResErr =? (await self.update(reservation)).errorOption: if createResErr =? (await self.update(reservation)).errorOption:
return failure(createResErr) return failure(createResErr)
# reduce availability size by the slot size, which is now accounted for in # reduce availability freeSize by the slot size, which is now accounted for in
# the newly created Reservation # the newly created Reservation
availability.size -= slotSize availability.freeSize -= slotSize
# update availability with reduced size # update availability with reduced size
if updateErr =? (await self.update(availability)).errorOption: if updateErr =? (await self.update(availability)).errorOption:
@ -393,7 +425,7 @@ proc returnBytesToAvailability*(
without var availability =? await self.get(availabilityKey, Availability), error: without var availability =? await self.get(availabilityKey, Availability), error:
return failure(error) return failure(error)
availability.size += bytesToBeReturned availability.freeSize += bytesToBeReturned
# Update availability with returned size # Update availability with returned size
if updateErr =? (await self.update(availability)).errorOption: if updateErr =? (await self.update(availability)).errorOption:
@ -456,11 +488,12 @@ iterator items(self: StorableIter): Future[?seq[byte]] =
proc storables( proc storables(
self: Reservations, self: Reservations,
T: type SomeStorableObject T: type SomeStorableObject,
queryKey: Key = ReservationsKey
): Future[?!StorableIter] {.async.} = ): Future[?!StorableIter] {.async.} =
var iter = StorableIter() var iter = StorableIter()
let query = Query.init(ReservationsKey) let query = Query.init(queryKey)
when T is Availability: when T is Availability:
# should indicate key length of 4, but let the .key logic determine it # should indicate key length of 4, but let the .key logic determine it
without defaultKey =? AvailabilityId.default.key, error: without defaultKey =? AvailabilityId.default.key, error:
@ -475,6 +508,7 @@ proc storables(
without results =? await self.repo.metaDs.query(query), error: without results =? await self.repo.metaDs.query(query), error:
return failure(error) return failure(error)
# /sales/reservations
proc next(): Future[?seq[byte]] {.async.} = proc next(): Future[?seq[byte]] {.async.} =
await idleAsync() await idleAsync()
iter.finished = results.finished iter.finished = results.finished
@ -491,14 +525,15 @@ proc storables(
iter.next = next iter.next = next
return success iter return success iter
proc all*( proc allImpl(
self: Reservations, self: Reservations,
T: type SomeStorableObject T: type SomeStorableObject,
queryKey: Key = ReservationsKey
): Future[?!seq[T]] {.async.} = ): Future[?!seq[T]] {.async.} =
var ret: seq[T] = @[] var ret: seq[T] = @[]
without storables =? (await self.storables(T)), error: without storables =? (await self.storables(T, queryKey)), error:
return failure(error) return failure(error)
for storable in storables.items: for storable in storables.items:
@ -515,6 +550,22 @@ proc all*(
return success(ret) return success(ret)
proc all*(
self: Reservations,
T: type SomeStorableObject
): Future[?!seq[T]] {.async.} =
return await self.allImpl(T)
proc all*(
self: Reservations,
T: type SomeStorableObject,
availabilityId: AvailabilityId
): Future[?!seq[T]] {.async.} =
without key =? (ReservationsKey / $availabilityId):
return failure("no key")
return await self.allImpl(T, key)
proc findAvailability*( proc findAvailability*(
self: Reservations, self: Reservations,
size, duration, minPrice, collateral: UInt256 size, duration, minPrice, collateral: UInt256
@ -528,13 +579,13 @@ proc findAvailability*(
if bytes =? (await item) and if bytes =? (await item) and
availability =? Availability.fromJson(bytes): availability =? Availability.fromJson(bytes):
if size <= availability.size and if size <= availability.freeSize and
duration <= availability.duration and duration <= availability.duration and
collateral <= availability.maxCollateral and collateral <= availability.maxCollateral and
minPrice >= availability.minPrice: minPrice >= availability.minPrice:
trace "availability matched", trace "availability matched",
size, availsize = availability.size, size, availFreeSize = availability.freeSize,
duration, availDuration = availability.duration, duration, availDuration = availability.duration,
minPrice, availMinPrice = availability.minPrice, minPrice, availMinPrice = availability.minPrice,
collateral, availMaxCollateral = availability.maxCollateral collateral, availMaxCollateral = availability.maxCollateral
@ -542,7 +593,7 @@ proc findAvailability*(
return some availability return some availability
trace "availability did not match", trace "availability did not match",
size, availsize = availability.size, size, availFreeSize = availability.freeSize,
duration, availDuration = availability.duration, duration, availDuration = availability.duration,
minPrice, availMinPrice = availability.minPrice, minPrice, availMinPrice = availability.minPrice,
collateral, availMaxCollateral = availability.maxCollateral collateral, availMaxCollateral = availability.maxCollateral

View File

@ -1,16 +0,0 @@
import pkg/questionable
import pkg/questionable/operators
export questionable
proc `as`*[T](value: T, U: type): ?U =
## Casts a value to another type, returns an Option.
## When the cast succeeds, the option will contain the casted value.
## When the cast fails, the option will have no value.
when value is U:
return some value
elif value is ref object:
if value of U:
return some U(value)
Option.liftBinary `as`

61
codex/utils/options.nim Normal file
View File

@ -0,0 +1,61 @@
import macros
import strutils
import pkg/questionable
import pkg/questionable/operators
export questionable
proc `as`*[T](value: T, U: type): ?U =
## Casts a value to another type, returns an Option.
## When the cast succeeds, the option will contain the casted value.
## When the cast fails, the option will have no value.
when value is U:
return some value
elif value is ref object:
if value of U:
return some U(value)
Option.liftBinary `as`
# Template that wraps type with `Option[]` only if it is already not `Option` type
template WrapOption*(input: untyped): type =
when input is Option:
input
else:
Option[input]
macro createType(t: typedesc): untyped =
var objectType = getType(t)
# Work around for https://github.com/nim-lang/Nim/issues/23112
while objectType.kind == nnkBracketExpr and objectType[0].eqIdent"typeDesc":
objectType = getType(objectType[1])
expectKind(objectType, NimNodeKind.nnkObjectTy)
var fields = nnkRecList.newTree()
# Generates the list of fields that are wrapped in `Option[T]`.
# Technically wrapped with `WrapOption` which is template used to prevent
# re-wrapping already filed which is `Option[T]`.
for field in objectType[2]:
let fieldType = getTypeInst(field)
let newFieldNode =
nnkIdentDefs.newTree(ident($field), nnkCall.newTree(ident("WrapOption"), fieldType), newEmptyNode())
fields.add(newFieldNode)
# Creates new object type T with the fields lists from steps above.
let tSym = genSym(nskType, "T")
nnkStmtList.newTree(
nnkTypeSection.newTree(
nnkTypeDef.newTree(tSym, newEmptyNode(), nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), fields))
),
tSym
)
template Optionalize*(t: typed): untyped =
## Takes object type and wraps all the first level fields into
## Option type unless it is already Option type.
createType(t)

View File

@ -20,6 +20,15 @@ components:
description: Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/ description: Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/
example: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N example: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
Id:
type: string
description: 32bits identifier encoded in hex-decimal string.
example: 0x...
BigInt:
type: string
description: Integer represented as decimal string
Cid: Cid:
type: string type: string
description: Content Identifier as specified at https://github.com/multiformats/cid description: Content Identifier as specified at https://github.com/multiformats/cid
@ -102,16 +111,12 @@ components:
SalesAvailability: SalesAvailability:
type: object type: object
required:
- size
- minPrice
properties: properties:
id: id:
$ref: "#/components/schemas/Id"
totalSize:
type: string type: string
description: Hexadecimal identifier of the availability description: Total size of availability's storage in bytes as decimal string
size:
type: string
description: Size of available storage in bytes as decimal string
duration: duration:
$ref: "#/components/schemas/Duration" $ref: "#/components/schemas/Duration"
minPrice: minPrice:
@ -121,6 +126,24 @@ components:
type: string type: string
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens) as decimal string description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens) as decimal string
SalesAvailabilityREAD:
allOf:
- $ref: "#/components/schemas/SalesAvailability"
- type: object
properties:
freeSize:
type: string
description: Unused size of availability's storage in bytes as decimal string
SalesAvailabilityCREATE:
allOf:
- $ref: "#/components/schemas/SalesAvailability"
- required:
- totalSize
- minPrice
- maxCollateral
- duration
Slot: Slot:
type: object type: object
properties: properties:
@ -132,6 +155,21 @@ components:
type: string type: string
description: Slot Index as hexadecimal string description: Slot Index as hexadecimal string
Reservation:
type: object
properties:
id:
$ref: "#/components/schemas/Id"
availabilityId:
$ref: "#/components/schemas/Id"
size:
$ref: "#/components/schemas/BigInt"
requestId:
$ref: "#/components/schemas/Id"
slotIndex:
type: string
description: Slot Index as hexadecimal string
StorageRequestCreation: StorageRequestCreation:
type: object type: object
required: required:
@ -493,16 +531,84 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/SalesAvailability" $ref: "#/components/schemas/SalesAvailabilityCREATE"
responses: responses:
"200": "201":
description: Created storage availability description: Created storage availability
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/SalesAvailability" $ref: "#/components/schemas/SalesAvailabilityREAD"
"400":
description: Invalid data input
"422":
description: Not enough node's storage quota available
"500": "500":
description: Error reserving availablility description: Error reserving availability
"503":
description: Sales are unavailable
"/sales/availability/{id}":
patch:
summary: "Updates availability"
description: |
The new parameters will be only considered for new requests.
Existing Requests linked to this Availability will continue as is.
operationId: updateOfferedStorage
tags: [ Marketplace ]
parameters:
- in: path
name: id
required: true
schema:
type: string
description: ID of Availability
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/SalesAvailability"
responses:
"204":
description: Availability successfully updated
"400":
description: Invalid data input
"404":
description: Availability not found
"422":
description: Not enough node's storage quota available
"500":
description: Error reserving availability
"503":
description: Sales are unavailable
"/sales/availability/{id}/reservations":
patch:
summary: "Get availability's reservations"
description: Return's list of Reservations for ongoing Storage Requests that the node hosts.
operationId: getReservations
tags: [ Marketplace ]
parameters:
- in: path
name: id
required: true
schema:
type: string
description: ID of Availability
responses:
"200":
description: Retrieved storage availabilities of the node
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Reservation"
"400":
description: Invalid Availability ID
"404":
description: Availability not found
"500":
description: Error getting reservations
"503": "503":
description: Sales are unavailable description: Sales are unavailable

View File

@ -60,7 +60,8 @@ proc example*(_: type MultiHash, mcodec = Sha256HashCodec): MultiHash =
proc example*(_: type Availability): Availability = proc example*(_: type Availability): Availability =
Availability.init( Availability.init(
size = uint16.example.u256, totalSize = uint16.example.u256,
freeSize = uint16.example.u256,
duration = uint16.example.u256, duration = uint16.example.u256,
minPrice = uint64.example.u256, minPrice = uint64.example.u256,
maxCollateral = uint16.example.u256 maxCollateral = uint16.example.u256

View File

@ -29,9 +29,9 @@ asyncchecksuite "Reservations module":
proc createAvailability(): Availability = proc createAvailability(): Availability =
let example = Availability.example let example = Availability.example
let size = rand(100000..200000) let totalSize = rand(100000..200000)
let availability = waitFor reservations.createAvailability( let availability = waitFor reservations.createAvailability(
size.u256, totalSize.u256,
example.duration, example.duration,
example.minPrice, example.minPrice,
example.maxCollateral example.maxCollateral
@ -39,7 +39,7 @@ asyncchecksuite "Reservations module":
return availability.get return availability.get
proc createReservation(availability: Availability): Reservation = proc createReservation(availability: Availability): Reservation =
let size = rand(1..<availability.size.truncate(int)) let size = rand(1..<availability.freeSize.truncate(int))
let reservation = waitFor reservations.createReservation( let reservation = waitFor reservations.createReservation(
availability.id, availability.id,
size.u256, size.u256,
@ -57,8 +57,8 @@ asyncchecksuite "Reservations module":
check (await reservations.all(Availability)).get.len == 0 check (await reservations.all(Availability)).get.len == 0
test "generates unique ids for storage availability": test "generates unique ids for storage availability":
let availability1 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256) let availability1 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256, 5.u256)
let availability2 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256) let availability2 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256, 5.u256)
check availability1.id != availability2.id check availability1.id != availability2.id
test "can reserve available storage": test "can reserve available storage":
@ -68,7 +68,7 @@ asyncchecksuite "Reservations module":
test "creating availability reserves bytes in repo": test "creating availability reserves bytes in repo":
let orig = repo.available let orig = repo.available
let availability = createAvailability() let availability = createAvailability()
check repo.available == (orig.u256 - availability.size).truncate(uint) check repo.available == (orig.u256 - availability.freeSize).truncate(uint)
test "can get all availabilities": test "can get all availabilities":
let availability1 = createAvailability() let availability1 = createAvailability()
@ -106,6 +106,19 @@ asyncchecksuite "Reservations module":
reservations.contains(reservation1) reservations.contains(reservation1)
reservations.contains(reservation2) reservations.contains(reservation2)
test "can get reservations of specific availability":
let availability1 = createAvailability()
let availability2 = createAvailability()
let reservation1 = createReservation(availability1)
let reservation2 = createReservation(availability2)
let reservations = !(await reservations.all(Reservation, availability1.id))
check:
# perform unordered checks
reservations.len == 1
reservations.contains(reservation1)
not reservations.contains(reservation2)
test "cannot create reservation with non-existant availability": test "cannot create reservation with non-existant availability":
let availability = Availability.example let availability = Availability.example
let created = await reservations.createReservation( let created = await reservations.createReservation(
@ -121,7 +134,7 @@ asyncchecksuite "Reservations module":
let availability = createAvailability() let availability = createAvailability()
let created = await reservations.createReservation( let created = await reservations.createReservation(
availability.id, availability.id,
availability.size + 1, availability.totalSize + 1,
RequestId.example, RequestId.example,
UInt256.example UInt256.example
) )
@ -130,11 +143,11 @@ asyncchecksuite "Reservations module":
test "creating reservation reduces availability size": test "creating reservation reduces availability size":
let availability = createAvailability() let availability = createAvailability()
let orig = availability.size let orig = availability.freeSize
let reservation = createReservation(availability) let reservation = createReservation(availability)
let key = availability.id.key.get let key = availability.id.key.get
let updated = (await reservations.get(key, Availability)).get let updated = (await reservations.get(key, Availability)).get
check updated.size == orig - reservation.size check updated.freeSize == orig - reservation.size
test "can check if reservation exists": test "can check if reservation exists":
let availability = createAvailability() let availability = createAvailability()
@ -166,19 +179,19 @@ asyncchecksuite "Reservations module":
test "deleting reservation returns bytes back to availability": test "deleting reservation returns bytes back to availability":
let availability = createAvailability() let availability = createAvailability()
let orig = availability.size let orig = availability.freeSize
let reservation = createReservation(availability) let reservation = createReservation(availability)
discard await reservations.deleteReservation( discard await reservations.deleteReservation(
reservation.id, reservation.availabilityId reservation.id, reservation.availabilityId
) )
let key = availability.key.get let key = availability.key.get
let updated = !(await reservations.get(key, Availability)) let updated = !(await reservations.get(key, Availability))
check updated.size == orig check updated.freeSize == orig
test "calling returnBytesToAvailability returns bytes back to availability": test "calling returnBytesToAvailability returns bytes back to availability":
let availability = createAvailability() let availability = createAvailability()
let reservation = createReservation(availability) let reservation = createReservation(availability)
let orig = availability.size - reservation.size let orig = availability.freeSize - reservation.size
let origQuota = repo.quotaReservedBytes let origQuota = repo.quotaReservedBytes
let returnedBytes = reservation.size + 200.u256 let returnedBytes = reservation.size + 200.u256
@ -189,10 +202,28 @@ asyncchecksuite "Reservations module":
let key = availability.key.get let key = availability.key.get
let updated = !(await reservations.get(key, Availability)) let updated = !(await reservations.get(key, Availability))
check updated.size > orig check updated.freeSize > orig
check (updated.size - orig) == 200.u256 check (updated.freeSize - orig) == 200.u256
check (repo.quotaReservedBytes - origQuota) == 200 check (repo.quotaReservedBytes - origQuota) == 200
test "update releases quota when lowering size":
let
availability = createAvailability()
origQuota = repo.quotaReservedBytes
availability.totalSize = availability.totalSize - 100
check isOk await reservations.update(availability)
check (origQuota - repo.quotaReservedBytes) == 100
test "update reserves quota when growing size":
let
availability = createAvailability()
origQuota = repo.quotaReservedBytes
availability.totalSize = availability.totalSize + 100
check isOk await reservations.update(availability)
check (repo.quotaReservedBytes - origQuota) == 100
test "reservation can be partially released": test "reservation can be partially released":
let availability = createAvailability() let availability = createAvailability()
let reservation = createReservation(availability) let reservation = createReservation(availability)
@ -240,7 +271,7 @@ asyncchecksuite "Reservations module":
let availability = createAvailability() let availability = createAvailability()
let found = await reservations.findAvailability( let found = await reservations.findAvailability(
availability.size, availability.freeSize,
availability.duration, availability.duration,
availability.minPrice, availability.minPrice,
availability.maxCollateral) availability.maxCollateral)
@ -252,7 +283,7 @@ asyncchecksuite "Reservations module":
let availability = createAvailability() let availability = createAvailability()
let found = await reservations.findAvailability( let found = await reservations.findAvailability(
availability.size + 1, availability.freeSize + 1,
availability.duration, availability.duration,
availability.minPrice, availability.minPrice,
availability.maxCollateral) availability.maxCollateral)
@ -262,7 +293,7 @@ asyncchecksuite "Reservations module":
test "non-existant availability cannot be found": test "non-existant availability cannot be found":
let availability = Availability.example let availability = Availability.example
let found = (await reservations.findAvailability( let found = (await reservations.findAvailability(
availability.size, availability.freeSize,
availability.duration, availability.duration,
availability.minPrice, availability.minPrice,
availability.maxCollateral availability.maxCollateral

View File

@ -127,7 +127,8 @@ asyncchecksuite "Sales":
setup: setup:
availability = Availability( availability = Availability(
size: 100.u256, totalSize: 100.u256,
freeSize: 100.u256,
duration: 60.u256, duration: 60.u256,
minPrice: 600.u256, minPrice: 600.u256,
maxCollateral: 400.u256 maxCollateral: 400.u256
@ -189,7 +190,7 @@ asyncchecksuite "Sales":
proc createAvailability() = proc createAvailability() =
let a = waitFor reservations.createAvailability( let a = waitFor reservations.createAvailability(
availability.size, availability.totalSize,
availability.duration, availability.duration,
availability.minPrice, availability.minPrice,
availability.maxCollateral availability.maxCollateral
@ -224,7 +225,7 @@ asyncchecksuite "Sales":
proc wasIgnored(): bool = proc wasIgnored(): bool =
let run = proc(): Future[bool] {.async.} = let run = proc(): Future[bool] {.async.} =
always ( always (
getAvailability().size == availability.size and getAvailability().freeSize == availability.freeSize and
(waitFor reservations.all(Reservation)).get.len == 0 (waitFor reservations.all(Reservation)).get.len == 0
) )
waitFor run() waitFor run()
@ -300,7 +301,7 @@ asyncchecksuite "Sales":
createAvailability() createAvailability()
await market.requestStorage(request) await market.requestStorage(request)
check eventually getAvailability().size == availability.size - request.ask.slotSize check eventually getAvailability().freeSize == availability.freeSize - request.ask.slotSize
test "non-downloaded bytes are returned to availability once finished": test "non-downloaded bytes are returned to availability once finished":
var slotIndex = 0.u256 var slotIndex = 0.u256
@ -316,7 +317,7 @@ asyncchecksuite "Sales":
sold.complete() sold.complete()
createAvailability() createAvailability()
let origSize = availability.size let origSize = availability.freeSize
await market.requestStorage(request) await market.requestStorage(request)
await allowRequestToStart() await allowRequestToStart()
await sold await sold
@ -325,7 +326,7 @@ asyncchecksuite "Sales":
market.slotState[request.slotId(slotIndex)] = SlotState.Finished market.slotState[request.slotId(slotIndex)] = SlotState.Finished
clock.advance(request.ask.duration.truncate(int64)) clock.advance(request.ask.duration.truncate(int64))
check eventually getAvailability().size == origSize - 1 check eventually getAvailability().freeSize == origSize - 1
test "ignores download when duration not long enough": test "ignores download when duration not long enough":
availability.duration = request.ask.duration - 1 availability.duration = request.ask.duration - 1
@ -334,7 +335,7 @@ asyncchecksuite "Sales":
check wasIgnored() check wasIgnored()
test "ignores request when slot size is too small": test "ignores request when slot size is too small":
availability.size = request.ask.slotSize - 1 availability.totalSize = request.ask.slotSize - 1
createAvailability() createAvailability()
await market.requestStorage(request) await market.requestStorage(request)
check wasIgnored() check wasIgnored()
@ -399,7 +400,7 @@ asyncchecksuite "Sales":
return failure(error) return failure(error)
createAvailability() createAvailability()
await market.requestStorage(request) await market.requestStorage(request)
check getAvailability().size == availability.size check getAvailability().freeSize == availability.freeSize
test "generates proof of storage": test "generates proof of storage":
var provingRequest: StorageRequest var provingRequest: StorageRequest
@ -472,7 +473,7 @@ asyncchecksuite "Sales":
check eventually (await reservations.all(Availability)).get == @[availability] check eventually (await reservations.all(Availability)).get == @[availability]
test "makes storage available again when request expires": test "makes storage available again when request expires":
let origSize = availability.size let origSize = availability.freeSize
sales.onStore = proc(request: StorageRequest, sales.onStore = proc(request: StorageRequest,
slot: UInt256, slot: UInt256,
onBatch: BatchProc): Future[?!void] {.async.} = onBatch: BatchProc): Future[?!void] {.async.} =
@ -487,10 +488,10 @@ asyncchecksuite "Sales":
market.requestState[request.id]=RequestState.Cancelled market.requestState[request.id]=RequestState.Cancelled
clock.set(request.expiry.truncate(int64)+1) clock.set(request.expiry.truncate(int64)+1)
check eventually (await reservations.all(Availability)).get == @[availability] check eventually (await reservations.all(Availability)).get == @[availability]
check getAvailability().size == origSize check getAvailability().freeSize == origSize
test "verifies that request is indeed expired from onchain before firing onCancelled": test "verifies that request is indeed expired from onchain before firing onCancelled":
let origSize = availability.size let origSize = availability.freeSize
sales.onStore = proc(request: StorageRequest, sales.onStore = proc(request: StorageRequest,
slot: UInt256, slot: UInt256,
onBatch: BatchProc): Future[?!void] {.async.} = onBatch: BatchProc): Future[?!void] {.async.} =
@ -504,10 +505,10 @@ asyncchecksuite "Sales":
# would otherwise not set the timeout early enough as it uses `clock.now` in the deadline calculation. # would otherwise not set the timeout early enough as it uses `clock.now` in the deadline calculation.
await sleepAsync(chronos.milliseconds(100)) await sleepAsync(chronos.milliseconds(100))
clock.set(request.expiry.truncate(int64)+1) clock.set(request.expiry.truncate(int64)+1)
check getAvailability().size == 0 check getAvailability().freeSize == 0
market.requestState[request.id]=RequestState.Cancelled # Now "on-chain" is also expired market.requestState[request.id]=RequestState.Cancelled # Now "on-chain" is also expired
check eventually getAvailability().size == origSize check eventually getAvailability().freeSize == origSize
test "loads active slots from market": test "loads active slots from market":
let me = await market.getSigner() let me = await market.getSigner()
@ -556,4 +557,4 @@ asyncchecksuite "Sales":
check (await reservations.all(Reservation)).get.len == 1 check (await reservations.all(Reservation)).get.len == 1
await sales.load() await sales.load()
check (await reservations.all(Reservation)).get.len == 0 check (await reservations.all(Reservation)).get.len == 0
check getAvailability().size == availability.size # was restored check getAvailability().freeSize == availability.freeSize # was restored

View File

@ -1,4 +1,4 @@
import ./utils/testoptionalcast import ./utils/testoptions
import ./utils/testkeyutils import ./utils/testkeyutils
import ./utils/testasyncstatemachine import ./utils/testasyncstatemachine
import ./utils/testtimer import ./utils/testtimer

View File

@ -1,5 +1,5 @@
import std/unittest import std/unittest
import codex/utils/optionalcast import codex/utils/options
import ../helpers import ../helpers
checksuite "optional casts": checksuite "optional casts":
@ -28,3 +28,22 @@ checksuite "optional casts":
check 42.some as int == some 42 check 42.some as int == some 42
check 42.some as string == string.none check 42.some as string == string.none
check int.none as int == int.none check int.none as int == int.none
checksuite "Optionalize":
test "does not except non-object types":
static:
doAssert not compiles(Optionalize(int))
test "converts object fields to option":
type BaseType = object
a: int
b: bool
c: string
d: Option[string]
type OptionalizedType = Optionalize(BaseType)
check OptionalizedType.a is Option[int]
check OptionalizedType.b is Option[bool]
check OptionalizedType.c is Option[string]
check OptionalizedType.d is Option[string]

View File

@ -133,27 +133,70 @@ proc getSlots*(client: CodexClient): ?!seq[Slot] =
proc postAvailability*( proc postAvailability*(
client: CodexClient, client: CodexClient,
size, duration, minPrice, maxCollateral: UInt256 totalSize, duration, minPrice, maxCollateral: UInt256
): ?!Availability = ): ?!Availability =
## Post sales availability endpoint ## Post sales availability endpoint
## ##
let url = client.baseurl & "/sales/availability" let url = client.baseurl & "/sales/availability"
let json = %*{ let json = %*{
"size": size, "totalSize": totalSize,
"duration": duration, "duration": duration,
"minPrice": minPrice, "minPrice": minPrice,
"maxCollateral": maxCollateral, "maxCollateral": maxCollateral,
} }
let response = client.http.post(url, $json) let response = client.http.post(url, $json)
doAssert response.status == "200 OK", "expected 200 OK, got " & response.status & ", body: " & response.body doAssert response.status == "201 Created", "expected 201 Created, got " & response.status & ", body: " & response.body
Availability.fromJson(response.body) Availability.fromJson(response.body)
proc patchAvailabilityRaw*(
client: CodexClient,
availabilityId: AvailabilityId,
totalSize, freeSize, duration, minPrice, maxCollateral: ?UInt256 = UInt256.none
): Response =
## 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 minPrice =? minPrice:
json["minPrice"] = %minPrice
if maxCollateral =? maxCollateral:
json["maxCollateral"] = %maxCollateral
client.http.patch(url, $json)
proc patchAvailability*(
client: CodexClient,
availabilityId: AvailabilityId,
totalSize, duration, minPrice, maxCollateral: ?UInt256 = UInt256.none
): void =
let response = client.patchAvailabilityRaw(availabilityId, totalSize=totalSize, duration=duration, minPrice=minPrice, maxCollateral=maxCollateral)
doAssert response.status == "200 OK", "expected 200 OK, got " & response.status
proc getAvailabilities*(client: CodexClient): ?!seq[Availability] = proc getAvailabilities*(client: CodexClient): ?!seq[Availability] =
## Call sales availability REST endpoint ## Call sales availability REST endpoint
let url = client.baseurl & "/sales/availability" let url = client.baseurl & "/sales/availability"
let body = client.http.getContent(url) let body = client.http.getContent(url)
seq[Availability].fromJson(body) seq[Availability].fromJson(body)
proc getAvailabilityReservations*(client: CodexClient, availabilityId: AvailabilityId): ?!seq[Reservation] =
## Retrieves Availability's Reservations
let url = client.baseurl & "/sales/availability/" & $availabilityId & "/reservations"
let body = client.http.getContent(url)
seq[Reservation].fromJson(body)
proc close*(client: CodexClient) = proc close*(client: CodexClient) =
client.http.close() client.http.close()

View File

@ -53,7 +53,7 @@ template marketplacesuite*(name: string, body: untyped) =
let provider = providers()[i].client let provider = providers()[i].client
discard provider.postAvailability( discard provider.postAvailability(
size=datasetSize.u256, # should match 1 slot only totalSize=datasetSize.u256, # should match 1 slot only
duration=duration.u256, duration=duration.u256,
minPrice=300.u256, minPrice=300.u256,
maxCollateral=200.u256 maxCollateral=200.u256

View File

@ -1,5 +1,6 @@
import std/options import std/options
import std/sequtils import std/sequtils
import std/strutils
import std/httpclient import std/httpclient
from pkg/libp2p import `==` from pkg/libp2p import `==`
import pkg/chronos import pkg/chronos
@ -12,8 +13,16 @@ import ../contracts/time
import ../contracts/deployment import ../contracts/deployment
import ../codex/helpers import ../codex/helpers
import ../examples import ../examples
import ../codex/examples
import ./twonodes import ./twonodes
proc findItem[T](items: seq[T], item: T): ?!T =
for tmp in items:
if tmp == item:
return success tmp
return failure("Not found")
# For debugging you can enable logging output with debugX = true # For debugging you can enable logging output with debugX = true
# You can also pass a string in same format like for the `--log-level` parameter # You can also pass a string in same format like for the `--log-level` parameter
# to enable custom logging levels for specific topics like: debug2 = "INFO; TRACE: marketplace" # to enable custom logging levels for specific topics like: debug2 = "INFO; TRACE: marketplace"
@ -38,7 +47,7 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
test "node shows used and available space": test "node shows used and available space":
discard client1.upload("some file contents").get discard client1.upload("some file contents").get
discard client1.postAvailability(size=12.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get discard client1.postAvailability(totalSize=12.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get
let space = client1.space().tryGet() let space = client1.space().tryGet()
check: check:
space.totalBlocks == 2.uint space.totalBlocks == 2.uint
@ -94,12 +103,12 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
[cid1, cid2].allIt(it in list.mapIt(it.cid)) [cid1, cid2].allIt(it in list.mapIt(it.cid))
test "node handles new storage availability": test "node handles new storage availability":
let availability1 = client1.postAvailability(size=1.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get let availability1 = client1.postAvailability(totalSize=1.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get
let availability2 = client1.postAvailability(size=4.u256, duration=5.u256, minPrice=6.u256, maxCollateral=7.u256).get let availability2 = client1.postAvailability(totalSize=4.u256, duration=5.u256, minPrice=6.u256, maxCollateral=7.u256).get
check availability1 != availability2 check availability1 != availability2
test "node lists storage that is for sale": test "node lists storage that is for sale":
let availability = client1.postAvailability(size=1.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get let availability = client1.postAvailability(totalSize=1.u256, duration=2.u256, minPrice=3.u256, maxCollateral=4.u256).get
check availability in client1.getAvailabilities().get check availability in client1.getAvailabilities().get
test "node handles storage request": test "node handles storage request":
@ -177,7 +186,7 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
let size = 0xFFFFFF.u256 let size = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks=8) let data = await RandomChunker.example(blocks=8)
# client 2 makes storage available # client 2 makes storage available
discard client2.postAvailability(size=size, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256) let availability = client2.postAvailability(totalSize=size, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256).get
# client 1 requests storage # client 1 requests storage
let expiry = (await ethProvider.currentTime()) + 5*60 let expiry = (await ethProvider.currentTime()) + 5*60
@ -197,9 +206,13 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
check purchase.error == none string check purchase.error == none string
let availabilities = client2.getAvailabilities().get let availabilities = client2.getAvailabilities().get
check availabilities.len == 1 check availabilities.len == 1
let newSize = availabilities[0].size let newSize = availabilities[0].freeSize
check newSize > 0 and newSize < size check newSize > 0 and newSize < size
let reservations = client2.getAvailabilityReservations(availability.id).get
check reservations.len == 5
check reservations[0].requestId == purchase.requestId
test "node slots gets paid out": test "node slots gets paid out":
let size = 0xFFFFFF.u256 let size = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = 8)
@ -212,7 +225,7 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
# client 2 makes storage available # client 2 makes storage available
let startBalance = await token.balanceOf(account2) let startBalance = await token.balanceOf(account2)
discard client2.postAvailability(size=size, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256).get discard client2.postAvailability(totalSize=size, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256).get
# client 1 requests storage # client 1 requests storage
let expiry = (await ethProvider.currentTime()) + 5*60 let expiry = (await ethProvider.currentTime()) + 5*60
@ -263,8 +276,69 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
let responsePast = client1.requestStorageRaw(cid, duration=1.u256, reward=2.u256, proofProbability=3.u256, collateral=200.u256, expiry=currentTime-10) let responsePast = client1.requestStorageRaw(cid, duration=1.u256, reward=2.u256, proofProbability=3.u256, collateral=200.u256, expiry=currentTime-10)
check responsePast.status == "400 Bad Request" check responsePast.status == "400 Bad Request"
check responsePast.body == "Expiry needs to be in future" check "Expiry needs to be in future" in responsePast.body
let responseBefore = client1.requestStorageRaw(cid, duration=1.u256, reward=2.u256, proofProbability=3.u256, collateral=200.u256, expiry=currentTime+10) let responseBefore = client1.requestStorageRaw(cid, duration=1.u256, reward=2.u256, proofProbability=3.u256, collateral=200.u256, expiry=currentTime+10)
check responseBefore.status == "400 Bad Request" check responseBefore.status == "400 Bad Request"
check responseBefore.body == "Expiry has to be before the request's end (now + duration)" check "Expiry has to be before the request's end (now + duration)" in responseBefore.body
test "updating non-existing availability":
let nonExistingResponse = client1.patchAvailabilityRaw(AvailabilityId.example, duration=100.u256.some, minPrice=200.u256.some, maxCollateral=200.u256.some)
check nonExistingResponse.status == "404 Not Found"
test "updating availability":
let availability = client1.postAvailability(totalSize=140000.u256, duration=200.u256, minPrice=300.u256, maxCollateral=300.u256).get
client1.patchAvailability(availability.id, duration=100.u256.some, minPrice=200.u256.some, maxCollateral=200.u256.some)
let updatedAvailability = (client1.getAvailabilities().get).findItem(availability).get
check updatedAvailability.duration == 100
check updatedAvailability.minPrice == 200
check updatedAvailability.maxCollateral == 200
check updatedAvailability.totalSize == 140000
check updatedAvailability.freeSize == 140000
test "updating availability - freeSize is not allowed to be changed":
let availability = client1.postAvailability(totalSize=140000.u256, duration=200.u256, minPrice=300.u256, maxCollateral=300.u256).get
let freeSizeResponse = client1.patchAvailabilityRaw(availability.id, freeSize=110000.u256.some)
check freeSizeResponse.status == "400 Bad Request"
check "not allowed" in freeSizeResponse.body
test "updating availability - updating totalSize":
let availability = client1.postAvailability(totalSize=140000.u256, duration=200.u256, minPrice=300.u256, maxCollateral=300.u256).get
client1.patchAvailability(availability.id, totalSize=100000.u256.some)
let updatedAvailability = (client1.getAvailabilities().get).findItem(availability).get
check updatedAvailability.totalSize == 100000
check updatedAvailability.freeSize == 100000
test "updating availability - updating totalSize does not allow bellow utilized":
let originalSize = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks=8)
let availability = client1.postAvailability(totalSize=originalSize, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256).get
# Lets create storage request that will utilize some of the availability's space
let expiry = (await ethProvider.currentTime()) + 5*60
let cid = client2.upload(data).get
let id = client2.requestStorage(
cid,
duration=10*60.u256,
reward=400.u256,
proofProbability=3.u256,
expiry=expiry,
collateral=200.u256,
nodes = 5,
tolerance = 2).get
check eventually(client2.purchaseStateIs(id, "started"), timeout=5*60*1000)
let updatedAvailability = (client1.getAvailabilities().get).findItem(availability).get
check updatedAvailability.totalSize != updatedAvailability.freeSize
let utilizedSize = updatedAvailability.totalSize - updatedAvailability.freeSize
let totalSizeResponse = client1.patchAvailabilityRaw(availability.id, totalSize=(utilizedSize-1.u256).some)
check totalSizeResponse.status == "400 Bad Request"
check "totalSize must be larger then current totalSize" in totalSizeResponse.body
client1.patchAvailability(availability.id, totalSize=(originalSize + 20000).some)
let newUpdatedAvailability = (client1.getAvailabilities().get).findItem(availability).get
check newUpdatedAvailability.totalSize == originalSize + 20000
check newUpdatedAvailability.freeSize - updatedAvailability.freeSize == 20000

View File

@ -41,7 +41,7 @@ marketplacesuite "Marketplace payouts":
discard providerApi.postAvailability( discard providerApi.postAvailability(
# make availability size small enough that we can't fill all the slots, # make availability size small enough that we can't fill all the slots,
# thus causing a cancellation # thus causing a cancellation
size=(data.len div 2).u256, totalSize=(data.len div 2).u256,
duration=duration.u256, duration=duration.u256,
minPrice=reward, minPrice=reward,
maxCollateral=collateral) maxCollateral=collateral)

View File

@ -230,7 +230,7 @@ marketplacesuite "Simulate invalid proofs":
# let slotSize = (DefaultBlockSize * 4.NBytes).Natural.u256 # let slotSize = (DefaultBlockSize * 4.NBytes).Natural.u256
# discard provider0.client.postAvailability( # discard provider0.client.postAvailability(
# size=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPrice=300.u256,
# maxCollateral=200.u256 # maxCollateral=200.u256
@ -263,7 +263,7 @@ marketplacesuite "Simulate invalid proofs":
# # now add availability for providers 1 and 2, which should allow them to to # # now add availability for providers 1 and 2, which should allow them to to
# # put the remaining slots in their queues # # put the remaining slots in their queues
# discard provider1.client.postAvailability( # discard provider1.client.postAvailability(
# size=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPrice=300.u256,
# maxCollateral=200.u256 # maxCollateral=200.u256
@ -272,7 +272,7 @@ marketplacesuite "Simulate invalid proofs":
# check eventually filledSlotIds.len > 1 # check eventually filledSlotIds.len > 1
# discard provider2.client.postAvailability( # discard provider2.client.postAvailability(
# size=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPrice=300.u256,
# maxCollateral=200.u256 # maxCollateral=200.u256

2
vendor/nim-presto vendored

@ -1 +1 @@
Subproject commit 3984431dc0fc829eb668e12e57e90542b041d298 Subproject commit c17bfdda2c60cf5fadb043feb22e328b7659c719