diff --git a/examples/vasp/wallet.py b/examples/vasp/wallet.py index 5fbdb8df..3dad995d 100644 --- a/examples/vasp/wallet.py +++ b/examples/vasp/wallet.py @@ -107,11 +107,14 @@ def pay( """make payment from given user account to intent_id""" intent = identifier.decode_intent(intent_id, self.hrp) + if not intent.amount or not intent.currency_code: + raise ValueError("should provide currency_code and amount") + command = offchain.PaymentCommand.init( self.gen_user_account_id(user_name), self.users[user_name].kyc_data(), intent.account_id, - intent.amount, + intent.amount, # pyre-ignore intent.currency_code, original_payment_reference_id=original_payment_reference_id, description=desc, diff --git a/src/diem/identifier/__init__.py b/src/diem/identifier/__init__.py index 8cf21849..6dcf5dd9 100644 --- a/src/diem/identifier/__init__.py +++ b/src/diem/identifier/__init__.py @@ -40,15 +40,15 @@ class Intent: account_address: diem_types.AccountAddress sub_address: typing.Optional[bytes] - currency_code: str - amount: int + currency_code: typing.Optional[str] + amount: typing.Optional[int] def __init__( self, account_address: diem_types.AccountAddress, sub_address: typing.Optional[bytes], - currency_code: str, - amount: int, + currency_code: typing.Optional[str], + amount: typing.Optional[int], hrp: str, ) -> None: self.account_address = account_address @@ -66,13 +66,22 @@ def account_id(self) -> str: return encode_account(self.account_address, self.sub_address, self.hrp) -def encode_intent(encoded_account_identifier: str, currency_code: str, amount: int) -> str: +def encode_intent( + encoded_account_identifier: str, currency_code: typing.Optional[str] = None, amount: typing.Optional[int] = None +) -> str: """ Encode account identifier string(encoded), currency code and amount into Diem intent identifier (https://dip.diem.com/dip-5/) """ - return "diem://%s?c=%s&am=%d" % (encoded_account_identifier, currency_code, amount) + params = [] + if currency_code: + params.append("c=%s" % currency_code) + if amount is not None and amount > 0: + params.append("am=%s" % amount) + if params: + return "diem://%s?%s" % (encoded_account_identifier, "&".join(params)) + return "diem://%s" % encoded_account_identifier def decode_intent(encoded_intent_identifier: str, hrp: str) -> Intent: @@ -113,7 +122,7 @@ def decode_intent(encoded_intent_identifier: str, hrp: str) -> Intent: def _decode_param(name, params, field, convert): # pyre-ignore if field not in params: - raise InvalidIntentIdentifierError(f"Can't decode {name}: not found in params {params}") + return None if not isinstance(params[field], list): raise InvalidIntentIdentifierError(f"Can't decode {name}: unknown type {params}") diff --git a/tests/test_identifier.py b/tests/test_identifier.py index c619c8bc..6312e99d 100644 --- a/tests/test_identifier.py +++ b/tests/test_identifier.py @@ -154,6 +154,22 @@ def test_intent_identifier(hrp_addresses): assert account_id == intent.account_id +def test_intent_identifier_without_params(hrp_addresses): + hrp, enocded_addr_with_none_subaddr, enocded_addr_with_subaddr = hrp_addresses + account_id = identifier.encode_account(test_onchain_address, None, hrp) + intent_id = identifier.encode_intent(account_id) + assert intent_id == "diem://%s" % enocded_addr_with_none_subaddr + + intent = identifier.decode_intent(intent_id, hrp) + assert intent.account_address == utils.account_address(test_onchain_address) + assert intent.account_address_bytes.hex() == test_onchain_address + assert intent.sub_address is None + assert intent.currency_code is None + assert intent.amount is None + + assert account_id == intent.account_id + + def test_intent_identifier_with_sub_address(hrp_addresses): hrp, enocded_addr_with_none_subaddr, enocded_addr_with_subaddr = hrp_addresses account_id = identifier.encode_account(test_onchain_address, test_sub_address, hrp) @@ -167,28 +183,30 @@ def test_intent_identifier_with_sub_address(hrp_addresses): assert intent.amount == 123 +def test_intent_identifier_with_one_param(): + account_id = "dm1p7ujcndcl7nudzwt8fglhx6wxnvqqqqqqqqqqqqqd8p9cq" + intent_id = identifier.encode_intent(account_id, currency_code="XUS") + assert intent_id == "diem://%s?c=%s" % (account_id, "XUS") + intent_id = identifier.encode_intent(account_id, currency_code="") + assert intent_id == "diem://%s" % account_id + intent_id = identifier.encode_intent(account_id, amount=122) + assert intent_id == "diem://%s?am=%s" % (account_id, 122) + intent_id = identifier.encode_intent(account_id, amount=0) + assert intent_id == "diem://%s" % account_id + intent_id = identifier.encode_intent(account_id, amount=-1) + assert intent_id == "diem://%s" % account_id + + def test_intent_identifier_decode_errors(hrp_addresses): hrp, enocded_addr_with_none_subaddr, enocded_addr_with_subaddr = hrp_addresses # amount is not int with pytest.raises(identifier.InvalidIntentIdentifierError): identifier.decode_intent("diem://%s?c=XUS&am=str" % (enocded_addr_with_none_subaddr), hrp) - # amount not exist - with pytest.raises(identifier.InvalidIntentIdentifierError): - identifier.decode_intent("diem://%s?c=XUS" % (enocded_addr_with_none_subaddr), hrp) - # too many amount with pytest.raises(identifier.InvalidIntentIdentifierError): identifier.decode_intent("diem://%s?c=XUS&am=2&am=3" % (enocded_addr_with_none_subaddr), hrp) - # amount is none - with pytest.raises(identifier.InvalidIntentIdentifierError): - identifier.decode_intent("diem://%s?c=XUS&am=" % (enocded_addr_with_none_subaddr), hrp) - - # currency code not exist - with pytest.raises(identifier.InvalidIntentIdentifierError): - identifier.decode_intent("diem://%s?am=2" % (enocded_addr_with_none_subaddr), hrp) - # scheme not match with pytest.raises(identifier.InvalidIntentIdentifierError): identifier.decode_intent("hello://%s?am=2&c=XUS" % (enocded_addr_with_none_subaddr), hrp)