Skip to content

Commit

Permalink
Merge pull request #8 from mike76-dev/price-development
Browse files Browse the repository at this point in the history
Pull historic price changes and display them in a chart
  • Loading branch information
mike76-dev authored Mar 26, 2024
2 parents 587da2f + f421f74 commit 67b94af
Show file tree
Hide file tree
Showing 19 changed files with 749 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ldflags= \
-X "github.com/mike76-dev/hostscore/internal/build.NodeBinaryName=hsd" \
-X "github.com/mike76-dev/hostscore/internal/build.NodeVersion=0.1.1" \
-X "github.com/mike76-dev/hostscore/internal/build.ClientBinaryName=hsc" \
-X "github.com/mike76-dev/hostscore/internal/build.ClientVersion=0.3.0" \
-X "github.com/mike76-dev/hostscore/internal/build.ClientVersion=0.4.0" \
-X "github.com/mike76-dev/hostscore/internal/build.GitRevision=${GIT_DIRTY}${GIT_REVISION}" \
-X "github.com/mike76-dev/hostscore/internal/build.BuildTime=${BUILD_TIME}"

Expand Down
68 changes: 66 additions & 2 deletions cmd/hsc/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import (
"go.uber.org/zap"
)

var lowBalanceThreshold = types.Siacoins(200)
var (
lowBalanceThreshold = types.Siacoins(200)
zeroBalanceThreshold = types.Siacoins(10)
)

type APIResponse struct {
Status string `json:"status"`
Expand Down Expand Up @@ -73,6 +76,22 @@ type statusResponse struct {
Nodes []nodeStatus `json:"nodes"`
}

type priceChange struct {
Timestamp time.Time `json:"timestamp"`
RemainingStorage uint64 `json:"remainingStorage"`
TotalStorage uint64 `json:"totalStorage"`
Collateral types.Currency `json:"collateral"`
StoragePrice types.Currency `json:"storagePrice"`
UploadPrice types.Currency `json:"uploadPrice"`
DownloadPrice types.Currency `json:"downloadPrice"`
}

type priceChangeResponse struct {
APIResponse
PublicKey types.PublicKey `json:"publicKey"`
PriceChanges []priceChange `json:"priceChanges"`
}

type nodeInteractions struct {
Uptime time.Duration `json:"uptime"`
Downtime time.Duration `json:"downtime"`
Expand Down Expand Up @@ -197,6 +216,9 @@ func (api *portalAPI) buildHTTPRoutes() {
router.GET("/benchmarks", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
api.benchmarksHandler(w, req, ps)
})
router.GET("/changes", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
api.changesHandler(w, req, ps)
})
router.GET("/status", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
api.statusHandler(w, req, ps)
})
Expand Down Expand Up @@ -525,7 +547,7 @@ func (api *portalAPI) benchmarksHandler(w http.ResponseWriter, req *http.Request
}

func balanceStatus(balance types.Currency) string {
if balance.IsZero() {
if balance.Cmp(zeroBalanceThreshold) < 0 {
return "empty"
}
if balance.Cmp(lowBalanceThreshold) < 0 {
Expand Down Expand Up @@ -594,6 +616,48 @@ func (api *portalAPI) onlineHostsHandler(w http.ResponseWriter, req *http.Reques
})
}

func (api *portalAPI) changesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
network := strings.ToLower(req.FormValue("network"))
if network == "" {
writeJSON(w, APIResponse{
Status: "error",
Message: "network not provided",
})
return
}
host := req.FormValue("host")
if host == "" {
writeJSON(w, APIResponse{
Status: "error",
Message: "host not provided",
})
return
}
var pk types.PublicKey
err := pk.UnmarshalText([]byte(host))
if err != nil {
writeJSON(w, APIResponse{
Status: "error",
Message: "invalid public key",
})
return
}
pcs, err := api.getPriceChanges(network, pk)
if err != nil {
api.log.Error("couldn't get price changes", zap.String("network", network), zap.Stringer("host", pk), zap.Error(err))
writeJSON(w, APIResponse{
Status: "error",
Message: "internal error",
})
return
}
writeJSON(w, priceChangeResponse{
APIResponse: APIResponse{Status: "ok"},
PublicKey: pk,
PriceChanges: pcs,
})
}

func writeJSON(w http.ResponseWriter, obj interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
err := json.NewEncoder(w).Encode(obj)
Expand Down
125 changes: 125 additions & 0 deletions cmd/hsc/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ func (api *portalAPI) insertUpdates(node string, updates hostdb.HostUpdates) err
}
defer benchmarkStmt.Close()

priceChangeStmt, err := tx.Prepare(`
INSERT INTO price_changes (
network,
public_key,
changed_at,
remaining_storage,
total_storage,
collateral,
storage_price,
upload_price,
download_price
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
tx.Rollback()
return utils.AddContext(err, "couldn't prepare price change statement")
}
defer priceChangeStmt.Close()

for _, host := range updates.Hosts {
var settings, pt bytes.Buffer
e := types.NewEncoder(&settings)
Expand Down Expand Up @@ -202,6 +222,35 @@ func (api *portalAPI) insertUpdates(node string, updates hostdb.HostUpdates) err
} else {
host, exists = api.hostsZen[h.PublicKey]
}
if !exists || pricesChanged(h.Settings, host.Settings) {
var cb, spb, upb, dpb bytes.Buffer
e := types.NewEncoder(&cb)
types.V1Currency(h.Settings.Collateral).EncodeTo(e)
e.Flush()
e = types.NewEncoder(&spb)
types.V1Currency(h.Settings.StoragePrice).EncodeTo(e)
e.Flush()
e = types.NewEncoder(&upb)
types.V1Currency(h.Settings.UploadBandwidthPrice).EncodeTo(e)
e.Flush()
e = types.NewEncoder(&dpb)
types.V1Currency(h.Settings.DownloadBandwidthPrice).EncodeTo(e)
e.Flush()
_, err := priceChangeStmt.Exec(
h.Network,
h.PublicKey[:],
time.Now().Unix(),
h.Settings.RemainingStorage,
h.Settings.TotalStorage,
cb.Bytes(),
spb.Bytes(),
upb.Bytes(),
dpb.Bytes(),
)
if err != nil {
api.log.Error("couldn't update price change", zap.String("network", h.Network), zap.Stringer("host", h.PublicKey), zap.Error(err))
}
}
if exists {
host.NetAddress = h.NetAddress
host.Blocked = h.Blocked
Expand Down Expand Up @@ -333,6 +382,23 @@ func (api *portalAPI) isOnline(host portalHost) bool {
return false
}

// pricesChanged returns true if any relevant part of the host's settings has changed.
func pricesChanged(os, ns rhpv2.HostSettings) bool {
if ns.RemainingStorage != os.RemainingStorage || ns.TotalStorage != os.TotalStorage {
return true
}
if ns.StoragePrice.Cmp(os.StoragePrice) != 0 || ns.Collateral.Cmp(os.Collateral) != 0 {
return true
}
if ns.UploadBandwidthPrice.Cmp(os.UploadBandwidthPrice) != 0 {
return true
}
if ns.DownloadBandwidthPrice.Cmp(os.DownloadBandwidthPrice) != 0 {
return true
}
return false
}

// getHost retrieves the information about a specific host.
func (api *portalAPI) getHost(network string, pk types.PublicKey) (host portalHost, err error) {
var h *portalHost
Expand Down Expand Up @@ -1061,3 +1127,62 @@ func (api *portalAPI) loadScans(hosts map[types.PublicKey]*portalHost, network s

return nil
}

// getPriceChanges retrieves the historic price changes of the given host.
func (api *portalAPI) getPriceChanges(network string, pk types.PublicKey) (pcs []priceChange, err error) {
rows, err := api.db.Query(`
SELECT
changed_at,
remaining_storage,
total_storage,
collateral,
storage_price,
upload_price,
download_price
FROM price_changes
WHERE network = ?
AND public_key = ?
AND changed_at >= ?
ORDER BY changed_at ASC
`, network, pk[:], time.Now().AddDate(-1, 0, 0))
if err != nil {
return nil, utils.AddContext(err, "couldn't query price changes")
}
defer rows.Close()

for rows.Next() {
var ca int64
var rs, ts uint64
var cb, spb, upb, dpb []byte
if err := rows.Scan(&ca, &rs, &ts, &cb, &spb, &upb, &dpb); err != nil {
return nil, utils.AddContext(err, "couldn't decode price change")
}

pc := priceChange{
Timestamp: time.Unix(ca, 0),
RemainingStorage: rs,
TotalStorage: ts,
}

d := types.NewBufDecoder(cb)
if (*types.V1Currency)(&pc.Collateral).DecodeFrom(d); d.Err() != nil {
return nil, utils.AddContext(err, "couldn't decode collateral")
}
d = types.NewBufDecoder(spb)
if (*types.V1Currency)(&pc.StoragePrice).DecodeFrom(d); d.Err() != nil {
return nil, utils.AddContext(err, "couldn't decode storage price")
}
d = types.NewBufDecoder(upb)
if (*types.V1Currency)(&pc.UploadPrice).DecodeFrom(d); d.Err() != nil {
return nil, utils.AddContext(err, "couldn't decode upload price")
}
d = types.NewBufDecoder(dpb)
if (*types.V1Currency)(&pc.DownloadPrice).DecodeFrom(d); d.Err() != nil {
return nil, utils.AddContext(err, "couldn't decode download price")
}

pcs = append(pcs, pc)
}

return
}
16 changes: 16 additions & 0 deletions init_portal.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ DROP TABLE IF EXISTS locations;
DROP TABLE IF EXISTS scans;
DROP TABLE IF EXISTS benchmarks;
DROP TABLE IF EXISTS interactions;
DROP TABLE IF EXISTS price_changes;
DROP TABLE IF EXISTS hosts;

CREATE TABLE hosts (
Expand Down Expand Up @@ -66,6 +67,21 @@ CREATE TABLE benchmarks (
FOREIGN KEY (public_key) REFERENCES hosts(public_key)
);

CREATE TABLE price_changes (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
network VARCHAR(8) NOT NULL,
public_key BINARY(32) NOT NULL,
changed_at BIGINT NOT NULL,
remaining_storage BIGINT UNSIGNED NOT NULL,
total_storage BIGINT UNSIGNED NOT NULL,
collateral TINYBLOB NOT NULL,
storage_price TINYBLOB NOT NULL,
upload_price TINYBLOB NOT NULL,
download_price TINYBLOB NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (public_key) REFERENCES hosts(public_key)
);

CREATE TABLE locations (
public_key BINARY(32) NOT NULL,
ip TEXT NOT NULL,
Expand Down
6 changes: 6 additions & 0 deletions web-demo/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,15 @@ select, input {
background-color: red;
}

.host-benchmarks {
table-layout: fixed;
}

.host-benchmarks td, .host-benchmarks th {
padding: 0.25rem 0.5rem;
border: 1px solid black;
white-space: normal;
word-break: break-word;
}

.benchmark-pass::after {
Expand Down
4 changes: 1 addition & 3 deletions web-demo/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const apiBaseURL = '/hostscore/api';
var locations = ['eu', 'us'];
var locations = ['eu', 'us', 'ap'];
var hosts = [];
var moreHosts = false;
var offset = 0;
Expand Down Expand Up @@ -115,8 +115,6 @@ function browseHost(obj) {
list.classList.add('hidden');
let latencies = [];
let benchmarks = [];
let from = new Date();
from.setDate(from.getDate() - 1);
fetch(apiBaseURL + '/scans?network=' + network + '&host=' + key +
'&number=48&success=true', options)
.then(response => response.json())
Expand Down
17 changes: 17 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"axios": "^1.6.7",
"chart.js": "^4.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.1",
Expand All @@ -24,7 +25,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
Expand Down
11 changes: 10 additions & 1 deletion web/src/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,13 @@ export const convertPricePerBlock = (value: string) => {
if (result < 1e3) return result.toFixed(0) + ' SC/TB/month'
if (result < 1e6) return (result / 1e3).toFixed(1) + ' KS/TB/month'
return (result / 1e6).toFixed(1) + ' MS/TB/month'
}
}

export const convertPriceRaw = (value: string) => {
if (value.length > 12) {
value = value.slice(0, value.length - 12) + '.' + value.slice(value.length - 12)
} else {
value = '0.' + '0'.repeat(12 - value.length) + value
}
return Number.parseFloat(value)
}
Loading

0 comments on commit 67b94af

Please sign in to comment.