Add get active slot /slots/{slotId} to REST api, use utils/json (#645)

* Add get active slot /slots/{slotId} to REST api, use utils/json

- Add endpoint /slots/{slotId} to get an active SalesAgent from the Sales module. Used in integration tests to test when a sale has reached a certain state. Those integration test changes will be included in a larger PR, coming later.
- Add OpenAPI changes for new endpoint and associated components
- Use utils/json instead of nim-json-serialization. Required exemption of imports from several packages that export nim-json-serialization by default.

* Only except `toJson` from import/export of chronicles
This commit is contained in:
Eric 2023-12-07 01:16:36 +00:00 committed by GitHub
parent 86a6ef9215
commit 3907ca4095
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 126 additions and 27 deletions

View File

@ -7,7 +7,7 @@ import ../clock
import ./interactions
export purchasing
export chronicles
export chronicles except toJson
type
ClientInteractions* = ref object of ContractInteractions

View File

@ -5,7 +5,7 @@ import ../../sales
import ./interactions
export sales
export chronicles
export chronicles except toJson
type
HostInteractions* = ref object of ContractInteractions

View File

@ -8,8 +8,8 @@ import ./requests
import ./config
export stint
export ethers
export erc20
export ethers except `%`, `%*`, toJson
export erc20 except `%`, `%*`, toJson
export config
export requests

View File

@ -75,6 +75,10 @@ proc fromHex*[T: distinct](_: type T, hex: string): T =
type baseType = T.distinctBase
T baseType.fromHex(hex)
proc toHex*[T: distinct](id: T): string =
type baseType = T.distinctBase
baseType(id).toHex
func fromTuple(_: type StorageRequest, tupl: tuple): StorageRequest =
StorageRequest(
client: tupl[0],

View File

@ -16,11 +16,10 @@ import std/sequtils
import pkg/questionable
import pkg/questionable/results
import pkg/chronicles
import pkg/chronicles except toJson
import pkg/chronos
import pkg/presto
import pkg/libp2p
import pkg/metrics
import pkg/presto except toJson
import pkg/metrics except toJson
import pkg/stew/base10
import pkg/stew/byteutils
import pkg/confutils
@ -32,8 +31,10 @@ import pkg/codexdht/discv5/spr as spr
import ../node
import ../blocktype
import ../conf
import ../contracts except `%*`, `%` # imported from contracts/marketplace (exporting ethers)
import ../streams
import ../contracts
import ../manifest
import ../streams/asyncstreamwrapper
import ../stores/blockstore
import ./coders
import ./json
@ -197,6 +198,29 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500)
router.api(
MethodGet,
"/api/codex/v1/sales/slots/{slotId}") do (slotId: SlotId) -> RestApiResponse:
## Returns active slot with id {slotId} for the host. Returns 404 if the
## slot is not active for the host.
without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Sales unavailable")
without slotId =? slotId.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
without agent =? await contracts.sales.activeSale(slotId):
return RestApiResponse.error(Http404, "Provider not filling slot")
let restAgent = RestSalesAgent(
state: agent.state() |? "none",
slotIndex: agent.data.slotIndex,
requestId: agent.data.requestId
)
return RestApiResponse.response(restAgent.toJson, contentType="application/json")
router.api(
MethodGet,
"/api/codex/v1/sales/availability") do () -> RestApiResponse:

View File

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

View File

@ -1,5 +1,4 @@
import pkg/questionable
import pkg/questionable/results
import pkg/stew/byteutils
import pkg/libp2p
import pkg/codexdht/discv5/node as dn
@ -7,7 +6,6 @@ import pkg/codexdht/discv5/routing_table as rt
import ../sales
import ../purchasing
import ../utils/json
import ../units
import ../manifest
export json
@ -34,6 +32,11 @@ type
minPrice* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256
RestSalesAgent* = object
state* {.serialize.}: string
requestId* {.serialize.}: RequestId
slotIndex* {.serialize.}: UInt256
RestContent* = object
cid* {.serialize.}: Cid
manifest* {.serialize.}: Manifest

View File

@ -39,6 +39,7 @@ import ./utils/trackedfutures
export stint
export reservations
export salesagent
logScope:
topics = "sales marketplace"
@ -208,6 +209,14 @@ proc mySlots*(sales: Sales): Future[seq[Slot]] {.async.} =
return slots
proc activeSale*(sales: Sales, slotId: SlotId): Future[?SalesAgent] {.async.} =
let market = sales.context.market
for agent in sales.agents:
if slotId(agent.data.requestId, agent.data.slotIndex) == slotId:
return some agent
return none SalesAgent
proc load*(sales: Sales) {.async.} =
let activeSlots = await sales.mySlots()

View File

@ -28,7 +28,7 @@ push: {.upraises: [].}
import std/typetraits
import pkg/chronos
import pkg/chronicles
import pkg/chronicles except toJson
import pkg/datastore
import pkg/nimcrypto
import pkg/questionable
@ -40,7 +40,7 @@ import ../contracts/requests
import ../utils/json
export requests
export chronicles
export chronicles except toJson
logScope:
topics = "sales reservations"

View File

@ -59,6 +59,11 @@ proc retrieveRequestState*(agent: SalesAgent): Future[?RequestState] {.async.} =
let market = agent.context.market
return await market.requestState(data.requestId)
func state*(agent: SalesAgent): ?string =
proc description(state: State): string =
$state
agent.query(description)
proc subscribeCancellation(agent: SalesAgent) {.async.} =
let data = agent.data
let clock = agent.context.clock

View File

@ -34,7 +34,7 @@ proc transition(_: type Event, previous, next: State): Event =
return some next
proc query*[T](machine: Machine, query: Query[T]): ?T =
if machine.state == nil:
if machine.state.isNil:
none T
else:
some query(machine.state)

View File

@ -18,7 +18,7 @@ import pkg/chronicles
import stew/io2
export io2
export chronicles
export chronicles except toJson
when defined(windows):
import stew/[windows/acl]

View File

@ -303,8 +303,7 @@ func `%`*[T: distinct](id: T): JsonNode =
type baseType = T.distinctBase
% baseType(id)
func toJson*(obj: object): string = $(%obj)
func toJson*(obj: ref object): string = $(%obj)
func toJson*[T](item: T): string = $(%item)
proc toJsnImpl(x: NimNode): NimNode =
case x.kind

View File

@ -25,6 +25,11 @@ components:
description: Content Identifier as specified at https://github.com/multiformats/cid
example: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
SlotId:
type: string
description: Keccak hash of the abi encoded tuple (RequestId, slot index)
example: 268a781e0db3f7cf36b18e5f4fdb7f586ec9edd08e5500b17c0e518a769f114a
LogLevel:
type: string
description: "One of the log levels: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL"
@ -120,8 +125,7 @@ components:
type: object
properties:
id:
type: string
description: Slot ID
$ref: "#/components/schemas/SlotId"
request:
$ref: "#/components/schemas/StorageRequest"
slotIndex:
@ -402,6 +406,35 @@ paths:
"503":
description: Sales are unavailable
"/sales/slots/{slotId}":
get:
summary: "Returns active slot with id {slotId} for the host"
tags: [ Marketplace ]
operationId: getActiveSlotById
parameters:
- in: path
name: slotId
required: true
schema:
$ref: "#/components/schemas/Cid"
description: File to be downloaded.
responses:
"200":
description: Retrieved active slot
content:
application/json:
schema:
$ref: "#/components/schemas/Slot"
"400":
description: Invalid or missing SlotId
"404":
description: Host is not in an active sale for the slot
"503":
description: Sales are unavailable
"/sales/availability":
get:
summary: "Returns storage that is for sale"
@ -504,9 +537,7 @@ paths:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Purchase"
$ref: "#/components/schemas/Purchase"
"400":
description: Invalid or missing Purchase ID
"404":

View File

@ -3,7 +3,7 @@ import std/options
import std/strformat
import std/strutils
import std/unittest
import pkg/chronicles
import pkg/chronicles except toJson
import pkg/stew/byteutils
import pkg/stint
import pkg/codex/contracts/requests

View File

@ -1,6 +1,7 @@
import std/times
import pkg/chronos
import codex/contracts/clock
import codex/utils/json
import ../ethertest
ethersuite "On-Chain Clock":

View File

@ -26,4 +26,4 @@ template ethersuite*(name, body) =
body
export asynctest
export ethers
export ethers except `%`

View File

@ -11,6 +11,8 @@ import pkg/codex/purchasing
import pkg/codex/errors
import pkg/codex/sales/reservations
export purchasing
type CodexClient* = ref object
http: HttpClient
baseurl: string
@ -106,6 +108,18 @@ proc getPurchase*(client: CodexClient, purchaseId: PurchaseId): ?!RestPurchase =
let json = ? parseJson(body).catch
RestPurchase.fromJson(json)
proc getSalesAgent*(client: CodexClient, slotId: SlotId): ?!RestSalesAgent =
let url = client.baseurl & "/sales/slots/" & slotId.toHex
echo "getting sales agent for id, ", slotId.toHex
try:
let body = client.http.getContent(url)
echo "get sales agent body: ", body
let json = ? parseJson(body).catch
return RestSalesAgent.fromJson(json)
except CatchableError as e:
echo "[client.getSalesAgent] error getting agent: ", e.msg
return failure e.msg
proc getSlots*(client: CodexClient): ?!seq[Slot] =
let url = client.baseurl & "/sales/slots"
let body = client.http.getContent(url)
@ -126,7 +140,7 @@ proc postAvailability*(
"maxCollateral": maxCollateral,
}
let response = client.http.post(url, $json)
assert response.status == "200 OK"
doAssert response.status == "200 OK", "expected 200 OK, got " & response.status & ", body: " & response.body
Availability.fromJson(response.body.parseJson)
proc getAvailabilities*(client: CodexClient): ?!seq[Availability] =
@ -141,3 +155,12 @@ proc close*(client: CodexClient) =
proc restart*(client: CodexClient) =
client.http.close()
client.http = newHttpClient()
proc purchaseStateIs*(client: CodexClient, id: PurchaseId, state: string): bool =
client.getPurchase(id).option.?state == some state
proc saleStateIs*(client: CodexClient, id: SlotId, state: string): bool =
client.getSalesAgent(id).option.?state == some state
proc requestId*(client: CodexClient, id: PurchaseId): ?RequestId =
return client.getPurchase(id).option.?requestId