Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin: Add account endpoint. #455

Merged
merged 3 commits into from
Jun 11, 2020
Merged
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
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)
JoeGruffins marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member

@chappjc chappjc Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test Accounts and AccountInfo before PayAccount to test the null cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a separate nullifying section.

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;`
Comment on lines +102 to +108
Copy link
Member

@chappjc chappjc Jun 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with these tests, although note that that the account id is the hash of the pubkey, so they will both be set always unless the DB is corrupted. Still the db scheme doesn't prevent pubkey from being null so I think this is a good test.


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