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,336 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import AsyncIterable
|
|
4
|
+
import asyncio
|
|
5
|
+
import grpc
|
|
6
|
+
|
|
7
|
+
from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
|
|
8
|
+
from fabric_protos.peer import chaincode_pb2 as cc_pb2
|
|
9
|
+
from fabric_protos.peer import proposal_response_pb2 as pr_pb
|
|
10
|
+
from src.fabric_shim.stub import ChaincodeStub
|
|
11
|
+
from src.fabric_shim.msg_queue_handler import MsgQueueHandler, QueueMessage
|
|
12
|
+
from src.fabric_shim.response import new_error_msg, ResponseCode
|
|
13
|
+
from src.fabric_shim.utils import generate_logging_prefix
|
|
14
|
+
from src.fabric_shim.logging import LOGGER
|
|
15
|
+
from src.fabric_shim.interfaces import Chaincode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class STATES:
|
|
19
|
+
CREATED = "created" # start state
|
|
20
|
+
ESTABLISHED = "established" # connection established
|
|
21
|
+
READY = "ready" # ready for requests
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
MIN_UNICODE_RUNE_VALUE = '\u0000' # U + 0000
|
|
25
|
+
MAX_UNICODE_RUNE_VALUE = '\u0010FFFF' # U+10FFFF - maximum (and unallocated) code point
|
|
26
|
+
COMPOSITEKEY_NS = '\x00'
|
|
27
|
+
EMPTY_KEY_SUBSTITUTE = '\x01'
|
|
28
|
+
|
|
29
|
+
class Handler:
|
|
30
|
+
def __init__(self, cc_id: str, cc: Chaincode) -> None:
|
|
31
|
+
self.chaincode_id = cc_pb2.ChaincodeID()
|
|
32
|
+
self.chaincode_id.name = cc_id
|
|
33
|
+
self.chaincode = cc
|
|
34
|
+
self.msg_queue_handler = None
|
|
35
|
+
self.context = None
|
|
36
|
+
self.state = STATES.CREATED
|
|
37
|
+
self._pending_tasks = set()
|
|
38
|
+
|
|
39
|
+
def _track_task(self, coro):
|
|
40
|
+
"""Schedule message handling while surfacing task failures in logs."""
|
|
41
|
+
task = asyncio.create_task(coro)
|
|
42
|
+
self._pending_tasks.add(task)
|
|
43
|
+
|
|
44
|
+
def _done_callback(done_task):
|
|
45
|
+
self._pending_tasks.discard(done_task)
|
|
46
|
+
try:
|
|
47
|
+
exc = done_task.exception()
|
|
48
|
+
except asyncio.CancelledError:
|
|
49
|
+
return
|
|
50
|
+
if exc is not None:
|
|
51
|
+
LOGGER.exception('Unhandled exception while processing peer message', exc_info=exc)
|
|
52
|
+
|
|
53
|
+
task.add_done_callback(_done_callback)
|
|
54
|
+
|
|
55
|
+
async def handle_stub_interaction(self, msg, action="Invoke"):
|
|
56
|
+
"""handle_message calls the Init | Invoke function of the associated chaincode."""
|
|
57
|
+
# Get the function and args from Payload
|
|
58
|
+
cc_input = cc_pb2.ChaincodeInput()
|
|
59
|
+
cc_input.ParseFromString(msg.payload)
|
|
60
|
+
|
|
61
|
+
stub = ChaincodeStub(self, msg.channel_id, msg.txid, cc_input, msg.proposal)
|
|
62
|
+
|
|
63
|
+
# Normalize action and call appropriate chaincode method
|
|
64
|
+
action_normalized = (action or "").lower()
|
|
65
|
+
if action_normalized == 'init':
|
|
66
|
+
method = 'Init'
|
|
67
|
+
resp: pr_pb.Response = await self.chaincode.init(stub)
|
|
68
|
+
else:
|
|
69
|
+
method = 'Invoke'
|
|
70
|
+
resp: pr_pb.Response = await self.chaincode.invoke(stub)
|
|
71
|
+
|
|
72
|
+
# check that a response object has been returned otherwise assume an error.
|
|
73
|
+
|
|
74
|
+
if not resp or not resp.status:
|
|
75
|
+
err_msg = '%s Calling chaincode %s() has not called success or error.' \
|
|
76
|
+
% (generate_logging_prefix(msg.channel_id, msg.txid), method)
|
|
77
|
+
LOGGER.info(err_msg)
|
|
78
|
+
|
|
79
|
+
resp = pr_pb.Response(
|
|
80
|
+
status=ResponseCode.ERROR,
|
|
81
|
+
message=err_msg
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
LOGGER.info('%s Calling chaincode %s(), response status: %s'
|
|
85
|
+
% (generate_logging_prefix(msg.channel_id, msg.txid), method, resp.status))
|
|
86
|
+
|
|
87
|
+
if resp.status >= ResponseCode.ERROR:
|
|
88
|
+
err_msg = '%s Calling chaincode %s() returned error response [%s]. Sending COMPLETED message back to peer' \
|
|
89
|
+
% (generate_logging_prefix(msg.channel_id, msg.txid), method, resp.message)
|
|
90
|
+
LOGGER.info(err_msg)
|
|
91
|
+
|
|
92
|
+
next_state_msg = ccshim_pb2.ChaincodeMessage(
|
|
93
|
+
type=ccshim_pb2.ChaincodeMessage.COMPLETED,
|
|
94
|
+
payload=resp.SerializeToString(),
|
|
95
|
+
txid=msg.txid,
|
|
96
|
+
channel_id=msg.channel_id
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
LOGGER.info('%s Calling chaincode %s() succeeded. Sending COMPLETED message back to peer'
|
|
100
|
+
% (generate_logging_prefix(msg.channel_id, msg.txid), method))
|
|
101
|
+
|
|
102
|
+
next_state_msg = ccshim_pb2.ChaincodeMessage(
|
|
103
|
+
type=ccshim_pb2.ChaincodeMessage.COMPLETED,
|
|
104
|
+
payload=resp.SerializeToString(),
|
|
105
|
+
txid=msg.txid,
|
|
106
|
+
channel_id=msg.channel_id
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
await self.context.write(next_state_msg)
|
|
110
|
+
|
|
111
|
+
async def handle_message_ready(self, msg):
|
|
112
|
+
"""handle_message_ready handles messages received from the peer when the handler is in the "ready" state."""
|
|
113
|
+
if msg.type == ccshim_pb2.ChaincodeMessage.RESPONSE or msg.type == ccshim_pb2.ChaincodeMessage.ERROR:
|
|
114
|
+
LOGGER.info("+++ ERROR or RESPONSE +++")
|
|
115
|
+
await self.msg_queue_handler.handle_msg_response(msg)
|
|
116
|
+
elif msg.type == ccshim_pb2.ChaincodeMessage.INIT:
|
|
117
|
+
LOGGER.info("+++ call INIT +++")
|
|
118
|
+
await self.handle_stub_interaction(msg, "Init")
|
|
119
|
+
return
|
|
120
|
+
elif msg.type == ccshim_pb2.ChaincodeMessage.TRANSACTION:
|
|
121
|
+
LOGGER.info("+++ call INVOKE +++")
|
|
122
|
+
await self.handle_stub_interaction(msg, "Invoke")
|
|
123
|
+
return
|
|
124
|
+
else:
|
|
125
|
+
await self.context.write(new_error_msg(msg, self.state))
|
|
126
|
+
|
|
127
|
+
async def handle_message_established(self, msg):
|
|
128
|
+
"""
|
|
129
|
+
handle_message_established handles messages received from the peer when the handler is in the "established" state.
|
|
130
|
+
"""
|
|
131
|
+
if msg.type != ccshim_pb2.ChaincodeMessage.READY:
|
|
132
|
+
LOGGER.error(f'Chaincode is in "established" state, can only process messages of type "ready", '
|
|
133
|
+
f'but received "{msg.type}"')
|
|
134
|
+
await self.context.write(new_error_msg(msg, self.state))
|
|
135
|
+
else:
|
|
136
|
+
LOGGER.info('Successfully established communication with peer node. State transferred to "ready"')
|
|
137
|
+
self.state = STATES.READY
|
|
138
|
+
|
|
139
|
+
async def handle_message_created(self, msg):
|
|
140
|
+
"""handle_message_created handles messages received from the peer when the handler is in the "created" state."""
|
|
141
|
+
if msg.type != ccshim_pb2.ChaincodeMessage.REGISTERED:
|
|
142
|
+
LOGGER.error(f'Chaincode is in "created" state, can only process messages of type "registered", '
|
|
143
|
+
f'but received "{msg.type}"')
|
|
144
|
+
await self.context.write(new_error_msg(msg, self.state))
|
|
145
|
+
else:
|
|
146
|
+
LOGGER.info('Successfully registered with peer node. State transferred to "established"')
|
|
147
|
+
self.state = STATES.ESTABLISHED
|
|
148
|
+
|
|
149
|
+
async def handle_message(self, msg: ccshim_pb2.ChaincodeMessage):
|
|
150
|
+
"""handle_message message handles loop for shim side of chaincode/peer stream."""
|
|
151
|
+
LOGGER.warning('-->> Look out!')
|
|
152
|
+
|
|
153
|
+
# TODO: ?
|
|
154
|
+
if msg.type == ccshim_pb2.ChaincodeMessage.KEEPALIVE:
|
|
155
|
+
LOGGER.info('-| KEEPALIVE')
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if self.state == STATES.READY:
|
|
159
|
+
await self.handle_message_ready(msg)
|
|
160
|
+
elif self.state == STATES.ESTABLISHED:
|
|
161
|
+
await self.handle_message_established(msg)
|
|
162
|
+
elif self.state == STATES.CREATED:
|
|
163
|
+
await self.handle_message_created(msg)
|
|
164
|
+
else:
|
|
165
|
+
await self.context.write(new_error_msg(msg, self.state))
|
|
166
|
+
|
|
167
|
+
async def chat_with_peer(self, stream: AsyncIterable[ccshim_pb2.ChaincodeMessage], context: grpc.aio.ServicerContext):
|
|
168
|
+
"""chat stream for peer-chaincode interactions post connection"""
|
|
169
|
+
self.state = STATES.CREATED
|
|
170
|
+
|
|
171
|
+
self.context = context
|
|
172
|
+
self.msg_queue_handler = MsgQueueHandler(self)
|
|
173
|
+
|
|
174
|
+
# Send the ChaincodeID during register.
|
|
175
|
+
cm = ccshim_pb2.ChaincodeMessage(
|
|
176
|
+
type=ccshim_pb2.ChaincodeMessage.REGISTER, payload=self.chaincode_id.SerializeToString())
|
|
177
|
+
cm.timestamp.FromDatetime(datetime.datetime.now())
|
|
178
|
+
|
|
179
|
+
# Register on the stream
|
|
180
|
+
await self.context.write(cm)
|
|
181
|
+
|
|
182
|
+
async for receive_message in stream:
|
|
183
|
+
LOGGER.info('Received message')
|
|
184
|
+
if receive_message is None:
|
|
185
|
+
err_str = "received nil message, ending chaincode stream"
|
|
186
|
+
LOGGER.error(err_str)
|
|
187
|
+
return ccshim_pb2.ChaincodeMessage(
|
|
188
|
+
type=ccshim_pb2.ChaincodeMessage.ERROR, payload=err_str.encode(encoding='utf-8'))
|
|
189
|
+
else:
|
|
190
|
+
# Keep handling asynchronous so the stream can continue receiving
|
|
191
|
+
# response frames needed by in-flight request futures.
|
|
192
|
+
self._track_task(self.handle_message(receive_message))
|
|
193
|
+
|
|
194
|
+
LOGGER.info(f'->>>> proposal {receive_message.proposal}')
|
|
195
|
+
LOGGER.info(f'->>>> payload {receive_message.payload}')
|
|
196
|
+
LOGGER.info(f'->>>> channel ID {receive_message.channel_id}')
|
|
197
|
+
LOGGER.info(f'->>>> Tx ID {receive_message.txid}')
|
|
198
|
+
|
|
199
|
+
if self._pending_tasks:
|
|
200
|
+
await asyncio.gather(*self._pending_tasks, return_exceptions=True)
|
|
201
|
+
|
|
202
|
+
async def handle_get_state(self, collection, key, channel_id, tx_id):
|
|
203
|
+
msg_pb = ccshim_pb2.GetState()
|
|
204
|
+
msg_pb.key = key
|
|
205
|
+
msg_pb.collection = collection
|
|
206
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
207
|
+
type=ccshim_pb2.ChaincodeMessage.GET_STATE,
|
|
208
|
+
payload=msg_pb.SerializeToString(),
|
|
209
|
+
txid=tx_id,
|
|
210
|
+
channel_id=channel_id
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
result = await self.__ask_peer_and_listen(msg, 'GetState')
|
|
214
|
+
return result.payload
|
|
215
|
+
|
|
216
|
+
async def handle_put_state(self, collection, key, value, channel_id, tx_id):
|
|
217
|
+
msg_pb = ccshim_pb2.PutState()
|
|
218
|
+
msg_pb.key = key
|
|
219
|
+
msg_pb.value = value
|
|
220
|
+
msg_pb.collection = collection
|
|
221
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
222
|
+
type=ccshim_pb2.ChaincodeMessage.PUT_STATE,
|
|
223
|
+
payload=msg_pb.SerializeToString(),
|
|
224
|
+
txid=tx_id,
|
|
225
|
+
channel_id=channel_id
|
|
226
|
+
)
|
|
227
|
+
return await self.__ask_peer_and_listen(msg, 'PutState')
|
|
228
|
+
|
|
229
|
+
async def handle_delete_state(self, collection, key, channel_id, tx_id):
|
|
230
|
+
msg_pb = ccshim_pb2.DelState()
|
|
231
|
+
msg_pb.key = key
|
|
232
|
+
msg_pb.collection = collection
|
|
233
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
234
|
+
type=ccshim_pb2.ChaincodeMessage.DEL_STATE,
|
|
235
|
+
payload=msg_pb.SerializeToString(),
|
|
236
|
+
txid=tx_id,
|
|
237
|
+
channel_id=channel_id
|
|
238
|
+
)
|
|
239
|
+
return await self.__ask_peer_and_listen(msg, 'DeleteState')
|
|
240
|
+
|
|
241
|
+
async def handle_get_state_by_range(self, collection, start_key, end_key,
|
|
242
|
+
channel_id, tx_id, metadata=None):
|
|
243
|
+
"""Send a GetStateByRange request to the peer.
|
|
244
|
+
|
|
245
|
+
``metadata`` (if present) is a serialised :class:`QueryMetadata`
|
|
246
|
+
message — used for pagination.
|
|
247
|
+
"""
|
|
248
|
+
msg_pb = ccshim_pb2.GetStateByRange()
|
|
249
|
+
# NB: the protobuf field names are camelCase (startKey/endKey), not
|
|
250
|
+
# snake_case.
|
|
251
|
+
msg_pb.startKey = start_key or ""
|
|
252
|
+
msg_pb.endKey = end_key or ""
|
|
253
|
+
msg_pb.collection = collection
|
|
254
|
+
if metadata is not None:
|
|
255
|
+
msg_pb.metadata = metadata
|
|
256
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
257
|
+
type=ccshim_pb2.ChaincodeMessage.GET_STATE_BY_RANGE,
|
|
258
|
+
payload=msg_pb.SerializeToString(),
|
|
259
|
+
txid=tx_id,
|
|
260
|
+
channel_id=channel_id,
|
|
261
|
+
)
|
|
262
|
+
return await self.__ask_peer_and_listen(msg, 'GetStateByRange')
|
|
263
|
+
|
|
264
|
+
async def handle_get_query_result(self, collection, query, channel_id, tx_id,
|
|
265
|
+
metadata=None):
|
|
266
|
+
"""Send a GetQueryResult (CouchDB rich query) request to the peer."""
|
|
267
|
+
msg_pb = ccshim_pb2.GetQueryResult()
|
|
268
|
+
msg_pb.query = query
|
|
269
|
+
msg_pb.collection = collection
|
|
270
|
+
if metadata is not None:
|
|
271
|
+
msg_pb.metadata = metadata
|
|
272
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
273
|
+
type=ccshim_pb2.ChaincodeMessage.GET_QUERY_RESULT,
|
|
274
|
+
payload=msg_pb.SerializeToString(),
|
|
275
|
+
txid=tx_id,
|
|
276
|
+
channel_id=channel_id,
|
|
277
|
+
)
|
|
278
|
+
return await self.__ask_peer_and_listen(msg, 'GetQueryResult')
|
|
279
|
+
|
|
280
|
+
async def handle_get_history_for_key(self, key, channel_id, tx_id):
|
|
281
|
+
"""Send a GetHistoryForKey request to the peer."""
|
|
282
|
+
msg_pb = ccshim_pb2.GetHistoryForKey()
|
|
283
|
+
msg_pb.key = key
|
|
284
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
285
|
+
type=ccshim_pb2.ChaincodeMessage.GET_HISTORY_FOR_KEY,
|
|
286
|
+
payload=msg_pb.SerializeToString(),
|
|
287
|
+
txid=tx_id,
|
|
288
|
+
channel_id=channel_id,
|
|
289
|
+
)
|
|
290
|
+
return await self.__ask_peer_and_listen(msg, 'GetHistoryForKey')
|
|
291
|
+
|
|
292
|
+
async def handle_query_state_next(self, query_id, channel_id, tx_id):
|
|
293
|
+
"""Fetch the next page of an in-flight paged query."""
|
|
294
|
+
msg_pb = ccshim_pb2.QueryStateNext()
|
|
295
|
+
msg_pb.id = query_id
|
|
296
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
297
|
+
type=ccshim_pb2.ChaincodeMessage.QUERY_STATE_NEXT,
|
|
298
|
+
payload=msg_pb.SerializeToString(),
|
|
299
|
+
txid=tx_id,
|
|
300
|
+
channel_id=channel_id,
|
|
301
|
+
)
|
|
302
|
+
return await self.__ask_peer_and_listen(msg, 'QueryStateNext')
|
|
303
|
+
|
|
304
|
+
async def handle_query_state_close(self, query_id, channel_id, tx_id):
|
|
305
|
+
"""Close an in-flight paged query."""
|
|
306
|
+
msg_pb = ccshim_pb2.QueryStateClose()
|
|
307
|
+
msg_pb.id = query_id
|
|
308
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
309
|
+
type=ccshim_pb2.ChaincodeMessage.QUERY_STATE_CLOSE,
|
|
310
|
+
payload=msg_pb.SerializeToString(),
|
|
311
|
+
txid=tx_id,
|
|
312
|
+
channel_id=channel_id,
|
|
313
|
+
)
|
|
314
|
+
return await self.__ask_peer_and_listen(msg, 'QueryStateClose')
|
|
315
|
+
|
|
316
|
+
async def handle_invoke_chaincode(self, chaincode_name, args, channel_id, tx_id):
|
|
317
|
+
"""Invoke another chaincode by name."""
|
|
318
|
+
ci = cc_pb2.ChaincodeInput()
|
|
319
|
+
for a in args:
|
|
320
|
+
ci.args.append(a.encode() if isinstance(a, str) else a)
|
|
321
|
+
msg = ccshim_pb2.ChaincodeMessage(
|
|
322
|
+
type=ccshim_pb2.ChaincodeMessage.INVOKE_CHAINCODE,
|
|
323
|
+
payload=ci.SerializeToString(),
|
|
324
|
+
txid=tx_id,
|
|
325
|
+
channel_id=channel_id,
|
|
326
|
+
)
|
|
327
|
+
return await self.__ask_peer_and_listen(msg, 'InvokeChaincode')
|
|
328
|
+
|
|
329
|
+
async def __ask_peer_and_listen(self, msg, action):
|
|
330
|
+
loop = asyncio.get_running_loop()
|
|
331
|
+
fut = loop.create_future()
|
|
332
|
+
|
|
333
|
+
message = QueueMessage(msg, action, fut)
|
|
334
|
+
await self.msg_queue_handler.queue_msg(message)
|
|
335
|
+
|
|
336
|
+
return await fut
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from fabric_protos.peer import proposal_response_pb2 as pb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChaincodeStubInterface(ABC):
|
|
8
|
+
"""
|
|
9
|
+
ChaincodeStubInterface is used by deployable chaincode apps to access and modify their ledgers
|
|
10
|
+
"""
|
|
11
|
+
def get_function_and_parameters(self): # Get the chaincode calling method name and parameter list
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def get_txid(self): # Get the ID of the chaincode calling transaction
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def get_channel_id(self): # Get the channel ID of the chaincode calling transaction
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def get_creator(self): # Get the user ID of the chaincode calling transaction
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def get_transient(self): # Get the transient dataset of chaincode call transactions
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def get_tx_timestamp(self): # Get the timestamp of the chaincode calling transaction
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_state(self, key: str): #-> bytearray:
|
|
31
|
+
"""
|
|
32
|
+
GetState returns the value of the specified `key` from the
|
|
33
|
+
ledger. Note that GetState doesn't read data from the writeset, which
|
|
34
|
+
has not been committed to the ledger. In other words, GetState doesn't
|
|
35
|
+
consider data modified by PutState that has not been committed.
|
|
36
|
+
If the key does not exist in the state database, (nil, nil) is returned.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def put_state(self, key: str, value): # update the state of the specified key on the ledger
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def delete_state(self, key: str): # delete the state of the specified key on the ledger
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def set_state_validation_parameter(self): # Set state validation parameters
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def get_state_validation_parameter(self): # Get state validation parameters
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def get_state_by_range(self): # Get the state of the keys in the specified range on the ledger
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def get_state_by_range_with_pagination(
|
|
55
|
+
self): # Pagination to get the state of the keys in the specified range on the ledger
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def get_query_result(self): # Get the node rich query result, which is only valid when couchdb is used
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def get_query_result_with_pagination(self): # Pagination to get node rich query results
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def get_history_for_key(self): # Get the update history of the specified key on the ledger
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def invoke_chaincode(self): # Invoke other chaincodes
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def set_event(self): # trigger chaincode event
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def create_composite_key(self): # Create a composite key
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
# split composite key, return composite key type and composition attribute value
|
|
77
|
+
def split_composite_key(self):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def get_state_by_partial_composite_key(self): # Query the ledger state using a partial composite key
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Use partial composite key pagination to query ledger state
|
|
84
|
+
def get_state_by_partial_composite_key_with_pagination(self):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def get_private_data(self): # Get the status of the specified key in the specified private dataset
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def get_private_data_hash(self): # Get the state hash of the specified key in the specified private dataset
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def put_private_data(self): # Update the state of the specified key in the specified private dataset
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def delete_private_data(self): # delete the specified key in the specified private data set
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def set_private_data_validation_parameter(self): # Set validation parameters for private data
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def get_private_data_validation_parameter(self): # Get the validation parameters for private data
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# Get the status of the keys of the specified range in the specified private dataset
|
|
106
|
+
def get_private_data_by_range(self):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def get_private_data_by_partial_composite_key(self): # Query a private dataset with a partial composite key
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Get rich query results for private datasets, only valid when couchdb is enabled
|
|
113
|
+
def get_private_data_query_result(self):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Chaincode(ABC):
|
|
118
|
+
@staticmethod
|
|
119
|
+
@abstractmethod
|
|
120
|
+
async def init(stub: ChaincodeStubInterface) -> pb.Response:
|
|
121
|
+
"""
|
|
122
|
+
init is called during Instantiate transaction after the chaincode container has been established for the
|
|
123
|
+
first time, allowing the chaincode to initialize its internal data
|
|
124
|
+
"""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def invoke(stub: ChaincodeStubInterface) -> pb.Response:
|
|
130
|
+
"""
|
|
131
|
+
invoke is called to update or query the ledger in a proposal transaction. Updated state variables are not
|
|
132
|
+
committed to the ledger until the transaction is committed.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Asynchronous iterators for Fabric state / history / query results.
|
|
3
|
+
|
|
4
|
+
The contract API exposes three operations that return iterators over ledger
|
|
5
|
+
data:
|
|
6
|
+
|
|
7
|
+
* :func:`ChaincodeStub.get_state_by_range` / :func:`get_state_by_partial_composite_key`
|
|
8
|
+
→ :class:`StateQueryIterator`
|
|
9
|
+
* :func:`ChaincodeStub.get_query_result` (CouchDB rich queries)
|
|
10
|
+
→ :class:`QueryResultIterator` (same wire protocol as state queries)
|
|
11
|
+
* :func:`ChaincodeStub.get_history_for_key`
|
|
12
|
+
→ :class:`HistoryQueryIterator` (yields :class:`KeyModification` records)
|
|
13
|
+
|
|
14
|
+
Each iterator transparently pages through the peer's responses by sending
|
|
15
|
+
``QueryStateNext`` messages until the peer reports ``has_more == false``,
|
|
16
|
+
then closes the query with ``QueryStateClose`` when the iteration ends.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
from typing import Any, AsyncIterator, List, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _QueryIteratorBase:
|
|
26
|
+
"""Common base for paged query iterators."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, handler, channel_id: str, tx_id: str,
|
|
29
|
+
initial_response, response_factory, parser):
|
|
30
|
+
self._handler = handler
|
|
31
|
+
self._channel_id = channel_id
|
|
32
|
+
self._tx_id = tx_id
|
|
33
|
+
self._response_factory = response_factory # callable(msg_bytes) -> QueryResponse
|
|
34
|
+
self._parser = parser # callable(QueryResultBytes) -> parsed value
|
|
35
|
+
self._buffer: List[Any] = []
|
|
36
|
+
self._id: Optional[str] = None
|
|
37
|
+
self._has_more: bool = False
|
|
38
|
+
self._closed: bool = False
|
|
39
|
+
self._consume(initial_response)
|
|
40
|
+
|
|
41
|
+
def _consume(self, raw_msg) -> None:
|
|
42
|
+
"""Parse a peer QueryResponse message into buffer + pagination state."""
|
|
43
|
+
if raw_msg is None:
|
|
44
|
+
return
|
|
45
|
+
resp = self._response_factory(raw_msg.payload)
|
|
46
|
+
self._id = resp.id
|
|
47
|
+
for qrb in resp.results:
|
|
48
|
+
self._buffer.append(self._parser(qrb))
|
|
49
|
+
self._has_more = bool(resp.has_more)
|
|
50
|
+
|
|
51
|
+
async def __aiter__(self):
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __aiter__(self):
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
async def __anext__(self):
|
|
58
|
+
if self._buffer:
|
|
59
|
+
return self._buffer.pop(0)
|
|
60
|
+
if not self._has_more or self._id is None:
|
|
61
|
+
await self._close()
|
|
62
|
+
raise StopAsyncIteration
|
|
63
|
+
# Request the next page from the peer.
|
|
64
|
+
next_response = await self._handler.handle_query_state_next(
|
|
65
|
+
self._id, self._channel_id, self._tx_id,
|
|
66
|
+
)
|
|
67
|
+
self._consume(next_response)
|
|
68
|
+
if self._buffer:
|
|
69
|
+
return self._buffer.pop(0)
|
|
70
|
+
await self._close()
|
|
71
|
+
raise StopAsyncIteration
|
|
72
|
+
|
|
73
|
+
async def _close(self) -> None:
|
|
74
|
+
if self._closed or self._id is None:
|
|
75
|
+
self._closed = True
|
|
76
|
+
return
|
|
77
|
+
self._closed = True
|
|
78
|
+
try:
|
|
79
|
+
await self._handler.handle_query_state_close(
|
|
80
|
+
self._id, self._channel_id, self._tx_id,
|
|
81
|
+
)
|
|
82
|
+
except Exception:
|
|
83
|
+
# Best-effort close; iterators must not raise on close.
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
async def aclose(self) -> None:
|
|
87
|
+
"""Explicit close — safe to call multiple times."""
|
|
88
|
+
await self._close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class StateQueryIterator(_QueryIteratorBase):
|
|
92
|
+
"""Iterates over (key, value) byte pairs returned by GetStateByRange."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class QueryResultIterator(_QueryIteratorBase):
|
|
96
|
+
"""Iterates over rich-query (CouchDB) results."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class HistoryQueryIterator(_QueryIteratorBase):
|
|
100
|
+
"""Iterates over :class:`KeyModification` records returned by GetHistoryForKey."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = [
|
|
104
|
+
"StateQueryIterator",
|
|
105
|
+
"QueryResultIterator",
|
|
106
|
+
"HistoryQueryIterator",
|
|
107
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import logging.handlers
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
# Python 3.7 and newer, fast reentrant implementation
|
|
8
|
+
# without task tracking (not needed for that when logging)
|
|
9
|
+
from queue import SimpleQueue as Queue
|
|
10
|
+
except ImportError:
|
|
11
|
+
from queue import Queue
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger("asyncio")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalQueueHandler(logging.handlers.QueueHandler):
|
|
18
|
+
"""
|
|
19
|
+
Network logging can block the event loop. It is recommended to use a separate thread for handling logs or use
|
|
20
|
+
non-blocking IO.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
24
|
+
# Removed the call to self.prepare(), handle task cancellation
|
|
25
|
+
try:
|
|
26
|
+
self.enqueue(record)
|
|
27
|
+
except asyncio.CancelledError:
|
|
28
|
+
raise
|
|
29
|
+
except Exception:
|
|
30
|
+
self.handleError(record)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def setup_logging_queue() -> List[logging.Handler]:
|
|
34
|
+
"""Move log handlers to a separate thread.
|
|
35
|
+
|
|
36
|
+
Replace handlers on the root logger with a LocalQueueHandler,
|
|
37
|
+
and start a logging.QueueListener holding the original
|
|
38
|
+
handlers.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
queue = Queue()
|
|
42
|
+
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
|
|
43
|
+
|
|
44
|
+
handlers: List[logging.Handler] = []
|
|
45
|
+
|
|
46
|
+
handler = LocalQueueHandler(queue)
|
|
47
|
+
LOGGER.addHandler(handler)
|
|
48
|
+
for h in LOGGER.handlers[:]:
|
|
49
|
+
if h is not handler:
|
|
50
|
+
LOGGER.removeHandler(h)
|
|
51
|
+
handlers.append(h)
|
|
52
|
+
|
|
53
|
+
listener = logging.handlers.QueueListener(
|
|
54
|
+
queue, *handlers, respect_handler_level=True
|
|
55
|
+
)
|
|
56
|
+
# Keep a global reference to the listener so it is not garbage collected
|
|
57
|
+
global _QUEUE_LISTENER
|
|
58
|
+
_QUEUE_LISTENER = listener
|
|
59
|
+
listener.start()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
setup_logging_queue()
|
|
63
|
+
# logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
|