Skip to content

Commit

Permalink
Merge pull request #167 from SiaFoundation/nate/add-revenue-metrics
Browse files Browse the repository at this point in the history
Add revenue metrics
  • Loading branch information
n8maninger authored Sep 12, 2023
2 parents 678ae36 + 341d0b4 commit 6df371e
Show file tree
Hide file tree
Showing 36 changed files with 2,073 additions and 515 deletions.
15 changes: 14 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"time"

"go.sia.tech/core/consensus"
rhp3 "go.sia.tech/core/rhp/v3"
"go.sia.tech/core/types"
"go.sia.tech/hostd/host/accounts"
"go.sia.tech/hostd/host/alerts"
"go.sia.tech/hostd/host/contracts"
"go.sia.tech/hostd/host/metrics"
Expand Down Expand Up @@ -72,6 +74,12 @@ type (
CheckIntegrity(ctx context.Context, contractID types.FileContractID) (<-chan contracts.IntegrityResult, uint64, error)
}

// An AccountManager manages ephemeral accounts
AccountManager interface {
Accounts(limit, offset int) ([]accounts.Account, error)
AccountFunding(accountID rhp3.Account) ([]accounts.FundingSource, error)
}

// Alerts retrieves and dismisses notifications
Alerts interface {
Active() []alerts.Alert
Expand Down Expand Up @@ -109,6 +117,7 @@ type (
syncer Syncer
chain ChainManager
tpool TPool
accounts AccountManager
contracts ContractManager
volumes VolumeManager
wallet Wallet
Expand All @@ -120,7 +129,7 @@ type (
)

// NewServer initializes the API
func NewServer(name string, hostKey types.PublicKey, a Alerts, g Syncer, chain ChainManager, tp TPool, cm ContractManager, vm VolumeManager, m Metrics, s Settings, w Wallet, log *zap.Logger) http.Handler {
func NewServer(name string, hostKey types.PublicKey, a Alerts, g Syncer, chain ChainManager, tp TPool, cm ContractManager, am AccountManager, vm VolumeManager, m Metrics, s Settings, w Wallet, log *zap.Logger) http.Handler {
api := &api{
hostKey: hostKey,
name: name,
Expand All @@ -135,6 +144,7 @@ func NewServer(name string, hostKey types.PublicKey, a Alerts, g Syncer, chain C
chain: chain,
tpool: tp,
contracts: cm,
accounts: am,
volumes: vm,
metrics: m,
settings: s,
Expand Down Expand Up @@ -167,6 +177,9 @@ func NewServer(name string, hostKey types.PublicKey, a Alerts, g Syncer, chain C
"GET /contracts/:id/integrity": api.handleGETContractCheck,
"PUT /contracts/:id/integrity": api.handlePUTContractCheck,
"DELETE /contracts/:id/integrity": api.handleDeleteContractCheck,
// account endpoints
"GET /accounts": api.handleGETAccounts,
"GET /accounts/:account/funding": api.handleGETAccountFunding,
// sector endpoints
"DELETE /sectors/:root": api.handleDeleteSector,
// volume endpoints
Expand Down
22 changes: 22 additions & 0 deletions api/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"runtime"
"time"

rhp3 "go.sia.tech/core/rhp/v3"
"go.sia.tech/core/types"
"go.sia.tech/hostd/build"
"go.sia.tech/hostd/host/contracts"
Expand Down Expand Up @@ -560,6 +561,27 @@ func (a *api) handleGETTPoolFee(c jape.Context) {
c.Encode(a.tpool.RecommendedFee())
}

func (a *api) handleGETAccounts(c jape.Context) {
limit, offset := parseLimitParams(c, 100, 500)
accounts, err := a.accounts.Accounts(limit, offset)
if !a.checkServerError(c, "failed to get accounts", err) {
return
}
c.Encode(accounts)
}

func (a *api) handleGETAccountFunding(c jape.Context) {
var account rhp3.Account
if err := c.DecodeParam("account", &account); err != nil {
return
}
funding, err := a.accounts.AccountFunding(account)
if !a.checkServerError(c, "failed to get account funding", err) {
return
}
c.Encode(funding)
}

func parseLimitParams(c jape.Context, defaultLimit, maxLimit int) (limit, offset int) {
if err := c.DecodeForm("limit", &limit); err != nil {
return
Expand Down
2 changes: 1 addition & 1 deletion cmd/hostd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ func main() {
auth := jape.BasicAuth(cfg.HTTP.Password)
web := http.Server{
Handler: webRouter{
api: auth(api.NewServer(cfg.Name, hostKey.PublicKey(), node.a, node.g, node.cm, node.tp, node.contracts, node.storage, node.metrics, node.settings, node.w, log.Named("api"))),
api: auth(api.NewServer(cfg.Name, hostKey.PublicKey(), node.a, node.g, node.cm, node.tp, node.contracts, node.accounts, node.storage, node.metrics, node.settings, node.w, log.Named("api"))),
ui: hostd.Handler(),
},
ReadTimeout: 30 * time.Second,
Expand Down
85 changes: 3 additions & 82 deletions cmd/hostd/node.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
package main

import (
"bytes"
"fmt"
"net"
"os"
"path/filepath"
"strings"

"gitlab.com/NebulousLabs/encoding"
"go.sia.tech/core/types"
"go.sia.tech/hostd/chain"
"go.sia.tech/hostd/host/accounts"
"go.sia.tech/hostd/host/alerts"
"go.sia.tech/hostd/host/contracts"
"go.sia.tech/hostd/host/metrics"
"go.sia.tech/hostd/host/registry"
"go.sia.tech/hostd/host/settings"
"go.sia.tech/hostd/host/storage"
"go.sia.tech/hostd/internal/chain"
"go.sia.tech/hostd/persist/sqlite"
"go.sia.tech/hostd/rhp"
rhpv2 "go.sia.tech/hostd/rhp/v2"
Expand All @@ -27,91 +25,14 @@ import (
"go.sia.tech/siad/modules/consensus"
"go.sia.tech/siad/modules/gateway"
"go.sia.tech/siad/modules/transactionpool"
stypes "go.sia.tech/siad/types"
"go.uber.org/zap"
)

func convertToSiad(core types.EncoderTo, siad encoding.SiaUnmarshaler) {
var buf bytes.Buffer
e := types.NewEncoder(&buf)
core.EncodeTo(e)
e.Flush()
if err := siad.UnmarshalSia(&buf); err != nil {
panic(err)
}
}

func convertToCore(siad encoding.SiaMarshaler, core types.DecoderFrom) {
var buf bytes.Buffer
siad.MarshalSia(&buf)
d := types.NewBufDecoder(buf.Bytes())
core.DecodeFrom(d)
if d.Err() != nil {
panic(d.Err())
}
}

type txpool struct {
tp modules.TransactionPool
}

func (tp txpool) RecommendedFee() (fee types.Currency) {
_, max := tp.tp.FeeEstimation()
convertToCore(&max, &fee)
return
}

func (tp txpool) Transactions() []types.Transaction {
stxns := tp.tp.Transactions()
txns := make([]types.Transaction, len(stxns))
for i := range txns {
convertToCore(&stxns[i], &txns[i])
}
return txns
}

func (tp txpool) AcceptTransactionSet(txns []types.Transaction) error {
stxns := make([]stypes.Transaction, len(txns))
for i := range stxns {
convertToSiad(&txns[i], &stxns[i])
}
return tp.tp.AcceptTransactionSet(stxns)
}

func (tp txpool) UnconfirmedParents(txn types.Transaction) ([]types.Transaction, error) {
pool := tp.Transactions()
outputToParent := make(map[types.SiacoinOutputID]*types.Transaction)
for i, txn := range pool {
for j := range txn.SiacoinOutputs {
outputToParent[txn.SiacoinOutputID(j)] = &pool[i]
}
}
var parents []types.Transaction
seen := make(map[types.TransactionID]bool)
for _, sci := range txn.SiacoinInputs {
if parent, ok := outputToParent[sci.ParentID]; ok {
if txid := parent.ID(); !seen[txid] {
seen[txid] = true
parents = append(parents, *parent)
}
}
}
return parents, nil
}

func (tp txpool) Subscribe(s modules.TransactionPoolSubscriber) {
tp.tp.TransactionPoolSubscribe(s)
}

func (tp txpool) Close() error {
return tp.tp.Close()
}

type node struct {
g modules.Gateway
a *alerts.Manager
cm *chain.Manager
tp *txpool
tp *chain.TransactionPool
w *wallet.SingleAddressWallet
store *sqlite.Store

Expand Down Expand Up @@ -203,7 +124,7 @@ func newNode(walletKey types.PrivateKey, logger *zap.Logger) (*node, types.Priva
if err != nil {
return nil, types.PrivateKey{}, fmt.Errorf("failed to create tpool: %w", err)
}
tp := &txpool{stp}
tp := chain.NewTPool(stp)

db, err := sqlite.OpenDatabase(filepath.Join(cfg.Directory, "hostd.db"), logger.Named("sqlite"))
if err != nil {
Expand Down
61 changes: 52 additions & 9 deletions host/accounts/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

rhpv3 "go.sia.tech/core/rhp/v3"
"go.sia.tech/core/types"
"go.sia.tech/hostd/host/contracts"
"go.sia.tech/hostd/host/settings"
)

Expand All @@ -23,13 +24,17 @@ var (
type (
// An AccountStore stores and updates account balances.
AccountStore interface {
// AccountFunding returns the remaining funding sources for an account.
AccountFunding(accountID rhpv3.Account) ([]FundingSource, error)
// Accounts returns a list of active ephemeral accounts
Accounts(limit, offset int) ([]Account, error)
// AccountBalance returns the balance of the account with the given ID.
AccountBalance(accountID rhpv3.Account) (types.Currency, error)
// CreditAccount adds the specified amount to the account with the given ID.
CreditAccount(accountID rhpv3.Account, amount types.Currency, expiration time.Time) (types.Currency, error)
// CreditAccountWithContract adds the specified amount to the account with the given ID.
CreditAccountWithContract(FundAccountWithContract) (types.Currency, error)
// DebitAccount subtracts the specified amount from the account with the given
// ID. Returns the remaining balance of the account.
DebitAccount(accountID rhpv3.Account, amount types.Currency) (types.Currency, error)
DebitAccount(accountID rhpv3.Account, usage Usage) (types.Currency, error)
}

// Settings returns the host's current settings.
Expand All @@ -42,6 +47,30 @@ type (
openTxns int
}

// FundingSource tracks a funding source for an account.
FundingSource struct {
ContractID types.FileContractID `json:"contractID"`
AccountID rhpv3.Account `json:"accountID"`
Amount types.Currency `json:"amount"`
}

// An Account holds the balance and expiration of an ephemeral account.
Account struct {
ID rhpv3.Account `json:"ID"`
Balance types.Currency `json:"balance"`
Expiration time.Time `json:"expiration"`
}

// FundAccountWithContract is a helper struct for funding an account with a
// contract.
FundAccountWithContract struct {
Account rhpv3.Account
Cost types.Currency
Amount types.Currency
Revision contracts.SignedRevision
Expiration time.Time
}

// An AccountManager manages deposits and withdrawals for accounts. It is
// primarily a synchronization wrapper around a store.
AccountManager struct {
Expand Down Expand Up @@ -70,30 +99,44 @@ func (am *AccountManager) Balance(accountID rhpv3.Account) (types.Currency, erro
return am.getBalance(accountID)
}

// Accounts returns a list of active ephemeral accounts
func (am *AccountManager) Accounts(limit, offset int) (acc []Account, err error) {
return am.store.Accounts(limit, offset)
}

// AccountFunding returns the remaining funding sources for an account.
func (am *AccountManager) AccountFunding(account rhpv3.Account) (srcs []FundingSource, err error) {
return am.store.AccountFunding(account)
}

// Credit adds the specified amount to the account with the given ID. Credits
// are synced to the underlying store immediately.
func (am *AccountManager) Credit(accountID rhpv3.Account, amount types.Currency, expiration time.Time, refund bool) (types.Currency, error) {
func (am *AccountManager) Credit(req FundAccountWithContract, refund bool) (types.Currency, error) {
am.mu.Lock()
defer am.mu.Unlock()

balance, err := am.getBalance(accountID)
if req.Expiration.Before(time.Now()) {
return types.ZeroCurrency, fmt.Errorf("account expiration cannot be in the past")
}

balance, err := am.getBalance(req.Account)
if err != nil {
return types.ZeroCurrency, fmt.Errorf("failed to get account balance: %w", err)
}

creditBalance := balance.Add(amount)
creditBalance := balance.Add(req.Amount)
if !refund && creditBalance.Cmp(am.settings.Settings().MaxAccountBalance) > 0 {
return types.ZeroCurrency, ErrBalanceExceeded
}

// credit the account
if _, err = am.store.CreditAccount(accountID, amount, expiration); err != nil {
if _, err = am.store.CreditAccountWithContract(req); err != nil {
return types.ZeroCurrency, fmt.Errorf("failed to credit account: %w", err)
}
// increment the balance in memory, if it exists
if state, ok := am.balances[accountID]; ok {
if state, ok := am.balances[req.Account]; ok {
state.balance = creditBalance
am.balances[accountID] = state
am.balances[req.Account] = state
}
return creditBalance, nil
}
Expand Down
Loading

0 comments on commit 6df371e

Please sign in to comment.