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,552 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""High-level contract chaincode.
|
|
3
|
+
|
|
4
|
+
This is the Python equivalent of Go's ``contractapi/contract_chaincode.go``.
|
|
5
|
+
It implements the :class:`fabric_shim.Chaincode` interface and dispatches
|
|
6
|
+
incoming Init/Invoke transactions to the appropriate registered contract's
|
|
7
|
+
transaction functions.
|
|
8
|
+
|
|
9
|
+
Typical usage::
|
|
10
|
+
|
|
11
|
+
from fabric_contract_api.contractapi import Contract, ContractChaincode
|
|
12
|
+
from fabric_shim import start
|
|
13
|
+
|
|
14
|
+
class MyContract(Contract):
|
|
15
|
+
Name = "MyContract"
|
|
16
|
+
|
|
17
|
+
def my_transaction(self, ctx, key: str) -> str:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
cc = ContractChaincode.new_chaincode(MyContract())
|
|
22
|
+
cc.start()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
31
|
+
|
|
32
|
+
from src.fabric_shim.interfaces import Chaincode, ChaincodeStubInterface
|
|
33
|
+
from src.fabric_shim.response import ResponseCode, success as _shim_success, error as _shim_error
|
|
34
|
+
from src.fabric_shim.logging import LOGGER
|
|
35
|
+
|
|
36
|
+
from ..internal.contract_function import CallType, ContractFunction
|
|
37
|
+
from ..internal.transaction_handler import (
|
|
38
|
+
TransactionHandler,
|
|
39
|
+
TransactionHandlerType,
|
|
40
|
+
new_transaction_handler,
|
|
41
|
+
)
|
|
42
|
+
from ..internal.utils import string_in_slice
|
|
43
|
+
from ..metadata import (
|
|
44
|
+
ComponentMetadata,
|
|
45
|
+
ContractChaincodeMetadata,
|
|
46
|
+
ContractMetadata,
|
|
47
|
+
InfoMetadata,
|
|
48
|
+
TransactionMetadata,
|
|
49
|
+
get_json_schema,
|
|
50
|
+
read_metadata_file,
|
|
51
|
+
validate_against_schema,
|
|
52
|
+
)
|
|
53
|
+
from ..serializer import JSONSerializer, TransactionSerializer
|
|
54
|
+
from .contract import (
|
|
55
|
+
Contract,
|
|
56
|
+
ContractInterface,
|
|
57
|
+
EvaluationContractInterface,
|
|
58
|
+
IgnoreContractInterface,
|
|
59
|
+
)
|
|
60
|
+
from .system_contract import SystemContract, SystemContractName
|
|
61
|
+
from .transaction_context import (
|
|
62
|
+
ClientIdentity,
|
|
63
|
+
SettableTransactionContextInterface,
|
|
64
|
+
TransactionContext,
|
|
65
|
+
TransactionContextInterface,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#: Environment variable that holds the chaincode server address.
|
|
70
|
+
SERVER_ADDRESS_VARIABLE = "CHAINCODE_SERVER_ADDRESS"
|
|
71
|
+
#: Environment variable that holds the chaincode ID name.
|
|
72
|
+
CHAINCODE_ID_VARIABLE = "CORE_CHAINCODE_ID_NAME"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Internal: per-contract metadata used by the dispatcher
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _ContractChaincodeContract:
|
|
81
|
+
"""Internal record describing one registered contract."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self.info: InfoMetadata = InfoMetadata()
|
|
85
|
+
self.functions: Dict[str, ContractFunction] = {}
|
|
86
|
+
self.unknown_transaction: Optional[TransactionHandler] = None
|
|
87
|
+
self.before_transaction: Optional[TransactionHandler] = None
|
|
88
|
+
self.after_transaction: Optional[TransactionHandler] = None
|
|
89
|
+
self.transaction_context_handler: type = TransactionContext
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# ContractChaincode
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ContractChaincode(Chaincode):
|
|
98
|
+
"""Chaincode adapter that dispatches to registered contracts."""
|
|
99
|
+
|
|
100
|
+
DefaultContract: str = ""
|
|
101
|
+
Info: InfoMetadata = InfoMetadata()
|
|
102
|
+
TransactionSerializer: TransactionSerializer = JSONSerializer()
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
# We don't call ``super().__init__`` because ``Chaincode`` is abstract
|
|
106
|
+
# and our intent is to provide concrete implementations of ``init``
|
|
107
|
+
# and ``invoke``.
|
|
108
|
+
self._contracts: Dict[str, _ContractChaincodeContract] = {}
|
|
109
|
+
self._metadata: ContractChaincodeMetadata = ContractChaincodeMetadata()
|
|
110
|
+
self._system_contract: SystemContract = SystemContract()
|
|
111
|
+
self._ci_methods: List[str] = _get_ci_methods()
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Public API
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def new_chaincode(cls, *contracts: ContractInterface) -> "ContractChaincode":
|
|
119
|
+
"""Create a new :class:`ContractChaincode` from one or more contracts.
|
|
120
|
+
|
|
121
|
+
Mirrors Go's ``NewChaincode``.
|
|
122
|
+
"""
|
|
123
|
+
cc = cls()
|
|
124
|
+
for contract in contracts:
|
|
125
|
+
additional_excludes: List[str] = []
|
|
126
|
+
# Mirror Go's duck-typing: if the contract exposes
|
|
127
|
+
# ``get_ignored_functions`` we treat it as an
|
|
128
|
+
# ``IgnoreContractInterface``.
|
|
129
|
+
ignored_fn = getattr(contract, "get_ignored_functions", None)
|
|
130
|
+
if callable(ignored_fn):
|
|
131
|
+
try:
|
|
132
|
+
additional_excludes = list(ignored_fn() or [])
|
|
133
|
+
except Exception:
|
|
134
|
+
additional_excludes = []
|
|
135
|
+
err = cc._add_contract(contract, cc._ci_methods + additional_excludes)
|
|
136
|
+
if err is not None:
|
|
137
|
+
raise err
|
|
138
|
+
|
|
139
|
+
# System contract
|
|
140
|
+
cc._system_contract = SystemContract()
|
|
141
|
+
cc._system_contract.Name = SystemContractName
|
|
142
|
+
err = cc._add_contract(cc._system_contract, cc._ci_methods)
|
|
143
|
+
if err is not None:
|
|
144
|
+
raise err
|
|
145
|
+
|
|
146
|
+
err = cc._augment_metadata()
|
|
147
|
+
if err is not None:
|
|
148
|
+
raise err
|
|
149
|
+
|
|
150
|
+
metadata_json = cc._metadata.to_json()
|
|
151
|
+
cc._system_contract._set_metadata(metadata_json)
|
|
152
|
+
cc.TransactionSerializer = JSONSerializer()
|
|
153
|
+
return cc
|
|
154
|
+
|
|
155
|
+
def start(self, cc_id: Optional[str] = None, address: Optional[str] = None,
|
|
156
|
+
key: Optional[bytes] = None, cert: Optional[bytes] = None,
|
|
157
|
+
client_ca_certs: Optional[bytes] = None) -> None:
|
|
158
|
+
"""Start the chaincode in the Fabric shim.
|
|
159
|
+
|
|
160
|
+
Reads the chaincode ID and server address from the environment when
|
|
161
|
+
not explicitly provided, mirroring Go's ``ContractChaincode.Start``.
|
|
162
|
+
"""
|
|
163
|
+
# Local import to avoid a hard dependency cycle when this module is
|
|
164
|
+
# imported eagerly by user code.
|
|
165
|
+
from src.fabric_shim.server import start as _shim_start
|
|
166
|
+
|
|
167
|
+
_shim_start(
|
|
168
|
+
cc=self,
|
|
169
|
+
cc_id=cc_id,
|
|
170
|
+
address=address,
|
|
171
|
+
key=key,
|
|
172
|
+
cert=cert,
|
|
173
|
+
client_ca_certs=client_ca_certs,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Chaincode interface
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
async def init(self, stub: ChaincodeStubInterface):
|
|
181
|
+
"""Handle the Init transaction.
|
|
182
|
+
|
|
183
|
+
Mirrors Go's ``Init``. When a function name is supplied (e.g. when
|
|
184
|
+
the chaincode container is instantiated with ``{Init, args}``), this
|
|
185
|
+
method delegates to :meth:`invoke`. Otherwise it returns a default
|
|
186
|
+
success message.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
ns_fcn, _ = _get_function_and_parameters(stub)
|
|
190
|
+
except Exception:
|
|
191
|
+
ns_fcn = ""
|
|
192
|
+
|
|
193
|
+
if not ns_fcn:
|
|
194
|
+
return _shim_success(b"Default initiator successful.")
|
|
195
|
+
return await self.invoke(stub)
|
|
196
|
+
|
|
197
|
+
async def invoke(self, stub: ChaincodeStubInterface):
|
|
198
|
+
"""Handle the Invoke transaction.
|
|
199
|
+
|
|
200
|
+
Mirrors Go's ``Invoke``.
|
|
201
|
+
"""
|
|
202
|
+
ns, fn, params = self._get_namespace_function_and_params(stub)
|
|
203
|
+
|
|
204
|
+
ns_contract = self._contracts.get(ns)
|
|
205
|
+
if ns_contract is None:
|
|
206
|
+
return _shim_error_response(f"Contract not found with name {ns}")
|
|
207
|
+
|
|
208
|
+
if not fn:
|
|
209
|
+
return _shim_error_response("Blank function name passed")
|
|
210
|
+
|
|
211
|
+
ctx = ns_contract.transaction_context_handler()
|
|
212
|
+
ctx.set_stub(stub)
|
|
213
|
+
ctx.set_client_identity(ClientIdentity(stub))
|
|
214
|
+
|
|
215
|
+
# Before-transaction
|
|
216
|
+
if ns_contract.before_transaction is not None:
|
|
217
|
+
_, _, err_res = await ns_contract.before_transaction.call(ctx, None, self.TransactionSerializer)
|
|
218
|
+
if err_res is not None:
|
|
219
|
+
return _shim_error_response(str(err_res))
|
|
220
|
+
|
|
221
|
+
# Look up the named function. Method names are case-sensitive —
|
|
222
|
+
# the contract API uses the original Python method names (typically
|
|
223
|
+
# snake_case). Callers may also use the capitalised Go-style form
|
|
224
|
+
# (``Create`` vs ``create``) for backwards-compatibility.
|
|
225
|
+
contract_fn = ns_contract.functions.get(fn) or ns_contract.functions.get(_to_first_rune_upper_case(fn))
|
|
226
|
+
success_return = ""
|
|
227
|
+
success_iface: Any = None
|
|
228
|
+
error_return: Optional[BaseException] = None
|
|
229
|
+
|
|
230
|
+
if contract_fn is None:
|
|
231
|
+
unknown = ns_contract.unknown_transaction
|
|
232
|
+
if unknown is None:
|
|
233
|
+
return _shim_error_response(
|
|
234
|
+
f"Function {fn} not found in contract {ns}"
|
|
235
|
+
)
|
|
236
|
+
success_return, success_iface, error_return = await unknown.call(
|
|
237
|
+
ctx, None, self.TransactionSerializer
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
transaction_schema: Optional[TransactionMetadata] = None
|
|
241
|
+
contract_meta = self._metadata.contracts.get(ns)
|
|
242
|
+
if contract_meta is not None:
|
|
243
|
+
for tx in contract_meta.transactions:
|
|
244
|
+
if tx.name == fn or tx.name == _to_first_rune_upper_case(fn):
|
|
245
|
+
transaction_schema = tx
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
success_return, success_iface, error_return = await contract_fn.call(
|
|
249
|
+
ctx, transaction_schema, self._metadata.components,
|
|
250
|
+
self.TransactionSerializer, *params,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if error_return is not None:
|
|
254
|
+
return _shim_error_response(str(error_return))
|
|
255
|
+
|
|
256
|
+
# After-transaction
|
|
257
|
+
if ns_contract.after_transaction is not None:
|
|
258
|
+
_, _, err_res = await ns_contract.after_transaction.call(
|
|
259
|
+
ctx, success_iface, self.TransactionSerializer
|
|
260
|
+
)
|
|
261
|
+
if err_res is not None:
|
|
262
|
+
return _shim_error_response(str(err_res))
|
|
263
|
+
|
|
264
|
+
# ``success_return`` is whatever the contract function returned
|
|
265
|
+
# after serializer conversion. It is typically a JSON string but
|
|
266
|
+
# may be raw bytes (e.g. for ``bytes`` return types). Build the
|
|
267
|
+
# Fabric Response via ``shim.success()`` so the bytes land in the
|
|
268
|
+
# ``payload`` field — the field ``peer chaincode query`` prints.
|
|
269
|
+
if isinstance(success_return, (bytes, bytearray)):
|
|
270
|
+
payload = bytes(success_return)
|
|
271
|
+
elif success_return is None:
|
|
272
|
+
payload = b""
|
|
273
|
+
else:
|
|
274
|
+
payload = str(success_return).encode("utf-8")
|
|
275
|
+
return _shim_success(payload)
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# Internal: contract registration
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def _add_contract(self, contract: ContractInterface,
|
|
282
|
+
exclude_funcs: List[str]) -> Optional[Exception]:
|
|
283
|
+
"""Register *contract* with this chaincode.
|
|
284
|
+
|
|
285
|
+
Mirrors Go's ``addContract``. Returns ``None`` on success or an
|
|
286
|
+
``Exception`` describing the failure.
|
|
287
|
+
"""
|
|
288
|
+
ns = contract.get_name() or type(contract).__name__
|
|
289
|
+
if not ns:
|
|
290
|
+
return ValueError("contract must have a name")
|
|
291
|
+
if ns in self._contracts:
|
|
292
|
+
return ValueError(
|
|
293
|
+
f"multiple contracts being merged into chaincode with name {ns}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
ccn = _ContractChaincodeContract()
|
|
297
|
+
|
|
298
|
+
# Resolve the transaction-context handler.
|
|
299
|
+
ctx_handler = contract.get_transaction_context_handler()
|
|
300
|
+
if isinstance(ctx_handler, type):
|
|
301
|
+
ccn.transaction_context_handler = ctx_handler
|
|
302
|
+
else:
|
|
303
|
+
ccn.transaction_context_handler = type(ctx_handler)
|
|
304
|
+
|
|
305
|
+
ccn.info = contract.get_info() or InfoMetadata()
|
|
306
|
+
if not ccn.info.version:
|
|
307
|
+
ccn.info.version = "latest"
|
|
308
|
+
if not ccn.info.title:
|
|
309
|
+
ccn.info.title = ns
|
|
310
|
+
|
|
311
|
+
# Before / After / Unknown transaction hooks.
|
|
312
|
+
ut = contract.get_unknown_transaction()
|
|
313
|
+
if ut is not None:
|
|
314
|
+
try:
|
|
315
|
+
ccn.unknown_transaction = new_transaction_handler(
|
|
316
|
+
ut, ccn.transaction_context_handler,
|
|
317
|
+
TransactionHandlerType.UNKNOWN,
|
|
318
|
+
)
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
return exc
|
|
321
|
+
|
|
322
|
+
bt = contract.get_before_transaction()
|
|
323
|
+
if bt is not None:
|
|
324
|
+
try:
|
|
325
|
+
ccn.before_transaction = new_transaction_handler(
|
|
326
|
+
bt, ccn.transaction_context_handler,
|
|
327
|
+
TransactionHandlerType.BEFORE,
|
|
328
|
+
)
|
|
329
|
+
except Exception as exc:
|
|
330
|
+
return exc
|
|
331
|
+
|
|
332
|
+
at = contract.get_after_transaction()
|
|
333
|
+
if at is not None:
|
|
334
|
+
try:
|
|
335
|
+
ccn.after_transaction = new_transaction_handler(
|
|
336
|
+
at, ccn.transaction_context_handler,
|
|
337
|
+
TransactionHandlerType.AFTER,
|
|
338
|
+
)
|
|
339
|
+
except Exception as exc:
|
|
340
|
+
return exc
|
|
341
|
+
|
|
342
|
+
evaluate_methods: List[str] = []
|
|
343
|
+
# Mirror Go's duck-typing: if the contract exposes
|
|
344
|
+
# ``get_evaluate_transactions`` we treat it as an
|
|
345
|
+
# ``EvaluationContractInterface``.
|
|
346
|
+
evaluate_fn = getattr(contract, "get_evaluate_transactions", None)
|
|
347
|
+
if callable(evaluate_fn):
|
|
348
|
+
try:
|
|
349
|
+
evaluate_methods = list(evaluate_fn() or [])
|
|
350
|
+
except Exception:
|
|
351
|
+
evaluate_methods = []
|
|
352
|
+
|
|
353
|
+
# Walk every public method on the contract instance.
|
|
354
|
+
for method_name in dir(contract):
|
|
355
|
+
if method_name.startswith("_"):
|
|
356
|
+
continue
|
|
357
|
+
if string_in_slice(method_name, exclude_funcs):
|
|
358
|
+
continue
|
|
359
|
+
attr = getattr(contract, method_name, None)
|
|
360
|
+
if not callable(attr):
|
|
361
|
+
continue
|
|
362
|
+
# Skip callables that came from the Contract base class itself
|
|
363
|
+
# (they're already covered by _ci_methods).
|
|
364
|
+
if _is_contract_interface_method(method_name):
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
call_type = CallType.EVALUATE if string_in_slice(method_name, evaluate_methods) else CallType.SUBMIT
|
|
368
|
+
try:
|
|
369
|
+
ccn.functions[method_name] = ContractFunction.from_callable(
|
|
370
|
+
attr, call_type, ccn.transaction_context_handler,
|
|
371
|
+
)
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
wrapped = ValueError(
|
|
374
|
+
f"error registering method {method_name} on contract {ns}: {exc}"
|
|
375
|
+
)
|
|
376
|
+
wrapped.__cause__ = exc
|
|
377
|
+
return wrapped
|
|
378
|
+
|
|
379
|
+
if not ccn.functions:
|
|
380
|
+
return ValueError(
|
|
381
|
+
f"contracts are required to have at least 1 (non-ignored) public method. "
|
|
382
|
+
f"Contract {ns} has none. Method names that have been ignored: "
|
|
383
|
+
f"{', '.join(exclude_funcs) or '(none)'}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
self._contracts[ns] = ccn
|
|
387
|
+
if not self.DefaultContract:
|
|
388
|
+
self.DefaultContract = ns
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
# Internal: metadata
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def _reflect_metadata(self) -> ContractChaincodeMetadata:
|
|
396
|
+
reflected = ContractChaincodeMetadata()
|
|
397
|
+
reflected.contracts = {}
|
|
398
|
+
reflected.components = ComponentMetadata()
|
|
399
|
+
reflected.info = self.Info
|
|
400
|
+
|
|
401
|
+
# Make a defensive copy so we don't mutate ``self.Info``.
|
|
402
|
+
if reflected.info is not None:
|
|
403
|
+
reflected.info = InfoMetadata(
|
|
404
|
+
description=reflected.info.description,
|
|
405
|
+
title=reflected.info.title,
|
|
406
|
+
contact=reflected.info.contact,
|
|
407
|
+
license=reflected.info.license,
|
|
408
|
+
version=reflected.info.version,
|
|
409
|
+
terms_of_service=reflected.info.terms_of_service,
|
|
410
|
+
)
|
|
411
|
+
if not reflected.info.version:
|
|
412
|
+
reflected.info.version = "latest"
|
|
413
|
+
if not reflected.info.title:
|
|
414
|
+
reflected.info.title = "undefined"
|
|
415
|
+
|
|
416
|
+
for key, contract in self._contracts.items():
|
|
417
|
+
contract_meta = ContractMetadata()
|
|
418
|
+
contract_meta.name = key
|
|
419
|
+
contract_meta.info = contract.info
|
|
420
|
+
if self.DefaultContract == key:
|
|
421
|
+
contract_meta.default = True
|
|
422
|
+
|
|
423
|
+
for fn_name, fn in contract.functions.items():
|
|
424
|
+
fn_meta = fn.reflect_metadata(fn_name, reflected.components)
|
|
425
|
+
contract_meta.transactions.append(fn_meta)
|
|
426
|
+
|
|
427
|
+
contract_meta.transactions.sort(key=lambda t: t.name)
|
|
428
|
+
reflected.contracts[key] = contract_meta
|
|
429
|
+
|
|
430
|
+
return reflected
|
|
431
|
+
|
|
432
|
+
def _augment_metadata(self) -> Optional[Exception]:
|
|
433
|
+
"""Merge reflected metadata with the optional user-supplied file."""
|
|
434
|
+
try:
|
|
435
|
+
file_metadata = read_metadata_file()
|
|
436
|
+
file_not_found = False
|
|
437
|
+
except FileNotFoundError:
|
|
438
|
+
file_metadata = ContractChaincodeMetadata()
|
|
439
|
+
file_not_found = True
|
|
440
|
+
|
|
441
|
+
reflected_metadata = self._reflect_metadata()
|
|
442
|
+
file_metadata.append(reflected_metadata)
|
|
443
|
+
err = file_metadata.compile_schemas()
|
|
444
|
+
if err is not None:
|
|
445
|
+
return err
|
|
446
|
+
err = validate_against_schema(file_metadata)
|
|
447
|
+
if err is not None:
|
|
448
|
+
return err
|
|
449
|
+
self._metadata = file_metadata
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
# Internal: routing
|
|
454
|
+
# ------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
def _get_namespace_function_and_params(self, stub: ChaincodeStubInterface) -> Tuple[str, str, List[str]]:
|
|
457
|
+
ns_fcn, params = _get_function_and_parameters(stub)
|
|
458
|
+
# Split on the last ':' so that contract names with embedded colons
|
|
459
|
+
# are still handled.
|
|
460
|
+
idx = ns_fcn.rfind(":")
|
|
461
|
+
if idx == -1:
|
|
462
|
+
return self.DefaultContract, ns_fcn, params
|
|
463
|
+
return ns_fcn[:idx], ns_fcn[idx + 1:], params
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
# Helpers
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _get_ci_methods() -> List[str]:
|
|
472
|
+
"""Return the names of every method defined on the contract interfaces."""
|
|
473
|
+
methods: List[str] = []
|
|
474
|
+
seen = set()
|
|
475
|
+
for iface in (ContractInterface, IgnoreContractInterface, EvaluationContractInterface):
|
|
476
|
+
for name in getattr(iface, "__abstractmethods__", frozenset()):
|
|
477
|
+
if name not in seen:
|
|
478
|
+
methods.append(name)
|
|
479
|
+
seen.add(name)
|
|
480
|
+
# Also pick up non-abstract public methods on the base classes
|
|
481
|
+
# (e.g. Contract.get_info is a concrete implementation).
|
|
482
|
+
for name in dir(iface):
|
|
483
|
+
if name.startswith("_"):
|
|
484
|
+
continue
|
|
485
|
+
if name in seen:
|
|
486
|
+
continue
|
|
487
|
+
attr = getattr(iface, name, None)
|
|
488
|
+
if callable(attr):
|
|
489
|
+
methods.append(name)
|
|
490
|
+
seen.add(name)
|
|
491
|
+
# The Contract base class also has the public attributes (Name, Info,
|
|
492
|
+
# ...) — but those aren't methods, so we only need to consider callables.
|
|
493
|
+
for name in dir(Contract):
|
|
494
|
+
if name.startswith("_"):
|
|
495
|
+
continue
|
|
496
|
+
if name in seen:
|
|
497
|
+
continue
|
|
498
|
+
attr = getattr(Contract, name, None)
|
|
499
|
+
if callable(attr):
|
|
500
|
+
methods.append(name)
|
|
501
|
+
seen.add(name)
|
|
502
|
+
return methods
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _is_contract_interface_method(name: str) -> bool:
|
|
506
|
+
"""Return ``True`` if *name* is a method defined on a contract interface."""
|
|
507
|
+
for iface in (ContractInterface, IgnoreContractInterface, EvaluationContractInterface, Contract):
|
|
508
|
+
if name in dir(iface):
|
|
509
|
+
return True
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _to_first_rune_upper_case(text: str) -> str:
|
|
514
|
+
"""Capitalize the first character of *text*.
|
|
515
|
+
|
|
516
|
+
Mirrors Go's ``toFirstRuneUpperCase``. Used so that callers can refer to
|
|
517
|
+
a transaction by either ``"myTransaction"`` or ``"mytransaction"`` —
|
|
518
|
+
Python convention is typically lower-case but we want to be lenient.
|
|
519
|
+
"""
|
|
520
|
+
if not text:
|
|
521
|
+
return text
|
|
522
|
+
if text[0].isupper():
|
|
523
|
+
return text
|
|
524
|
+
return text[0].upper() + text[1:]
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _get_function_and_parameters(stub: ChaincodeStubInterface) -> Tuple[str, List[str]]:
|
|
528
|
+
"""Return ``(function_name, params)`` from the chaincode stub.
|
|
529
|
+
|
|
530
|
+
The shim already provides ``get_function_and_parameters`` but raises when
|
|
531
|
+
called without arguments — we wrap it to make it safer.
|
|
532
|
+
"""
|
|
533
|
+
try:
|
|
534
|
+
fcn, params = stub.get_function_and_parameters()
|
|
535
|
+
return fcn, list(params)
|
|
536
|
+
except Exception:
|
|
537
|
+
return "", []
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _shim_error_response(msg: str):
|
|
541
|
+
"""Build a ``shim.Error()``-equivalent response.
|
|
542
|
+
|
|
543
|
+
The Response's ``message`` field is a protobuf ``string`` — the peer
|
|
544
|
+
prints it to stderr when ``status >= 400``. We pass the string
|
|
545
|
+
directly (no UTF-8 encoding) so the peer shows the error message
|
|
546
|
+
verbatim.
|
|
547
|
+
"""
|
|
548
|
+
from fabric_protos.peer import proposal_response_pb2 as pb
|
|
549
|
+
return pb.Response(status=ResponseCode.ERROR, message=str(msg))
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
__all__ = ["ContractChaincode"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""System contract added to every chaincode.
|
|
3
|
+
|
|
4
|
+
This is the Python equivalent of Go's ``contractapi/system_contract.go``.
|
|
5
|
+
The system contract exposes a single ``GetMetadata`` transaction that
|
|
6
|
+
returns the JSON-formatted metadata of the chaincode it belongs to.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from .contract import Contract
|
|
14
|
+
|
|
15
|
+
#: The well-known name of the system contract.
|
|
16
|
+
SystemContractName = "org.hyperledger.fabric"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SystemContract(Contract):
|
|
20
|
+
"""Built-in contract that exposes chaincode metadata."""
|
|
21
|
+
|
|
22
|
+
Name: str = SystemContractName
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._metadata: str = ""
|
|
27
|
+
|
|
28
|
+
def _set_metadata(self, metadata: str) -> None:
|
|
29
|
+
"""Inject the metadata JSON into the system contract.
|
|
30
|
+
|
|
31
|
+
Called by :class:`ContractChaincode` once the metadata has been
|
|
32
|
+
compiled from the registered contracts.
|
|
33
|
+
"""
|
|
34
|
+
self._metadata = metadata
|
|
35
|
+
|
|
36
|
+
def get_metadata(self) -> str:
|
|
37
|
+
"""Return the JSON metadata of the chaincode this contract belongs to."""
|
|
38
|
+
return self._metadata
|
|
39
|
+
|
|
40
|
+
def get_evaluate_transactions(self) -> List[str]:
|
|
41
|
+
"""Return the names of the transactions that should be evaluated."""
|
|
42
|
+
return ["get_metadata"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["SystemContract", "SystemContractName"]
|