# pyWalletConnect
![pyWalletConnect logo](logo.png)
### A WalletConnect implementation for wallets in Python
A Python3 library to link a wallet with a WalletConnect web3 app. This library connects a Python wallet with a web3 app online, using the WalletConnect standard.
Thanks to WalletConnect, a Dapp is able to send JSON-RPC call requests to be handled by the wallet, remotely signing transactions or messages. Using WalletConnect, the wallet is a JSON-RPC service that the dapp can query through an encrypted tunnel and an online relay. This library is built for the wallet part, which establishes a link with the dapp and receives requests.
pyWalletConnect manages automatically on its own all the WalletConnect stack :
```
WalletConnect
|
Topics mgmt
|
JSON-RPC
|
EncryptedTunnel
|
WebSocket
|
HTTP
|
TLS
|
Socket
```
## Installation and requirements
Works with Python >= 3.7.
### Installation of this library
Easiest way :
`python3 -m pip install pyWalletConnect`
From sources, download and run in this directory :
`python3 -m pip install .`
### Use
Instanciate with `pywalletconnect.WCClient.from_wc_uri`, then use methods functions of this object.
Basic example :
```python
from pywalletconnect import WCClient, WCClientInvalidOption
# Input the wc URI
string_uri = input("Input the WalletConnect URI : ")
WCClient.set_wallet_metadata(WALLET_METADATA) # Optional, else identify pyWalletConnect as the wallet
WCClient.set_project_id(WALLETCONNECT_PROJECT_ID) # Required for v2
WCClient.set_origin(WALLETCONNECT_ORIGIN_DOMAIN) # Optional for v2
try:
wallet_dapp = WCClient.from_wc_uri(string_uri)
except WCClientInvalidOption as exc:
# In case error in the wc URI provided
if hasattr(wallet_dapp, "wc_client"):
wallet_dapp.close()
raise InvalidOption(exc)
# Wait for the sessionRequest info
# Can throw WCClientException "sessionRequest timeout"
req_id, chain_ids, request_info = wallet_dapp.open_session()
if str(account.chainID) not in chain_ids:
# Chain id mismatch
wallet_dapp.close()
raise InvalidOption("Chain ID from Dapp is not the same as the wallet.")
# Display to the user request details provided by the Dapp.
user_ok = input(f"WalletConnect link request from : {request_info['name']}. Approve? [y/N]")
if user_ok.lower() == "y":
# User approved
wallet_dapp.reply_session_request(req_id, account.chainID, account.address)
# Now the session with the Dapp is opened
<...>
else:
# User rejected
wclient.reject_session_request(req_id)
wallet_dapp.close()
raise UserInteration("user rejected the dapp connection request.")
```
There's a basic minimal working CLI demo at: https://gist.github.com/bitlogik/89b41bb60443c041704f82bcd9b43901
pyWalletConnect maintains a TLS WebSocket opened with the host relay. It builds an internal pool of received request messages from the dapp.
Once the session is opened, you can read the pending messages received from the Dapp from time to time. And then your wallet app can process these requests, and send back the reply.
Use a daemon thread timer for example, to call the `get_message()` method in a short time frequency. 3-6 seconds is an acceptable delay. This can also be performed in a blocking *for* loop with a sleeping time. Then process the Dapp queries for further user wallet actions.
Remember to keep track of the request id, as it is needed for `.reply(req_id, result)` ultimately when sending the processing result back to the dapp service. One way is to provide the id in argument in your processing methods. Also this can be done with global or shared parameters.
When a WCClient object (created from a WC link) is closed or deleted, it will automatically send to the dapp a closing session message.
```python
def process_sendtransaction(call_id, tx):
# Processing the RPC query eth_sendTransaction
# Collect the user approval about the tx query
< Accept (tx) ? >
if approved :
# Build and sign the provided transaction
<...>
# Broadcast the tx
# Provide the transaction id as result
return "0x..." # Tx id
def watch_messages():
# Watch for messages received.
# For WalletConnect calls reading.
# Read all the message requests received from the dapp.
# Then dispatch to the wallet service handlers.
# get_message gives (id, method, params) or (None, "", [])
wc_message = wallet_dapp.get_message()
# Loop in the waiting messages pool, until depleted
while wc_message[0] is not None:
# Read a WalletConnect call message available
id_request = wc_message[0]
method = wc_message[1]
parameters = wc_message[2]
if method == "wc_sessionRequest" or method == "wc_sessionPayload":
# Read if v2 and convert to v1 format
if parameters.get("request"):
method = parameters["request"].get("method")
parameters = parameters["request"].get("params")
if "wc_sessionUpdate" == method:
if parameters[0].get("approved") is False:
raise Exception("Disconnected by the Dapp.")
# v2 disconnect
if "wc_sessionDelete" == method:
raise Exception("Disconnected by the Dapp.")
# Dispatch query processing
elif "eth_signTypedData" == method:
result = process_signtypeddata(id_request, parameters[1])
wallet_dapp.reply(call_id, result)
elif "eth_sendTransaction" == method:
approve_ask = input("Approve (y/N)?: ").lower()
if approve_ask == 'y':
result = process_sendtransaction(id_request, parameters[0])
wallet_dapp.reply(call_id, result)
else:
wallet_dapp.reply_error(call_id, "User rejected request.", 4001)
elif "eth_sign" == method:
approve_ask = input("Approve (y/N)?: ").lower()
if approve_ask == 'y':
result = process_signtransaction(parameters[1])
wallet_dapp.reply(call_id, result)
else:
wallet_dapp.reject(call_id)
<...>
# Next loop
wc_message = wallet_dapp.get_message()
# GUI timer repeated or threading daemon
# Will call watch_messages every 4 seconds
apptimer = Timer(4000)
# Call watch_messages when expires periodically
apptimer.notify = watch_messages
```
See also the [RPC methods in WalletConnect](https://docs.walletconnect.org/v/1.0/json-rpc-api-methods/ethereum) to know more about the expected result regarding a specific RPC call.
## Interface methods of WCClient
`WCClient.set_wallet_metadata( wallet_metadata )`
Class method to set the wallet metadata as object (v2). See [the WalletConnect standard for the format details](https://docs.walletconnect.com/2.0/specs/clients/core/pairing/data-structures#metadata).
Optional. If not provided, when v2, it sends the default pyWalletConnect metadata as wallet identification.
`WCClient.set_wallet_namespace( wallet_namespace )`
Class method to set the wallet [namespace](https://docs.walletconnect.com/2.0/advanced/glossary#namespaces), i.e. supported chain collection.
Only for v2, optional. Defaults to 'eip155' aka EVM-based chains.
`WCClient.set_project_id( project_id )`
Class method to set the WalletConnect project id. This is mandatory to use a project id when using WC v2 with the official central bridge relay.
`WCClient.set_origin( origin_domain )`
Class method to set the origin of the first HTTP query for websocket. Only for v2, optional.
`WCClient.from_wc_uri( wc_uri_str )`
Create a WalletConnect wallet client from a wc v1 or v2 URI. (class method constructor)
*wc_uri_str* : the wc full EIP1328 URI provided by the Dapp.
You need to call *open_session* immediately after to get the session request info.
`.close()`
Send a session close message, and close the underlying WebSocket connection.
`.get_relay_url()`
Give the page address of the WebSocket relay bridge.
`.get_message()`
Get a RPC call message from the internal waiting list. pyWalletConnect maintains an internal pool of received request messages from the dapp. And this get_message method pops out a message in a FIFO manner : the first method call provides the oldest (first) received message. It can be used like a pump : call *get_message()* until an empty response. Because it reads a message from the receiving bucket one by one.
This needs to be called periodically because this triggers the auto reconnection (When the WebSocket is abruptly disconnected by the relay).
Return : (RPCid, method, params) or (None, "", []) when no data were received since the last call (or from the initial session connection).
Non-blocking, so always returns immediately when there's no message, and returns (None, "", []).
When a v2 ping *wc_sessionPing* is received, it is automatically replied when getting it with get_message. In this case, the *get_message* method returns an empty method and no params. So filter *get_message* calls with 'id is None', means no more message left.
`.reply( req_id, result_str )`
Send a RPC response to the webapp (through the relay).
*req_id* is the JSON-RPC id of the corresponding query request, where the result belongs to. One must kept track this id from the get_message, up to this reply. So a reply result is given back with its associated call query id.
*result_str* is the result field to provide in the RPC result response.
`.reject( req_id, error_code=5002 )`
Inform the webapp that this request was rejected by the user.
*req_id* is the JSON-RPC id of the corresponding query request.
*error_code* is a rejection code to send to webapp (default 5002).
`.reply_error( req_id, message, error_code )`
Send a RPC error to the webapp (through the relay).
*req_id* is the JSON-RPC id of the corresponding query request.
*message* is a string providing a short description of the error.
*error_code* is a number that indicates the error type that occurred. See [the WalletConnect standard Error Codes](https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes).
`.open_session()`
Start a WalletConnect session : wait for the session call request message.
Must be called right after a WCClient creation.
Returns : (message RPCid, chainIDsList, peerMeta data object).
Or throws WalletConnectClientException("sessionRequest timeout")
after 8 seconds and no sessionRequest received.
chainIDsList is a list of string.
`reply_session_request( msg_id, chain_id, account_address )`
Send a session approval message, when user approved the connection session request in the wallet.
*msg_id* is the RPC id of the session approval request.
*chain_id* is the integer (or its string representation) identifying the blockchain.
*account_address* is a string of the address of the wallet account ("0x...").
`.reject_session_request( req_id )`
Send a session rejection message to the dapp (through the relay).
*req_id* is the RPC id of the session approval request.
## License
Copyright (C) 2021-2023 BitLogiK SAS
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
## Support
Open an issue in the Github repository for help about its use.
Raw data
{
"_id": null,
"home_page": "https://github.com/bitlogik/pyWalletConnect",
"name": "pyWalletConnect",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "",
"keywords": "blockchain wallet cryptography security",
"author": "BitLogiK",
"author_email": "contact@bitlogik.fr",
"download_url": "",
"platform": null,
"description": "# pyWalletConnect\r\n\r\n![pyWalletConnect logo](logo.png)\r\n\r\n### A WalletConnect implementation for wallets in Python\r\n\r\nA Python3 library to link a wallet with a WalletConnect web3 app. This library connects a Python wallet with a web3 app online, using the WalletConnect standard.\r\n\r\nThanks to WalletConnect, a Dapp is able to send JSON-RPC call requests to be handled by the wallet, remotely signing transactions or messages. Using WalletConnect, the wallet is a JSON-RPC service that the dapp can query through an encrypted tunnel and an online relay. This library is built for the wallet part, which establishes a link with the dapp and receives requests.\r\n\r\npyWalletConnect manages automatically on its own all the WalletConnect stack :\r\n\r\n```\r\nWalletConnect\r\n |\r\nTopics mgmt\r\n |\r\n JSON-RPC\r\n |\r\nEncryptedTunnel\r\n |\r\n WebSocket\r\n |\r\n HTTP\r\n |\r\n TLS\r\n |\r\n Socket\r\n```\r\n\r\n## Installation and requirements\r\n\r\nWorks with Python >= 3.7.\r\n\r\n### Installation of this library\r\n\r\nEasiest way : \r\n`python3 -m pip install pyWalletConnect` \r\n\r\nFrom sources, download and run in this directory : \r\n`python3 -m pip install .`\r\n\r\n### Use\r\n\r\nInstanciate with `pywalletconnect.WCClient.from_wc_uri`, then use methods functions of this object.\r\n\r\nBasic example :\r\n\r\n```python\r\nfrom pywalletconnect import WCClient, WCClientInvalidOption\r\n# Input the wc URI\r\nstring_uri = input(\"Input the WalletConnect URI : \")\r\nWCClient.set_wallet_metadata(WALLET_METADATA) # Optional, else identify pyWalletConnect as the wallet\r\nWCClient.set_project_id(WALLETCONNECT_PROJECT_ID) # Required for v2\r\nWCClient.set_origin(WALLETCONNECT_ORIGIN_DOMAIN) # Optional for v2\r\ntry:\r\n wallet_dapp = WCClient.from_wc_uri(string_uri)\r\nexcept WCClientInvalidOption as exc:\r\n # In case error in the wc URI provided\r\n if hasattr(wallet_dapp, \"wc_client\"):\r\n wallet_dapp.close()\r\n raise InvalidOption(exc)\r\n# Wait for the sessionRequest info\r\n# Can throw WCClientException \"sessionRequest timeout\"\r\nreq_id, chain_ids, request_info = wallet_dapp.open_session()\r\nif str(account.chainID) not in chain_ids:\r\n # Chain id mismatch\r\n wallet_dapp.close()\r\n raise InvalidOption(\"Chain ID from Dapp is not the same as the wallet.\")\r\n# Display to the user request details provided by the Dapp.\r\nuser_ok = input(f\"WalletConnect link request from : {request_info['name']}. Approve? [y/N]\")\r\nif user_ok.lower() == \"y\":\r\n # User approved\r\n wallet_dapp.reply_session_request(req_id, account.chainID, account.address)\r\n # Now the session with the Dapp is opened\r\n <...>\r\nelse:\r\n # User rejected\r\n wclient.reject_session_request(req_id)\r\n wallet_dapp.close()\r\n raise UserInteration(\"user rejected the dapp connection request.\")\r\n```\r\n\r\nThere's a basic minimal working CLI demo at: https://gist.github.com/bitlogik/89b41bb60443c041704f82bcd9b43901\r\n\r\npyWalletConnect maintains a TLS WebSocket opened with the host relay. It builds an internal pool of received request messages from the dapp.\r\n\r\nOnce the session is opened, you can read the pending messages received from the Dapp from time to time. And then your wallet app can process these requests, and send back the reply.\r\n\r\nUse a daemon thread timer for example, to call the `get_message()` method in a short time frequency. 3-6 seconds is an acceptable delay. This can also be performed in a blocking *for* loop with a sleeping time. Then process the Dapp queries for further user wallet actions.\r\n\r\nRemember to keep track of the request id, as it is needed for `.reply(req_id, result)` ultimately when sending the processing result back to the dapp service. One way is to provide the id in argument in your processing methods. Also this can be done with global or shared parameters.\r\n\r\nWhen a WCClient object (created from a WC link) is closed or deleted, it will automatically send to the dapp a closing session message.\r\n\r\n```python\r\ndef process_sendtransaction(call_id, tx):\r\n # Processing the RPC query eth_sendTransaction\r\n # Collect the user approval about the tx query\r\n < Accept (tx) ? >\r\n if approved :\r\n # Build and sign the provided transaction\r\n <...>\r\n # Broadcast the tx\r\n # Provide the transaction id as result\r\n return \"0x...\" # Tx id\r\n\r\ndef watch_messages():\r\n # Watch for messages received.\r\n # For WalletConnect calls reading.\r\n # Read all the message requests received from the dapp.\r\n # Then dispatch to the wallet service handlers.\r\n # get_message gives (id, method, params) or (None, \"\", [])\r\n wc_message = wallet_dapp.get_message()\r\n # Loop in the waiting messages pool, until depleted\r\n while wc_message[0] is not None:\r\n # Read a WalletConnect call message available\r\n id_request = wc_message[0]\r\n method = wc_message[1]\r\n parameters = wc_message[2]\r\n if method == \"wc_sessionRequest\" or method == \"wc_sessionPayload\":\r\n # Read if v2 and convert to v1 format\r\n if parameters.get(\"request\"):\r\n method = parameters[\"request\"].get(\"method\")\r\n parameters = parameters[\"request\"].get(\"params\")\r\n if \"wc_sessionUpdate\" == method:\r\n if parameters[0].get(\"approved\") is False:\r\n raise Exception(\"Disconnected by the Dapp.\")\r\n # v2 disconnect\r\n if \"wc_sessionDelete\" == method:\r\n raise Exception(\"Disconnected by the Dapp.\")\r\n # Dispatch query processing\r\n elif \"eth_signTypedData\" == method:\r\n result = process_signtypeddata(id_request, parameters[1])\r\n wallet_dapp.reply(call_id, result)\r\n elif \"eth_sendTransaction\" == method:\r\n approve_ask = input(\"Approve (y/N)?: \").lower()\r\n if approve_ask == 'y':\r\n result = process_sendtransaction(id_request, parameters[0])\r\n wallet_dapp.reply(call_id, result)\r\n else:\r\n wallet_dapp.reply_error(call_id, \"User rejected request.\", 4001)\r\n elif \"eth_sign\" == method:\r\n approve_ask = input(\"Approve (y/N)?: \").lower()\r\n if approve_ask == 'y':\r\n result = process_signtransaction(parameters[1])\r\n wallet_dapp.reply(call_id, result)\r\n else:\r\n wallet_dapp.reject(call_id)\r\n <...>\r\n # Next loop\r\n wc_message = wallet_dapp.get_message()\r\n\r\n\r\n# GUI timer repeated or threading daemon\r\n# Will call watch_messages every 4 seconds\r\napptimer = Timer(4000)\r\n# Call watch_messages when expires periodically\r\napptimer.notify = watch_messages\r\n```\r\n\r\nSee also the [RPC methods in WalletConnect](https://docs.walletconnect.org/v/1.0/json-rpc-api-methods/ethereum) to know more about the expected result regarding a specific RPC call.\r\n\r\n## Interface methods of WCClient\r\n\r\n`WCClient.set_wallet_metadata( wallet_metadata )` \r\nClass method to set the wallet metadata as object (v2). See [the WalletConnect standard for the format details](https://docs.walletconnect.com/2.0/specs/clients/core/pairing/data-structures#metadata). \r\nOptional. If not provided, when v2, it sends the default pyWalletConnect metadata as wallet identification.\r\n\r\n`WCClient.set_wallet_namespace( wallet_namespace )` \r\nClass method to set the wallet [namespace](https://docs.walletconnect.com/2.0/advanced/glossary#namespaces), i.e. supported chain collection. \r\nOnly for v2, optional. Defaults to 'eip155' aka EVM-based chains.\r\n\r\n`WCClient.set_project_id( project_id )` \r\nClass method to set the WalletConnect project id. This is mandatory to use a project id when using WC v2 with the official central bridge relay.\r\n\r\n`WCClient.set_origin( origin_domain )` \r\nClass method to set the origin of the first HTTP query for websocket. Only for v2, optional.\r\n\r\n`WCClient.from_wc_uri( wc_uri_str )` \r\nCreate a WalletConnect wallet client from a wc v1 or v2 URI. (class method constructor) \r\n*wc_uri_str* : the wc full EIP1328 URI provided by the Dapp. \r\nYou need to call *open_session* immediately after to get the session request info.\r\n\r\n`.close()` \r\nSend a session close message, and close the underlying WebSocket connection.\r\n\r\n`.get_relay_url()` \r\nGive the page address of the WebSocket relay bridge.\r\n\r\n`.get_message()` \r\nGet a RPC call message from the internal waiting list. pyWalletConnect maintains an internal pool of received request messages from the dapp. And this get_message method pops out a message in a FIFO manner : the first method call provides the oldest (first) received message. It can be used like a pump : call *get_message()* until an empty response. Because it reads a message from the receiving bucket one by one. \r\nThis needs to be called periodically because this triggers the auto reconnection (When the WebSocket is abruptly disconnected by the relay). \r\nReturn : (RPCid, method, params) or (None, \"\", []) when no data were received since the last call (or from the initial session connection). \r\nNon-blocking, so always returns immediately when there's no message, and returns (None, \"\", []). \r\nWhen a v2 ping *wc_sessionPing* is received, it is automatically replied when getting it with get_message. In this case, the *get_message* method returns an empty method and no params. So filter *get_message* calls with 'id is None', means no more message left.\r\n\r\n`.reply( req_id, result_str )` \r\nSend a RPC response to the webapp (through the relay). \r\n*req_id* is the JSON-RPC id of the corresponding query request, where the result belongs to. One must kept track this id from the get_message, up to this reply. So a reply result is given back with its associated call query id. \r\n*result_str* is the result field to provide in the RPC result response.\r\n\r\n`.reject( req_id, error_code=5002 )` \r\nInform the webapp that this request was rejected by the user. \r\n*req_id* is the JSON-RPC id of the corresponding query request. \r\n*error_code* is a rejection code to send to webapp (default 5002). \r\n\r\n`.reply_error( req_id, message, error_code )` \r\nSend a RPC error to the webapp (through the relay). \r\n*req_id* is the JSON-RPC id of the corresponding query request. \r\n*message* is a string providing a short description of the error. \r\n*error_code* is a number that indicates the error type that occurred. See [the WalletConnect standard Error Codes](https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes). \r\n\r\n`.open_session()` \r\nStart a WalletConnect session : wait for the session call request message. \r\nMust be called right after a WCClient creation. \r\nReturns : (message RPCid, chainIDsList, peerMeta data object). \r\nOr throws WalletConnectClientException(\"sessionRequest timeout\")\r\nafter 8 seconds and no sessionRequest received. \r\nchainIDsList is a list of string.\r\n\r\n`reply_session_request( msg_id, chain_id, account_address )` \r\nSend a session approval message, when user approved the connection session request in the wallet. \r\n*msg_id* is the RPC id of the session approval request.\r\n*chain_id* is the integer (or its string representation) identifying the blockchain.\r\n*account_address* is a string of the address of the wallet account (\"0x...\").\r\n\r\n`.reject_session_request( req_id )` \r\nSend a session rejection message to the dapp (through the relay).\r\n*req_id* is the RPC id of the session approval request.\r\n\r\n## License\r\n\r\nCopyright (C) 2021-2023 BitLogiK SAS\r\n\r\nThis program is free software: you can redistribute it and/or modify \r\nit under the terms of the GNU General Public License as published by \r\nthe Free Software Foundation, version 3 of the License.\r\n\r\nThis program is distributed in the hope that it will be useful, \r\nbut WITHOUT ANY WARRANTY; without even the implied warranty of \r\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. \r\nSee the GNU General Public License for more details.\r\n\r\n## Support\r\n\r\nOpen an issue in the Github repository for help about its use.\r\n\r\n\r\n",
"bugtrack_url": null,
"license": "GPLv3",
"summary": "WalletConnect implementation for Python wallets",
"version": "1.6.2",
"project_urls": {
"Homepage": "https://github.com/bitlogik/pyWalletConnect"
},
"split_keywords": [
"blockchain",
"wallet",
"cryptography",
"security"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "c57f6a59651b2ae3cf3798124a5516c934b455f05a03c671e61842c44a941a08",
"md5": "3c65b91d77872c9f158c5101b1c349ba",
"sha256": "c2d7c42fbdfadd59503bbfe8eb0e5a3c5be07025dca2cc17884a868da6b05871"
},
"downloads": -1,
"filename": "pyWalletConnect-1.6.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "3c65b91d77872c9f158c5101b1c349ba",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 43500,
"upload_time": "2023-11-21T17:48:51",
"upload_time_iso_8601": "2023-11-21T17:48:51.723077Z",
"url": "https://files.pythonhosted.org/packages/c5/7f/6a59651b2ae3cf3798124a5516c934b455f05a03c671e61842c44a941a08/pyWalletConnect-1.6.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-11-21 17:48:51",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "bitlogik",
"github_project": "pyWalletConnect",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "pywalletconnect"
}