test client: factor out wallet sync API into class

In preparation for adding the LBRY SDK api class
This commit is contained in:
Daniel Krol 2022-06-10 21:07:55 -04:00
parent 66e5cf7fe7
commit d467559dcd
4 changed files with 159 additions and 107 deletions

View file

@ -103,7 +103,7 @@ func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest
}
if err := json.NewDecoder(req.Body).Decode(&reqStruct); err != nil {
errorJson(w, http.StatusBadRequest, "Malformed request body JSON")
errorJson(w, http.StatusBadRequest, "Request body JSON malformed or structure mismatch")
return false
}

View file

@ -25,9 +25,9 @@ Now that the account exists, grab an auth token with both clients.
```
>>> c1.get_auth_token()
Got auth token: a489d5cacc0a3db4811c34d203683482d90c605b03ae007fa5ae32ef17252bd9
Got auth token: b646a357038d394ef7b70f350c666aeb29e24072c7eeaaad4fb6759c1fca281a
>>> c2.get_auth_token()
Got auth token: 1fe687db8ab493ed260f499b674cfa49edefd3c03a718905c62d3f850dc50567
Got auth token: bd24ae67cb1bde2da8a27f2c2e5ec8ff1f9bebccb11e16dd35aea31bf422133c
```
## Syncing
@ -38,7 +38,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST
```
>>> c1.new_wallet_state()
>>> c1.post_wallet()
>>> c1.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=1, encrypted_wallet='-')
@ -57,7 +57,7 @@ WalletState(sequence=1, encrypted_wallet='-')
Push a new version, GET it with the other client. Even though we haven't edited the encrypted wallet yet, we can still increment the sequence number.
```
>>> c2.post_wallet()
>>> c2.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=2, encrypted_wallet='-')
@ -75,21 +75,21 @@ For demo purposes, this test client represents each change to the wallet by appe
'-'
>>> c1.change_encrypted_wallet()
>>> c1.cur_encrypted_wallet()
'-:cfF6'
'-:a776'
```
The wallet is synced between the clients.
```
>>> c1.post_wallet()
>>> c1.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=3, encrypted_wallet='-:cfF6')
WalletState(sequence=3, encrypted_wallet='-:a776')
>>> c2.get_wallet()
Got latest walletState:
WalletState(sequence=3, encrypted_wallet='-:cfF6')
WalletState(sequence=3, encrypted_wallet='-:a776')
>>> c2.cur_encrypted_wallet()
'-:cfF6'
'-:a776'
```
## Merging Changes
@ -100,44 +100,44 @@ Both clients create changes. They now have diverging wallets.
>>> c1.change_encrypted_wallet()
>>> c2.change_encrypted_wallet()
>>> c1.cur_encrypted_wallet()
'-:cfF6:565b'
'-:a776:8fc8'
>>> c2.cur_encrypted_wallet()
'-:cfF6:6De1'
'-:a776:2433'
```
One client POSTs its change first.
```
>>> c1.post_wallet()
>>> c1.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=4, encrypted_wallet='-:cfF6:565b')
WalletState(sequence=4, encrypted_wallet='-:a776:8fc8')
```
The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally.
The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:cfF6"`, the first part of the wallet which both clients had in common before the merge.
The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:a776"`, the first part of the wallet which both clients had in common before the merge.
```
>>> c2.get_wallet()
Got latest walletState:
WalletState(sequence=4, encrypted_wallet='-:cfF6:565b')
WalletState(sequence=4, encrypted_wallet='-:a776:8fc8')
>>> c2.cur_encrypted_wallet()
'-:cfF6:565b:6De1'
'-:a776:8fc8:2433'
```
Finally, the client with the merged wallet pushes it to the server, and the other client GETs the update.
```
>>> c2.post_wallet()
>>> c2.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1')
WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433')
>>> c1.get_wallet()
Got latest walletState:
WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1')
WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433')
>>> c1.cur_encrypted_wallet()
'-:cfF6:565b:6De1'
'-:a776:8fc8:2433'
```
## Conflicts
@ -148,22 +148,22 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang
```
>>> c2.change_encrypted_wallet()
>>> c2.post_wallet()
>>> c2.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a')
WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD')
>>> c1.change_encrypted_wallet()
>>> c1.post_wallet()
>>> c1.update_wallet()
Wallet state out of date. Getting updated wallet state. Try posting again after this.
Got new walletState:
WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a')
WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD')
```
Now the merge is complete, and the client can make a second POST request containing the merged wallet.
```
>>> c1.post_wallet()
>>> c1.update_wallet()
Successfully updated wallet state on server
Got new walletState:
WalletState(sequence=7, encrypted_wallet='-:cfF6:565b:6De1:053a:6774')
WalletState(sequence=7, encrypted_wallet='-:a776:8fc8:2433:0FdD:BA43')
```

View file

@ -59,7 +59,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST
code_block("""
c1.new_wallet_state()
c1.post_wallet()
c1.update_wallet()
""")
print("""
@ -77,7 +77,7 @@ Push a new version, GET it with the other client. Even though we haven't edited
""")
code_block("""
c2.post_wallet()
c2.update_wallet()
c1.get_wallet()
""")
@ -98,7 +98,7 @@ The wallet is synced between the clients.
""")
code_block("""
c1.post_wallet()
c1.update_wallet()
c2.get_wallet()
c2.cur_encrypted_wallet()
""")
@ -123,7 +123,7 @@ One client POSTs its change first.
""")
code_block("""
c1.post_wallet()
c1.update_wallet()
""")
print("""
@ -142,7 +142,7 @@ Finally, the client with the merged wallet pushes it to the server, and the othe
""")
code_block("""
c2.post_wallet()
c2.update_wallet()
c1.get_wallet()
c1.cur_encrypted_wallet()
""")
@ -157,9 +157,9 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang
code_block("""
c2.change_encrypted_wallet()
c2.post_wallet()
c2.update_wallet()
c1.change_encrypted_wallet()
c1.post_wallet()
c1.update_wallet()
""")
print("""
@ -167,5 +167,5 @@ Now the merge is complete, and the client can make a second POST request contain
""")
code_block("""
c1.post_wallet()
c1.update_wallet()
""")

View file

@ -5,18 +5,101 @@ from pprint import pprint
CURRENT_VERSION = 1
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
class WalletSync():
BASE_URL = 'http://localhost:8090'
AUTH_URL = BASE_URL + '/auth/full'
REGISTER_URL = BASE_URL + '/signup'
WALLET_URL = BASE_URL + '/wallet'
@classmethod
def register(cls, email, password):
body = json.dumps({
'email': email,
'password': password,
})
response = requests.post(cls.REGISTER_URL, body)
if response.status_code != 201:
print ('Error', response.status_code)
print (response.content)
return False
return True
@classmethod
def get_auth_token(cls, email, password, device_id):
body = json.dumps({
'email': email,
'password': password,
'deviceId': device_id,
})
response = requests.post(cls.AUTH_URL, body)
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return None
return response.json()['token']
@classmethod
def get_wallet(cls, token):
params = {
'token': token,
}
response = requests.get(cls.WALLET_URL, params=params)
# TODO check response version on client side now
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return None, None
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac
@classmethod
def update_wallet(cls, wallet_state, hmac, token):
body = json.dumps({
'version': CURRENT_VERSION,
'token': token,
"encryptedWallet": wallet_state.encrypted_wallet,
"sequence": wallet_state.sequence,
"hmac": hmac,
})
response = requests.post(cls.WALLET_URL, body)
# TODO check that response.json().version == CURRENT_VERSION
if response.status_code == 200:
conflict = False
print ('Successfully updated wallet state on server')
elif response.status_code == 409:
conflict = True
print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.')
# Not an error! We still want to merge in the returned wallet.
else:
print ('Error', response.status_code)
print (response.content)
return None, None, None
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac, conflict
# TODO - We should have:
# * self.last_synced_wallet_state - as described
# * self.current_wallet_state - WalletState(cur_encrypted_wallet(), sequence + 1) - and current_wallet_state
# We don't need it yet but we'd be avoiding the entire point of the syncing system. At least keep it around in this demo.
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
# TODO - do this correctly. This is a hack example.
def derive_login_password(root_password):
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:10]
@ -88,29 +171,22 @@ class Client():
self.root_password = root_password
def register(self):
body = json.dumps({
'email': self.email,
'password': derive_login_password(self.root_password),
})
response = requests.post(REGISTER_URL, body)
if response.status_code != 201:
print ('Error', response.status_code)
print (response.content)
return
success = WalletSync.register(
self.email,
derive_login_password(self.root_password),
)
if success:
print ("Registered")
def get_auth_token(self):
body = json.dumps({
'email': self.email,
'password': derive_login_password(self.root_password),
'deviceId': self.device_id,
})
response = requests.post(AUTH_URL, body)
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
token = WalletSync.get_auth_token(
self.email,
derive_login_password(self.root_password),
self.device_id,
)
if not token:
return
self.auth_token = json.loads(response.content)['token']
self.auth_token = token
print ("Got auth token: ", self.auth_token)
# TODO - What about cases where we are managing multiple different wallets?
@ -119,31 +195,22 @@ class Client():
# want to annotate them with which account we're talking about. Again, we
# should see how LBRY Desktop/SDK deal with it.
def get_wallet(self):
params = {
'token': self.auth_token,
}
response = requests.get(WALLET_URL, params=params)
if response.status_code != 200:
# TODO check response version on client side now
print ('Error', response.status_code)
print (response.content)
new_wallet_state, hmac = WalletSync.get_wallet(self.auth_token)
# If there was a failure
if not new_wallet_state:
return
hmac_key = derive_hmac_key(self.root_password)
new_wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
if not check_hmac(new_wallet_state, hmac_key, hmac):
print ('Error - bad hmac on new wallet')
print (response.content)
print (new_wallet_state, hmac)
return
if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate')
print (response.content)
print ('current:', self.wallet_state)
print ('got:', new_wallet_state)
return
if self.wallet_state is None:
@ -156,7 +223,7 @@ class Client():
print ("Got latest walletState:")
pprint(self.wallet_state)
def post_wallet(self):
def update_wallet(self):
# Create a *new* wallet state, indicating that it was last updated by this
# device, with the updated sequence, and include our local encrypted wallet changes.
# Don't set self.wallet_state to this until we know that it's accepted by
@ -171,45 +238,30 @@ class Client():
encrypted_wallet=self.cur_encrypted_wallet(),
sequence=self.wallet_state.sequence + 1
)
wallet_request = {
'version': CURRENT_VERSION,
'token': self.auth_token,
"encryptedWallet": submitted_wallet_state.encrypted_wallet,
"sequence": submitted_wallet_state.sequence,
"hmac": create_hmac(submitted_wallet_state, hmac_key),
}
hmac = create_hmac(submitted_wallet_state, hmac_key)
response = requests.post(WALLET_URL, json.dumps(wallet_request))
# Submit our wallet, get the latest wallet back as a response
new_wallet_state, new_hmac, conflict = WalletSync.update_wallet(submitted_wallet_state, hmac, self.auth_token)
if response.status_code == 200:
# TODO check response version on client side now
# Our local changes are no longer local, so we reset them
self._encrypted_wallet_local_changes = ''
print ('Successfully updated wallet state on server')
elif response.status_code == 409:
print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.')
# Don't return yet! We got the updated state here, so we still process it below.
else:
print ('Error', response.status_code)
print (response.content)
# If there was a failure (not just a conflict)
if not new_wallet_state:
return
# Now we get a new wallet back as a response
# TODO - factor this code into the same thing as the get_wallet function
# If there's not a conflict, we submitted successfully and should reset our previously local changes
if not conflict:
self._encrypted_wallet_local_changes = ''
new_wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
if not check_hmac(new_wallet_state, hmac_key, hmac):
# TODO - there's some code in common here with the get_wallet function. factor it out.
if not check_hmac(new_wallet_state, hmac_key, new_hmac):
print ('Error - bad hmac on new wallet')
print (response.content)
print (new_wallet_state, hmac)
return
if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate')
print (response.content)
print ('current:', self.wallet_state)
print ('got:', new_wallet_state)
return
self.wallet_state = new_wallet_state