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,91 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import asyncio
|
|
3
|
+
from src.fabric_shim.logging import LOGGER
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QueueMessage:
|
|
7
|
+
def __init__(self, msg, method, future: asyncio.Future) -> None:
|
|
8
|
+
self.msg = msg
|
|
9
|
+
self.method = method
|
|
10
|
+
self.future = future
|
|
11
|
+
|
|
12
|
+
def get_msg(self):
|
|
13
|
+
return self.msg
|
|
14
|
+
|
|
15
|
+
def get_msg_txContextId(self):
|
|
16
|
+
return self.msg.channel_id + self.msg.txid
|
|
17
|
+
|
|
18
|
+
def get_method(self):
|
|
19
|
+
return self.method
|
|
20
|
+
|
|
21
|
+
def success(self, response):
|
|
22
|
+
self.future.set_result(response)
|
|
23
|
+
|
|
24
|
+
def fail(self, err):
|
|
25
|
+
self.future.set_exception(Exception(err))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MsgQueueHandler:
|
|
29
|
+
"""This class handles queuing messages to be sent to the peer based on transaction id"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, handler) -> None:
|
|
32
|
+
self.handler = handler
|
|
33
|
+
self.tx_queues = {}
|
|
34
|
+
self.context = handler.context
|
|
35
|
+
|
|
36
|
+
async def queue_msg(self, msg: QueueMessage):
|
|
37
|
+
"""Queue a message to be sent to the peer"""
|
|
38
|
+
tx_context_id = msg.get_msg_txContextId()
|
|
39
|
+
msg_queue = self.tx_queues.setdefault(tx_context_id, [])
|
|
40
|
+
msg_queue.append(msg)
|
|
41
|
+
if len(msg_queue) == 1:
|
|
42
|
+
await self.__send_msg(tx_context_id)
|
|
43
|
+
|
|
44
|
+
async def __send_msg(self, tx_context_id):
|
|
45
|
+
"""send the current message to the peer"""
|
|
46
|
+
msg: QueueMessage = self.__get_current_msg(tx_context_id)
|
|
47
|
+
if msg:
|
|
48
|
+
try:
|
|
49
|
+
await self.context.write(msg.get_msg())
|
|
50
|
+
except Exception as e:
|
|
51
|
+
msg.fail(e)
|
|
52
|
+
|
|
53
|
+
async def handle_msg_response(self, response):
|
|
54
|
+
tx_id = response.txid
|
|
55
|
+
channel_id = response.channel_id
|
|
56
|
+
tx_context_id = channel_id + tx_id
|
|
57
|
+
msg: QueueMessage = self.__get_current_msg(tx_context_id)
|
|
58
|
+
|
|
59
|
+
if msg:
|
|
60
|
+
try:
|
|
61
|
+
# parsed_response = parse_response(self.handler, response, msg.get_method())
|
|
62
|
+
msg.success(response)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
msg.fail(e)
|
|
65
|
+
await self.__remove_current_and_send_next(tx_context_id)
|
|
66
|
+
else:
|
|
67
|
+
LOGGER.warning('No queued message found for tx_context_id %s', tx_context_id)
|
|
68
|
+
|
|
69
|
+
def __get_current_msg(self, tx_context_id):
|
|
70
|
+
"""returns the message at the top of the queue for the particular transaction"""
|
|
71
|
+
msg_queue = self.tx_queues.get(tx_context_id)
|
|
72
|
+
if msg_queue:
|
|
73
|
+
return msg_queue[0]
|
|
74
|
+
|
|
75
|
+
LOGGER.debug('Failed to find a message for transaction context id %s', tx_context_id)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
async def __remove_current_and_send_next(self, tx_context_id):
|
|
79
|
+
"""Remove the current message and send the next message in the queue if there is one"""
|
|
80
|
+
msg_queue = self.tx_queues.get(tx_context_id)
|
|
81
|
+
if msg_queue and len(msg_queue) > 0:
|
|
82
|
+
msg_queue.pop(0)
|
|
83
|
+
if len(msg_queue) != 0:
|
|
84
|
+
await self.__send_msg(tx_context_id)
|
|
85
|
+
else:
|
|
86
|
+
# remove empty queue to free memory
|
|
87
|
+
try:
|
|
88
|
+
del self.tx_queues[tx_context_id]
|
|
89
|
+
except KeyError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
3
|
+
from fabric_protos.peer import proposal_response_pb2 as pb
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResponseCode:
|
|
7
|
+
"""
|
|
8
|
+
OK constant - status code less than 400, endorser will endorse it.
|
|
9
|
+
OK means init or invoke successfully.
|
|
10
|
+
|
|
11
|
+
ERRORTHRESHOLD constant - status code greater than or equal to 400 will be considered an error and rejected by
|
|
12
|
+
endorser.
|
|
13
|
+
|
|
14
|
+
ERROR constant - default error value
|
|
15
|
+
"""
|
|
16
|
+
OK, ERRORTHRESHOLD, ERROR = 200, 400, 500
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def new_error_msg(msg, state) -> ccshim_pb2.ChaincodeMessage:
|
|
20
|
+
err_str = f"[{msg.txid}] Chaincode h cannot handle message ({msg.type}) while in state: {state}"
|
|
21
|
+
return ccshim_pb2.ChaincodeMessage(type=ccshim_pb2.ChaincodeMessage.ERROR,
|
|
22
|
+
payload=err_str.encode(encoding='utf-8'), txid=msg.txid)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def success(payload):
|
|
26
|
+
"""Build a successful Fabric Response.
|
|
27
|
+
|
|
28
|
+
Puts *payload* in the ``payload`` (bytes) field of the Response — this
|
|
29
|
+
is the field that ``peer chaincode query`` / ``invoke`` reads to print
|
|
30
|
+
the chaincode result. ``payload`` may be ``bytes``, ``str`` (will be
|
|
31
|
+
UTF-8 encoded) or ``None`` (treated as an empty payload).
|
|
32
|
+
"""
|
|
33
|
+
if payload is None:
|
|
34
|
+
payload = b""
|
|
35
|
+
if isinstance(payload, str):
|
|
36
|
+
payload = payload.encode("utf-8")
|
|
37
|
+
elif isinstance(payload, bytearray):
|
|
38
|
+
payload = bytes(payload)
|
|
39
|
+
return pb.Response(status=ResponseCode.OK, payload=payload)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def error(message: str = ""):
|
|
43
|
+
"""Build an error Fabric Response.
|
|
44
|
+
|
|
45
|
+
Puts *message* in the ``message`` (string) field of the Response — this
|
|
46
|
+
is what ``peer chaincode query`` / ``invoke`` prints to stderr when
|
|
47
|
+
the chaincode returns an error.
|
|
48
|
+
"""
|
|
49
|
+
return pb.Response(status=ResponseCode.ERROR, message=message)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from typing import AsyncIterable, Type
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
import queue
|
|
10
|
+
|
|
11
|
+
from src.fabric_shim.handler import Handler
|
|
12
|
+
from src.fabric_shim.interfaces import Chaincode
|
|
13
|
+
from src.fabric_shim.logging import LOGGER
|
|
14
|
+
from fabric_protos.peer import chaincode_shim_pb2_grpc as ccshim_grpc_pb2
|
|
15
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
16
|
+
|
|
17
|
+
# Coroutines to be invoked when the event loop is shutting down.
|
|
18
|
+
_cleanup_coroutines = []
|
|
19
|
+
|
|
20
|
+
send_queue = queue.SimpleQueue()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChaincodeService(ccshim_grpc_pb2.ChaincodeServicer):
|
|
24
|
+
"""Chaincode as a server - peer establishes a connection to the chaincode as a client
|
|
25
|
+
Currently only supports a stream connection.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, chaincode_id: str, chaincode: Chaincode):
|
|
29
|
+
self._ccid = chaincode_id
|
|
30
|
+
self._cc = chaincode
|
|
31
|
+
|
|
32
|
+
async def Connect(self, request_iterator: AsyncIterable[ccshim_pb2.ChaincodeMessage],
|
|
33
|
+
context: grpc.aio.ServicerContext) -> None: # Iterable[ccshim_pb2.ChaincodeMessage]:
|
|
34
|
+
try:
|
|
35
|
+
handler = Handler(self._ccid, self._cc)
|
|
36
|
+
await handler.chat_with_peer(request_iterator, context)
|
|
37
|
+
except asyncio.CancelledError:
|
|
38
|
+
LOGGER.info("Cancelling RPC due to exhausted resources.")
|
|
39
|
+
# context.abort()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_tls_config(key: bytes = None, cert: bytes = None, client_ca_certs: bytes = None) -> grpc.ServerCredentials:
|
|
43
|
+
"""
|
|
44
|
+
load_tls_config loads the TLS configuration for the chaincode
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A grpc.ServerCredentials
|
|
48
|
+
"""
|
|
49
|
+
# Accept either file paths (str) or raw bytes. Normalize to bytes.
|
|
50
|
+
def _read_bytes(v):
|
|
51
|
+
if v is None:
|
|
52
|
+
return None
|
|
53
|
+
if isinstance(v, (bytes, bytearray)):
|
|
54
|
+
return bytes(v)
|
|
55
|
+
if isinstance(v, str):
|
|
56
|
+
try:
|
|
57
|
+
with open(v, 'rb') as fh:
|
|
58
|
+
return fh.read()
|
|
59
|
+
except Exception:
|
|
60
|
+
return v.encode()
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
key_b = _read_bytes(key)
|
|
64
|
+
cert_b = _read_bytes(cert)
|
|
65
|
+
ca_b = _read_bytes(client_ca_certs)
|
|
66
|
+
|
|
67
|
+
client_auth = True if ca_b else False
|
|
68
|
+
# Loading credentials
|
|
69
|
+
return grpc.ssl_server_credentials(((key_b, cert_b,),), ca_b, client_auth)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _internal_server(**kwargs) -> grpc.aio.Server:
|
|
73
|
+
server = grpc.aio.server()
|
|
74
|
+
|
|
75
|
+
key = kwargs.pop("key", os.getenv('CORE_TLS_CLIENT_KEY_PATH'))
|
|
76
|
+
cert = kwargs.pop("cert", os.getenv('CORE_TLS_CLIENT_CERT_PATH'))
|
|
77
|
+
address = kwargs.get("address")
|
|
78
|
+
|
|
79
|
+
if not key or not cert:
|
|
80
|
+
port = server.add_insecure_port(address)
|
|
81
|
+
else:
|
|
82
|
+
client_ca_certs = kwargs.pop("client_ca_certs", os.getenv('CORE_PEER_TLS_ROOTCERT_FILE'))
|
|
83
|
+
server_credentials = load_tls_config(key, cert, client_ca_certs)
|
|
84
|
+
# Pass down credentials
|
|
85
|
+
port = server.add_secure_port(address, server_credentials)
|
|
86
|
+
|
|
87
|
+
ccid = kwargs.get("ccid")
|
|
88
|
+
cc = kwargs.pop("cc")
|
|
89
|
+
|
|
90
|
+
ccshim_grpc_pb2.add_ChaincodeServicer_to_server(ChaincodeService(ccid, cc), server)
|
|
91
|
+
logging.info('Server is listening at port :%d', port)
|
|
92
|
+
return server
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _internal_start(server: grpc.aio.Server) -> None:
|
|
96
|
+
await server.start()
|
|
97
|
+
|
|
98
|
+
async def server_graceful_shutdown():
|
|
99
|
+
logging.info("Starting graceful shutdown...")
|
|
100
|
+
# Shuts down the server with 3 seconds of grace period. During the
|
|
101
|
+
# grace period, the server won't accept new connections and allow
|
|
102
|
+
# existing RPCs to continue within the grace period.
|
|
103
|
+
await server.stop(3)
|
|
104
|
+
|
|
105
|
+
_cleanup_coroutines.append(server_graceful_shutdown())
|
|
106
|
+
await server.wait_for_termination()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def start(cc: Type[Chaincode],
|
|
110
|
+
cc_id: str = None,
|
|
111
|
+
address: str = None,
|
|
112
|
+
key: bytes = None,
|
|
113
|
+
cert: bytes = None,
|
|
114
|
+
client_ca_certs: bytes = None):
|
|
115
|
+
"""
|
|
116
|
+
start the server
|
|
117
|
+
|
|
118
|
+
address Is the listen address of the chaincode server.
|
|
119
|
+
key TLS Private key passed to chaincode server.
|
|
120
|
+
cert TLS Certificate passed to chaincode server. Note
|
|
121
|
+
that this argument is compatible with 'key' - if some
|
|
122
|
+
are missing, 'TLS disabled'.
|
|
123
|
+
client_ca_certs Set if connecting peer should be verified.
|
|
124
|
+
"""
|
|
125
|
+
cc_id = os.getenv('CHAINCODE_ID', cc_id)
|
|
126
|
+
address = os.getenv('CHAINCODE_SERVER_ADDRESS', address)
|
|
127
|
+
if not cc_id:
|
|
128
|
+
raise Exception("cc_id must be specified")
|
|
129
|
+
if not address:
|
|
130
|
+
raise Exception("address must be specified")
|
|
131
|
+
|
|
132
|
+
if cc is None:
|
|
133
|
+
raise Exception("chaincode must be specified")
|
|
134
|
+
|
|
135
|
+
# Accept either a Chaincode subclass (class) or an instance implementing Chaincode
|
|
136
|
+
if isinstance(cc, type):
|
|
137
|
+
if not issubclass(cc, Chaincode):
|
|
138
|
+
raise Exception("chaincode must subclass Chaincode")
|
|
139
|
+
# instantiate the chaincode class
|
|
140
|
+
try:
|
|
141
|
+
cc = cc()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise Exception(f"failed to instantiate chaincode class: {e}")
|
|
144
|
+
else:
|
|
145
|
+
if not isinstance(cc, Chaincode):
|
|
146
|
+
raise Exception("chaincode must be an instance of Chaincode")
|
|
147
|
+
|
|
148
|
+
server = _internal_server(ccid=cc_id, address=address, cc=cc, key=key, cert=cert, client_ca_certs=client_ca_certs)
|
|
149
|
+
loop = asyncio.get_event_loop()
|
|
150
|
+
try:
|
|
151
|
+
loop.run_until_complete(_internal_start(server))
|
|
152
|
+
finally:
|
|
153
|
+
if _cleanup_coroutines:
|
|
154
|
+
loop.run_until_complete(asyncio.gather(*_cleanup_coroutines))
|
|
155
|
+
loop.close()
|
src/fabric_shim/stub.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from src.fabric_shim.interfaces import ChaincodeStubInterface
|
|
3
|
+
from src.fabric_shim.utils import (
|
|
4
|
+
COMPOSITEKEY_NS,
|
|
5
|
+
MIN_UNICODE_RUNE_VALUE,
|
|
6
|
+
validate_composite_key_attribute,
|
|
7
|
+
)
|
|
8
|
+
from fabric_protos.peer import chaincode_pb2 as pb
|
|
9
|
+
from fabric_protos.common import common_pb2 as cm_pb
|
|
10
|
+
from fabric_protos.peer import proposal_pb2 as pr_pb
|
|
11
|
+
from fabric_protos.msp import identities_pb2 as id_pb
|
|
12
|
+
from fabric_protos.peer import chaincode_event_pb2 as e_pb
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from src.fabric_shim.logging import LOGGER
|
|
15
|
+
|
|
16
|
+
VALIDATION_PARAMETER: str = 'VALIDATION_PARAMETER'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChaincodeStub(ChaincodeStubInterface):
|
|
20
|
+
"""The stub encapsulates the APIs between the chaincode implementation and the Fabric peer"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client, channel_id, tx_id, cc_input, signed_proposal_pb):
|
|
23
|
+
self.client = client
|
|
24
|
+
self.channel_id = channel_id
|
|
25
|
+
self.tx_id = tx_id
|
|
26
|
+
self.cc_input = cc_input
|
|
27
|
+
self.signed_proposal_pb = signed_proposal_pb
|
|
28
|
+
self.creator = {}
|
|
29
|
+
self.tx_timestamp = None
|
|
30
|
+
self.proposal = None
|
|
31
|
+
self.validationParameterMetakey = VALIDATION_PARAMETER
|
|
32
|
+
|
|
33
|
+
if self.signed_proposal_pb:
|
|
34
|
+
# Only attempt to decode the signed proposal if it contains proposal_bytes
|
|
35
|
+
proposal_bytes = getattr(self.signed_proposal_pb, 'proposal_bytes', None)
|
|
36
|
+
if proposal_bytes:
|
|
37
|
+
decoded_sp = {
|
|
38
|
+
'signature': getattr(self.signed_proposal_pb, 'signature', None)
|
|
39
|
+
}
|
|
40
|
+
proposal = pr_pb.Proposal.FromString(proposal_bytes)
|
|
41
|
+
decoded_sp['proposal'] = {}
|
|
42
|
+
self.proposal = proposal
|
|
43
|
+
|
|
44
|
+
if not proposal.header or len(proposal.header) == 0:
|
|
45
|
+
raise Exception('Proposal header is empty')
|
|
46
|
+
|
|
47
|
+
if not proposal.payload or len(proposal.payload) == 0:
|
|
48
|
+
raise Exception('Proposal payload is empty')
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
header = cm_pb.Header.FromString(proposal.header)
|
|
52
|
+
# Use a dict for the decoded header fields rather than assigning
|
|
53
|
+
# the protobuf Header object directly (protobuf objects don't
|
|
54
|
+
# support item assignment). Keep the parsed `header` protobuf
|
|
55
|
+
# in a local variable for further extraction.
|
|
56
|
+
decoded_sp['proposal']['header'] = {}
|
|
57
|
+
decoded_sp['proposal']['header']['_raw_header'] = header
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise Exception('Could not extract the header from the proposal: ' + str(e))
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
signature_header = cm_pb.SignatureHeader.FromString(header.signature_header)
|
|
63
|
+
decoded_sp['proposal']['header']['signatureHeader'] = \
|
|
64
|
+
{'nonce': signature_header.nonce, 'creator_u8': signature_header.creator}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise Exception('Decoding SignatureHeader failed: ' + str(e))
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
creator = id_pb.SerializedIdentity.FromString(signature_header.creator)
|
|
70
|
+
decoded_sp['proposal']['header']['signatureHeader']['creator'] = creator
|
|
71
|
+
self.creator = {'mspid': creator.mspid, 'idBytes': creator.id_bytes}
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise Exception('Decoding SerializedIdentity failed: ' + str(e))
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
channel_header = cm_pb.ChannelHeader.FromString(header.channel_header)
|
|
77
|
+
decoded_sp['proposal']['header']['channelHeader'] = channel_header
|
|
78
|
+
self.tx_timestamp = channel_header.timestamp
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise Exception('Decoding ChannelHeader failed: ' + str(e))
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
ccpp = pr_pb.ChaincodeProposalPayload.FromString(proposal.payload)
|
|
84
|
+
decoded_sp['proposal']['payload'] = ccpp
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise Exception('Decoding ChaincodeProposalPayload failed: %s' + str(e))
|
|
87
|
+
|
|
88
|
+
self.signed_proposal_pb = decoded_sp
|
|
89
|
+
else:
|
|
90
|
+
# No proposal bytes provided; treat as unsigned/no proposal for tests
|
|
91
|
+
self.signed_proposal_pb = None
|
|
92
|
+
|
|
93
|
+
def get_channel_id(self):
|
|
94
|
+
"""Get the channel ID of the chaincode calling transaction"""
|
|
95
|
+
return self.channel_id
|
|
96
|
+
|
|
97
|
+
def get_tx_timestamp(self):
|
|
98
|
+
"""Get the timestamp of the chaincode calling transaction"""
|
|
99
|
+
return self.tx_timestamp
|
|
100
|
+
|
|
101
|
+
def get_creator(self):
|
|
102
|
+
"""Get the user ID of the chaincode calling transaction"""
|
|
103
|
+
return self.creator
|
|
104
|
+
|
|
105
|
+
def get_txid(self):
|
|
106
|
+
"""Get the ID of the chaincode calling transaction"""
|
|
107
|
+
return self.tx_id
|
|
108
|
+
|
|
109
|
+
def get_function_and_parameters(self):
|
|
110
|
+
"""Get function name and parameters of the chaincode calling transaction"""
|
|
111
|
+
args = [arg.decode() for arg in self.cc_input.args]
|
|
112
|
+
if len(args) == 0:
|
|
113
|
+
raise Exception('no function name provided in transaction payload')
|
|
114
|
+
function: str = args[0]
|
|
115
|
+
params = args[1:]
|
|
116
|
+
|
|
117
|
+
return function, params
|
|
118
|
+
|
|
119
|
+
async def get_state(self, key: str): #-> bytearray:
|
|
120
|
+
"""Get asset state from ledger"""
|
|
121
|
+
LOGGER.info('get_state called with key:%s' % key)
|
|
122
|
+
# Access public data by setting the collection to empty string
|
|
123
|
+
collection = ''
|
|
124
|
+
return await self.client.handle_get_state(collection, key, self.channel_id, self.tx_id)
|
|
125
|
+
|
|
126
|
+
async def put_state(self, key: str, value):
|
|
127
|
+
"""Put asset state to ledger"""
|
|
128
|
+
LOGGER.info('put_state called with key:%s and value:%s' % (key, value))
|
|
129
|
+
# Access public data by setting the collection to empty string
|
|
130
|
+
collection = ''
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
value = bytes(value.encode())
|
|
133
|
+
return await self.client.handle_put_state(collection, key, value, self.channel_id, self.tx_id)
|
|
134
|
+
|
|
135
|
+
async def delete_state(self, key: str):
|
|
136
|
+
"""Delete asset state from ledger"""
|
|
137
|
+
LOGGER.info('delete_state called with key:%s' % key)
|
|
138
|
+
# Access public data by setting the collection to empty string
|
|
139
|
+
collection = ''
|
|
140
|
+
return await self.client.handle_delete_state(collection, key, self.channel_id, self.tx_id)
|
|
141
|
+
|
|
142
|
+
async def get_state_by_range(self, start_key: str, end_key: str):
|
|
143
|
+
"""Returns a range of keys from the ledger.
|
|
144
|
+
|
|
145
|
+
Returns an async iterator yielding :class:`KV` records (with
|
|
146
|
+
``key`` and ``value`` byte fields). Both keys may be empty strings
|
|
147
|
+
to mean "unbounded".
|
|
148
|
+
"""
|
|
149
|
+
collection = ''
|
|
150
|
+
from src.fabric_shim.iterators import StateQueryIterator
|
|
151
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
152
|
+
from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
|
|
153
|
+
|
|
154
|
+
raw = await self.client.handle_get_state_by_range(
|
|
155
|
+
collection, start_key, end_key,
|
|
156
|
+
self.channel_id, self.tx_id,
|
|
157
|
+
)
|
|
158
|
+
return StateQueryIterator(
|
|
159
|
+
handler=self.client,
|
|
160
|
+
channel_id=self.channel_id,
|
|
161
|
+
tx_id=self.tx_id,
|
|
162
|
+
initial_response=raw,
|
|
163
|
+
response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
|
|
164
|
+
parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def get_state_by_range_with_pagination(self, start_key: str, end_key: str,
|
|
168
|
+
page_size: int, bookmark: str = ""):
|
|
169
|
+
"""Paginated version of :meth:`get_state_by_range`.
|
|
170
|
+
|
|
171
|
+
Returns an async iterator yielding :class:`KV` records. Use the
|
|
172
|
+
:meth:`aclose` method of the iterator when you want to stop early.
|
|
173
|
+
"""
|
|
174
|
+
collection = ''
|
|
175
|
+
from src.fabric_shim.iterators import StateQueryIterator
|
|
176
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
177
|
+
from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
|
|
178
|
+
|
|
179
|
+
meta = ccshim_pb2.QueryMetadata()
|
|
180
|
+
meta.pageSize = int(page_size)
|
|
181
|
+
meta.bookmark = bookmark or ""
|
|
182
|
+
raw = await self.client.handle_get_state_by_range(
|
|
183
|
+
collection, start_key, end_key,
|
|
184
|
+
self.channel_id, self.tx_id, metadata=meta.SerializeToString(),
|
|
185
|
+
)
|
|
186
|
+
return StateQueryIterator(
|
|
187
|
+
handler=self.client,
|
|
188
|
+
channel_id=self.channel_id,
|
|
189
|
+
tx_id=self.tx_id,
|
|
190
|
+
initial_response=raw,
|
|
191
|
+
response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
|
|
192
|
+
parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def get_query_result(self, query: str):
|
|
196
|
+
"""Run a CouchDB rich query against the world state.
|
|
197
|
+
|
|
198
|
+
Returns an async iterator yielding :class:`KV` records.
|
|
199
|
+
"""
|
|
200
|
+
collection = ''
|
|
201
|
+
from src.fabric_shim.iterators import QueryResultIterator
|
|
202
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
203
|
+
from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
|
|
204
|
+
|
|
205
|
+
raw = await self.client.handle_get_query_result(
|
|
206
|
+
collection, query, self.channel_id, self.tx_id,
|
|
207
|
+
)
|
|
208
|
+
return QueryResultIterator(
|
|
209
|
+
handler=self.client,
|
|
210
|
+
channel_id=self.channel_id,
|
|
211
|
+
tx_id=self.tx_id,
|
|
212
|
+
initial_response=raw,
|
|
213
|
+
response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
|
|
214
|
+
parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
async def get_query_result_with_pagination(self, query: str, page_size: int,
|
|
218
|
+
bookmark: str = ""):
|
|
219
|
+
"""Paginated version of :meth:`get_query_result`."""
|
|
220
|
+
collection = ''
|
|
221
|
+
from src.fabric_shim.iterators import QueryResultIterator
|
|
222
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
223
|
+
from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
|
|
224
|
+
|
|
225
|
+
meta = ccshim_pb2.QueryMetadata()
|
|
226
|
+
meta.pageSize = int(page_size)
|
|
227
|
+
meta.bookmark = bookmark or ""
|
|
228
|
+
raw = await self.client.handle_get_query_result(
|
|
229
|
+
collection, query, self.channel_id, self.tx_id,
|
|
230
|
+
metadata=meta.SerializeToString(),
|
|
231
|
+
)
|
|
232
|
+
return QueryResultIterator(
|
|
233
|
+
handler=self.client,
|
|
234
|
+
channel_id=self.channel_id,
|
|
235
|
+
tx_id=self.tx_id,
|
|
236
|
+
initial_response=raw,
|
|
237
|
+
response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
|
|
238
|
+
parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def get_history_for_key(self, key: str):
|
|
242
|
+
"""Return the history of modifications for *key*.
|
|
243
|
+
|
|
244
|
+
Yields :class:`KeyModification` records (``tx_id``, ``value``,
|
|
245
|
+
``timestamp``, ``is_delete``).
|
|
246
|
+
"""
|
|
247
|
+
from src.fabric_shim.iterators import HistoryQueryIterator
|
|
248
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
249
|
+
from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
|
|
250
|
+
|
|
251
|
+
raw = await self.client.handle_get_history_for_key(
|
|
252
|
+
key, self.channel_id, self.tx_id,
|
|
253
|
+
)
|
|
254
|
+
return HistoryQueryIterator(
|
|
255
|
+
handler=self.client,
|
|
256
|
+
channel_id=self.channel_id,
|
|
257
|
+
tx_id=self.tx_id,
|
|
258
|
+
initial_response=raw,
|
|
259
|
+
response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
|
|
260
|
+
parser=lambda qrb: kv_pb.KeyModification.FromString(qrb.resultBytes),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def get_state_by_partial_composite_key(self, object_type: str,
|
|
264
|
+
attributes):
|
|
265
|
+
"""Range query on the ledger using a partial composite key.
|
|
266
|
+
|
|
267
|
+
Builds a composite key prefix from *object_type* and *attributes*
|
|
268
|
+
and runs a range query from that prefix to the next code point.
|
|
269
|
+
"""
|
|
270
|
+
full_key = self.create_composite_key(object_type, attributes)
|
|
271
|
+
# Range from the composite key to the same prefix with the maximum
|
|
272
|
+
# rune appended — this is the Fabric convention for "prefix" range
|
|
273
|
+
# queries against composite keys.
|
|
274
|
+
end_key = full_key + '\U0010FFFF'
|
|
275
|
+
return await self.get_state_by_range(full_key, end_key)
|
|
276
|
+
|
|
277
|
+
async def invoke_chaincode(self, chaincode_name: str, args, channel: str = ""):
|
|
278
|
+
"""Invoke another chaincode by name.
|
|
279
|
+
|
|
280
|
+
``args`` is a list of strings (or bytes). The return value is the
|
|
281
|
+
``Response`` produced by the invoked chaincode.
|
|
282
|
+
"""
|
|
283
|
+
from fabric_protos.peer import proposal_response_pb2 as pr_pb
|
|
284
|
+
# If a channel is supplied, build a fully-qualified name.
|
|
285
|
+
fully_qualified = f"{chaincode_name}/{channel}" if channel else chaincode_name
|
|
286
|
+
raw = await self.client.handle_invoke_chaincode(
|
|
287
|
+
fully_qualified, list(args), self.channel_id, self.tx_id,
|
|
288
|
+
)
|
|
289
|
+
# raw.payload is a serialised proposal Response
|
|
290
|
+
return pr_pb.Response.FromString(raw.payload)
|
|
291
|
+
|
|
292
|
+
def set_event(self, name: str, payload):
|
|
293
|
+
"""Emit a chaincode event.
|
|
294
|
+
|
|
295
|
+
``payload`` is stored on the stub and serialised into the final
|
|
296
|
+
``COMPLETED`` message built by the handler.
|
|
297
|
+
"""
|
|
298
|
+
if not isinstance(name, str) or not name:
|
|
299
|
+
raise Exception('event name must be a non-empty string')
|
|
300
|
+
if isinstance(payload, str):
|
|
301
|
+
payload = payload.encode()
|
|
302
|
+
elif payload is None:
|
|
303
|
+
payload = b''
|
|
304
|
+
# Lazily initialise the event list on the stub.
|
|
305
|
+
if not hasattr(self, '_events') or self._events is None:
|
|
306
|
+
self._events = []
|
|
307
|
+
from fabric_protos.peer import chaincode_event_pb2 as e_pb
|
|
308
|
+
evt = e_pb.ChaincodeEvent()
|
|
309
|
+
evt.chaincode_id = self.chaincode_id_name if hasattr(self, 'chaincode_id_name') else ""
|
|
310
|
+
evt.tx_id = self.tx_id
|
|
311
|
+
evt.event_name = name
|
|
312
|
+
evt.payload = bytes(payload)
|
|
313
|
+
self._events.append(evt)
|
|
314
|
+
|
|
315
|
+
def get_events(self):
|
|
316
|
+
"""Internal: return the list of accumulated events (used by handler)."""
|
|
317
|
+
return list(getattr(self, '_events', []) or [])
|
|
318
|
+
|
|
319
|
+
def create_composite_key(self, object_type, attributes):
|
|
320
|
+
"""Creates a composite key by combining the objectType string
|
|
321
|
+
and the given `attributes` to form a composite key"""
|
|
322
|
+
validate_composite_key_attribute(object_type)
|
|
323
|
+
if not isinstance(attributes, Sequence):
|
|
324
|
+
raise Exception('attributes must be an array')
|
|
325
|
+
|
|
326
|
+
composite_key = COMPOSITEKEY_NS + object_type + MIN_UNICODE_RUNE_VALUE
|
|
327
|
+
for attribute in attributes:
|
|
328
|
+
validate_composite_key_attribute(attribute)
|
|
329
|
+
composite_key = composite_key + attribute + MIN_UNICODE_RUNE_VALUE
|
|
330
|
+
return composite_key
|
|
331
|
+
|
|
332
|
+
def split_composite_key(self, composite_key):
|
|
333
|
+
object_type = None
|
|
334
|
+
attributes = []
|
|
335
|
+
if composite_key and len(composite_key) > 1 and composite_key[0] == COMPOSITEKEY_NS:
|
|
336
|
+
split_key = composite_key[1:].split(MIN_UNICODE_RUNE_VALUE)
|
|
337
|
+
object_type = split_key[0]
|
|
338
|
+
split_key.pop()
|
|
339
|
+
if len(split_key) > 1:
|
|
340
|
+
split_key.pop(0)
|
|
341
|
+
attributes = split_key
|
|
342
|
+
return object_type, attributes
|
src/fabric_shim/utils.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Auxiliary tools
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def enum_type(*sequential, **named) -> type:
|
|
6
|
+
"""enum other than class Enum. better performance
|
|
7
|
+
|
|
8
|
+
Typical usage example:
|
|
9
|
+
>> Numbers = enum('ZERO', 'ONE', 'TWO')
|
|
10
|
+
>> Numbers.ZERO
|
|
11
|
+
0
|
|
12
|
+
or:
|
|
13
|
+
>> Numbers = enum_type(ONE=1, TWO=2, THREE='three')
|
|
14
|
+
>> Numbers.THREE
|
|
15
|
+
'three'
|
|
16
|
+
Args:
|
|
17
|
+
sequential: Collects all the positional arguments in a tuple.
|
|
18
|
+
named: Collects all the keyword arguments in a dictionary.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
enums = dict(zip(sequential, range(len(sequential))), **named)
|
|
22
|
+
return type('Enum', (), enums)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MIN_UNICODE_RUNE_VALUE = '\u0000'
|
|
26
|
+
COMPOSITEKEY_NS = '\x00'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_composite_key_attribute(attr):
|
|
30
|
+
if attr is None or not isinstance(attr, str) or len(attr) == 0:
|
|
31
|
+
raise Exception('object type or attribute not a non-zero length string')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_simple_keys(keys):
|
|
35
|
+
for key in keys:
|
|
36
|
+
if key and isinstance(key, str) and key[0] == COMPOSITEKEY_NS:
|
|
37
|
+
raise Exception('first character of the key %s contains a null character which is not allowed' % key)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def generate_logging_prefix(channel_id, tx_id):
|
|
41
|
+
return '[%s-%s]' % (channel_id, tx_id)
|
src/version.py
ADDED