Update API: PostWallet no longer returns a wallet

This commit is contained in:
Daniel Krol 2022-06-23 15:22:31 -04:00
parent 94114ec36d
commit 3d492d8b86
9 changed files with 205 additions and 365 deletions

View file

@ -78,6 +78,8 @@ func TestIntegrationWalletUpdates(t *testing.T) {
`{"email": "abc@example.com", "password": "123"}`, `{"email": "abc@example.com", "password": "123"}`,
) )
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
//////////////////// ////////////////////
// Get auth token - device 1 // Get auth token - device 1
//////////////////// ////////////////////
@ -130,13 +132,13 @@ func TestIntegrationWalletUpdates(t *testing.T) {
// Put first wallet - device 1 // Put first wallet - device 1
//////////////////// ////////////////////
var walletResponse WalletResponse var walletPostResponse struct{}
responseBody, statusCode = request( responseBody, statusCode = request(
t, t,
http.MethodPost, http.MethodPost,
s.postWallet, s.postWallet,
PathWallet, PathWallet,
&walletResponse, &walletPostResponse,
fmt.Sprintf(`{ fmt.Sprintf(`{
"token": "%s", "token": "%s",
"encryptedWallet": "my-encrypted-wallet-1", "encryptedWallet": "my-encrypted-wallet-1",
@ -147,34 +149,30 @@ func TestIntegrationWalletUpdates(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
////////////////////
// Get wallet - device 2
////////////////////
var walletGetResponse WalletResponse
responseBody, statusCode = request(
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken2.Token),
&walletGetResponse,
"",
)
checkStatusCode(t, statusCode, responseBody)
expectedResponse := WalletResponse{ expectedResponse := WalletResponse{
EncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-1"), EncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-1"),
Sequence: wallet.Sequence(1), Sequence: wallet.Sequence(1),
Hmac: wallet.WalletHmac("my-hmac-1"), Hmac: wallet.WalletHmac("my-hmac-1"),
} }
if !reflect.DeepEqual(walletResponse, expectedResponse) { if !reflect.DeepEqual(walletGetResponse, expectedResponse) {
t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletResponse) t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletGetResponse)
}
////////////////////
// Get wallet - device 2
////////////////////
responseBody, statusCode = request(
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken2.Token),
&walletResponse,
"",
)
checkStatusCode(t, statusCode, responseBody)
// Expect the same response getting from device 2 as when posting from device 1
if !reflect.DeepEqual(walletResponse, expectedResponse) {
t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletResponse)
} }
//////////////////// ////////////////////
@ -186,7 +184,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
http.MethodPost, http.MethodPost,
s.postWallet, s.postWallet,
PathWallet, PathWallet,
&walletResponse, &walletPostResponse,
fmt.Sprintf(`{ fmt.Sprintf(`{
"token": "%s", "token": "%s",
"encryptedWallet": "my-encrypted-wallet-2", "encryptedWallet": "my-encrypted-wallet-2",
@ -197,16 +195,6 @@ func TestIntegrationWalletUpdates(t *testing.T) {
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
expectedResponse = WalletResponse{
EncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-2"),
Sequence: wallet.Sequence(2),
Hmac: wallet.WalletHmac("my-hmac-2"),
}
if !reflect.DeepEqual(walletResponse, expectedResponse) {
t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletResponse)
}
//////////////////// ////////////////////
// Get wallet - device 1 // Get wallet - device 1
//////////////////// ////////////////////
@ -216,14 +204,20 @@ func TestIntegrationWalletUpdates(t *testing.T) {
http.MethodGet, http.MethodGet,
s.getWallet, s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken1.Token), fmt.Sprintf("%s?token=%s", PathWallet, authToken1.Token),
&walletResponse, &walletGetResponse,
"", "",
) )
checkStatusCode(t, statusCode, responseBody) checkStatusCode(t, statusCode, responseBody)
expectedResponse = WalletResponse{
EncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-2"),
Sequence: wallet.Sequence(2),
Hmac: wallet.WalletHmac("my-hmac-2"),
}
// Expect the same response getting from device 2 as when posting from device 1 // Expect the same response getting from device 2 as when posting from device 1
if !reflect.DeepEqual(walletResponse, expectedResponse) { if !reflect.DeepEqual(walletGetResponse, expectedResponse) {
t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletResponse) t.Fatalf("Unexpected response values. want: %+v got: %+v", expectedResponse, walletGetResponse)
} }
} }

View file

@ -12,7 +12,7 @@ import (
// TODO proper doc comments! // TODO proper doc comments!
const ApiVersion = "1" const ApiVersion = "2"
const PathPrefix = "/api/" + ApiVersion const PathPrefix = "/api/" + ApiVersion
const PathAuthToken = PathPrefix + "/auth/full" const PathAuthToken = PathPrefix + "/auth/full"

View file

@ -67,7 +67,6 @@ type TestStore struct {
TestEncryptedWallet wallet.EncryptedWallet TestEncryptedWallet wallet.EncryptedWallet
TestSequence wallet.Sequence TestSequence wallet.Sequence
TestHmac wallet.WalletHmac TestHmac wallet.WalletHmac
TestSequenceCorrect bool
} }
func (s *TestStore) SaveToken(authToken *auth.AuthToken) error { func (s *TestStore) SaveToken(authToken *auth.AuthToken) error {
@ -95,16 +94,9 @@ func (s *TestStore) SetWallet(
encryptedWallet wallet.EncryptedWallet, encryptedWallet wallet.EncryptedWallet,
sequence wallet.Sequence, sequence wallet.Sequence,
hmac wallet.WalletHmac, hmac wallet.WalletHmac,
) (latestEncryptedWallet wallet.EncryptedWallet, latestSequence wallet.Sequence, latestHmac wallet.WalletHmac, sequenceCorrect bool, err error) { ) (err error) {
s.Called.SetWallet = SetWalletCall{encryptedWallet, sequence, hmac} s.Called.SetWallet = SetWalletCall{encryptedWallet, sequence, hmac}
err = s.Errors.SetWallet return s.Errors.SetWallet
if err == nil {
latestEncryptedWallet = s.TestEncryptedWallet
latestSequence = s.TestSequence
latestHmac = s.TestHmac
sequenceCorrect = s.TestSequenceCorrect
}
return
} }
func (s *TestStore) GetWallet(userId auth.UserId) (encryptedWallet wallet.EncryptedWallet, sequence wallet.Sequence, hmac wallet.WalletHmac, err error) { func (s *TestStore) GetWallet(userId auth.UserId) (encryptedWallet wallet.EncryptedWallet, sequence wallet.Sequence, hmac wallet.WalletHmac, err error) {
@ -229,7 +221,7 @@ func TestServerHelperGetGetDataSuccess(t *testing.T) {
} }
} }
func TestServerHelperGetGetDataErrors(t *testing.T) { func TestServerHelperGetGetDataErrors(t *testing.T) {
// Only error right now is if you do a POST request // Only error right now is if you do a POST request
req := httptest.NewRequest(http.MethodPost, "/test", nil) req := httptest.NewRequest(http.MethodPost, "/test", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
success := getGetData(w, req) success := getGetData(w, req)

View file

@ -27,7 +27,6 @@ type WalletResponse struct {
EncryptedWallet wallet.EncryptedWallet `json:"encryptedWallet"` EncryptedWallet wallet.EncryptedWallet `json:"encryptedWallet"`
Sequence wallet.Sequence `json:"sequence"` Sequence wallet.Sequence `json:"sequence"`
Hmac wallet.WalletHmac `json:"hmac"` Hmac wallet.WalletHmac `json:"hmac"`
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) handleWallet(w http.ResponseWriter, req *http.Request) { func (s *Server) handleWallet(w http.ResponseWriter, req *http.Request) {
@ -103,6 +102,11 @@ func (s *Server) getWallet(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, string(response)) fmt.Fprintf(w, string(response))
} }
// Response Code:
// 200: Update successful
// 409: Update unsuccessful due to new wallet's sequence not being 1 +
// current wallet's sequence
// 500: Update unsuccessful for unanticipated reasons
func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) { func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
var walletRequest WalletRequest var walletRequest WalletRequest
if !getPostData(w, req, &walletRequest) { if !getPostData(w, req, &walletRequest) {
@ -114,21 +118,10 @@ func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
return return
} }
latestEncryptedWallet, latestSequence, latestHmac, sequenceCorrect, err := s.store.SetWallet( err := s.store.SetWallet(authToken.UserId, walletRequest.EncryptedWallet, walletRequest.Sequence, walletRequest.Hmac)
authToken.UserId,
walletRequest.EncryptedWallet,
walletRequest.Sequence,
walletRequest.Hmac,
)
var response []byte if err == store.ErrWrongSequence {
errorJson(w, http.StatusConflict, "Bad sequence number")
if err == store.ErrNoWallet {
// We failed to update, and when we tried pulling the latest wallet,
// there was nothing there. This should only happen if the client sets
// sequence != 1 for the first wallet, 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)")
return return
} else if err != nil { } else if err != nil {
// Something other than sequence error // Something other than sequence error
@ -136,16 +129,8 @@ func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
return return
} }
walletResponse := WalletResponse{ var response []byte
EncryptedWallet: latestEncryptedWallet, var walletResponse struct{}
Sequence: latestSequence,
Hmac: latestHmac,
}
if !sequenceCorrect {
// TODO - should we even call this an error?
walletResponse.Error = http.StatusText(http.StatusConflict) + ": " + "Bad sequence number"
}
response, err = json.Marshal(walletResponse) response, err = json.Marshal(walletResponse)
if err != nil { if err != nil {
@ -153,18 +138,5 @@ func (s *Server) postWallet(w http.ResponseWriter, req *http.Request) {
return return
} }
// Response Code: fmt.Fprintf(w, string(response))
// 200: Update successful
// 409: Update unsuccessful, probably due to new wallet's
// sequence not being 1 + current wallet's sequence
//
// Response Body:
// Current wallet (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 sequenceCorrect {
fmt.Fprintf(w, string(response))
} else {
http.Error(w, string(response), http.StatusConflict)
}
} }

View file

@ -131,10 +131,7 @@ func TestServerPostWallet(t *testing.T) {
expectedStatusCode int expectedStatusCode int
expectedErrorString string expectedErrorString string
expectSetWalletCall bool
// There is one case where we expect both the error field and the normal
// body fields. So, this needs to be separate.
expectWalletBody bool
// This is getting messy, but in the case of validation failures, we don't // This is getting messy, but in the case of validation failures, we don't
// even get around to trying to get an auth token, since the token string is // even get around to trying to get an auth token, since the token string is
@ -144,93 +141,38 @@ func TestServerPostWallet(t *testing.T) {
// `new...` refers to what is being passed into the via POST request (and // `new...` refers to what is being passed into the via POST request (and
// what gets passed into SetWallet for the *non-error* cases below) // what gets passed into SetWallet for the *non-error* cases below)
// `returned...` refers to what the SetWallet function returns (and what newEncryptedWallet wallet.EncryptedWallet
// gets returned in the request response for the *non-error* cases below) newSequence wallet.Sequence
newEncryptedWallet wallet.EncryptedWallet newHmac wallet.WalletHmac
returnedEncryptedWallet wallet.EncryptedWallet
newSequence wallet.Sequence
returnedSequence wallet.Sequence
newHmac wallet.WalletHmac
returnedHmac wallet.WalletHmac
// Passed-in sequence was correct. No conflict.
sequenceCorrect bool
storeErrors TestStoreFunctionsErrors storeErrors TestStoreFunctionsErrors
}{ }{
{ {
name: "success", name: "success",
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
expectWalletBody: true, expectSetWalletCall: true,
// Simulates a situation where the existing sequence is 1, the new // Simulates a situation where the existing sequence is 1, the new
// sequence is 2. We don't see the existing data in this case because it // sequence is 2.
// successfully updates to and returns the new data. New and returned are
// the same here.
sequenceCorrect: true,
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
returnedEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newSequence: wallet.Sequence(2),
returnedSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
returnedHmac: wallet.WalletHmac("my-hmac"),
},
{
name: "conflict",
expectedStatusCode: http.StatusConflict,
// In the special case of "conflict" where there *is* a stored wallet, we
// process the function in a normal way, but we still have the Error field.
// So, we can't rely on `tc.expectedErrorString == ""` to indicate that it
// is the type of error without a body. So, we need to specify this
// separately. In this case we will check the error string along with the
// body.
expectWalletBody: true,
// Simulates a situation where the existing sequence is 3, the new sequence
// is 2. This is a conflict, because the new sequence should be 4. A new
// sequence of 3 would also be a conflict, but we want two different
// sequence numbers for a clear test. We return the existing data in this
// case for the client to merge in. Note that we're passing in a sequence
// that makes sense for a conflict case, the actual behavior is triggered by
// sequenceCorrect=false
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
}, {
name: "conflict",
expectedStatusCode: http.StatusConflict,
expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number", expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number",
expectSetWalletCall: true,
sequenceCorrect: false, // Simulates a situation where the existing sequence is *not* 1, the new
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-new"), // proposed sequence is 2, and it thus fails with a conflict.
returnedEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-existing"),
newSequence: wallet.Sequence(2),
returnedSequence: wallet.Sequence(3),
newHmac: wallet.WalletHmac("my-hmac-new"),
returnedHmac: wallet.WalletHmac("my-hmac-existing"),
},
{
name: "conflict with no stored wallet",
expectedStatusCode: http.StatusConflict,
// Simulates a situation where there is no stored wallet. In such a case the newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-new"),
// correct sequence would be 1, which implies the wallet should be inserted newSequence: wallet.Sequence(2),
// (as opposed to updated). We will be passing in a sequence of 2 for newHmac: wallet.WalletHmac("my-hmac-new"),
// clarity, but what will actually trigger the desired error we are testing
// for is SetWallet returning ErrNoWallet, which is what the store is
// expected to return in this situation.
expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number (No existing wallet)", storeErrors: TestStoreFunctionsErrors{SetWallet: store.ErrWrongSequence},
}, {
// In this case the "returned" values are blank because there will be
// nothing to return
sequenceCorrect: false,
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
returnedEncryptedWallet: wallet.EncryptedWallet(""),
newSequence: wallet.Sequence(2),
returnedSequence: wallet.Sequence(0),
newHmac: wallet.WalletHmac("my-hmac"),
returnedHmac: wallet.WalletHmac(""),
storeErrors: TestStoreFunctionsErrors{SetWallet: store.ErrNoWallet},
},
{
name: "validation error", name: "validation error",
expectedStatusCode: http.StatusBadRequest, expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation", expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation",
@ -240,47 +182,33 @@ func TestServerPostWallet(t *testing.T) {
// validate function is called. We'll check the rest of the validation // validate function is called. We'll check the rest of the validation
// errors in the other test below. // errors in the other test below.
sequenceCorrect: true, newEncryptedWallet: wallet.EncryptedWallet(""),
newEncryptedWallet: wallet.EncryptedWallet(""), newSequence: wallet.Sequence(2),
returnedEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"), newHmac: wallet.WalletHmac("my-hmac"),
newSequence: wallet.Sequence(2), }, {
returnedSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
returnedHmac: wallet.WalletHmac("my-hmac"),
},
{
name: "auth error", name: "auth error",
expectedStatusCode: http.StatusUnauthorized, expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found", expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
// Putting in valid data here so it's clear that this isn't what causes // Putting in valid data here so it's clear that this isn't what causes
// the error // the error
sequenceCorrect: true, newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"), newSequence: wallet.Sequence(2),
returnedEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"), newHmac: wallet.WalletHmac("my-hmac"),
newSequence: wallet.Sequence(2),
returnedSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
returnedHmac: wallet.WalletHmac("my-hmac"),
// What causes the error // What causes the error
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoToken}, storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoToken},
}, }, {
{ name: "db error setting wallet",
name: "db error setting or getting wallet",
expectedStatusCode: http.StatusInternalServerError, expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError), expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectSetWalletCall: true,
// Putting in valid data here so it's clear that this isn't what causes // Putting in valid data here so it's clear that this isn't what causes
// the error // the error
sequenceCorrect: true, newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"), newSequence: wallet.Sequence(2),
returnedEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"), newHmac: wallet.WalletHmac("my-hmac"),
newSequence: wallet.Sequence(2),
returnedSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
returnedHmac: wallet.WalletHmac("my-hmac"),
// What causes the error // What causes the error
storeErrors: TestStoreFunctionsErrors{SetWallet: fmt.Errorf("Some random db problem")}, storeErrors: TestStoreFunctionsErrors{SetWallet: fmt.Errorf("Some random db problem")},
@ -301,11 +229,6 @@ func TestServerPostWallet(t *testing.T) {
Scope: auth.ScopeFull, Scope: auth.ScopeFull,
}, },
TestEncryptedWallet: tc.returnedEncryptedWallet,
TestSequence: tc.returnedSequence,
TestHmac: tc.returnedHmac,
TestSequenceCorrect: tc.sequenceCorrect,
Errors: tc.storeErrors, Errors: tc.storeErrors,
} }
@ -338,21 +261,11 @@ func TestServerPostWallet(t *testing.T) {
expectStatusCode(t, w, tc.expectedStatusCode) expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString) expectErrorString(t, body, tc.expectedErrorString)
if !tc.expectWalletBody { if tc.expectedErrorString == "" && string(body) != "{}" {
return // The rest of the test does not apply t.Errorf("Expected post wallet response to be \"{}\": result: %+v", string(body))
} }
var result WalletResponse if want, got := (SetWalletCall{tc.newEncryptedWallet, tc.newSequence, tc.newHmac}), testStore.Called.SetWallet; tc.expectSetWalletCall && want != got {
err := json.Unmarshal(body, &result)
if err != nil ||
result.EncryptedWallet != tc.returnedEncryptedWallet ||
result.Hmac != tc.returnedHmac ||
result.Sequence != tc.returnedSequence {
t.Errorf("Expected wallet response to have the test wallet values: result: %+v err: %+v", string(body), err)
}
if want, got := (SetWalletCall{tc.newEncryptedWallet, tc.newSequence, tc.newHmac}), testStore.Called.SetWallet; want != got {
t.Errorf("Store.SetWallet called with: expected %+v, got %+v", want, got) t.Errorf("Store.SetWallet called with: expected %+v, got %+v", want, got)
} }
}) })

View file

@ -21,6 +21,8 @@ var (
ErrDuplicateWallet = fmt.Errorf("Wallet already exists for this user") ErrDuplicateWallet = fmt.Errorf("Wallet already exists for this user")
ErrNoWallet = fmt.Errorf("Wallet does not exist for this user at this sequence") ErrNoWallet = fmt.Errorf("Wallet does not exist for this user at this sequence")
ErrWrongSequence = fmt.Errorf("Wallet could not be updated to this sequence")
ErrDuplicateEmail = fmt.Errorf("Email already exists for this user") ErrDuplicateEmail = fmt.Errorf("Email already exists for this user")
ErrDuplicateAccount = fmt.Errorf("User already has an account") ErrDuplicateAccount = fmt.Errorf("User already has an account")
@ -31,7 +33,7 @@ var (
type StoreInterface interface { type StoreInterface interface {
SaveToken(*auth.AuthToken) error SaveToken(*auth.AuthToken) error
GetToken(auth.TokenString) (*auth.AuthToken, error) GetToken(auth.TokenString) (*auth.AuthToken, error)
SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, bool, error) SetWallet(auth.UserId, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error) GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
GetUserId(auth.Email, auth.Password) (auth.UserId, error) GetUserId(auth.Email, auth.Password) (auth.UserId, error)
CreateAccount(auth.Email, auth.Password) (err error) CreateAccount(auth.Email, auth.Password) (err error)
@ -282,39 +284,17 @@ func (s *Store) updateWalletToSequence(
} }
// Assumption: Sequence has been validated (>=1) // Assumption: Sequence has been validated (>=1)
func (s *Store) SetWallet( func (s *Store) SetWallet(userId auth.UserId, encryptedWallet wallet.EncryptedWallet, sequence wallet.Sequence, hmac wallet.WalletHmac) (err error) {
userId auth.UserId,
encryptedWallet wallet.EncryptedWallet,
sequence wallet.Sequence,
hmac wallet.WalletHmac,
// TODO `sequenceCorrect` should probably be replaced with `status`, that can
// equal `Updated` or `SequenceMismatch`. Maybe with a message for the API.
// Like an error, but not, because the function still returns a value.
// Right now, we have:
// `sequenceCorrect==true` and `err==nil` implies it updated.
// We could also have:
// `sequenceMismatch==true` or `err!=nil` implying it didn't update.
// Or:
// `updated==false` and `err=nil` implying the sequence mismatched.
// I don't like this implication stuff, the "status" should be explicit so
// we don't make bugs.
) (latestEncryptedWallet wallet.EncryptedWallet, latestSequence wallet.Sequence, latestHmac wallet.WalletHmac, sequenceCorrect bool, err error) {
if sequence == 1 { if sequence == 1 {
// If sequence == 1, the client assumed that this is our first // If sequence == 1, the client assumed that this is our first
// wallet. Try to insert. If we get a conflict, the client // wallet. Try to insert. If we get a conflict, the client
// assumed incorrectly and we proceed below to return the latest // assumed incorrectly and we proceed below to return the latest
// wallet from the db. // wallet from the db.
err = s.insertFirstWallet(userId, encryptedWallet, hmac) err = s.insertFirstWallet(userId, encryptedWallet, hmac)
if err == nil { if err == ErrDuplicateWallet {
// Successful update // A wallet already exists. That means the input sequence should not be 1.
latestEncryptedWallet = encryptedWallet // To the caller, this means the sequence was wrong.
latestSequence = sequence err = ErrWrongSequence
latestHmac = hmac
sequenceCorrect = true
return
} else if err != ErrDuplicateWallet {
// Unsuccessful update for reasons other than sequence conflict
return
} }
} else { } else {
// If sequence > 1, the client assumed that it is replacing wallet // If sequence > 1, the client assumed that it is replacing wallet
@ -322,25 +302,12 @@ func (s *Store) SetWallet(
// sequence - 1. If we updated no rows, the client assumed incorrectly // sequence - 1. If we updated no rows, the client assumed incorrectly
// and we proceed below to return the latest wallet from the db. // and we proceed below to return the latest wallet from the db.
err = s.updateWalletToSequence(userId, encryptedWallet, sequence, hmac) err = s.updateWalletToSequence(userId, encryptedWallet, sequence, hmac)
if err == nil { if err == ErrNoWallet {
latestEncryptedWallet = encryptedWallet // No wallet found to replace at the `sequence - 1`. To the caller, this
latestSequence = sequence // means the sequence they put in was wrong.
latestHmac = hmac err = ErrWrongSequence
sequenceCorrect = true
return
} else if err != ErrNoWallet {
return
} }
} }
// We failed to update above due to a sequence conflict. Perhaps the client
// was unaware of an update done by another client. Let's send back the latest
// version right away so the requesting client can take care of it.
//
// Note that this means that `err` will not be `nil` at this point, but we
// already accounted for it with `sequenceCorrect=false`. Instead, we'll pass
// on any errors from calling `GetWallet`.
latestEncryptedWallet, latestSequence, latestHmac, err = s.GetWallet(userId)
return return
} }

View file

@ -25,36 +25,34 @@ Now that the account exists, grab an auth token with both clients.
``` ```
>>> c1.get_auth_token() >>> c1.get_auth_token()
Got auth token: 2e1c00c0f2f205defc177bd21e64dd01c669e234cf23bbc19f73e720ac1ef12d Got auth token: 4a3d9b8569c3b06079ff26d60ebc56db6254305217602c19b0af6e02db6d95d7
>>> c2.get_auth_token() >>> c2.get_auth_token()
Got auth token: 07ab32cfac3961d30570537d4082abdf08123de6e5a28670dbf680be45e442d5 Got auth token: 33fd77031ccaec966018867e960446bf39d51a3c492c3d997d5f1aa13c75298d
``` ```
## Syncing ## Syncing
Create a new wallet + metadata (we'll wrap it in a struct we'll call `WalletState` in this client) using `init_wallet_state` and POST them to the server. The metadata (as of now) in the walletstate is only `sequence`. `sequence` is an integer that increments for every POSTed wallet. This is bookkeeping to prevent certain syncing errors. Create a new wallet + metadata (we'll wrap it in a struct we'll call `WalletState` in this client) using `init_wallet_state` and POST them to the server. The metadata (as of now) in the walletstate is only `sequence`. `sequence` is an integer that increments for every POSTed wallet. This is bookkeeping to prevent certain syncing errors.
_Note that after POSTing, it says it "got" a new wallet. This is because the POST endpoint also returns the latest version. The purpose of this will be explained in "Conflicts" below._
``` ```
>>> c1.init_wallet_state() >>> c1.init_wallet_state()
Wallet not found
No wallet found on the server for this account. Starting a new one.
>>> c1.update_remote_wallet() >>> c1.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6CjZHlCv4ZyHiPKA7PoIOOkQ6Fh9fYUYPe9xwiZRYdLKHDgtEQCIcwkNldP1TN8TwTht4Qj5QEnApwQkd2Y20nVjdCUTKLzu4gmdP8QBz2EEGR+XmZgosX937E8bmmqgC55ttgt8fh0o62cTonF4h1LLI7DoWw1SvEcqIIAEn/dc=') WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6/MNVSMrjIqPzrD/oaub++J3lc5qW+baxD0EI6n5/XqGgRsUND3G7fqRsn/riULM4zap+jI8XgW6l1rieJWGZXPQvIZJP8B7gQvBDfzlY0BxUgECeX38I5EtRFNWU3sTwmAaAaDuBpaBXvnf2hu4SEp5xl/OQVg9h+BluTZBdLSU=')
'Success' 'Success'
``` ```
Now, call `init_wallet_state` with the other client. This time, `init_wallet_state` will GET the wallet from the server. In general, `init_wallet_state` is used to set up a new client; first it checks the server, then failing that, it initializes it locally. (In a real client, it would save the walletstate to disk, and `init_wallet_state` would check there before checking the server). Now, call `init_wallet_state` with the other client. Then, we call `get_remote_wallet` to GET the wallet from the server. (In a real client, it would also save the walletstate to disk, and `init_wallet_state` would check there before checking the server).
(There are a few potential unresolved issues surrounding this related to sequence of events. Check comments on `init_wallet_state`. SDK again works around them with the timestamps.) (There are a few potential unresolved issues surrounding this related to sequence of events. Check comments on `init_wallet_state`. SDK again works around them with the timestamps.)
``` ```
>>> c2.init_wallet_state() >>> c2.init_wallet_state()
Got latest walletState: >>> c2.get_remote_wallet()
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6CjZHlCv4ZyHiPKA7PoIOOkQ6Fh9fYUYPe9xwiZRYdLKHDgtEQCIcwkNldP1TN8TwTht4Qj5QEnApwQkd2Y20nVjdCUTKLzu4gmdP8QBz2EEGR+XmZgosX937E8bmmqgC55ttgt8fh0o62cTonF4h1LLI7DoWw1SvEcqIIAEn/dc=') Got (and maybe merged in) latest walletState:
WalletState(sequence=1, encrypted_wallet='czo4MTkyOjE2OjE6/MNVSMrjIqPzrD/oaub++J3lc5qW+baxD0EI6n5/XqGgRsUND3G7fqRsn/riULM4zap+jI8XgW6l1rieJWGZXPQvIZJP8B7gQvBDfzlY0BxUgECeX38I5EtRFNWU3sTwmAaAaDuBpaBXvnf2hu4SEp5xl/OQVg9h+BluTZBdLSU=')
'Success'
``` ```
## Updating ## Updating
@ -64,12 +62,12 @@ Push a new version, GET it with the other client. Even though we haven't edited
``` ```
>>> c2.update_remote_wallet() >>> c2.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6LsWo7O3EQVw+buxGPuqJBBEn3oBM3/sAII2NjpbKi7tEvWxbWmKb+nNyr3fuvQ6YZZda0i0Rb7Veuq7ym+hYAn2pTt/8WrYR8K1HFnxs3y1m91HQIsXrl6NwxU5t+mZ6uInQUfEGEV6JLHfbt1NJ2pYlYvYTelusKZXq/kja8i4=') WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6MIPxgbxNGbaZWboH6ci6wBT3izdpb/B3JYdl3nJdQn6EV54W4QaYUvuUxMa5XngiXlNLcLbmFRqeYj/mgAbEVXRKLyLQxjB7rIhGcRxsHbzGR8YDMVvP+m5dWaxevlZc7cEZkpRQKfFyuc+pnjPEk9SUvEgioN1Hxir6DonMqlA=')
'Success' 'Success'
>>> c1.get_remote_wallet() >>> c1.get_remote_wallet()
Got latest walletState: Got (and maybe merged in) latest walletState:
WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6LsWo7O3EQVw+buxGPuqJBBEn3oBM3/sAII2NjpbKi7tEvWxbWmKb+nNyr3fuvQ6YZZda0i0Rb7Veuq7ym+hYAn2pTt/8WrYR8K1HFnxs3y1m91HQIsXrl6NwxU5t+mZ6uInQUfEGEV6JLHfbt1NJ2pYlYvYTelusKZXq/kja8i4=') WalletState(sequence=2, encrypted_wallet='czo4MTkyOjE2OjE6MIPxgbxNGbaZWboH6ci6wBT3izdpb/B3JYdl3nJdQn6EV54W4QaYUvuUxMa5XngiXlNLcLbmFRqeYj/mgAbEVXRKLyLQxjB7rIhGcRxsHbzGR8YDMVvP+m5dWaxevlZc7cEZkpRQKfFyuc+pnjPEk9SUvEgioN1Hxir6DonMqlA=')
'Success' 'Success'
``` ```
@ -94,12 +92,12 @@ The wallet is synced between the clients. The client with the changed preference
``` ```
>>> c1.update_remote_wallet() >>> c1.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6l5SVvs2yNDoC5j1316n0xQ5H6K1UEso/JpdpShfLW2bCY3lg9vOcwayO1v085RyItxEwtrtSnD3fnan3kr86GmSI8U6x5DxASHVdgceLBrclVkuCpFXllz6YNtWo5thjbf63PWSg4k6LHI8w50BT2tu9FUufCi67n7sTWnGb/0AjAFYU1sUTJ9aoeiZYrrur') WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6YUEKfjxhUXeHrNbPuWpMt5o/6H5fSSKFZAMkb8YugMGEHzVAZDfGMdowwdycXkyTZtPRiMSs+kgOX8BLomcz/I+de8b1EsXribYR05sgySRJiPoW8VBRlmgbRapZ9iGaxvgJJWmVAO42beNWtnuE3bdpDtWtZjgcXWq6lnhNlETmKEEPthezGB8svHPHt/rJ')
'Success' 'Success'
>>> c2.get_remote_wallet() >>> c2.get_remote_wallet()
Got latest walletState: Got (and maybe merged in) latest walletState:
WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6l5SVvs2yNDoC5j1316n0xQ5H6K1UEso/JpdpShfLW2bCY3lg9vOcwayO1v085RyItxEwtrtSnD3fnan3kr86GmSI8U6x5DxASHVdgceLBrclVkuCpFXllz6YNtWo5thjbf63PWSg4k6LHI8w50BT2tu9FUufCi67n7sTWnGb/0AjAFYU1sUTJ9aoeiZYrrur') WalletState(sequence=3, encrypted_wallet='czo4MTkyOjE2OjE6YUEKfjxhUXeHrNbPuWpMt5o/6H5fSSKFZAMkb8YugMGEHzVAZDfGMdowwdycXkyTZtPRiMSs+kgOX8BLomcz/I+de8b1EsXribYR05sgySRJiPoW8VBRlmgbRapZ9iGaxvgJJWmVAO42beNWtnuE3bdpDtWtZjgcXWq6lnhNlETmKEEPthezGB8svHPHt/rJ')
'Success' 'Success'
>>> c2.get_preferences() >>> c2.get_preferences()
{'animal': 'cow', 'car': ''} {'animal': 'cow', 'car': ''}
@ -125,8 +123,8 @@ One client POSTs its change first.
``` ```
>>> c1.update_remote_wallet() >>> c1.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE66nridrsrXcL/DlcudUs7RaAIZ3jRYQJhaacRH3vPNx0TZqkJbDcjMiHbiHY6U2AVhoAsLPIf/zcU+uDTw4IRcOL9Gozupc8tCrIcgm/kwXjnQI9RNzIfDsFxalBKj0u7Xf0c+5f/ntr4Hs9Q/Y7qthseNbUBZKU12KxNlmDcE7knLOui6NQdsUvFpuI/Rzgr') WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6ZAO02VSfc0UTNcKJosuTzdpB1GCRw+f1bCrR/1aFDGoK5Iq/OyKXygp3p2trj2EU1SUfp6m/FiWYdN920uzpaQnIbOlEs6anPpd3alNQmNfuT1s8bKnliO6so657VjZf0QdadDrCVa8WZMiuHY+wP2H5LpzDIrRYrzNyyUuhffbh8yk8cQhgRScFKczpAnu+')
'Success' 'Success'
``` ```
@ -136,8 +134,8 @@ Eventually, the client will be responsible (or at least more responsible) for me
``` ```
>>> c2.get_remote_wallet() >>> c2.get_remote_wallet()
Got latest walletState: Got (and maybe merged in) latest walletState:
WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE66nridrsrXcL/DlcudUs7RaAIZ3jRYQJhaacRH3vPNx0TZqkJbDcjMiHbiHY6U2AVhoAsLPIf/zcU+uDTw4IRcOL9Gozupc8tCrIcgm/kwXjnQI9RNzIfDsFxalBKj0u7Xf0c+5f/ntr4Hs9Q/Y7qthseNbUBZKU12KxNlmDcE7knLOui6NQdsUvFpuI/Rzgr') WalletState(sequence=4, encrypted_wallet='czo4MTkyOjE2OjE6ZAO02VSfc0UTNcKJosuTzdpB1GCRw+f1bCrR/1aFDGoK5Iq/OyKXygp3p2trj2EU1SUfp6m/FiWYdN920uzpaQnIbOlEs6anPpd3alNQmNfuT1s8bKnliO6so657VjZf0QdadDrCVa8WZMiuHY+wP2H5LpzDIrRYrzNyyUuhffbh8yk8cQhgRScFKczpAnu+')
'Success' 'Success'
>>> c2.get_preferences() >>> c2.get_preferences()
{'animal': 'horse', 'car': 'Audi'} {'animal': 'horse', 'car': 'Audi'}
@ -148,12 +146,12 @@ Finally, the client with the merged wallet pushes it to the server, and the othe
``` ```
>>> c2.update_remote_wallet() >>> c2.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE68NAahtUE4gg2M6Fam/E3brb4sv1TzcXJLvGRh4CY4416haOF1lxmKSdrvIPpOBvpNPS0B5qCbmpaKQ8Pm/WRCLj1yYUDVKgSZx0ru7AJBHiBLtpKA99Ia7XlWl129p6WtjJkbOoW8Ya+PEii72g4nrtM+j40Xe9UbVI463tlKYaRvmKr/BcoFGMJSB10Whh8') WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE6cat6gX80ib+t6bX9QlBw3jspj4jJ6U8AGULRDPNa8PbL4CX6ohZoXkt+duNYPxWDdyl8xqhwisWXTXkuGUBwP2zrVmZC3TNt5A9Pk/y/tNgMz50CY3JmNYcbCeZyoY+uV+cMfdO+n3p3hYriNKgn539NC6ug80U/2heevVax4NgMAF0lWEBM2E886+KkvfHG')
'Success' 'Success'
>>> c1.get_remote_wallet() >>> c1.get_remote_wallet()
Got latest walletState: Got (and maybe merged in) latest walletState:
WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE68NAahtUE4gg2M6Fam/E3brb4sv1TzcXJLvGRh4CY4416haOF1lxmKSdrvIPpOBvpNPS0B5qCbmpaKQ8Pm/WRCLj1yYUDVKgSZx0ru7AJBHiBLtpKA99Ia7XlWl129p6WtjJkbOoW8Ya+PEii72g4nrtM+j40Xe9UbVI463tlKYaRvmKr/BcoFGMJSB10Whh8') WalletState(sequence=5, encrypted_wallet='czo4MTkyOjE2OjE6cat6gX80ib+t6bX9QlBw3jspj4jJ6U8AGULRDPNa8PbL4CX6ohZoXkt+duNYPxWDdyl8xqhwisWXTXkuGUBwP2zrVmZC3TNt5A9Pk/y/tNgMz50CY3JmNYcbCeZyoY+uV+cMfdO+n3p3hYriNKgn539NC6ug80U/2heevVax4NgMAF0lWEBM2E886+KkvfHG')
'Success' 'Success'
>>> c1.get_preferences() >>> c1.get_preferences()
{'animal': 'horse', 'car': 'Audi'} {'animal': 'horse', 'car': 'Audi'}
@ -178,29 +176,34 @@ So for example, let's say we create diverging changes in the wallets:
{'animal': 'horse', 'car': 'Toyota'} {'animal': 'horse', 'car': 'Toyota'}
``` ```
We try to POST both of them to the server, but the second one fails because of the conflict. Instead, merges the two locally: We try to POST both of them to the server. The second one fails because of the conflict, and we see that its preferences don't change yet.
``` ```
>>> c2.update_remote_wallet() >>> c2.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6sh95Bt0OfcDY3QwUWaPgPWD1WPYCkN2yg1+XLD/5puONhNyjzVAnhINqVvPy52pxfkVgkIScLacMQFq4W19d+SC5LConu+fPchBzYj14Wvc3/IEQiQIxbmkv6N9USvYsjAzjGqK7szistRJY4MHC4/wRbWRprfIE7BFcDaisFSe18mRs8D2KlhEzjNJu+X8+') WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6IQ+uyjKiGAIEjoNliOsANoq2h/exQpwordUQFVbbHVhj27UbJS7ykMV4or5avEwNo+aCYC8j7HEqqaPnhvNYeeyPbmpfZS0lU7MXBehoqvIPR3GyTLM002t7SUrB+KxdvUX8RAamjiahDI8OeTOBmYhgQLSZt/ZDtRL/3f5l1JgLCjEbVKJY6Pim0hk7AlpK')
'Success' 'Success'
>>> c1.update_remote_wallet() >>> c1.update_remote_wallet()
Wallet state out of date. Getting updated wallet state. Try posting again after this. Submitted wallet is out of date.
Got new walletState: Could not update. Need to get new wallet and merge
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6sh95Bt0OfcDY3QwUWaPgPWD1WPYCkN2yg1+XLD/5puONhNyjzVAnhINqVvPy52pxfkVgkIScLacMQFq4W19d+SC5LConu+fPchBzYj14Wvc3/IEQiQIxbmkv6N9USvYsjAzjGqK7szistRJY4MHC4/wRbWRprfIE7BFcDaisFSe18mRs8D2KlhEzjNJu+X8+') 'Failure'
>>> c1.get_preferences()
{'animal': 'horse', 'car': 'Toyota'}
```
The client that is out of date will then call `get_remote_wallet`, which GETs and automatically merges in the latest wallet. We see the preferences are now merged. Now it can make a second POST request containing the merged wallet.
```
>>> c1.get_remote_wallet()
Got (and maybe merged in) latest walletState:
WalletState(sequence=6, encrypted_wallet='czo4MTkyOjE2OjE6IQ+uyjKiGAIEjoNliOsANoq2h/exQpwordUQFVbbHVhj27UbJS7ykMV4or5avEwNo+aCYC8j7HEqqaPnhvNYeeyPbmpfZS0lU7MXBehoqvIPR3GyTLM002t7SUrB+KxdvUX8RAamjiahDI8OeTOBmYhgQLSZt/ZDtRL/3f5l1JgLCjEbVKJY6Pim0hk7AlpK')
'Success' 'Success'
>>> c1.get_preferences() >>> c1.get_preferences()
{'animal': 'beaver', 'car': 'Toyota'} {'animal': 'beaver', 'car': 'Toyota'}
```
Now that the merge is complete, the client can make a second POST request containing the merged wallet.
```
>>> c1.update_remote_wallet() >>> c1.update_remote_wallet()
Successfully updated wallet state on server Successfully updated wallet state on server
Got new walletState: Synced walletState:
WalletState(sequence=7, encrypted_wallet='czo4MTkyOjE2OjE68J19IGGfoiRDm15Nb1sTj5yP9Mc3jpAeYarh206kLXKMKLKCIahmhLDMqBCXgwDe098uaIqB6IwKDfXbCVJHhWfqzu/5GoWPK1QZjhCu0rGxteFv4Tio0IYGg8CUYCvOhpQA319SXEf4sF9cyC32VwlL6qkJ2TzWTu9bTGUfZRGV3q9Rt9oL4OQHxuNIPEiE') WalletState(sequence=7, encrypted_wallet='czo4MTkyOjE2OjE63OwBCfczOA+n0EMe0lHPwVvmrXsJwKJXGPYFSmdDseHbd3HRpOZ/Id5WeOuata5/dHJ4vdaaw8RNfpgR4KVzOkM5BUZNxzBaVf/BEYL8nJcbv7l5ZLs6Q15IqvlmZ3HBPVzxO/WYqm4aL9+CNeoYG2LzaIxsnzf31ZoG9I78B6wxK5JXCjDS+nuh/4NM+REE')
'Success' 'Success'
``` ```

View file

@ -67,8 +67,6 @@ print("""
## Syncing ## Syncing
Create a new wallet + metadata (we'll wrap it in a struct we'll call `WalletState` in this client) using `init_wallet_state` and POST them to the server. The metadata (as of now) in the walletstate is only `sequence`. `sequence` is an integer that increments for every POSTed wallet. This is bookkeeping to prevent certain syncing errors. Create a new wallet + metadata (we'll wrap it in a struct we'll call `WalletState` in this client) using `init_wallet_state` and POST them to the server. The metadata (as of now) in the walletstate is only `sequence`. `sequence` is an integer that increments for every POSTed wallet. This is bookkeeping to prevent certain syncing errors.
_Note that after POSTing, it says it "got" a new wallet. This is because the POST endpoint also returns the latest version. The purpose of this will be explained in "Conflicts" below._
""") """)
code_block(""" code_block("""
@ -77,13 +75,14 @@ c1.update_remote_wallet()
""") """)
print(""" print("""
Now, call `init_wallet_state` with the other client. This time, `init_wallet_state` will GET the wallet from the server. In general, `init_wallet_state` is used to set up a new client; first it checks the server, then failing that, it initializes it locally. (In a real client, it would save the walletstate to disk, and `init_wallet_state` would check there before checking the server). Now, call `init_wallet_state` with the other client. Then, we call `get_remote_wallet` to GET the wallet from the server. (In a real client, it would also save the walletstate to disk, and `init_wallet_state` would check there before checking the server).
(There are a few potential unresolved issues surrounding this related to sequence of events. Check comments on `init_wallet_state`. SDK again works around them with the timestamps.) (There are a few potential unresolved issues surrounding this related to sequence of events. Check comments on `init_wallet_state`. SDK again works around them with the timestamps.)
""") """)
code_block(""" code_block("""
c2.init_wallet_state() c2.init_wallet_state()
c2.get_remote_wallet()
""") """)
print(""" print("""
@ -183,7 +182,7 @@ c1.get_preferences()
""") """)
print(""" print("""
We try to POST both of them to the server, but the second one fails because of the conflict. Instead, merges the two locally: We try to POST both of them to the server. The second one fails because of the conflict, and we see that its preferences don't change yet.
""") """)
code_block(""" code_block("""
@ -193,9 +192,11 @@ c1.get_preferences()
""") """)
print(""" print("""
Now that the merge is complete, the client can make a second POST request containing the merged wallet. The client that is out of date will then call `get_remote_wallet`, which GETs and automatically merges in the latest wallet. We see the preferences are now merged. Now it can make a second POST request containing the merged wallet.
""") """)
code_block(""" code_block("""
c1.get_remote_wallet()
c1.get_preferences()
c1.update_remote_wallet() c1.update_remote_wallet()
""") """)

View file

@ -58,7 +58,7 @@ class LBRYSDK():
class WalletSync(): class WalletSync():
def __init__(self, local): def __init__(self, local):
self.API_VERSION = 1 self.API_VERSION = 2
if local: if local:
BASE_URL = 'http://localhost:8090' BASE_URL = 'http://localhost:8090'
@ -131,24 +131,16 @@ class WalletSync():
response = requests.post(self.WALLET_URL, body) response = requests.post(self.WALLET_URL, body)
if response.status_code == 200: if response.status_code == 200:
conflict = False
print ('Successfully updated wallet state on server') print ('Successfully updated wallet state on server')
return True
elif response.status_code == 409: elif response.status_code == 409:
conflict = True print ('Submitted wallet is out of date.')
print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.') return False
# Not an error! We still want to merge in the returned wallet.
else: else:
print ('Error', response.status_code) print ('Error', response.status_code)
print (response.content) print (response.content)
raise Exception("Unexpected status code") raise Exception("Unexpected status code")
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac, conflict
def derive_secrets(root_password, salt): def derive_secrets(root_password, salt):
# TODO - Audit me audit me audit me! I don't know if these values are # TODO - Audit me audit me audit me! I don't know if these values are
# optimal. # optimal.
@ -256,31 +248,23 @@ class Client():
# now, the SDK handles merges with timestamps and such so it's as safe as # now, the SDK handles merges with timestamps and such so it's as safe as
# always to just merge in. # always to just merge in.
# TODO - Be careful of cases where get_remote_wallet comes back with "Not # TODO - Save wallet state to disk, and init by pulling from disk. That way,
# Found" even though a wallet is actually on the server. We would start with # we'll know what the merge base is, and we won't have to merge from 0 each
# sequence=0 with the local encrypted wallet. Then next time we # time the app restarts.
# get_remote_wallet, it returns the actual wallet, let's say sequence=5, and
# it would write over what's locally saved. With the SDK as it is using
# sync_apply, that's okay because it has a decent way of merging it. However
# when the Desktop is in charge of merging, this could be a hazard. Saving
# the state to disk could help. Or perhaps pushing first before pulling. Or
# maybe if we're writing over sequence=0, we can know we should be "merging"
# the local encrypted wallet with whatever comes from the server.
# TODO - what if two clients initialize, make changes, and then both push? # TODO - Wrap this back into __init__, now that I got the empty encrypted
# We'll need a way to "merge" from a merge base of an empty wallet. # wallet right.
def init_wallet_state(self): def init_wallet_state(self):
if self.get_remote_wallet() == "Not Found": # Represents what's been synced to the wallet sync server. It starts with
print("No wallet found on the server for this account. Starting a new one.") # sequence=0 which means nothing has been synced yet. As such, we start
# with an empty encrypted_wallet here. Anything currently in the SDK is a
# Represents what's been synced to the wallet sync server. It starts with # local-only change until it's pushed. If there's a merge conflict,
# sequence=0 which means nothing has been synced yet. We start with # sequence=0, empty encrypted_wallet will be the merge base. That way we
# whatever is in the SDK. Whether it's new or not, we (who choose to run # won't lose any changes.
# this method) are assuming it's not synced yet. self.synced_wallet_state = WalletState(
self.synced_wallet_state = WalletState( sequence=0,
sequence=0, encrypted_wallet="",
encrypted_wallet=self.get_local_encrypted_wallet() )
)
def register(self): def register(self):
success = self.wallet_sync_api.register( success = self.wallet_sync_api.register(
@ -308,6 +292,29 @@ class Client():
# we're talking about. Again, we should see how LBRY Desktop/SDK deal with # we're talking about. Again, we should see how LBRY Desktop/SDK deal with
# it. # it.
def get_merged_wallet_state(self, new_wallet_state):
# Eventually, we will look for local changes in
# `get_local_encrypted_wallet()` by comparing it to
# `self.synced_wallet_state.encrypted_wallet`.
#
# If there are no local changes, we can just return `new_wallet_state`.
#
# If there are local changes, we will merge between `new_wallet_state` and
# `get_local_encrypted_wallet()`, using
# `self.synced_wallet_state.encrypted_wallet` as our merge base.
#
# For really hairy cases, this could even be a whole interactive process,
# not just a function.
# For now, the SDK handles merging (in a way that we hope to improve with
# the above eventually) so we will just return `new_wallet_state`.
#
# It would be nice to have a little "we just merged in changes" log output
# if there are local changes, just for demo purpoes. Unfortunately, the SDK
# outputs a different encrypted blob each time we ask it for the encrypted
# wallet, so there's no easy way to check if it actually changed.
return new_wallet_state
# Returns: status # Returns: status
def get_remote_wallet(self): def get_remote_wallet(self):
new_wallet_state, hmac = self.wallet_sync_api.get_wallet(self.auth_token) new_wallet_state, hmac = self.wallet_sync_api.get_wallet(self.auth_token)
@ -327,11 +334,14 @@ class Client():
print ('got:', new_wallet_state) print ('got:', new_wallet_state)
return "Error" return "Error"
self.synced_wallet_state = new_wallet_state merged_wallet_state = self.get_merged_wallet_state(new_wallet_state)
# TODO errors? sequence of events? This isn't gonna be quite right. Look at state diagrams.
self.update_local_encrypted_wallet(new_wallet_state.encrypted_wallet)
print ("Got latest walletState:") # TODO error recovery between these two steps? sequence of events?
# This isn't gonna be quite right. Look at state diagrams.
self.synced_wallet_state = merged_wallet_state
self.update_local_encrypted_wallet(merged_wallet_state.encrypted_wallet)
print ("Got (and maybe merged in) latest walletState:")
pprint(self.synced_wallet_state) pprint(self.synced_wallet_state)
return "Success" return "Success"
@ -351,31 +361,19 @@ class Client():
) )
hmac = create_hmac(submitted_wallet_state, self.hmac_key) hmac = create_hmac(submitted_wallet_state, self.hmac_key)
# Submit our wallet, get the latest wallet back as a response # Submit our wallet.
new_wallet_state, new_hmac, conflict = self.wallet_sync_api.update_wallet(submitted_wallet_state, hmac, self.auth_token) updated = self.wallet_sync_api.update_wallet(submitted_wallet_state, hmac, self.auth_token)
# TODO - there's some code in common here with the get_remote_wallet function. factor it out. if updated:
# We updated it. Now it's synced and we mark it as such.
self.synced_wallet_state = submitted_wallet_state
if not check_hmac(new_wallet_state, self.hmac_key, new_hmac): print ("Synced walletState:")
print ('Error - bad hmac on new wallet') pprint(self.synced_wallet_state)
print (new_wallet_state, hmac) return "Success"
return "Error"
if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state): print ("Could not update. Need to get new wallet and merge")
print ('Error - new wallet does not validate') return "Failure"
print ('current:', self.synced_wallet_state)
print ('got:', new_wallet_state)
return "Error"
# TODO - `concflict` determines whether we need to a smart merge here.
# However for now the SDK handles all of that.
self.synced_wallet_state = new_wallet_state
# TODO errors? sequence of events? This isn't gonna be quite right. Look at state diagrams.
self.update_local_encrypted_wallet(new_wallet_state.encrypted_wallet)
print ("Got new walletState:")
pprint(self.synced_wallet_state)
return "Success"
def set_preference(self, key, value): def set_preference(self, key, value):
# TODO - error checking # TODO - error checking