feat: collateral per slot (#390)
Co-authored-by: Eric Mastro <github@egonat.me>
This commit is contained in:
parent
86a3f74448
commit
131d003a0c
|
@ -32,33 +32,6 @@ let client = provider.getSigner(accounts[0])
|
||||||
let host = provider.getSigner(accounts[1])
|
let host = provider.getSigner(accounts[1])
|
||||||
```
|
```
|
||||||
|
|
||||||
Collateral
|
|
||||||
----------
|
|
||||||
|
|
||||||
Hosts need to put up collateral before participating in storage contracts.
|
|
||||||
|
|
||||||
A host can learn about the amount of collateral that is required:
|
|
||||||
```nim
|
|
||||||
let config = await marketplace.config()
|
|
||||||
let collateral = config.collateral.initialAmount
|
|
||||||
```
|
|
||||||
|
|
||||||
After preparing the payment, the host can deposit collateral:
|
|
||||||
```nim
|
|
||||||
await storage
|
|
||||||
.connect(host)
|
|
||||||
.deposit(collateral)
|
|
||||||
```
|
|
||||||
|
|
||||||
When a host is not participating in storage offers or contracts, it can withdraw
|
|
||||||
its collateral:
|
|
||||||
|
|
||||||
```
|
|
||||||
await storage
|
|
||||||
.connect(host)
|
|
||||||
.withdraw()
|
|
||||||
```
|
|
||||||
|
|
||||||
Storage requests
|
Storage requests
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ type
|
||||||
collateral*: CollateralConfig
|
collateral*: CollateralConfig
|
||||||
proofs*: ProofConfig
|
proofs*: ProofConfig
|
||||||
CollateralConfig* = object
|
CollateralConfig* = object
|
||||||
initialAmount*: UInt256 # amount of collateral necessary to fill a slot
|
repairRewardPercentage*: uint8 # percentage of remaining collateral slot has after it has been freed
|
||||||
minimumAmount*: UInt256 # frees slot when collateral drops below this minimum
|
maxNumberOfSlashes*: uint8 # frees slot when the number of slashes reaches this value
|
||||||
slashCriterion*: UInt256 # amount of proofs missed that lead to slashing
|
slashCriterion*: uint16 # amount of proofs missed that lead to slashing
|
||||||
slashPercentage*: UInt256 # percentage of the collateral that is slashed
|
slashPercentage*: uint8 # percentage of the collateral that is slashed
|
||||||
ProofConfig* = object
|
ProofConfig* = object
|
||||||
period*: UInt256 # proofs requirements are calculated per period (in seconds)
|
period*: UInt256 # proofs requirements are calculated per period (in seconds)
|
||||||
timeout*: UInt256 # mark proofs as missing before the timeout (in seconds)
|
timeout*: UInt256 # mark proofs as missing before the timeout (in seconds)
|
||||||
|
@ -28,8 +28,8 @@ func fromTuple(_: type ProofConfig, tupl: tuple): ProofConfig =
|
||||||
|
|
||||||
func fromTuple(_: type CollateralConfig, tupl: tuple): CollateralConfig =
|
func fromTuple(_: type CollateralConfig, tupl: tuple): CollateralConfig =
|
||||||
CollateralConfig(
|
CollateralConfig(
|
||||||
initialAmount: tupl[0],
|
repairRewardPercentage: tupl[0],
|
||||||
minimumAmount: tupl[1],
|
maxNumberOfSlashes: tupl[1],
|
||||||
slashCriterion: tupl[2],
|
slashCriterion: tupl[2],
|
||||||
slashPercentage: tupl[3]
|
slashPercentage: tupl[3]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import std/strutils
|
import std/strutils
|
||||||
|
import pkg/chronicles
|
||||||
import pkg/ethers
|
import pkg/ethers
|
||||||
import pkg/ethers/testing
|
import pkg/ethers/testing
|
||||||
import pkg/upraises
|
import pkg/upraises
|
||||||
|
@ -8,6 +9,9 @@ import ./marketplace
|
||||||
|
|
||||||
export market
|
export market
|
||||||
|
|
||||||
|
logScope:
|
||||||
|
topics = "onchain market"
|
||||||
|
|
||||||
type
|
type
|
||||||
OnChainMarket* = ref object of Market
|
OnChainMarket* = ref object of Market
|
||||||
contract: Marketplace
|
contract: Marketplace
|
||||||
|
@ -25,7 +29,8 @@ func new*(_: type OnChainMarket, contract: Marketplace): OnChainMarket =
|
||||||
signer: signer,
|
signer: signer,
|
||||||
)
|
)
|
||||||
|
|
||||||
method approveFunds*(market: OnChainMarket, amount: UInt256) {.async.} =
|
proc approveFunds(market: OnChainMarket, amount: UInt256) {.async.} =
|
||||||
|
notice "approving tokens", amount
|
||||||
let tokenAddress = await market.contract.token()
|
let tokenAddress = await market.contract.token()
|
||||||
let token = Erc20Token.new(tokenAddress, market.signer)
|
let token = Erc20Token.new(tokenAddress, market.signer)
|
||||||
|
|
||||||
|
@ -41,6 +46,7 @@ method mySlots*(market: OnChainMarket): Future[seq[SlotId]] {.async.} =
|
||||||
return await market.contract.mySlots()
|
return await market.contract.mySlots()
|
||||||
|
|
||||||
method requestStorage(market: OnChainMarket, request: StorageRequest){.async.} =
|
method requestStorage(market: OnChainMarket, request: StorageRequest){.async.} =
|
||||||
|
await market.approveFunds(request.price())
|
||||||
await market.contract.requestStorage(request)
|
await market.contract.requestStorage(request)
|
||||||
|
|
||||||
method getRequest(market: OnChainMarket,
|
method getRequest(market: OnChainMarket,
|
||||||
|
@ -93,7 +99,9 @@ method getActiveSlot*(
|
||||||
method fillSlot(market: OnChainMarket,
|
method fillSlot(market: OnChainMarket,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256,
|
slotIndex: UInt256,
|
||||||
proof: seq[byte]) {.async.} =
|
proof: seq[byte],
|
||||||
|
collateral: UInt256) {.async.} =
|
||||||
|
await market.approveFunds(collateral)
|
||||||
await market.contract.fillSlot(requestId, slotIndex, proof)
|
await market.contract.fillSlot(requestId, slotIndex, proof)
|
||||||
|
|
||||||
method withdrawFunds(market: OnChainMarket,
|
method withdrawFunds(market: OnChainMarket,
|
||||||
|
|
|
@ -38,10 +38,6 @@ proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.}
|
||||||
proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.}
|
proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.}
|
||||||
proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.}
|
proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.}
|
||||||
|
|
||||||
proc deposit*(marketplace: Marketplace, amount: UInt256) {.contract.}
|
|
||||||
proc withdraw*(marketplace: Marketplace) {.contract.}
|
|
||||||
proc balanceOf*(marketplace: Marketplace, account: Address): UInt256 {.contract, view.}
|
|
||||||
|
|
||||||
proc requestStorage*(marketplace: Marketplace, request: StorageRequest) {.contract.}
|
proc requestStorage*(marketplace: Marketplace, request: StorageRequest) {.contract.}
|
||||||
proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: seq[byte]) {.contract.}
|
proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: seq[byte]) {.contract.}
|
||||||
proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId) {.contract.}
|
proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId) {.contract.}
|
||||||
|
|
|
@ -19,6 +19,7 @@ type
|
||||||
duration*: UInt256
|
duration*: UInt256
|
||||||
proofProbability*: UInt256
|
proofProbability*: UInt256
|
||||||
reward*: UInt256
|
reward*: UInt256
|
||||||
|
collateral*: UInt256
|
||||||
maxSlotLoss*: uint64
|
maxSlotLoss*: uint64
|
||||||
StorageContent* = object
|
StorageContent* = object
|
||||||
cid*: string
|
cid*: string
|
||||||
|
@ -84,7 +85,8 @@ func fromTuple(_: type StorageAsk, tupl: tuple): StorageAsk =
|
||||||
duration: tupl[2],
|
duration: tupl[2],
|
||||||
proofProbability: tupl[3],
|
proofProbability: tupl[3],
|
||||||
reward: tupl[4],
|
reward: tupl[4],
|
||||||
maxSlotLoss: tupl[5]
|
collateral: tupl[5],
|
||||||
|
maxSlotLoss: tupl[6]
|
||||||
)
|
)
|
||||||
|
|
||||||
func fromTuple(_: type StorageContent, tupl: tuple): StorageContent =
|
func fromTuple(_: type StorageContent, tupl: tuple): StorageContent =
|
||||||
|
|
|
@ -19,10 +19,6 @@ type
|
||||||
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
||||||
OnRequestFailed* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
OnRequestFailed* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
|
||||||
|
|
||||||
method approveFunds*(market: Market, amount: UInt256) {.base, async.} =
|
|
||||||
## This is generally needed for On-Chain ERC20 functionality to approve funds transfer to marketplace contract
|
|
||||||
raiseAssert("not implemented")
|
|
||||||
|
|
||||||
method getSigner*(market: Market): Future[Address] {.base, async.} =
|
method getSigner*(market: Market): Future[Address] {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
|
@ -67,7 +63,8 @@ method getActiveSlot*(
|
||||||
method fillSlot*(market: Market,
|
method fillSlot*(market: Market,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256,
|
slotIndex: UInt256,
|
||||||
proof: seq[byte]) {.base, async.} =
|
proof: seq[byte],
|
||||||
|
collateral: UInt256) {.base, async.} =
|
||||||
raiseAssert("not implemented")
|
raiseAssert("not implemented")
|
||||||
|
|
||||||
method withdrawFunds*(market: Market,
|
method withdrawFunds*(market: Market,
|
||||||
|
|
|
@ -244,6 +244,7 @@ proc requestStorage*(self: CodexNodeRef,
|
||||||
nodes: uint,
|
nodes: uint,
|
||||||
tolerance: uint,
|
tolerance: uint,
|
||||||
reward: UInt256,
|
reward: UInt256,
|
||||||
|
collateral: UInt256,
|
||||||
expiry = UInt256.none): Future[?!PurchaseId] {.async.} =
|
expiry = UInt256.none): Future[?!PurchaseId] {.async.} =
|
||||||
## Initiate a request for storage sequence, this might
|
## Initiate a request for storage sequence, this might
|
||||||
## be a multistep procedure.
|
## be a multistep procedure.
|
||||||
|
@ -288,6 +289,7 @@ proc requestStorage*(self: CodexNodeRef,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
proofProbability: proofProbability,
|
proofProbability: proofProbability,
|
||||||
reward: reward,
|
reward: reward,
|
||||||
|
collateral: collateral,
|
||||||
maxSlotLoss: tolerance
|
maxSlotLoss: tolerance
|
||||||
),
|
),
|
||||||
content: StorageContent(
|
content: StorageContent(
|
||||||
|
|
|
@ -10,7 +10,6 @@ method enterAsync(state: PurchasePending) {.async.} =
|
||||||
raiseAssert "invalid state"
|
raiseAssert "invalid state"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await purchase.market.approveFunds(request.price())
|
|
||||||
await purchase.market.requestStorage(request)
|
await purchase.market.requestStorage(request)
|
||||||
except CatchableError as error:
|
except CatchableError as error:
|
||||||
state.switch(PurchaseErrored(error: error))
|
state.switch(PurchaseErrored(error: error))
|
||||||
|
|
|
@ -140,6 +140,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
|
||||||
## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data
|
## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data
|
||||||
## nodes - minimal number of nodes the content should be stored on
|
## nodes - minimal number of nodes the content should be stored on
|
||||||
## tolerance - allowed number of nodes that can be lost before pronouncing the content lost
|
## tolerance - allowed number of nodes that can be lost before pronouncing the content lost
|
||||||
|
## colateral - requested collateral from hosts when they fill slot
|
||||||
|
|
||||||
without cid =? cid.tryGet.catch, error:
|
without cid =? cid.tryGet.catch, error:
|
||||||
return RestApiResponse.error(Http400, error.msg)
|
return RestApiResponse.error(Http400, error.msg)
|
||||||
|
@ -159,6 +160,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
|
||||||
nodes,
|
nodes,
|
||||||
tolerance,
|
tolerance,
|
||||||
params.reward,
|
params.reward,
|
||||||
|
params.collateral,
|
||||||
params.expiry), error:
|
params.expiry), error:
|
||||||
|
|
||||||
return RestApiResponse.error(Http500, error.msg)
|
return RestApiResponse.error(Http500, error.msg)
|
||||||
|
@ -272,6 +274,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
|
||||||
## size - size of available storage in bytes
|
## size - size of available storage in bytes
|
||||||
## duration - maximum time the storage should be sold for (in seconds)
|
## duration - maximum time the storage should be sold for (in seconds)
|
||||||
## minPrice - minimum price to be paid (in amount of tokens)
|
## minPrice - minimum price to be paid (in amount of tokens)
|
||||||
|
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens)
|
||||||
|
|
||||||
without contracts =? node.contracts.host:
|
without contracts =? node.contracts.host:
|
||||||
return RestApiResponse.error(Http503, "Sales unavailable")
|
return RestApiResponse.error(Http503, "Sales unavailable")
|
||||||
|
|
|
@ -10,6 +10,7 @@ type
|
||||||
duration*: UInt256
|
duration*: UInt256
|
||||||
proofProbability*: UInt256
|
proofProbability*: UInt256
|
||||||
reward*: UInt256
|
reward*: UInt256
|
||||||
|
collateral*: UInt256
|
||||||
expiry*: ?UInt256
|
expiry*: ?UInt256
|
||||||
nodes*: ?uint
|
nodes*: ?uint
|
||||||
tolerance*: ?uint
|
tolerance*: ?uint
|
||||||
|
@ -19,7 +20,8 @@ proc fromJson*(_: type Availability, bytes: seq[byte]): ?!Availability =
|
||||||
let size = ?catch UInt256.fromHex(json["size"].getStr)
|
let size = ?catch UInt256.fromHex(json["size"].getStr)
|
||||||
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
|
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
|
||||||
let minPrice = ?catch UInt256.fromHex(json["minPrice"].getStr)
|
let minPrice = ?catch UInt256.fromHex(json["minPrice"].getStr)
|
||||||
success Availability.init(size, duration, minPrice)
|
let maxCollateral = ?catch UInt256.fromHex(json["maxCollateral"].getStr)
|
||||||
|
success Availability.init(size, duration, minPrice, maxCollateral)
|
||||||
|
|
||||||
proc fromJson*(_: type StorageRequestParams,
|
proc fromJson*(_: type StorageRequestParams,
|
||||||
bytes: seq[byte]): ?! StorageRequestParams =
|
bytes: seq[byte]): ?! StorageRequestParams =
|
||||||
|
@ -27,6 +29,7 @@ proc fromJson*(_: type StorageRequestParams,
|
||||||
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
|
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
|
||||||
let proofProbability = ?catch UInt256.fromHex(json["proofProbability"].getStr)
|
let proofProbability = ?catch UInt256.fromHex(json["proofProbability"].getStr)
|
||||||
let reward = ?catch UInt256.fromHex(json["reward"].getStr)
|
let reward = ?catch UInt256.fromHex(json["reward"].getStr)
|
||||||
|
let collateral = ?catch UInt256.fromHex(json["collateral"].getStr)
|
||||||
let expiry = UInt256.fromHex(json["expiry"].getStr).catch.option
|
let expiry = UInt256.fromHex(json["expiry"].getStr).catch.option
|
||||||
let nodes = strutils.fromHex[uint](json["nodes"].getStr).catch.option
|
let nodes = strutils.fromHex[uint](json["nodes"].getStr).catch.option
|
||||||
let tolerance = strutils.fromHex[uint](json["tolerance"].getStr).catch.option
|
let tolerance = strutils.fromHex[uint](json["tolerance"].getStr).catch.option
|
||||||
|
@ -34,6 +37,7 @@ proc fromJson*(_: type StorageRequestParams,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
proofProbability: proofProbability,
|
proofProbability: proofProbability,
|
||||||
reward: reward,
|
reward: reward,
|
||||||
|
collateral: collateral,
|
||||||
expiry: expiry,
|
expiry: expiry,
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
tolerance: tolerance
|
tolerance: tolerance
|
||||||
|
|
|
@ -38,6 +38,7 @@ type
|
||||||
size*: UInt256
|
size*: UInt256
|
||||||
duration*: UInt256
|
duration*: UInt256
|
||||||
minPrice*: UInt256
|
minPrice*: UInt256
|
||||||
|
maxCollateral*: UInt256
|
||||||
used*: bool
|
used*: bool
|
||||||
Reservations* = ref object
|
Reservations* = ref object
|
||||||
repo: RepoStore
|
repo: RepoStore
|
||||||
|
@ -67,11 +68,12 @@ proc init*(
|
||||||
_: type Availability,
|
_: type Availability,
|
||||||
size: UInt256,
|
size: UInt256,
|
||||||
duration: UInt256,
|
duration: UInt256,
|
||||||
minPrice: UInt256): Availability =
|
minPrice: UInt256,
|
||||||
|
maxCollateral: UInt256): Availability =
|
||||||
|
|
||||||
var id: array[32, byte]
|
var id: array[32, byte]
|
||||||
doAssert randomBytes(id) == 32
|
doAssert randomBytes(id) == 32
|
||||||
Availability(id: AvailabilityId(id), size: size, duration: duration, minPrice: minPrice)
|
Availability(id: AvailabilityId(id), size: size, duration: duration, minPrice: minPrice, maxCollateral: maxCollateral)
|
||||||
|
|
||||||
func toArray*(id: AvailabilityId): array[32, byte] =
|
func toArray*(id: AvailabilityId): array[32, byte] =
|
||||||
array[32, byte](id)
|
array[32, byte](id)
|
||||||
|
@ -81,6 +83,7 @@ proc `==`*(x, y: Availability): bool =
|
||||||
x.id == y.id and
|
x.id == y.id and
|
||||||
x.size == y.size and
|
x.size == y.size and
|
||||||
x.duration == y.duration and
|
x.duration == y.duration and
|
||||||
|
x.maxCollateral == y.maxCollateral and
|
||||||
x.minPrice == y.minPrice
|
x.minPrice == y.minPrice
|
||||||
|
|
||||||
proc `$`*(id: AvailabilityId): string = id.toArray.toHex
|
proc `$`*(id: AvailabilityId): string = id.toArray.toHex
|
||||||
|
@ -318,7 +321,7 @@ proc unused*(r: Reservations): Future[?!seq[Availability]] {.async.} =
|
||||||
|
|
||||||
proc find*(
|
proc find*(
|
||||||
self: Reservations,
|
self: Reservations,
|
||||||
size, duration, minPrice: UInt256,
|
size, duration, minPrice: UInt256, collateral: UInt256,
|
||||||
used: bool): Future[?Availability] {.async.} =
|
used: bool): Future[?Availability] {.async.} =
|
||||||
|
|
||||||
|
|
||||||
|
@ -332,13 +335,15 @@ proc find*(
|
||||||
if used == availability.used and
|
if used == availability.used and
|
||||||
size <= availability.size and
|
size <= availability.size and
|
||||||
duration <= availability.duration and
|
duration <= availability.duration and
|
||||||
|
collateral <= availability.maxCollateral and
|
||||||
minPrice >= availability.minPrice:
|
minPrice >= availability.minPrice:
|
||||||
|
|
||||||
trace "availability matched",
|
trace "availability matched",
|
||||||
used, availUsed = availability.used,
|
used, availUsed = availability.used,
|
||||||
size, availsize = availability.size,
|
size, availsize = availability.size,
|
||||||
duration, availDuration = availability.duration,
|
duration, availDuration = availability.duration,
|
||||||
minPrice, availMinPrice = availability.minPrice
|
minPrice, availMinPrice = availability.minPrice,
|
||||||
|
collateral, availMaxCollateral = availability.maxCollateral
|
||||||
|
|
||||||
return some availability
|
return some availability
|
||||||
|
|
||||||
|
@ -346,4 +351,5 @@ proc find*(
|
||||||
used, availUsed = availability.used,
|
used, availUsed = availability.used,
|
||||||
size, availsize = availability.size,
|
size, availsize = availability.size,
|
||||||
duration, availDuration = availability.duration,
|
duration, availDuration = availability.duration,
|
||||||
minPrice, availMinPrice = availability.minPrice
|
minPrice, availMinPrice = availability.minPrice,
|
||||||
|
collateral, availMaxCollateral = availability.maxCollateral
|
||||||
|
|
|
@ -50,6 +50,7 @@ method run*(state: SaleDownloading, machine: Machine): Future[?State] {.async.}
|
||||||
request.ask.slotSize,
|
request.ask.slotSize,
|
||||||
request.ask.duration,
|
request.ask.duration,
|
||||||
request.ask.pricePerSlot,
|
request.ask.pricePerSlot,
|
||||||
|
request.ask.collateral,
|
||||||
used = false):
|
used = false):
|
||||||
info "no availability found for request, ignoring",
|
info "no availability found for request, ignoring",
|
||||||
slotSize = request.ask.slotSize,
|
slotSize = request.ask.slotSize,
|
||||||
|
|
|
@ -25,5 +25,7 @@ method onSlotFilled*(state: SaleFilling, requestId: RequestId,
|
||||||
method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} =
|
method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} =
|
||||||
let data = SalesAgent(machine).data
|
let data = SalesAgent(machine).data
|
||||||
let market = SalesAgent(machine).context.market
|
let market = SalesAgent(machine).context.market
|
||||||
|
without (collateral =? data.request.?ask.?collateral):
|
||||||
|
raiseAssert "Request not set"
|
||||||
|
|
||||||
await market.fillSlot(data.requestId, data.slotIndex, state.proof)
|
await market.fillSlot(data.requestId, data.slotIndex, state.proof, collateral)
|
||||||
|
|
|
@ -112,6 +112,9 @@ components:
|
||||||
minPrice:
|
minPrice:
|
||||||
type: string
|
type: string
|
||||||
description: Minimum price to be paid (in amount of tokens) as hexadecimal string
|
description: Minimum price to be paid (in amount of tokens) as hexadecimal string
|
||||||
|
maxCollateral:
|
||||||
|
type: string
|
||||||
|
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens)
|
||||||
|
|
||||||
StorageRequestCreation:
|
StorageRequestCreation:
|
||||||
type: object
|
type: object
|
||||||
|
@ -119,6 +122,7 @@ components:
|
||||||
- reward
|
- reward
|
||||||
- duration
|
- duration
|
||||||
- proofProbability
|
- proofProbability
|
||||||
|
- collateral
|
||||||
properties:
|
properties:
|
||||||
duration:
|
duration:
|
||||||
$ref: "#/components/schemas/Duration"
|
$ref: "#/components/schemas/Duration"
|
||||||
|
@ -134,6 +138,9 @@ components:
|
||||||
type: number
|
type: number
|
||||||
description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost
|
description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost
|
||||||
default: 0
|
default: 0
|
||||||
|
collateral:
|
||||||
|
type: string
|
||||||
|
description: Hexadecimal encoded number that represents how much collateral is asked from hosts that wants to fill a slots
|
||||||
|
|
||||||
StorageAsk:
|
StorageAsk:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -57,5 +57,6 @@ proc example*(_: type Availability): Availability =
|
||||||
Availability.init(
|
Availability.init(
|
||||||
size = uint16.example.u256,
|
size = uint16.example.u256,
|
||||||
duration = uint16.example.u256,
|
duration = uint16.example.u256,
|
||||||
minPrice = uint64.example.u256
|
minPrice = uint64.example.u256,
|
||||||
|
maxCollateral = uint16.example.u256
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,10 +67,10 @@ proc hash*(requestId: RequestId): Hash =
|
||||||
proc new*(_: type MockMarket): MockMarket =
|
proc new*(_: type MockMarket): MockMarket =
|
||||||
let config = MarketplaceConfig(
|
let config = MarketplaceConfig(
|
||||||
collateral: CollateralConfig(
|
collateral: CollateralConfig(
|
||||||
initialAmount: 100.u256,
|
repairRewardPercentage: 10,
|
||||||
minimumAmount: 40.u256,
|
maxNumberOfSlashes: 5,
|
||||||
slashCriterion: 3.u256,
|
slashCriterion: 3,
|
||||||
slashPercentage: 10.u256
|
slashPercentage: 10
|
||||||
),
|
),
|
||||||
proofs: ProofConfig(
|
proofs: ProofConfig(
|
||||||
period: 10.u256,
|
period: 10.u256,
|
||||||
|
@ -80,9 +80,6 @@ proc new*(_: type MockMarket): MockMarket =
|
||||||
)
|
)
|
||||||
MockMarket(signer: Address.example, config: config)
|
MockMarket(signer: Address.example, config: config)
|
||||||
|
|
||||||
method approveFunds*(market: MockMarket, amount: UInt256) {.async.} =
|
|
||||||
discard
|
|
||||||
|
|
||||||
method getSigner*(market: MockMarket): Future[Address] {.async.} =
|
method getSigner*(market: MockMarket): Future[Address] {.async.} =
|
||||||
return market.signer
|
return market.signer
|
||||||
|
|
||||||
|
@ -182,7 +179,8 @@ proc fillSlot*(market: MockMarket,
|
||||||
method fillSlot*(market: MockMarket,
|
method fillSlot*(market: MockMarket,
|
||||||
requestId: RequestId,
|
requestId: RequestId,
|
||||||
slotIndex: UInt256,
|
slotIndex: UInt256,
|
||||||
proof: seq[byte]) {.async.} =
|
proof: seq[byte],
|
||||||
|
collateral: UInt256) {.async.} =
|
||||||
market.fillSlot(requestId, slotIndex, proof, market.signer)
|
market.fillSlot(requestId, slotIndex, proof, market.signer)
|
||||||
|
|
||||||
method withdrawFunds*(market: MockMarket,
|
method withdrawFunds*(market: MockMarket,
|
||||||
|
|
|
@ -39,8 +39,8 @@ suite "Reservations module":
|
||||||
check (await reservations.allAvailabilities()).len == 0
|
check (await reservations.allAvailabilities()).len == 0
|
||||||
|
|
||||||
test "generates unique ids for storage availability":
|
test "generates unique ids for storage availability":
|
||||||
let availability1 = Availability.init(1.u256, 2.u256, 3.u256)
|
let availability1 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256)
|
||||||
let availability2 = Availability.init(1.u256, 2.u256, 3.u256)
|
let availability2 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256)
|
||||||
check availability1.id != availability2.id
|
check availability1.id != availability2.id
|
||||||
|
|
||||||
test "can reserve available storage":
|
test "can reserve available storage":
|
||||||
|
@ -125,7 +125,7 @@ suite "Reservations module":
|
||||||
check isOk await reservations.markUsed(availability.id)
|
check isOk await reservations.markUsed(availability.id)
|
||||||
|
|
||||||
without available =? await reservations.find(availability.size,
|
without available =? await reservations.find(availability.size,
|
||||||
availability.duration, availability.minPrice, used = true):
|
availability.duration, availability.minPrice, availability.maxCollateral, used = true):
|
||||||
|
|
||||||
fail()
|
fail()
|
||||||
|
|
||||||
|
@ -133,13 +133,13 @@ suite "Reservations module":
|
||||||
check isOk await reservations.reserve(availability)
|
check isOk await reservations.reserve(availability)
|
||||||
|
|
||||||
without available =? await reservations.find(availability.size,
|
without available =? await reservations.find(availability.size,
|
||||||
availability.duration, availability.minPrice, used = false):
|
availability.duration, availability.minPrice, availability.maxCollateral, used = false):
|
||||||
|
|
||||||
fail()
|
fail()
|
||||||
|
|
||||||
test "non-existant availability cannot be found":
|
test "non-existant availability cannot be found":
|
||||||
check isNone (await reservations.find(availability.size,
|
check isNone (await reservations.find(availability.size,
|
||||||
availability.duration, availability.minPrice, used = false))
|
availability.duration, availability.minPrice, availability.maxCollateral, used = false))
|
||||||
|
|
||||||
test "non-existant availability cannot be retrieved":
|
test "non-existant availability cannot be retrieved":
|
||||||
let r = await reservations.get(availability.id)
|
let r = await reservations.get(availability.id)
|
||||||
|
|
|
@ -38,7 +38,8 @@ suite "Sales":
|
||||||
availability = Availability.init(
|
availability = Availability.init(
|
||||||
size=100.u256,
|
size=100.u256,
|
||||||
duration=60.u256,
|
duration=60.u256,
|
||||||
minPrice=600.u256
|
minPrice=600.u256,
|
||||||
|
maxCollateral=400.u256
|
||||||
)
|
)
|
||||||
request = StorageRequest(
|
request = StorageRequest(
|
||||||
ask: StorageAsk(
|
ask: StorageAsk(
|
||||||
|
@ -46,6 +47,7 @@ suite "Sales":
|
||||||
slotSize: 100.u256,
|
slotSize: 100.u256,
|
||||||
duration: 60.u256,
|
duration: 60.u256,
|
||||||
reward: 10.u256,
|
reward: 10.u256,
|
||||||
|
collateral: 200.u256,
|
||||||
),
|
),
|
||||||
content: StorageContent(
|
content: StorageContent(
|
||||||
cid: "some cid"
|
cid: "some cid"
|
||||||
|
@ -134,6 +136,13 @@ suite "Sales":
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
check getAvailability().?used == success false
|
check getAvailability().?used == success false
|
||||||
|
|
||||||
|
test "ignores request when asked collateral is too high":
|
||||||
|
var tooBigCollateral = request
|
||||||
|
tooBigCollateral.ask.collateral = availability.maxCollateral + 1
|
||||||
|
check isOk await reservations.reserve(availability)
|
||||||
|
await market.requestStorage(tooBigCollateral)
|
||||||
|
check await wasIgnored()
|
||||||
|
|
||||||
test "retrieves and stores data locally":
|
test "retrieves and stores data locally":
|
||||||
var storingRequest: StorageRequest
|
var storingRequest: StorageRequest
|
||||||
var storingSlot: UInt256
|
var storingSlot: UInt256
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import pkg/chronos
|
|
||||||
import pkg/stint
|
|
||||||
import codex/contracts
|
|
||||||
import ./token
|
|
||||||
import ../ethertest
|
|
||||||
|
|
||||||
ethersuite "Collateral":
|
|
||||||
|
|
||||||
let collateral = 100.u256
|
|
||||||
|
|
||||||
var marketplace: Marketplace
|
|
||||||
var token: TestToken
|
|
||||||
|
|
||||||
setup:
|
|
||||||
let deployment = Deployment.init()
|
|
||||||
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
|
|
||||||
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
|
|
||||||
await token.mint(accounts[0], 1000.u256)
|
|
||||||
|
|
||||||
test "increases collateral":
|
|
||||||
await token.approve(marketplace.address, collateral)
|
|
||||||
await marketplace.deposit(collateral)
|
|
||||||
let balance = await marketplace.balanceOf(accounts[0])
|
|
||||||
check balance == collateral
|
|
||||||
|
|
||||||
test "withdraws collateral":
|
|
||||||
await token.approve(marketplace.address, collateral)
|
|
||||||
await marketplace.deposit(collateral)
|
|
||||||
let balanceBefore = await token.balanceOf(accounts[0])
|
|
||||||
await marketplace.withdraw()
|
|
||||||
let balanceAfter = await token.balanceOf(accounts[0])
|
|
||||||
check (balanceAfter - balanceBefore) == collateral
|
|
|
@ -1,20 +1,19 @@
|
||||||
import std/json
|
import std/json
|
||||||
import pkg/chronos
|
import pkg/chronos
|
||||||
import pkg/ethers/testing
|
import pkg/ethers/testing
|
||||||
|
import pkg/ethers/erc20
|
||||||
import codex/contracts
|
import codex/contracts
|
||||||
import codex/storageproofs
|
import codex/storageproofs
|
||||||
import ../ethertest
|
import ../ethertest
|
||||||
import ./examples
|
import ./examples
|
||||||
import ./time
|
import ./time
|
||||||
import ./token
|
|
||||||
|
|
||||||
ethersuite "Marketplace contracts":
|
ethersuite "Marketplace contracts":
|
||||||
let proof = exampleProof()
|
let proof = exampleProof()
|
||||||
|
|
||||||
var client, host: Signer
|
var client, host: Signer
|
||||||
var marketplace: Marketplace
|
var marketplace: Marketplace
|
||||||
var token: TestToken
|
var token: Erc20Token
|
||||||
var collateral: UInt256
|
|
||||||
var periodicity: Periodicity
|
var periodicity: Periodicity
|
||||||
var request: StorageRequest
|
var request: StorageRequest
|
||||||
var slotId: SlotId
|
var slotId: SlotId
|
||||||
|
@ -29,13 +28,11 @@ ethersuite "Marketplace contracts":
|
||||||
|
|
||||||
let deployment = Deployment.init()
|
let deployment = Deployment.init()
|
||||||
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
|
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
|
||||||
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
|
|
||||||
|
|
||||||
await token.mint(await client.getAddress(), 1_000_000_000.u256)
|
let tokenAddress = await marketplace.token()
|
||||||
await token.mint(await host.getAddress(), 1000_000_000.u256)
|
token = Erc20Token.new(tokenAddress, provider.getSigner())
|
||||||
|
|
||||||
let config = await marketplace.config()
|
let config = await marketplace.config()
|
||||||
collateral = config.collateral.initialAmount
|
|
||||||
periodicity = Periodicity(seconds: config.proofs.period)
|
periodicity = Periodicity(seconds: config.proofs.period)
|
||||||
|
|
||||||
request = StorageRequest.example
|
request = StorageRequest.example
|
||||||
|
@ -45,8 +42,7 @@ ethersuite "Marketplace contracts":
|
||||||
await token.approve(marketplace.address, request.price)
|
await token.approve(marketplace.address, request.price)
|
||||||
await marketplace.requestStorage(request)
|
await marketplace.requestStorage(request)
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
await token.approve(marketplace.address, collateral)
|
await token.approve(marketplace.address, request.ask.collateral)
|
||||||
await marketplace.deposit(collateral)
|
|
||||||
await marketplace.fillSlot(request.id, 0.u256, proof)
|
await marketplace.fillSlot(request.id, 0.u256, proof)
|
||||||
slotId = request.slotId(0.u256)
|
slotId = request.slotId(0.u256)
|
||||||
|
|
||||||
|
@ -61,6 +57,7 @@ ethersuite "Marketplace contracts":
|
||||||
|
|
||||||
proc startContract() {.async.} =
|
proc startContract() {.async.} =
|
||||||
for slotIndex in 1..<request.ask.slots:
|
for slotIndex in 1..<request.ask.slots:
|
||||||
|
await token.approve(marketplace.address, request.ask.collateral)
|
||||||
await marketplace.fillSlot(request.id, slotIndex.u256, proof)
|
await marketplace.fillSlot(request.id, slotIndex.u256, proof)
|
||||||
|
|
||||||
test "accept marketplace proofs":
|
test "accept marketplace proofs":
|
||||||
|
@ -85,7 +82,7 @@ ethersuite "Marketplace contracts":
|
||||||
let startBalance = await token.balanceOf(address)
|
let startBalance = await token.balanceOf(address)
|
||||||
await marketplace.freeSlot(slotId)
|
await marketplace.freeSlot(slotId)
|
||||||
let endBalance = await token.balanceOf(address)
|
let endBalance = await token.balanceOf(address)
|
||||||
check endBalance == (startBalance + request.ask.duration * request.ask.reward)
|
check endBalance == (startBalance + request.ask.duration * request.ask.reward + request.ask.collateral)
|
||||||
|
|
||||||
test "cannot mark proofs missing for cancelled request":
|
test "cannot mark proofs missing for cancelled request":
|
||||||
await provider.advanceTimeTo(request.expiry + 1)
|
await provider.advanceTimeTo(request.expiry + 1)
|
||||||
|
|
|
@ -6,14 +6,12 @@ import codex/storageproofs
|
||||||
import ../ethertest
|
import ../ethertest
|
||||||
import ./examples
|
import ./examples
|
||||||
import ./time
|
import ./time
|
||||||
import ./token
|
|
||||||
|
|
||||||
ethersuite "On-Chain Market":
|
ethersuite "On-Chain Market":
|
||||||
let proof = exampleProof()
|
let proof = exampleProof()
|
||||||
|
|
||||||
var market: OnChainMarket
|
var market: OnChainMarket
|
||||||
var marketplace: Marketplace
|
var marketplace: Marketplace
|
||||||
var token: TestToken
|
|
||||||
var request: StorageRequest
|
var request: StorageRequest
|
||||||
var slotIndex: UInt256
|
var slotIndex: UInt256
|
||||||
var periodicity: Periodicity
|
var periodicity: Periodicity
|
||||||
|
@ -21,13 +19,7 @@ ethersuite "On-Chain Market":
|
||||||
setup:
|
setup:
|
||||||
let deployment = Deployment.init()
|
let deployment = Deployment.init()
|
||||||
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
|
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
|
||||||
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
|
|
||||||
await token.mint(accounts[0], 1_000_000_000.u256)
|
|
||||||
|
|
||||||
let config = await marketplace.config()
|
let config = await marketplace.config()
|
||||||
let collateral = config.collateral.initialAmount
|
|
||||||
await token.approve(marketplace.address, collateral)
|
|
||||||
await marketplace.deposit(collateral)
|
|
||||||
|
|
||||||
market = OnChainMarket.new(marketplace)
|
market = OnChainMarket.new(marketplace)
|
||||||
periodicity = Periodicity(seconds: config.proofs.period)
|
periodicity = Periodicity(seconds: config.proofs.period)
|
||||||
|
@ -55,18 +47,15 @@ ethersuite "On-Chain Market":
|
||||||
check (await market.getSigner()) == (await provider.getSigner().getAddress())
|
check (await market.getSigner()) == (await provider.getSigner().getAddress())
|
||||||
|
|
||||||
test "supports marketplace requests":
|
test "supports marketplace requests":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
|
|
||||||
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(marketplace.address, request.price)
|
|
||||||
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(marketplace.address, request.price)
|
|
||||||
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)
|
||||||
|
@ -78,26 +67,22 @@ ethersuite "On-Chain Market":
|
||||||
receivedIds.add(id)
|
receivedIds.add(id)
|
||||||
receivedAsks.add(ask)
|
receivedAsks.add(ask)
|
||||||
let subscription = await market.subscribeRequests(onRequest)
|
let subscription = await market.subscribeRequests(onRequest)
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
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(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
|
|
||||||
test "can retrieve host that filled slot":
|
test "can retrieve host that filled slot":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
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, request.ask.collateral)
|
||||||
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(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
var receivedSlotIndices: seq[UInt256]
|
var receivedSlotIndices: seq[UInt256]
|
||||||
|
@ -105,34 +90,32 @@ ethersuite "On-Chain Market":
|
||||||
receivedIds.add(id)
|
receivedIds.add(id)
|
||||||
receivedSlotIndices.add(slotIndex)
|
receivedSlotIndices.add(slotIndex)
|
||||||
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
check receivedSlotIndices == @[slotIndex]
|
check receivedSlotIndices == @[slotIndex]
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
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(marketplace.address, request.price)
|
|
||||||
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)
|
||||||
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
|
||||||
await market.fillSlot(request.id, otherSlot, proof)
|
await market.fillSlot(request.id, otherSlot, proof, request.ask.collateral)
|
||||||
check receivedSlotIndices.len == 0
|
check receivedSlotIndices.len == 0
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
check receivedSlotIndices == @[slotIndex]
|
check receivedSlotIndices == @[slotIndex]
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
test "support fulfillment subscriptions":
|
test "support fulfillment subscriptions":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
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)
|
||||||
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
||||||
for slotIndex in 0..<request.ask.slots:
|
for slotIndex in 0..<request.ask.slots:
|
||||||
await market.fillSlot(request.id, slotIndex.u256, proof)
|
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
|
@ -140,9 +123,7 @@ ethersuite "On-Chain Market":
|
||||||
var otherRequest = StorageRequest.example
|
var otherRequest = StorageRequest.example
|
||||||
otherRequest.client = accounts[0]
|
otherRequest.client = accounts[0]
|
||||||
|
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await token.approve(marketplace.address, otherRequest.price)
|
|
||||||
await market.requestStorage(otherRequest)
|
await market.requestStorage(otherRequest)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
|
@ -152,16 +133,15 @@ ethersuite "On-Chain Market":
|
||||||
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
|
||||||
|
|
||||||
for slotIndex in 0..<request.ask.slots:
|
for slotIndex in 0..<request.ask.slots:
|
||||||
await market.fillSlot(request.id, slotIndex.u256, proof)
|
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
|
||||||
for slotIndex in 0..<otherRequest.ask.slots:
|
for slotIndex in 0..<otherRequest.ask.slots:
|
||||||
await market.fillSlot(otherRequest.id, slotIndex.u256, proof)
|
await market.fillSlot(otherRequest.id, slotIndex.u256, proof, otherRequest.ask.collateral)
|
||||||
|
|
||||||
check receivedIds == @[request.id]
|
check receivedIds == @[request.id]
|
||||||
|
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
test "support request cancelled subscriptions":
|
test "support request cancelled subscriptions":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
|
@ -175,7 +155,6 @@ ethersuite "On-Chain Market":
|
||||||
await subscription.unsubscribe()
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
test "support request failed subscriptions":
|
test "support request failed subscriptions":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
|
@ -184,7 +163,7 @@ ethersuite "On-Chain Market":
|
||||||
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
|
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
|
||||||
|
|
||||||
for slotIndex in 0..<request.ask.slots:
|
for slotIndex in 0..<request.ask.slots:
|
||||||
await market.fillSlot(request.id, slotIndex.u256, proof)
|
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
|
||||||
for slotIndex in 0..request.ask.maxSlotLoss:
|
for slotIndex in 0..request.ask.maxSlotLoss:
|
||||||
let slotId = request.slotId(slotIndex.u256)
|
let slotId = request.slotId(slotIndex.u256)
|
||||||
while true:
|
while true:
|
||||||
|
@ -201,9 +180,7 @@ ethersuite "On-Chain Market":
|
||||||
test "subscribes only to a certain request cancellation":
|
test "subscribes only to a certain request cancellation":
|
||||||
var otherRequest = request
|
var otherRequest = request
|
||||||
otherRequest.nonce = Nonce.example
|
otherRequest.nonce = Nonce.example
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await token.approve(marketplace.address, otherRequest.price)
|
|
||||||
await market.requestStorage(otherRequest)
|
await market.requestStorage(otherRequest)
|
||||||
|
|
||||||
var receivedIds: seq[RequestId]
|
var receivedIds: seq[RequestId]
|
||||||
|
@ -222,11 +199,9 @@ ethersuite "On-Chain Market":
|
||||||
check isNone await market.getRequest(request.id)
|
check isNone await market.getRequest(request.id)
|
||||||
|
|
||||||
test "can retrieve active requests":
|
test "can retrieve active requests":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
var request2 = StorageRequest.example
|
var request2 = StorageRequest.example
|
||||||
request2.client = accounts[0]
|
request2.client = accounts[0]
|
||||||
await token.approve(marketplace.address, request2.price)
|
|
||||||
await market.requestStorage(request2)
|
await market.requestStorage(request2)
|
||||||
check (await market.myRequests()) == @[request.id, request2.id]
|
check (await market.myRequests()) == @[request.id, request2.id]
|
||||||
|
|
||||||
|
@ -234,31 +209,27 @@ ethersuite "On-Chain Market":
|
||||||
check (await market.requestState(request.id)) == none RequestState
|
check (await market.requestState(request.id)) == none RequestState
|
||||||
|
|
||||||
test "can retrieve request state":
|
test "can retrieve request state":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
for slotIndex in 0..<request.ask.slots:
|
for slotIndex in 0..<request.ask.slots:
|
||||||
await market.fillSlot(request.id, slotIndex.u256, proof)
|
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
|
||||||
check (await market.requestState(request.id)) == some RequestState.Started
|
check (await market.requestState(request.id)) == some RequestState.Started
|
||||||
|
|
||||||
test "can retrieve active slots":
|
test "can retrieve active slots":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await market.fillSlot(request.id, slotIndex - 1, proof)
|
await market.fillSlot(request.id, slotIndex - 1, proof, request.ask.collateral)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
let slotId1 = request.slotId(slotIndex - 1)
|
let slotId1 = request.slotId(slotIndex - 1)
|
||||||
let slotId2 = request.slotId(slotIndex)
|
let slotId2 = request.slotId(slotIndex)
|
||||||
check (await market.mySlots()) == @[slotId1, slotId2]
|
check (await market.mySlots()) == @[slotId1, slotId2]
|
||||||
|
|
||||||
test "returns none when slot is empty":
|
test "returns none when slot is empty":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
let slotId = request.slotId(slotIndex)
|
let slotId = request.slotId(slotIndex)
|
||||||
check (await market.getActiveSlot(slotId)) == none Slot
|
check (await market.getActiveSlot(slotId)) == none Slot
|
||||||
|
|
||||||
test "can retrieve request details from slot id":
|
test "can retrieve request details from slot id":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
let slotId = request.slotId(slotIndex)
|
let slotId = request.slotId(slotIndex)
|
||||||
let expected = Slot(request: request, slotIndex: slotIndex)
|
let expected = Slot(request: request, slotIndex: slotIndex)
|
||||||
check (await market.getActiveSlot(slotId)) == some expected
|
check (await market.getActiveSlot(slotId)) == some expected
|
||||||
|
@ -268,8 +239,7 @@ ethersuite "On-Chain Market":
|
||||||
check (await market.slotState(slotId)) == SlotState.Free
|
check (await market.slotState(slotId)) == SlotState.Free
|
||||||
|
|
||||||
test "retrieves correct slot state once filled":
|
test "retrieves correct slot state once filled":
|
||||||
await token.approve(marketplace.address, request.price)
|
|
||||||
await market.requestStorage(request)
|
await market.requestStorage(request)
|
||||||
await market.fillSlot(request.id, slotIndex, proof)
|
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
|
||||||
let slotId = request.slotId(slotIndex)
|
let slotId = request.slotId(slotIndex)
|
||||||
check (await market.slotState(slotId)) == SlotState.Filled
|
check (await market.slotState(slotId)) == SlotState.Filled
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import pkg/chronos
|
|
||||||
import pkg/stint
|
|
||||||
import pkg/ethers
|
|
||||||
import pkg/ethers/erc20
|
|
||||||
|
|
||||||
type
|
|
||||||
TestToken* = ref object of Erc20Token
|
|
||||||
|
|
||||||
proc mint*(token: TestToken, holder: Address, amount: UInt256) {.contract.}
|
|
|
@ -28,6 +28,7 @@ proc example*(_: type StorageRequest): StorageRequest =
|
||||||
slots: 4,
|
slots: 4,
|
||||||
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
|
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
|
||||||
duration: (10 * 60 * 60).u256, # 10 hours
|
duration: (10 * 60 * 60).u256, # 10 hours
|
||||||
|
collateral: 200.u256,
|
||||||
proofProbability: 4.u256, # require a proof roughly once every 4 periods
|
proofProbability: 4.u256, # require a proof roughly once every 4 periods
|
||||||
reward: 84.u256,
|
reward: 84.u256,
|
||||||
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
|
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
|
||||||
|
|
|
@ -31,13 +31,15 @@ proc requestStorage*(client: CodexClient,
|
||||||
duration: uint64,
|
duration: uint64,
|
||||||
reward: uint64,
|
reward: uint64,
|
||||||
proofProbability: uint64,
|
proofProbability: uint64,
|
||||||
expiry: UInt256): string =
|
expiry: UInt256,
|
||||||
|
collateral: uint64): string =
|
||||||
let url = client.baseurl & "/storage/request/" & cid
|
let url = client.baseurl & "/storage/request/" & cid
|
||||||
let json = %*{
|
let json = %*{
|
||||||
"duration": "0x" & duration.toHex,
|
"duration": "0x" & duration.toHex,
|
||||||
"reward": "0x" & reward.toHex,
|
"reward": "0x" & reward.toHex,
|
||||||
"proofProbability": "0x" & proofProbability.toHex,
|
"proofProbability": "0x" & proofProbability.toHex,
|
||||||
"expiry": "0x" & expiry.toHex
|
"expiry": "0x" & expiry.toHex,
|
||||||
|
"collateral": "0x" & collateral.toHex,
|
||||||
}
|
}
|
||||||
let response = client.http.post(url, $json)
|
let response = client.http.post(url, $json)
|
||||||
assert response.status == "200 OK"
|
assert response.status == "200 OK"
|
||||||
|
@ -49,12 +51,13 @@ proc getPurchase*(client: CodexClient, purchase: string): JsonNode =
|
||||||
parseJson(body).catch |? nil
|
parseJson(body).catch |? nil
|
||||||
|
|
||||||
proc postAvailability*(client: CodexClient,
|
proc postAvailability*(client: CodexClient,
|
||||||
size, duration, minPrice: uint64): JsonNode =
|
size, duration, minPrice: uint64, maxCollateral: uint64): JsonNode =
|
||||||
let url = client.baseurl & "/sales/availability"
|
let url = client.baseurl & "/sales/availability"
|
||||||
let json = %*{
|
let json = %*{
|
||||||
"size": "0x" & size.toHex,
|
"size": "0x" & size.toHex,
|
||||||
"duration": "0x" & duration.toHex,
|
"duration": "0x" & duration.toHex,
|
||||||
"minPrice": "0x" & minPrice.toHex
|
"minPrice": "0x" & minPrice.toHex,
|
||||||
|
"maxCollateral": "0x" & maxCollateral.toHex
|
||||||
}
|
}
|
||||||
let response = client.http.post(url, $json)
|
let response = client.http.post(url, $json)
|
||||||
assert response.status == "200 OK"
|
assert response.status == "200 OK"
|
||||||
|
|
|
@ -4,14 +4,14 @@ import pkg/stint
|
||||||
import ../contracts/time
|
import ../contracts/time
|
||||||
import ../codex/helpers/eventually
|
import ../codex/helpers/eventually
|
||||||
import ./twonodes
|
import ./twonodes
|
||||||
import ./tokens
|
|
||||||
|
|
||||||
twonodessuite "Integration tests", debug1 = false, debug2 = false:
|
twonodessuite "Integration tests", debug1 = false, debug2 = false:
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
await provider.getSigner(accounts[0]).mint()
|
# Our Hardhat configuration does use automine, which means that time tracked by `provider.currentTime()` is not
|
||||||
await provider.getSigner(accounts[1]).mint()
|
# advanced until blocks are mined and that happens only when transaction is submitted.
|
||||||
await provider.getSigner(accounts[1]).deposit()
|
# As we use in tests provider.currentTime() which uses block timestamp this can lead to synchronization issues.
|
||||||
|
await provider.advanceTime(1.u256)
|
||||||
|
|
||||||
test "nodes can print their peer information":
|
test "nodes can print their peer information":
|
||||||
check client1.info() != client2.info()
|
check client1.info() != client2.info()
|
||||||
|
@ -25,25 +25,25 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
|
||||||
check cid1 != cid2
|
check cid1 != cid2
|
||||||
|
|
||||||
test "node handles new storage availability":
|
test "node handles new storage availability":
|
||||||
let availability1 = client1.postAvailability(size=1, duration=2, minPrice=3)
|
let availability1 = client1.postAvailability(size=1, duration=2, minPrice=3, maxCollateral=4)
|
||||||
let availability2 = client1.postAvailability(size=4, duration=5, minPrice=6)
|
let availability2 = client1.postAvailability(size=4, duration=5, minPrice=6, maxCollateral=7)
|
||||||
check availability1 != availability2
|
check availability1 != availability2
|
||||||
|
|
||||||
test "node lists storage that is for sale":
|
test "node lists storage that is for sale":
|
||||||
let availability = client1.postAvailability(size=1, duration=2, minPrice=3)
|
let availability = client1.postAvailability(size=1, duration=2, minPrice=3, maxCollateral=4)
|
||||||
check availability in client1.getAvailabilities()
|
check availability in client1.getAvailabilities()
|
||||||
|
|
||||||
test "node handles storage request":
|
test "node handles storage request":
|
||||||
let expiry = (await provider.currentTime()) + 30
|
let expiry = (await provider.currentTime()) + 30
|
||||||
let cid = client1.upload("some file contents")
|
let cid = client1.upload("some file contents")
|
||||||
let id1 = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
|
let id1 = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
|
||||||
let id2 = client1.requestStorage(cid, duration=4, reward=5, proofProbability=6, expiry=expiry)
|
let id2 = client1.requestStorage(cid, duration=4, reward=5, proofProbability=6, expiry=expiry, collateral=201)
|
||||||
check id1 != id2
|
check id1 != id2
|
||||||
|
|
||||||
test "node retrieves purchase status":
|
test "node retrieves purchase status":
|
||||||
let expiry = (await provider.currentTime()) + 30
|
let expiry = (await provider.currentTime()) + 30
|
||||||
let cid = client1.upload("some file contents")
|
let cid = client1.upload("some file contents")
|
||||||
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
|
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
|
||||||
let purchase = client1.getPurchase(id)
|
let purchase = client1.getPurchase(id)
|
||||||
check purchase{"request"}{"ask"}{"duration"} == %"0x1"
|
check purchase{"request"}{"ask"}{"duration"} == %"0x1"
|
||||||
check purchase{"request"}{"ask"}{"reward"} == %"0x2"
|
check purchase{"request"}{"ask"}{"reward"} == %"0x2"
|
||||||
|
@ -52,7 +52,7 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
|
||||||
test "node remembers purchase status after restart":
|
test "node remembers purchase status after restart":
|
||||||
let expiry = (await provider.currentTime()) + 30
|
let expiry = (await provider.currentTime()) + 30
|
||||||
let cid = client1.upload("some file contents")
|
let cid = client1.upload("some file contents")
|
||||||
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
|
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
|
||||||
check eventually client1.getPurchase(id){"state"}.getStr() == "submitted"
|
check eventually client1.getPurchase(id){"state"}.getStr() == "submitted"
|
||||||
|
|
||||||
node1.restart()
|
node1.restart()
|
||||||
|
@ -65,12 +65,12 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
|
||||||
test "nodes negotiate contracts on the marketplace":
|
test "nodes negotiate contracts on the marketplace":
|
||||||
let size: uint64 = 0xFFFFF
|
let size: uint64 = 0xFFFFF
|
||||||
# client 2 makes storage available
|
# client 2 makes storage available
|
||||||
discard client2.postAvailability(size=size, duration=200, minPrice=300)
|
discard client2.postAvailability(size=size, duration=200, minPrice=300, maxCollateral=300)
|
||||||
|
|
||||||
# client 1 requests storage
|
# client 1 requests storage
|
||||||
let expiry = (await provider.currentTime()) + 30
|
let expiry = (await provider.currentTime()) + 30
|
||||||
let cid = client1.upload("some file contents")
|
let cid = client1.upload("some file contents")
|
||||||
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry)
|
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry, collateral=200)
|
||||||
|
|
||||||
check eventually client1.getPurchase(purchase){"state"} == %"started"
|
check eventually client1.getPurchase(purchase){"state"} == %"started"
|
||||||
check client1.getPurchase(purchase){"error"} == newJNull()
|
check client1.getPurchase(purchase){"error"} == newJNull()
|
||||||
|
|
|
@ -3,7 +3,6 @@ import codex/contracts/deployment
|
||||||
import ../contracts/time
|
import ../contracts/time
|
||||||
import ../codex/helpers/eventually
|
import ../codex/helpers/eventually
|
||||||
import ./twonodes
|
import ./twonodes
|
||||||
import ./tokens
|
|
||||||
|
|
||||||
twonodessuite "Proving integration test", debug1=false, debug2=false:
|
twonodessuite "Proving integration test", debug1=false, debug2=false:
|
||||||
|
|
||||||
|
@ -14,15 +13,17 @@ twonodessuite "Proving integration test", debug1=false, debug2=false:
|
||||||
let deployment = Deployment.init()
|
let deployment = Deployment.init()
|
||||||
marketplace = Marketplace.new(!deployment.address(Marketplace), provider)
|
marketplace = Marketplace.new(!deployment.address(Marketplace), provider)
|
||||||
config = await marketplace.config()
|
config = await marketplace.config()
|
||||||
await provider.getSigner(accounts[0]).mint()
|
|
||||||
await provider.getSigner(accounts[1]).mint()
|
# Our Hardhat configuration does use automine, which means that time tracked by `provider.currentTime()` is not
|
||||||
await provider.getSigner(accounts[1]).deposit()
|
# advanced until blocks are mined and that happens only when transaction is submitted.
|
||||||
|
# As we use in tests provider.currentTime() which uses block timestamp this can lead to synchronization issues.
|
||||||
|
await provider.advanceTime(1.u256)
|
||||||
|
|
||||||
proc waitUntilPurchaseIsStarted {.async.} =
|
proc waitUntilPurchaseIsStarted {.async.} =
|
||||||
discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300)
|
discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300, maxCollateral=200)
|
||||||
let expiry = (await provider.currentTime()) + 30
|
let expiry = (await provider.currentTime()) + 30
|
||||||
let cid = client1.upload("some file contents")
|
let cid = client1.upload("some file contents")
|
||||||
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry)
|
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry, collateral=100)
|
||||||
check eventually client1.getPurchase(purchase){"state"} == %"started"
|
check eventually client1.getPurchase(purchase){"state"} == %"started"
|
||||||
|
|
||||||
test "hosts submit periodic proofs for slots they fill":
|
test "hosts submit periodic proofs for slots they fill":
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import pkg/ethers/erc20
|
|
||||||
import codex/contracts
|
|
||||||
import ../contracts/token
|
|
||||||
|
|
||||||
proc mint*(signer: Signer, amount = 1_000_000.u256) {.async.} =
|
|
||||||
## Mints a considerable amount of tokens and approves them for transfer to
|
|
||||||
## the Marketplace contract.
|
|
||||||
let deployment = Deployment.init()
|
|
||||||
let token = TestToken.new(!deployment.address(TestToken), signer)
|
|
||||||
let marketplace = Marketplace.new(!deployment.address(Marketplace), signer)
|
|
||||||
await token.mint(await signer.getAddress(), amount)
|
|
||||||
|
|
||||||
proc deposit*(signer: Signer) {.async.} =
|
|
||||||
## Deposits sufficient collateral into the Marketplace contract.
|
|
||||||
let deployment = Deployment.init()
|
|
||||||
let marketplace = Marketplace.new(!deployment.address(Marketplace), signer)
|
|
||||||
let config = await marketplace.config()
|
|
||||||
let tokenAddress = await marketplace.token()
|
|
||||||
let token = Erc20Token.new(tokenAddress, signer)
|
|
||||||
|
|
||||||
await token.approve(marketplace.address, config.collateral.initialAmount)
|
|
||||||
await marketplace.deposit(config.collateral.initialAmount)
|
|
|
@ -1,4 +1,3 @@
|
||||||
import ./contracts/testCollateral
|
|
||||||
import ./contracts/testContracts
|
import ./contracts/testContracts
|
||||||
import ./contracts/testMarket
|
import ./contracts/testMarket
|
||||||
import ./contracts/testProofs
|
import ./contracts/testProofs
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit fb76f7d0b2f94914b00f2a0f4136ebfb27df6abc
|
Subproject commit 6e66abbfcd9be6cbd0434dc5a80f1419c66a914e
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6cbbda7e4d009e02d0583b325b31dc68dff27854
|
Subproject commit 30e4184a99c8c1ba329925912d2c5d4b09acf8cc
|
Loading…
Reference in New Issue