[marketplace] sales state machine tests
This commit is contained in:
parent
fb96a9eb17
commit
a3f9e54cc1
|
@ -69,7 +69,7 @@ method getHost(market: OnChainMarket,
|
|||
else:
|
||||
return none Address
|
||||
|
||||
method getSlot(market: OnChainMarket, slotId: SlotId): Future[?Slot] {.async.} =
|
||||
method getSlot*(market: OnChainMarket, slotId: SlotId): Future[?Slot] {.async.} =
|
||||
try:
|
||||
return some await market.contract.getSlot(slotId)
|
||||
except ProviderError as e:
|
||||
|
|
|
@ -39,12 +39,10 @@ type
|
|||
Cancelled
|
||||
Finished
|
||||
Failed
|
||||
Slot* = object
|
||||
Slot* = object of RootObj
|
||||
host*: Address
|
||||
hostPaid*: bool
|
||||
requestId*: RequestId
|
||||
slotIndex*: UInt256
|
||||
proof*: seq[byte]
|
||||
|
||||
proc `==`*(x, y: Nonce): bool {.borrow.}
|
||||
proc `==`*(x, y: RequestId): bool {.borrow.}
|
||||
|
@ -117,6 +115,9 @@ func solidityType*(_: type StorageAsk): string =
|
|||
func solidityType*(_: type StorageRequest): string =
|
||||
solidityType(StorageRequest.fieldTypes)
|
||||
|
||||
func solidityType*(_: type Slot): string =
|
||||
solidityType(Slot.fieldTypes)
|
||||
|
||||
func solidityType*[T: RequestId | SlotId | Nonce](_: type T): string =
|
||||
solidityType(array[32, byte])
|
||||
|
||||
|
|
|
@ -111,7 +111,6 @@ method exit(state: AsyncState) =
|
|||
proc switchAsync*(machine: StateMachineAsync, newState: AsyncState) {.async.} =
|
||||
if state =? (machine.state as AsyncState):
|
||||
trace "Switching sales state", `from`=state, to=newState
|
||||
debugEcho "switching from ", state, " to ", newState
|
||||
if activeTransition =? state.activeTransition and
|
||||
not activeTransition.completed:
|
||||
await activeTransition.cancelAndWait()
|
||||
|
@ -121,7 +120,6 @@ proc switchAsync*(machine: StateMachineAsync, newState: AsyncState) {.async.} =
|
|||
state.context = none StateMachine
|
||||
else:
|
||||
trace "Switching sales state", `from`="no state", to=newState
|
||||
debugEcho "switching from no state to ", newState
|
||||
|
||||
machine.state = some State(newState)
|
||||
newState.context = some StateMachine(machine)
|
||||
|
|
|
@ -2,6 +2,7 @@ import std/sequtils
|
|||
import std/tables
|
||||
import std/hashes
|
||||
import pkg/codex/market
|
||||
import pkg/codex/contracts/requests
|
||||
|
||||
export market
|
||||
export tables
|
||||
|
@ -14,7 +15,7 @@ type
|
|||
requestEnds*: Table[RequestId, SecondsSince1970]
|
||||
state*: Table[RequestId, RequestState]
|
||||
fulfilled*: seq[Fulfillment]
|
||||
filled*: seq[Slot]
|
||||
filled*: seq[MockSlot]
|
||||
withdrawn*: seq[RequestId]
|
||||
signer: Address
|
||||
subscriptions: Subscriptions
|
||||
|
@ -22,6 +23,9 @@ type
|
|||
requestId*: RequestId
|
||||
proof*: seq[byte]
|
||||
host*: Address
|
||||
MockSlot* = object of Slot
|
||||
slotIndex*: UInt256
|
||||
proof*: seq[byte]
|
||||
Subscriptions = object
|
||||
onRequest: seq[RequestSubscription]
|
||||
onFulfillment: seq[FulfillmentSubscription]
|
||||
|
@ -83,6 +87,14 @@ method getRequest(market: MockMarket,
|
|||
return some request
|
||||
return none StorageRequest
|
||||
|
||||
method getSlot*(market: MockMarket,
|
||||
slotId: SlotId): Future[?Slot] {.async.} =
|
||||
for slot in market.filled:
|
||||
if slotId(slot.requestId, slot.slotIndex) == slotId:
|
||||
return some Slot(host: slot.host,
|
||||
requestId: slot.requestId)
|
||||
return none Slot
|
||||
|
||||
method getState*(market: MockMarket,
|
||||
requestId: RequestId): Future[?RequestState] {.async.} =
|
||||
return market.state.?[requestId]
|
||||
|
@ -132,7 +144,7 @@ proc fillSlot*(market: MockMarket,
|
|||
slotIndex: UInt256,
|
||||
proof: seq[byte],
|
||||
host: Address) =
|
||||
let slot = Slot(
|
||||
let slot = MockSlot(
|
||||
requestId: requestId,
|
||||
slotIndex: slotIndex,
|
||||
proof: proof,
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import std/sets
|
||||
import std/sequtils
|
||||
import std/sugar
|
||||
import std/times
|
||||
import pkg/asynctest
|
||||
import pkg/chronos
|
||||
import pkg/codex/contracts/requests
|
||||
# import pkg/codex/contracts/requests
|
||||
import pkg/codex/sales
|
||||
import pkg/codex/sales/states/[downloading, cancelled, errored, filled, filling,
|
||||
failed, proving, finished, unknown]
|
||||
import ./helpers/mockmarket
|
||||
import ./helpers/mockclock
|
||||
import ./examples
|
||||
|
@ -214,3 +218,301 @@ suite "Sales":
|
|||
discard await market.requestStorage(request)
|
||||
check proving.slots.len == 1
|
||||
check proving.slots.contains(request.slotId(soldSlotIndex))
|
||||
|
||||
suite "Sales state machine":
|
||||
|
||||
let availability = Availability.init(
|
||||
size=100.u256,
|
||||
duration=60.u256,
|
||||
minPrice=600.u256
|
||||
)
|
||||
var request = StorageRequest(
|
||||
ask: StorageAsk(
|
||||
slots: 4,
|
||||
slotSize: 100.u256,
|
||||
duration: 60.u256,
|
||||
reward: 10.u256,
|
||||
),
|
||||
content: StorageContent(
|
||||
cid: "some cid"
|
||||
)
|
||||
)
|
||||
let proof = exampleProof()
|
||||
|
||||
var sales: Sales
|
||||
var market: MockMarket
|
||||
var clock: MockClock
|
||||
var proving: Proving
|
||||
|
||||
setup:
|
||||
market = MockMarket.new()
|
||||
clock = MockClock.new()
|
||||
proving = Proving.new()
|
||||
sales = Sales.new(market, clock, proving)
|
||||
sales.onStore = proc(request: StorageRequest,
|
||||
slot: UInt256,
|
||||
availability: ?Availability) {.async.} =
|
||||
discard
|
||||
sales.onProve = proc(request: StorageRequest,
|
||||
slot: UInt256): Future[seq[byte]] {.async.} =
|
||||
return proof
|
||||
await sales.start()
|
||||
request.expiry = (clock.now() + 42).u256
|
||||
|
||||
teardown:
|
||||
await sales.stop()
|
||||
|
||||
proc newSalesAgent(slotIdx: UInt256 = 0.u256): SalesAgent =
|
||||
let agent = sales.newSalesAgent(request.id,
|
||||
some availability,
|
||||
some request)
|
||||
agent.slotIndex = some slotIdx
|
||||
return agent
|
||||
|
||||
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
|
||||
|
||||
test "moves to SaleErrored when SaleFilled errors":
|
||||
let agent = newSalesAgent()
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleUnknown())
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale host mismatch"
|
||||
|
||||
test "moves to SaleFinished when request state is New":
|
||||
let agent = newSalesAgent()
|
||||
await fillSlot()
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleUnknown())
|
||||
check (agent.state as SaleFinished).isSome
|
||||
|
||||
test "moves to SaleFinished when request state is Started":
|
||||
let agent = newSalesAgent()
|
||||
await fillSlot()
|
||||
market.state[request.id] = RequestState.Started
|
||||
agent.switch(SaleUnknown())
|
||||
check (agent.state as SaleFinished).isSome
|
||||
|
||||
test "moves to SaleFinished when request state is Finished":
|
||||
let agent = newSalesAgent()
|
||||
market.state[request.id] = RequestState.Finished
|
||||
agent.switch(SaleUnknown())
|
||||
check (agent.state as SaleFinished).isSome
|
||||
|
||||
test "moves to SaleErrored when request state is Cancelled":
|
||||
let agent = newSalesAgent()
|
||||
market.state[request.id] = RequestState.Cancelled
|
||||
agent.switch(SaleUnknown())
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale cancelled due to timeout"
|
||||
|
||||
test "moves to SaleErrored when request state is Failed":
|
||||
let agent = newSalesAgent()
|
||||
market.state[request.id] = RequestState.Failed
|
||||
agent.switch(SaleUnknown())
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale failed"
|
||||
|
||||
test "moves to SaleErrored when Downloading and request expires":
|
||||
sales.onStore = proc(request: StorageRequest,
|
||||
slot: UInt256,
|
||||
availability: ?Availability) {.async.} =
|
||||
await sleepAsync(chronos.minutes(1)) # "far" in the future
|
||||
request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleDownloading())
|
||||
clock.set(request.expiry.truncate(int64))
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale cancelled due to timeout"
|
||||
|
||||
test "moves to SaleErrored when Downloading and request fails":
|
||||
sales.onStore = proc(request: StorageRequest,
|
||||
slot: UInt256,
|
||||
availability: ?Availability) {.async.} =
|
||||
await sleepAsync(chronos.minutes(1)) # "far" in the future
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleDownloading())
|
||||
market.emitRequestFailed(request.id)
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale failed"
|
||||
|
||||
test "moves to SaleErrored when Filling and request expires":
|
||||
request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleFilling())
|
||||
clock.set(request.expiry.truncate(int64))
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale cancelled due to timeout"
|
||||
|
||||
test "moves to SaleErrored when Filling and request fails":
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleFilling())
|
||||
market.emitRequestFailed(request.id)
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale failed"
|
||||
|
||||
test "moves to SaleErrored when Finished and request expires":
|
||||
request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.Finished
|
||||
await agent.switchAsync(SaleFinished())
|
||||
clock.set(request.expiry.truncate(int64))
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale cancelled due to timeout"
|
||||
|
||||
test "moves to SaleErrored when Finished and request fails":
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.Finished
|
||||
await agent.switchAsync(SaleFinished())
|
||||
market.emitRequestFailed(request.id)
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale failed"
|
||||
|
||||
test "moves to SaleErrored when Proving and request expires":
|
||||
sales.onProve = proc(request: StorageRequest,
|
||||
slot: UInt256): Future[seq[byte]] {.async.} =
|
||||
await sleepAsync(chronos.minutes(1)) # "far" in the future
|
||||
return @[]
|
||||
request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleProving())
|
||||
clock.set(request.expiry.truncate(int64))
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale cancelled due to timeout"
|
||||
|
||||
test "moves to SaleErrored when Proving and request fails":
|
||||
sales.onProve = proc(request: StorageRequest,
|
||||
slot: UInt256): Future[seq[byte]] {.async.} =
|
||||
await sleepAsync(chronos.minutes(1)) # "far" in the future
|
||||
return @[]
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await agent.switchAsync(SaleProving())
|
||||
market.emitRequestFailed(request.id)
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleErrored)
|
||||
check state.isSome
|
||||
check (!state).error.msg == "Sale failed"
|
||||
|
||||
test "moves from SaleDownloading to SaleFinished, calling necessary callbacks":
|
||||
var onProveCalled, onStoreCalled, onClearCalled, onSaleCalled: bool
|
||||
sales.onProve = proc(request: StorageRequest,
|
||||
slot: UInt256): Future[seq[byte]] {.async.} =
|
||||
onProveCalled = true
|
||||
return @[]
|
||||
sales.onStore = proc(request: StorageRequest,
|
||||
slot: UInt256,
|
||||
availability: ?Availability) {.async.} =
|
||||
onStoreCalled = true
|
||||
sales.onClear = proc(availability: ?Availability,
|
||||
request: StorageRequest,
|
||||
slotIndex: UInt256) =
|
||||
onClearCalled = true
|
||||
sales.onSale = proc(availability: ?Availability,
|
||||
request: StorageRequest,
|
||||
slotIndex: UInt256) =
|
||||
onSaleCalled = true
|
||||
|
||||
let agent = newSalesAgent()
|
||||
await agent.init(request.ask.slots)
|
||||
market.requested.add request
|
||||
market.state[request.id] = RequestState.New
|
||||
await fillSlot(!agent.slotIndex)
|
||||
await agent.switchAsync(SaleDownloading())
|
||||
market.emitRequestFulfilled(request.id)
|
||||
await sleepAsync chronos.seconds(2)
|
||||
|
||||
let state = (agent.state as SaleFinished)
|
||||
check state.isSome
|
||||
check onProveCalled
|
||||
check onStoreCalled
|
||||
check not onClearCalled
|
||||
check onSaleCalled
|
||||
|
||||
test "loads active slots from market":
|
||||
let me = await market.getSigner()
|
||||
|
||||
request.ask.slots = 2
|
||||
market.requested = @[request]
|
||||
market.state[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)]
|
||||
|
||||
await sales.load()
|
||||
let expected = SalesAgent(sales: sales,
|
||||
requestId: request.id,
|
||||
availability: none Availability,
|
||||
request: some request)
|
||||
# because sales.load() calls agent.init, we won't know the slotIndex
|
||||
# randomly selected for the agent, and we also won't know the value of
|
||||
# `failed`/`fulfilled`/`cancelled` futures, so we need to compare
|
||||
# the properties we know
|
||||
proc `==` (agent0, agent1: SalesAgent): bool =
|
||||
return agent0.sales == agent1.sales and
|
||||
agent0.requestId == agent1.requestId and
|
||||
agent0.availability == agent1.availability and
|
||||
agent0.request == agent1.request
|
||||
|
||||
check sales.agents.all(agent => agent == expected)
|
||||
|
|
|
@ -80,7 +80,7 @@ ethersuite "On-Chain Market":
|
|||
test "supports request subscriptions":
|
||||
var receivedIds: seq[RequestId]
|
||||
var receivedAsks: seq[StorageAsk]
|
||||
proc onRequest(id: RequestId, ask: StorageAsk) =
|
||||
proc onRequest(id: RequestId, ask: StorageAsk) {.async.} =
|
||||
receivedIds.add(id)
|
||||
receivedAsks.add(ask)
|
||||
let subscription = await market.subscribeRequests(onRequest)
|
||||
|
@ -107,7 +107,7 @@ ethersuite "On-Chain Market":
|
|||
discard await market.requestStorage(request)
|
||||
var receivedIds: seq[RequestId]
|
||||
var receivedSlotIndices: seq[UInt256]
|
||||
proc onSlotFilled(id: RequestId, slotIndex: UInt256) =
|
||||
proc onSlotFilled(id: RequestId, slotIndex: UInt256) {.async.} =
|
||||
receivedIds.add(id)
|
||||
receivedSlotIndices.add(slotIndex)
|
||||
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
||||
|
@ -121,7 +121,7 @@ ethersuite "On-Chain Market":
|
|||
await token.approve(storage.address, request.price)
|
||||
discard await market.requestStorage(request)
|
||||
var receivedSlotIndices: seq[UInt256]
|
||||
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
|
||||
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) {.async.} =
|
||||
receivedSlotIndices.add(slotIndex)
|
||||
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
||||
await market.fillSlot(request.id, slotIndex - 1, proof)
|
||||
|
@ -134,7 +134,7 @@ ethersuite "On-Chain Market":
|
|||
await token.approve(storage.address, request.price)
|
||||
discard await market.requestStorage(request)
|
||||
var receivedIds: seq[RequestId]
|
||||
proc onFulfillment(id: RequestId) =
|
||||
proc onFulfillment(id: RequestId) {.async.} =
|
||||
receivedIds.add(id)
|
||||
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
||||
for slotIndex in 0..<request.ask.slots:
|
||||
|
@ -152,7 +152,7 @@ ethersuite "On-Chain Market":
|
|||
discard await market.requestStorage(otherRequest)
|
||||
|
||||
var receivedIds: seq[RequestId]
|
||||
proc onFulfillment(id: RequestId) =
|
||||
proc onFulfillment(id: RequestId) {.async.} =
|
||||
receivedIds.add(id)
|
||||
|
||||
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
||||
|
@ -171,7 +171,7 @@ ethersuite "On-Chain Market":
|
|||
discard await market.requestStorage(request)
|
||||
|
||||
var receivedIds: seq[RequestId]
|
||||
proc onRequestCancelled(id: RequestId) =
|
||||
proc onRequestCancelled(id: RequestId) {.async.} =
|
||||
receivedIds.add(id)
|
||||
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
|
||||
|
||||
|
@ -185,7 +185,7 @@ ethersuite "On-Chain Market":
|
|||
discard await market.requestStorage(request)
|
||||
|
||||
var receivedIds: seq[RequestId]
|
||||
proc onRequestFailed(id: RequestId) =
|
||||
proc onRequestFailed(id: RequestId) {.async.} =
|
||||
receivedIds.add(id)
|
||||
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
|
||||
|
||||
|
@ -213,7 +213,7 @@ ethersuite "On-Chain Market":
|
|||
discard await market.requestStorage(otherRequest)
|
||||
|
||||
var receivedIds: seq[RequestId]
|
||||
proc onRequestCancelled(requestId: RequestId) =
|
||||
proc onRequestCancelled(requestId: RequestId) {.async.} =
|
||||
receivedIds.add(requestId)
|
||||
|
||||
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
|
||||
|
@ -244,3 +244,27 @@ ethersuite "On-Chain Market":
|
|||
for slotIndex in 0..<request.ask.slots:
|
||||
await market.fillSlot(request.id, slotIndex.u256, proof)
|
||||
check (await market.getState(request.id)) == RequestState.Started
|
||||
|
||||
test "can retrieve active slots":
|
||||
await token.approve(storage.address, request.price)
|
||||
discard await market.requestStorage(request)
|
||||
await market.fillSlot(request.id, slotIndex - 1, proof)
|
||||
await market.fillSlot(request.id, slotIndex, proof)
|
||||
let slotId1 = request.slotId(slotIndex - 1)
|
||||
let slotId2 = request.slotId(slotIndex)
|
||||
check (await market.mySlots()) == @[slotId1, slotId2]
|
||||
|
||||
test "returns none when slot is empty":
|
||||
await token.approve(storage.address, request.price)
|
||||
discard await market.requestStorage(request)
|
||||
let slotId = request.slotId(slotIndex)
|
||||
check (await market.getSlot(slotId)) == none Slot
|
||||
|
||||
test "can retrieve slot details":
|
||||
await token.approve(storage.address, request.price)
|
||||
discard await market.requestStorage(request)
|
||||
await market.fillSlot(request.id, slotIndex, proof)
|
||||
let slotId = request.slotId(slotIndex)
|
||||
let expected = Slot(host: request.client,
|
||||
requestId: request.id)
|
||||
check (await market.getSlot(slotId)) == some expected
|
||||
|
|
Loading…
Reference in New Issue