import std/sets import std/sequtils import std/sugar import std/times import pkg/asynctest import pkg/chronos import pkg/datastore import pkg/questionable import pkg/questionable/results import pkg/codex/sales import pkg/codex/sales/salesdata import pkg/codex/sales/salescontext import pkg/codex/sales/reservations import pkg/codex/stores/repostore import pkg/codex/proving import pkg/codex/blocktype as bt import pkg/codex/node import ../helpers/mockmarket import ../helpers/mockclock import ../helpers/eventually import ../examples import ./helpers suite "Sales": let proof = exampleProof() var availability: Availability var request: StorageRequest var sales: Sales var market: MockMarket var clock: MockClock var proving: Proving var reservations: Reservations var repo: RepoStore setup: availability = Availability.init( size=100.u256, duration=60.u256, minPrice=600.u256, maxCollateral=400.u256 ) request = StorageRequest( ask: StorageAsk( slots: 4, slotSize: 100.u256, duration: 60.u256, reward: 10.u256, collateral: 200.u256, ), content: StorageContent( cid: "some cid" ), expiry: (getTime() + initDuration(hours=1)).toUnix.u256 ) market = MockMarket.new() clock = MockClock.new() proving = Proving.new() let repoDs = SQLiteDatastore.new(Memory).tryGet() let metaDs = SQLiteDatastore.new(Memory).tryGet() repo = RepoStore.new(repoDs, metaDs) await repo.start() sales = Sales.new(market, clock, proving, repo) reservations = sales.context.reservations sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = return success() proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = return proof await sales.start() request.expiry = (clock.now() + 42).u256 teardown: await repo.stop() await sales.stop() proc getAvailability: ?!Availability = waitFor reservations.get(availability.id) proc wasIgnored: Future[bool] {.async.} = return eventually sales.agents.len == 1 and # agent created at first eventually sales.agents.len == 0 # then removed once ignored test "makes storage unavailable when downloading a matched request": var used = false sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = without avail =? await reservations.get(availability.id): fail() used = avail.used return success() check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually used test "reduces remaining availability size after download": let blk = bt.Block.example request.ask.slotSize = blk.data.len.u256 availability.size = request.ask.slotSize + 1 sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = await onBatch(@[blk]) return success() check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually getAvailability().?size == success 1.u256 test "ignores download when duration not long enough": availability.duration = request.ask.duration - 1 check isOk await reservations.reserve(availability) await market.requestStorage(request) check await wasIgnored() test "ignores request when slot size is too small": availability.size = request.ask.slotSize - 1 check isOk await reservations.reserve(availability) await market.requestStorage(request) check await wasIgnored() test "ignores request when reward is too low": availability.minPrice = request.ask.pricePerSlot + 1 check isOk await reservations.reserve(availability) await market.requestStorage(request) check await wasIgnored() test "availability remains unused when request is ignored": availability.minPrice = request.ask.pricePerSlot + 1 check isOk await reservations.reserve(availability) await market.requestStorage(request) check getAvailability().?used == success false test "ignores request when asked collateral is too high": var tooBigCollateral = request tooBigCollateral.ask.collateral = availability.maxCollateral + 1 check isOk await reservations.reserve(availability) await market.requestStorage(tooBigCollateral) check await wasIgnored() test "retrieves and stores data locally": var storingRequest: StorageRequest var storingSlot: UInt256 sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = storingRequest = request storingSlot = slot return success() check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually storingRequest == request check storingSlot < request.ask.slots.u256 test "handles errors during state run": var saleFailed = false proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = # raise exception so machine.onError is called raise newException(ValueError, "some error") # onClear is called in SaleErrored.run sales.onClear = proc(request: StorageRequest, idx: UInt256) = saleFailed = true check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually saleFailed test "makes storage available again when data retrieval fails": let error = newException(IOError, "data retrieval failed") sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = return failure(error) check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually getAvailability().?used == success false check getAvailability().?size == success availability.size test "generates proof of storage": var provingRequest: StorageRequest var provingSlot: UInt256 proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = provingRequest = slot.request provingSlot = slot.slotIndex check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually provingRequest == request check provingSlot < request.ask.slots.u256 test "fills a slot": check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually market.filled.len == 1 check market.filled[0].requestId == request.id check market.filled[0].slotIndex < request.ask.slots.u256 check market.filled[0].proof == proof check market.filled[0].host == await market.getSigner() test "calls onSale when slot is filled": var soldAvailability: Availability var soldRequest: StorageRequest var soldSlotIndex: UInt256 sales.onSale = proc(request: StorageRequest, slotIndex: UInt256) = if a =? availability: soldAvailability = a soldRequest = request soldSlotIndex = slotIndex check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually soldAvailability == availability check soldRequest == request check soldSlotIndex < request.ask.slots.u256 test "calls onClear when storage becomes available again": # fail the proof intentionally to trigger `agent.finish(success=false)`, # which then calls the onClear callback proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = raise newException(IOError, "proof failed") var clearedRequest: StorageRequest var clearedSlotIndex: UInt256 sales.onClear = proc(request: StorageRequest, slotIndex: UInt256) = clearedRequest = request clearedSlotIndex = slotIndex check isOk await reservations.reserve(availability) await market.requestStorage(request) check eventually clearedRequest == request check clearedSlotIndex < request.ask.slots.u256 test "makes storage available again when other host fills the slot": let otherHost = Address.example sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = await sleepAsync(chronos.hours(1)) return success() check isOk await reservations.reserve(availability) await market.requestStorage(request) for slotIndex in 0.. agent.data == expected)