import std/sequtils import std/sugar import std/times import pkg/asynctest/chronos/unittest 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/sales/slotqueue import pkg/codex/stores/repostore import pkg/codex/blocktype as bt import pkg/codex/node import pkg/codex/utils/asyncspawn import ../helpers import ../helpers/mockmarket import ../helpers/mockclock import ../helpers/always import ../examples asyncchecksuite "Sales - start": let proof = exampleProof() var request: StorageRequest var sales: Sales var market: MockMarket var clock: MockClock var reservations: Reservations var repo: RepoStore var queue: SlotQueue var itemsProcessed: seq[SlotQueueItem] setup: 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() 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, repo) reservations = sales.context.reservations sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = return success() sales.onExpiryUpdate = proc(rootCid: string, expiry: SecondsSince1970): Future[?!void] {.async.} = return success() queue = sales.context.slotQueue sales.onProve = proc(slot: Slot, challenge: ProofChallenge): Future[?!seq[byte]] {.async.} = return success(proof) itemsProcessed = @[] request.expiry = (clock.now() + 42).u256 teardown: await sales.stop() await repo.stop() proc fillSlot(slotIdx: UInt256 = 0.u256) {.async.} = let address = await market.getSigner() let slot = MockSlot(requestId: request.id, slotIndex: slotIdx, proof: proof, host: address) market.filled.add slot market.slotState[slotId(request.id, slotIdx)] = SlotState.Filled test "load slots when Sales module starts": let me = await market.getSigner() request.ask.slots = 2 market.requested = @[request] market.requestState[request.id] = RequestState.New let slot0 = MockSlot(requestId: request.id, slotIndex: 0.u256, proof: proof, host: me) await fillSlot(slot0.slotIndex) let slot1 = MockSlot(requestId: request.id, slotIndex: 1.u256, proof: proof, host: me) await fillSlot(slot1.slotIndex) market.activeSlots[me] = @[request.slotId(0.u256), request.slotId(1.u256)] market.requested = @[request] market.activeRequests[me] = @[request.id] await sales.start() check eventually sales.agents.len == 2 check sales.agents.any(agent => agent.data.requestId == request.id and agent.data.slotIndex == 0.u256) check sales.agents.any(agent => agent.data.requestId == request.id and agent.data.slotIndex == 1.u256) asyncchecksuite "Sales": let proof = exampleProof() var availability: Availability var request: StorageRequest var sales: Sales var market: MockMarket var clock: MockClock var reservations: Reservations var repo: RepoStore var queue: SlotQueue var itemsProcessed: seq[SlotQueueItem] setup: availability = Availability( 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() let me = await market.getSigner() market.activeSlots[me] = @[] market.requestEnds[request.id] = request.expiry.toSecondsSince1970 clock = MockClock.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, repo) reservations = sales.context.reservations sales.onStore = proc(request: StorageRequest, slot: UInt256, onBatch: BatchProc): Future[?!void] {.async.} = return success() sales.onExpiryUpdate = proc(rootCid: string, expiry: SecondsSince1970): Future[?!void] {.async.} = return success() queue = sales.context.slotQueue sales.onProve = proc(slot: Slot, challenge: ProofChallenge): Future[?!seq[byte]] {.async.} = return success(proof) await sales.start() itemsProcessed = @[] teardown: await sales.stop() await repo.stop() proc getAvailability: Availability = let key = availability.id.key.get (waitFor reservations.get(key, Availability)).get proc createAvailability() = let a = waitFor reservations.createAvailability( availability.size, availability.duration, availability.minPrice, availability.maxCollateral ) availability = a.get # update id proc notProcessed(itemsProcessed: seq[SlotQueueItem], request: StorageRequest): bool = let items = SlotQueueItem.init(request) for i in 0.. 0 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 onFilled when slot is filled": var soldRequest = StorageRequest.default var soldSlotIndex = UInt256.high sales.onSale = proc(request: StorageRequest, slotIndex: UInt256) = soldRequest = request soldSlotIndex = slotIndex createAvailability() await market.requestStorage(request) check eventually 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 sales.onProve = proc(slot: Slot, challenge: ProofChallenge): 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 createAvailability() 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() createAvailability() await market.requestStorage(request) for slotIndex in 0.. agent.data.requestId == request.id and agent.data.slotIndex == 0.u256) check sales.agents.any(agent => agent.data.requestId == request.id and agent.data.slotIndex == 1.u256) test "deletes inactive reservations on load": createAvailability() discard await reservations.createReservation( availability.id, 100.u256, RequestId.example, UInt256.example) check (await reservations.all(Reservation)).get.len == 1 await sales.load() check (await reservations.all(Reservation)).get.len == 0 check getAvailability().size == availability.size # was restored