Skip to content

Commit

Permalink
Merge branch 'cancel-contract' into 'master'
Browse files Browse the repository at this point in the history
Add API endpoint to cancel a Renter's contract

Closes NebulousLabs#3159

See merge request NebulousLabs/Sia!3164
  • Loading branch information
lukechampine committed Aug 28, 2018
2 parents 4f3bca7 + 895d13c commit 110f45b
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 2 deletions.
15 changes: 15 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ Renter
| --------------------------------------------------------------------------| --------- |
| [/renter](#renter-get) | GET |
| [/renter](#renter-post) | POST |
| [/renter/contract/cancel](#rentercontractcancel-post) | POST |
| [/renter/contracts](#rentercontracts-get) | GET |
| [/renter/downloads](#renterdownloads-get) | GET |
| [/renter/downloads/clear](#renterdownloadsclear-post) | POST |
Expand Down Expand Up @@ -933,6 +934,20 @@ streamcachesize // number of data chunks cached when streaming
standard success or error response. See
[#standard-responses](#standard-responses).

#### /renter/contract/cancel [POST]

cancels a specific contract of the Renter.

###### Query String Parameters [(with comments)](/doc/api/Renter.md#query-string-parameter)
```
// ID of the file contract
id
```

###### Response
standard success or error response. See
[API.md#standard-responses](/doc/API.md#standard-responses).

#### /renter/contracts [GET]

returns the renter's contracts. Active contracts are contracts that the Renter
Expand Down
15 changes: 15 additions & 0 deletions doc/api/Renter.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Index
| ------------------------------------------------------------------------------- | --------- |
| [/renter](#renter-get) | GET |
| [/renter](#renter-post) | POST |
| [/renter/contract/cancel](#rentercontractcancel-post) | POST |
| [/renter/contracts](#rentercontracts-get) | GET |
| [/renter/downloads](#renterdownloads-get) | GET |
| [/renter/downloads/clear](#renterdownloadsclear-post) | POST |
Expand Down Expand Up @@ -146,6 +147,20 @@ streamcachesize
standard success or error response. See
[API.md#standard-responses](/doc/API.md#standard-responses).

#### /renter/contract/cancel [POST]

cancels a specific contract of the Renter.

###### Query String Parameter
```
// ID of the file contract
id
```

###### Response
standard success or error response. See
[API.md#standard-responses](/doc/API.md#standard-responses).

#### /renter/contracts [GET]

returns the renter's contracts. Active contracts are contracts that the Renter
Expand Down
3 changes: 3 additions & 0 deletions modules/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ type Renter interface {
// Close closes the Renter.
Close() error

// CancelContract cancels a specific contract of the renter.
CancelContract(id types.FileContractID) error

// Contracts returns the staticContracts of the renter's hostContractor.
Contracts() []RenterContract

Expand Down
5 changes: 5 additions & 0 deletions modules/renter/contractor/contractmaintenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ func (c *Contractor) managedMarkContractsUtility() error {
// Update utility fields for each contract.
for _, contract := range c.staticContracts.ViewAll() {
utility := func() (u modules.ContractUtility) {
// Record current utility of the contract
u.GoodForRenew = contract.Utility.GoodForRenew
u.GoodForUpload = contract.Utility.GoodForUpload
u.Locked = contract.Utility.Locked

// Start the contract in good standing if the utility wasn't
// locked.
if !u.Locked {
Expand Down
10 changes: 10 additions & 0 deletions modules/renter/contractor/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ func (c *Contractor) ContractByPublicKey(pk types.SiaPublicKey) (modules.RenterC
return c.staticContracts.View(id)
}

// CancelContract cancels the Contractor's contract by marking it !GoodForRenew
// and !GoodForUpload
func (c *Contractor) CancelContract(id types.FileContractID) error {
return c.managedUpdateContractUtility(id, modules.ContractUtility{
GoodForRenew: false,
GoodForUpload: false,
Locked: true,
})
}

// Contracts returns the contracts formed by the contractor in the current
// allowance period. Only contracts formed with currently online hosts are
// returned.
Expand Down
8 changes: 8 additions & 0 deletions modules/renter/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ type hostContractor interface {
// Close closes the hostContractor.
Close() error

// CancelContract cancels the Renter's contract
CancelContract(id types.FileContractID) error

// Contracts returns the staticContracts of the renter's hostContractor.
Contracts() []modules.RenterContract

Expand Down Expand Up @@ -388,6 +391,11 @@ func (r *Renter) EstimateHostScore(e modules.HostDBEntry) modules.HostScoreBreak
return r.hostDB.EstimateHostScore(e)
}

// CancelContract cancels a renter's contract by ID by setting goodForRenew and goodForUpload to false
func (r *Renter) CancelContract(id types.FileContractID) error {
return r.hostContractor.CancelContract(id)
}

// Contracts returns an array of host contractor's staticContracts
func (r *Renter) Contracts() []modules.RenterContract { return r.hostContractor.Contracts() }

Expand Down
10 changes: 10 additions & 0 deletions node/api/client/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ import (

"gitlab.com/NebulousLabs/Sia/modules"
"gitlab.com/NebulousLabs/Sia/node/api"
"gitlab.com/NebulousLabs/Sia/types"
)

// RenterContractCancelPost uses the /renter/contract/cancel endpoint to cancel
// a contract
func (c *Client) RenterContractCancelPost(id types.FileContractID) error {
values := url.Values{}
values.Set("id", id.String())
err := c.post("/renter/contract/cancel", values.Encode(), nil)
return err
}

// RenterContractsGet requests the /renter/contracts resource and returns
// Contracts and ActiveContracts
func (c *Client) RenterContractsGet() (rc api.RenterContracts, err error) {
Expand Down
17 changes: 17 additions & 0 deletions node/api/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,21 @@ func (api *API) renterHandlerPOST(w http.ResponseWriter, req *http.Request, _ ht
WriteSuccess(w)
}

// renterContractCancelHandler handles the API call to cancel a specific Renter contract.
func (api *API) renterContractCancelHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
var fcid types.FileContractID
if err := fcid.LoadString(req.FormValue("id")); err != nil {
WriteError(w, Error{"unable to parse id:" + err.Error()}, http.StatusBadRequest)
return
}
err := api.renter.CancelContract(fcid)
if err != nil {
WriteError(w, Error{"unable to cancel contract:" + err.Error()}, http.StatusBadRequest)
return
}
WriteSuccess(w)
}

// renterContractsHandler handles the API call to request the Renter's
// contracts.
//
Expand All @@ -290,10 +305,12 @@ func (api *API) renterContractsHandler(w http.ResponseWriter, req *http.Request,
// Parse flags
inactive, err := scanBool(req.FormValue("inactive"))
if err != nil {
WriteError(w, Error{"unable to parse inactive:" + err.Error()}, http.StatusBadRequest)
return
}
expired, err := scanBool(req.FormValue("expired"))
if err != nil {
WriteError(w, Error{"unable to parse expired:" + err.Error()}, http.StatusBadRequest)
return
}

Expand Down
1 change: 1 addition & 0 deletions node/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (api *API) buildHTTPRoutes(requiredUserAgent string, requiredPassword strin
if api.renter != nil {
router.GET("/renter", api.renterHandlerGET)
router.POST("/renter", RequirePassword(api.renterHandlerPOST, requiredPassword))
router.POST("/renter/contract/cancel", RequirePassword(api.renterContractCancelHandler, requiredPassword))
router.GET("/renter/contracts", api.renterContractsHandler)
router.GET("/renter/downloads", api.renterDownloadsHandler)
router.POST("/renter/downloads/clear", RequirePassword(api.renterClearDownloadsHandler, requiredPassword))
Expand Down
51 changes: 49 additions & 2 deletions siatest/renter/renter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ func TestRenterCancelAllowance(t *testing.T) {
}

// TestRenterContractEndHeight makes sure that the endheight of renewed
// contracts is set properly
// contracts is set properly, this test also tests canceling a contract
func TestRenterContractEndHeight(t *testing.T) {
if testing.Short() {
t.SkipNow()
Expand All @@ -1233,7 +1233,8 @@ func TestRenterContractEndHeight(t *testing.T) {
Renters: 1,
Miners: 1,
}
tg, err := siatest.NewGroupFromTemplate(renterTestDir(t.Name()), groupParams)
testDir := renterTestDir(t.Name())
tg, err := siatest.NewGroupFromTemplate(testDir, groupParams)
if err != nil {
t.Fatal("Failed to create group: ", err)
}
Expand Down Expand Up @@ -1391,6 +1392,52 @@ func TestRenterContractEndHeight(t *testing.T) {
t.Fatalf("Contract endheight Changed, EH was %v, expected %v\n", c.EndHeight, endHeight)
}
}

// Test canceling contract
// Grab contract to cancel
contract := rc.ActiveContracts[0]
// Cancel Contract
if err := r.RenterContractCancelPost(contract.ID); err != nil {
t.Fatal(err)
}

// Add a new host so new contract can be formed
hostParams := node.Host(testDir + "/host")
_, err = tg.AddNodes(hostParams)
if err != nil {
t.Fatal(err)
}

err = build.Retry(200, 100*time.Millisecond, func() error {
// Check that Contract is now in inactive contracts and no longer in Active contracts
rc, err = r.RenterInactiveContractsGet()
if err != nil {
t.Fatal(err)
}
// Confirm Renter has the expected number of contracts, meaning canceled contract should have been replaced.
if len(rc.ActiveContracts) != len(tg.Hosts())-1 {
return fmt.Errorf("Canceled contract was not replaced, only %v active contracts, expected %v", len(rc.ActiveContracts), len(tg.Hosts())-1)
}
for _, c := range rc.ActiveContracts {
if c.ID == contract.ID {
return errors.New("Contract not cancelled, contract found in Active Contracts")
}
}
i := 1
for _, c := range rc.InactiveContracts {
if c.ID == contract.ID {
break
}
if i == len(rc.InactiveContracts) {
return errors.New("Contract not found in Inactive Contracts")
}
i++
}
return nil
})
if err != nil {
t.Fatal(err)
}
}

// TestRenterContractsEndpoint tests the API endpoint for old contracts
Expand Down

0 comments on commit 110f45b

Please sign in to comment.