Skip to content

Commit

Permalink
admin: Add account endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Jun 11, 2020
1 parent 51eb62d commit 5fb47a6
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 7 deletions.
15 changes: 15 additions & 0 deletions dex/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ func (b Bytes) MarshalJSON() ([]byte, error) {
return json.Marshal(hex.EncodeToString(b))
}

// Scan implements the sql.Scanner interface.
func (b *Bytes) Scan(src interface{}) error {
switch src := src.(type) {
case []byte:
// src may be reused, so create a new slice.
dst := make(Bytes, len(src))
copy(dst, src)
*b = dst
return nil
case nil:
return nil
}
return fmt.Errorf("cannot convert %T to Bytes", src)
}

// UnmarshalJSON satisfies the json.Unmarshaler interface, and expects a UTF-8
// encoding of a hex string.
func (b *Bytes) UnmarshalJSON(encHex []byte) (err error) {
Expand Down
24 changes: 24 additions & 0 deletions server/admin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package admin

import (
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
Expand All @@ -12,6 +13,7 @@ import (
"time"

"decred.org/dcrdex/dex/encode"
"decred.org/dcrdex/server/account"
"github.com/go-chi/chi"
)

Expand Down Expand Up @@ -156,3 +158,25 @@ func (s *Server) apiAccounts(w http.ResponseWriter, _ *http.Request) {
}
writeJSON(w, accts)
}

// apiAccountInfo is the handler for the '/account/{account id}' API request.
func (s *Server) apiAccountInfo(w http.ResponseWriter, r *http.Request) {
acctIDStr := chi.URLParam(r, accountNameKey)
acctIDSlice, err := hex.DecodeString(acctIDStr)
if err != nil {
http.Error(w, fmt.Sprintf("could not decode accout id: %v", err), http.StatusBadRequest)
return
}
if len(acctIDSlice) != account.HashSize {
http.Error(w, "account id has incorrect length", http.StatusBadRequest)
return
}
var acctID account.AccountID
copy(acctID[:], acctIDSlice)
acctInfo, err := s.core.AccountInfo(acctID)
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve account: %v", err), http.StatusInternalServerError)
return
}
writeJSON(w, acctInfo)
}
8 changes: 7 additions & 1 deletion server/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"sync"
"time"

"decred.org/dcrdex/server/account"
"decred.org/dcrdex/server/db"
"decred.org/dcrdex/server/market"
"github.com/decred/slog"
Expand All @@ -30,7 +31,8 @@ const (
// is closed.
rpcTimeoutSeconds = 10

marketNameKey = "market"
marketNameKey = "market"
accountNameKey = "account"
)

var (
Expand All @@ -40,6 +42,7 @@ var (
// SvrCore is satisfied by server/dex.DEX.
type SvrCore interface {
Accounts() ([]*db.Account, error)
AccountInfo(account.AccountID) (*db.Account, error)
ConfigMsg() json.RawMessage
MarketRunning(mktName string) (found, running bool)
MarketStatus(mktName string) *market.Status
Expand Down Expand Up @@ -121,6 +124,9 @@ func NewServer(cfg *SrvConfig) (*Server, error) {
r.Get("/ping", s.apiPing)
r.Get("/config", s.apiConfig)
r.Get("/accounts", s.apiAccounts)
r.Route("/account/{"+accountNameKey+"}", func(rm chi.Router) {
rm.Get("/", s.apiAccountInfo)
})

r.Get("/markets", s.apiMarkets)
r.Route("/market/{"+marketNameKey+"}", func(rm chi.Router) {
Expand Down
129 changes: 129 additions & 0 deletions server/admin/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type TCore struct {
markets map[string]*TMarket
accounts []*db.Account
accountsErr error
account *db.Account
accountErr error
}

func (c *TCore) ConfigMsg() json.RawMessage { return nil }
Expand Down Expand Up @@ -138,6 +140,9 @@ func (w *tResponseWriter) WriteHeader(statusCode int) {
}

func (c *TCore) Accounts() ([]*db.Account, error) { return c.accounts, c.accountsErr }
func (c *TCore) AccountInfo(_ account.AccountID) (*db.Account, error) {
return c.account, c.accountErr
}

// genCertPair generates a key/cert pair to the paths provided.
func genCertPair(certFile, keyFile string) error {
Expand Down Expand Up @@ -794,3 +799,127 @@ func TestAccounts(t *testing.T) {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusInternalServerError)
}
}

func TestAccountInfo(t *testing.T) {
core := new(TCore)
srv := &Server{
core: core,
}

acctIDStr := "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc"

mux := chi.NewRouter()
mux.Route("/account/{"+accountNameKey+"}", func(rm chi.Router) {
rm.Get("/", srv.apiAccountInfo)
})

// No account.
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "https://localhost/account/"+acctIDStr, nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusOK)
}
respBody := w.Body.String()
if respBody != "null\n" {
t.Errorf("incorrect response body: %q", respBody)
}

accountIDSlice, err := hex.DecodeString(acctIDStr)
if err != nil {
t.Fatal(err)
}
var accountID account.AccountID
copy(accountID[:], accountIDSlice)
pubkey, err := hex.DecodeString("0204988a498d5d19514b217e872b4dbd1cf071d365c4879e64ed5919881c97eb19")
if err != nil {
t.Fatal(err)
}
feeCoin, err := hex.DecodeString("6e515ff861f2016fd0da2f3eccdf8290c03a9d116bfba2f6729e648bdc6e5aed00000005")
if err != nil {
t.Fatal(err)
}

// An account.
core.account = &db.Account{
AccountID: accountID,
Pubkey: dex.Bytes(pubkey),
FeeAddress: "DsdQFmH3azyoGKJHt2ArJNxi35LCEgMqi8k",
FeeCoin: dex.Bytes(feeCoin),
BrokenRule: account.Rule(byte(255)),
}

w = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "https://localhost/account/"+acctIDStr, nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusOK)
}

exp := `{
"accountid": "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc",
"pubkey": "0204988a498d5d19514b217e872b4dbd1cf071d365c4879e64ed5919881c97eb19",
"feeaddress": "DsdQFmH3azyoGKJHt2ArJNxi35LCEgMqi8k",
"feecoin": "6e515ff861f2016fd0da2f3eccdf8290c03a9d116bfba2f6729e648bdc6e5aed00000005",
"brokenrule": 255
}
`
if exp != w.Body.String() {
t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp)
}

// ok, upper case account id
w = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "https://localhost/account/"+strings.ToUpper(acctIDStr), nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusOK)
}
if exp != w.Body.String() {
t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp)
}

// acct id is not hex
w = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "https://localhost/account/nothex", nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusBadRequest {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusBadRequest)
}

// acct id wrong length
w = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "https://localhost/account/"+acctIDStr[2:], nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusBadRequest {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusBadRequest)
}

// core.Account error
core.accountErr = errors.New("error")

w = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "https://localhost/account/"+acctIDStr, nil)
r.RemoteAddr = "localhost"

mux.ServeHTTP(w, r)

if w.Code != http.StatusInternalServerError {
t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusInternalServerError)
}
}
17 changes: 16 additions & 1 deletion server/db/driver/pg/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ func (a *Archiver) Accounts() ([]*db.Account, error) {
}
defer rows.Close()
var accts []*db.Account
var feeAddress sql.NullString
for rows.Next() {
a := new(db.Account)
err = rows.Scan(&a.AccountID, &a.Pubkey, &a.FeeAddress, &a.FeeCoin, &a.BrokenRule)
err = rows.Scan(&a.AccountID, &a.Pubkey, &feeAddress, &a.FeeCoin, &a.BrokenRule)
if err != nil {
return nil, err
}
a.FeeAddress = feeAddress.String
accts = append(accts, a)
}
if err = rows.Err(); err != nil {
Expand All @@ -67,6 +69,19 @@ func (a *Archiver) Accounts() ([]*db.Account, error) {
return accts, nil
}

// AccountInfo returns data for an account.
func (a *Archiver) AccountInfo(aid account.AccountID) (*db.Account, error) {
stmt := fmt.Sprintf(internal.SelectAccountInfo, a.tables.accounts)
acct := new(db.Account)
var feeAddress sql.NullString
if err := a.db.QueryRow(stmt, aid).Scan(&acct.AccountID, &acct.Pubkey, &feeAddress,
&acct.FeeCoin, &acct.BrokenRule); err != nil {
return nil, err
}
acct.FeeAddress = feeAddress.String
return acct, nil
}

// CreateAccount creates an entry for a new account in the accounts table. A
// DCR registration fee address is created and returned.
func (a *Archiver) CreateAccount(acct *account.Account) (string, error) {
Expand Down
45 changes: 45 additions & 0 deletions server/db/driver/pg/accounts_online_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package pg

import (
"encoding/hex"
"fmt"
"reflect"
"testing"

"decred.org/dcrdex/server/account"
Expand Down Expand Up @@ -89,6 +91,49 @@ func TestAccounts(t *testing.T) {
t.Fatal("accounts has unexpected data")
}

anAcct, err := archie.AccountInfo(accts[0].AccountID)
if err != nil {
t.Fatalf("error getting account info: %v", err)
}
if !reflect.DeepEqual(accts[0], anAcct) {
t.Fatal("error getting account info: actual does not equal expected")
}

// The Account ID cannot be null. broken_rule has a default value of 0
// and is unexpected to become null.
nullAccounts := `UPDATE %s
SET
pubkey = null ,
fee_address = null,
fee_coin = null;`

stmt := fmt.Sprintf(nullAccounts, archie.tables.accounts)
if _, err = sqlExec(archie.db, stmt); err != nil {
t.Fatalf("error nullifying account: %v", err)
}

accts, err = archie.Accounts()
if err != nil {
t.Fatalf("error getting null accounts: %v", err)
}

// All fields except account ID are null.
if accts[0].AccountID.String() != "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc" ||
accts[0].Pubkey.String() != "" ||
accts[0].FeeAddress != "" ||
accts[0].FeeCoin.String() != "" ||
byte(accts[0].BrokenRule) != byte(0) {
t.Fatal("accounts has unexpected data")
}

anAcct, err = archie.AccountInfo(accts[0].AccountID)
if err != nil {
t.Fatalf("error getting null account info: %v", err)
}
if !reflect.DeepEqual(accts[0], anAcct) {
t.Fatal("error getting null account info: actual does not equal expected")
}

// Close the account for failure to complete a swap.
archie.CloseAccount(tAcctID, account.FailureToAct)
_, _, open = archie.Account(tAcctID)
Expand Down
4 changes: 4 additions & 0 deletions server/db/driver/pg/internal/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const (
// SelectAllAccounts retrieves all accounts.
SelectAllAccounts = `SELECT * FROM %s;`

// SelectAccountInfo retrieves all fields for an account.
SelectAccountInfo = `SELECT * FROM %s
WHERE account_id = $1;`

// CreateAccount creates an entry for a new account.
CreateAccount = `INSERT INTO %s (account_id, pubkey, fee_address)
VALUES ($1, $2, $3);`
Expand Down
3 changes: 3 additions & 0 deletions server/db/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ type AccountArchiver interface {

// Accounts returns data for all accounts.
Accounts() ([]*Account, error)

// AccountInfo returns data for an account.
AccountInfo(account.AccountID) (*Account, error)
}

// MatchData represents an order pair match, but with just the order IDs instead
Expand Down
6 changes: 6 additions & 0 deletions server/dex/dex.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
"decred.org/dcrdex/dex/msgjson"
"decred.org/dcrdex/server/account"
"decred.org/dcrdex/server/asset"
dcrasset "decred.org/dcrdex/server/asset/dcr"
"decred.org/dcrdex/server/auth"
Expand Down Expand Up @@ -552,3 +553,8 @@ func (dm *DEX) SuspendMarket(name string, tSusp time.Time, persistBooks bool) *m
func (dm *DEX) Accounts() ([]*db.Account, error) {
return dm.storage.Accounts()
}

// AccountInfo returns data for an account.
func (dm *DEX) AccountInfo(aid account.AccountID) (*db.Account, error) {
return dm.storage.AccountInfo(aid)
}
11 changes: 6 additions & 5 deletions server/market/market_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,12 @@ func (ta *TArchivist) CloseAccount(account.AccountID, account.Rule) {}
func (ta *TArchivist) Account(account.AccountID) (acct *account.Account, paid, open bool) {
return nil, false, false
}
func (ta *TArchivist) CreateAccount(*account.Account) (string, error) { return "", nil }
func (ta *TArchivist) AccountRegAddr(account.AccountID) (string, error) { return "", nil }
func (ta *TArchivist) PayAccount(account.AccountID, []byte) error { return nil }
func (ta *TArchivist) Accounts() ([]*db.Account, error) { return nil, nil }
func (ta *TArchivist) Close() error { return nil }
func (ta *TArchivist) CreateAccount(*account.Account) (string, error) { return "", nil }
func (ta *TArchivist) AccountRegAddr(account.AccountID) (string, error) { return "", nil }
func (ta *TArchivist) PayAccount(account.AccountID, []byte) error { return nil }
func (ta *TArchivist) Accounts() ([]*db.Account, error) { return nil, nil }
func (ta *TArchivist) AccountInfo(account.AccountID) (*db.Account, error) { return nil, nil }
func (ta *TArchivist) Close() error { return nil }

func randomOrderID() order.OrderID {
pk := randomBytes(order.OrderIDSize)
Expand Down

0 comments on commit 5fb47a6

Please sign in to comment.