golem_base_sdk
GolemBase Python SDK.
1"""GolemBase Python SDK.""" 2 3import asyncio 4import base64 5import logging 6import logging.config 7import typing 8from collections.abc import ( 9 AsyncGenerator, 10 Callable, 11 Coroutine, 12 Sequence, 13) 14from typing import ( 15 Any, 16 cast, 17) 18 19from eth_typing import ChecksumAddress, HexStr 20from web3 import AsyncWeb3, WebSocketProvider 21from web3.contract import AsyncContract 22from web3.exceptions import ProviderConnectionError, Web3RPCError 23from web3.method import Method, default_root_munger 24from web3.middleware import SignAndSendRawMiddlewareBuilder 25from web3.types import LogReceipt, RPCEndpoint, TxParams, TxReceipt, Wei 26from web3.utils.subscriptions import ( 27 LogsSubscription, 28 LogsSubscriptionContext, 29) 30 31from .constants import ( 32 GOLEM_BASE_ABI, 33 STORAGE_ADDRESS, 34) 35from .types import ( 36 Address, 37 Annotation, 38 CreateEntityReturnType, 39 EntityKey, 40 EntityMetadata, 41 ExtendEntityReturnType, 42 GenericBytes, 43 GolemBaseCreate, 44 GolemBaseDelete, 45 GolemBaseExtend, 46 GolemBaseTransaction, 47 GolemBaseTransactionReceipt, 48 GolemBaseUpdate, 49 QueryEntitiesResult, 50 UpdateEntityReturnType, 51 WatchLogsHandle, 52) 53from .utils import rlp_encode_transaction 54from .wallet import ( 55 WalletError, 56 decrypt_wallet, 57) 58 59__all__: Sequence[str] = [ 60 # Exports from .types 61 "Address", 62 "Annotation", 63 "CreateEntityReturnType", 64 "EntityKey", 65 "EntityMetadata", 66 "ExtendEntityReturnType", 67 "GenericBytes", 68 "GolemBaseCreate", 69 "GolemBaseDelete", 70 "GolemBaseExtend", 71 "GolemBaseTransaction", 72 "GolemBaseTransactionReceipt", 73 "GolemBaseUpdate", 74 "QueryEntitiesResult", 75 "UpdateEntityReturnType", 76 "WatchLogsHandle", 77 # Exports from .constants 78 "GOLEM_BASE_ABI", 79 "STORAGE_ADDRESS", 80 # Exports from .wallet 81 "decrypt_wallet", 82 "WalletError", 83 # Exports from this file 84 "GolemBaseClient", 85 # Re-exports 86 "Wei", 87] 88 89 90logger = logging.getLogger(__name__) 91"""@private""" 92 93 94class GolemBaseHttpClient(AsyncWeb3): 95 """Subclass of AsyncWeb3 with added Golem Base methods.""" 96 97 def __init__(self, rpc_url: str): 98 super().__init__( 99 AsyncWeb3.AsyncHTTPProvider(rpc_url, request_kwargs={"timeout": 60}) 100 ) 101 102 self.eth.attach_methods( 103 { 104 "get_storage_value": Method( 105 json_rpc_method=RPCEndpoint("golembase_getStorageValue"), 106 mungers=[default_root_munger], 107 ), 108 "get_entity_metadata": Method( 109 json_rpc_method=RPCEndpoint("golembase_getEntityMetaData"), 110 mungers=[default_root_munger], 111 ), 112 "get_entities_to_expire_at_block": Method( 113 json_rpc_method=RPCEndpoint("golembase_getEntitiesToExpireAtBlock"), 114 mungers=[default_root_munger], 115 ), 116 "get_entity_count": Method( 117 json_rpc_method=RPCEndpoint("golembase_getEntityCount"), 118 mungers=[default_root_munger], 119 ), 120 "get_all_entity_keys": Method( 121 json_rpc_method=RPCEndpoint("golembase_getAllEntityKeys"), 122 mungers=[default_root_munger], 123 ), 124 "get_entities_of_owner": Method( 125 json_rpc_method=RPCEndpoint("golembase_getEntitiesOfOwner"), 126 mungers=[default_root_munger], 127 ), 128 "query_entities": Method( 129 json_rpc_method=RPCEndpoint("golembase_queryEntities"), 130 mungers=[default_root_munger], 131 ), 132 } 133 ) 134 135 async def get_storage_value(self, entity_key: EntityKey) -> bytes: 136 """Get the storage value stored in the given entity.""" 137 return base64.b64decode( 138 await self.eth.get_storage_value( # type: ignore[attr-defined] 139 entity_key.as_hex_string() 140 ) 141 ) 142 143 async def get_entity_metadata(self, entity_key: EntityKey) -> EntityMetadata: 144 """Get the metadata of the given entity.""" 145 metadata = await self.eth.get_entity_metadata( # type: ignore[attr-defined] 146 entity_key.as_hex_string() 147 ) 148 149 return EntityMetadata( 150 entity_key=entity_key, 151 owner=Address(GenericBytes.from_hex_string(metadata.owner)), 152 expires_at_block=metadata.expiresAtBlock, 153 string_annotations=list( 154 map( 155 lambda ann: Annotation(key=ann["key"], value=ann["value"]), 156 metadata.stringAnnotations, 157 ) 158 ), 159 numeric_annotations=list( 160 map( 161 lambda ann: Annotation(key=ann["key"], value=ann["value"]), 162 metadata.numericAnnotations, 163 ) 164 ), 165 ) 166 167 async def get_entities_to_expire_at_block( 168 self, block_number: int 169 ) -> Sequence[EntityKey]: 170 """Get all entities that will expire at the given block.""" 171 return list( 172 map( 173 lambda e: EntityKey(GenericBytes.from_hex_string(e)), 174 await self.eth.get_entities_to_expire_at_block( # type: ignore[attr-defined] 175 block_number 176 ), 177 ) 178 ) 179 180 async def get_entity_count(self) -> int: 181 """Get the total entity count in Golem Base.""" 182 return cast(int, await self.eth.get_entity_count()) # type: ignore[attr-defined] 183 184 async def get_all_entity_keys(self) -> Sequence[EntityKey]: 185 """Get all entity keys in Golem Base.""" 186 return list( 187 map( 188 lambda e: EntityKey(GenericBytes.from_hex_string(e)), 189 await self.eth.get_all_entity_keys(), # type: ignore[attr-defined] 190 ) 191 ) 192 193 async def get_entities_of_owner( 194 self, owner: ChecksumAddress 195 ) -> Sequence[EntityKey]: 196 """Get all the entities owned by the given address.""" 197 return list( 198 map( 199 lambda e: EntityKey(GenericBytes.from_hex_string(e)), 200 await self.eth.get_entities_of_owner(owner), # type: ignore[attr-defined] 201 ) 202 ) 203 204 async def query_entities(self, query: str) -> Sequence[QueryEntitiesResult]: 205 """Get all entities that satisfy the given Golem Base query.""" 206 return list( 207 map( 208 lambda result: QueryEntitiesResult( 209 entity_key=result.key, storage_value=base64.b64decode(result.value) 210 ), 211 await self.eth.query_entities(query), # type: ignore[attr-defined] 212 ) 213 ) 214 215 216class GolemBaseROClient: 217 _http_client: GolemBaseHttpClient 218 _ws_client: AsyncWeb3 219 _golem_base_contract: AsyncContract 220 _background_tasks: set[asyncio.Task[None]] 221 222 @staticmethod 223 async def create_ro_client(rpc_url: str, ws_url: str) -> "GolemBaseROClient": 224 """ 225 Create a `GolemBaseClient` instance. 226 227 This is the preferred method to create an instance. 228 """ 229 return GolemBaseROClient( 230 rpc_url, await GolemBaseROClient._create_ws_client(ws_url) 231 ) 232 233 @staticmethod 234 async def _create_ws_client(ws_url: str) -> "AsyncWeb3": 235 ws_client: AsyncWeb3 = await AsyncWeb3(WebSocketProvider(ws_url)) 236 return ws_client 237 238 def __init__(self, rpc_url: str, ws_client: AsyncWeb3) -> None: 239 """Initialise the GolemBaseClient instance.""" 240 self._http_client = GolemBaseHttpClient(rpc_url) 241 self._ws_client = ws_client 242 243 # Keep references to async tasks we created 244 self._background_tasks = set() 245 246 def is_connected( 247 client: AsyncWeb3, 248 ) -> Callable[[bool], Coroutine[Any, Any, bool]]: 249 async def inner(show_traceback: bool) -> bool: 250 try: 251 logger.debug("Calling eth_blockNumber to test connectivity...") 252 await client.eth.get_block_number() 253 return True 254 except (OSError, ProviderConnectionError) as e: 255 logger.debug( 256 "Problem connecting to provider", exc_info=show_traceback 257 ) 258 if show_traceback: 259 raise ProviderConnectionError( 260 "Problem connecting to provider" 261 ) from e 262 return False 263 264 return inner 265 266 # The default is_connected method calls web3_clientVersion, but the web3 267 # API is not enabled on all our nodes, so let's monkey patch this to call 268 # eth_getBlockNumber instead. 269 # The method on the provider is usually not called directly, instead you 270 # can call the eponymous method on the client, which will delegate to the 271 # provider. 272 object.__setattr__( 273 self.http_client().provider, 274 "is_connected", 275 is_connected(self.http_client()), 276 ) 277 278 # Allow caching of certain methods to improve performance 279 self.http_client().provider.cache_allowed_requests = True 280 281 # https://github.com/pylint-dev/pylint/issues/3162 282 # pylint: disable=no-member 283 self.golem_base_contract = self.http_client().eth.contract( 284 address=STORAGE_ADDRESS.as_address(), 285 abi=GOLEM_BASE_ABI, 286 ) 287 for event in self.golem_base_contract.all_events(): 288 logger.debug( 289 "Registered event %s with hash %s", event.signature, event.topic 290 ) 291 292 def http_client(self) -> GolemBaseHttpClient: 293 """Get the underlying web3 http client.""" 294 return self._http_client 295 296 def ws_client(self) -> AsyncWeb3: 297 """Get the underlying web3 websocket client.""" 298 return self._ws_client 299 300 async def is_connected(self) -> bool: 301 """Check whether the client's underlying http client is connected.""" 302 return cast(bool, await self.http_client().is_connected()) # type: ignore[redundant-cast] 303 304 async def disconnect(self) -> None: 305 """ 306 Disconnect this client. 307 308 this method disconnects both the underlying http and ws clients and 309 unsubscribes from all subscriptions. 310 """ 311 await self.http_client().provider.disconnect() 312 await self.ws_client().subscription_manager.unsubscribe_all() 313 await self.ws_client().provider.disconnect() 314 315 async def get_storage_value(self, entity_key: EntityKey) -> bytes: 316 """Get the storage value stored in the given entity.""" 317 return await self.http_client().get_storage_value(entity_key) 318 319 async def get_entity_metadata(self, entity_key: EntityKey) -> EntityMetadata: 320 """Get the metadata of the given entity.""" 321 return await self.http_client().get_entity_metadata(entity_key) 322 323 async def get_entities_to_expire_at_block( 324 self, block_number: int 325 ) -> Sequence[EntityKey]: 326 """Get all entities that will expire at the given block.""" 327 return await self.http_client().get_entities_to_expire_at_block(block_number) 328 329 async def get_entity_count(self) -> int: 330 """Get the total entity count in Golem Base.""" 331 return await self.http_client().get_entity_count() 332 333 async def get_all_entity_keys(self) -> Sequence[EntityKey]: 334 """Get all entity keys in Golem Base.""" 335 return await self.http_client().get_all_entity_keys() 336 337 async def get_entities_of_owner( 338 self, owner: ChecksumAddress 339 ) -> Sequence[EntityKey]: 340 """Get all the entities owned by the given address.""" 341 return await self.http_client().get_entities_of_owner(owner) 342 343 async def query_entities(self, query: str) -> Sequence[QueryEntitiesResult]: 344 """Get all entities that satisfy the given Golem Base query.""" 345 return await self.http_client().query_entities(query) 346 347 async def watch_logs( 348 self, 349 *, 350 label: str, 351 create_callback: Callable[[CreateEntityReturnType], None] | None = None, 352 update_callback: Callable[[UpdateEntityReturnType], None] | None = None, 353 delete_callback: Callable[[EntityKey], None] | None = None, 354 extend_callback: Callable[[ExtendEntityReturnType], None] | None = None, 355 ) -> WatchLogsHandle: 356 """ 357 Subscribe to events on Golem Base. 358 359 You can pass in four different callbacks, and the right one will 360 be invoked for every create, update, delete, and extend operation. 361 """ 362 363 async def log_handler( 364 handler_context: LogsSubscriptionContext, 365 ) -> None: 366 # We only use this handler for log receipts 367 # TypeDicts cannot be checked at runtime 368 log_receipt = typing.cast(LogReceipt, handler_context.result) 369 logger.debug("New log: %s", log_receipt) 370 res = await self._process_golem_base_log_receipt(log_receipt) 371 372 if create_callback: 373 for create in res.creates: 374 create_callback(create) 375 if update_callback: 376 for update in res.updates: 377 update_callback(update) 378 if delete_callback: 379 for key in res.deletes: 380 delete_callback(key) 381 if extend_callback: 382 for extension in res.extensions: 383 extend_callback(extension) 384 385 def create_subscription(topic: HexStr) -> LogsSubscription: 386 return LogsSubscription( 387 label=f"Golem Base subscription to topic {topic} with label {label}", 388 address=self.golem_base_contract.address, 389 topics=[topic], 390 handler=log_handler, 391 # optional `handler_context` args to help parse a response 392 handler_context={}, 393 ) 394 395 event_names = [] 396 if create_callback: 397 event_names.append("GolemBaseStorageEntityCreated") 398 if update_callback: 399 event_names.append("GolemBaseStorageEntityUpdated") 400 if delete_callback: 401 event_names.append("GolemBaseStorageEntityDeleted") 402 if extend_callback: 403 event_names.append("GolemBaseStorageEntityBTLExtended") 404 405 events = list( 406 map( 407 lambda event_name: create_subscription( 408 self.golem_base_contract.get_event_by_name(event_name).topic 409 ), 410 event_names, 411 ) 412 ) 413 subscription_ids = await self._ws_client.subscription_manager.subscribe( 414 events, 415 ) 416 logger.info("Sub ID: %s", subscription_ids) 417 418 # Start a subscription loop in case there is none running 419 await self._start_subscription_loop() 420 421 async def unsubscribe() -> None: 422 await self._ws_client.subscription_manager.unsubscribe(subscription_ids) 423 424 return WatchLogsHandle(_unsubscribe=unsubscribe) 425 426 async def _start_subscription_loop(self) -> None: 427 """Create a long running task to handle subscriptions.""" 428 # The loop will finish when there are no subscriptions left, so this method 429 # gets called every time a subscription is created, and we'll check 430 # whether we need to make a new task or whether one is already running. 431 if not self._background_tasks: 432 # Start the asyncio event loop 433 task = asyncio.create_task( 434 self.ws_client().subscription_manager.handle_subscriptions() 435 ) 436 self._background_tasks.add(task) 437 438 def task_done(task: asyncio.Task[None]) -> None: 439 logger.info("Subscription background task done, removing...") 440 self._background_tasks.discard(task) 441 442 task.add_done_callback(task_done) 443 444 async def _process_golem_base_log_receipt( 445 self, 446 log_receipt: LogReceipt, 447 ) -> GolemBaseTransactionReceipt: 448 # Read the first entry of the topics array, 449 # which is the hash of the event signature, identifying the event 450 topic = AsyncWeb3.to_hex(log_receipt["topics"][0]) 451 # Look up the corresponding event 452 # If there is no such event in the ABI, it probably needs to be added 453 event = self.golem_base_contract.get_event_by_topic(topic) 454 # Use the event to process the whole log 455 event_data = event.process_log(log_receipt) 456 457 creates: list[CreateEntityReturnType] = [] 458 updates: list[UpdateEntityReturnType] = [] 459 deletes: list[EntityKey] = [] 460 extensions: list[ExtendEntityReturnType] = [] 461 462 match event_data["event"]: 463 case "GolemBaseStorageEntityCreated": 464 creates.append( 465 CreateEntityReturnType( 466 expiration_block=event_data["args"]["expirationBlock"], 467 entity_key=EntityKey( 468 GenericBytes( 469 event_data["args"]["entityKey"].to_bytes(32, "big") 470 ) 471 ), 472 ) 473 ) 474 case "GolemBaseStorageEntityUpdated": 475 updates.append( 476 UpdateEntityReturnType( 477 expiration_block=event_data["args"]["expirationBlock"], 478 entity_key=EntityKey( 479 GenericBytes( 480 event_data["args"]["entityKey"].to_bytes(32, "big") 481 ) 482 ), 483 ) 484 ) 485 case "GolemBaseStorageEntityDeleted": 486 deletes.append( 487 EntityKey( 488 GenericBytes( 489 event_data["args"]["entityKey"].to_bytes(32, "big") 490 ), 491 ) 492 ) 493 case "GolemBaseStorageEntityBTLExtended": 494 extensions.append( 495 ExtendEntityReturnType( 496 old_expiration_block=event_data["args"]["oldExpirationBlock"], 497 new_expiration_block=event_data["args"]["newExpirationBlock"], 498 entity_key=EntityKey( 499 GenericBytes( 500 event_data["args"]["entityKey"].to_bytes(32, "big") 501 ) 502 ), 503 ) 504 ) 505 506 return GolemBaseTransactionReceipt( 507 creates=creates, 508 updates=updates, 509 deletes=deletes, 510 extensions=extensions, 511 ) 512 513 async def _process_golem_base_receipt( 514 self, receipt: TxReceipt 515 ) -> GolemBaseTransactionReceipt: 516 # There doesn't seem to be a method for this in the web3 lib. 517 # The only option in the lib is to iterate over the events in the ABI 518 # and call process_receipt on each of them to try and decode the logs. 519 # This is inefficient though compared to reading the actual topic signature 520 # and immediately selecting the right event from the ABI, which is what 521 # we do here. 522 async def process_receipt( 523 receipt: TxReceipt, 524 ) -> AsyncGenerator[GolemBaseTransactionReceipt, None]: 525 for log in receipt["logs"]: 526 yield await self._process_golem_base_log_receipt(log) 527 528 creates: list[CreateEntityReturnType] = [] 529 updates: list[UpdateEntityReturnType] = [] 530 deletes: list[EntityKey] = [] 531 extensions: list[ExtendEntityReturnType] = [] 532 533 async for res in process_receipt(receipt): 534 creates.extend(res.creates) 535 updates.extend(res.updates) 536 deletes.extend(res.deletes) 537 extensions.extend(res.extensions) 538 539 return GolemBaseTransactionReceipt( 540 creates=creates, 541 updates=updates, 542 deletes=deletes, 543 extensions=extensions, 544 ) 545 546 547class GolemBaseClient(GolemBaseROClient): 548 """ 549 The Golem Base client used to interact with Golem Base. 550 551 Many useful methods are implemented directly on this type, while more 552 generic ethereum methods can be accessed through the underlying 553 web3 client that you can access with the 554 `GolemBaseClient.http_client()` 555 method. 556 """ 557 558 @staticmethod 559 async def create_rw_client( 560 rpc_url: str, ws_url: str, private_key: bytes 561 ) -> "GolemBaseClient": 562 """ 563 Create a read-write Golem Base client. 564 565 This is the preferred method to create an instance. 566 """ 567 return GolemBaseClient( 568 rpc_url, await GolemBaseROClient._create_ws_client(ws_url), private_key 569 ) 570 571 @staticmethod 572 async def create( 573 rpc_url: str, ws_url: str, private_key: bytes 574 ) -> "GolemBaseClient": 575 """ 576 Create a read-write Golem Base client. 577 578 This method is deprecated in favour of `GolemBaseClient.create_rw_client()`. 579 """ 580 return await GolemBaseClient.create_rw_client(rpc_url, ws_url, private_key) 581 582 def __init__(self, rpc_url: str, ws_client: AsyncWeb3, private_key: bytes) -> None: 583 """Initialise the GolemBaseClient instance.""" 584 super().__init__(rpc_url, ws_client) 585 586 # Set up the ethereum account 587 self.account = self.http_client().eth.account.from_key(private_key) 588 # Inject a middleware that will sign transactions with the account that 589 # we created 590 self.http_client().middleware_onion.inject( 591 # pylint doesn't detect nested @curry annotations properly... 592 # pylint: disable=no-value-for-parameter 593 SignAndSendRawMiddlewareBuilder.build(self.account), 594 layer=0, 595 ) 596 # Set the account as the default, so we don't need to specify the from field 597 # every time 598 self.http_client().eth.default_account = self.account.address 599 logger.debug("Using account: %s", self.account.address) 600 601 def get_account_address(self) -> ChecksumAddress: 602 """Get the address associated with the private key of this client.""" 603 return cast(ChecksumAddress, self.account.address) 604 605 async def create_entities( 606 self, 607 creates: Sequence[GolemBaseCreate], 608 *, 609 gas: int | None = None, 610 maxFeePerGas: Wei | None = None, 611 maxPriorityFeePerGas: Wei | None = None, 612 ) -> Sequence[CreateEntityReturnType]: 613 """Create entities in Golem Base.""" 614 return ( 615 await self.send_transaction( 616 creates=creates, 617 gas=gas, 618 maxFeePerGas=maxFeePerGas, 619 maxPriorityFeePerGas=maxPriorityFeePerGas, 620 ) 621 ).creates 622 623 async def update_entities( 624 self, 625 updates: Sequence[GolemBaseUpdate], 626 *, 627 gas: int | None = None, 628 maxFeePerGas: Wei | None = None, 629 maxPriorityFeePerGas: Wei | None = None, 630 ) -> Sequence[UpdateEntityReturnType]: 631 """Update entities in Golem Base.""" 632 return ( 633 await self.send_transaction( 634 updates=updates, 635 gas=gas, 636 maxFeePerGas=maxFeePerGas, 637 maxPriorityFeePerGas=maxPriorityFeePerGas, 638 ) 639 ).updates 640 641 async def delete_entities( 642 self, 643 deletes: Sequence[GolemBaseDelete], 644 *, 645 gas: int | None = None, 646 maxFeePerGas: Wei | None = None, 647 maxPriorityFeePerGas: Wei | None = None, 648 ) -> Sequence[EntityKey]: 649 """Delete entities from Golem Base.""" 650 return ( 651 await self.send_transaction( 652 deletes=deletes, 653 gas=gas, 654 maxFeePerGas=maxFeePerGas, 655 maxPriorityFeePerGas=maxPriorityFeePerGas, 656 ) 657 ).deletes 658 659 async def extend_entities( 660 self, 661 extensions: Sequence[GolemBaseExtend], 662 *, 663 gas: int | None = None, 664 maxFeePerGas: Wei | None = None, 665 maxPriorityFeePerGas: Wei | None = None, 666 ) -> Sequence[ExtendEntityReturnType]: 667 """Extend the BTL of entities in Golem Base.""" 668 return ( 669 await self.send_transaction( 670 extensions=extensions, 671 gas=gas, 672 maxFeePerGas=maxFeePerGas, 673 maxPriorityFeePerGas=maxPriorityFeePerGas, 674 ) 675 ).extensions 676 677 async def send_transaction( 678 self, 679 *, 680 creates: Sequence[GolemBaseCreate] | None = None, 681 updates: Sequence[GolemBaseUpdate] | None = None, 682 deletes: Sequence[GolemBaseDelete] | None = None, 683 extensions: Sequence[GolemBaseExtend] | None = None, 684 gas: int | None = None, 685 maxFeePerGas: Wei | None = None, 686 maxPriorityFeePerGas: Wei | None = None, 687 ) -> GolemBaseTransactionReceipt: 688 """ 689 Send a generic transaction to Golem Base. 690 691 This transaction can contain multiple create, update, delete and 692 extend operations. 693 """ 694 tx = GolemBaseTransaction( 695 creates=creates, 696 updates=updates, 697 deletes=deletes, 698 extensions=extensions, 699 gas=gas, 700 maxFeePerGas=maxFeePerGas, 701 maxPriorityFeePerGas=maxPriorityFeePerGas, 702 ) 703 return await self._send_gb_transaction(tx) 704 705 async def _send_gb_transaction( 706 self, tx: GolemBaseTransaction 707 ) -> GolemBaseTransactionReceipt: 708 txData: TxParams = { 709 # https://github.com/pylint-dev/pylint/issues/3162 710 # pylint: disable=no-member 711 "to": STORAGE_ADDRESS.as_address(), 712 "value": AsyncWeb3.to_wei(0, "ether"), 713 "data": rlp_encode_transaction(tx), 714 } 715 716 if tx.gas: 717 txData |= {"gas": tx.gas} 718 if tx.maxFeePerGas: 719 txData |= {"maxFeePerGas": tx.maxFeePerGas} 720 if tx.maxPriorityFeePerGas: 721 txData |= {"maxPriorityFeePerGas": tx.maxPriorityFeePerGas} 722 723 txhash = await self.http_client().eth.send_transaction(txData) 724 receipt = await self.http_client().eth.wait_for_transaction_receipt(txhash) 725 726 # If we get a receipt and the transaction was failed, we run the same 727 # transaction with eth_call, which will simulate it and get us back the 728 # error that was reported by geth. 729 # Otherwise the error is not actually present in the receipt and so we 730 # don't have something useful to present to the user. 731 # This only happens when the gas price was explicitly provided, since 732 # otherwise there will be a call to eth_estimateGas, which will fail with 733 # the same error message that we would get here (and so we'll never actually 734 # get to submitting the transaction). 735 # The status in the receipt is either 0x0 for failed or 0x1 for success. 736 if not int(receipt["status"]): 737 # This call will lead to an exception, but that's OK, what we want 738 # is to raise a useful exception to the user with an error message. 739 try: 740 await self.http_client().eth.call(txData) 741 except Web3RPCError as e: 742 if e.rpc_response: 743 error = e.rpc_response["error"]["message"] 744 raise Exception( 745 f"Error while processing transaction: {error}" 746 ) from e 747 else: 748 raise e 749 750 return await self._process_golem_base_receipt(receipt)
54@dataclass(frozen=True) 55class Annotation(Generic[V]): 56 """Class to represent generic annotations.""" 57 58 key: str 59 value: V 60 61 # @override 62 def __repr__(self) -> str: 63 """Encode annotation as a string.""" 64 return f"{type(self).__name__}({self.key} -> {self.value})"
Class to represent generic annotations.
145@dataclass(frozen=True) 146class CreateEntityReturnType: 147 """The return type of a Golem Base create operation.""" 148 149 expiration_block: int 150 entity_key: EntityKey
The return type of a Golem Base create operation.
180@dataclass(frozen=True) 181class EntityMetadata: 182 """A class representing entity metadata.""" 183 184 entity_key: EntityKey 185 owner: Address 186 expires_at_block: int 187 string_annotations: Sequence[Annotation[str]] 188 numeric_annotations: Sequence[Annotation[int]]
A class representing entity metadata.
161@dataclass(frozen=True) 162class ExtendEntityReturnType: 163 """The return type of a Golem Base extend operation.""" 164 165 old_expiration_block: int 166 new_expiration_block: int 167 entity_key: EntityKey
The return type of a Golem Base extend operation.
18@dataclass(frozen=True) 19class GenericBytes: 20 """Class to represent bytes that can be converted to more meaningful types.""" 21 22 generic_bytes: bytes 23 24 def as_hex_string(self) -> HexStr: 25 """Convert this instance to a hexadecimal string.""" 26 return HexStr("0x" + self.generic_bytes.hex()) 27 28 def as_address(self) -> ChecksumAddress: 29 """Convert this instance to a `eth_typing.ChecksumAddress`.""" 30 return AsyncWeb3.to_checksum_address(self.as_hex_string()) 31 32 # @override 33 def __repr__(self) -> str: 34 """Encode bytes as a string.""" 35 return f"{type(self).__name__}({self.as_hex_string()})" 36 37 @staticmethod 38 def from_hex_string(hexstr: str) -> "GenericBytes": 39 """Create a `GenericBytes` instance from a hexadecimal string.""" 40 assert hexstr.startswith("0x") 41 assert len(hexstr) % 2 == 0 42 43 return GenericBytes(bytes.fromhex(hexstr[2:]))
Class to represent bytes that can be converted to more meaningful types.
24 def as_hex_string(self) -> HexStr: 25 """Convert this instance to a hexadecimal string.""" 26 return HexStr("0x" + self.generic_bytes.hex())
Convert this instance to a hexadecimal string.
28 def as_address(self) -> ChecksumAddress: 29 """Convert this instance to a `eth_typing.ChecksumAddress`.""" 30 return AsyncWeb3.to_checksum_address(self.as_hex_string())
Convert this instance to a eth_typing.ChecksumAddress.
37 @staticmethod 38 def from_hex_string(hexstr: str) -> "GenericBytes": 39 """Create a `GenericBytes` instance from a hexadecimal string.""" 40 assert hexstr.startswith("0x") 41 assert len(hexstr) % 2 == 0 42 43 return GenericBytes(bytes.fromhex(hexstr[2:]))
Create a GenericBytes instance from a hexadecimal string.
67@dataclass(frozen=True) 68class GolemBaseCreate: 69 """Class to represent a create operation in Golem Base.""" 70 71 data: bytes 72 btl: int 73 string_annotations: Sequence[Annotation[str]] 74 numeric_annotations: Sequence[Annotation[int]]
Class to represent a create operation in Golem Base.
88@dataclass(frozen=True) 89class GolemBaseDelete: 90 """Class to represent a delete operation in Golem Base.""" 91 92 entity_key: EntityKey
Class to represent a delete operation in Golem Base.
95@dataclass(frozen=True) 96class GolemBaseExtend: 97 """Class to represent a BTL extend operation in Golem Base.""" 98 99 entity_key: EntityKey 100 number_of_blocks: int
Class to represent a BTL extend operation in Golem Base.
103@dataclass(frozen=True) 104class GolemBaseTransaction: 105 """ 106 Class to represent a transaction in Golem Base. 107 108 A transaction consist of one or more 109 `GolemBaseCreate`, 110 `GolemBaseUpdate`, 111 `GolemBaseDelete` and 112 `GolemBaseExtend` 113 operations. 114 """ 115 116 def __init__( 117 self, 118 *, 119 creates: Sequence[GolemBaseCreate] | None = None, 120 updates: Sequence[GolemBaseUpdate] | None = None, 121 deletes: Sequence[GolemBaseDelete] | None = None, 122 extensions: Sequence[GolemBaseExtend] | None = None, 123 gas: int | None = None, 124 maxFeePerGas: Wei | None = None, 125 maxPriorityFeePerGas: Wei | None = None, 126 ): 127 """Initialise the GolemBaseTransaction instance.""" 128 object.__setattr__(self, "creates", creates or []) 129 object.__setattr__(self, "updates", updates or []) 130 object.__setattr__(self, "deletes", deletes or []) 131 object.__setattr__(self, "extensions", extensions or []) 132 object.__setattr__(self, "gas", gas) 133 object.__setattr__(self, "maxFeePerGas", maxFeePerGas) 134 object.__setattr__(self, "maxPriorityFeePerGas", maxPriorityFeePerGas) 135 136 creates: Sequence[GolemBaseCreate] 137 updates: Sequence[GolemBaseUpdate] 138 deletes: Sequence[GolemBaseDelete] 139 extensions: Sequence[GolemBaseExtend] 140 gas: int | None 141 maxFeePerGas: Wei | None 142 maxPriorityFeePerGas: Wei | None
Class to represent a transaction in Golem Base.
A transaction consist of one or more
GolemBaseCreate,
GolemBaseUpdate,
GolemBaseDelete and
GolemBaseExtend
operations.
116 def __init__( 117 self, 118 *, 119 creates: Sequence[GolemBaseCreate] | None = None, 120 updates: Sequence[GolemBaseUpdate] | None = None, 121 deletes: Sequence[GolemBaseDelete] | None = None, 122 extensions: Sequence[GolemBaseExtend] | None = None, 123 gas: int | None = None, 124 maxFeePerGas: Wei | None = None, 125 maxPriorityFeePerGas: Wei | None = None, 126 ): 127 """Initialise the GolemBaseTransaction instance.""" 128 object.__setattr__(self, "creates", creates or []) 129 object.__setattr__(self, "updates", updates or []) 130 object.__setattr__(self, "deletes", deletes or []) 131 object.__setattr__(self, "extensions", extensions or []) 132 object.__setattr__(self, "gas", gas) 133 object.__setattr__(self, "maxFeePerGas", maxFeePerGas) 134 object.__setattr__(self, "maxPriorityFeePerGas", maxPriorityFeePerGas)
Initialise the GolemBaseTransaction instance.
170@dataclass(frozen=True) 171class GolemBaseTransactionReceipt: 172 """The return type of a Golem Base transaction.""" 173 174 creates: Sequence[CreateEntityReturnType] 175 updates: Sequence[UpdateEntityReturnType] 176 extensions: Sequence[ExtendEntityReturnType] 177 deletes: Sequence[EntityKey]
The return type of a Golem Base transaction.
77@dataclass(frozen=True) 78class GolemBaseUpdate: 79 """Class to represent an update operation in Golem Base.""" 80 81 entity_key: EntityKey 82 data: bytes 83 btl: int 84 string_annotations: Sequence[Annotation[str]] 85 numeric_annotations: Sequence[Annotation[int]]
Class to represent an update operation in Golem Base.
191@dataclass(frozen=True) 192class QueryEntitiesResult: 193 """A class representing the return value of a Golem Base query.""" 194 195 entity_key: EntityKey 196 storage_value: bytes
A class representing the return value of a Golem Base query.
153@dataclass(frozen=True) 154class UpdateEntityReturnType: 155 """The return type of a Golem Base update operation.""" 156 157 expiration_block: int 158 entity_key: EntityKey
The return type of a Golem Base update operation.
199@dataclass(frozen=True) 200class WatchLogsHandle: 201 """ 202 Class returned by `GolemBaseClient.watch_logs`. 203 204 Allows you to unsubscribe from the associated subscription. 205 """ 206 207 _unsubscribe: Callable[[], Coroutine[Any, Any, None]] 208 209 async def unsubscribe(self) -> None: 210 """Unsubscribe from this subscription.""" 211 await self._unsubscribe()
Class returned by GolemBaseClient.watch_logs.
Allows you to unsubscribe from the associated subscription.
21async def decrypt_wallet() -> bytes: 22 """Decrypts the wallet and returns the private key bytes.""" 23 if not WALLET_PATH.exists(): 24 raise WalletError(f"Expected wallet file to exist at '{WALLET_PATH}'") 25 26 async with await anyio.open_file( 27 WALLET_PATH, 28 "r", 29 ) as f: 30 keyfile_json = json.loads(await f.read()) 31 32 if not sys.stdin.isatty(): 33 password = sys.stdin.read().rstrip() 34 else: 35 password = getpass.getpass("Enter password to decrypt wallet: ") 36 37 try: 38 print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") 39 private_key = Account.decrypt(keyfile_json, password) 40 print("Successfully decrypted wallet") 41 except ValueError as e: 42 raise WalletError("Incorrect password or corrupted wallet file.") from e 43 44 return cast(bytes, private_key)
Decrypts the wallet and returns the private key bytes.
Base class for wallet-related errors.
548class GolemBaseClient(GolemBaseROClient): 549 """ 550 The Golem Base client used to interact with Golem Base. 551 552 Many useful methods are implemented directly on this type, while more 553 generic ethereum methods can be accessed through the underlying 554 web3 client that you can access with the 555 `GolemBaseClient.http_client()` 556 method. 557 """ 558 559 @staticmethod 560 async def create_rw_client( 561 rpc_url: str, ws_url: str, private_key: bytes 562 ) -> "GolemBaseClient": 563 """ 564 Create a read-write Golem Base client. 565 566 This is the preferred method to create an instance. 567 """ 568 return GolemBaseClient( 569 rpc_url, await GolemBaseROClient._create_ws_client(ws_url), private_key 570 ) 571 572 @staticmethod 573 async def create( 574 rpc_url: str, ws_url: str, private_key: bytes 575 ) -> "GolemBaseClient": 576 """ 577 Create a read-write Golem Base client. 578 579 This method is deprecated in favour of `GolemBaseClient.create_rw_client()`. 580 """ 581 return await GolemBaseClient.create_rw_client(rpc_url, ws_url, private_key) 582 583 def __init__(self, rpc_url: str, ws_client: AsyncWeb3, private_key: bytes) -> None: 584 """Initialise the GolemBaseClient instance.""" 585 super().__init__(rpc_url, ws_client) 586 587 # Set up the ethereum account 588 self.account = self.http_client().eth.account.from_key(private_key) 589 # Inject a middleware that will sign transactions with the account that 590 # we created 591 self.http_client().middleware_onion.inject( 592 # pylint doesn't detect nested @curry annotations properly... 593 # pylint: disable=no-value-for-parameter 594 SignAndSendRawMiddlewareBuilder.build(self.account), 595 layer=0, 596 ) 597 # Set the account as the default, so we don't need to specify the from field 598 # every time 599 self.http_client().eth.default_account = self.account.address 600 logger.debug("Using account: %s", self.account.address) 601 602 def get_account_address(self) -> ChecksumAddress: 603 """Get the address associated with the private key of this client.""" 604 return cast(ChecksumAddress, self.account.address) 605 606 async def create_entities( 607 self, 608 creates: Sequence[GolemBaseCreate], 609 *, 610 gas: int | None = None, 611 maxFeePerGas: Wei | None = None, 612 maxPriorityFeePerGas: Wei | None = None, 613 ) -> Sequence[CreateEntityReturnType]: 614 """Create entities in Golem Base.""" 615 return ( 616 await self.send_transaction( 617 creates=creates, 618 gas=gas, 619 maxFeePerGas=maxFeePerGas, 620 maxPriorityFeePerGas=maxPriorityFeePerGas, 621 ) 622 ).creates 623 624 async def update_entities( 625 self, 626 updates: Sequence[GolemBaseUpdate], 627 *, 628 gas: int | None = None, 629 maxFeePerGas: Wei | None = None, 630 maxPriorityFeePerGas: Wei | None = None, 631 ) -> Sequence[UpdateEntityReturnType]: 632 """Update entities in Golem Base.""" 633 return ( 634 await self.send_transaction( 635 updates=updates, 636 gas=gas, 637 maxFeePerGas=maxFeePerGas, 638 maxPriorityFeePerGas=maxPriorityFeePerGas, 639 ) 640 ).updates 641 642 async def delete_entities( 643 self, 644 deletes: Sequence[GolemBaseDelete], 645 *, 646 gas: int | None = None, 647 maxFeePerGas: Wei | None = None, 648 maxPriorityFeePerGas: Wei | None = None, 649 ) -> Sequence[EntityKey]: 650 """Delete entities from Golem Base.""" 651 return ( 652 await self.send_transaction( 653 deletes=deletes, 654 gas=gas, 655 maxFeePerGas=maxFeePerGas, 656 maxPriorityFeePerGas=maxPriorityFeePerGas, 657 ) 658 ).deletes 659 660 async def extend_entities( 661 self, 662 extensions: Sequence[GolemBaseExtend], 663 *, 664 gas: int | None = None, 665 maxFeePerGas: Wei | None = None, 666 maxPriorityFeePerGas: Wei | None = None, 667 ) -> Sequence[ExtendEntityReturnType]: 668 """Extend the BTL of entities in Golem Base.""" 669 return ( 670 await self.send_transaction( 671 extensions=extensions, 672 gas=gas, 673 maxFeePerGas=maxFeePerGas, 674 maxPriorityFeePerGas=maxPriorityFeePerGas, 675 ) 676 ).extensions 677 678 async def send_transaction( 679 self, 680 *, 681 creates: Sequence[GolemBaseCreate] | None = None, 682 updates: Sequence[GolemBaseUpdate] | None = None, 683 deletes: Sequence[GolemBaseDelete] | None = None, 684 extensions: Sequence[GolemBaseExtend] | None = None, 685 gas: int | None = None, 686 maxFeePerGas: Wei | None = None, 687 maxPriorityFeePerGas: Wei | None = None, 688 ) -> GolemBaseTransactionReceipt: 689 """ 690 Send a generic transaction to Golem Base. 691 692 This transaction can contain multiple create, update, delete and 693 extend operations. 694 """ 695 tx = GolemBaseTransaction( 696 creates=creates, 697 updates=updates, 698 deletes=deletes, 699 extensions=extensions, 700 gas=gas, 701 maxFeePerGas=maxFeePerGas, 702 maxPriorityFeePerGas=maxPriorityFeePerGas, 703 ) 704 return await self._send_gb_transaction(tx) 705 706 async def _send_gb_transaction( 707 self, tx: GolemBaseTransaction 708 ) -> GolemBaseTransactionReceipt: 709 txData: TxParams = { 710 # https://github.com/pylint-dev/pylint/issues/3162 711 # pylint: disable=no-member 712 "to": STORAGE_ADDRESS.as_address(), 713 "value": AsyncWeb3.to_wei(0, "ether"), 714 "data": rlp_encode_transaction(tx), 715 } 716 717 if tx.gas: 718 txData |= {"gas": tx.gas} 719 if tx.maxFeePerGas: 720 txData |= {"maxFeePerGas": tx.maxFeePerGas} 721 if tx.maxPriorityFeePerGas: 722 txData |= {"maxPriorityFeePerGas": tx.maxPriorityFeePerGas} 723 724 txhash = await self.http_client().eth.send_transaction(txData) 725 receipt = await self.http_client().eth.wait_for_transaction_receipt(txhash) 726 727 # If we get a receipt and the transaction was failed, we run the same 728 # transaction with eth_call, which will simulate it and get us back the 729 # error that was reported by geth. 730 # Otherwise the error is not actually present in the receipt and so we 731 # don't have something useful to present to the user. 732 # This only happens when the gas price was explicitly provided, since 733 # otherwise there will be a call to eth_estimateGas, which will fail with 734 # the same error message that we would get here (and so we'll never actually 735 # get to submitting the transaction). 736 # The status in the receipt is either 0x0 for failed or 0x1 for success. 737 if not int(receipt["status"]): 738 # This call will lead to an exception, but that's OK, what we want 739 # is to raise a useful exception to the user with an error message. 740 try: 741 await self.http_client().eth.call(txData) 742 except Web3RPCError as e: 743 if e.rpc_response: 744 error = e.rpc_response["error"]["message"] 745 raise Exception( 746 f"Error while processing transaction: {error}" 747 ) from e 748 else: 749 raise e 750 751 return await self._process_golem_base_receipt(receipt)
The Golem Base client used to interact with Golem Base.
Many useful methods are implemented directly on this type, while more
generic ethereum methods can be accessed through the underlying
web3 client that you can access with the
GolemBaseClient.http_client()
method.
583 def __init__(self, rpc_url: str, ws_client: AsyncWeb3, private_key: bytes) -> None: 584 """Initialise the GolemBaseClient instance.""" 585 super().__init__(rpc_url, ws_client) 586 587 # Set up the ethereum account 588 self.account = self.http_client().eth.account.from_key(private_key) 589 # Inject a middleware that will sign transactions with the account that 590 # we created 591 self.http_client().middleware_onion.inject( 592 # pylint doesn't detect nested @curry annotations properly... 593 # pylint: disable=no-value-for-parameter 594 SignAndSendRawMiddlewareBuilder.build(self.account), 595 layer=0, 596 ) 597 # Set the account as the default, so we don't need to specify the from field 598 # every time 599 self.http_client().eth.default_account = self.account.address 600 logger.debug("Using account: %s", self.account.address)
Initialise the GolemBaseClient instance.
559 @staticmethod 560 async def create_rw_client( 561 rpc_url: str, ws_url: str, private_key: bytes 562 ) -> "GolemBaseClient": 563 """ 564 Create a read-write Golem Base client. 565 566 This is the preferred method to create an instance. 567 """ 568 return GolemBaseClient( 569 rpc_url, await GolemBaseROClient._create_ws_client(ws_url), private_key 570 )
Create a read-write Golem Base client.
This is the preferred method to create an instance.
572 @staticmethod 573 async def create( 574 rpc_url: str, ws_url: str, private_key: bytes 575 ) -> "GolemBaseClient": 576 """ 577 Create a read-write Golem Base client. 578 579 This method is deprecated in favour of `GolemBaseClient.create_rw_client()`. 580 """ 581 return await GolemBaseClient.create_rw_client(rpc_url, ws_url, private_key)
Create a read-write Golem Base client.
This method is deprecated in favour of GolemBaseClient.create_rw_client().
602 def get_account_address(self) -> ChecksumAddress: 603 """Get the address associated with the private key of this client.""" 604 return cast(ChecksumAddress, self.account.address)
Get the address associated with the private key of this client.
606 async def create_entities( 607 self, 608 creates: Sequence[GolemBaseCreate], 609 *, 610 gas: int | None = None, 611 maxFeePerGas: Wei | None = None, 612 maxPriorityFeePerGas: Wei | None = None, 613 ) -> Sequence[CreateEntityReturnType]: 614 """Create entities in Golem Base.""" 615 return ( 616 await self.send_transaction( 617 creates=creates, 618 gas=gas, 619 maxFeePerGas=maxFeePerGas, 620 maxPriorityFeePerGas=maxPriorityFeePerGas, 621 ) 622 ).creates
Create entities in Golem Base.
624 async def update_entities( 625 self, 626 updates: Sequence[GolemBaseUpdate], 627 *, 628 gas: int | None = None, 629 maxFeePerGas: Wei | None = None, 630 maxPriorityFeePerGas: Wei | None = None, 631 ) -> Sequence[UpdateEntityReturnType]: 632 """Update entities in Golem Base.""" 633 return ( 634 await self.send_transaction( 635 updates=updates, 636 gas=gas, 637 maxFeePerGas=maxFeePerGas, 638 maxPriorityFeePerGas=maxPriorityFeePerGas, 639 ) 640 ).updates
Update entities in Golem Base.
642 async def delete_entities( 643 self, 644 deletes: Sequence[GolemBaseDelete], 645 *, 646 gas: int | None = None, 647 maxFeePerGas: Wei | None = None, 648 maxPriorityFeePerGas: Wei | None = None, 649 ) -> Sequence[EntityKey]: 650 """Delete entities from Golem Base.""" 651 return ( 652 await self.send_transaction( 653 deletes=deletes, 654 gas=gas, 655 maxFeePerGas=maxFeePerGas, 656 maxPriorityFeePerGas=maxPriorityFeePerGas, 657 ) 658 ).deletes
Delete entities from Golem Base.
660 async def extend_entities( 661 self, 662 extensions: Sequence[GolemBaseExtend], 663 *, 664 gas: int | None = None, 665 maxFeePerGas: Wei | None = None, 666 maxPriorityFeePerGas: Wei | None = None, 667 ) -> Sequence[ExtendEntityReturnType]: 668 """Extend the BTL of entities in Golem Base.""" 669 return ( 670 await self.send_transaction( 671 extensions=extensions, 672 gas=gas, 673 maxFeePerGas=maxFeePerGas, 674 maxPriorityFeePerGas=maxPriorityFeePerGas, 675 ) 676 ).extensions
Extend the BTL of entities in Golem Base.
678 async def send_transaction( 679 self, 680 *, 681 creates: Sequence[GolemBaseCreate] | None = None, 682 updates: Sequence[GolemBaseUpdate] | None = None, 683 deletes: Sequence[GolemBaseDelete] | None = None, 684 extensions: Sequence[GolemBaseExtend] | None = None, 685 gas: int | None = None, 686 maxFeePerGas: Wei | None = None, 687 maxPriorityFeePerGas: Wei | None = None, 688 ) -> GolemBaseTransactionReceipt: 689 """ 690 Send a generic transaction to Golem Base. 691 692 This transaction can contain multiple create, update, delete and 693 extend operations. 694 """ 695 tx = GolemBaseTransaction( 696 creates=creates, 697 updates=updates, 698 deletes=deletes, 699 extensions=extensions, 700 gas=gas, 701 maxFeePerGas=maxFeePerGas, 702 maxPriorityFeePerGas=maxPriorityFeePerGas, 703 ) 704 return await self._send_gb_transaction(tx)
Send a generic transaction to Golem Base.
This transaction can contain multiple create, update, delete and extend operations.