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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """streamlit-web3 package."""
2
+
3
+ from .provider import WalletHandler, wallet_get
4
+
5
+ __all__ = ["WalletHandler", "wallet_get"]
@@ -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