test client: factor out wallet sync API into class
In preparation for adding the LBRY SDK api class
This commit is contained in:
parent
66e5cf7fe7
commit
d467559dcd
4 changed files with 159 additions and 107 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
```
|
||||
|
|
|
@ -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()
|
||||
""")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue