Skip to content

Commit

Permalink
MiniWallet MVP (#227)
Browse files Browse the repository at this point in the history
MiniWallet MVP:
1. Implemented all APIs defined in spec https://github.com/diem/client-sdks/blob/master/specs/mini_wallet.md
2. Created cli for start a miniwallet service
3. Created initial testsuite for testing a MiniWallet API.
4. MiniWallet API openapi spec
5. Refund is not handled yet, will be follow up PRs.
  • Loading branch information
Xiao Li authored Mar 2, 2021
1 parent 89c667b commit 5c5bcb7
Show file tree
Hide file tree
Showing 40 changed files with 2,467 additions and 50 deletions.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
# SPDX-License-Identifier: Apache-2.0

recursive-include src *.py
recursive-include src *.yaml
recursive-include src *.html
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ black:

lint:
./venv/bin/pylama src tests examples
./venv/bin/pyre --search-path venv/lib/python3.9/site-packages check
./venv/bin/pyre --search-path venv/lib/python3.7/site-packages --search-path venv/lib/python3.8/site-packages --search-path venv/lib/python3.9/site-packages check

format:
./venv/bin/python -m black src tests examples

test: format lint runtest
test: format runtest

runtest:
./venv/bin/pytest tests/test_* examples/* -k "$(t)" $(args)
DMW_SELF_CHECK=Y ./venv/bin/pytest src/diem/testing/suites tests examples -k "$(t)" $(args)

profile:
./venv/bin/python -m profile -m pytest tests examples -k "$(t)" $(args)

cover:
./venv/bin/pytest --cov-report html --cov=src tests/test_* examples/*
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Example curl to hit the server (should get an error response):
curl -X POST -H "X-REQUEST-ID: 3185027f-0574-6f55-2668-3a38fdb5de98" -H "X-REQUEST-SENDER-ADDRESS: tdm1pacrzjajt6vuamzkswyd50e28pg77m6wylnc3spg3xj7r6" -d "invalid-jws-body" http://localhost:8080/v2/command
```

## MiniWallet and MiniWallet Test Suite

See [mini_wallet.md](mini-wallet.md)


## Build & Test

```
Expand Down
15 changes: 15 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) The Diem Core Contributors
# SPDX-License-Identifier: Apache-2.0

from diem import chain_ids, testnet
import pytest, os


@pytest.fixture(scope="session", autouse=True)
def setup_testnet() -> None:
if os.getenv("dt"):
os.system("make docker")
print("swap testnet default values to local testnet launched by docker-compose")
testnet.JSON_RPC_URL = "http://localhost:8080/v1"
testnet.FAUCET_URL = "http://localhost:8000/mint"
testnet.CHAIN_ID = chain_ids.TESTING
115 changes: 115 additions & 0 deletions mini-wallet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
## Try MiniWallet

Install Diem python sdk with MiniWallet and MiniWallet Test Suite:
```
pip install diem[all]
```

`dmw` cli will be installed. You can check it out by:
```
dmw --help
```

Start a MiniWallet server, connects to Diem testnet by default
```
dmw start-server
```

Open http://localhost:8888 to check MiniWallet API specification document (Defined by OpenAPI Specification 3.0.3).
The document includes simple examples to try out the API.

`start-server` options:

```
dmw start-server --help
Usage: dmw start-server [OPTIONS]
Options:
-n, --name TEXT Application name. [default: mini-wallet]
-h, --host TEXT Start server host. [default: localhost]
-p, --port INTEGER Start server port. [default: 8888]
-j, --jsonrpc TEXT Diem fullnode JSON-RPC URL. [default:
http://testnet.diem.com/v1]
-f, --faucet TEXT Testnet faucet URL. [default:
http://testnet.diem.com/mint]
-l, --logfile TEXT Log to a file instead of printing into
console.
-o, --enable-debug-api BOOLEAN Enable debug API. [default: True]
--help Show this message and exit.
```


## MiniWallet Test Suite

Try out MiniWallet Test Suite by hitting the target server we started by `dmw start-server`
```
dmw test --target http://localhost:8888
```
You should see something like this:

```
> dmw test
Diem JSON-RPC URL: http://testnet.diem.com/v1
Diem Testnet Faucet URL: http://testnet.diem.com/mint
======================================== test session starts ===============================================
…...
collected 61 items
src/diem/testing/suites/test_miniwallet_api.py ......................ss. [ 40%]
src/diem/testing/suites/test_payment.py .................................... [100%]
============================== 59 passed, 2 skipped, 198 deselected in 18.26s ===============================
```

Follow MiniWallet API specification to create a proxy server for your wallet application.
Assume you run your application at port 9999, run MiniWallet Test Suite:
```
dmw test --target http://localhost:9999
```

`test` options:
```
dmw test --help
Usage: dmw test [OPTIONS]
Options:
-t, --target TEXT Target mini-wallet application URL. [default:
http://localhost:8888]
-j, --jsonrpc TEXT Diem fullnode JSON-RPC URL. [default:
http://testnet.diem.com/v1]
-f, --faucet TEXT Testnet faucet URL. [default:
http://testnet.diem.com/mint]
--pytest-args TEXT Additional pytest arguments, split by empty
space, e.g. `--pytest-args '-v -s'`.
-d, --test-debug-api BOOLEAN Run tests for debug APIs. [default: False]
-v, --verbose BOOLEAN Enable verbose log output. [default: False]
--help Show this message and exit.
```

### How it works

1. Test suite is located at diem.testing.suites package, including a pytest conftest.py.
2. `dmw test` will launch a pytest runner to run tests.
3. The conftest.py will start a stub MiniWallet as counterparty service for testing payment with the target server specified by the `--target` option.


### Work with local testnet

1. [Download](https://docs.docker.com/get-docker/) and install Docker and Docker Compose (comes with Docker for Mac and Windows).
2. Download Diem testnet docker compose config: https://github.com/diem/diem/blob/master/docker/compose/validator-testnet/docker-compose.yaml
3. Run `docker-compose -f <validator-testnet/docker-compose.yaml file path> up --detach`
* Faucet will be available at http://127.0.0.1:8000
* JSON-RPC will be available at http://127.0.0.1:8080
4. Test your application with local testnet: `dmw test --jsonrpc http://127.0.0.1:8080 --faucet http://127.0.0.1:8000 --target http://localhost:9999`

### Test Off-chain API

As the test counterparty wallet application server is started at local, you need make sure your wallet application's off-chain API can access the stub server by it's base_url: `http://localhost:<port>`.
If your wallet application is not running local, you will need to make sure setup tunnel for your wallet application to access the stub server.
6 changes: 5 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ requests==2.20.0
cryptography==3.3.2
numpy==1.18
protobuf==3.12.4
pytest
pytest==6.2.1
click==7.1
pylama
black
pyre-check==0.0.58
pytest-cov
mypy-protobuf
pdoc3
falcon
waitress
pygount
15 changes: 10 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@
long_description_content_type='text/markdown',
license="Apache-2.0",
url="https://github.com/diem/client-sdk-python",
# author="Diem Open Source",
author="The Diem Core Contributors",
author_email="[email protected]",
py_modules=["diem.testing.cli.click"],
entry_points='''
[console_scripts]
dmw=diem.testing.cli.click:main
''',
python_requires=">=3.7", # requires dataclasses
packages=["diem"],
# package_data={"": ["src/diem/jsonrpc/*.pyi"]},
package_dir={"": "src"},
include_package_data=True, # see MANIFEST.in
zip_safe=True,
install_requires=["requests>=2.20.0", "cryptography>=2.8", "numpy>=1.18", "protobuf>=3.12.4"],
setup_requires=[
# Setuptools 18.0 properly handles Cython extensions.
"setuptools>=18.0",
],
extras_require={
"all": ["falcon>=2.0.0", "waitress>=1.4.4", "pytest>=6.2.1", "click>=7.1"]
},
classifiers=[
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
Expand Down
4 changes: 4 additions & 0 deletions src/diem/identifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ def __init__(
self.amount = amount
self.hrp = hrp

@property
def subaddress(self) -> typing.Optional[bytes]:
return self.sub_address

@property
def account_address_bytes(self) -> bytes:
return self.account_address.to_bytes()
Expand Down
5 changes: 4 additions & 1 deletion src/diem/local_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .auth_key import AuthKey
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from typing import Dict, Optional
from typing import Dict, Optional, Tuple
from dataclasses import dataclass, field
from copy import copy
import time
Expand Down Expand Up @@ -90,6 +90,9 @@ def compliance_public_key_bytes(self) -> bytes:
def account_identifier(self, subaddress: Optional[bytes] = None) -> str:
return identifier.encode_account(self.account_address, subaddress, self.hrp)

def decode_account_identifier(self, encoded_id: str) -> Tuple[diem_types.AccountAddress, Optional[bytes]]:
return identifier.decode_account(encoded_id, self.hrp)

def sign(self, txn: diem_types.RawTransaction) -> diem_types.SignedTransaction:
"""Create signed transaction for given raw transaction"""

Expand Down
62 changes: 39 additions & 23 deletions src/diem/offchain/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ def __init__(self, resp: CommandResponseObject) -> None:
self.resp = resp


class InvalidCurrencyCodeError(ValueError):
pass


class UnsupportedCurrencyCodeError(ValueError):
pass


@dataclasses.dataclass
class Client:
"""Client for communicating with offchain service.
Expand Down Expand Up @@ -160,7 +168,7 @@ def process_inbound_request(self, request_sender_address: str, request_bytes: by
self.validate_addresses(payment, request_sender_address)
cmd = self.create_inbound_payment_command(request.cid, payment)
if cmd.is_initial():
self.validate_dual_attestation_limit(cmd.payment.action)
self.validate_dual_attestation_limit_by_action(cmd.payment.action)
elif cmd.is_rsend():
self.validate_recipient_signature(cmd, public_key)
return cmd
Expand All @@ -182,34 +190,42 @@ def validate_recipient_signature(self, cmd: PaymentCommand, public_key: Ed25519P
ErrorCode.invalid_recipient_signature, str(e), "command.payment.recipient_signature"
) from e

def validate_dual_attestation_limit(self, action: PaymentActionObject) -> None:
def validate_dual_attestation_limit_by_action(self, action: PaymentActionObject) -> None:
msg = self.is_under_dual_attestation_limit(action.currency, action.amount)
if msg:
raise command_error(ErrorCode.no_kyc_needed, msg, "command.payment.action.amount")

def is_under_dual_attestation_limit(self, currency: str, amount: int) -> typing.Optional[str]:
currencies = self.jsonrpc_client.get_currencies()
currency_codes = list(map(lambda c: c.code, currencies))
supported_codes = _filter_supported_currency_codes(self.supported_currency_codes, currency_codes)
if action.currency not in currency_codes:
raise command_error(
ErrorCode.invalid_field_value,
f"currency code is invalid: {action.currency}",
"command.payment.action.currency",
)
try:
self.validate_currency_code(currency, currencies)
except InvalidCurrencyCodeError as e:
raise command_error(ErrorCode.invalid_field_value, str(e), "command.payment.action.currency")
except UnsupportedCurrencyCodeError as e:
raise command_error(ErrorCode.unsupported_currency, str(e), "command.payment.action.currency")

if action.currency not in supported_codes:
raise command_error(
ErrorCode.unsupported_currency,
f"currency code is not supported: {action.currency}",
"command.payment.action.currency",
)
limit = self.jsonrpc_client.get_metadata().dual_attestation_limit
for info in currencies:
if info.code == action.currency:
if _is_under_the_threshold(limit, info.to_xdx_exchange_rate, action.amount):
raise command_error(
ErrorCode.no_kyc_needed,
"payment amount is %s (rate: %s) under travel rule threshold %s"
% (action.amount, info.to_xdx_exchange_rate, limit),
"command.payment.action.amount",
if info.code == currency:
if _is_under_the_threshold(limit, info.to_xdx_exchange_rate, amount):
return "payment amount is %s (rate: %s) under travel rule threshold %s" % (
amount,
info.to_xdx_exchange_rate,
limit,
)

def validate_currency_code(
self, currency: str, currencies: typing.Optional[typing.List[jsonrpc.CurrencyInfo]] = None
) -> None:
if currencies is None:
currencies = self.jsonrpc_client.get_currencies()
currency_codes = list(map(lambda c: c.code, currencies))
supported_codes = _filter_supported_currency_codes(self.supported_currency_codes, currency_codes)
if currency not in currency_codes:
raise InvalidCurrencyCodeError(f"currency code is invalid: {currency}")
if currency not in supported_codes:
raise UnsupportedCurrencyCodeError(f"currency code is not supported: {currency}")

def validate_addresses(self, payment: PaymentObject, request_sender_address: str) -> None:
self.validate_actor_address("sender", payment.sender)
self.validate_actor_address("receiver", payment.receiver)
Expand Down
3 changes: 3 additions & 0 deletions src/diem/offchain/payment_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ def new_request(self) -> CommandRequestObject:

# the followings are PaymentCommand specific methods

def my_subaddress(self, hrp: str) -> typing.Optional[bytes]:
return identifier.decode_account_subaddress(self.my_actor_address, hrp)

def validate_state_trigger_actor(self) -> None:
if self.inbound and self.opponent_actor() != self.state_trigger_actor():
raise command_error(
Expand Down
2 changes: 2 additions & 0 deletions src/diem/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) The Diem Core Contributors
# SPDX-License-Identifier: Apache-2.0
2 changes: 2 additions & 0 deletions src/diem/testing/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) The Diem Core Contributors
# SPDX-License-Identifier: Apache-2.0
Loading

0 comments on commit 5c5bcb7

Please sign in to comment.