Module diem.identifier

DIP-5 Diem Account Identifier and Intent Identifier Utilities.

See https://dip.diem.com/dip-5 for more details

Expand source code
# Copyright (c) The Diem Core Contributors
# SPDX-License-Identifier: Apache-2.0

"""DIP-5 Diem Account Identifier and Intent Identifier Utilities.

See https://dip.diem.com/dip-5 for more details

"""


import typing
from urllib import parse
from typing import List

from . import bech32
from .. import diem_types, utils, chain_ids

from .bech32 import bech32_address_encode, bech32_address_decode, Bech32Error, _DIEM_BECH32_SIZE
from .subaddress import DIEM_SUBADDRESS_SIZE, DIEM_ZERO_SUBADDRESS, gen_subaddress

DM = "dm"  # mainnet
TDM = "tdm"  # testnet
PDM = "pdm"  # premainnet
DDM = "ddm"  # dry-run mainnet

HRPS: typing.Dict[int, str] = {
    chain_ids.MAINNET.to_int(): DM,
    chain_ids.TESTNET.to_int(): TDM,
    chain_ids.DEVNET.to_int(): TDM,
    chain_ids.TESTING.to_int(): TDM,
}


class InvalidIntentIdentifierError(Exception):
    pass


class Intent:
    """Intent is a struct hold data decoded from Diem Intent Identifier string"""

    account_address: diem_types.AccountAddress
    sub_address: typing.Optional[bytes]
    currency_code: typing.Optional[str]
    amount: typing.Optional[int]

    def __init__(
        self,
        account_address: diem_types.AccountAddress,
        sub_address: typing.Optional[bytes],
        currency_code: typing.Optional[str],
        amount: typing.Optional[int],
        hrp: str,
    ) -> None:
        self.account_address = account_address
        self.sub_address = sub_address
        self.currency_code = currency_code
        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()

    @property
    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: 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/)
    """

    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:
    """
    Decode Diem intent identifier (https://dip.diem.com/dip-5/) int 3 parts:
    1. account identifier: account address & sub-address
    2. currency code
    3. amount

    InvalidIntentIdentifierError is raised if given identifier is invalid
    """

    result = parse.urlparse(encoded_intent_identifier)
    if result.scheme != "diem":
        raise InvalidIntentIdentifierError(
            f"Unknown intent identifier scheme {result.scheme} " f"in {encoded_intent_identifier}"
        )

    account_identifier = result.netloc
    params = parse.parse_qs(result.query)

    amount = _decode_param("amount", params, "am", lambda am: int(am))
    currency_code = _decode_param("currency code", params, "c", lambda c: str(c))

    try:
        account_address, sub_address = decode_account(account_identifier, hrp)
    except ValueError as e:
        raise InvalidIntentIdentifierError(f"decode account identifier failed: {e}:")

    return Intent(
        account_address=utils.account_address(account_address),
        sub_address=sub_address,
        currency_code=currency_code,
        amount=amount,
        hrp=hrp,
    )


def _decode_param(name, params, field, convert):  # pyre-ignore
    if field not in params:
        return None

    if not isinstance(params[field], list):
        raise InvalidIntentIdentifierError(f"Can't decode {name}: unknown type {params}")

    if len(params[field]) != 1:
        raise InvalidIntentIdentifierError(f"Can't decode {name}: too many values {params}")

    value = params[field][0]
    try:
        return convert(value)
    except ValueError as e:
        raise InvalidIntentIdentifierError(f"Can't decode {name}: {value}, error: {e}")


def encode_account(
    onchain_addr: typing.Union[diem_types.AccountAddress, str],
    subaddr: typing.Union[str, bytes, None],
    hrp: str,
) -> str:
    """Encode onchain address and (optional) subaddress with human readable prefix(hrp) into bech32 format"""

    onchain_address_bytes = utils.account_address_bytes(onchain_addr)
    subaddress_bytes = utils.sub_address(subaddr) if subaddr else None

    try:
        encoded_address = bech32_address_encode(hrp, onchain_address_bytes, subaddress_bytes)
    except Bech32Error as e:
        raise ValueError(
            f"Can't encode from "
            f"onchain_addr: {onchain_addr}, "
            f"subaddr: {subaddr}, "
            f"hrp: {hrp}, got error: {e}"
        )
    return encoded_address


def decode_account(encoded_address: str, hrp: str) -> typing.Tuple[diem_types.AccountAddress, typing.Optional[bytes]]:
    """Return (addrees_str, subaddress_str) given a bech32 encoded str & human readable prefix(hrp)"""
    try:
        (_version, onchain_address_bytes, subaddress_bytes) = bech32.bech32_address_decode(hrp, encoded_address)
    except Bech32Error as e:
        raise ValueError(f"Can't decode from encoded str {encoded_address}, " f"got error: {e}")

    address = utils.account_address(onchain_address_bytes)
    # If subaddress is absent, subaddress_bytes is a list of 0
    if subaddress_bytes != DIEM_ZERO_SUBADDRESS:
        return (address, subaddress_bytes)
    return (address, None)


def decode_hrp(encoded_address: str) -> str:
    if len(encoded_address) not in _DIEM_BECH32_SIZE:
        raise ValueError("Invalid account identifier address size: {encoded_address}")
    if encoded_address[:2] == DM:
        return DM
    return encoded_address[:3]


def decode_account_address(encoded_address: str, hrp: str) -> diem_types.AccountAddress:
    address, _ = decode_account(encoded_address, hrp)
    return address


def decode_account_subaddress(encoded_address: str, hrp: str) -> typing.Optional[bytes]:
    _, subaddress = decode_account(encoded_address, hrp)
    return subaddress

Sub-modules

diem.identifier.bech32
diem.identifier.subaddress

Functions

def decode_account(encoded_address: str, hrp: str) ‑> Tuple[AccountAddress, Optional[bytes]]

Return (addrees_str, subaddress_str) given a bech32 encoded str & human readable prefix(hrp)

Expand source code
def decode_account(encoded_address: str, hrp: str) -> typing.Tuple[diem_types.AccountAddress, typing.Optional[bytes]]:
    """Return (addrees_str, subaddress_str) given a bech32 encoded str & human readable prefix(hrp)"""
    try:
        (_version, onchain_address_bytes, subaddress_bytes) = bech32.bech32_address_decode(hrp, encoded_address)
    except Bech32Error as e:
        raise ValueError(f"Can't decode from encoded str {encoded_address}, " f"got error: {e}")

    address = utils.account_address(onchain_address_bytes)
    # If subaddress is absent, subaddress_bytes is a list of 0
    if subaddress_bytes != DIEM_ZERO_SUBADDRESS:
        return (address, subaddress_bytes)
    return (address, None)
def decode_account_address(encoded_address: str, hrp: str) ‑> AccountAddress
Expand source code
def decode_account_address(encoded_address: str, hrp: str) -> diem_types.AccountAddress:
    address, _ = decode_account(encoded_address, hrp)
    return address
def decode_account_subaddress(encoded_address: str, hrp: str) ‑> Optional[bytes]
Expand source code
def decode_account_subaddress(encoded_address: str, hrp: str) -> typing.Optional[bytes]:
    _, subaddress = decode_account(encoded_address, hrp)
    return subaddress
def decode_hrp(encoded_address: str) ‑> str
Expand source code
def decode_hrp(encoded_address: str) -> str:
    if len(encoded_address) not in _DIEM_BECH32_SIZE:
        raise ValueError("Invalid account identifier address size: {encoded_address}")
    if encoded_address[:2] == DM:
        return DM
    return encoded_address[:3]
def decode_intent(encoded_intent_identifier: str, hrp: str) ‑> Intent

Decode Diem intent identifier (https://dip.diem.com/dip-5/) int 3 parts: 1. account identifier: account address & sub-address 2. currency code 3. amount

InvalidIntentIdentifierError is raised if given identifier is invalid

Expand source code
def decode_intent(encoded_intent_identifier: str, hrp: str) -> Intent:
    """
    Decode Diem intent identifier (https://dip.diem.com/dip-5/) int 3 parts:
    1. account identifier: account address & sub-address
    2. currency code
    3. amount

    InvalidIntentIdentifierError is raised if given identifier is invalid
    """

    result = parse.urlparse(encoded_intent_identifier)
    if result.scheme != "diem":
        raise InvalidIntentIdentifierError(
            f"Unknown intent identifier scheme {result.scheme} " f"in {encoded_intent_identifier}"
        )

    account_identifier = result.netloc
    params = parse.parse_qs(result.query)

    amount = _decode_param("amount", params, "am", lambda am: int(am))
    currency_code = _decode_param("currency code", params, "c", lambda c: str(c))

    try:
        account_address, sub_address = decode_account(account_identifier, hrp)
    except ValueError as e:
        raise InvalidIntentIdentifierError(f"decode account identifier failed: {e}:")

    return Intent(
        account_address=utils.account_address(account_address),
        sub_address=sub_address,
        currency_code=currency_code,
        amount=amount,
        hrp=hrp,
    )
def encode_account(onchain_addr: Union[AccountAddress, str], subaddr: Union[str, bytes, NoneType], hrp: str) ‑> str

Encode onchain address and (optional) subaddress with human readable prefix(hrp) into bech32 format

Expand source code
def encode_account(
    onchain_addr: typing.Union[diem_types.AccountAddress, str],
    subaddr: typing.Union[str, bytes, None],
    hrp: str,
) -> str:
    """Encode onchain address and (optional) subaddress with human readable prefix(hrp) into bech32 format"""

    onchain_address_bytes = utils.account_address_bytes(onchain_addr)
    subaddress_bytes = utils.sub_address(subaddr) if subaddr else None

    try:
        encoded_address = bech32_address_encode(hrp, onchain_address_bytes, subaddress_bytes)
    except Bech32Error as e:
        raise ValueError(
            f"Can't encode from "
            f"onchain_addr: {onchain_addr}, "
            f"subaddr: {subaddr}, "
            f"hrp: {hrp}, got error: {e}"
        )
    return encoded_address
def encode_intent(encoded_account_identifier: str, currency_code: Optional[str] = None, amount: Optional[int] = None) ‑> str

Encode account identifier string(encoded), currency code and amount into Diem intent identifier (https://dip.diem.com/dip-5/)

Expand source code
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/)
    """

    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

Classes

class Intent (account_address: AccountAddress, sub_address: Optional[bytes], currency_code: Optional[str], amount: Optional[int], hrp: str)

Intent is a struct hold data decoded from Diem Intent Identifier string

Expand source code
class Intent:
    """Intent is a struct hold data decoded from Diem Intent Identifier string"""

    account_address: diem_types.AccountAddress
    sub_address: typing.Optional[bytes]
    currency_code: typing.Optional[str]
    amount: typing.Optional[int]

    def __init__(
        self,
        account_address: diem_types.AccountAddress,
        sub_address: typing.Optional[bytes],
        currency_code: typing.Optional[str],
        amount: typing.Optional[int],
        hrp: str,
    ) -> None:
        self.account_address = account_address
        self.sub_address = sub_address
        self.currency_code = currency_code
        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()

    @property
    def account_id(self) -> str:
        return encode_account(self.account_address, self.sub_address, self.hrp)

Class variables

var account_addressAccountAddress
var amount : Optional[int]
var currency_code : Optional[str]
var sub_address : Optional[bytes]

Instance variables

var account_address_bytes : bytes
Expand source code
@property
def account_address_bytes(self) -> bytes:
    return self.account_address.to_bytes()
var account_id : str
Expand source code
@property
def account_id(self) -> str:
    return encode_account(self.account_address, self.sub_address, self.hrp)
var subaddress : Optional[bytes]
Expand source code
@property
def subaddress(self) -> typing.Optional[bytes]:
    return self.sub_address
class InvalidIntentIdentifierError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class InvalidIntentIdentifierError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException