wallet-sync-server/server/wallet_state.go
Daniel Krol 2fbcf6ee6d Get/Post WalletState, account recover, test client
A few things at once because it was faster to get a demo out the door. Skipping most test implementation though I made failing stubs so I know what to fill in later.

* Get/Post WalletState
* downloadKey/email so that a second client can log in, and/or recover from lost client
* Test client in Python to demonstrate the above
* Organize into packages
2022-01-04 16:07:23 -05:00

206 lines
5.9 KiB
Go

package server
import (
"encoding/json"
"fmt"
"net/http"
"orblivion/lbry-id/auth"
"orblivion/lbry-id/store"
"orblivion/lbry-id/wallet"
)
type WalletStateRequest struct {
Token auth.AuthTokenString `json:"token"`
BodyJSON string `json:"bodyJSON"`
PubKey auth.PublicKey `json:"publicKey"`
Signature auth.Signature `json:"signature"`
// downloadKey is derived from the same password used to encrypt the wallet.
// We want to keep it all in sync so we update it at the same time.
DownloadKey auth.DownloadKey `json:"downloadKey"`
}
func (r *WalletStateRequest) validate() bool {
return (r.Token != auth.AuthTokenString("") &&
r.BodyJSON != "" &&
r.PubKey != auth.PublicKey("") &&
r.Signature != auth.Signature(""))
}
type WalletStateResponse struct {
BodyJSON string `json:"bodyJSON"`
Signature auth.Signature `json:"signature"`
Error string `json:"error"` // in case of 409 Conflict responses. TODO - make field not show up if it's empty, to avoid confusion
}
func (s *Server) handleWalletState(w http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
s.getWalletState(w, req)
} else if req.Method == http.MethodPost {
s.postWalletState(w, req)
} else {
errorJSON(w, http.StatusMethodNotAllowed, "")
}
}
// TODO - There's probably a struct-based solution here like with POST/PUT.
// We could put that struct up top as well.
func getWalletStateParams(req *http.Request) (pubKey auth.PublicKey, deviceId string, token auth.AuthTokenString, err error) {
tokenSlice, hasTokenSlice := req.URL.Query()["token"]
deviceIDSlice, hasDeviceId := req.URL.Query()["deviceId"]
pubKeySlice, hasPubKey := req.URL.Query()["publicKey"]
if !hasDeviceId {
err = fmt.Errorf("Missing deviceId parameter")
}
if !hasTokenSlice {
err = fmt.Errorf("Missing token parameter")
}
if !hasPubKey {
err = fmt.Errorf("Missing publicKey parameter")
}
if err == nil {
deviceId = deviceIDSlice[0]
token = auth.AuthTokenString(tokenSlice[0])
pubKey = auth.PublicKey(pubKeySlice[0])
}
return
}
func (s *Server) getWalletState(w http.ResponseWriter, req *http.Request) {
if !getGetData(w, req) {
return
}
pubKey, deviceId, token, err := getWalletStateParams(req)
if err != nil {
// In this specific case, err is limited to values that are safe to give to
// the user
errorJSON(w, http.StatusBadRequest, err.Error())
return
}
if !s.checkAuth(w, pubKey, deviceId, token, auth.ScopeGetWalletState) {
return
}
latestWalletStateJSON, latestSignature, err := s.store.GetWalletState(pubKey)
var response []byte
if err == store.ErrNoWalletState {
errorJSON(w, http.StatusNotFound, "No wallet state")
return
} else if err != nil {
internalServiceErrorJSON(w, err, "Error retrieving walletState")
return
}
walletStateResponse := WalletStateResponse{
BodyJSON: latestWalletStateJSON,
Signature: latestSignature,
}
response, err = json.Marshal(walletStateResponse)
if err != nil {
internalServiceErrorJSON(w, err, "Error generating latestWalletState response")
return
}
fmt.Fprintf(w, string(response))
}
func (s *Server) postWalletState(w http.ResponseWriter, req *http.Request) {
var walletStateRequest WalletStateRequest
if !getPostData(w, req, &walletStateRequest) {
return
}
if !s.auth.IsValidSignature(walletStateRequest.PubKey, walletStateRequest.BodyJSON, walletStateRequest.Signature) {
errorJSON(w, http.StatusBadRequest, "Bad signature")
return
}
var walletState wallet.WalletState
if err := json.Unmarshal([]byte(walletStateRequest.BodyJSON), &walletState); err != nil {
errorJSON(w, http.StatusBadRequest, "Malformed walletState JSON")
return
}
if s.walletUtil.ValidateWalletState(&walletState) {
// TODO
}
if !s.checkAuth(
w,
walletStateRequest.PubKey,
walletState.DeviceID,
walletStateRequest.Token,
auth.ScopeFull,
) {
return
}
// TODO - We could do an extra check - pull from db, make sure the new
// walletState doesn't regress lastSynced for any given device.
// This is primarily the responsibility of the clients, but we may want to
// trade a db call here for a double-check against bugs in the client.
// We do already do some validation checks here, but those doesn't require
// new database calls.
latestWalletStateJSON, latestSignature, updated, err := s.store.SetWalletState(
walletStateRequest.PubKey,
walletStateRequest.BodyJSON,
walletState.Sequence(),
walletStateRequest.Signature,
walletStateRequest.DownloadKey,
)
var response []byte
if err == store.ErrNoWalletState {
// We failed to update, and when we tried pulling the latest wallet state,
// there was nothing there. This should only happen if the client sets
// sequence != 1 for the first walletState, which would be a bug.
// TODO - figure out better error messages and/or document this
errorJSON(w, http.StatusConflict, "Bad sequence number (No existing wallet state)")
return
} else if err != nil {
// Something other than sequence error
internalServiceErrorJSON(w, err, "Error saving walletState")
return
}
walletStateResponse := WalletStateResponse{
BodyJSON: latestWalletStateJSON,
Signature: latestSignature,
}
if !updated {
// TODO - should we even call this an error?
walletStateResponse.Error = "Bad sequence number"
}
response, err = json.Marshal(walletStateResponse)
if err != nil {
internalServiceErrorJSON(w, err, "Error generating walletState response")
return
}
// Response Code:
// 200: Update successful
// 409: Update unsuccessful, probably due to new walletState's
// sequence not being 1 + current walletState's sequence
//
// Response Body:
// Current walletState (if it exists). If update successful, we just return
// the same one passed in. If update not successful, return the latest one
// from the db for the client to merge.
if updated {
fmt.Fprintf(w, string(response))
} else {
http.Error(w, string(response), http.StatusConflict)
}
}