From 3907ca40951dcdb3858ed395aed7911a15a1b942 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:16:36 +0000 Subject: [PATCH] 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 --- .../interactions/clientinteractions.nim | 2 +- .../interactions/hostinteractions.nim | 2 +- codex/contracts/marketplace.nim | 4 +- codex/contracts/requests.nim | 4 ++ codex/rest/api.nim | 36 +++++++++++++--- codex/rest/coders.nim | 2 +- codex/rest/json.nim | 7 +++- codex/sales.nim | 9 ++++ codex/sales/reservations.nim | 4 +- codex/sales/salesagent.nim | 5 +++ codex/utils/asyncstatemachine.nim | 2 +- codex/utils/fileutils.nim | 2 +- codex/utils/json.nim | 3 +- openapi.yaml | 41 ++++++++++++++++--- tests/codex/utils/testjson.nim | 2 +- tests/contracts/testClock.nim | 1 + tests/ethertest.nim | 2 +- tests/integration/codexclient.nim | 25 ++++++++++- 18 files changed, 126 insertions(+), 27 deletions(-) diff --git a/codex/contracts/interactions/clientinteractions.nim b/codex/contracts/interactions/clientinteractions.nim index c30a22e4..e2354d85 100644 --- a/codex/contracts/interactions/clientinteractions.nim +++ b/codex/contracts/interactions/clientinteractions.nim @@ -7,7 +7,7 @@ import ../clock import ./interactions export purchasing -export chronicles +export chronicles except toJson type ClientInteractions* = ref object of ContractInteractions diff --git a/codex/contracts/interactions/hostinteractions.nim b/codex/contracts/interactions/hostinteractions.nim index bfb9ac9c..e9749df5 100644 --- a/codex/contracts/interactions/hostinteractions.nim +++ b/codex/contracts/interactions/hostinteractions.nim @@ -5,7 +5,7 @@ import ../../sales import ./interactions export sales -export chronicles +export chronicles except toJson type HostInteractions* = ref object of ContractInteractions diff --git a/codex/contracts/marketplace.nim b/codex/contracts/marketplace.nim index f09e8720..721e5ce5 100644 --- a/codex/contracts/marketplace.nim +++ b/codex/contracts/marketplace.nim @@ -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 diff --git a/codex/contracts/requests.nim b/codex/contracts/requests.nim index 1abfcd7e..60c3c577 100644 --- a/codex/contracts/requests.nim +++ b/codex/contracts/requests.nim @@ -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], diff --git a/codex/rest/api.nim b/codex/rest/api.nim index 7eb6d578..deea019b 100644 --- a/codex/rest/api.nim +++ b/codex/rest/api.nim @@ -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: diff --git a/codex/rest/coders.nim b/codex/rest/coders.nim index 66b41ee3..2fa4da49 100644 --- a/codex/rest/coders.nim +++ b/codex/rest/coders.nim @@ -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)) diff --git a/codex/rest/json.nim b/codex/rest/json.nim index da26af0a..c7892aa7 100644 --- a/codex/rest/json.nim +++ b/codex/rest/json.nim @@ -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 diff --git a/codex/sales.nim b/codex/sales.nim index b68f9ccc..53b5860b 100644 --- a/codex/sales.nim +++ b/codex/sales.nim @@ -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() diff --git a/codex/sales/reservations.nim b/codex/sales/reservations.nim index 19aff95d..001d6a5a 100644 --- a/codex/sales/reservations.nim +++ b/codex/sales/reservations.nim @@ -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" diff --git a/codex/sales/salesagent.nim b/codex/sales/salesagent.nim index 3db520b4..b189650d 100644 --- a/codex/sales/salesagent.nim +++ b/codex/sales/salesagent.nim @@ -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 diff --git a/codex/utils/asyncstatemachine.nim b/codex/utils/asyncstatemachine.nim index b01cba79..27d09b64 100644 --- a/codex/utils/asyncstatemachine.nim +++ b/codex/utils/asyncstatemachine.nim @@ -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) diff --git a/codex/utils/fileutils.nim b/codex/utils/fileutils.nim index af56ba58..03cffeba 100644 --- a/codex/utils/fileutils.nim +++ b/codex/utils/fileutils.nim @@ -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] diff --git a/codex/utils/json.nim b/codex/utils/json.nim index 8ece70e0..ade6936d 100644 --- a/codex/utils/json.nim +++ b/codex/utils/json.nim @@ -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 diff --git a/openapi.yaml b/openapi.yaml index 90fdc732..e3eeaa36 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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": diff --git a/tests/codex/utils/testjson.nim b/tests/codex/utils/testjson.nim index a32f3bdb..aa3731e5 100644 --- a/tests/codex/utils/testjson.nim +++ b/tests/codex/utils/testjson.nim @@ -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 diff --git a/tests/contracts/testClock.nim b/tests/contracts/testClock.nim index 7f691eaf..fafb6092 100644 --- a/tests/contracts/testClock.nim +++ b/tests/contracts/testClock.nim @@ -1,6 +1,7 @@ import std/times import pkg/chronos import codex/contracts/clock +import codex/utils/json import ../ethertest ethersuite "On-Chain Clock": diff --git a/tests/ethertest.nim b/tests/ethertest.nim index 3a76cd2e..a1c64948 100644 --- a/tests/ethertest.nim +++ b/tests/ethertest.nim @@ -26,4 +26,4 @@ template ethersuite*(name, body) = body export asynctest -export ethers +export ethers except `%` diff --git a/tests/integration/codexclient.nim b/tests/integration/codexclient.nim index 28739d7d..285d365f 100644 --- a/tests/integration/codexclient.nim +++ b/tests/integration/codexclient.nim @@ -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