Skip to content
This repository has been archived by the owner on Nov 2, 2018. It is now read-only.

Add API endpoint to cancel a Renter's contract #3164

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions modules/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ 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 active contracts formed by the renter.
// Contracts returns the staticContracts of the renter's hostContractor.
Contracts() []RenterContract

Expand Down
10 changes: 10 additions & 0 deletions modules/renter/contractor/contractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,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
5 changes: 5 additions & 0 deletions modules/renter/contractor/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ type hostContractor interface {
// Close closes the hostContractor.
Close() error

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

// Contracts returns the active contracts formed by the contractor.
// Contracts returns the staticContracts of the renter's hostContractor.
Contracts() []modules.RenterContract

Expand Down Expand Up @@ -388,6 +392,12 @@ 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 active contracts
// 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 (

"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/node/api"
"github.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
13 changes: 13 additions & 0 deletions node/api/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,19 @@ 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 {
return
}
err := api.renter.CancelContract(fcid)
if err != nil {
return
}
WriteSuccess(w)
}

// renterContractsHandler handles the API call to request the Renter's
// contracts.
//
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
92 changes: 92 additions & 0 deletions siatest/renter/renter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,98 @@ func TestRenterCancelAllowance(t *testing.T) {
}
}

// TestRenterCancelContract tests the RenterCancelContract Endpoint
func TestRenterCancelContract(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
t.Parallel()

// Create a group for testing.
groupParams := siatest.GroupParams{
Hosts: 2,
Renters: 1,
Miners: 1,
}
tg, err := siatest.NewGroupFromTemplate(groupParams)
if err != nil {
t.Fatal("Failed to create group: ", err)
}
defer func() {
if err := tg.Close(); err != nil {
t.Fatal(err)
}
}()

// Grab the first of the group's renters
r := tg.Renters()[0]

// Grab contracts
rc, err := r.RenterContractsGet()
if err != nil {
t.Fatal(err)
}

// 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
hostDir, err := siatest.TestDir(filepath.Join(t.Name(), "host"))
if err != nil {
t.Fatal(err)
}
hostParams := node.Host(hostDir)
_, err = tg.AddNodes(hostParams)
if err != nil {
t.Fatal(err)
}

// Mine block to trigger threadedContractMaintenance
m := tg.Miners()[0]
if err := m.MineBlock(); err != nil {
t.Fatal(err)
}
if err := tg.Sync(); 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()))
}
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)
}
}

// TestRenterContractEndHeight makes sure that the endheight of renewed
// contracts is set properly
func TestRenterContractEndHeight(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions types/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ func (fcr *FileContractRevision) UnmarshalSia(r io.Reader) error {
return d.Err()
}

// LoadString loads a FileContractID from a string
func (fcid *FileContractID) LoadString(str string) error {
return (*crypto.Hash)(fcid).LoadString(str)
}

// MarshalJSON marshals an id as a hex string.
func (fcid FileContractID) MarshalJSON() ([]byte, error) {
return json.Marshal(fcid.String())
Expand Down