Skip to content

Commit

Permalink
Merge pull request etherapis#25 from karalabe/service-pending-ops
Browse files Browse the repository at this point in the history
etherapis, etherapis/dashboard: add service life cycle management
  • Loading branch information
karalabe committed Mar 26, 2016
2 parents 6bea71c + b89264c commit 808c2c2
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 99 deletions.
35 changes: 18 additions & 17 deletions etherapis/contract/contract.go

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions etherapis/contract/contract.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
contract ServiceProviders {
// PaymentModel is the possible payment models that proxies should be able to handle.
enum PaymentModel {PerCall, PerData, PerTime}

struct Terms {
uint price;
uint cancellation;
PaymentModel model;
uint price;
uint cancellation;
}

struct Service {
Expand Down Expand Up @@ -38,6 +42,7 @@ contract ServiceProviders {
string name,
address owner,
string endpoint,
uint model,
uint price,
uint cancellation,
bool enabled,
Expand All @@ -48,6 +53,7 @@ contract ServiceProviders {
service.name,
service.owner,
service.endpoint,
uint(service.terms.model),
service.terms.price,
service.terms.cancellation,
service.enabled,
Expand All @@ -71,14 +77,15 @@ contract ServiceProviders {
}

// Add a new service.
function addService(string name, string endpoint, uint price, uint cancellation) {
function addService(string name, string endpoint, uint model, uint price, uint cancellation) {
Service service = services[services.length++];
service.exist = true;
service.enabled = false;
service.id = services.length-1;
service.owner = msg.sender;
service.name = name;
service.endpoint = endpoint;
service.terms.model = PaymentModel(model);
service.terms.price = price;
service.terms.cancellation = cancellation;

Expand Down
4 changes: 2 additions & 2 deletions etherapis/contract/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestDeploymentAndIntegration(t *testing.T) {

// use a session based approach so that we do not need
// to repass these settings all the time.
session := &EtherApisSession{
session := &EtherAPIsSession{
Contract: api,
CallOpts: bind.CallOpts{
Pending: true,
Expand All @@ -38,7 +38,7 @@ func TestDeploymentAndIntegration(t *testing.T) {
}

// add a new service
_, err = session.AddService("etherapis", "https://etherapis.io", big.NewInt(10), big.NewInt(432000))
_, err = session.AddService("etherapis", "https://etherapis.io", big.NewInt(0), big.NewInt(10), big.NewInt(432000))
if err != nil {
t.Error(err)
}
Expand Down
42 changes: 40 additions & 2 deletions etherapis/dashboard/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func newAPIServeMux(base string, eapis *etherapis.EtherAPIs) *mux.Router {
router.HandleFunc(base+"accounts/{address:0(x|X)[0-9a-fA-F]{40}}", handler.Account)
router.HandleFunc(base+"accounts/{address:0(x|X)[0-9a-fA-F]{40}}/transactions", handler.Transactions)
router.HandleFunc(base+"services/{address:0(x|X)[0-9a-fA-F]{40}}", handler.Services)
router.HandleFunc(base+"services/{address:0(x|X)[0-9a-fA-F]{40}}/{id:[0-9]+}", handler.Services)

return router
}
Expand Down Expand Up @@ -177,13 +178,19 @@ func (a *api) Services(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)

switch {
case r.Method == "POST":
case r.Method == "POST" && len(params["id"]) == 0:
// Create a brand new service based on parameters
var (
owner = common.HexToAddress(params["address"])
name = r.FormValue("name")
url = r.FormValue("url")
)
model, ok := new(big.Int).SetString(r.FormValue("model"), 10)
if !ok || model.Cmp(big.NewInt(0)) < 0 || model.Cmp(big.NewInt(2)) > 0 {
log15.Error("Invalid payment model for new service", "model", r.FormValue("model"))
http.Error(w, fmt.Sprintf("Invalid payment model: %s", r.FormValue("model")), http.StatusBadRequest)
return
}
price, ok := new(big.Int).SetString(r.FormValue("price"), 10)
if !ok {
log15.Error("Invalid price for new service", "price", r.FormValue("price"))
Expand All @@ -196,14 +203,45 @@ func (a *api) Services(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("Invalid cancellation time: %s", r.FormValue("cancel")), http.StatusBadRequest)
return
}
tx, err := a.eapis.CreateService(owner, name, url, price, cancel.Uint64())
tx, err := a.eapis.CreateService(owner, name, url, model, price, cancel.Uint64())
if err != nil {
log15.Error("Failed to register service", "error", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Write([]byte(fmt.Sprintf("0x%x", tx.Hash())))

case r.Method == "POST" && len(params["id"]) > 0:
// Modify the status of an existing service
id, _ := new(big.Int).SetString(params["id"], 10)

switch r.FormValue("action") {
case "lock":
if _, err := a.eapis.LockService(common.HexToAddress(params["address"]), id); err != nil {
log15.Error("Failed to lock service", "id", id, "owner", params["address"], "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
case "unlock":
if _, err := a.eapis.UnlockService(common.HexToAddress(params["address"]), id); err != nil {
log15.Error("Failed to unlock service", "id", id, "owner", params["address"], "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
default:
http.Error(w, "Unsupported service action: "+r.FormValue("action"), http.StatusMethodNotAllowed)
}

case r.Method == "DELETE":
// Delete the service and return an error if something goes wrong
id, _ := new(big.Int).SetString(params["id"], 10)

if _, err := a.eapis.DeleteService(common.HexToAddress(params["address"]), id); err != nil {
log15.Error("Failed to delete service", "id", id, "owner", params["address"], "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

default:
http.Error(w, "Unsupported method: "+r.Method, http.StatusMethodNotAllowed)
}
Expand Down
8 changes: 4 additions & 4 deletions etherapis/dashboard/assets.go

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions etherapis/dashboard/assets/components/market.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ var Market = React.createClass({
<table className="table table-condensed">
<thead>
<tr>
<th className="text-nowrap"><i className="fa fa-bookmark"></i> Name</th>
<th className="text-nowrap"><i className="fa fa-star"></i> Rating</th>
<th className="text-nowrap"><i className="fa fa-bookmark-o"></i> Name</th>
<th className="text-nowrap"><i className="fa fa-star-half-o"></i> Rating</th>
<th className="text-nowrap"><i className="fa fa-link"></i> Endpoint</th>
<th className="text-nowrap"><i className="fa fa-user"></i> Owner</th>
<th className="text-nowrap">&Xi; Price</th>
<th className="text-nowrap"><i className="fa fa-ban"></i> Cancellation</th>
<th className="text-nowrap text-center"><i className="fa fa-credit-card-alt"></i> Model</th>
<th className="text-nowrap text-center">&Xi; Price</th>
<th className="text-nowrap text-center"><i className="fa fa-ban"></i> Cancellation</th>
<th className="text-nowrap"></th>
</tr>
</thead>
Expand All @@ -30,6 +31,13 @@ var Market = React.createClass({
<td><RatingBar rating={6 * service.name.length}/></td>
<td><small>{service.endpoint}</small></td>
<td><Address address={service.owner} small/></td>
<td className="text-nowrap text-center"><small>{
service.model == 0 ? "call" :
service.model == 1 ? "data" :
service.model == 2 ? "time" :
"unknown"
}
</small></td>
<td className="text-nowrap text-center"><small>{formatBalance(service.price)}</small></td>
<td className="text-nowrap text-center"><small>{moment.duration(service.cancellation, "seconds").humanize()}</small></td>
<td><Subscribe/></td>
Expand Down
94 changes: 65 additions & 29 deletions etherapis/dashboard/assets/components/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var Provider = React.createClass({
{
pairs.map(function(pair) {
return (
<div key={pair.first.owner + pair.first.name} className="row">
<div key={pair.first.id} className="row">
<div className="col-lg-6">
<Service apiurl={this.props.apiurl} service={pair.first}/>
</div>
Expand Down Expand Up @@ -84,16 +84,32 @@ var Service = React.createClass({
// render flattens the service stats into a UI panel.
render: function() {
return (
<div className={this.props.service.enabled ? "panel panel-default" : "panel panel-warning"}>
<div className={
this.props.service.creating ? "panel panel-info" :
this.props.service.deleting ? "panel panel-danger" :
!this.props.service.enabled && this.props.service.changing ? "panel panel-warning" :
this.props.service.enabled && this.props.service.changing ? "panel panel-success" :
"panel panel-default"}>
<div className="panel-heading">
<div className="pull-right"><i className={this.props.service.enabled ? "fa fa-unlock" : "fa fa-lock"}></i></div>
<h3 className="panel-title">{this.props.service.name}&nbsp;</h3>
<div className="pull-right"><i className={this.props.service.creating || this.props.service.deleting || this.props.service.changing ? "fa fa-spinner fa-spin" : (this.props.service.enabled ? "fa fa-unlock" : "fa fa-lock")}></i></div>
<h3 className="panel-title">{this.props.service.name}{
this.props.service.creating ? " – Registering..." :
this.props.service.deleting ? " – Deleting..." :
!this.props.service.enabled && this.props.service.changing ? " – Disabling..." :
this.props.service.enabled && this.props.service.changing ? " – Enabling..." :
null }</h3>
</div>
<div className="panel-body" id="services">
<table className="table table-condensed">
<tbody>
<tr><td className="text-center"><i className="fa fa-user"></i></td><td>Owner</td><td style={{width: "100%"}}><Address address={this.props.service.owner}/></td></tr>
<tr><td className="text-center"><i className="fa fa-link"></i></td><td>Endpoint</td><td>{this.props.service.endpoint}</td></tr>
<tr><td className="text-center"><i className="fa fa-credit-card-alt"></i></td><td>Payment</td><td>{
this.props.service.model == 0 ? "per API invocation (calls)" :
this.props.service.model == 1 ? "per consumed data traffic (bytes)" :
this.props.service.model == 2 ? "per maintained connection time (seconds)" :
"Something's wrong!"
}</td></tr>
<tr><td className="text-center">&Xi;</td><td>Price</td><td>{formatBalance(this.props.service.price)}</td></tr>
<tr><td className="text-center"><i className="fa fa-ban"></i></td><td>Cancellation</td><td>{moment.duration(this.props.service.cancellation, "seconds").humanize()} ({this.props.service.cancellation} secs)</td></tr>
</tbody>
Expand Down Expand Up @@ -123,17 +139,20 @@ var Service = React.createClass({
</tr>
</tbody>
</table>
<div className="clearfix">
<hr style={{margin: "10px 0"}}/>
<div className="pull-right">
{this.props.service.enabled ?
<a href="#" className="btn btn-sm btn-warning" onClick={this.confirmLock}><i className="fa fa-lock"></i> Disable</a> :
<a href="#" className="btn btn-sm btn-success" onClick={this.confirmUnlock}><i className="fa fa-unlock"></i> Enable</a>
}
&nbsp;
<a href="#" className="btn btn-sm btn-danger" onClick={this.confirmDelete}><i className="fa fa-times"></i> Delete</a>
{
this.props.service.creating || this.props.service.deleting || this.props.service.changing ? null :
<div className="clearfix">
<hr style={{margin: "10px 0"}}/>
<div className="pull-right">
{this.props.service.enabled ?
<a href="#" className="btn btn-sm btn-warning" onClick={this.confirmLock}><i className="fa fa-lock"></i> Disable</a> :
<a href="#" className="btn btn-sm btn-success" onClick={this.confirmUnlock}><i className="fa fa-unlock"></i> Enable</a>
}
&nbsp;
<a href="#" className="btn btn-sm btn-danger" onClick={this.confirmDelete}><i className="fa fa-times"></i> Delete</a>
</div>
</div>
</div>
}
<UnlockConfirm apiurl={this.props.apiurl} service={this.props.service} hide={this.state.action != "unlock"} abort={this.abortAction}/>
<LockConfirm apiurl={this.props.apiurl} service={this.props.service} hide={this.state.action != "lock"} abort={this.abortAction}/>
<DeleteConfirm apiurl={this.props.apiurl} service={this.props.service} hide={this.state.action != "delete"} abort={this.abortAction}/>
Expand Down Expand Up @@ -162,12 +181,19 @@ var UnlockConfirm = React.createClass({
// Show the spinner until something happens
this.setState({progress: true});

// Execute the service locking
/*$.ajax({type: "DELETE", url: this.props.apiurl + "/" + this.props.service.name, cache: false,
// Execute the service unlocking
var form = new FormData();
form.append("action", "unlock");

$.ajax({type: "POST", url: this.props.apiurl + "/" + this.props.service.owner + "/" + this.props.service.id, cache: false, data: form, processData: false, contentType: false,
success: function(data) {
this.setState({progress: false, failure: null});
this.props.abort();
}.bind(this),
error: function(xhr, status, err) {
this.setState({progress: false, failure: xhr.responseText});
}.bind(this),
});*/
});
},
// render flattens the account stats into a UI panel.
render: function() {
Expand Down Expand Up @@ -217,11 +243,18 @@ var LockConfirm = React.createClass({
this.setState({progress: true});

// Execute the service locking
/*$.ajax({type: "DELETE", url: this.props.apiurl + "/" + this.props.service.name, cache: false,
var form = new FormData();
form.append("action", "lock");

$.ajax({type: "POST", url: this.props.apiurl + "/" + this.props.service.owner + "/" + this.props.service.id, cache: false, data: form, processData: false, contentType: false,
success: function(data) {
this.setState({progress: false, failure: null});
this.props.abort();
}.bind(this),
error: function(xhr, status, err) {
this.setState({progress: false, failure: xhr.responseText});
}.bind(this),
});*/
});
},
// render flattens the account stats into a UI panel.
render: function() {
Expand Down Expand Up @@ -269,12 +302,16 @@ var DeleteConfirm = React.createClass({
// Show the spinner until something happens
this.setState({progress: true});

// Execute the account deletion request
/*$.ajax({type: "DELETE", url: this.props.apiurl + "/" + this.props.service.name, cache: false,
// Execute the service deletion request
$.ajax({type: "DELETE", url: this.props.apiurl + "/" + this.props.service.owner + "/" + this.props.service.id, cache: false,
success: function(data) {
this.setState({progress: false, failure: null});
this.props.abort();
}.bind(this),
error: function(xhr, status, err) {
this.setState({progress: false, failure: xhr.responseText});
}.bind(this),
});*/
});
},
// render flattens the account stats into a UI panel.
render: function() {
Expand Down Expand Up @@ -312,7 +349,7 @@ var ServiceCreator = React.createClass({
public: true,
name: "",
endpoint: "",
payment: "call",
model: "0",
price: "",
denom: EthereumUnits[4],
cancel: "",
Expand All @@ -337,6 +374,7 @@ var ServiceCreator = React.createClass({
},
updateName: function(event) { this.setState({name: event.target.value}); },
updateEndpoint: function(event) { this.setState({endpoint: event.target.value}); },
updateModel: function(event) { this.setState({model: event.target.value} ); },
updatePrice: function(event) { this.setState({price: event.target.value}); },
updateCancel: function(event) { this.setState({cancel: event.target.value}); },

Expand All @@ -359,6 +397,7 @@ var ServiceCreator = React.createClass({
var form = new FormData();
form.append("name", this.state.name);
form.append("url", this.state.endpoint);
form.append("model", this.state.model);
form.append("price", weiAmount(this.state.price, this.state.denom));
form.append("cancel", secondsDuration(this.state.cancel, this.state.scale));

Expand Down Expand Up @@ -452,20 +491,17 @@ var ServiceCreator = React.createClass({
<div className="col-lg-10">
<div className="radio">
<label>
<input type="radio" name="serviceType" defaultChecked/>
Per API invocation (calls)
<input type="radio" name="serviceType" value="0" checked={this.state.model == "0"} onChange={this.updateModel}/>Per API invocation (calls)
</label>
</div>
<div className="radio">
<label>
<input type="radio" name="serviceType"/>
Per consumed data traffic (bytes)
<input type="radio" name="serviceType" value="1" checked={this.state.model == "1"} onChange={this.updateModel}/>Per consumed data traffic (bytes)
</label>
</div>
<div className="radio">
<label>
<input type="radio" name="serviceType"/>
Per maintained connection time (seconds)
<input type="radio" name="serviceType" value="2" checked={this.state.model == "2"} onChange={this.updateModel}/>Per maintained connection time (seconds)
</label>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions etherapis/dashboard/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (server *stateServer) start() {
origin, current, height, pulled, known := eth.Downloader().Progress()
server.state.Ethereum.Syncing = &downloader.Progress{origin, current, height, pulled, known}

services, _ := server.eapis.Services(false)
services, _ := server.eapis.Services()
server.state.Services = make(map[string][]*etherapis.Service)
for account, service := range services {
server.state.Services[account.Hex()] = service
Expand Down Expand Up @@ -174,7 +174,7 @@ func (server *stateServer) loop() {
}
// Quick hack helper method to check for service updates
updateServices := func(update *stateUpdate) {
all, _ := server.eapis.Services(true)
all, _ := server.eapis.Services()
for address, services := range all {
if !reflect.DeepEqual(services, server.state.Services[address.Hex()]) {
update.Diffs = append(update.Diffs, stateDiff{Path: []string{"services", address.Hex()}, Node: services})
Expand Down
Loading

0 comments on commit 808c2c2

Please sign in to comment.