fabric-chaincode-python 2.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fabric_chaincode_python-2.5.2.dist-info/METADATA +101 -0
- fabric_chaincode_python-2.5.2.dist-info/RECORD +40 -0
- fabric_chaincode_python-2.5.2.dist-info/WHEEL +5 -0
- fabric_chaincode_python-2.5.2.dist-info/licenses/LICENSE +201 -0
- fabric_chaincode_python-2.5.2.dist-info/licenses/NOTICE +18 -0
- fabric_chaincode_python-2.5.2.dist-info/top_level.txt +1 -0
- src/__init__.py +5 -0
- src/fabric_contract_api/__init__.py +51 -0
- src/fabric_contract_api/contractapi/__init__.py +29 -0
- src/fabric_contract_api/contractapi/contract.py +141 -0
- src/fabric_contract_api/contractapi/contract_chaincode.py +552 -0
- src/fabric_contract_api/contractapi/system_contract.py +45 -0
- src/fabric_contract_api/contractapi/transaction_context.py +160 -0
- src/fabric_contract_api/contractapi/utils/__init__.py +4 -0
- src/fabric_contract_api/contractapi/utils/undefined_interface.py +33 -0
- src/fabric_contract_api/internal/__init__.py +16 -0
- src/fabric_contract_api/internal/contract_function.py +453 -0
- src/fabric_contract_api/internal/transaction_handler.py +131 -0
- src/fabric_contract_api/internal/types.py +180 -0
- src/fabric_contract_api/internal/types_handler.py +224 -0
- src/fabric_contract_api/internal/utils.py +59 -0
- src/fabric_contract_api/metadata/__init__.py +32 -0
- src/fabric_contract_api/metadata/metadata.py +456 -0
- src/fabric_contract_api/metadata/schema.py +238 -0
- src/fabric_contract_api/serializer/__init__.py +5 -0
- src/fabric_contract_api/serializer/json_transaction_serializer.py +363 -0
- src/fabric_contract_api/serializer/transaction_serializer.py +45 -0
- src/fabric_shim/__init__.py +2 -0
- src/fabric_shim/chaincode.py +8 -0
- src/fabric_shim/handler.py +336 -0
- src/fabric_shim/interfaces.py +134 -0
- src/fabric_shim/iterators.py +107 -0
- src/fabric_shim/logger.py +1 -0
- src/fabric_shim/logging.py +63 -0
- src/fabric_shim/msg_queue_handler.py +91 -0
- src/fabric_shim/response.py +49 -0
- src/fabric_shim/server.py +155 -0
- src/fabric_shim/stub.py +342 -0
- src/fabric_shim/utils.py +41 -0
- src/version.py +3 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Transaction context for :mod:`fabric_contract_api`.
|
|
3
|
+
|
|
4
|
+
This module is the Python equivalent of Go's
|
|
5
|
+
``contractapi/transaction_context.go``. It defines:
|
|
6
|
+
|
|
7
|
+
* :class:`TransactionContextInterface` — an :class:`abc.ABC` describing the
|
|
8
|
+
minimum surface area a transaction context must provide (``get_stub`` and
|
|
9
|
+
``get_client_identity``).
|
|
10
|
+
* :class:`SettableTransactionContextInterface` — an :class:`abc.ABC` that
|
|
11
|
+
adds ``set_stub`` and ``set_client_identity`` so the chaincode dispatcher
|
|
12
|
+
can inject the active :class:`ChaincodeStub` and client identity.
|
|
13
|
+
* :class:`TransactionContext` — a default implementation of both interfaces
|
|
14
|
+
that contracts can embed.
|
|
15
|
+
* :class:`ClientIdentity` — a minimal client-identity wrapper that mirrors
|
|
16
|
+
the surface area of Go's ``cid.ClientIdentity``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import abc
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClientIdentity:
|
|
26
|
+
"""Minimal client-identity implementation.
|
|
27
|
+
|
|
28
|
+
Mirrors the surface area of Go's ``cid.ClientIdentity`` that the contract
|
|
29
|
+
API relies on. Built from the proposal metadata decoded by the shim's
|
|
30
|
+
:class:`ChaincodeStub`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, stub: Any) -> None:
|
|
34
|
+
self._stub = stub
|
|
35
|
+
self._msp_id: Optional[str] = None
|
|
36
|
+
self._cert: Optional[bytes] = None
|
|
37
|
+
self._attrs: dict = {}
|
|
38
|
+
self._loaded = False
|
|
39
|
+
|
|
40
|
+
def _ensure_loaded(self) -> None:
|
|
41
|
+
if self._loaded:
|
|
42
|
+
return
|
|
43
|
+
self._loaded = True
|
|
44
|
+
try:
|
|
45
|
+
creator = self._stub.get_creator()
|
|
46
|
+
if isinstance(creator, dict):
|
|
47
|
+
self._msp_id = creator.get("mspid")
|
|
48
|
+
self._cert = creator.get("idBytes")
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def get_mspid(self) -> str:
|
|
53
|
+
"""Return the caller's MSP ID."""
|
|
54
|
+
self._ensure_loaded()
|
|
55
|
+
return self._msp_id or ""
|
|
56
|
+
|
|
57
|
+
def get_id(self) -> str:
|
|
58
|
+
"""Return the caller's DN, base64-encoded.
|
|
59
|
+
|
|
60
|
+
Mirrors Go's ``ClientIdentity.GetID``.
|
|
61
|
+
"""
|
|
62
|
+
import base64
|
|
63
|
+
self._ensure_loaded()
|
|
64
|
+
if not self._cert:
|
|
65
|
+
raise Exception("Failed to get client identity. Client identity does not have a certificate")
|
|
66
|
+
# Strip PEM headers if present.
|
|
67
|
+
cert = self._cert
|
|
68
|
+
if isinstance(cert, bytes):
|
|
69
|
+
cert_str = cert.decode("utf-8", errors="replace")
|
|
70
|
+
else:
|
|
71
|
+
cert_str = str(cert)
|
|
72
|
+
cert_str = cert_str.replace("-----BEGIN CERTIFICATE-----", "").replace("-----END CERTIFICATE-----", "")
|
|
73
|
+
cert_str = "".join(cert_str.split())
|
|
74
|
+
try:
|
|
75
|
+
raw = base64.b64decode(cert_str)
|
|
76
|
+
except Exception:
|
|
77
|
+
raw = cert_str.encode()
|
|
78
|
+
return base64.b64encode(raw).decode()
|
|
79
|
+
|
|
80
|
+
def get_attribute(self, attr_name: str) -> Optional[str]:
|
|
81
|
+
"""Return a single attribute from the caller's certificate."""
|
|
82
|
+
self._ensure_loaded()
|
|
83
|
+
return self._attrs.get(attr_name)
|
|
84
|
+
|
|
85
|
+
def assert_attribute(self, attr_name: str) -> bool:
|
|
86
|
+
"""Return ``True`` if *attr_name* exists in the caller's certificate."""
|
|
87
|
+
self._ensure_loaded()
|
|
88
|
+
return attr_name in self._attrs
|
|
89
|
+
|
|
90
|
+
def get_x509(self) -> bytes:
|
|
91
|
+
"""Return the raw X.509 certificate bytes of the caller."""
|
|
92
|
+
self._ensure_loaded()
|
|
93
|
+
if not self._cert:
|
|
94
|
+
raise Exception("Client identity does not have a certificate")
|
|
95
|
+
if isinstance(self._cert, str):
|
|
96
|
+
return self._cert.encode()
|
|
97
|
+
return self._cert
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TransactionContextInterface(abc.ABC):
|
|
101
|
+
"""Read-only interface for transaction contexts."""
|
|
102
|
+
|
|
103
|
+
@abc.abstractmethod
|
|
104
|
+
def get_stub(self) -> Any:
|
|
105
|
+
"""Return the active :class:`ChaincodeStub`."""
|
|
106
|
+
|
|
107
|
+
@abc.abstractmethod
|
|
108
|
+
def get_client_identity(self) -> ClientIdentity:
|
|
109
|
+
"""Return the caller's :class:`ClientIdentity`."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SettableTransactionContextInterface(TransactionContextInterface):
|
|
113
|
+
"""Interface that adds the setters used by the chaincode dispatcher."""
|
|
114
|
+
|
|
115
|
+
@abc.abstractmethod
|
|
116
|
+
def set_stub(self, stub: Any) -> None: ...
|
|
117
|
+
|
|
118
|
+
@abc.abstractmethod
|
|
119
|
+
def set_client_identity(self, ci: ClientIdentity) -> None: ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TransactionContext(SettableTransactionContextInterface):
|
|
123
|
+
"""Default transaction context.
|
|
124
|
+
|
|
125
|
+
Contracts that don't need a custom transaction context can simply embed
|
|
126
|
+
:class:`TransactionContext`::
|
|
127
|
+
|
|
128
|
+
class MyContract(Contract):
|
|
129
|
+
TransactionContextHandler = TransactionContext
|
|
130
|
+
|
|
131
|
+
def my_transaction(self, ctx: TransactionContextInterface) -> str:
|
|
132
|
+
stub = ctx.get_stub()
|
|
133
|
+
...
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self) -> None:
|
|
137
|
+
self._stub: Any = None
|
|
138
|
+
self._client_identity: Optional[ClientIdentity] = None
|
|
139
|
+
|
|
140
|
+
def set_stub(self, stub: Any) -> None:
|
|
141
|
+
self._stub = stub
|
|
142
|
+
|
|
143
|
+
def set_client_identity(self, ci: ClientIdentity) -> None:
|
|
144
|
+
self._client_identity = ci
|
|
145
|
+
|
|
146
|
+
def get_stub(self) -> Any:
|
|
147
|
+
return self._stub
|
|
148
|
+
|
|
149
|
+
def get_client_identity(self) -> ClientIdentity:
|
|
150
|
+
if self._client_identity is None:
|
|
151
|
+
self._client_identity = ClientIdentity(self._stub)
|
|
152
|
+
return self._client_identity
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"ClientIdentity",
|
|
157
|
+
"TransactionContext",
|
|
158
|
+
"TransactionContextInterface",
|
|
159
|
+
"SettableTransactionContextInterface",
|
|
160
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Sentinel type for the "no return value" case in after-transactions.
|
|
3
|
+
|
|
4
|
+
Mirrors Go's ``contractapi/utils/undefined_interface.go``. When an
|
|
5
|
+
after-transaction function declares a single non-context parameter, the
|
|
6
|
+
contract chaincode passes it an instance of :class:`UndefinedInterface` when
|
|
7
|
+
the named function returned no success value.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UndefinedInterface:
|
|
14
|
+
"""Sentinel type used when a contract function returns no value."""
|
|
15
|
+
|
|
16
|
+
_instance: "UndefinedInterface | None" = None
|
|
17
|
+
|
|
18
|
+
def __new__(cls) -> "UndefinedInterface":
|
|
19
|
+
if cls._instance is None:
|
|
20
|
+
cls._instance = super().__new__(cls)
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str: # pragma: no cover - cosmetic
|
|
24
|
+
return "UndefinedInterface"
|
|
25
|
+
|
|
26
|
+
def __eq__(self, other: object) -> bool:
|
|
27
|
+
return isinstance(other, UndefinedInterface)
|
|
28
|
+
|
|
29
|
+
def __hash__(self) -> int:
|
|
30
|
+
return hash("UndefinedInterface")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["UndefinedInterface"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Internal package for :mod:`fabric_contract_api`.
|
|
3
|
+
|
|
4
|
+
This package mirrors Go's ``internal/`` directory and contains the building
|
|
5
|
+
blocks used by :mod:`fabric_contract_api.contractapi`.
|
|
6
|
+
|
|
7
|
+
To avoid circular imports we intentionally do **not** eagerly re-export the
|
|
8
|
+
sub-modules here — they are imported lazily by the modules that need them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Sub-modules are imported on demand:
|
|
12
|
+
# from .internal.types import BasicTypes, ErrorType, TimeType, is_bytes
|
|
13
|
+
# from .internal.utils import slice_as_comma_sentence, string_in_slice, validate_errors_to_string
|
|
14
|
+
# from .internal.contract_function import CallType, ContractFunction
|
|
15
|
+
# from .internal.transaction_handler import TransactionHandler, TransactionHandlerType, new_transaction_handler
|
|
16
|
+
# from .internal.types_handler import type_is_valid, type_matches_interface, list_basic_types
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Contract function wrapper.
|
|
3
|
+
|
|
4
|
+
This is the Python equivalent of Go's ``internal/contract_function.go``.
|
|
5
|
+
A :class:`ContractFunction` wraps a Python callable (typically a method of a
|
|
6
|
+
contract class) along with a description of its parameters and return type
|
|
7
|
+
so that it can be called by the :class:`ContractChaincode` dispatcher.
|
|
8
|
+
|
|
9
|
+
Compared to the Go implementation, the Python version does not need to use
|
|
10
|
+
runtime reflection (``reflect``) — instead it uses :mod:`inspect` and
|
|
11
|
+
:mod:`typing` annotations to extract parameter and return types at
|
|
12
|
+
registration time.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import dataclasses
|
|
19
|
+
import inspect
|
|
20
|
+
import typing as _t
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from ..metadata import (
|
|
24
|
+
ComponentMetadata,
|
|
25
|
+
ParameterMetadata,
|
|
26
|
+
ReturnMetadata,
|
|
27
|
+
TransactionMetadata,
|
|
28
|
+
)
|
|
29
|
+
from ..metadata.schema import get_schema
|
|
30
|
+
from ..internal.types_handler import type_is_valid, type_matches_interface
|
|
31
|
+
from ..internal.types import ErrorType, TimeType
|
|
32
|
+
from ..internal.utils import slice_as_comma_sentence
|
|
33
|
+
from .types import BasicTypes
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# CallType enum
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CallType:
|
|
42
|
+
"""Enum-like constants for the call type of a contract function.
|
|
43
|
+
|
|
44
|
+
Mirrors Go's ``CallType`` constants.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
NA = 0
|
|
48
|
+
SUBMIT = 1
|
|
49
|
+
EVALUATE = 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Helpers
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _strip_optional(tp: Any) -> Any:
|
|
58
|
+
origin = _t.get_origin(tp)
|
|
59
|
+
if origin is _t.Union:
|
|
60
|
+
args = [a for a in _t.get_args(tp) if a is not type(None)]
|
|
61
|
+
if len(args) == 1:
|
|
62
|
+
return args[0]
|
|
63
|
+
return tp
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_context_type(tp: Any, context_handler_type: Any) -> bool:
|
|
67
|
+
"""Return ``True`` if *tp* is the transaction-context type.
|
|
68
|
+
|
|
69
|
+
A type is considered the context type when:
|
|
70
|
+
* it equals ``context_handler_type``; or
|
|
71
|
+
* it is an :class:`abc.ABC` protocol and *context_handler_type* structurally
|
|
72
|
+
matches it (mirrors the Go interface check).
|
|
73
|
+
"""
|
|
74
|
+
tp = _strip_optional(tp)
|
|
75
|
+
if tp is context_handler_type:
|
|
76
|
+
return True
|
|
77
|
+
# If the parameter type is an abstract base class or a typing.Protocol,
|
|
78
|
+
# check whether the context type implements all of its abstract methods.
|
|
79
|
+
if tp is not None and getattr(tp, "__abstractmethods__", None):
|
|
80
|
+
if type_matches_interface(context_handler_type, tp) is None:
|
|
81
|
+
return True
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_param(p: inspect.Parameter, method_name: str,
|
|
86
|
+
context_handler_type: Any) -> Tuple[bool, Any]:
|
|
87
|
+
"""Parse a single parameter.
|
|
88
|
+
|
|
89
|
+
Returns ``(is_context, type_annotation)``. *type_annotation* is ``None``
|
|
90
|
+
when the parameter is the transaction context (and thus should not be
|
|
91
|
+
passed as a positional arg from the serializer).
|
|
92
|
+
"""
|
|
93
|
+
annotation = p.annotation
|
|
94
|
+
if annotation is inspect.Parameter.empty:
|
|
95
|
+
# No annotation — assume it's the context if it's the first
|
|
96
|
+
# parameter named ``ctx``/``context``; otherwise treat as Any.
|
|
97
|
+
if p.name in ("ctx", "context", "ctx_", "transaction_context"):
|
|
98
|
+
return True, context_handler_type
|
|
99
|
+
return False, _t.Any
|
|
100
|
+
|
|
101
|
+
if _is_context_type(annotation, context_handler_type):
|
|
102
|
+
return True, annotation
|
|
103
|
+
return False, annotation
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parse_method(fn: Callable[..., Any], context_handler_type: Any
|
|
107
|
+
) -> Tuple[List[Any], bool, Any, bool]:
|
|
108
|
+
"""Parse the signature of *fn*.
|
|
109
|
+
|
|
110
|
+
Returns ``(field_types, uses_context, success_type, returns_error)``.
|
|
111
|
+
"""
|
|
112
|
+
sig = inspect.signature(fn)
|
|
113
|
+
try:
|
|
114
|
+
hints = _t.get_type_hints(fn)
|
|
115
|
+
except Exception:
|
|
116
|
+
hints = {}
|
|
117
|
+
|
|
118
|
+
field_types: List[Any] = []
|
|
119
|
+
uses_context = False
|
|
120
|
+
|
|
121
|
+
params = list(sig.parameters.values())
|
|
122
|
+
if params and params[0].name == "self":
|
|
123
|
+
params = params[1:]
|
|
124
|
+
|
|
125
|
+
for i, p in enumerate(params):
|
|
126
|
+
annotation = hints.get(p.name, p.annotation)
|
|
127
|
+
if annotation is inspect.Parameter.empty:
|
|
128
|
+
annotation = _t.Any
|
|
129
|
+
|
|
130
|
+
is_ctx, tp = _parse_param_named(p, annotation, context_handler_type)
|
|
131
|
+
if is_ctx:
|
|
132
|
+
if i != 0:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"functions requiring the TransactionContext must require it "
|
|
135
|
+
f"as the first parameter. {fn.__name__} takes it as parameter {i}"
|
|
136
|
+
)
|
|
137
|
+
uses_context = True
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
err = type_is_valid(tp, allow_error=False)
|
|
141
|
+
if err is not None:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"{fn.__name__} contains invalid parameter type. {err}"
|
|
144
|
+
)
|
|
145
|
+
field_types.append(tp)
|
|
146
|
+
|
|
147
|
+
return_type = hints.get("return", None)
|
|
148
|
+
success_type, returns_error = _parse_return(return_type, fn.__name__)
|
|
149
|
+
|
|
150
|
+
return field_types, uses_context, success_type, returns_error
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_param_named(p: inspect.Parameter, annotation: Any,
|
|
154
|
+
context_handler_type: Any) -> Tuple[bool, Any]:
|
|
155
|
+
"""Like :func:`_parse_param` but uses the resolved annotation."""
|
|
156
|
+
if _is_context_type(annotation, context_handler_type):
|
|
157
|
+
return True, annotation
|
|
158
|
+
return False, annotation
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parse_return(return_type: Any, method_name: str) -> Tuple[Any, bool]:
|
|
162
|
+
"""Parse a return annotation.
|
|
163
|
+
|
|
164
|
+
Returns ``(success_type, returns_error)``. ``success_type`` is ``None``
|
|
165
|
+
when the function does not return a value. ``returns_error`` is ``True``
|
|
166
|
+
when the function declares an error return (a tuple whose last element is
|
|
167
|
+
``Exception`` / ``BaseException``).
|
|
168
|
+
|
|
169
|
+
The Python contract API supports three idioms for returning errors from a
|
|
170
|
+
contract function:
|
|
171
|
+
|
|
172
|
+
1. **Raise an exception** — the simplest, most Pythonic form. The
|
|
173
|
+
function's return type annotation is the success type only.
|
|
174
|
+
2. **Return ``(value, exception_or_None)``** — a Go-style tuple. The
|
|
175
|
+
return annotation is ``Tuple[T, Optional[Exception]]``. When the
|
|
176
|
+
exception is ``None`` the success value is used; otherwise the
|
|
177
|
+
exception is propagated.
|
|
178
|
+
3. **Return ``Optional[T]`` and raise on ``None``** — equivalent to
|
|
179
|
+
form 1 for type-checking purposes.
|
|
180
|
+
"""
|
|
181
|
+
if return_type is None or return_type is inspect.Parameter.empty:
|
|
182
|
+
return None, False
|
|
183
|
+
if return_type is type(None):
|
|
184
|
+
return None, False
|
|
185
|
+
|
|
186
|
+
return_type = _strip_optional(return_type)
|
|
187
|
+
origin = _t.get_origin(return_type)
|
|
188
|
+
args = _t.get_args(return_type)
|
|
189
|
+
|
|
190
|
+
if origin is tuple or origin is _t.Tuple:
|
|
191
|
+
if not args:
|
|
192
|
+
return None, True
|
|
193
|
+
success = args[0]
|
|
194
|
+
if len(args) == 2 and (args[1] is Exception or args[1] is BaseException):
|
|
195
|
+
return success, True
|
|
196
|
+
if len(args) == 2:
|
|
197
|
+
# Treat the second element as an error type if it's a subclass of
|
|
198
|
+
# BaseException, otherwise as a regular success type pair.
|
|
199
|
+
second = args[1]
|
|
200
|
+
if isinstance(second, type) and issubclass(second, BaseException):
|
|
201
|
+
return success, True
|
|
202
|
+
return return_type, False
|
|
203
|
+
return return_type, False
|
|
204
|
+
|
|
205
|
+
if isinstance(return_type, type) and issubclass(return_type, BaseException):
|
|
206
|
+
return None, True
|
|
207
|
+
|
|
208
|
+
return return_type, False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# ContractFunction
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclasses.dataclass
|
|
217
|
+
class _ContractFunctionParams:
|
|
218
|
+
context: Optional[Any] = None # the type annotation of the context, or None
|
|
219
|
+
fields: List[Any] = dataclasses.field(default_factory=list)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclasses.dataclass
|
|
223
|
+
class _ContractFunctionReturns:
|
|
224
|
+
success: Optional[Any] = None
|
|
225
|
+
error: bool = False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class ContractFunction:
|
|
229
|
+
"""Wraps a callable so that it can be dispatched by the chaincode.
|
|
230
|
+
|
|
231
|
+
Public attributes mirror Go's ``ContractFunction``:
|
|
232
|
+
|
|
233
|
+
* ``function`` — the underlying callable.
|
|
234
|
+
* ``call_type`` — one of :class:`CallType` constants.
|
|
235
|
+
* ``params`` — a :class:`_ContractFunctionParams` describing the parameters.
|
|
236
|
+
* ``returns`` — a :class:`_ContractFunctionReturns` describing the return.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, fn: Callable[..., Any], call_type: int,
|
|
240
|
+
param_details: _ContractFunctionParams,
|
|
241
|
+
return_details: _ContractFunctionReturns) -> None:
|
|
242
|
+
self.function = fn
|
|
243
|
+
self.call_type = call_type
|
|
244
|
+
self.params = param_details
|
|
245
|
+
self.returns = return_details
|
|
246
|
+
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
# Construction helpers
|
|
249
|
+
# ------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def from_callable(cls, fn: Callable[..., Any], call_type: int,
|
|
253
|
+
context_handler_type: Any) -> "ContractFunction":
|
|
254
|
+
"""Build a :class:`ContractFunction` from an arbitrary callable.
|
|
255
|
+
|
|
256
|
+
Mirrors Go's ``NewContractFunctionFromFunc``.
|
|
257
|
+
"""
|
|
258
|
+
if not callable(fn):
|
|
259
|
+
raise TypeError(
|
|
260
|
+
f"cannot create new contract function from {type(fn).__name__}. "
|
|
261
|
+
"Can only use callable"
|
|
262
|
+
)
|
|
263
|
+
param_details, return_details = cls._parse(fn, context_handler_type)
|
|
264
|
+
return cls(fn, call_type, param_details, return_details)
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def from_method(cls, instance: Any, method_name: str, call_type: int,
|
|
268
|
+
context_handler_type: Any) -> "ContractFunction":
|
|
269
|
+
"""Build a :class:`ContractFunction` from a method on *instance*.
|
|
270
|
+
|
|
271
|
+
Mirrors Go's ``NewContractFunctionFromReflect``.
|
|
272
|
+
"""
|
|
273
|
+
fn = getattr(instance, method_name)
|
|
274
|
+
return cls.from_callable(fn, call_type, context_handler_type)
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _parse(fn: Callable[..., Any], context_handler_type: Any
|
|
278
|
+
) -> Tuple[_ContractFunctionParams, _ContractFunctionReturns]:
|
|
279
|
+
try:
|
|
280
|
+
field_types, uses_context, success_type, returns_error = _parse_method(
|
|
281
|
+
fn, context_handler_type
|
|
282
|
+
)
|
|
283
|
+
except ValueError:
|
|
284
|
+
raise
|
|
285
|
+
return (
|
|
286
|
+
_ContractFunctionParams(
|
|
287
|
+
context=context_handler_type if uses_context else None,
|
|
288
|
+
fields=field_types,
|
|
289
|
+
),
|
|
290
|
+
_ContractFunctionReturns(success=success_type, error=returns_error),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
# Metadata
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def reflect_metadata(self, name: str,
|
|
298
|
+
existing_components: ComponentMetadata) -> TransactionMetadata:
|
|
299
|
+
"""Return the metadata describing this function.
|
|
300
|
+
|
|
301
|
+
Mirrors Go's ``ContractFunction.ReflectMetadata``.
|
|
302
|
+
"""
|
|
303
|
+
tx_metadata = TransactionMetadata(name=name, tag=[])
|
|
304
|
+
tx_type = "SUBMIT"
|
|
305
|
+
tx_type_deprecated = "submit"
|
|
306
|
+
if self.call_type == CallType.EVALUATE:
|
|
307
|
+
tx_type = "EVALUATE"
|
|
308
|
+
tx_type_deprecated = "evaluate"
|
|
309
|
+
tx_metadata.tag = [tx_type_deprecated, tx_type]
|
|
310
|
+
|
|
311
|
+
for index, field in enumerate(self.params.fields):
|
|
312
|
+
schema = get_schema(field, existing_components)
|
|
313
|
+
param = ParameterMetadata(name=f"param{index}", schema=schema)
|
|
314
|
+
tx_metadata.parameters.append(param)
|
|
315
|
+
|
|
316
|
+
if self.returns.success is not None:
|
|
317
|
+
schema = get_schema(self.returns.success, existing_components)
|
|
318
|
+
tx_metadata.returns = ReturnMetadata(schema=schema)
|
|
319
|
+
|
|
320
|
+
return tx_metadata
|
|
321
|
+
|
|
322
|
+
# ------------------------------------------------------------------
|
|
323
|
+
# Calling
|
|
324
|
+
# ------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
async def call(self, ctx: Any, supplementary_metadata: Optional[TransactionMetadata],
|
|
327
|
+
components: Optional[ComponentMetadata], serializer, *params: str
|
|
328
|
+
) -> Tuple[str, Any, Optional[Exception]]:
|
|
329
|
+
"""Invoke the wrapped function with the given string parameters.
|
|
330
|
+
|
|
331
|
+
Returns ``(success_string, success_value, error)`` — mirroring Go's
|
|
332
|
+
``ContractFunction.Call``.
|
|
333
|
+
"""
|
|
334
|
+
parameter_metadata: Optional[List[ParameterMetadata]] = None
|
|
335
|
+
if supplementary_metadata is not None:
|
|
336
|
+
parameter_metadata = supplementary_metadata.parameters
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
values = self._format_args(ctx, parameter_metadata, components,
|
|
340
|
+
list(params), serializer)
|
|
341
|
+
except Exception as exc:
|
|
342
|
+
return "", None, exc
|
|
343
|
+
|
|
344
|
+
# Invoke the underlying function. Both sync and async callables are
|
|
345
|
+
# supported.
|
|
346
|
+
try:
|
|
347
|
+
if inspect.iscoroutinefunction(self.function):
|
|
348
|
+
raw_response = await self.function(*values)
|
|
349
|
+
else:
|
|
350
|
+
raw_response = self.function(*values)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
return "", None, exc
|
|
353
|
+
|
|
354
|
+
returns_metadata = None
|
|
355
|
+
if supplementary_metadata is not None:
|
|
356
|
+
returns_metadata = supplementary_metadata.returns
|
|
357
|
+
|
|
358
|
+
return self._handle_response(raw_response, returns_metadata, components, serializer)
|
|
359
|
+
|
|
360
|
+
# ------------------------------------------------------------------
|
|
361
|
+
# Argument formatting
|
|
362
|
+
# ------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def _format_args(self, ctx: Any,
|
|
365
|
+
supplementary_metadata: Optional[List[ParameterMetadata]],
|
|
366
|
+
components: Optional[ComponentMetadata],
|
|
367
|
+
params: List[str], serializer) -> List[Any]:
|
|
368
|
+
"""Convert the list of string parameters into typed Python values."""
|
|
369
|
+
num_params = len(self.params.fields)
|
|
370
|
+
|
|
371
|
+
if supplementary_metadata is not None and len(supplementary_metadata) != num_params:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"incorrect number of params in supplementary metadata. "
|
|
374
|
+
f"Expected {num_params}, received {len(supplementary_metadata)}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
values: List[Any] = []
|
|
378
|
+
if self.params.context is not None:
|
|
379
|
+
values.append(ctx)
|
|
380
|
+
|
|
381
|
+
if len(params) < num_params:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"incorrect number of params. Expected {num_params}, received {len(params)}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
for i in range(num_params):
|
|
387
|
+
field_type = self.params.fields[i]
|
|
388
|
+
param_metadata = supplementary_metadata[i] if supplementary_metadata else None
|
|
389
|
+
value, err = serializer.from_string(
|
|
390
|
+
params[i], field_type, param_metadata, components,
|
|
391
|
+
)
|
|
392
|
+
if err is not None:
|
|
393
|
+
param_name = ""
|
|
394
|
+
if param_metadata is not None:
|
|
395
|
+
param_name = " " + param_metadata.name
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"error managing parameter{param_name}. {err}"
|
|
398
|
+
) from err
|
|
399
|
+
values.append(value)
|
|
400
|
+
|
|
401
|
+
return values
|
|
402
|
+
|
|
403
|
+
# ------------------------------------------------------------------
|
|
404
|
+
# Response handling
|
|
405
|
+
# ------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def _handle_response(self, response: Any,
|
|
408
|
+
returns_metadata: Optional[ReturnMetadata],
|
|
409
|
+
components: Optional[ComponentMetadata],
|
|
410
|
+
serializer) -> Tuple[str, Any, Optional[Exception]]:
|
|
411
|
+
"""Validate and serialise the function's return value.
|
|
412
|
+
|
|
413
|
+
Supports:
|
|
414
|
+
* no return -> ``("", None, None)``
|
|
415
|
+
* single success value -> ``(stringified, value, None)``
|
|
416
|
+
* Go-style ``(value, exception_or_None)`` tuple -> success + error
|
|
417
|
+
"""
|
|
418
|
+
success_response: Any = None
|
|
419
|
+
error_response: Optional[BaseException] = None
|
|
420
|
+
|
|
421
|
+
if self.returns.error and isinstance(response, tuple) and len(response) == 2:
|
|
422
|
+
success_response = response[0]
|
|
423
|
+
err = response[1]
|
|
424
|
+
if isinstance(err, BaseException):
|
|
425
|
+
error_response = err
|
|
426
|
+
elif err is not None:
|
|
427
|
+
# The "error" slot was a non-exception — treat as a success-only return.
|
|
428
|
+
success_response = response
|
|
429
|
+
else:
|
|
430
|
+
success_response = response
|
|
431
|
+
|
|
432
|
+
success_string = ""
|
|
433
|
+
iface: Any = None
|
|
434
|
+
|
|
435
|
+
if self.returns.success is not None and success_response is not None:
|
|
436
|
+
if serializer is not None:
|
|
437
|
+
s, err = serializer.to_string(
|
|
438
|
+
success_response, self.returns.success,
|
|
439
|
+
returns_metadata, components,
|
|
440
|
+
)
|
|
441
|
+
if err is not None:
|
|
442
|
+
wrapped = ValueError(
|
|
443
|
+
f"error handling success response. {err}"
|
|
444
|
+
)
|
|
445
|
+
wrapped.__cause__ = err
|
|
446
|
+
return "", None, wrapped
|
|
447
|
+
success_string = s
|
|
448
|
+
iface = success_response
|
|
449
|
+
|
|
450
|
+
return success_string, iface, error_response
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
__all__ = ["CallType", "ContractFunction"]
|