Module diem.offchain.types

This module defines off-chain protocol data structures and PaymentCommand data structures.

It also defines to_json() and from_json() functions for serializing objects to json string for sending out command and deserializing objects from json string for processing inbound request command.

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

""" This module defines off-chain protocol data structures and PaymentCommand data structures.

It also defines `to_json` and `from_json` functions for serializing objects to json string for sending
out command and deserializing objects from json string for processing inbound request command.
"""

from .command_types import (
    OffChainErrorType,
    ErrorCode,
    CommandResponseObject,
    OffChainErrorObject,
    CommandRequestObject,
    CommandResponseStatus,
    ReferenceIDCommandObject,
    ReferenceIDCommandResultObject,
    FundPullPreApprovalCommandObject,
    UUID_REGEX,
)
from .payment_types import (
    AbortCode,
    NationalIdObject,
    AddressObject,
    Status,
    StatusObject,
    PaymentObject,
    PaymentActorObject,
    PaymentActionObject,
    KycDataObject,
    KycDataObjectType,
    CommandType,
    PaymentCommandObject,
)

import dataclasses, json, re, secrets, typing, uuid


class FieldError(ValueError):
    def __init__(self, code: str, field: str, msg: str) -> None:
        super().__init__(msg)
        self.code: str = code
        self.field: typing.Optional[str] = field if field else None


class InvalidOverwriteError(FieldError):
    def __init__(
        self, field: str, prior_value: typing.Any, new_value: typing.Any, field_type: str  # pyre-ignore
    ) -> None:
        msg = f"{field_type} field '{field}': {prior_value} => {new_value}"
        super().__init__(ErrorCode.invalid_overwrite, field, msg)


T = typing.TypeVar("T")


_OBJECT_TYPES: typing.Dict[str, typing.Any] = {
    "CommandResponseObject": CommandResponseObject,
    "CommandRequestObject": CommandRequestObject,
    CommandType.PaymentCommand: PaymentCommandObject,
    CommandType.FundPullPreApprovalCommand: FundPullPreApprovalCommandObject,
}

_OBJECT_TYPE_FIELD_NAME = "_ObjectType"


def to_json(obj: T, indent: typing.Optional[int] = None) -> str:
    return json.dumps(to_dict(obj), indent=indent)


def to_dict(obj: T) -> typing.Dict[str, typing.Any]:
    if dataclasses.is_dataclass(obj):
        raw = dataclasses.asdict(obj)
    elif isinstance(obj, list):
        raw = list(map(dataclasses.asdict, obj))
    else:
        raw = obj
    return _delete_none(raw)


def from_json(data: str, klass: typing.Optional[typing.Type[T]] = None) -> T:
    return from_dict(json.loads(data), klass)


def from_dict(obj: typing.Any, klass: typing.Optional[typing.Type[T]] = None, field_path: str = "") -> T:  # pyre-ignore
    if klass is None or _is_union(klass):
        if not isinstance(obj, dict):
            code = ErrorCode.invalid_field_value if field_path else ErrorCode.invalid_object
            raise FieldError(code, field_path, f"expect json object, but got {type(obj).__name__}: {obj}")
        klass = _find_object_type(obj, field_path)
    return _from_dict(obj, klass, field_path)


def _from_dict(obj: typing.Any, klass: typing.Type[typing.Any], field_path: str) -> typing.Any:  # pyre-ignore
    if not isinstance(obj, dict) or not dataclasses.is_dataclass(klass):
        item_type = None
        if hasattr(klass, "__origin__") and klass.__origin__ == list and hasattr(klass, "__args__"):
            item_type = klass.__args__[0]
            klass = list
        if not isinstance(obj, klass):
            code = ErrorCode.invalid_field_value if field_path else ErrorCode.invalid_object
            raise FieldError(code, field_path, f"expect type {klass.__name__}, but got {type(obj).__name__}")
        if klass == list and item_type:
            return [from_dict(item, item_type, field_path) for item in obj]
        return obj

    unknown_fields = list(obj.keys())
    fields = {}
    for field in dataclasses.fields(klass):
        if field.name in unknown_fields:
            unknown_fields.remove(field.name)
        fields[field.name] = _field_value_from_dict(field, obj, field_path)

    if len(unknown_fields) > 0:
        unknown_fields.sort()
        full_name = _join_field_path(field_path, unknown_fields[0])
        field_names = ", ".join(unknown_fields)
        raise FieldError(ErrorCode.unknown_field, full_name, f"{field_path}: {field_names}")
    return klass(**fields)


def _field_value_from_dict(field: dataclasses.Field, obj: typing.Any, field_path: str) -> typing.Any:  # pyre-ignore
    full_name = _join_field_path(field_path, field.name)
    field_type = field.type
    args = field.type.__args__ if hasattr(field.type, "__args__") else []
    is_optional = len(args) == 2 and isinstance(None, args[1])  # pyre-ignore
    if is_optional:
        field_type = args[0]
    val = obj.get(field.name)
    if val is None:
        if is_optional:
            return None
        raise FieldError(ErrorCode.missing_field, full_name, f"missing field: {full_name}")

    valid_values = field.metadata.get("valid-values")
    if valid_values:
        if isinstance(valid_values, list) and val not in valid_values:
            raise FieldError(ErrorCode.invalid_field_value, full_name, f"expect one of {valid_values}, but got: {val}")
        if isinstance(val, str) and isinstance(valid_values, re.Pattern) and not valid_values.match(val):
            raise FieldError(
                ErrorCode.invalid_field_value, full_name, f"{val} does not match pattern {valid_values.pattern}"
            )
    return from_dict(val, field_type, full_name)


def _join_field_path(path: str, field: str) -> str:
    return f"{path}.{field}" if path else field


def new_payment_object(
    sender_account_id: str,
    sender_kyc_data: KycDataObject,
    receiver_account_id: str,
    amount: int,
    currency: str,
    original_payment_reference_id: typing.Optional[str] = None,
    description: typing.Optional[str] = None,
) -> PaymentObject:
    """Initialize a payment request command

    returns generated reference_id and created `CommandRequestObject`
    """

    return PaymentObject(
        reference_id=str(uuid.uuid4()),
        sender=PaymentActorObject(
            address=sender_account_id,
            kyc_data=sender_kyc_data,
            status=StatusObject(status=Status.needs_kyc_data),
        ),
        receiver=PaymentActorObject(
            address=receiver_account_id,
            status=StatusObject(status=Status.none),
        ),
        action=PaymentActionObject(amount=amount, currency=currency),
        description=description,
        original_payment_reference_id=original_payment_reference_id,
    )


def replace_payment_actor(
    actor: PaymentActorObject,
    status: typing.Optional[str] = None,
    kyc_data: typing.Optional[KycDataObject] = None,
    additional_kyc_data: typing.Optional[str] = None,
    abort_code: typing.Optional[str] = None,
    abort_message: typing.Optional[str] = None,
    metadata: typing.Optional[typing.List[str]] = None,
) -> PaymentActorObject:
    changes = {}
    if kyc_data:
        changes["kyc_data"] = kyc_data
    if additional_kyc_data:
        changes["additional_kyc_data"] = additional_kyc_data
    if status or abort_code or abort_message:
        changes["status"] = replace_payment_status(
            actor.status,
            status=status,
            abort_code=abort_code,
            abort_message=abort_message,
        )
    if metadata:
        if not isinstance(metadata, list):
            raise ValueError("metadata should be a list of string")
        changes["metadata"] = actor.metadata + metadata if actor.metadata else metadata
    return dataclasses.replace(actor, **changes)


def replace_payment_status(
    status_obj: StatusObject,
    status: typing.Optional[str] = None,
    abort_code: typing.Optional[str] = None,
    abort_message: typing.Optional[str] = None,
) -> StatusObject:
    changes = {}
    if status:
        changes["status"] = status
    if abort_code:
        changes["abort_code"] = abort_code
    if abort_message:
        changes["abort_message"] = abort_message
    return dataclasses.replace(status_obj, **changes)


def new_payment_request(
    payment: PaymentObject,
    cid: typing.Optional[str] = None,
) -> CommandRequestObject:
    return CommandRequestObject(
        cid=cid or str(uuid.uuid4()),
        command_type=CommandType.PaymentCommand,
        command=to_dict(
            PaymentCommandObject(
                _ObjectType=CommandType.PaymentCommand,
                payment=payment,
            )
        ),
    )


def reply_request(
    cid: typing.Optional[str],
    err: typing.Optional[OffChainErrorObject] = None,
) -> CommandResponseObject:
    return CommandResponseObject(
        status=CommandResponseStatus.failure if err else CommandResponseStatus.success,
        error=err,
        cid=cid,
    )


def individual_kyc_data(**kwargs) -> KycDataObject:  # pyre-ignore
    return KycDataObject(
        type=KycDataObjectType.individual,
        **kwargs,
    )


def entity_kyc_data(**kwargs) -> KycDataObject:  # pyre-ignore
    return KycDataObject(
        type=KycDataObjectType.entity,
        **kwargs,
    )


def validate_write_once_fields(path: str, new: typing.Any, prior: typing.Any) -> None:  # pyre-ignore
    if new is None or prior is None:
        return

    new_type = type(new)
    if type(prior) != new_type:
        raise TypeError(f"field {path} type is different, expect {type(prior)}, but got {new_type}")

    if not dataclasses.is_dataclass(new_type):
        return

    for field in dataclasses.fields(new_type):
        prior_value = getattr(prior, field.name)
        new_value = getattr(new, field.name)
        field_path = path + "." + field.name
        if field.metadata.get("immutable") and prior_value != new_value:
            raise InvalidOverwriteError(field_path, prior_value, new_value, "immutable")
        if field.metadata.get("write_once") and prior_value is not None and prior_value != new_value:
            raise InvalidOverwriteError(field_path, prior_value, new_value, "write once")
        validate_write_once_fields(field_path, new_value, prior_value)


def _delete_none(obj: typing.Any) -> typing.Any:  # pyre-ignore
    if isinstance(obj, dict):
        for key, val in list(obj.items()):
            if val is None:
                del obj[key]
            else:
                obj[key] = _delete_none(val)
    elif isinstance(obj, list):
        for val in obj:
            _delete_none(val)
    return obj


def _find_object_type(obj: typing.Dict[str, typing.Any], field_path: str) -> typing.Type[typing.Any]:  # pyre-ignore
    obj_type = obj.get(_OBJECT_TYPE_FIELD_NAME)
    full_name = _join_field_path(field_path, _OBJECT_TYPE_FIELD_NAME)
    if not obj_type:
        raise FieldError(ErrorCode.missing_field, full_name, f"missing field: {full_name}")
    t = _OBJECT_TYPES.get(obj_type)
    if t is None:
        raise FieldError(
            ErrorCode.invalid_field_value,
            full_name,
            f"could not find object type: {obj_type}, valid types: {list(_OBJECT_TYPES.keys())}",
        )
    return t


def _is_union(klass: typing.Any) -> bool:  # pyre-ignore
    return hasattr(klass, "__origin__") and klass.__origin__ == typing.Union

Sub-modules

diem.offchain.types.command_types
diem.offchain.types.payment_types

Functions

def entity_kyc_data(**kwargs) ‑> KycDataObject
Expand source code
def entity_kyc_data(**kwargs) -> KycDataObject:  # pyre-ignore
    return KycDataObject(
        type=KycDataObjectType.entity,
        **kwargs,
    )
def from_dict(obj: Any, klass: Optional[Type[~T]] = None, field_path: str = '') ‑> ~T
Expand source code
def from_dict(obj: typing.Any, klass: typing.Optional[typing.Type[T]] = None, field_path: str = "") -> T:  # pyre-ignore
    if klass is None or _is_union(klass):
        if not isinstance(obj, dict):
            code = ErrorCode.invalid_field_value if field_path else ErrorCode.invalid_object
            raise FieldError(code, field_path, f"expect json object, but got {type(obj).__name__}: {obj}")
        klass = _find_object_type(obj, field_path)
    return _from_dict(obj, klass, field_path)
def from_json(data: str, klass: Optional[Type[~T]] = None) ‑> ~T
Expand source code
def from_json(data: str, klass: typing.Optional[typing.Type[T]] = None) -> T:
    return from_dict(json.loads(data), klass)
def individual_kyc_data(**kwargs) ‑> KycDataObject
Expand source code
def individual_kyc_data(**kwargs) -> KycDataObject:  # pyre-ignore
    return KycDataObject(
        type=KycDataObjectType.individual,
        **kwargs,
    )
def new_payment_object(sender_account_id: str, sender_kyc_data: KycDataObject, receiver_account_id: str, amount: int, currency: str, original_payment_reference_id: Optional[str] = None, description: Optional[str] = None) ‑> PaymentObject

Initialize a payment request command

returns generated reference_id and created CommandRequestObject

Expand source code
def new_payment_object(
    sender_account_id: str,
    sender_kyc_data: KycDataObject,
    receiver_account_id: str,
    amount: int,
    currency: str,
    original_payment_reference_id: typing.Optional[str] = None,
    description: typing.Optional[str] = None,
) -> PaymentObject:
    """Initialize a payment request command

    returns generated reference_id and created `CommandRequestObject`
    """

    return PaymentObject(
        reference_id=str(uuid.uuid4()),
        sender=PaymentActorObject(
            address=sender_account_id,
            kyc_data=sender_kyc_data,
            status=StatusObject(status=Status.needs_kyc_data),
        ),
        receiver=PaymentActorObject(
            address=receiver_account_id,
            status=StatusObject(status=Status.none),
        ),
        action=PaymentActionObject(amount=amount, currency=currency),
        description=description,
        original_payment_reference_id=original_payment_reference_id,
    )
def new_payment_request(payment: PaymentObject, cid: Optional[str] = None) ‑> CommandRequestObject
Expand source code
def new_payment_request(
    payment: PaymentObject,
    cid: typing.Optional[str] = None,
) -> CommandRequestObject:
    return CommandRequestObject(
        cid=cid or str(uuid.uuid4()),
        command_type=CommandType.PaymentCommand,
        command=to_dict(
            PaymentCommandObject(
                _ObjectType=CommandType.PaymentCommand,
                payment=payment,
            )
        ),
    )
def replace_payment_actor(actor: PaymentActorObject, status: Optional[str] = None, kyc_data: Optional[KycDataObject] = None, additional_kyc_data: Optional[str] = None, abort_code: Optional[str] = None, abort_message: Optional[str] = None, metadata: Optional[List[str]] = None) ‑> PaymentActorObject
Expand source code
def replace_payment_actor(
    actor: PaymentActorObject,
    status: typing.Optional[str] = None,
    kyc_data: typing.Optional[KycDataObject] = None,
    additional_kyc_data: typing.Optional[str] = None,
    abort_code: typing.Optional[str] = None,
    abort_message: typing.Optional[str] = None,
    metadata: typing.Optional[typing.List[str]] = None,
) -> PaymentActorObject:
    changes = {}
    if kyc_data:
        changes["kyc_data"] = kyc_data
    if additional_kyc_data:
        changes["additional_kyc_data"] = additional_kyc_data
    if status or abort_code or abort_message:
        changes["status"] = replace_payment_status(
            actor.status,
            status=status,
            abort_code=abort_code,
            abort_message=abort_message,
        )
    if metadata:
        if not isinstance(metadata, list):
            raise ValueError("metadata should be a list of string")
        changes["metadata"] = actor.metadata + metadata if actor.metadata else metadata
    return dataclasses.replace(actor, **changes)
def replace_payment_status(status_obj: StatusObject, status: Optional[str] = None, abort_code: Optional[str] = None, abort_message: Optional[str] = None) ‑> StatusObject
Expand source code
def replace_payment_status(
    status_obj: StatusObject,
    status: typing.Optional[str] = None,
    abort_code: typing.Optional[str] = None,
    abort_message: typing.Optional[str] = None,
) -> StatusObject:
    changes = {}
    if status:
        changes["status"] = status
    if abort_code:
        changes["abort_code"] = abort_code
    if abort_message:
        changes["abort_message"] = abort_message
    return dataclasses.replace(status_obj, **changes)
def reply_request(cid: Optional[str], err: Optional[OffChainErrorObject] = None) ‑> CommandResponseObject
Expand source code
def reply_request(
    cid: typing.Optional[str],
    err: typing.Optional[OffChainErrorObject] = None,
) -> CommandResponseObject:
    return CommandResponseObject(
        status=CommandResponseStatus.failure if err else CommandResponseStatus.success,
        error=err,
        cid=cid,
    )
def to_dict(obj: ~T) ‑> Dict[str, Any]
Expand source code
def to_dict(obj: T) -> typing.Dict[str, typing.Any]:
    if dataclasses.is_dataclass(obj):
        raw = dataclasses.asdict(obj)
    elif isinstance(obj, list):
        raw = list(map(dataclasses.asdict, obj))
    else:
        raw = obj
    return _delete_none(raw)
def to_json(obj: ~T, indent: Optional[int] = None) ‑> str
Expand source code
def to_json(obj: T, indent: typing.Optional[int] = None) -> str:
    return json.dumps(to_dict(obj), indent=indent)
def validate_write_once_fields(path: str, new: Any, prior: Any) ‑> NoneType
Expand source code
def validate_write_once_fields(path: str, new: typing.Any, prior: typing.Any) -> None:  # pyre-ignore
    if new is None or prior is None:
        return

    new_type = type(new)
    if type(prior) != new_type:
        raise TypeError(f"field {path} type is different, expect {type(prior)}, but got {new_type}")

    if not dataclasses.is_dataclass(new_type):
        return

    for field in dataclasses.fields(new_type):
        prior_value = getattr(prior, field.name)
        new_value = getattr(new, field.name)
        field_path = path + "." + field.name
        if field.metadata.get("immutable") and prior_value != new_value:
            raise InvalidOverwriteError(field_path, prior_value, new_value, "immutable")
        if field.metadata.get("write_once") and prior_value is not None and prior_value != new_value:
            raise InvalidOverwriteError(field_path, prior_value, new_value, "write once")
        validate_write_once_fields(field_path, new_value, prior_value)

Classes

class FieldError (code: str, field: str, msg: str)

Inappropriate argument value (of correct type).

Expand source code
class FieldError(ValueError):
    def __init__(self, code: str, field: str, msg: str) -> None:
        super().__init__(msg)
        self.code: str = code
        self.field: typing.Optional[str] = field if field else None

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException

Subclasses

class InvalidOverwriteError (field: str, prior_value: Any, new_value: Any, field_type: str)

Inappropriate argument value (of correct type).

Expand source code
class InvalidOverwriteError(FieldError):
    def __init__(
        self, field: str, prior_value: typing.Any, new_value: typing.Any, field_type: str  # pyre-ignore
    ) -> None:
        msg = f"{field_type} field '{field}': {prior_value} => {new_value}"
        super().__init__(ErrorCode.invalid_overwrite, field, msg)

Ancestors

  • FieldError
  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException