Load purchase state from chain (#283)
* [purchasing] Simplify test * [utils] Move StorageRequest.example up one level * [purchasing] Load purchases from market * [purchasing] load purchase states * Implement myRequest() and getState() methods for OnChainMarket * [proofs] Fix intermittently failing tests Ensures that examples of proofs in tests are never of length 0; these are considered invalid proofs by the smart contract logic. * [contracts] Fix failing test With the new solidity contracts update, a contract can only be paid out after it started. * [market] Add method to get request end time * [purchasing] wait until purchase is finished Purchase.wait() would previously wait until purchase was started, now we wait until it is finished. * [purchasing] Handle 'finished' and 'failed' states * [marketplace] move to failed state once request fails - Add support for subscribing to request failure events. - Add supporting contract tests for subscribing to request failure events. - Allow the PurchaseStarted state to move to PurchaseFailure once a request failure event is emitted - Add supporting tests for moving from PurchaseStarted to PurchaseFailure - Add state transition tests for PurchaseUnknown. * [marketplace] Fix test with longer sleepAsync * [integration] Add function to restart a codex node * [purchasing] Set client address before requesting storage To prevent the purchase id (which equals the request id) from changing once it's been submitted. * [contracts] Fix: OnChainMarket.getState() Had the wrong method signature before * [purchasing] Load purchases on node start * [purchasing] Rename state 'PurchaseError' to 'PurchaseErrored' Allows for an exception type called 'PurchaseError' * [purchasing] Load purchases in background No longer calls market.getRequest() for every purchase on node start. * [contracts] Add `$` for RequestId, SlotId and Nonce To aid with debugging * [purchasing] Add Purchasing.stop() To ensure that all contract interactions have both a start() and a stop() for * [tests] Remove sleepAsync where possible Use `eventually` loop instead, to make sure that we're not waiting unnecessarily. * [integration] Fix: handle non-json response in test * [purchasing] Add purchase state to json * [integration] Ensure that purchase is submitted before restart Fixes test failure on slower CI * [purchasing] re-implement `description` as method Allows description to be set in the same module where the state type is defined. Co-authored-by: Eric Mastro <eric.mastro@gmail.com> * [contracts] fix typo Co-authored-by: Eric Mastro <eric.mastro@gmail.com> * [market] Use more generic error type Should we decide to change the provider type later Co-authored-by: Eric Mastro <eric.mastro@gmail.com> Co-authored-by: Eric Mastro <eric.mastro@gmail.com>
This commit is contained in:
parent
be32b9619b
commit
4175689745
|
@ -69,8 +69,10 @@ proc start*(interactions: ContractInteractions) {.async.} =
|
||||||
await interactions.clock.start()
|
await interactions.clock.start()
|
||||||
await interactions.sales.start()
|
await interactions.sales.start()
|
||||||
await interactions.proving.start()
|
await interactions.proving.start()
|
||||||
|
await interactions.purchasing.start()
|
||||||
|
|
||||||
proc stop*(interactions: ContractInteractions) {.async.} =
|
proc stop*(interactions: ContractInteractions) {.async.} =
|
||||||
|
await interactions.purchasing.stop()
|
||||||
await interactions.sales.stop()
|
await interactions.sales.stop()
|
||||||
await interactions.proving.stop()
|
await interactions.proving.stop()
|
||||||
await interactions.clock.stop()
|
await interactions.clock.stop()
|
||||||
|
|
|
@ -28,13 +28,11 @@ func new*(_: type OnChainMarket, contract: Storage): OnChainMarket =
|
||||||
method getSigner*(market: OnChainMarket): Future[Address] {.async.} =
|
method getSigner*(market: OnChainMarket): Future[Address] {.async.} =
|
||||||
return await market.signer.getAddress()
|
return await market.signer.getAddress()
|
||||||
|
|
||||||
method requestStorage(market: OnChainMarket,
|
method myRequests*(market: OnChainMarket): Future[seq[RequestId]] {.async.} =
|
||||||
request: StorageRequest):
|
return await market.contract.myRequests
|
||||||
Future[StorageRequest] {.async.} =
|
|
||||||
var request = request
|
method requestStorage(market: OnChainMarket, request: StorageRequest){.async.} =
|
||||||
request.client = await market.signer.getAddress()
|
|
||||||
await market.contract.requestStorage(request)
|
await market.contract.requestStorage(request)
|
||||||
return request
|
|
||||||
|
|
||||||
method getRequest(market: OnChainMarket,
|
method getRequest(market: OnChainMarket,
|
||||||
id: RequestId): Future[?StorageRequest] {.async.} =
|
id: RequestId): Future[?StorageRequest] {.async.} =
|
||||||
|
@ -45,6 +43,19 @@ method getRequest(market: OnChainMarket,
|
||||||
return none StorageRequest
|
return none StorageRequest
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
method getState*(market: OnChainMarket,
|
||||||
|
requestId: RequestId): Future[?RequestState] {.async.} =
|
||||||
|
try:
|
||||||
|
return some await market.contract.state(requestId)
|
||||||
|
except ProviderError as e:
|
||||||
|
if e.revertReason.contains("Unknown request"):
|
||||||
|
return none RequestState
|
||||||
|
raise e
|
||||||
|
|
||||||
|
method getRequestEnd*(market: OnChainMarket,
|
||||||
|
id: RequestId): Future[SecondsSince1970] {.async.} =
|
||||||
|
return await market.contract.requestEnd(id)
|
||||||
|
|
||||||
method getHost(market: OnChainMarket,
|
method getHost(market: OnChainMarket,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256): Future[?Address] {.async.} =
|
slotIndex: UInt256): Future[?Address] {.async.} =
|
||||||
|
@ -104,5 +115,15 @@ method subscribeRequestCancelled*(market: OnChainMarket,
|
||||||
let subscription = await market.contract.subscribe(RequestCancelled, onEvent)
|
let subscription = await market.contract.subscribe(RequestCancelled, onEvent)
|
||||||
return OnChainMarketSubscription(eventSubscription: subscription)
|
return OnChainMarketSubscription(eventSubscription: subscription)
|
||||||
|
|
||||||
|
method subscribeRequestFailed*(market: OnChainMarket,
|
||||||
|
requestId: RequestId,
|
||||||
|
callback: OnRequestFailed):
|
||||||
|
Future[MarketSubscription] {.async.} =
|
||||||
|
proc onEvent(event: RequestFailed) {.upraises:[].} =
|
||||||
|
if event.requestId == requestId:
|
||||||
|
callback(event.requestId)
|
||||||
|
let subscription = await market.contract.subscribe(RequestFailed, onEvent)
|
||||||
|
return OnChainMarketSubscription(eventSubscription: subscription)
|
||||||
|
|
||||||
method unsubscribe*(subscription: OnChainMarketSubscription) {.async.} =
|
method unsubscribe*(subscription: OnChainMarketSubscription) {.async.} =
|
||||||
await subscription.eventSubscription.unsubscribe()
|
await subscription.eventSubscription.unsubscribe()
|
||||||
|
|
|
@ -33,6 +33,12 @@ type
|
||||||
SlotId* = distinct array[32, byte]
|
SlotId* = distinct array[32, byte]
|
||||||
RequestId* = distinct array[32, byte]
|
RequestId* = distinct array[32, byte]
|
||||||
Nonce* = distinct array[32, byte]
|
Nonce* = distinct array[32, byte]
|
||||||
|
RequestState* {.pure.} = enum
|
||||||
|
New
|
||||||
|
Started
|
||||||
|
Cancelled
|
||||||
|
Finished
|
||||||
|
Failed
|
||||||
|
|
||||||
proc `==`*(x, y: Nonce): bool {.borrow.}
|
proc `==`*(x, y: Nonce): bool {.borrow.}
|
||||||
proc `==`*(x, y: RequestId): bool {.borrow.}
|
proc `==`*(x, y: RequestId): bool {.borrow.}
|
||||||
|
@ -42,6 +48,9 @@ proc hash*(x: SlotId): Hash {.borrow.}
|
||||||
func toArray*(id: RequestId | SlotId | Nonce): array[32, byte] =
|
func toArray*(id: RequestId | SlotId | Nonce): array[32, byte] =
|
||||||
array[32, byte](id)
|
array[32, byte](id)
|
||||||
|
|
||||||
|
proc `$`*(id: RequestId | SlotId | Nonce): string =
|
||||||
|
id.toArray.toHex
|
||||||
|
|
||||||
func fromTuple(_: type StorageRequest, tupl: tuple): StorageRequest =
|
func fromTuple(_: type StorageRequest, tupl: tuple): StorageRequest =
|
||||||
StorageRequest(
|
StorageRequest(
|
||||||
client: tupl[0],
|
client: tupl[0],
|
||||||
|
|
|
@ -2,6 +2,7 @@ import pkg/ethers
|
||||||
import pkg/json_rpc/rpcclient
|
import pkg/json_rpc/rpcclient
|
||||||
import pkg/stint
|
import pkg/stint
|
||||||
import pkg/chronos
|
import pkg/chronos
|
||||||
|
import ../clock
|
||||||
import ./requests
|
import ./requests
|
||||||
|
|
||||||
export stint
|
export stint
|
||||||
|
@ -20,6 +21,8 @@ type
|
||||||
requestId* {.indexed.}: RequestId
|
requestId* {.indexed.}: RequestId
|
||||||
RequestCancelled* = object of Event
|
RequestCancelled* = object of Event
|
||||||
requestId* {.indexed.}: RequestId
|
requestId* {.indexed.}: RequestId
|
||||||
|
RequestFailed* = object of Event
|
||||||
|
requestId* {.indexed.}: RequestId
|
||||||
ProofSubmitted* = object of Event
|
ProofSubmitted* = object of Event
|
||||||
id*: SlotId
|
id*: SlotId
|
||||||
proof*: seq[byte]
|
proof*: seq[byte]
|
||||||
|
@ -41,6 +44,10 @@ proc payoutSlot*(storage: Storage, requestId: RequestId, slotIndex: UInt256) {.c
|
||||||
proc getRequest*(storage: Storage, id: RequestId): StorageRequest {.contract, view.}
|
proc getRequest*(storage: Storage, id: RequestId): StorageRequest {.contract, view.}
|
||||||
proc getHost*(storage: Storage, id: SlotId): Address {.contract, view.}
|
proc getHost*(storage: Storage, id: SlotId): Address {.contract, view.}
|
||||||
|
|
||||||
|
proc myRequests*(storage: Storage): seq[RequestId] {.contract, view.}
|
||||||
|
proc state*(storage: Storage, requestId: RequestId): RequestState {.contract, view.}
|
||||||
|
proc requestEnd*(storage: Storage, requestId: RequestId): SecondsSince1970 {.contract, view.}
|
||||||
|
|
||||||
proc proofPeriod*(storage: Storage): UInt256 {.contract, view.}
|
proc proofPeriod*(storage: Storage): UInt256 {.contract, view.}
|
||||||
proc proofTimeout*(storage: Storage): UInt256 {.contract, view.}
|
proc proofTimeout*(storage: Storage): UInt256 {.contract, view.}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,12 @@ import pkg/chronos
|
||||||
import pkg/upraises
|
import pkg/upraises
|
||||||
import pkg/questionable
|
import pkg/questionable
|
||||||
import ./contracts/requests
|
import ./contracts/requests
|
||||||
|
import ./clock
|
||||||
|
|
||||||
export chronos
|
export chronos
|
||||||
export questionable
|
export questionable
|
||||||
export requests
|
export requests
|
||||||
|
export SecondsSince1970
|
||||||
|
|
||||||
type
|
type
|
||||||
Market* = ref object of RootObj
|
Market* = ref object of RootObj
|
||||||
|
@ -14,13 +16,16 @@ type
|
||||||
OnFulfillment* = proc(requestId: RequestId) {.gcsafe, upraises: [].}
|
OnFulfillment* = proc(requestId: RequestId) {.gcsafe, upraises: [].}
|
||||||
OnSlotFilled* = proc(requestId: RequestId, slotIndex: UInt256) {.gcsafe, upraises:[].}
|
OnSlotFilled* = proc(requestId: RequestId, slotIndex: UInt256) {.gcsafe, upraises:[].}
|
||||||
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
||||||
|
OnRequestFailed* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
||||||
|
|
||||||
method getSigner*(market: Market): Future[Address] {.base, async.} =
|
method getSigner*(market: Market): Future[Address] {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method requestStorage*(market: Market,
|
method requestStorage*(market: Market,
|
||||||
request: StorageRequest):
|
request: StorageRequest) {.base, async.} =
|
||||||
Future[StorageRequest] {.base, async.} =
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
|
method myRequests*(market: Market): Future[seq[RequestId]] {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method getRequest*(market: Market,
|
method getRequest*(market: Market,
|
||||||
|
@ -28,6 +33,14 @@ method getRequest*(market: Market,
|
||||||
Future[?StorageRequest] {.base, async.} =
|
Future[?StorageRequest] {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
|
method getState*(market: Market,
|
||||||
|
requestId: RequestId): Future[?RequestState] {.base, async.} =
|
||||||
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
|
method getRequestEnd*(market: Market,
|
||||||
|
id: RequestId): Future[SecondsSince1970] {.base, async.} =
|
||||||
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method getHost*(market: Market,
|
method getHost*(market: Market,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256): Future[?Address] {.base, async.} =
|
slotIndex: UInt256): Future[?Address] {.base, async.} =
|
||||||
|
@ -62,9 +75,15 @@ method subscribeSlotFilled*(market: Market,
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method subscribeRequestCancelled*(market: Market,
|
method subscribeRequestCancelled*(market: Market,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
callback: OnRequestCancelled):
|
callback: OnRequestCancelled):
|
||||||
Future[Subscription] {.base, async.} =
|
Future[Subscription] {.base, async.} =
|
||||||
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
|
method subscribeRequestFailed*(market: Market,
|
||||||
|
requestId: RequestId,
|
||||||
|
callback: OnRequestFailed):
|
||||||
|
Future[Subscription] {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method unsubscribe*(subscription: Subscription) {.base, async, upraises:[].} =
|
method unsubscribe*(subscription: Subscription) {.base, async, upraises:[].} =
|
||||||
|
|
|
@ -288,7 +288,7 @@ proc requestStorage*(self: CodexNodeRef,
|
||||||
expiry: expiry |? 0.u256
|
expiry: expiry |? 0.u256
|
||||||
)
|
)
|
||||||
|
|
||||||
let purchase = contracts.purchasing.purchase(request)
|
let purchase = await contracts.purchasing.purchase(request)
|
||||||
return success purchase.id
|
return success purchase.id
|
||||||
|
|
||||||
proc new*(
|
proc new*(
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ./clock
|
||||||
import ./purchasing/purchase
|
import ./purchasing/purchase
|
||||||
|
|
||||||
export questionable
|
export questionable
|
||||||
|
export chronos
|
||||||
export market
|
export market
|
||||||
export purchase
|
export purchase
|
||||||
|
|
||||||
|
@ -31,7 +32,22 @@ proc new*(_: type Purchasing, market: Market, clock: Clock): Purchasing =
|
||||||
requestExpiryInterval: DefaultRequestExpiryInterval,
|
requestExpiryInterval: DefaultRequestExpiryInterval,
|
||||||
)
|
)
|
||||||
|
|
||||||
proc populate*(purchasing: Purchasing, request: StorageRequest): StorageRequest =
|
proc load*(purchasing: Purchasing) {.async.} =
|
||||||
|
let market = purchasing.market
|
||||||
|
let requestIds = await market.myRequests()
|
||||||
|
for requestId in requestIds:
|
||||||
|
let purchase = Purchase.new(requestId, purchasing.market, purchasing.clock)
|
||||||
|
purchase.load()
|
||||||
|
purchasing.purchases[purchase.id] = purchase
|
||||||
|
|
||||||
|
proc start*(purchasing: Purchasing) {.async.} =
|
||||||
|
await purchasing.load()
|
||||||
|
|
||||||
|
proc stop*(purchasing: Purchasing) {.async.} =
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc populate*(purchasing: Purchasing,
|
||||||
|
request: StorageRequest): Future[StorageRequest] {.async.} =
|
||||||
result = request
|
result = request
|
||||||
if result.ask.proofProbability == 0.u256:
|
if result.ask.proofProbability == 0.u256:
|
||||||
result.ask.proofProbability = purchasing.proofProbability
|
result.ask.proofProbability = purchasing.proofProbability
|
||||||
|
@ -41,13 +57,15 @@ proc populate*(purchasing: Purchasing, request: StorageRequest): StorageRequest
|
||||||
var id = result.nonce.toArray
|
var id = result.nonce.toArray
|
||||||
doAssert randomBytes(id) == 32
|
doAssert randomBytes(id) == 32
|
||||||
result.nonce = Nonce(id)
|
result.nonce = Nonce(id)
|
||||||
|
result.client = await purchasing.market.getSigner()
|
||||||
|
|
||||||
proc purchase*(purchasing: Purchasing, request: StorageRequest): Purchase =
|
proc purchase*(purchasing: Purchasing,
|
||||||
let request = purchasing.populate(request)
|
request: StorageRequest): Future[Purchase] {.async.} =
|
||||||
let purchase = newPurchase(request, purchasing.market, purchasing.clock)
|
let request = await purchasing.populate(request)
|
||||||
|
let purchase = Purchase.new(request, purchasing.market, purchasing.clock)
|
||||||
purchase.start()
|
purchase.start()
|
||||||
purchasing.purchases[purchase.id] = purchase
|
purchasing.purchases[purchase.id] = purchase
|
||||||
purchase
|
return purchase
|
||||||
|
|
||||||
func getPurchase*(purchasing: Purchasing, id: PurchaseId): ?Purchase =
|
func getPurchase*(purchasing: Purchasing, id: PurchaseId): ?Purchase =
|
||||||
if purchasing.purchases.hasKey(id):
|
if purchasing.purchases.hasKey(id):
|
||||||
|
|
|
@ -1,37 +1,59 @@
|
||||||
import ./statemachine
|
import ./statemachine
|
||||||
import ./states/pending
|
import ./states/pending
|
||||||
|
import ./states/unknown
|
||||||
import ./purchaseid
|
import ./purchaseid
|
||||||
|
|
||||||
# Purchase is implemented as a state machine:
|
# Purchase is implemented as a state machine.
|
||||||
#
|
#
|
||||||
# pending ----> submitted ----------> started
|
# It can either be a new (pending) purchase that still needs to be submitted
|
||||||
# \ \ \
|
# on-chain, or it is a purchase that was previously submitted on-chain, and
|
||||||
# \ \ -----------> cancelled
|
# we're just restoring its (unknown) state after a node restart.
|
||||||
# \ \ \
|
|
||||||
# --------------------------------------> error
|
|
||||||
#
|
#
|
||||||
|
# |
|
||||||
|
# v
|
||||||
|
# ------------------------- unknown
|
||||||
|
# | / /
|
||||||
|
# v v /
|
||||||
|
# pending ----> submitted ----> started ---------> finished <----/
|
||||||
|
# \ \ /
|
||||||
|
# \ ------------> failed <----/
|
||||||
|
# \ /
|
||||||
|
# --> cancelled <-----------------------
|
||||||
|
|
||||||
export Purchase
|
export Purchase
|
||||||
export purchaseid
|
export purchaseid
|
||||||
|
export statemachine
|
||||||
|
|
||||||
func newPurchase*(request: StorageRequest,
|
func new*(_: type Purchase,
|
||||||
market: Market,
|
requestId: RequestId,
|
||||||
clock: Clock): Purchase =
|
market: Market,
|
||||||
|
clock: Clock): Purchase =
|
||||||
Purchase(
|
Purchase(
|
||||||
future: Future[void].new(),
|
future: Future[void].new(),
|
||||||
request: request,
|
requestId: requestId,
|
||||||
market: market,
|
market: market,
|
||||||
clock: clock
|
clock: clock
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func new*(_: type Purchase,
|
||||||
|
request: StorageRequest,
|
||||||
|
market: Market,
|
||||||
|
clock: Clock): Purchase =
|
||||||
|
let purchase = Purchase.new(request.id, market, clock)
|
||||||
|
purchase.request = some request
|
||||||
|
return purchase
|
||||||
|
|
||||||
proc start*(purchase: Purchase) =
|
proc start*(purchase: Purchase) =
|
||||||
purchase.switch(PurchasePending())
|
purchase.switch(PurchasePending())
|
||||||
|
|
||||||
|
proc load*(purchase: Purchase) =
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
|
||||||
proc wait*(purchase: Purchase) {.async.} =
|
proc wait*(purchase: Purchase) {.async.} =
|
||||||
await purchase.future
|
await purchase.future
|
||||||
|
|
||||||
func id*(purchase: Purchase): PurchaseId =
|
func id*(purchase: Purchase): PurchaseId =
|
||||||
PurchaseId(purchase.request.id)
|
PurchaseId(purchase.requestId)
|
||||||
|
|
||||||
func finished*(purchase: Purchase): bool =
|
func finished*(purchase: Purchase): bool =
|
||||||
purchase.future.finished
|
purchase.future.finished
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import ../utils/statemachine
|
import ../utils/statemachine
|
||||||
import ../market
|
import ../market
|
||||||
import ../clock
|
import ../clock
|
||||||
|
import ../errors
|
||||||
|
|
||||||
export market
|
export market
|
||||||
export clock
|
export clock
|
||||||
|
@ -11,5 +12,10 @@ type
|
||||||
future*: Future[void]
|
future*: Future[void]
|
||||||
market*: Market
|
market*: Market
|
||||||
clock*: Clock
|
clock*: Clock
|
||||||
request*: StorageRequest
|
requestId*: RequestId
|
||||||
|
request*: ?StorageRequest
|
||||||
PurchaseState* = ref object of AsyncState
|
PurchaseState* = ref object of AsyncState
|
||||||
|
PurchaseError* = object of CodexError
|
||||||
|
|
||||||
|
method description*(state: PurchaseState): string {.base.} =
|
||||||
|
raiseAssert "description not implemented for state"
|
||||||
|
|
|
@ -8,10 +8,13 @@ method enterAsync*(state: PurchaseCancelled) {.async.} =
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await purchase.market.withdrawFunds(purchase.request.id)
|
await purchase.market.withdrawFunds(purchase.requestId)
|
||||||
except CatchableError as error:
|
except CatchableError as error:
|
||||||
state.switch(PurchaseError(error: error))
|
state.switch(PurchaseErrored(error: error))
|
||||||
return
|
return
|
||||||
|
|
||||||
let error = newException(Timeout, "Purchase cancelled due to timeout")
|
let error = newException(Timeout, "Purchase cancelled due to timeout")
|
||||||
state.switch(PurchaseError(error: error))
|
state.switch(PurchaseErrored(error: error))
|
||||||
|
|
||||||
|
method description*(state: PurchaseCancelled): string =
|
||||||
|
"cancelled"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
|
||||||
type PurchaseError* = ref object of PurchaseState
|
type PurchaseErrored* = ref object of PurchaseState
|
||||||
error*: ref CatchableError
|
error*: ref CatchableError
|
||||||
|
|
||||||
method enter*(state: PurchaseError) =
|
method enter*(state: PurchaseErrored) =
|
||||||
without purchase =? (state.context as Purchase):
|
without purchase =? (state.context as Purchase):
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
purchase.future.fail(state.error)
|
purchase.future.fail(state.error)
|
||||||
|
|
||||||
|
method description*(state: PurchaseErrored): string =
|
||||||
|
"errored"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import ../statemachine
|
||||||
|
import ./error
|
||||||
|
|
||||||
|
type
|
||||||
|
PurchaseFailed* = ref object of PurchaseState
|
||||||
|
|
||||||
|
method enter*(state: PurchaseFailed) =
|
||||||
|
let error = newException(PurchaseError, "Purchase failed")
|
||||||
|
state.switch(PurchaseErrored(error: error))
|
||||||
|
|
||||||
|
method description*(state: PurchaseFailed): string =
|
||||||
|
"failed"
|
|
@ -0,0 +1,12 @@
|
||||||
|
import ../statemachine
|
||||||
|
|
||||||
|
type PurchaseFinished* = ref object of PurchaseState
|
||||||
|
|
||||||
|
method enter*(state: PurchaseFinished) =
|
||||||
|
without purchase =? (state.context as Purchase):
|
||||||
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
|
purchase.future.complete()
|
||||||
|
|
||||||
|
method description*(state: PurchaseFinished): string =
|
||||||
|
"finished"
|
|
@ -5,13 +5,17 @@ import ./error
|
||||||
type PurchasePending* = ref object of PurchaseState
|
type PurchasePending* = ref object of PurchaseState
|
||||||
|
|
||||||
method enterAsync(state: PurchasePending) {.async.} =
|
method enterAsync(state: PurchasePending) {.async.} =
|
||||||
without purchase =? (state.context as Purchase):
|
without purchase =? (state.context as Purchase) and
|
||||||
|
request =? purchase.request:
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
purchase.request = await purchase.market.requestStorage(purchase.request)
|
await purchase.market.requestStorage(request)
|
||||||
except CatchableError as error:
|
except CatchableError as error:
|
||||||
state.switch(PurchaseError(error: error))
|
state.switch(PurchaseErrored(error: error))
|
||||||
return
|
return
|
||||||
|
|
||||||
state.switch(PurchaseSubmitted())
|
state.switch(PurchaseSubmitted())
|
||||||
|
|
||||||
|
method description*(state: PurchasePending): string =
|
||||||
|
"pending"
|
||||||
|
|
|
@ -1,9 +1,32 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./error
|
||||||
|
import ./finished
|
||||||
|
import ./failed
|
||||||
|
|
||||||
type PurchaseStarted* = ref object of PurchaseState
|
type PurchaseStarted* = ref object of PurchaseState
|
||||||
|
|
||||||
method enter*(state: PurchaseStarted) =
|
method enterAsync*(state: PurchaseStarted) {.async.} =
|
||||||
without purchase =? (state.context as Purchase):
|
without purchase =? (state.context as Purchase):
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
purchase.future.complete()
|
let clock = purchase.clock
|
||||||
|
let market = purchase.market
|
||||||
|
|
||||||
|
let failed = newFuture[void]()
|
||||||
|
proc callback(_: RequestId) =
|
||||||
|
failed.complete()
|
||||||
|
let subscription = await market.subscribeRequestFailed(purchase.requestId, callback)
|
||||||
|
|
||||||
|
let ended = clock.waitUntil(await market.getRequestEnd(purchase.requestId))
|
||||||
|
try:
|
||||||
|
let fut = await one(ended, failed)
|
||||||
|
if fut.id == failed.id:
|
||||||
|
state.switch(PurchaseFailed())
|
||||||
|
else:
|
||||||
|
state.switch(PurchaseFinished())
|
||||||
|
await subscription.unsubscribe()
|
||||||
|
except CatchableError as error:
|
||||||
|
state.switch(PurchaseErrored(error: error))
|
||||||
|
|
||||||
|
method description*(state: PurchaseStarted): string =
|
||||||
|
"started"
|
||||||
|
|
|
@ -6,7 +6,8 @@ import ./cancelled
|
||||||
type PurchaseSubmitted* = ref object of PurchaseState
|
type PurchaseSubmitted* = ref object of PurchaseState
|
||||||
|
|
||||||
method enterAsync(state: PurchaseSubmitted) {.async.} =
|
method enterAsync(state: PurchaseSubmitted) {.async.} =
|
||||||
without purchase =? (state.context as Purchase):
|
without purchase =? (state.context as Purchase) and
|
||||||
|
request =? purchase.request:
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
let market = purchase.market
|
let market = purchase.market
|
||||||
|
@ -16,13 +17,12 @@ method enterAsync(state: PurchaseSubmitted) {.async.} =
|
||||||
let done = newFuture[void]()
|
let done = newFuture[void]()
|
||||||
proc callback(_: RequestId) =
|
proc callback(_: RequestId) =
|
||||||
done.complete()
|
done.complete()
|
||||||
let request = purchase.request
|
|
||||||
let subscription = await market.subscribeFulfillment(request.id, callback)
|
let subscription = await market.subscribeFulfillment(request.id, callback)
|
||||||
await done
|
await done
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
proc withTimeout(future: Future[void]) {.async.} =
|
proc withTimeout(future: Future[void]) {.async.} =
|
||||||
let expiry = purchase.request.expiry.truncate(int64)
|
let expiry = request.expiry.truncate(int64)
|
||||||
await future.withTimeout(clock, expiry)
|
await future.withTimeout(clock, expiry)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -31,7 +31,10 @@ method enterAsync(state: PurchaseSubmitted) {.async.} =
|
||||||
state.switch(PurchaseCancelled())
|
state.switch(PurchaseCancelled())
|
||||||
return
|
return
|
||||||
except CatchableError as error:
|
except CatchableError as error:
|
||||||
state.switch(PurchaseError(error: error))
|
state.switch(PurchaseErrored(error: error))
|
||||||
return
|
return
|
||||||
|
|
||||||
state.switch(PurchaseStarted())
|
state.switch(PurchaseStarted())
|
||||||
|
|
||||||
|
method description*(state: PurchaseSubmitted): string =
|
||||||
|
"submitted"
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import ../statemachine
|
||||||
|
import ./submitted
|
||||||
|
import ./started
|
||||||
|
import ./cancelled
|
||||||
|
import ./finished
|
||||||
|
import ./failed
|
||||||
|
import ./error
|
||||||
|
|
||||||
|
type PurchaseUnknown* = ref object of PurchaseState
|
||||||
|
|
||||||
|
method enterAsync(state: PurchaseUnknown) {.async.} =
|
||||||
|
without purchase =? (state.context as Purchase):
|
||||||
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if (request =? await purchase.market.getRequest(purchase.requestId)) and
|
||||||
|
(requestState =? await purchase.market.getState(purchase.requestId)):
|
||||||
|
|
||||||
|
purchase.request = some request
|
||||||
|
|
||||||
|
case requestState
|
||||||
|
of RequestState.New:
|
||||||
|
state.switch(PurchaseSubmitted())
|
||||||
|
of RequestState.Started:
|
||||||
|
state.switch(PurchaseStarted())
|
||||||
|
of RequestState.Cancelled:
|
||||||
|
state.switch(PurchaseCancelled())
|
||||||
|
of RequestState.Finished:
|
||||||
|
state.switch(PurchaseFinished())
|
||||||
|
of RequestState.Failed:
|
||||||
|
state.switch(PurchaseFailed())
|
||||||
|
|
||||||
|
except CatchableError as error:
|
||||||
|
state.switch(PurchaseErrored(error: error))
|
||||||
|
|
||||||
|
method description*(state: PurchaseUnknown): string =
|
||||||
|
"unknown"
|
|
@ -50,7 +50,7 @@ func `%`*(id: RequestId | SlotId | Nonce): JsonNode =
|
||||||
|
|
||||||
func `%`*(purchase: Purchase): JsonNode =
|
func `%`*(purchase: Purchase): JsonNode =
|
||||||
%*{
|
%*{
|
||||||
"finished": purchase.finished,
|
"state": (purchase.state as PurchaseState).?description |? "none",
|
||||||
"error": purchase.error.?msg,
|
"error": purchase.error.?msg,
|
||||||
"request": purchase.request,
|
"request": purchase.request,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import std/sequtils
|
import std/sequtils
|
||||||
|
import std/tables
|
||||||
|
import std/hashes
|
||||||
import pkg/codex/market
|
import pkg/codex/market
|
||||||
|
|
||||||
export market
|
export market
|
||||||
|
export tables
|
||||||
|
|
||||||
type
|
type
|
||||||
MockMarket* = ref object of Market
|
MockMarket* = ref object of Market
|
||||||
|
activeRequests*: Table[Address, seq[RequestId]]
|
||||||
requested*: seq[StorageRequest]
|
requested*: seq[StorageRequest]
|
||||||
|
requestEnds*: Table[RequestId, SecondsSince1970]
|
||||||
|
state*: Table[RequestId, RequestState]
|
||||||
fulfilled*: seq[Fulfillment]
|
fulfilled*: seq[Fulfillment]
|
||||||
filled*: seq[Slot]
|
filled*: seq[Slot]
|
||||||
|
withdrawn*: seq[RequestId]
|
||||||
signer: Address
|
signer: Address
|
||||||
subscriptions: Subscriptions
|
subscriptions: Subscriptions
|
||||||
Fulfillment* = object
|
Fulfillment* = object
|
||||||
|
@ -24,6 +31,7 @@ type
|
||||||
onFulfillment: seq[FulfillmentSubscription]
|
onFulfillment: seq[FulfillmentSubscription]
|
||||||
onSlotFilled: seq[SlotFilledSubscription]
|
onSlotFilled: seq[SlotFilledSubscription]
|
||||||
onRequestCancelled: seq[RequestCancelledSubscription]
|
onRequestCancelled: seq[RequestCancelledSubscription]
|
||||||
|
onRequestFailed: seq[RequestFailedSubscription]
|
||||||
RequestSubscription* = ref object of Subscription
|
RequestSubscription* = ref object of Subscription
|
||||||
market: MockMarket
|
market: MockMarket
|
||||||
callback: OnRequest
|
callback: OnRequest
|
||||||
|
@ -40,6 +48,16 @@ type
|
||||||
market: MockMarket
|
market: MockMarket
|
||||||
requestId: RequestId
|
requestId: RequestId
|
||||||
callback: OnRequestCancelled
|
callback: OnRequestCancelled
|
||||||
|
RequestFailedSubscription* = ref object of Subscription
|
||||||
|
market: MockMarket
|
||||||
|
requestId: RequestId
|
||||||
|
callback: OnRequestCancelled
|
||||||
|
|
||||||
|
proc hash*(address: Address): Hash =
|
||||||
|
hash(address.toArray)
|
||||||
|
|
||||||
|
proc hash*(requestId: RequestId): Hash =
|
||||||
|
hash(requestId.toArray)
|
||||||
|
|
||||||
proc new*(_: type MockMarket): MockMarket =
|
proc new*(_: type MockMarket): MockMarket =
|
||||||
MockMarket(signer: Address.example)
|
MockMarket(signer: Address.example)
|
||||||
|
@ -47,14 +65,14 @@ proc new*(_: type MockMarket): MockMarket =
|
||||||
method getSigner*(market: MockMarket): Future[Address] {.async.} =
|
method getSigner*(market: MockMarket): Future[Address] {.async.} =
|
||||||
return market.signer
|
return market.signer
|
||||||
|
|
||||||
method requestStorage*(market: MockMarket,
|
method requestStorage*(market: MockMarket, request: StorageRequest) {.async.} =
|
||||||
request: StorageRequest):
|
|
||||||
Future[StorageRequest] {.async.} =
|
|
||||||
market.requested.add(request)
|
market.requested.add(request)
|
||||||
var subscriptions = market.subscriptions.onRequest
|
var subscriptions = market.subscriptions.onRequest
|
||||||
for subscription in subscriptions:
|
for subscription in subscriptions:
|
||||||
subscription.callback(request.id, request.ask)
|
subscription.callback(request.id, request.ask)
|
||||||
return request
|
|
||||||
|
method myRequests*(market: MockMarket): Future[seq[RequestId]] {.async.} =
|
||||||
|
return market.activeRequests[market.signer]
|
||||||
|
|
||||||
method getRequest(market: MockMarket,
|
method getRequest(market: MockMarket,
|
||||||
id: RequestId): Future[?StorageRequest] {.async.} =
|
id: RequestId): Future[?StorageRequest] {.async.} =
|
||||||
|
@ -63,6 +81,14 @@ method getRequest(market: MockMarket,
|
||||||
return some request
|
return some request
|
||||||
return none StorageRequest
|
return none StorageRequest
|
||||||
|
|
||||||
|
method getState*(market: MockMarket,
|
||||||
|
requestId: RequestId): Future[?RequestState] {.async.} =
|
||||||
|
return market.state.?[requestId]
|
||||||
|
|
||||||
|
method getRequestEnd*(market: MockMarket,
|
||||||
|
id: RequestId): Future[SecondsSince1970] {.async.} =
|
||||||
|
return market.requestEnds[id]
|
||||||
|
|
||||||
method getHost(market: MockMarket,
|
method getHost(market: MockMarket,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256): Future[?Address] {.async.} =
|
slotIndex: UInt256): Future[?Address] {.async.} =
|
||||||
|
@ -93,6 +119,12 @@ proc emitRequestFulfilled*(market: MockMarket, requestId: RequestId) =
|
||||||
if subscription.requestId == requestId:
|
if subscription.requestId == requestId:
|
||||||
subscription.callback(requestId)
|
subscription.callback(requestId)
|
||||||
|
|
||||||
|
proc emitRequestFailed*(market: MockMarket, requestId: RequestId) =
|
||||||
|
var subscriptions = market.subscriptions.onRequestFailed
|
||||||
|
for subscription in subscriptions:
|
||||||
|
if subscription.requestId == requestId:
|
||||||
|
subscription.callback(requestId)
|
||||||
|
|
||||||
proc fillSlot*(market: MockMarket,
|
proc fillSlot*(market: MockMarket,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256,
|
slotIndex: UInt256,
|
||||||
|
@ -115,6 +147,7 @@ method fillSlot*(market: MockMarket,
|
||||||
|
|
||||||
method withdrawFunds*(market: MockMarket,
|
method withdrawFunds*(market: MockMarket,
|
||||||
requestId: RequestId) {.async.} =
|
requestId: RequestId) {.async.} =
|
||||||
|
market.withdrawn.add(requestId)
|
||||||
market.emitRequestCancelled(requestId)
|
market.emitRequestCancelled(requestId)
|
||||||
|
|
||||||
method subscribeRequests*(market: MockMarket,
|
method subscribeRequests*(market: MockMarket,
|
||||||
|
@ -165,6 +198,18 @@ method subscribeRequestCancelled*(market: MockMarket,
|
||||||
market.subscriptions.onRequestCancelled.add(subscription)
|
market.subscriptions.onRequestCancelled.add(subscription)
|
||||||
return subscription
|
return subscription
|
||||||
|
|
||||||
|
method subscribeRequestFailed*(market: MockMarket,
|
||||||
|
requestId: RequestId,
|
||||||
|
callback: OnRequestFailed):
|
||||||
|
Future[Subscription] {.async.} =
|
||||||
|
let subscription = RequestFailedSubscription(
|
||||||
|
market: market,
|
||||||
|
requestId: requestId,
|
||||||
|
callback: callback
|
||||||
|
)
|
||||||
|
market.subscriptions.onRequestFailed.add(subscription)
|
||||||
|
return subscription
|
||||||
|
|
||||||
method unsubscribe*(subscription: RequestSubscription) {.async.} =
|
method unsubscribe*(subscription: RequestSubscription) {.async.} =
|
||||||
subscription.market.subscriptions.onRequest.keepItIf(it != subscription)
|
subscription.market.subscriptions.onRequest.keepItIf(it != subscription)
|
||||||
|
|
||||||
|
@ -176,3 +221,6 @@ method unsubscribe*(subscription: SlotFilledSubscription) {.async.} =
|
||||||
|
|
||||||
method unsubscribe*(subscription: RequestCancelledSubscription) {.async.} =
|
method unsubscribe*(subscription: RequestCancelledSubscription) {.async.} =
|
||||||
subscription.market.subscriptions.onRequestCancelled.keepItIf(it != subscription)
|
subscription.market.subscriptions.onRequestCancelled.keepItIf(it != subscription)
|
||||||
|
|
||||||
|
method unsubscribe*(subscription: RequestFailedSubscription) {.async.} =
|
||||||
|
subscription.market.subscriptions.onRequestFailed.keepItIf(it != subscription)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import pkg/chronos
|
||||||
import pkg/codex/proving
|
import pkg/codex/proving
|
||||||
import ./helpers/mockproofs
|
import ./helpers/mockproofs
|
||||||
import ./helpers/mockclock
|
import ./helpers/mockclock
|
||||||
|
import ./helpers/eventually
|
||||||
import ./examples
|
import ./examples
|
||||||
|
|
||||||
suite "Proving":
|
suite "Proving":
|
||||||
|
@ -23,7 +24,6 @@ suite "Proving":
|
||||||
proc advanceToNextPeriod(proofs: MockProofs) {.async.} =
|
proc advanceToNextPeriod(proofs: MockProofs) {.async.} =
|
||||||
let periodicity = await proofs.periodicity()
|
let periodicity = await proofs.periodicity()
|
||||||
clock.advance(periodicity.seconds.truncate(int64))
|
clock.advance(periodicity.seconds.truncate(int64))
|
||||||
await sleepAsync(2.seconds)
|
|
||||||
|
|
||||||
test "maintains a list of contract ids to watch":
|
test "maintains a list of contract ids to watch":
|
||||||
let id1, id2 = SlotId.example
|
let id1, id2 = SlotId.example
|
||||||
|
@ -49,7 +49,7 @@ suite "Proving":
|
||||||
proving.onProofRequired = onProofRequired
|
proving.onProofRequired = onProofRequired
|
||||||
proofs.setProofRequired(id, true)
|
proofs.setProofRequired(id, true)
|
||||||
await proofs.advanceToNextPeriod()
|
await proofs.advanceToNextPeriod()
|
||||||
check called
|
check eventually called
|
||||||
|
|
||||||
test "callback receives id of contract for which proof is required":
|
test "callback receives id of contract for which proof is required":
|
||||||
let id1, id2 = SlotId.example
|
let id1, id2 = SlotId.example
|
||||||
|
@ -61,11 +61,11 @@ suite "Proving":
|
||||||
proving.onProofRequired = onProofRequired
|
proving.onProofRequired = onProofRequired
|
||||||
proofs.setProofRequired(id1, true)
|
proofs.setProofRequired(id1, true)
|
||||||
await proofs.advanceToNextPeriod()
|
await proofs.advanceToNextPeriod()
|
||||||
check callbackIds == @[id1]
|
check eventually callbackIds == @[id1]
|
||||||
proofs.setProofRequired(id1, false)
|
proofs.setProofRequired(id1, false)
|
||||||
proofs.setProofRequired(id2, true)
|
proofs.setProofRequired(id2, true)
|
||||||
await proofs.advanceToNextPeriod()
|
await proofs.advanceToNextPeriod()
|
||||||
check callbackIds == @[id1, id2]
|
check eventually callbackIds == @[id1, id2]
|
||||||
|
|
||||||
test "invokes callback when proof is about to be required":
|
test "invokes callback when proof is about to be required":
|
||||||
let id = SlotId.example
|
let id = SlotId.example
|
||||||
|
@ -77,7 +77,7 @@ suite "Proving":
|
||||||
proofs.setProofRequired(id, false)
|
proofs.setProofRequired(id, false)
|
||||||
proofs.setProofToBeRequired(id, true)
|
proofs.setProofToBeRequired(id, true)
|
||||||
await proofs.advanceToNextPeriod()
|
await proofs.advanceToNextPeriod()
|
||||||
check called
|
check eventually called
|
||||||
|
|
||||||
test "stops watching when contract has ended":
|
test "stops watching when contract has ended":
|
||||||
let id = SlotId.example
|
let id = SlotId.example
|
||||||
|
@ -90,17 +90,17 @@ suite "Proving":
|
||||||
proving.onProofRequired = onProofRequired
|
proving.onProofRequired = onProofRequired
|
||||||
proofs.setProofRequired(id, true)
|
proofs.setProofRequired(id, true)
|
||||||
await proofs.advanceToNextPeriod()
|
await proofs.advanceToNextPeriod()
|
||||||
check not proving.slots.contains(id)
|
check eventually (not proving.slots.contains(id))
|
||||||
check not called
|
check not called
|
||||||
|
|
||||||
test "submits proofs":
|
test "submits proofs":
|
||||||
let id = SlotId.example
|
let id = SlotId.example
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
await proving.submitProof(id, proof)
|
await proving.submitProof(id, proof)
|
||||||
|
|
||||||
test "supports proof submission subscriptions":
|
test "supports proof submission subscriptions":
|
||||||
let id = SlotId.example
|
let id = SlotId.example
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
|
|
||||||
var receivedIds: seq[SlotId]
|
var receivedIds: seq[SlotId]
|
||||||
var receivedProofs: seq[seq[byte]]
|
var receivedProofs: seq[seq[byte]]
|
||||||
|
|
|
@ -4,8 +4,10 @@ import pkg/chronos
|
||||||
import pkg/upraises
|
import pkg/upraises
|
||||||
import pkg/stint
|
import pkg/stint
|
||||||
import pkg/codex/purchasing
|
import pkg/codex/purchasing
|
||||||
|
import pkg/codex/purchasing/states/[finished, failed, error, started, submitted, unknown]
|
||||||
import ./helpers/mockmarket
|
import ./helpers/mockmarket
|
||||||
import ./helpers/mockclock
|
import ./helpers/mockclock
|
||||||
|
import ./helpers/eventually
|
||||||
import ./examples
|
import ./examples
|
||||||
|
|
||||||
suite "Purchasing":
|
suite "Purchasing":
|
||||||
|
@ -29,7 +31,7 @@ suite "Purchasing":
|
||||||
)
|
)
|
||||||
|
|
||||||
test "submits a storage request when asked":
|
test "submits a storage request when asked":
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
let submitted = market.requested[0]
|
let submitted = market.requested[0]
|
||||||
check submitted.ask.slots == request.ask.slots
|
check submitted.ask.slots == request.ask.slots
|
||||||
check submitted.ask.slotSize == request.ask.slotSize
|
check submitted.ask.slotSize == request.ask.slotSize
|
||||||
|
@ -37,8 +39,8 @@ suite "Purchasing":
|
||||||
check submitted.ask.reward == request.ask.reward
|
check submitted.ask.reward == request.ask.reward
|
||||||
|
|
||||||
test "remembers purchases":
|
test "remembers purchases":
|
||||||
let purchase1 = purchasing.purchase(request)
|
let purchase1 = await purchasing.purchase(request)
|
||||||
let purchase2 = purchasing.purchase(request)
|
let purchase2 = await purchasing.purchase(request)
|
||||||
check purchasing.getPurchase(purchase1.id) == some purchase1
|
check purchasing.getPurchase(purchase1.id) == some purchase1
|
||||||
check purchasing.getPurchase(purchase2.id) == some purchase2
|
check purchasing.getPurchase(purchase2.id) == some purchase2
|
||||||
|
|
||||||
|
@ -47,12 +49,12 @@ suite "Purchasing":
|
||||||
|
|
||||||
test "can change default value for proof probability":
|
test "can change default value for proof probability":
|
||||||
purchasing.proofProbability = 42.u256
|
purchasing.proofProbability = 42.u256
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
check market.requested[0].ask.proofProbability == 42.u256
|
check market.requested[0].ask.proofProbability == 42.u256
|
||||||
|
|
||||||
test "can override proof probability per request":
|
test "can override proof probability per request":
|
||||||
request.ask.proofProbability = 42.u256
|
request.ask.proofProbability = 42.u256
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
check market.requested[0].ask.proofProbability == 42.u256
|
check market.requested[0].ask.proofProbability == 42.u256
|
||||||
|
|
||||||
test "has a default value for request expiration interval":
|
test "has a default value for request expiration interval":
|
||||||
|
@ -61,53 +63,178 @@ suite "Purchasing":
|
||||||
test "can change default value for request expiration interval":
|
test "can change default value for request expiration interval":
|
||||||
purchasing.requestExpiryInterval = 42.u256
|
purchasing.requestExpiryInterval = 42.u256
|
||||||
let start = getTime().toUnix()
|
let start = getTime().toUnix()
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
check market.requested[0].expiry == (start + 42).u256
|
check market.requested[0].expiry == (start + 42).u256
|
||||||
|
|
||||||
test "can override expiry time per request":
|
test "can override expiry time per request":
|
||||||
let expiry = (getTime().toUnix() + 42).u256
|
let expiry = (getTime().toUnix() + 42).u256
|
||||||
request.expiry = expiry
|
request.expiry = expiry
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
check market.requested[0].expiry == expiry
|
check market.requested[0].expiry == expiry
|
||||||
|
|
||||||
test "includes a random nonce in every storage request":
|
test "includes a random nonce in every storage request":
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
discard purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
check market.requested[0].nonce != market.requested[1].nonce
|
check market.requested[0].nonce != market.requested[1].nonce
|
||||||
|
|
||||||
test "succeeds when request is fulfilled":
|
test "sets client address in request":
|
||||||
let purchase = purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check market.requested[0].client == await market.getSigner()
|
||||||
|
|
||||||
|
test "succeeds when request is finished":
|
||||||
|
let purchase = await purchasing.purchase(request)
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
|
let requestEnd = getTime().toUnix() + 42
|
||||||
|
market.requestEnds[request.id] = requestEnd
|
||||||
market.emitRequestFulfilled(request.id)
|
market.emitRequestFulfilled(request.id)
|
||||||
|
clock.set(requestEnd)
|
||||||
await purchase.wait()
|
await purchase.wait()
|
||||||
check purchase.error.isNone
|
check purchase.error.isNone
|
||||||
|
|
||||||
test "fails when request times out":
|
test "fails when request times out":
|
||||||
let purchase = purchasing.purchase(request)
|
let purchase = await purchasing.purchase(request)
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
clock.set(request.expiry.truncate(int64))
|
clock.set(request.expiry.truncate(int64))
|
||||||
expect PurchaseTimeout:
|
expect PurchaseTimeout:
|
||||||
await purchase.wait()
|
await purchase.wait()
|
||||||
|
|
||||||
test "checks that funds were withdrawn when purchase times out":
|
test "checks that funds were withdrawn when purchase times out":
|
||||||
let purchase = purchasing.purchase(request)
|
let purchase = await purchasing.purchase(request)
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
var receivedIds: seq[RequestId]
|
|
||||||
clock.set(request.expiry.truncate(int64))
|
clock.set(request.expiry.truncate(int64))
|
||||||
|
expect PurchaseTimeout:
|
||||||
proc onRequestCancelled(id: RequestId) {.gcsafe, upraises:[].} =
|
|
||||||
receivedIds.add(id)
|
|
||||||
|
|
||||||
# will only be fired when `withdrawFunds` is called on purchase timeout
|
|
||||||
let subscription = await market.subscribeRequestCancelled(
|
|
||||||
request.id,
|
|
||||||
onRequestCancelled)
|
|
||||||
var purchaseTimedOut = false
|
|
||||||
try:
|
|
||||||
await purchase.wait()
|
await purchase.wait()
|
||||||
except PurchaseTimeout:
|
check market.withdrawn == @[request.id]
|
||||||
purchaseTimedOut = true
|
|
||||||
|
|
||||||
await subscription.unsubscribe()
|
suite "Purchasing state machine":
|
||||||
check purchaseTimedOut
|
|
||||||
check receivedIds == @[request.id]
|
var purchasing: Purchasing
|
||||||
|
var market: MockMarket
|
||||||
|
var clock: MockClock
|
||||||
|
var request: StorageRequest
|
||||||
|
|
||||||
|
setup:
|
||||||
|
market = MockMarket.new()
|
||||||
|
clock = MockClock.new()
|
||||||
|
purchasing = Purchasing.new(market, clock)
|
||||||
|
request = StorageRequest(
|
||||||
|
ask: StorageAsk(
|
||||||
|
slots: uint8.example.uint64,
|
||||||
|
slotSize: uint32.example.u256,
|
||||||
|
duration: uint16.example.u256,
|
||||||
|
reward: uint8.example.u256
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
test "loads active purchases from market":
|
||||||
|
let me = await market.getSigner()
|
||||||
|
let request1, request2, request3 = StorageRequest.example
|
||||||
|
market.requested = @[request1, request2, request3]
|
||||||
|
market.activeRequests[me] = @[request1.id, request2.id]
|
||||||
|
await purchasing.load()
|
||||||
|
check isSome purchasing.getPurchase(PurchaseId(request1.id))
|
||||||
|
check isSome purchasing.getPurchase(PurchaseId(request2.id))
|
||||||
|
check isNone purchasing.getPurchase(PurchaseId(request3.id))
|
||||||
|
|
||||||
|
test "loads correct purchase.future state for purchases from market":
|
||||||
|
let me = await market.getSigner()
|
||||||
|
let request1, request2, request3, request4, request5 = StorageRequest.example
|
||||||
|
market.requested = @[request1, request2, request3, request4, request5]
|
||||||
|
market.activeRequests[me] = @[request1.id, request2.id, request3.id, request4.id, request5.id]
|
||||||
|
market.state[request1.id] = RequestState.New
|
||||||
|
market.state[request2.id] = RequestState.Started
|
||||||
|
market.state[request3.id] = RequestState.Cancelled
|
||||||
|
market.state[request4.id] = RequestState.Finished
|
||||||
|
market.state[request5.id] = RequestState.Failed
|
||||||
|
|
||||||
|
# ensure the started state doesn't error, giving a false positive test result
|
||||||
|
market.requestEnds[request2.id] = clock.now() - 1
|
||||||
|
|
||||||
|
await purchasing.load()
|
||||||
|
check purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some
|
||||||
|
check purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some
|
||||||
|
check purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some
|
||||||
|
check purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some
|
||||||
|
check purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some
|
||||||
|
check purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome
|
||||||
|
|
||||||
|
test "moves to PurchaseSubmitted when request state is New":
|
||||||
|
let request = StorageRequest.example
|
||||||
|
let purchase = Purchase.new(request, market, clock)
|
||||||
|
market.requested = @[request]
|
||||||
|
market.state[request.id] = RequestState.New
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
check (purchase.state as PurchaseSubmitted).isSome
|
||||||
|
|
||||||
|
test "moves to PurchaseStarted when request state is Started":
|
||||||
|
let request = StorageRequest.example
|
||||||
|
let purchase = Purchase.new(request, market, clock)
|
||||||
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
|
market.requested = @[request]
|
||||||
|
market.state[request.id] = RequestState.Started
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
check (purchase.state as PurchaseStarted).isSome
|
||||||
|
|
||||||
|
test "moves to PurchaseErrored when request state is Cancelled":
|
||||||
|
let request = StorageRequest.example
|
||||||
|
let purchase = Purchase.new(request, market, clock)
|
||||||
|
market.requested = @[request]
|
||||||
|
market.state[request.id] = RequestState.Cancelled
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
check (purchase.state as PurchaseErrored).isSome
|
||||||
|
check purchase.error.?msg == "Purchase cancelled due to timeout".some
|
||||||
|
|
||||||
|
test "moves to PurchaseFinished when request state is Finished":
|
||||||
|
let request = StorageRequest.example
|
||||||
|
let purchase = Purchase.new(request, market, clock)
|
||||||
|
market.requested = @[request]
|
||||||
|
market.state[request.id] = RequestState.Finished
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
check (purchase.state as PurchaseFinished).isSome
|
||||||
|
|
||||||
|
test "moves to PurchaseErrored when request state is Failed":
|
||||||
|
let request = StorageRequest.example
|
||||||
|
let purchase = Purchase.new(request, market, clock)
|
||||||
|
market.requested = @[request]
|
||||||
|
market.state[request.id] = RequestState.Failed
|
||||||
|
purchase.switch(PurchaseUnknown())
|
||||||
|
check (purchase.state as PurchaseErrored).isSome
|
||||||
|
check purchase.error.?msg == "Purchase failed".some
|
||||||
|
|
||||||
|
test "moves to PurchaseErrored state once RequestFailed emitted":
|
||||||
|
let me = await market.getSigner()
|
||||||
|
let request = StorageRequest.example
|
||||||
|
market.requested = @[request]
|
||||||
|
market.activeRequests[me] = @[request.id]
|
||||||
|
market.state[request.id] = RequestState.Started
|
||||||
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
|
await purchasing.load()
|
||||||
|
|
||||||
|
# emit mock contract failure event
|
||||||
|
market.emitRequestFailed(request.id)
|
||||||
|
# must allow time for the callback to trigger the completion of the future
|
||||||
|
await sleepAsync(chronos.milliseconds(10))
|
||||||
|
|
||||||
|
# now check the result
|
||||||
|
let purchase = purchasing.getPurchase(PurchaseId(request.id))
|
||||||
|
let state = purchase.?state
|
||||||
|
check (state as PurchaseErrored).isSome
|
||||||
|
check (!purchase).error.?msg == "Purchase failed".some
|
||||||
|
|
||||||
|
test "moves to PurchaseFinished state once request finishes":
|
||||||
|
let me = await market.getSigner()
|
||||||
|
let request = StorageRequest.example
|
||||||
|
market.requested = @[request]
|
||||||
|
market.activeRequests[me] = @[request.id]
|
||||||
|
market.state[request.id] = RequestState.Started
|
||||||
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
|
await purchasing.load()
|
||||||
|
|
||||||
|
# advance the clock to the end of the request
|
||||||
|
clock.advance(request.ask.duration.truncate(int64))
|
||||||
|
|
||||||
|
# now check the result
|
||||||
|
proc getState: ?PurchaseState =
|
||||||
|
purchasing.getPurchase(PurchaseId(request.id)).?state as PurchaseState
|
||||||
|
|
||||||
|
check eventually (getState() as PurchaseFinished).isSome
|
||||||
|
|
|
@ -6,6 +6,7 @@ import pkg/codex/proving
|
||||||
import pkg/codex/sales
|
import pkg/codex/sales
|
||||||
import ./helpers/mockmarket
|
import ./helpers/mockmarket
|
||||||
import ./helpers/mockclock
|
import ./helpers/mockclock
|
||||||
|
import ./helpers/eventually
|
||||||
import ./examples
|
import ./examples
|
||||||
|
|
||||||
suite "Sales":
|
suite "Sales":
|
||||||
|
@ -26,7 +27,7 @@ suite "Sales":
|
||||||
cid: "some cid"
|
cid: "some cid"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
|
|
||||||
var sales: Sales
|
var sales: Sales
|
||||||
var market: MockMarket
|
var market: MockMarket
|
||||||
|
@ -75,21 +76,21 @@ suite "Sales":
|
||||||
|
|
||||||
test "makes storage unavailable when matching request comes in":
|
test "makes storage unavailable when matching request comes in":
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check sales.available.len == 0
|
check sales.available.len == 0
|
||||||
|
|
||||||
test "ignores request when no matching storage is available":
|
test "ignores request when no matching storage is available":
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
var tooBig = request
|
var tooBig = request
|
||||||
tooBig.ask.slotSize = request.ask.slotSize + 1
|
tooBig.ask.slotSize = request.ask.slotSize + 1
|
||||||
discard await market.requestStorage(tooBig)
|
await market.requestStorage(tooBig)
|
||||||
check sales.available == @[availability]
|
check sales.available == @[availability]
|
||||||
|
|
||||||
test "ignores request when reward is too low":
|
test "ignores request when reward is too low":
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
var tooCheap = request
|
var tooCheap = request
|
||||||
tooCheap.ask.reward = request.ask.reward - 1
|
tooCheap.ask.reward = request.ask.reward - 1
|
||||||
discard await market.requestStorage(tooCheap)
|
await market.requestStorage(tooCheap)
|
||||||
check sales.available == @[availability]
|
check sales.available == @[availability]
|
||||||
|
|
||||||
test "retrieves and stores data locally":
|
test "retrieves and stores data locally":
|
||||||
|
@ -103,8 +104,8 @@ suite "Sales":
|
||||||
storingSlot = slot
|
storingSlot = slot
|
||||||
storingAvailability = availability
|
storingAvailability = availability
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
let requested = await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check storingRequest == requested
|
check storingRequest == request
|
||||||
check storingSlot < request.ask.slots.u256
|
check storingSlot < request.ask.slots.u256
|
||||||
check storingAvailability == availability
|
check storingAvailability == availability
|
||||||
|
|
||||||
|
@ -115,7 +116,7 @@ suite "Sales":
|
||||||
availability: Availability) {.async.} =
|
availability: Availability) {.async.} =
|
||||||
raise error
|
raise error
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check sales.available == @[availability]
|
check sales.available == @[availability]
|
||||||
|
|
||||||
test "generates proof of storage":
|
test "generates proof of storage":
|
||||||
|
@ -126,13 +127,13 @@ suite "Sales":
|
||||||
provingRequest = request
|
provingRequest = request
|
||||||
provingSlot = slot
|
provingSlot = slot
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
let requested = await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check provingRequest == requested
|
check provingRequest == request
|
||||||
check provingSlot < request.ask.slots.u256
|
check provingSlot < request.ask.slots.u256
|
||||||
|
|
||||||
test "fills a slot":
|
test "fills a slot":
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check market.filled.len == 1
|
check market.filled.len == 1
|
||||||
check market.filled[0].requestId == request.id
|
check market.filled[0].requestId == request.id
|
||||||
check market.filled[0].slotIndex < request.ask.slots.u256
|
check market.filled[0].slotIndex < request.ask.slots.u256
|
||||||
|
@ -150,7 +151,7 @@ suite "Sales":
|
||||||
soldRequest = request
|
soldRequest = request
|
||||||
soldSlotIndex = slotIndex
|
soldSlotIndex = slotIndex
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check soldAvailability == availability
|
check soldAvailability == availability
|
||||||
check soldRequest == request
|
check soldRequest == request
|
||||||
check soldSlotIndex < request.ask.slots.u256
|
check soldSlotIndex < request.ask.slots.u256
|
||||||
|
@ -171,7 +172,7 @@ suite "Sales":
|
||||||
clearedRequest = request
|
clearedRequest = request
|
||||||
clearedSlotIndex = slotIndex
|
clearedSlotIndex = slotIndex
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check clearedAvailability == availability
|
check clearedAvailability == availability
|
||||||
check clearedRequest == request
|
check clearedRequest == request
|
||||||
check clearedSlotIndex < request.ask.slots.u256
|
check clearedSlotIndex < request.ask.slots.u256
|
||||||
|
@ -183,7 +184,7 @@ suite "Sales":
|
||||||
availability: Availability) {.async.} =
|
availability: Availability) {.async.} =
|
||||||
await sleepAsync(1.hours)
|
await sleepAsync(1.hours)
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
for slotIndex in 0..<request.ask.slots:
|
for slotIndex in 0..<request.ask.slots:
|
||||||
market.fillSlot(request.id, slotIndex.u256, proof, otherHost)
|
market.fillSlot(request.id, slotIndex.u256, proof, otherHost)
|
||||||
check sales.available == @[availability]
|
check sales.available == @[availability]
|
||||||
|
@ -194,10 +195,9 @@ suite "Sales":
|
||||||
availability: Availability) {.async.} =
|
availability: Availability) {.async.} =
|
||||||
await sleepAsync(1.hours)
|
await sleepAsync(1.hours)
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
clock.set(request.expiry.truncate(int64))
|
clock.set(request.expiry.truncate(int64))
|
||||||
await sleepAsync(2.seconds)
|
check eventually (sales.available == @[availability])
|
||||||
check sales.available == @[availability]
|
|
||||||
|
|
||||||
test "adds proving for slot when slot is filled":
|
test "adds proving for slot when slot is filled":
|
||||||
var soldSlotIndex: UInt256
|
var soldSlotIndex: UInt256
|
||||||
|
@ -207,6 +207,6 @@ suite "Sales":
|
||||||
soldSlotIndex = slotIndex
|
soldSlotIndex = slotIndex
|
||||||
check proving.slots.len == 0
|
check proving.slots.len == 0
|
||||||
sales.add(availability)
|
sales.add(availability)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check proving.slots.len == 1
|
check proving.slots.len == 1
|
||||||
check proving.slots.contains(request.slotId(soldSlotIndex))
|
check proving.slots.contains(request.slotId(soldSlotIndex))
|
||||||
|
|
|
@ -1,36 +1,7 @@
|
||||||
import std/times
|
|
||||||
import pkg/stint
|
|
||||||
import pkg/ethers
|
import pkg/ethers
|
||||||
import codex/contracts
|
|
||||||
import ../examples
|
import ../examples
|
||||||
|
|
||||||
export examples
|
export examples
|
||||||
|
|
||||||
proc example*(_: type Address): Address =
|
proc example*(_: type Address): Address =
|
||||||
Address(array[20, byte].example)
|
Address(array[20, byte].example)
|
||||||
|
|
||||||
proc example*(_: type StorageRequest): StorageRequest =
|
|
||||||
StorageRequest(
|
|
||||||
client: Address.example,
|
|
||||||
ask: StorageAsk(
|
|
||||||
slots: 4,
|
|
||||||
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
|
|
||||||
duration: (10 * 60 * 60).u256, # 10 hours
|
|
||||||
proofProbability: 4.u256, # require a proof roughly once every 4 periods
|
|
||||||
reward: 84.u256,
|
|
||||||
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
|
|
||||||
),
|
|
||||||
content: StorageContent(
|
|
||||||
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
|
|
||||||
erasure: StorageErasure(
|
|
||||||
totalChunks: 12,
|
|
||||||
),
|
|
||||||
por: StoragePor(
|
|
||||||
u: @(array[480, byte].example),
|
|
||||||
publicKey: @(array[96, byte].example),
|
|
||||||
name: @(array[512, byte].example)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
expiry: (getTime() + initDuration(hours=1)).toUnix.u256,
|
|
||||||
nonce: Nonce.example
|
|
||||||
)
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ./examples
|
||||||
import ./time
|
import ./time
|
||||||
|
|
||||||
ethersuite "Storage contracts":
|
ethersuite "Storage contracts":
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
|
|
||||||
var client, host: Signer
|
var client, host: Signer
|
||||||
var storage: Storage
|
var storage: Storage
|
||||||
|
@ -58,6 +58,10 @@ ethersuite "Storage contracts":
|
||||||
):
|
):
|
||||||
await provider.advanceTime(periodicity.seconds)
|
await provider.advanceTime(periodicity.seconds)
|
||||||
|
|
||||||
|
proc startContract() {.async.} =
|
||||||
|
for slotIndex in 1..<request.ask.slots:
|
||||||
|
await storage.fillSlot(request.id, slotIndex.u256, proof)
|
||||||
|
|
||||||
test "accept storage proofs":
|
test "accept storage proofs":
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
await waitUntilProofRequired(slotId)
|
await waitUntilProofRequired(slotId)
|
||||||
|
@ -71,9 +75,11 @@ ethersuite "Storage contracts":
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await storage.markProofAsMissing(slotId, missingPeriod)
|
await storage.markProofAsMissing(slotId, missingPeriod)
|
||||||
|
|
||||||
test "can be payed out at the end":
|
test "can be paid out at the end":
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
await provider.advanceTimeTo(await storage.proofEnd(slotId))
|
await startContract()
|
||||||
|
let requestEnd = await storage.requestEnd(request.id)
|
||||||
|
await provider.advanceTimeTo(requestEnd.u256)
|
||||||
await storage.payoutSlot(request.id, 0.u256)
|
await storage.payoutSlot(request.id, 0.u256)
|
||||||
|
|
||||||
test "cannot mark proofs missing for cancelled request":
|
test "cannot mark proofs missing for cancelled request":
|
||||||
|
|
|
@ -3,18 +3,20 @@ import pkg/chronos
|
||||||
import pkg/ethers/testing
|
import pkg/ethers/testing
|
||||||
import codex/contracts
|
import codex/contracts
|
||||||
import codex/contracts/testtoken
|
import codex/contracts/testtoken
|
||||||
|
import codex/storageproofs
|
||||||
import ../ethertest
|
import ../ethertest
|
||||||
import ./examples
|
import ./examples
|
||||||
import ./time
|
import ./time
|
||||||
|
|
||||||
ethersuite "On-Chain Market":
|
ethersuite "On-Chain Market":
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
|
|
||||||
var market: OnChainMarket
|
var market: OnChainMarket
|
||||||
var storage: Storage
|
var storage: Storage
|
||||||
var token: TestToken
|
var token: TestToken
|
||||||
var request: StorageRequest
|
var request: StorageRequest
|
||||||
var slotIndex: UInt256
|
var slotIndex: UInt256
|
||||||
|
var periodicity: Periodicity
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
let deployment = deployment()
|
let deployment = deployment()
|
||||||
|
@ -27,12 +29,22 @@ ethersuite "On-Chain Market":
|
||||||
await storage.deposit(collateral)
|
await storage.deposit(collateral)
|
||||||
|
|
||||||
market = OnChainMarket.new(storage)
|
market = OnChainMarket.new(storage)
|
||||||
|
periodicity = Periodicity(seconds: await storage.proofPeriod())
|
||||||
|
|
||||||
request = StorageRequest.example
|
request = StorageRequest.example
|
||||||
request.client = accounts[0]
|
request.client = accounts[0]
|
||||||
|
|
||||||
slotIndex = (request.ask.slots div 2).u256
|
slotIndex = (request.ask.slots div 2).u256
|
||||||
|
|
||||||
|
proc waitUntilProofRequired(slotId: SlotId) {.async.} =
|
||||||
|
let currentPeriod = periodicity.periodOf(await provider.currentTime())
|
||||||
|
await provider.advanceTimeTo(periodicity.periodEnd(currentPeriod))
|
||||||
|
while not (
|
||||||
|
(await storage.isProofRequired(slotId)) and
|
||||||
|
(await storage.getPointer(slotId)) < 250
|
||||||
|
):
|
||||||
|
await provider.advanceTime(periodicity.seconds)
|
||||||
|
|
||||||
test "fails to instantiate when contract does not have a signer":
|
test "fails to instantiate when contract does not have a signer":
|
||||||
let storageWithoutSigner = storage.connect(provider)
|
let storageWithoutSigner = storage.connect(provider)
|
||||||
expect AssertionError:
|
expect AssertionError:
|
||||||
|
@ -43,25 +55,18 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
test "supports storage requests":
|
test "supports storage requests":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
check (await market.requestStorage(request)) == request
|
await market.requestStorage(request)
|
||||||
|
|
||||||
test "sets client address when submitting storage request":
|
|
||||||
var requestWithoutClient = request
|
|
||||||
requestWithoutClient.client = Address.default
|
|
||||||
await token.approve(storage.address, request.price)
|
|
||||||
let submitted = await market.requestStorage(requestWithoutClient)
|
|
||||||
check submitted.client == accounts[0]
|
|
||||||
|
|
||||||
test "can retrieve previously submitted requests":
|
test "can retrieve previously submitted requests":
|
||||||
check (await market.getRequest(request.id)) == none StorageRequest
|
check (await market.getRequest(request.id)) == none StorageRequest
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
let r = await market.getRequest(request.id)
|
let r = await market.getRequest(request.id)
|
||||||
check (r) == some request
|
check (r) == some request
|
||||||
|
|
||||||
test "supports withdrawing of funds":
|
test "supports withdrawing of funds":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await provider.advanceTimeTo(request.expiry)
|
await provider.advanceTimeTo(request.expiry)
|
||||||
await market.withdrawFunds(request.id)
|
await market.withdrawFunds(request.id)
|
||||||
|
|
||||||
|
@ -73,26 +78,26 @@ ethersuite "On-Chain Market":
|
||||||
receivedAsks.add(ask)
|
receivedAsks.add(ask)
|
||||||
let subscription = await market.subscribeRequests(onRequest)
|
let subscription = await market.subscribeRequests(onRequest)
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
check receivedAsks == @[request.ask]
|
check receivedAsks == @[request.ask]
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
test "supports filling of slots":
|
test "supports filling of slots":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof)
|
||||||
|
|
||||||
test "can retrieve host that filled slot":
|
test "can retrieve host that filled slot":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check (await market.getHost(request.id, slotIndex)) == none Address
|
check (await market.getHost(request.id, slotIndex)) == none Address
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof)
|
||||||
check (await market.getHost(request.id, slotIndex)) == some accounts[0]
|
check (await market.getHost(request.id, slotIndex)) == some accounts[0]
|
||||||
|
|
||||||
test "support slot filled subscriptions":
|
test "support slot filled subscriptions":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
var receivedSlotIndices: seq[UInt256]
|
var receivedSlotIndices: seq[UInt256]
|
||||||
proc onSlotFilled(id: RequestId, slotIndex: UInt256) =
|
proc onSlotFilled(id: RequestId, slotIndex: UInt256) =
|
||||||
|
@ -107,7 +112,7 @@ ethersuite "On-Chain Market":
|
||||||
test "subscribes only to a certain slot":
|
test "subscribes only to a certain slot":
|
||||||
var otherSlot = slotIndex - 1
|
var otherSlot = slotIndex - 1
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
var receivedSlotIndices: seq[UInt256]
|
var receivedSlotIndices: seq[UInt256]
|
||||||
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
|
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
|
||||||
receivedSlotIndices.add(slotIndex)
|
receivedSlotIndices.add(slotIndex)
|
||||||
|
@ -120,7 +125,7 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
test "support fulfillment subscriptions":
|
test "support fulfillment subscriptions":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
proc onFulfillment(id: RequestId) =
|
proc onFulfillment(id: RequestId) =
|
||||||
receivedIds.add(id)
|
receivedIds.add(id)
|
||||||
|
@ -135,9 +140,9 @@ ethersuite "On-Chain Market":
|
||||||
otherRequest.client = accounts[0]
|
otherRequest.client = accounts[0]
|
||||||
|
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await token.approve(storage.address, otherRequest.price)
|
await token.approve(storage.address, otherRequest.price)
|
||||||
discard await market.requestStorage(otherRequest)
|
await market.requestStorage(otherRequest)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
proc onFulfillment(id: RequestId) =
|
proc onFulfillment(id: RequestId) =
|
||||||
|
@ -156,7 +161,7 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
test "support request cancelled subscriptions":
|
test "support request cancelled subscriptions":
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
proc onRequestCancelled(id: RequestId) =
|
proc onRequestCancelled(id: RequestId) =
|
||||||
|
@ -168,12 +173,38 @@ ethersuite "On-Chain Market":
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
test "subscribes only to a certain request cancellation":
|
test "support request failed subscriptions":
|
||||||
let otherRequest = StorageRequest.example
|
|
||||||
await token.approve(storage.address, request.price)
|
await token.approve(storage.address, request.price)
|
||||||
discard await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
|
|
||||||
|
var receivedIds: seq[RequestId]
|
||||||
|
proc onRequestFailed(id: RequestId) =
|
||||||
|
receivedIds.add(id)
|
||||||
|
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
|
||||||
|
|
||||||
|
for slotIndex in 0..<request.ask.slots:
|
||||||
|
await market.fillSlot(request.id, slotIndex.u256, proof)
|
||||||
|
for slotIndex in 0..request.ask.maxSlotLoss:
|
||||||
|
let slotId = request.slotId(slotIndex.u256)
|
||||||
|
while true:
|
||||||
|
try:
|
||||||
|
await waitUntilProofRequired(slotId)
|
||||||
|
let missingPeriod = periodicity.periodOf(await provider.currentTime())
|
||||||
|
await provider.advanceTime(periodicity.seconds)
|
||||||
|
await storage.markProofAsMissing(slotId, missingPeriod)
|
||||||
|
except ProviderError as e:
|
||||||
|
if e.revertReason == "Slot empty":
|
||||||
|
break
|
||||||
|
check receivedIds == @[request.id]
|
||||||
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
|
test "subscribes only to a certain request cancellation":
|
||||||
|
var otherRequest = request
|
||||||
|
otherRequest.nonce = Nonce.example
|
||||||
|
await token.approve(storage.address, request.price)
|
||||||
|
await market.requestStorage(request)
|
||||||
await token.approve(storage.address, otherRequest.price)
|
await token.approve(storage.address, otherRequest.price)
|
||||||
discard await market.requestStorage(otherRequest)
|
await market.requestStorage(otherRequest)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
proc onRequestCancelled(requestId: RequestId) =
|
proc onRequestCancelled(requestId: RequestId) =
|
||||||
|
@ -181,9 +212,7 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
|
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
|
||||||
await provider.advanceTimeTo(request.expiry) # shares expiry with otherRequest
|
await provider.advanceTimeTo(request.expiry) # shares expiry with otherRequest
|
||||||
check await market
|
await market.withdrawFunds(otherRequest.id)
|
||||||
.withdrawFunds(otherRequest.id)
|
|
||||||
.reverts("Invalid client address")
|
|
||||||
check receivedIds.len == 0
|
check receivedIds.len == 0
|
||||||
await market.withdrawFunds(request.id)
|
await market.withdrawFunds(request.id)
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
|
@ -191,3 +220,19 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
test "request is none when unknown":
|
test "request is none when unknown":
|
||||||
check isNone await market.getRequest(request.id)
|
check isNone await market.getRequest(request.id)
|
||||||
|
|
||||||
|
test "can retrieve active requests":
|
||||||
|
await token.approve(storage.address, request.price)
|
||||||
|
await market.requestStorage(request)
|
||||||
|
var request2 = StorageRequest.example
|
||||||
|
request2.client = accounts[0]
|
||||||
|
await token.approve(storage.address, request2.price)
|
||||||
|
await market.requestStorage(request2)
|
||||||
|
check (await market.myRequests()) == @[request.id, request2.id]
|
||||||
|
|
||||||
|
test "can retrieve request state":
|
||||||
|
await token.approve(storage.address, request.price)
|
||||||
|
await market.requestStorage(request)
|
||||||
|
for slotIndex in 0..<request.ask.slots:
|
||||||
|
await market.fillSlot(request.id, slotIndex.u256, proof)
|
||||||
|
check (await market.getState(request.id)) == some RequestState.Started
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ./time
|
||||||
ethersuite "On-Chain Proofs":
|
ethersuite "On-Chain Proofs":
|
||||||
|
|
||||||
let contractId = SlotId.example
|
let contractId = SlotId.example
|
||||||
let proof = seq[byte].example
|
let proof = exampleProof()
|
||||||
|
|
||||||
var proofs: OnChainProofs
|
var proofs: OnChainProofs
|
||||||
var storage: Storage
|
var storage: Storage
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import std/random
|
import std/random
|
||||||
import std/sequtils
|
import std/sequtils
|
||||||
|
import std/times
|
||||||
import pkg/codex/proving
|
import pkg/codex/proving
|
||||||
import pkg/stint
|
import pkg/stint
|
||||||
|
|
||||||
|
@ -19,3 +20,35 @@ proc example*(_: type UInt256): UInt256 =
|
||||||
|
|
||||||
proc example*[T: RequestId | SlotId | Nonce](_: type T): T =
|
proc example*[T: RequestId | SlotId | Nonce](_: type T): T =
|
||||||
T(array[32, byte].example)
|
T(array[32, byte].example)
|
||||||
|
|
||||||
|
proc example*(_: type StorageRequest): StorageRequest =
|
||||||
|
StorageRequest(
|
||||||
|
client: Address.example,
|
||||||
|
ask: StorageAsk(
|
||||||
|
slots: 4,
|
||||||
|
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
|
||||||
|
duration: (10 * 60 * 60).u256, # 10 hours
|
||||||
|
proofProbability: 4.u256, # require a proof roughly once every 4 periods
|
||||||
|
reward: 84.u256,
|
||||||
|
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
|
||||||
|
),
|
||||||
|
content: StorageContent(
|
||||||
|
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
|
||||||
|
erasure: StorageErasure(
|
||||||
|
totalChunks: 12,
|
||||||
|
),
|
||||||
|
por: StoragePor(
|
||||||
|
u: @(array[480, byte].example),
|
||||||
|
publicKey: @(array[96, byte].example),
|
||||||
|
name: @(array[512, byte].example)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
expiry: (getTime() + initDuration(hours=1)).toUnix.u256,
|
||||||
|
nonce: Nonce.example
|
||||||
|
)
|
||||||
|
|
||||||
|
proc exampleProof*(): seq[byte] =
|
||||||
|
var proof: seq[byte]
|
||||||
|
while proof.len == 0:
|
||||||
|
proof = seq[byte].example
|
||||||
|
return proof
|
||||||
|
|
|
@ -6,17 +6,43 @@ import std/strutils
|
||||||
const workingDir = currentSourcePath() / ".." / ".." / ".."
|
const workingDir = currentSourcePath() / ".." / ".." / ".."
|
||||||
const executable = "build" / "codex"
|
const executable = "build" / "codex"
|
||||||
|
|
||||||
proc startNode*(args: openArray[string], debug = false): Process =
|
type NodeProcess* = ref object
|
||||||
if debug:
|
process: Process
|
||||||
result = startProcess(executable, workingDir, args, options={poParentStreams})
|
arguments: seq[string]
|
||||||
|
debug: bool
|
||||||
|
|
||||||
|
proc start(node: NodeProcess) =
|
||||||
|
if node.debug:
|
||||||
|
node.process = startProcess(
|
||||||
|
executable,
|
||||||
|
workingDir,
|
||||||
|
node.arguments,
|
||||||
|
options={poParentStreams}
|
||||||
|
)
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
else:
|
else:
|
||||||
result = startProcess(executable, workingDir, args)
|
node.process = startProcess(
|
||||||
for line in result.outputStream.lines:
|
executable,
|
||||||
|
workingDir,
|
||||||
|
node.arguments
|
||||||
|
)
|
||||||
|
for line in node.process.outputStream.lines:
|
||||||
if line.contains("Started codex node"):
|
if line.contains("Started codex node"):
|
||||||
break
|
break
|
||||||
|
|
||||||
proc stop*(node: Process) =
|
proc startNode*(args: openArray[string], debug = false): NodeProcess =
|
||||||
node.terminate()
|
## Starts a Codex Node with the specified arguments.
|
||||||
discard node.waitForExit(timeout=5_000)
|
## Set debug to 'true' to see output of the node.
|
||||||
node.close()
|
let node = NodeProcess(arguments: @args, debug: debug)
|
||||||
|
node.start()
|
||||||
|
node
|
||||||
|
|
||||||
|
proc stop*(node: NodeProcess) =
|
||||||
|
let process = node.process
|
||||||
|
process.terminate()
|
||||||
|
discard process.waitForExit(timeout=5_000)
|
||||||
|
process.close()
|
||||||
|
|
||||||
|
proc restart*(node: NodeProcess) =
|
||||||
|
node.stop()
|
||||||
|
node.start()
|
||||||
|
|
|
@ -9,10 +9,11 @@ import ./ethertest
|
||||||
import ./contracts/time
|
import ./contracts/time
|
||||||
import ./integration/nodes
|
import ./integration/nodes
|
||||||
import ./integration/tokens
|
import ./integration/tokens
|
||||||
|
import ./codex/helpers/eventually
|
||||||
|
|
||||||
ethersuite "Integration tests":
|
ethersuite "Integration tests":
|
||||||
|
|
||||||
var node1, node2: Process
|
var node1, node2: NodeProcess
|
||||||
var baseurl1, baseurl2: string
|
var baseurl1, baseurl2: string
|
||||||
var client: HttpClient
|
var client: HttpClient
|
||||||
|
|
||||||
|
@ -34,8 +35,8 @@ ethersuite "Integration tests":
|
||||||
"--nat=127.0.0.1",
|
"--nat=127.0.0.1",
|
||||||
"--disc-ip=127.0.0.1",
|
"--disc-ip=127.0.0.1",
|
||||||
"--disc-port=8090",
|
"--disc-port=8090",
|
||||||
"--persistence",
|
"--persistence",
|
||||||
"--eth-account=" & $accounts[0]
|
"--eth-account=" & $accounts[0]
|
||||||
], debug = false)
|
], debug = false)
|
||||||
|
|
||||||
node2 = startNode([
|
node2 = startNode([
|
||||||
|
@ -97,6 +98,26 @@ ethersuite "Integration tests":
|
||||||
check json["request"]["ask"]["duration"].getStr == "0x1"
|
check json["request"]["ask"]["duration"].getStr == "0x1"
|
||||||
check json["request"]["ask"]["reward"].getStr == "0x2"
|
check json["request"]["ask"]["reward"].getStr == "0x2"
|
||||||
|
|
||||||
|
test "node remembers purchase status after restart":
|
||||||
|
let cid = client.post(baseurl1 & "/upload", "some file contents").body
|
||||||
|
let request = %*{"duration": "0x1", "reward": "0x2"}
|
||||||
|
let id = client.post(baseurl1 & "/storage/request/" & cid, $request).body
|
||||||
|
|
||||||
|
proc getPurchase(id: string): JsonNode =
|
||||||
|
let response = client.get(baseurl1 & "/storage/purchases/" & id)
|
||||||
|
return parseJson(response.body).catch |? nil
|
||||||
|
|
||||||
|
check eventually getPurchase(id){"state"}.getStr == "submitted"
|
||||||
|
|
||||||
|
node1.restart()
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
client = newHttpClient()
|
||||||
|
|
||||||
|
check eventually (not isNil getPurchase(id){"request"}{"ask"})
|
||||||
|
check getPurchase(id){"request"}{"ask"}{"duration"}.getStr == "0x1"
|
||||||
|
check getPurchase(id){"request"}{"ask"}{"reward"}.getStr == "0x2"
|
||||||
|
|
||||||
test "nodes negotiate contracts on the marketplace":
|
test "nodes negotiate contracts on the marketplace":
|
||||||
proc sell =
|
proc sell =
|
||||||
let json = %*{"size": "0xFFFFF", "duration": "0x200", "minPrice": "0x300"}
|
let json = %*{"size": "0xFFFFF", "duration": "0x200", "minPrice": "0x300"}
|
||||||
|
@ -110,14 +131,14 @@ ethersuite "Integration tests":
|
||||||
|
|
||||||
proc buy(cid: string): string =
|
proc buy(cid: string): string =
|
||||||
let expiry = ((waitFor provider.currentTime()) + 30).toHex
|
let expiry = ((waitFor provider.currentTime()) + 30).toHex
|
||||||
let json = %*{"duration": "0x100", "reward": "0x400", "expiry": expiry}
|
let json = %*{"duration": "0x1", "reward": "0x400", "expiry": expiry}
|
||||||
client.post(baseurl1 & "/storage/request/" & cid, $json).body
|
client.post(baseurl1 & "/storage/request/" & cid, $json).body
|
||||||
|
|
||||||
proc finish(purchase: string): Future[JsonNode] {.async.} =
|
proc finish(purchase: string): Future[JsonNode] {.async.} =
|
||||||
while true:
|
while true:
|
||||||
let response = client.get(baseurl1 & "/storage/purchases/" & purchase)
|
let response = client.get(baseurl1 & "/storage/purchases/" & purchase)
|
||||||
let json = parseJson(response.body)
|
let json = parseJson(response.body)
|
||||||
if json["finished"].getBool: return json
|
if json["state"].getStr == "finished": return json
|
||||||
await sleepAsync(1.seconds)
|
await sleepAsync(1.seconds)
|
||||||
|
|
||||||
sell()
|
sell()
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 087c13a7fc2b44a5ad52b8a624f51b711a10d783
|
Subproject commit 61b8f5fc352838866b0fe27b936323de45bf269c
|
Loading…
Reference in New Issue