streamlit-browser-web3 0.1.0__tar.gz
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.
- streamlit_browser_web3-0.1.0/LICENSE +21 -0
- streamlit_browser_web3-0.1.0/PKG-INFO +130 -0
- streamlit_browser_web3-0.1.0/README.md +109 -0
- streamlit_browser_web3-0.1.0/pyproject.toml +38 -0
- streamlit_browser_web3-0.1.0/setup.cfg +4 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3/__init__.py +5 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3/frontend/provider/index.html +285 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3/provider.py +382 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3.egg-info/PKG-INFO +130 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3.egg-info/SOURCES.txt +11 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3.egg-info/dependency_links.txt +1 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3.egg-info/requires.txt +1 -0
- streamlit_browser_web3-0.1.0/src/streamlit_browser_web3.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streamlit-browser-web3
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Streamlit components and helpers for browser-injected EIP-1193 provider utility.
|
|
5
|
+
Project-URL: Homepage, https://github.com/luismasuelli/streamlit-browser-web3
|
|
6
|
+
Project-URL: Repository, https://github.com/luismasuelli/streamlit-browser-web3
|
|
7
|
+
Keywords: streamlit,web3,ethereum,eip-1193,metamask
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: streamlit>=1.36
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# streamlit-web3
|
|
23
|
+
|
|
24
|
+
`streamlit-web3` packages a hidden Streamlit component that bridges a browser-injected EIP-1193 wallet provider into
|
|
25
|
+
Web3.py.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install streamlit-web3
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Minimal usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import streamlit as st
|
|
37
|
+
|
|
38
|
+
from streamlit_browser_web3 import wallet_get
|
|
39
|
+
|
|
40
|
+
handler = wallet_get()
|
|
41
|
+
if handler.status == "connected":
|
|
42
|
+
if st.button("Disconnect your wallet"):
|
|
43
|
+
handler.disconnect()
|
|
44
|
+
page_body(handler)
|
|
45
|
+
else:
|
|
46
|
+
if st.button("Connect your wallet"):
|
|
47
|
+
handler.connect()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Important Streamlit behavior
|
|
51
|
+
|
|
52
|
+
Wallet operations are asynchronous in the browser but Streamlit scripts are synchronous and rerun-driven.
|
|
53
|
+
|
|
54
|
+
- `wallet_get()` must be rendered on every rerun.
|
|
55
|
+
- A wallet request may complete over multiple reruns.
|
|
56
|
+
- For button-triggered Web3 calls, persist the user intent in `st.session_state` until the call returns.
|
|
57
|
+
- Requests are serialized through the hidden browser bridge.
|
|
58
|
+
|
|
59
|
+
Chain metadata and ERC-20 helper code are intentionally kept in the example app, not in the package.
|
|
60
|
+
|
|
61
|
+
## Handler API
|
|
62
|
+
|
|
63
|
+
`wallet_get()` returns a `WalletHandler` instance. Render it on every rerun and use the same handler object as your app's wallet state interface.
|
|
64
|
+
|
|
65
|
+
### State properties
|
|
66
|
+
|
|
67
|
+
- `handler.status`: One of `"not-available"`, `"disconnected"`, or `"connected"`.
|
|
68
|
+
- `handler.available`: `True` when an injected browser provider such as `window.ethereum` exists.
|
|
69
|
+
- `handler.connected`: `True` when the wallet is connected and at least one account is available.
|
|
70
|
+
- `handler.accounts`: List of currently exposed account addresses.
|
|
71
|
+
- `handler.chain_id`: Current chain id as an `int`, or `None`.
|
|
72
|
+
- `handler.chain_id_hex`: Current chain id as a hex string such as `"0x1"`, or `None`.
|
|
73
|
+
- `handler.busy`: `True` while a connect/disconnect action or an interactive wallet request is still pending.
|
|
74
|
+
- `handler.last_error`: Last wallet or bridge error message, if any.
|
|
75
|
+
- `handler.snapshot`: Raw snapshot dictionary returned by the hidden browser component.
|
|
76
|
+
|
|
77
|
+
### Methods
|
|
78
|
+
|
|
79
|
+
- `handler.connect()`: Starts the wallet connection flow and reruns the Streamlit script.
|
|
80
|
+
- `handler.disconnect()`: Starts the wallet disconnect flow and reruns the Streamlit script.
|
|
81
|
+
- `handler.request(method, params=None, *, key)`: Sends an EIP-1193 request and returns `(status, result)`.
|
|
82
|
+
- `handler.forget(key)`: Removes a tracked request so the same key can be reused for a new flow.
|
|
83
|
+
- `handler.snapshot_view()`: Returns a typed `WalletSnapshot` dataclass with the main handler fields.
|
|
84
|
+
|
|
85
|
+
### `request()` status values
|
|
86
|
+
|
|
87
|
+
`handler.request(...)` returns a `(status, result)` tuple:
|
|
88
|
+
|
|
89
|
+
- `"pending"`: The wallet request is still waiting for completion in the browser.
|
|
90
|
+
- `"success"`: The request completed successfully and `result` contains the returned value.
|
|
91
|
+
- `"error"`: The request failed and `result` contains an error message.
|
|
92
|
+
|
|
93
|
+
The `key` argument is required. Requests are tracked by key across reruns, which lets button-triggered flows continue until the browser returns a result.
|
|
94
|
+
|
|
95
|
+
### Supported request methods
|
|
96
|
+
|
|
97
|
+
The handler has built-in support for these immediate methods, which resolve from the current snapshot without opening a wallet prompt:
|
|
98
|
+
|
|
99
|
+
- `eth_accounts`
|
|
100
|
+
- `eth_chainId`
|
|
101
|
+
- `eth_coinbase`
|
|
102
|
+
- `net_version`
|
|
103
|
+
|
|
104
|
+
The handler also marks these methods as interactive, meaning they can leave `handler.busy == True` while the wallet is waiting for user confirmation:
|
|
105
|
+
|
|
106
|
+
- `eth_requestAccounts`
|
|
107
|
+
- `eth_sendTransaction`
|
|
108
|
+
- `eth_sign`
|
|
109
|
+
- `eth_signTransaction`
|
|
110
|
+
- `eth_signTypedData`
|
|
111
|
+
- `eth_signTypedData_v1`
|
|
112
|
+
- `eth_signTypedData_v3`
|
|
113
|
+
- `eth_signTypedData_v4`
|
|
114
|
+
- `personal_sign`
|
|
115
|
+
- `wallet_addEthereumChain`
|
|
116
|
+
- `wallet_requestPermissions`
|
|
117
|
+
- `wallet_revokePermissions`
|
|
118
|
+
- `wallet_switchEthereumChain`
|
|
119
|
+
|
|
120
|
+
Other provider methods can still be passed to `handler.request(...)`. They are tracked as non-interactive requests unless they are in the interactive list above.
|
|
121
|
+
|
|
122
|
+
## Build
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m build
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Example app
|
|
129
|
+
|
|
130
|
+
An end-to-end example is included at `examples/streamlit_app.py`.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# streamlit-web3
|
|
2
|
+
|
|
3
|
+
`streamlit-web3` packages a hidden Streamlit component that bridges a browser-injected EIP-1193 wallet provider into
|
|
4
|
+
Web3.py.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install streamlit-web3
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Minimal usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import streamlit as st
|
|
16
|
+
|
|
17
|
+
from streamlit_browser_web3 import wallet_get
|
|
18
|
+
|
|
19
|
+
handler = wallet_get()
|
|
20
|
+
if handler.status == "connected":
|
|
21
|
+
if st.button("Disconnect your wallet"):
|
|
22
|
+
handler.disconnect()
|
|
23
|
+
page_body(handler)
|
|
24
|
+
else:
|
|
25
|
+
if st.button("Connect your wallet"):
|
|
26
|
+
handler.connect()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Important Streamlit behavior
|
|
30
|
+
|
|
31
|
+
Wallet operations are asynchronous in the browser but Streamlit scripts are synchronous and rerun-driven.
|
|
32
|
+
|
|
33
|
+
- `wallet_get()` must be rendered on every rerun.
|
|
34
|
+
- A wallet request may complete over multiple reruns.
|
|
35
|
+
- For button-triggered Web3 calls, persist the user intent in `st.session_state` until the call returns.
|
|
36
|
+
- Requests are serialized through the hidden browser bridge.
|
|
37
|
+
|
|
38
|
+
Chain metadata and ERC-20 helper code are intentionally kept in the example app, not in the package.
|
|
39
|
+
|
|
40
|
+
## Handler API
|
|
41
|
+
|
|
42
|
+
`wallet_get()` returns a `WalletHandler` instance. Render it on every rerun and use the same handler object as your app's wallet state interface.
|
|
43
|
+
|
|
44
|
+
### State properties
|
|
45
|
+
|
|
46
|
+
- `handler.status`: One of `"not-available"`, `"disconnected"`, or `"connected"`.
|
|
47
|
+
- `handler.available`: `True` when an injected browser provider such as `window.ethereum` exists.
|
|
48
|
+
- `handler.connected`: `True` when the wallet is connected and at least one account is available.
|
|
49
|
+
- `handler.accounts`: List of currently exposed account addresses.
|
|
50
|
+
- `handler.chain_id`: Current chain id as an `int`, or `None`.
|
|
51
|
+
- `handler.chain_id_hex`: Current chain id as a hex string such as `"0x1"`, or `None`.
|
|
52
|
+
- `handler.busy`: `True` while a connect/disconnect action or an interactive wallet request is still pending.
|
|
53
|
+
- `handler.last_error`: Last wallet or bridge error message, if any.
|
|
54
|
+
- `handler.snapshot`: Raw snapshot dictionary returned by the hidden browser component.
|
|
55
|
+
|
|
56
|
+
### Methods
|
|
57
|
+
|
|
58
|
+
- `handler.connect()`: Starts the wallet connection flow and reruns the Streamlit script.
|
|
59
|
+
- `handler.disconnect()`: Starts the wallet disconnect flow and reruns the Streamlit script.
|
|
60
|
+
- `handler.request(method, params=None, *, key)`: Sends an EIP-1193 request and returns `(status, result)`.
|
|
61
|
+
- `handler.forget(key)`: Removes a tracked request so the same key can be reused for a new flow.
|
|
62
|
+
- `handler.snapshot_view()`: Returns a typed `WalletSnapshot` dataclass with the main handler fields.
|
|
63
|
+
|
|
64
|
+
### `request()` status values
|
|
65
|
+
|
|
66
|
+
`handler.request(...)` returns a `(status, result)` tuple:
|
|
67
|
+
|
|
68
|
+
- `"pending"`: The wallet request is still waiting for completion in the browser.
|
|
69
|
+
- `"success"`: The request completed successfully and `result` contains the returned value.
|
|
70
|
+
- `"error"`: The request failed and `result` contains an error message.
|
|
71
|
+
|
|
72
|
+
The `key` argument is required. Requests are tracked by key across reruns, which lets button-triggered flows continue until the browser returns a result.
|
|
73
|
+
|
|
74
|
+
### Supported request methods
|
|
75
|
+
|
|
76
|
+
The handler has built-in support for these immediate methods, which resolve from the current snapshot without opening a wallet prompt:
|
|
77
|
+
|
|
78
|
+
- `eth_accounts`
|
|
79
|
+
- `eth_chainId`
|
|
80
|
+
- `eth_coinbase`
|
|
81
|
+
- `net_version`
|
|
82
|
+
|
|
83
|
+
The handler also marks these methods as interactive, meaning they can leave `handler.busy == True` while the wallet is waiting for user confirmation:
|
|
84
|
+
|
|
85
|
+
- `eth_requestAccounts`
|
|
86
|
+
- `eth_sendTransaction`
|
|
87
|
+
- `eth_sign`
|
|
88
|
+
- `eth_signTransaction`
|
|
89
|
+
- `eth_signTypedData`
|
|
90
|
+
- `eth_signTypedData_v1`
|
|
91
|
+
- `eth_signTypedData_v3`
|
|
92
|
+
- `eth_signTypedData_v4`
|
|
93
|
+
- `personal_sign`
|
|
94
|
+
- `wallet_addEthereumChain`
|
|
95
|
+
- `wallet_requestPermissions`
|
|
96
|
+
- `wallet_revokePermissions`
|
|
97
|
+
- `wallet_switchEthereumChain`
|
|
98
|
+
|
|
99
|
+
Other provider methods can still be passed to `handler.request(...)`. They are tracked as non-interactive requests unless they are in the interactive list above.
|
|
100
|
+
|
|
101
|
+
## Build
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Example app
|
|
108
|
+
|
|
109
|
+
An end-to-end example is included at `examples/streamlit_app.py`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "streamlit-browser-web3"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Streamlit components and helpers for browser-injected EIP-1193 provider utility."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"streamlit>=1.36"
|
|
13
|
+
]
|
|
14
|
+
keywords = ["streamlit", "web3", "ethereum", "eip-1193", "metamask"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/luismasuelli/streamlit-browser-web3"
|
|
28
|
+
Repository = "https://github.com/luismasuelli/streamlit-browser-web3"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
package-dir = {"" = "src"}
|
|
32
|
+
include-package-data = true
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
"streamlit_browser_web3" = ["frontend/provider/index.html"]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Awesome Crypto Browser Provider</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 1px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
background: transparent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#mount {
|
|
18
|
+
width: 1px;
|
|
19
|
+
height: 1px;
|
|
20
|
+
opacity: 0;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="mount" aria-hidden="true"></div>
|
|
27
|
+
<script>
|
|
28
|
+
const Streamlit = {
|
|
29
|
+
setComponentReady() {
|
|
30
|
+
window.parent.postMessage(
|
|
31
|
+
{ isStreamlitMessage: true, type: "streamlit:componentReady", apiVersion: 1 },
|
|
32
|
+
"*",
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
setFrameHeight(height) {
|
|
36
|
+
window.parent.postMessage(
|
|
37
|
+
{ isStreamlitMessage: true, type: "streamlit:setFrameHeight", height },
|
|
38
|
+
"*",
|
|
39
|
+
)
|
|
40
|
+
},
|
|
41
|
+
setComponentValue(value) {
|
|
42
|
+
window.parent.postMessage(
|
|
43
|
+
{ isStreamlitMessage: true, type: "streamlit:setComponentValue", value },
|
|
44
|
+
"*",
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const state = {
|
|
50
|
+
snapshot: {
|
|
51
|
+
providerAvailable: false,
|
|
52
|
+
connected: false,
|
|
53
|
+
accounts: [],
|
|
54
|
+
chainIdHex: null,
|
|
55
|
+
chainIdDecimal: null,
|
|
56
|
+
},
|
|
57
|
+
error: null,
|
|
58
|
+
lastActionResult: null,
|
|
59
|
+
requestResults: {},
|
|
60
|
+
handledActionNonce: null,
|
|
61
|
+
handledRequestIds: new Set(),
|
|
62
|
+
activeInteractiveRequestId: null,
|
|
63
|
+
listenersAttached: false,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function publish() {
|
|
67
|
+
Streamlit.setComponentValue({
|
|
68
|
+
snapshot: state.snapshot,
|
|
69
|
+
error: state.error,
|
|
70
|
+
lastActionResult: state.lastActionResult,
|
|
71
|
+
requestResults: state.requestResults,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resize() {
|
|
76
|
+
Streamlit.setFrameHeight(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function syncSnapshot() {
|
|
80
|
+
state.snapshot.providerAvailable = Boolean(window.ethereum)
|
|
81
|
+
if (!window.ethereum) {
|
|
82
|
+
state.snapshot.connected = false
|
|
83
|
+
state.snapshot.accounts = []
|
|
84
|
+
state.snapshot.chainIdHex = null
|
|
85
|
+
state.snapshot.chainIdDecimal = null
|
|
86
|
+
state.error = "window.ethereum is not available."
|
|
87
|
+
publish()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const [accounts, chainIdHex] = await Promise.all([
|
|
93
|
+
window.ethereum.request({ method: "eth_accounts" }),
|
|
94
|
+
window.ethereum.request({ method: "eth_chainId" }),
|
|
95
|
+
])
|
|
96
|
+
state.snapshot.accounts = accounts || []
|
|
97
|
+
state.snapshot.connected = state.snapshot.accounts.length > 0
|
|
98
|
+
state.snapshot.chainIdHex = chainIdHex || null
|
|
99
|
+
state.snapshot.chainIdDecimal = chainIdHex ? parseInt(chainIdHex, 16) : null
|
|
100
|
+
state.error = null
|
|
101
|
+
} catch (error) {
|
|
102
|
+
state.error = error.message || String(error)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
publish()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleAction(action) {
|
|
109
|
+
if (!action || !action.action || state.handledActionNonce === action.nonce) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
state.handledActionNonce = action.nonce
|
|
113
|
+
|
|
114
|
+
if (!window.ethereum) {
|
|
115
|
+
state.lastActionResult = {
|
|
116
|
+
nonce: action.nonce,
|
|
117
|
+
action: action.action,
|
|
118
|
+
status: "error",
|
|
119
|
+
error: "window.ethereum is not available.",
|
|
120
|
+
}
|
|
121
|
+
publish()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (action.action === "connect") {
|
|
127
|
+
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" })
|
|
128
|
+
state.lastActionResult = {
|
|
129
|
+
nonce: action.nonce,
|
|
130
|
+
action: action.action,
|
|
131
|
+
status: "success",
|
|
132
|
+
result: accounts || [],
|
|
133
|
+
}
|
|
134
|
+
} else if (action.action === "disconnect") {
|
|
135
|
+
await window.ethereum.request({
|
|
136
|
+
method: "wallet_revokePermissions",
|
|
137
|
+
params: [{ eth_accounts: {} }],
|
|
138
|
+
})
|
|
139
|
+
state.lastActionResult = {
|
|
140
|
+
nonce: action.nonce,
|
|
141
|
+
action: action.action,
|
|
142
|
+
status: "success",
|
|
143
|
+
result: true,
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
state.lastActionResult = {
|
|
147
|
+
nonce: action.nonce,
|
|
148
|
+
action: action.action,
|
|
149
|
+
status: "error",
|
|
150
|
+
error: `Unsupported action: ${action.action}`,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
state.lastActionResult = {
|
|
155
|
+
nonce: action.nonce,
|
|
156
|
+
action: action.action,
|
|
157
|
+
status: "error",
|
|
158
|
+
error: error.message || String(error),
|
|
159
|
+
}
|
|
160
|
+
state.error = state.lastActionResult.error
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await syncSnapshot()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function executeRequest(request) {
|
|
167
|
+
if (!request || !request.method || state.handledRequestIds.has(request.requestId)) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
state.handledRequestIds.add(request.requestId)
|
|
171
|
+
|
|
172
|
+
if (!window.ethereum) {
|
|
173
|
+
state.requestResults[String(request.requestId)] = {
|
|
174
|
+
requestId: request.requestId,
|
|
175
|
+
method: request.method,
|
|
176
|
+
status: "error",
|
|
177
|
+
error: "window.ethereum is not available.",
|
|
178
|
+
}
|
|
179
|
+
publish()
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const result = await window.ethereum.request({
|
|
185
|
+
method: request.method,
|
|
186
|
+
params: request.params || [],
|
|
187
|
+
})
|
|
188
|
+
state.requestResults[String(request.requestId)] = {
|
|
189
|
+
requestId: request.requestId,
|
|
190
|
+
method: request.method,
|
|
191
|
+
status: "success",
|
|
192
|
+
result: result,
|
|
193
|
+
}
|
|
194
|
+
state.error = null
|
|
195
|
+
} catch (error) {
|
|
196
|
+
state.requestResults[String(request.requestId)] = {
|
|
197
|
+
requestId: request.requestId,
|
|
198
|
+
method: request.method,
|
|
199
|
+
status: "error",
|
|
200
|
+
error: error.message || String(error),
|
|
201
|
+
code: typeof error.code === "number" ? error.code : -32000,
|
|
202
|
+
}
|
|
203
|
+
state.error = state.requestResults[String(request.requestId)].error
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
publish()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function handleRequests(requests) {
|
|
210
|
+
const requestList = Array.isArray(requests) ? requests : []
|
|
211
|
+
const readRequests = []
|
|
212
|
+
|
|
213
|
+
for (const request of requestList) {
|
|
214
|
+
if (!request || !request.method || state.handledRequestIds.has(request.requestId)) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (request.interactive) {
|
|
218
|
+
if (state.activeInteractiveRequestId !== null) {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
state.activeInteractiveRequestId = request.requestId
|
|
222
|
+
try {
|
|
223
|
+
await executeRequest(request)
|
|
224
|
+
} finally {
|
|
225
|
+
state.activeInteractiveRequestId = null
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
readRequests.push(executeRequest(request))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (readRequests.length > 0) {
|
|
233
|
+
await Promise.all(readRequests)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function attachListeners() {
|
|
238
|
+
if (state.listenersAttached || !window.ethereum) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
state.listenersAttached = true
|
|
243
|
+
|
|
244
|
+
window.ethereum.on("accountsChanged", async (accounts) => {
|
|
245
|
+
state.snapshot.accounts = accounts || []
|
|
246
|
+
state.snapshot.connected = state.snapshot.accounts.length > 0
|
|
247
|
+
publish()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
window.ethereum.on("chainChanged", async (chainIdHex) => {
|
|
251
|
+
state.snapshot.chainIdHex = chainIdHex || null
|
|
252
|
+
state.snapshot.chainIdDecimal = chainIdHex ? parseInt(chainIdHex, 16) : null
|
|
253
|
+
publish()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
window.ethereum.on("connect", async () => {
|
|
257
|
+
await syncSnapshot()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
window.ethereum.on("disconnect", async (error) => {
|
|
261
|
+
state.error = error && error.message ? error.message : null
|
|
262
|
+
await syncSnapshot()
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
window.addEventListener("message", async (event) => {
|
|
267
|
+
if (!event.data || event.data.type !== "streamlit:render") {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const args = event.data.args || {}
|
|
272
|
+
attachListeners()
|
|
273
|
+
await syncSnapshot()
|
|
274
|
+
await handleAction(args.action || null)
|
|
275
|
+
await handleRequests(args.requests || [])
|
|
276
|
+
resize()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
Streamlit.setComponentReady()
|
|
280
|
+
attachListeners()
|
|
281
|
+
resize()
|
|
282
|
+
syncSnapshot()
|
|
283
|
+
</script>
|
|
284
|
+
</body>
|
|
285
|
+
</html>
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from importlib.resources import as_file, files
|
|
6
|
+
from typing import Any
|
|
7
|
+
import streamlit as st
|
|
8
|
+
import streamlit.components.v1 as components
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
with as_file(files("streamlit_web3").joinpath("frontend/provider")) as _component_root:
|
|
12
|
+
_wallet_component = components.declare_component(
|
|
13
|
+
"streamlit_browser_web3_wallet_component",
|
|
14
|
+
path=str(_component_root),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
INTERACTIVE_METHODS = {
|
|
19
|
+
"eth_requestAccounts",
|
|
20
|
+
"eth_sendTransaction",
|
|
21
|
+
"eth_sign",
|
|
22
|
+
"eth_signTransaction",
|
|
23
|
+
"eth_signTypedData",
|
|
24
|
+
"eth_signTypedData_v1",
|
|
25
|
+
"eth_signTypedData_v3",
|
|
26
|
+
"eth_signTypedData_v4",
|
|
27
|
+
"personal_sign",
|
|
28
|
+
"wallet_addEthereumChain",
|
|
29
|
+
"wallet_requestPermissions",
|
|
30
|
+
"wallet_revokePermissions",
|
|
31
|
+
"wallet_switchEthereumChain",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
IMMEDIATE_METHODS = {
|
|
36
|
+
"eth_accounts",
|
|
37
|
+
"eth_chainId",
|
|
38
|
+
"eth_coinbase",
|
|
39
|
+
"net_version",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class WalletSnapshot:
|
|
45
|
+
status: str
|
|
46
|
+
busy: bool
|
|
47
|
+
chain_id: int | None
|
|
48
|
+
chain_id_hex: str | None
|
|
49
|
+
accounts: list[str]
|
|
50
|
+
available: bool
|
|
51
|
+
connected: bool
|
|
52
|
+
last_error: str | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_STATE_DEFAULTS: dict[str, Any] = {
|
|
56
|
+
"snapshot": {
|
|
57
|
+
"providerAvailable": False,
|
|
58
|
+
"connected": False,
|
|
59
|
+
"accounts": [],
|
|
60
|
+
"chainIdHex": None,
|
|
61
|
+
"chainIdDecimal": None,
|
|
62
|
+
},
|
|
63
|
+
"pending_action": None,
|
|
64
|
+
"action_counter": 0,
|
|
65
|
+
"request_counter": 0,
|
|
66
|
+
"requests": {},
|
|
67
|
+
"last_error": None,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _namespace(key: str) -> str:
|
|
72
|
+
return f"streamlit_web3:{key}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _state_get(key: str) -> dict[str, Any]:
|
|
76
|
+
namespaced = _namespace(key)
|
|
77
|
+
if namespaced not in st.session_state:
|
|
78
|
+
st.session_state[namespaced] = {
|
|
79
|
+
name: value.copy() if isinstance(value, dict) else value
|
|
80
|
+
for name, value in _STATE_DEFAULTS.items()
|
|
81
|
+
}
|
|
82
|
+
return st.session_state[namespaced]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _json_default(value: Any) -> Any:
|
|
86
|
+
if isinstance(value, bytes):
|
|
87
|
+
return "0x" + value.hex()
|
|
88
|
+
raise TypeError(f"Unsupported JSON value: {type(value)!r}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fingerprint(method: str, params: Any) -> str:
|
|
92
|
+
return json.dumps({"method": method, "params": params}, sort_keys=True, default=_json_default)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_interactive(method: str) -> bool:
|
|
96
|
+
return method in INTERACTIVE_METHODS
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _pending_requests_payload(state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
100
|
+
payload: list[dict[str, Any]] = []
|
|
101
|
+
for slot in state["requests"].values():
|
|
102
|
+
if slot.get("status") != "pending":
|
|
103
|
+
continue
|
|
104
|
+
payload.append(
|
|
105
|
+
{
|
|
106
|
+
"requestId": slot["request_id"],
|
|
107
|
+
"method": slot["method"],
|
|
108
|
+
"params": slot["params"],
|
|
109
|
+
"interactive": slot["interactive"],
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return payload
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class WalletHandler:
|
|
116
|
+
def __init__(self, *, state: dict[str, Any]) -> None:
|
|
117
|
+
self._state = state
|
|
118
|
+
|
|
119
|
+
def connect(self) -> None:
|
|
120
|
+
self._state["action_counter"] = int(self._state.get("action_counter", 0)) + 1
|
|
121
|
+
self._state["pending_action"] = {
|
|
122
|
+
"action": "connect",
|
|
123
|
+
"nonce": self._state["action_counter"],
|
|
124
|
+
"created_at": time.time(),
|
|
125
|
+
}
|
|
126
|
+
st.rerun()
|
|
127
|
+
|
|
128
|
+
def disconnect(self) -> None:
|
|
129
|
+
self._state["action_counter"] = int(self._state.get("action_counter", 0)) + 1
|
|
130
|
+
self._state["pending_action"] = {
|
|
131
|
+
"action": "disconnect",
|
|
132
|
+
"nonce": self._state["action_counter"],
|
|
133
|
+
"created_at": time.time(),
|
|
134
|
+
}
|
|
135
|
+
st.rerun()
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def available(self) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Tells whether the provider is available in the browser
|
|
141
|
+
or not (i.e. window.ethereum being injected or not).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
return bool(self.snapshot.get("providerAvailable"))
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def connected(self) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Tells whether the wallet is connected or not.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
snapshot = self.snapshot
|
|
153
|
+
return bool(snapshot.get("connected")) and bool(snapshot.get("accounts"))
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def accounts(self) -> list[str]:
|
|
157
|
+
"""
|
|
158
|
+
Gets the list of available accounts.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
return list(self.snapshot.get("accounts") or [])
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def chain_id(self) -> int | None:
|
|
165
|
+
"""
|
|
166
|
+
Gets the current chain id.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
return self.snapshot.get("chainIdDecimal")
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def chain_id_hex(self) -> str | None:
|
|
173
|
+
"""
|
|
174
|
+
Returns the hex. version of the chain id.
|
|
175
|
+
"""
|
|
176
|
+
return self.snapshot.get("chainIdHex")
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def snapshot(self) -> dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Gets a snapshot of the current state.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
return self._state["snapshot"]
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def busy(self) -> bool:
|
|
188
|
+
"""
|
|
189
|
+
Tells whether there is a long-running interactive
|
|
190
|
+
method being executed at this time. Only one of
|
|
191
|
+
those methods can run at once.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
if self._state.get("pending_action"):
|
|
195
|
+
return True
|
|
196
|
+
return any(slot.get("status") == "pending" and slot.get("interactive")
|
|
197
|
+
for slot in self._state["requests"].values())
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def last_error(self) -> str | None:
|
|
201
|
+
"""
|
|
202
|
+
Gets the last error, if any.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
return self._state.get("last_error")
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def status(self) -> str:
|
|
209
|
+
"""
|
|
210
|
+
The status means:
|
|
211
|
+
- not-available: There's no injected provider like window.ethereum.
|
|
212
|
+
- disconnected: The provider exists, but it is not connected.
|
|
213
|
+
- connected: The provider is connected and can be used.
|
|
214
|
+
:return:
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
if not self.available:
|
|
218
|
+
return "not-available"
|
|
219
|
+
if not self.connected:
|
|
220
|
+
return "disconnected"
|
|
221
|
+
return "connected"
|
|
222
|
+
|
|
223
|
+
def snapshot_view(self) -> WalletSnapshot:
|
|
224
|
+
"""
|
|
225
|
+
Gets a snapshot of the current state, in a structured
|
|
226
|
+
format.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
return WalletSnapshot(
|
|
230
|
+
status=self.status,
|
|
231
|
+
busy=self.busy,
|
|
232
|
+
chain_id=self.chain_id,
|
|
233
|
+
chain_id_hex=self.chain_id_hex,
|
|
234
|
+
accounts=self.accounts,
|
|
235
|
+
available=self.available,
|
|
236
|
+
connected=self.connected,
|
|
237
|
+
last_error=self.last_error,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def forget(self, key: str) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Pops a particular request.
|
|
243
|
+
:param key: The request key to forget / stop tracking.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
self._state["requests"].pop(key, None)
|
|
247
|
+
|
|
248
|
+
def request(self, method: str, params: list[Any] | None = None, *, key: str) -> tuple[str, Any]:
|
|
249
|
+
"""
|
|
250
|
+
Performs a request with the given method and params. Certain
|
|
251
|
+
methods are deemed interactive. A key is always mandatory so
|
|
252
|
+
the request can be tracked.
|
|
253
|
+
|
|
254
|
+
Certain methods are interactive and will mark this provider
|
|
255
|
+
as .busy. Make sure you don't invoke two concurrent methods
|
|
256
|
+
like this, and always account for the .busy flag somewhere
|
|
257
|
+
before invoking these methods.
|
|
258
|
+
:param method: The RPC method name.
|
|
259
|
+
:param params: The RPC method params.
|
|
260
|
+
:param key: The key of this request.
|
|
261
|
+
:return: The result of the request (status, content), where
|
|
262
|
+
status is "success", "error" or "pending" and the content
|
|
263
|
+
can be any valid content for the "error" status (a detail
|
|
264
|
+
on the failure) or "success" (e.g. the tx. receipt hash).
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
params = params or []
|
|
268
|
+
|
|
269
|
+
if method in IMMEDIATE_METHODS:
|
|
270
|
+
return "success", self._immediate_result(method)
|
|
271
|
+
|
|
272
|
+
if not self.available:
|
|
273
|
+
return "error", "window.ethereum is not available"
|
|
274
|
+
|
|
275
|
+
interactive = _is_interactive(method)
|
|
276
|
+
slot = self._state["requests"].get(key)
|
|
277
|
+
fingerprint = _fingerprint(method, params)
|
|
278
|
+
|
|
279
|
+
if slot and slot["fingerprint"] != fingerprint:
|
|
280
|
+
if slot["status"] == "pending":
|
|
281
|
+
return "error", f"Request key `{key}` already has a different pending request."
|
|
282
|
+
self.forget(key)
|
|
283
|
+
slot = None
|
|
284
|
+
|
|
285
|
+
if interactive and self.busy and not (slot and slot["status"] == "pending" and slot["interactive"]):
|
|
286
|
+
return "error", "Another interactive wallet request is already pending."
|
|
287
|
+
|
|
288
|
+
if slot:
|
|
289
|
+
if slot["status"] == "pending":
|
|
290
|
+
return "pending", None
|
|
291
|
+
if slot["status"] == "success":
|
|
292
|
+
return "success", slot.get("result")
|
|
293
|
+
return "error", slot.get("error")
|
|
294
|
+
|
|
295
|
+
request_id = self._next_counter("request_counter")
|
|
296
|
+
self._state["requests"][key] = {
|
|
297
|
+
"request_id": request_id,
|
|
298
|
+
"key": key,
|
|
299
|
+
"method": method,
|
|
300
|
+
"params": params,
|
|
301
|
+
"fingerprint": fingerprint,
|
|
302
|
+
"interactive": interactive,
|
|
303
|
+
"status": "pending",
|
|
304
|
+
"result": None,
|
|
305
|
+
"error": None,
|
|
306
|
+
"created_at": time.time(),
|
|
307
|
+
}
|
|
308
|
+
st.rerun()
|
|
309
|
+
|
|
310
|
+
def _immediate_result(self, method: str) -> Any:
|
|
311
|
+
if method == "eth_accounts":
|
|
312
|
+
return self.accounts
|
|
313
|
+
if method == "eth_chainId":
|
|
314
|
+
return self.chain_id_hex
|
|
315
|
+
if method == "eth_coinbase":
|
|
316
|
+
return self.accounts[0] if self.accounts else None
|
|
317
|
+
if method == "net_version":
|
|
318
|
+
return str(self.chain_id) if self.chain_id is not None else None
|
|
319
|
+
raise ValueError(f"Unsupported immediate method: {method}")
|
|
320
|
+
|
|
321
|
+
def _next_counter(self, counter_name: str) -> int:
|
|
322
|
+
self._state[counter_name] = int(self._state.get(counter_name, 0)) + 1
|
|
323
|
+
return self._state[counter_name]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _sync_component_value(state: dict[str, Any], component_value: dict[str, Any]) -> None:
|
|
327
|
+
if not isinstance(component_value, dict):
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
snapshot = component_value.get("snapshot")
|
|
331
|
+
if isinstance(snapshot, dict):
|
|
332
|
+
state["snapshot"] = snapshot
|
|
333
|
+
|
|
334
|
+
error = component_value.get("error")
|
|
335
|
+
if error:
|
|
336
|
+
state["last_error"] = error
|
|
337
|
+
|
|
338
|
+
action_result = component_value.get("lastActionResult") or {}
|
|
339
|
+
pending_action = state.get("pending_action")
|
|
340
|
+
if pending_action and action_result.get("nonce") == pending_action.get("nonce"):
|
|
341
|
+
state["pending_action"] = None
|
|
342
|
+
if action_result.get("error"):
|
|
343
|
+
state["last_error"] = action_result["error"]
|
|
344
|
+
|
|
345
|
+
request_results = component_value.get("requestResults") or {}
|
|
346
|
+
requests_by_id = {slot["request_id"]: slot for slot in state["requests"].values()}
|
|
347
|
+
for raw_request_id, result in request_results.items():
|
|
348
|
+
try:
|
|
349
|
+
request_id = int(raw_request_id)
|
|
350
|
+
except (TypeError, ValueError):
|
|
351
|
+
continue
|
|
352
|
+
slot = requests_by_id.get(request_id)
|
|
353
|
+
if not slot or slot["status"] != "pending":
|
|
354
|
+
continue
|
|
355
|
+
if result.get("status") == "success":
|
|
356
|
+
slot["status"] = "success"
|
|
357
|
+
slot["result"] = result.get("result")
|
|
358
|
+
slot["error"] = None
|
|
359
|
+
else:
|
|
360
|
+
slot["status"] = "error"
|
|
361
|
+
slot["error"] = result.get("error") or "Wallet request failed."
|
|
362
|
+
slot["result"] = None
|
|
363
|
+
state["last_error"] = slot["error"]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def wallet_get() -> WalletHandler:
|
|
367
|
+
"""
|
|
368
|
+
Gets a wallet handler. Meant to be used as a singleton call.
|
|
369
|
+
:return:
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
key = "__handler__"
|
|
373
|
+
state = _state_get(key)
|
|
374
|
+
component_value = _wallet_component(
|
|
375
|
+
key=f"streamlit_web3_wallet_component:{key}",
|
|
376
|
+
default={},
|
|
377
|
+
action=state.get("pending_action"),
|
|
378
|
+
requests=_pending_requests_payload(state),
|
|
379
|
+
hidden=True,
|
|
380
|
+
)
|
|
381
|
+
_sync_component_value(state, component_value if isinstance(component_value, dict) else {})
|
|
382
|
+
return WalletHandler(state=state)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streamlit-browser-web3
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Streamlit components and helpers for browser-injected EIP-1193 provider utility.
|
|
5
|
+
Project-URL: Homepage, https://github.com/luismasuelli/streamlit-browser-web3
|
|
6
|
+
Project-URL: Repository, https://github.com/luismasuelli/streamlit-browser-web3
|
|
7
|
+
Keywords: streamlit,web3,ethereum,eip-1193,metamask
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: streamlit>=1.36
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# streamlit-web3
|
|
23
|
+
|
|
24
|
+
`streamlit-web3` packages a hidden Streamlit component that bridges a browser-injected EIP-1193 wallet provider into
|
|
25
|
+
Web3.py.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install streamlit-web3
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Minimal usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import streamlit as st
|
|
37
|
+
|
|
38
|
+
from streamlit_browser_web3 import wallet_get
|
|
39
|
+
|
|
40
|
+
handler = wallet_get()
|
|
41
|
+
if handler.status == "connected":
|
|
42
|
+
if st.button("Disconnect your wallet"):
|
|
43
|
+
handler.disconnect()
|
|
44
|
+
page_body(handler)
|
|
45
|
+
else:
|
|
46
|
+
if st.button("Connect your wallet"):
|
|
47
|
+
handler.connect()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Important Streamlit behavior
|
|
51
|
+
|
|
52
|
+
Wallet operations are asynchronous in the browser but Streamlit scripts are synchronous and rerun-driven.
|
|
53
|
+
|
|
54
|
+
- `wallet_get()` must be rendered on every rerun.
|
|
55
|
+
- A wallet request may complete over multiple reruns.
|
|
56
|
+
- For button-triggered Web3 calls, persist the user intent in `st.session_state` until the call returns.
|
|
57
|
+
- Requests are serialized through the hidden browser bridge.
|
|
58
|
+
|
|
59
|
+
Chain metadata and ERC-20 helper code are intentionally kept in the example app, not in the package.
|
|
60
|
+
|
|
61
|
+
## Handler API
|
|
62
|
+
|
|
63
|
+
`wallet_get()` returns a `WalletHandler` instance. Render it on every rerun and use the same handler object as your app's wallet state interface.
|
|
64
|
+
|
|
65
|
+
### State properties
|
|
66
|
+
|
|
67
|
+
- `handler.status`: One of `"not-available"`, `"disconnected"`, or `"connected"`.
|
|
68
|
+
- `handler.available`: `True` when an injected browser provider such as `window.ethereum` exists.
|
|
69
|
+
- `handler.connected`: `True` when the wallet is connected and at least one account is available.
|
|
70
|
+
- `handler.accounts`: List of currently exposed account addresses.
|
|
71
|
+
- `handler.chain_id`: Current chain id as an `int`, or `None`.
|
|
72
|
+
- `handler.chain_id_hex`: Current chain id as a hex string such as `"0x1"`, or `None`.
|
|
73
|
+
- `handler.busy`: `True` while a connect/disconnect action or an interactive wallet request is still pending.
|
|
74
|
+
- `handler.last_error`: Last wallet or bridge error message, if any.
|
|
75
|
+
- `handler.snapshot`: Raw snapshot dictionary returned by the hidden browser component.
|
|
76
|
+
|
|
77
|
+
### Methods
|
|
78
|
+
|
|
79
|
+
- `handler.connect()`: Starts the wallet connection flow and reruns the Streamlit script.
|
|
80
|
+
- `handler.disconnect()`: Starts the wallet disconnect flow and reruns the Streamlit script.
|
|
81
|
+
- `handler.request(method, params=None, *, key)`: Sends an EIP-1193 request and returns `(status, result)`.
|
|
82
|
+
- `handler.forget(key)`: Removes a tracked request so the same key can be reused for a new flow.
|
|
83
|
+
- `handler.snapshot_view()`: Returns a typed `WalletSnapshot` dataclass with the main handler fields.
|
|
84
|
+
|
|
85
|
+
### `request()` status values
|
|
86
|
+
|
|
87
|
+
`handler.request(...)` returns a `(status, result)` tuple:
|
|
88
|
+
|
|
89
|
+
- `"pending"`: The wallet request is still waiting for completion in the browser.
|
|
90
|
+
- `"success"`: The request completed successfully and `result` contains the returned value.
|
|
91
|
+
- `"error"`: The request failed and `result` contains an error message.
|
|
92
|
+
|
|
93
|
+
The `key` argument is required. Requests are tracked by key across reruns, which lets button-triggered flows continue until the browser returns a result.
|
|
94
|
+
|
|
95
|
+
### Supported request methods
|
|
96
|
+
|
|
97
|
+
The handler has built-in support for these immediate methods, which resolve from the current snapshot without opening a wallet prompt:
|
|
98
|
+
|
|
99
|
+
- `eth_accounts`
|
|
100
|
+
- `eth_chainId`
|
|
101
|
+
- `eth_coinbase`
|
|
102
|
+
- `net_version`
|
|
103
|
+
|
|
104
|
+
The handler also marks these methods as interactive, meaning they can leave `handler.busy == True` while the wallet is waiting for user confirmation:
|
|
105
|
+
|
|
106
|
+
- `eth_requestAccounts`
|
|
107
|
+
- `eth_sendTransaction`
|
|
108
|
+
- `eth_sign`
|
|
109
|
+
- `eth_signTransaction`
|
|
110
|
+
- `eth_signTypedData`
|
|
111
|
+
- `eth_signTypedData_v1`
|
|
112
|
+
- `eth_signTypedData_v3`
|
|
113
|
+
- `eth_signTypedData_v4`
|
|
114
|
+
- `personal_sign`
|
|
115
|
+
- `wallet_addEthereumChain`
|
|
116
|
+
- `wallet_requestPermissions`
|
|
117
|
+
- `wallet_revokePermissions`
|
|
118
|
+
- `wallet_switchEthereumChain`
|
|
119
|
+
|
|
120
|
+
Other provider methods can still be passed to `handler.request(...)`. They are tracked as non-interactive requests unless they are in the interactive list above.
|
|
121
|
+
|
|
122
|
+
## Build
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m build
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Example app
|
|
129
|
+
|
|
130
|
+
An end-to-end example is included at `examples/streamlit_app.py`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/streamlit_browser_web3/__init__.py
|
|
5
|
+
src/streamlit_browser_web3/provider.py
|
|
6
|
+
src/streamlit_browser_web3.egg-info/PKG-INFO
|
|
7
|
+
src/streamlit_browser_web3.egg-info/SOURCES.txt
|
|
8
|
+
src/streamlit_browser_web3.egg-info/dependency_links.txt
|
|
9
|
+
src/streamlit_browser_web3.egg-info/requires.txt
|
|
10
|
+
src/streamlit_browser_web3.egg-info/top_level.txt
|
|
11
|
+
src/streamlit_browser_web3/frontend/provider/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
streamlit>=1.36
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
streamlit_browser_web3
|