Spaces:
Build error
Build error
Upload 54 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- src/.DS_Store +0 -0
- src/.claude/settings.local.json +23 -0
- src/.gitignore +77 -0
- src/.python-version +1 -0
- src/.run/run.run.xml +29 -0
- src/.streamlit/config.toml +40 -0
- src/Dockerfile +27 -0
- src/LICENSE +21 -0
- src/Makefile +5 -0
- src/Readme.md +40 -0
- src/aidocs/1p-draft.pdf +0 -0
- src/app.py +538 -0
- src/components/__init__.py +1 -0
- src/components/__pycache__/__init__.cpython-313.pyc +0 -0
- src/components/__pycache__/auth_component.cpython-313.pyc +0 -0
- src/components/auth_component.py +121 -0
- src/justfile +17 -0
- src/local.justfile +0 -0
- src/pages/.DS_Store +0 -0
- src/pages/__init__.py +24 -0
- src/pages/account.py +31 -0
- src/pages/authentication.py +244 -0
- src/pages/manage_wallet.py +231 -0
- src/pages/registration.py +433 -0
- src/pages/transaction_history.py +149 -0
- src/pages/wallet_setup.py +264 -0
- src/pyproject.toml +46 -0
- src/requirements.txt +7 -0
- src/scripts/verify_env.sh +20 -0
- src/static/timeout.js +22 -0
- src/tests/test_helpers.py +20 -0
- src/todo.1.md +1 -0
- src/todo.current.md +38 -0
- src/todo.md +30 -0
- src/todo.next.md +1 -0
- src/utils/.DS_Store +0 -0
- src/utils/__init__.py +1 -0
- src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- src/utils/__pycache__/aptos_sync.cpython-313.pyc +0 -0
- src/utils/__pycache__/auth_utils.cpython-313.pyc +0 -0
- src/utils/__pycache__/helpers.cpython-313.pyc +0 -0
- src/utils/__pycache__/nest_runner.cpython-313.pyc +0 -0
- src/utils/__pycache__/streamlit_async.cpython-313.pyc +0 -0
- src/utils/__pycache__/thread.cpython-310.pyc +0 -0
- src/utils/__pycache__/thread.cpython-311.pyc +0 -0
- src/utils/__pycache__/transfer_utils.cpython-313.pyc +0 -0
- src/utils/aptos_sync.py +86 -0
- src/utils/auth_utils.py +112 -0
- src/utils/helpers.py +22 -0
- src/utils/nest_runner.py +100 -0
src/.DS_Store
ADDED
|
Binary file (10.2 kB). View file
|
|
|
src/.claude/settings.local.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(uv sync:*)",
|
| 5 |
+
"Bash(uv list:*)",
|
| 6 |
+
"Bash(uv pip:*)",
|
| 7 |
+
"Bash(uv run:*)",
|
| 8 |
+
"Bash(timeout 10s uv run streamlit run:*)",
|
| 9 |
+
"Bash(pip show:*)",
|
| 10 |
+
"Bash(pip install:*)",
|
| 11 |
+
"WebFetch(domain:github.com)",
|
| 12 |
+
"WebFetch(domain:api.aptos.dev)",
|
| 13 |
+
"WebFetch(domain:aptos.dev)",
|
| 14 |
+
"Bash(if [ -f \"/Volumes/N/1p/1p-wallet/requirements.txt\" ])",
|
| 15 |
+
"Bash(else echo \"File does not exist\")",
|
| 16 |
+
"Bash(fi)",
|
| 17 |
+
"Bash(find:*)",
|
| 18 |
+
"Bash(pkill:*)"
|
| 19 |
+
],
|
| 20 |
+
"deny": [],
|
| 21 |
+
"ask": []
|
| 22 |
+
}
|
| 23 |
+
}
|
src/.gitignore
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Sure! Here's an example of an ignore file (`.gitignore`) for a Python project, including the `.env` file:
|
| 2 |
+
|
| 3 |
+
```
|
| 4 |
+
# Ignore Python virtual environment files
|
| 5 |
+
venv/
|
| 6 |
+
ENV/
|
| 7 |
+
bin/
|
| 8 |
+
lib/
|
| 9 |
+
share/
|
| 10 |
+
include/
|
| 11 |
+
pyvenv.cfg
|
| 12 |
+
.venv
|
| 13 |
+
|
| 14 |
+
# Ignore compiled Python files
|
| 15 |
+
*.pyc
|
| 16 |
+
*.pyo
|
| 17 |
+
__pycache__/
|
| 18 |
+
|
| 19 |
+
# Ignore environment-specific settings
|
| 20 |
+
.env
|
| 21 |
+
|
| 22 |
+
# Ignore editor files and directories
|
| 23 |
+
.vscode/
|
| 24 |
+
.idea/
|
| 25 |
+
.run/
|
| 26 |
+
|
| 27 |
+
# Ignore logs
|
| 28 |
+
*.log
|
| 29 |
+
|
| 30 |
+
# Ignore cache and temporary files
|
| 31 |
+
*.cache/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
|
| 36 |
+
# Ignore package build files
|
| 37 |
+
dist/
|
| 38 |
+
build/
|
| 39 |
+
*.egg-info/
|
| 40 |
+
|
| 41 |
+
# Ignore database files
|
| 42 |
+
*.db
|
| 43 |
+
|
| 44 |
+
# Ignore compiled extensions
|
| 45 |
+
*.so
|
| 46 |
+
|
| 47 |
+
# Ignore system-specific files
|
| 48 |
+
.DS_Store
|
| 49 |
+
Thumbs.db
|
| 50 |
+
|
| 51 |
+
# Jupyter Notebook
|
| 52 |
+
.ipynb_checkpoints/
|
| 53 |
+
etc/
|
| 54 |
+
flagged/
|
| 55 |
+
|
| 56 |
+
# pdf
|
| 57 |
+
*.pdf
|
| 58 |
+
You can modify this file as per your specific requirements.
|
| 59 |
+
|
| 60 |
+
# Audio Buffer obj
|
| 61 |
+
<_io.BytesIO object at*
|
| 62 |
+
*.mp3
|
| 63 |
+
|
| 64 |
+
.env.*
|
| 65 |
+
|
| 66 |
+
*.sav
|
| 67 |
+
|
| 68 |
+
#*.zip
|
| 69 |
+
|
| 70 |
+
/samples/sample_v3/linear-regression.ipynb.html
|
| 71 |
+
/samples/vehicle_solution/vehicle-dataset.html
|
| 72 |
+
/log.html
|
| 73 |
+
/output.xml
|
| 74 |
+
/report.html
|
| 75 |
+
/selenium-screenshot*.png
|
| 76 |
+
|
| 77 |
+
tests/reports
|
src/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.13
|
src/.run/run.run.xml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<component name="ProjectRunConfigurationManager">
|
| 2 |
+
<configuration default="false" name="run" type="PythonConfigurationType" factoryName="Python">
|
| 3 |
+
<module name="timeout-user.streamlit.app" />
|
| 4 |
+
<option name="INTERPRETER_OPTIONS" value="-m streamlit run" />
|
| 5 |
+
<option name="PARENT_ENVS" value="true" />
|
| 6 |
+
<envs>
|
| 7 |
+
<env name="PYTHONUNBUFFERED" value="1" />
|
| 8 |
+
</envs>
|
| 9 |
+
<option name="SDK_HOME" value="" />
|
| 10 |
+
<option name="SDK_NAME" value="py311" />
|
| 11 |
+
<option name="WORKING_DIRECTORY" value="" />
|
| 12 |
+
<option name="IS_MODULE_SDK" value="false" />
|
| 13 |
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
| 14 |
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
| 15 |
+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
| 16 |
+
<EXTENSION ID="com.fapiko.jetbrains.plugins.better_direnv.runconfigs.PycharmRunConfigurationExtension">
|
| 17 |
+
<option name="DIRENV_ENABLED" value="false" />
|
| 18 |
+
<option name="DIRENV_TRUSTED" value="false" />
|
| 19 |
+
</EXTENSION>
|
| 20 |
+
<option name="SCRIPT_NAME" value="app.py" />
|
| 21 |
+
<option name="PARAMETERS" value="" />
|
| 22 |
+
<option name="SHOW_COMMAND_LINE" value="false" />
|
| 23 |
+
<option name="EMULATE_TERMINAL" value="false" />
|
| 24 |
+
<option name="MODULE_MODE" value="false" />
|
| 25 |
+
<option name="REDIRECT_INPUT" value="false" />
|
| 26 |
+
<option name="INPUT_FILE" value="" />
|
| 27 |
+
<method v="2" />
|
| 28 |
+
</configuration>
|
| 29 |
+
</component>
|
src/.streamlit/config.toml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# doc: https://docs.streamlit.io/library/advanced-features/theming
|
| 2 |
+
|
| 3 |
+
[server]
|
| 4 |
+
folderWatchBlacklist = [".idea", ".run"]
|
| 5 |
+
runOnSave = true
|
| 6 |
+
port = 8502
|
| 7 |
+
cookieSecret = "a-random-key-appears-here" # consider setting via an environment variable for production
|
| 8 |
+
baseUrlPath = ""
|
| 9 |
+
enableCORS = true
|
| 10 |
+
enableXsrfProtection = false
|
| 11 |
+
maxUploadSize = 1024 # mb
|
| 12 |
+
maxMessageSize = 200 # mb
|
| 13 |
+
enableWebsocketCompression = false
|
| 14 |
+
|
| 15 |
+
enableStaticServing = true
|
| 16 |
+
# served in ./static :=/app/static/filename.jpg
|
| 17 |
+
|
| 18 |
+
[theme]
|
| 19 |
+
primaryColor = "#fff"
|
| 20 |
+
backgroundColor = "#000000"
|
| 21 |
+
secondaryBackgroundColor = "#252B48"
|
| 22 |
+
textColor = "#ffffff"
|
| 23 |
+
font = "sans serif"
|
| 24 |
+
|
| 25 |
+
[browser]
|
| 26 |
+
serverAddress = "localhost"
|
| 27 |
+
gatherUsageStats = true
|
| 28 |
+
serverPort = 8502
|
| 29 |
+
|
| 30 |
+
[mapbox]
|
| 31 |
+
# Mapbox token: prefer setting the MAPBOX_API_KEY environment variable
|
| 32 |
+
# or passing api_keys to pydeck. The token field is deprecated and will be
|
| 33 |
+
# removed in future Streamlit versions.
|
| 34 |
+
# token = ""
|
| 35 |
+
|
| 36 |
+
# Removed deprecated/invalid options: Streamlit no longer recognizes several
|
| 37 |
+
# of the [deprecation] and [runner] options. If you need similar behavior,
|
| 38 |
+
# manage it from code or environment variables. Keeping this file minimal
|
| 39 |
+
# avoids runtime warnings about invalid config keys.
|
| 40 |
+
# avoids runtime warnings about invalid config keys.
|
src/Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13.5-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y \
|
| 6 |
+
build-essential \
|
| 7 |
+
curl \
|
| 8 |
+
git \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Install uv (from Astral's official installation script)
|
| 12 |
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 13 |
+
|
| 14 |
+
# Add uv to PATH (uv installs under ~/.local/bin by default)
|
| 15 |
+
ENV PATH="/root/.local/bin:${PATH}"
|
| 16 |
+
|
| 17 |
+
COPY requirements.txt ./
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# Install dependencies with uv
|
| 21 |
+
RUN uv pip install .
|
| 22 |
+
|
| 23 |
+
EXPOSE 8501
|
| 24 |
+
|
| 25 |
+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 26 |
+
|
| 27 |
+
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
src/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Hiro
|
| 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.
|
src/Makefile
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
setup:
|
| 2 |
+
uv pip install .
|
| 3 |
+
|
| 4 |
+
run:
|
| 5 |
+
uv run streamlit run app.py
|
src/Readme.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1P Wallet
|
| 2 |
+
|
| 3 |
+
A Streamlit app implementing a 2FA-like visual authentication for Aptos wallets.
|
| 4 |
+
|
| 5 |
+
Quick start (dev/testnet):
|
| 6 |
+
|
| 7 |
+
1. Create a virtualenv and install deps:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
python -m venv .venv
|
| 11 |
+
source .venv/bin/activate
|
| 12 |
+
pip install -r requirements.txt
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
2. Set required environment variables (for full functionality):
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
export APTOS_ACCOUNT=0x... # system wallet address
|
| 19 |
+
export APTOS_PRIVATE_KEY=... # system wallet private key (hex)
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
3. Run the app:
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
streamlit run app.py
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Notes:
|
| 29 |
+
|
| 30 |
+
- This project is for demonstration. Do not use the provided scripts in production without proper key management.
|
| 31 |
+
- Use `scripts/verify_env.sh` to confirm environment variables are present.
|
| 32 |
+
|
| 33 |
+
Notes from runtime:
|
| 34 |
+
|
| 35 |
+
- Streamlit recommends installing `watchdog` for better file-change performance (`pip install watchdog`).
|
| 36 |
+
- It is recommended that private keys are AIP-80 compliant: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md
|
| 37 |
+
- For the one-click browser localStorage save/restore feature, install `streamlit-javascript`:
|
| 38 |
+
```bash
|
| 39 |
+
pip install streamlit-javascript
|
| 40 |
+
```
|
src/aidocs/1p-draft.pdf
ADDED
|
Binary file (54.4 kB). View file
|
|
|
src/app.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
import time
|
| 5 |
+
import string
|
| 6 |
+
import secrets
|
| 7 |
+
import hashlib
|
| 8 |
+
from queue import Queue
|
| 9 |
+
from typing import List, Dict, Optional
|
| 10 |
+
|
| 11 |
+
import streamlit as st
|
| 12 |
+
from aptos_sdk.async_client import RestClient
|
| 13 |
+
from aptos_sdk.account import Account
|
| 14 |
+
from aptos_sdk.transactions import EntryFunction
|
| 15 |
+
from aptos_sdk.bcs import Serializer
|
| 16 |
+
from ecdsa import SigningKey, VerifyingKey, SECP256k1
|
| 17 |
+
from ecdsa.util import sigencode_der, sigdecode_der
|
| 18 |
+
from collections import defaultdict
|
| 19 |
+
from itertools import islice
|
| 20 |
+
import random
|
| 21 |
+
from dotenv import load_dotenv
|
| 22 |
+
|
| 23 |
+
from pages import initApp
|
| 24 |
+
|
| 25 |
+
# Load environment variables
|
| 26 |
+
load_dotenv()
|
| 27 |
+
|
| 28 |
+
# UTF-8 character domains for elegant password selection
|
| 29 |
+
DOMAINS = {
|
| 30 |
+
'ascii': string.ascii_letters + string.digits,
|
| 31 |
+
'symbols': '!@#$%^&*()_+-=[]{}|;:,.<>?',
|
| 32 |
+
'emojis': "😀😂❤️👍🙏😍😭😅🎉🔥💯😎🤔🤦😴🤖👀✨✅🚀💎🌟⭐💫🎯🎨🎪🎸🎵🎶🏆🏅🎊🎈🎁🎀🌈🌸🌺🌻🌷🌹",
|
| 33 |
+
'hearts': "💖💝💘💗💓💕💞💜🧡💛💚💙🤍🖤🤎❣️💋",
|
| 34 |
+
'nature': "🌳🌲🌴🌿🍀🌾🌻🌺🌸🌷🌹🌼🌵🌱🍃🌿🦋🐝🐞🕷️",
|
| 35 |
+
'food': "🍎🍌🍇🍓🍈🍉🍊🍋🥭🍑🍒🥝🍍🥥🍅🥑🍆🥔🥕🌽",
|
| 36 |
+
'animals': "🐶🐱🐭🐹🐰🦊🐻🐼🐨🦁🐯🐮🐷🐸🐵🐔🐧🦆🦉🦅🐺🐗🐴",
|
| 37 |
+
'travel': "✈️🚆🚂🚄🚘🚲🛴🛵🏍️🚕🚖🚁🚀🛸🚢🚤🏝️🏖️🏔️⛰️🏕️🌋",
|
| 38 |
+
'sports': "⚽⚾🏀🏐🏈🏉🎾🏓🏸🥊🥋⛳🏌️♂️🏄♀️🏊♀️🧗♂️🚴♀️🏆🏅🥇🥈🥉",
|
| 39 |
+
'tech': "📱💻⌨️🖥️🖨️💾💿📷🔌📡🔋🔬🔭📚📝✏️🔍🔑🔒",
|
| 40 |
+
'music': "🎵🎶🎸🎹🎷🎺🎻🥁🎼🎤🎧📻🎙️🎚️🎛️",
|
| 41 |
+
'weather': "☀️🌤️⛅🌥️☁️🌦️🌧️⛈️🌩️🌨️❄️💨☃️⛄🌬️🌀🌈☔⚡",
|
| 42 |
+
'zodiac': "♈♉♊♋♌♍♎♏♐♑♒♓⛎",
|
| 43 |
+
'numbers': "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣🔟",
|
| 44 |
+
'japanese': "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん",
|
| 45 |
+
'korean': "ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣ",
|
| 46 |
+
'chinese': "的一是不了人我在有他这为之大来以个中上们",
|
| 47 |
+
'arabic': "ابتثجحخدذرزسشصضطظعغفقكلمنهوي",
|
| 48 |
+
'cyrillic': "абвгдеёжзийклмнопрстуфхцчшщъыьэюя",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
COLORS = ["red", "green", "blue", "yellow"]
|
| 52 |
+
DIRECTIONS = ["Up", "Down", "Left", "Right", "Skip"]
|
| 53 |
+
DIRECTION_MAP = {
|
| 54 |
+
"Up": "U", "Down": "D", "Left": "L", "Right": "R", "Skip": "S"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# System configuration
|
| 58 |
+
SYSTEM_WALLET_ADDRESS = os.getenv('APTOS_ACCOUNT') or "0xSYSTEM_WALLET_NOT_SET"
|
| 59 |
+
SYSTEM_WALLET_PRIVATE_KEY = os.getenv('APTOS_PRIVATE_KEY')
|
| 60 |
+
|
| 61 |
+
def generate_nonce() -> str:
|
| 62 |
+
return secrets.token_hex(32)
|
| 63 |
+
|
| 64 |
+
def keccak256(data: str) -> str:
|
| 65 |
+
return hashlib.sha3_256(data.encode('utf-8')).hexdigest()
|
| 66 |
+
|
| 67 |
+
def generate_entropy_layers(seed: str, layers: int) -> List[int]:
|
| 68 |
+
arr = []
|
| 69 |
+
cur = seed
|
| 70 |
+
for _ in range(layers):
|
| 71 |
+
random_bytes = secrets.token_bytes(2).hex()
|
| 72 |
+
h = keccak256(cur)
|
| 73 |
+
val = int(h[:8], 16)
|
| 74 |
+
arr.append(val)
|
| 75 |
+
cur = h + random_bytes
|
| 76 |
+
return arr
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class SessionState:
|
| 80 |
+
failure_count: int = 0
|
| 81 |
+
first_failure_ts: Optional[float] = None
|
| 82 |
+
last_failure_ts: Optional[float] = None
|
| 83 |
+
d: int = 1
|
| 84 |
+
high_abuse: bool = False
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class Transaction:
|
| 88 |
+
"""Represents a single transaction in the system"""
|
| 89 |
+
txn_hash: str
|
| 90 |
+
sender: str
|
| 91 |
+
recipient: str
|
| 92 |
+
amount: float # Amount in APT
|
| 93 |
+
timestamp: float # Unix timestamp
|
| 94 |
+
is_credit: bool # True if receiving funds, False if sending
|
| 95 |
+
status: str # "completed", "pending", "failed"
|
| 96 |
+
description: str = "" # Optional description
|
| 97 |
+
|
| 98 |
+
@dataclass
|
| 99 |
+
class App:
|
| 100 |
+
queue: Queue = field(default_factory=Queue)
|
| 101 |
+
wallet: Optional[Account] = None
|
| 102 |
+
client: RestClient = field(default_factory=lambda: RestClient("https://testnet.aptoslabs.com/v1"))
|
| 103 |
+
system_wallet: Optional[Account] = None
|
| 104 |
+
is_registered: bool = False
|
| 105 |
+
is_authenticated: bool = False
|
| 106 |
+
selected_secret: Optional[str] = None
|
| 107 |
+
direction_mapping: Dict[str, str] = field(default_factory=dict)
|
| 108 |
+
recent_characters: List[str] = field(default_factory=list)
|
| 109 |
+
favorite_characters: List[str] = field(default_factory=list)
|
| 110 |
+
transactions: List[Transaction] = field(default_factory=list) # Track all transactions
|
| 111 |
+
|
| 112 |
+
async def get_account_balance(self, address):
|
| 113 |
+
"""Get account balance in APT"""
|
| 114 |
+
if not self.wallet:
|
| 115 |
+
logging.error("No wallet connected; cannot fetch balance.")
|
| 116 |
+
return 0
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
resources = await self.client.account_resources(address)
|
| 120 |
+
apt_balance = 0
|
| 121 |
+
for resource in resources:
|
| 122 |
+
if resource['type'] == '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>':
|
| 123 |
+
apt_balance = int(resource['data']['coin']['value']) / 100000000 # Convert from octas to APT
|
| 124 |
+
break
|
| 125 |
+
logging.info("Fetch resources , got resources:", resources)
|
| 126 |
+
logging.info(f"Fetched balance for {address}: {apt_balance} APT")
|
| 127 |
+
return apt_balance
|
| 128 |
+
except Exception as e:
|
| 129 |
+
logging.error(f"Error fetching balance for {address}: {str(e)}")
|
| 130 |
+
raise Exception(f"Failed to check balance: {str(e)}")
|
| 131 |
+
|
| 132 |
+
def get_account_balance_sync(self, address):
|
| 133 |
+
"""Synchronous wrapper for get_account_balance"""
|
| 134 |
+
try:
|
| 135 |
+
# Use our clean nest_asyncio implementation
|
| 136 |
+
# Important: Create a fresh coroutine each time, never reuse
|
| 137 |
+
from utils.nest_runner import async_to_sync
|
| 138 |
+
# We call the function directly to get a fresh coroutine
|
| 139 |
+
return async_to_sync(self.get_account_balance(address))
|
| 140 |
+
except ValueError as e:
|
| 141 |
+
logging.error(f"Coroutine error: {str(e)}")
|
| 142 |
+
# Try one more time with a new coroutine
|
| 143 |
+
return async_to_sync(self.get_account_balance(address))
|
| 144 |
+
except Exception as e:
|
| 145 |
+
logging.error(f"Error in get_account_balance_sync: {str(e)}")
|
| 146 |
+
# Return 0 for balance rather than crashing completely
|
| 147 |
+
return 0.0
|
| 148 |
+
|
| 149 |
+
def add_transaction(self, txn_hash, sender, recipient, amount, is_credit=None, status="completed", description=""):
|
| 150 |
+
"""Add a transaction to the transaction history"""
|
| 151 |
+
if is_credit is None:
|
| 152 |
+
# Determine if this is a credit or debit based on sender/recipient
|
| 153 |
+
if self.wallet:
|
| 154 |
+
is_credit = recipient == str(self.wallet.address())
|
| 155 |
+
else:
|
| 156 |
+
is_credit = False
|
| 157 |
+
|
| 158 |
+
# Create new transaction record
|
| 159 |
+
txn = Transaction(
|
| 160 |
+
txn_hash=txn_hash,
|
| 161 |
+
sender=sender,
|
| 162 |
+
recipient=recipient,
|
| 163 |
+
amount=amount,
|
| 164 |
+
timestamp=time.time(),
|
| 165 |
+
is_credit=is_credit,
|
| 166 |
+
status=status,
|
| 167 |
+
description=description
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Add to transaction list
|
| 171 |
+
self.transactions.append(txn)
|
| 172 |
+
logging.info(f"Added transaction to history: {txn_hash} {'Credit' if is_credit else 'Debit'} {amount} APT")
|
| 173 |
+
|
| 174 |
+
return txn
|
| 175 |
+
|
| 176 |
+
async def fetch_account_transactions(self, address=None, limit=20):
|
| 177 |
+
"""Fetch transaction history for the given address from the blockchain"""
|
| 178 |
+
if not address and self.wallet:
|
| 179 |
+
address = str(self.wallet.address())
|
| 180 |
+
|
| 181 |
+
if not address:
|
| 182 |
+
logging.error("No wallet address provided for transaction history")
|
| 183 |
+
return []
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
# Use Aptos SDK to get account transactions
|
| 187 |
+
# We need to handle this differently since AsyncRestClient doesn't have get_account_transactions
|
| 188 |
+
from utils.aptos_sync import RestClientSync
|
| 189 |
+
|
| 190 |
+
# Create a sync client with the same URL as our async client
|
| 191 |
+
sync_client = RestClientSync(self.client.base_url)
|
| 192 |
+
|
| 193 |
+
# Use the sync client to get transactions
|
| 194 |
+
transactions = sync_client.get_account_transactions(address, limit=limit)
|
| 195 |
+
|
| 196 |
+
# Process transactions to identify credits and debits
|
| 197 |
+
processed_txns = []
|
| 198 |
+
for txn in transactions:
|
| 199 |
+
try:
|
| 200 |
+
# Extract basic transaction data
|
| 201 |
+
txn_hash = txn.get('hash', '')
|
| 202 |
+
txn_version = txn.get('version', 0)
|
| 203 |
+
sender = txn.get('sender', '')
|
| 204 |
+
timestamp = txn.get('timestamp', 0) / 1000000 # Convert to seconds
|
| 205 |
+
|
| 206 |
+
# Extract payload data to determine transaction type and amount
|
| 207 |
+
payload = txn.get('payload', {})
|
| 208 |
+
function = payload.get('function', '')
|
| 209 |
+
|
| 210 |
+
# Only process coin transfers for now
|
| 211 |
+
if '0x1::coin::transfer' in function:
|
| 212 |
+
args = payload.get('arguments', [])
|
| 213 |
+
if len(args) >= 2:
|
| 214 |
+
recipient = args[0]
|
| 215 |
+
amount_octas = int(args[1])
|
| 216 |
+
amount_apt = amount_octas / 100000000 # Convert octas to APT
|
| 217 |
+
|
| 218 |
+
# Determine if credit or debit
|
| 219 |
+
is_credit = recipient == address
|
| 220 |
+
|
| 221 |
+
# Create transaction object
|
| 222 |
+
transaction = Transaction(
|
| 223 |
+
txn_hash=txn_hash,
|
| 224 |
+
sender=sender,
|
| 225 |
+
recipient=recipient,
|
| 226 |
+
amount=amount_apt,
|
| 227 |
+
timestamp=timestamp,
|
| 228 |
+
is_credit=is_credit,
|
| 229 |
+
status="completed",
|
| 230 |
+
description=f"Transaction {txn_version}"
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
processed_txns.append(transaction)
|
| 234 |
+
|
| 235 |
+
except Exception as e:
|
| 236 |
+
logging.error(f"Error processing transaction: {str(e)}")
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
return processed_txns
|
| 240 |
+
|
| 241 |
+
except Exception as e:
|
| 242 |
+
logging.error(f"Error fetching transactions for {address}: {str(e)}")
|
| 243 |
+
return []
|
| 244 |
+
|
| 245 |
+
def fetch_account_transactions_sync(self, address=None, limit=20):
|
| 246 |
+
"""Synchronous wrapper for fetch_account_transactions using nest_asyncio"""
|
| 247 |
+
if not address and self.wallet:
|
| 248 |
+
address = str(self.wallet.address())
|
| 249 |
+
|
| 250 |
+
if not address:
|
| 251 |
+
logging.error("No wallet address provided for transaction history")
|
| 252 |
+
return []
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
# Use our clean nest_asyncio implementation
|
| 256 |
+
from utils.nest_runner import async_to_sync
|
| 257 |
+
return async_to_sync(self.fetch_account_transactions(address, limit=limit))
|
| 258 |
+
except Exception as e:
|
| 259 |
+
logging.error(f"Error fetching transactions synchronously: {str(e)}")
|
| 260 |
+
return []
|
| 261 |
+
|
| 262 |
+
def update_transaction_history(self):
|
| 263 |
+
"""Update the transaction history from the blockchain"""
|
| 264 |
+
if not self.wallet:
|
| 265 |
+
logging.error("No wallet connected; cannot update transaction history")
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
try:
|
| 269 |
+
# Fetch transactions from blockchain
|
| 270 |
+
new_txns = self.fetch_account_transactions_sync(str(self.wallet.address()))
|
| 271 |
+
|
| 272 |
+
# Add new transactions that aren't already in our list
|
| 273 |
+
existing_txn_hashes = {txn.txn_hash for txn in self.transactions}
|
| 274 |
+
|
| 275 |
+
for txn in new_txns:
|
| 276 |
+
if txn.txn_hash not in existing_txn_hashes:
|
| 277 |
+
self.transactions.append(txn)
|
| 278 |
+
|
| 279 |
+
# Sort by timestamp, most recent first
|
| 280 |
+
self.transactions.sort(key=lambda x: x.timestamp, reverse=True)
|
| 281 |
+
|
| 282 |
+
return True
|
| 283 |
+
except Exception as e:
|
| 284 |
+
logging.error(f"Error updating transaction history: {str(e)}")
|
| 285 |
+
return False
|
| 286 |
+
|
| 287 |
+
def __post_init__(self):
|
| 288 |
+
# Initialize system wallet
|
| 289 |
+
if SYSTEM_WALLET_PRIVATE_KEY:
|
| 290 |
+
try:
|
| 291 |
+
# Create system wallet from private key hex
|
| 292 |
+
self.system_wallet = Account.load_key(SYSTEM_WALLET_PRIVATE_KEY)
|
| 293 |
+
except Exception as e:
|
| 294 |
+
st.error(f"Failed to initialize system wallet: {str(e)}")
|
| 295 |
+
else:
|
| 296 |
+
# Inform the operator that system wallet isn't configured
|
| 297 |
+
st.warning("System wallet private key not set (APTOS_PRIVATE_KEY). System-send and registration actions will be disabled until configured.")
|
| 298 |
+
|
| 299 |
+
# Sync any session-backed state (cached wallet, auth sessions, etc.) into this App instance
|
| 300 |
+
try:
|
| 301 |
+
self.load_from_session()
|
| 302 |
+
except Exception:
|
| 303 |
+
# Avoid crashing pages on import; failures here should not stop Streamlit page load
|
| 304 |
+
logging.exception("Failed to load session state into App during __post_init__")
|
| 305 |
+
|
| 306 |
+
# Persist this App object into Streamlit session_state for pages to access
|
| 307 |
+
try:
|
| 308 |
+
st.session_state['app'] = self
|
| 309 |
+
except Exception:
|
| 310 |
+
# Some Streamlit environments may not allow writing at import time; ignore
|
| 311 |
+
pass
|
| 312 |
+
|
| 313 |
+
# --- Session-backed helpers -------------------------------------------------
|
| 314 |
+
@property
|
| 315 |
+
def cached_wallet(self):
|
| 316 |
+
"""Proxy property for st.session_state['cached_wallet']"""
|
| 317 |
+
return st.session_state.get('cached_wallet')
|
| 318 |
+
|
| 319 |
+
@cached_wallet.setter
|
| 320 |
+
def cached_wallet(self, value):
|
| 321 |
+
st.session_state['cached_wallet'] = value
|
| 322 |
+
# Keep the live App object in session as well
|
| 323 |
+
st.session_state['app'] = self
|
| 324 |
+
|
| 325 |
+
@property
|
| 326 |
+
def auth_session(self):
|
| 327 |
+
return st.session_state.get('auth_session')
|
| 328 |
+
|
| 329 |
+
@auth_session.setter
|
| 330 |
+
def auth_session(self, value):
|
| 331 |
+
st.session_state['auth_session'] = value
|
| 332 |
+
st.session_state['app'] = self
|
| 333 |
+
|
| 334 |
+
@property
|
| 335 |
+
def registration_auth(self):
|
| 336 |
+
return st.session_state.get('registration_auth')
|
| 337 |
+
|
| 338 |
+
@registration_auth.setter
|
| 339 |
+
def registration_auth(self, value):
|
| 340 |
+
st.session_state['registration_auth'] = value
|
| 341 |
+
st.session_state['app'] = self
|
| 342 |
+
|
| 343 |
+
def load_from_session(self):
|
| 344 |
+
"""Load common session-backed keys into the App instance.
|
| 345 |
+
|
| 346 |
+
This ensures pages can safely rely on `app` fields even when navigating
|
| 347 |
+
directly to a page mid-session.
|
| 348 |
+
"""
|
| 349 |
+
# Load cached wallet if present
|
| 350 |
+
cached = st.session_state.get('cached_wallet')
|
| 351 |
+
if cached and not self.wallet:
|
| 352 |
+
try:
|
| 353 |
+
pk = cached.get('private_key')
|
| 354 |
+
if pk:
|
| 355 |
+
clean_pk = pk[2:] if pk.startswith('0x') else pk
|
| 356 |
+
self.wallet = Account.load_key(clean_pk)
|
| 357 |
+
except Exception:
|
| 358 |
+
logging.exception("Failed to load cached wallet from session")
|
| 359 |
+
|
| 360 |
+
# Bring in boolean flags if present
|
| 361 |
+
self.is_registered = bool(st.session_state.get('is_registered', self.is_registered))
|
| 362 |
+
self.is_authenticated = bool(st.session_state.get('is_authenticated', self.is_authenticated))
|
| 363 |
+
|
| 364 |
+
# Load any other structured session items if present
|
| 365 |
+
if 'direction_mapping' in st.session_state and not self.direction_mapping:
|
| 366 |
+
self.direction_mapping = st.session_state.get('direction_mapping', self.direction_mapping)
|
| 367 |
+
|
| 368 |
+
def save_to_session(self):
|
| 369 |
+
"""Persist useful App fields into Streamlit session_state.
|
| 370 |
+
|
| 371 |
+
Call this after mutating the App so pages and reruns see updated values.
|
| 372 |
+
"""
|
| 373 |
+
try:
|
| 374 |
+
if self.wallet:
|
| 375 |
+
st.session_state['cached_wallet'] = {
|
| 376 |
+
'address': str(self.wallet.address()),
|
| 377 |
+
'private_key': self.wallet.private_key.hex()
|
| 378 |
+
}
|
| 379 |
+
st.session_state['is_registered'] = self.is_registered
|
| 380 |
+
st.session_state['is_authenticated'] = self.is_authenticated
|
| 381 |
+
st.session_state['direction_mapping'] = self.direction_mapping
|
| 382 |
+
st.session_state['app'] = self
|
| 383 |
+
except Exception:
|
| 384 |
+
logging.exception("Failed to save App state into session")
|
| 385 |
+
|
| 386 |
+
app = initApp()
|
| 387 |
+
# Page configuration
|
| 388 |
+
st.set_page_config(
|
| 389 |
+
page_title="1P Wallet - 2FA for Web3",
|
| 390 |
+
page_icon="🔒",
|
| 391 |
+
layout="wide",
|
| 392 |
+
initial_sidebar_state="expanded"
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Sidebar navigation
|
| 396 |
+
st.sidebar.title("🔒 1P Wallet")
|
| 397 |
+
st.sidebar.markdown("---")
|
| 398 |
+
|
| 399 |
+
# Navigation menu
|
| 400 |
+
pages = {
|
| 401 |
+
"🏠 Home": "home",
|
| 402 |
+
"💳 Import/Generate Wallet": "wallet_setup",
|
| 403 |
+
"📝 Registration": "registration",
|
| 404 |
+
"🔐 Authentication": "authentication",
|
| 405 |
+
"👤 Account": "account",
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
# Show Transaction History once wallet is connected
|
| 409 |
+
if app.wallet:
|
| 410 |
+
pages["📋 Transaction History"] = "transaction_history"
|
| 411 |
+
|
| 412 |
+
# Only show Manage Wallet if authenticated
|
| 413 |
+
if app.is_authenticated:
|
| 414 |
+
pages["💰 Manage Wallet"] = "manage_wallet"
|
| 415 |
+
|
| 416 |
+
# Page selection
|
| 417 |
+
selected_page = st.sidebar.selectbox(
|
| 418 |
+
"Navigate to:",
|
| 419 |
+
options=list(pages.keys()),
|
| 420 |
+
key=f"app_page_selector_{id(pages)}"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
current_page = pages[selected_page]
|
| 424 |
+
|
| 425 |
+
# Display current status in sidebar
|
| 426 |
+
st.sidebar.markdown("---")
|
| 427 |
+
st.sidebar.subheader("Status")
|
| 428 |
+
if app.wallet:
|
| 429 |
+
st.sidebar.success("✅ Wallet Connected")
|
| 430 |
+
st.sidebar.text(f"Address: {str(app.wallet.address())[:10]}...")
|
| 431 |
+
else:
|
| 432 |
+
st.sidebar.error("❌ No Wallet")
|
| 433 |
+
|
| 434 |
+
if app.is_registered:
|
| 435 |
+
st.sidebar.success("✅ Registered")
|
| 436 |
+
else:
|
| 437 |
+
st.sidebar.warning("⚠️ Not Registered")
|
| 438 |
+
|
| 439 |
+
if app.is_authenticated:
|
| 440 |
+
st.sidebar.success("✅ Authenticated")
|
| 441 |
+
else:
|
| 442 |
+
st.sidebar.warning("⚠️ Not Authenticated")
|
| 443 |
+
|
| 444 |
+
# Main content area
|
| 445 |
+
st.title("🔒 1P Wallet - 2FA for Web3")
|
| 446 |
+
|
| 447 |
+
# Route to appropriate page
|
| 448 |
+
if current_page == "home":
|
| 449 |
+
st.markdown("""
|
| 450 |
+
## Welcome to 1P Wallet
|
| 451 |
+
|
| 452 |
+
A secure 2FA system for Web3 wallets using elegant UTF-8 character selection.
|
| 453 |
+
|
| 454 |
+
### How it works:
|
| 455 |
+
1. **Import or Generate** an Aptos wallet
|
| 456 |
+
2. **Register** by selecting a single UTF-8 character as your secret
|
| 457 |
+
3. **Transfer funds** to our secure system wallet
|
| 458 |
+
4. **Authenticate** using the 1P visual grid system
|
| 459 |
+
5. **Manage** your wallet securely through our system
|
| 460 |
+
|
| 461 |
+
### Features:
|
| 462 |
+
- 🎨 Elegant UTF-8 character selection (no keyboard typing!)
|
| 463 |
+
- 🔒 Secure backend wallet system
|
| 464 |
+
- 🌍 Multi-language support
|
| 465 |
+
- 🎯 Visual grid-based authentication
|
| 466 |
+
- 💯 No private key exposure after registration
|
| 467 |
+
""")
|
| 468 |
+
|
| 469 |
+
if not app.wallet:
|
| 470 |
+
st.info("👈 Start by setting up your wallet in the sidebar")
|
| 471 |
+
elif not app.is_registered:
|
| 472 |
+
st.info("👈 Next, register your 1P secret")
|
| 473 |
+
elif not app.is_authenticated:
|
| 474 |
+
st.info("👈 Authenticate to access wallet management")
|
| 475 |
+
|
| 476 |
+
else:
|
| 477 |
+
# Import and execute the page module properly
|
| 478 |
+
import sys
|
| 479 |
+
import importlib.util
|
| 480 |
+
|
| 481 |
+
# Define variables that will be available to the page modules
|
| 482 |
+
page_globals = {
|
| 483 |
+
'st': st,
|
| 484 |
+
'app': app,
|
| 485 |
+
'DOMAINS': DOMAINS,
|
| 486 |
+
'COLORS': COLORS,
|
| 487 |
+
'DIRECTIONS': DIRECTIONS,
|
| 488 |
+
'SYSTEM_WALLET_ADDRESS': SYSTEM_WALLET_ADDRESS,
|
| 489 |
+
'DIRECTION_MAP': DIRECTION_MAP,
|
| 490 |
+
'Account': Account,
|
| 491 |
+
'EntryFunction': EntryFunction,
|
| 492 |
+
'Serializer': Serializer,
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
# Handle page routing
|
| 496 |
+
if current_page == "wallet_setup":
|
| 497 |
+
spec = importlib.util.spec_from_file_location("wallet_setup", "pages/wallet_setup.py")
|
| 498 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 499 |
+
page_module.__dict__.update(page_globals)
|
| 500 |
+
spec.loader.exec_module(page_module)
|
| 501 |
+
|
| 502 |
+
elif current_page == "registration":
|
| 503 |
+
spec = importlib.util.spec_from_file_location("registration", "pages/registration.py")
|
| 504 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 505 |
+
page_module.__dict__.update(page_globals)
|
| 506 |
+
spec.loader.exec_module(page_module)
|
| 507 |
+
|
| 508 |
+
elif current_page == "authentication":
|
| 509 |
+
spec = importlib.util.spec_from_file_location("authentication", "pages/authentication.py")
|
| 510 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 511 |
+
page_module.__dict__.update(page_globals)
|
| 512 |
+
spec.loader.exec_module(page_module)
|
| 513 |
+
|
| 514 |
+
elif current_page == "manage_wallet":
|
| 515 |
+
if app.is_authenticated:
|
| 516 |
+
spec = importlib.util.spec_from_file_location("manage_wallet", "pages/manage_wallet.py")
|
| 517 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 518 |
+
page_module.__dict__.update(page_globals)
|
| 519 |
+
spec.loader.exec_module(page_module)
|
| 520 |
+
else:
|
| 521 |
+
st.error("Please authenticate first to access wallet management.")
|
| 522 |
+
st.info("👈 Use the Authentication page to verify your 1P secret")
|
| 523 |
+
|
| 524 |
+
elif current_page == "account":
|
| 525 |
+
spec = importlib.util.spec_from_file_location("account", "pages/account.py")
|
| 526 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 527 |
+
page_module.__dict__.update(page_globals)
|
| 528 |
+
spec.loader.exec_module(page_module)
|
| 529 |
+
|
| 530 |
+
elif current_page == "transaction_history":
|
| 531 |
+
spec = importlib.util.spec_from_file_location("transaction_history", "pages/transaction_history.py")
|
| 532 |
+
page_module = importlib.util.module_from_spec(spec)
|
| 533 |
+
page_module.__dict__.update(page_globals)
|
| 534 |
+
spec.loader.exec_module(page_module)
|
| 535 |
+
|
| 536 |
+
# Footer
|
| 537 |
+
st.sidebar.markdown("---")
|
| 538 |
+
st.sidebar.markdown("Made with ❤️ using Streamlit")
|
src/components/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file makes the components directory a Python package
|
src/components/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (143 Bytes). View file
|
|
|
src/components/__pycache__/auth_component.cpython-313.pyc
ADDED
|
Binary file (4.68 kB). View file
|
|
|
src/components/auth_component.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from typing import Dict, List, Callable, Optional
|
| 3 |
+
|
| 4 |
+
from utils.auth_utils import run_one_round_authentication
|
| 5 |
+
|
| 6 |
+
def one_round_auth(
|
| 7 |
+
secret: str,
|
| 8 |
+
direction_mapping: Dict[str, str],
|
| 9 |
+
colors: List[str],
|
| 10 |
+
direction_map: Dict[str, str],
|
| 11 |
+
domains: Dict[str, Dict],
|
| 12 |
+
session_key: str = "one_round_auth",
|
| 13 |
+
on_success: Optional[Callable] = None,
|
| 14 |
+
on_failure: Optional[Callable] = None,
|
| 15 |
+
show_reference: bool = True
|
| 16 |
+
) -> bool:
|
| 17 |
+
"""
|
| 18 |
+
Streamlit component for one-round 1P authentication.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
secret: The user's secret character
|
| 22 |
+
direction_mapping: Mapping of colors to directions
|
| 23 |
+
colors: List of available colors
|
| 24 |
+
direction_map: Mapping of direction names to codes
|
| 25 |
+
domains: Available character domains
|
| 26 |
+
session_key: Unique key for session state
|
| 27 |
+
on_success: Optional callback function when auth succeeds
|
| 28 |
+
on_failure: Optional callback function when auth fails
|
| 29 |
+
show_reference: Whether to show direction mapping reference
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
True if authentication is completed successfully, False otherwise
|
| 33 |
+
"""
|
| 34 |
+
# Initialize session state for this component
|
| 35 |
+
if session_key not in st.session_state:
|
| 36 |
+
st.session_state[session_key] = {
|
| 37 |
+
'started': False,
|
| 38 |
+
'completed': False,
|
| 39 |
+
'success': False,
|
| 40 |
+
'grid_html': None,
|
| 41 |
+
'expected': None
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
sess = st.session_state[session_key]
|
| 45 |
+
|
| 46 |
+
# If not started yet, show start button
|
| 47 |
+
if not sess['started']:
|
| 48 |
+
st.info("Click 'Authenticate' to verify your identity")
|
| 49 |
+
if st.button("🔐 Authenticate", type="primary", key=f"{session_key}_start_btn"):
|
| 50 |
+
# Generate challenge
|
| 51 |
+
grid_html, expected = run_one_round_authentication(
|
| 52 |
+
secret, direction_mapping, colors, direction_map, domains
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Update session state
|
| 56 |
+
sess['started'] = True
|
| 57 |
+
sess['grid_html'] = grid_html
|
| 58 |
+
sess['expected'] = expected
|
| 59 |
+
st.rerun()
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
# If already completed, return result
|
| 63 |
+
if sess['completed']:
|
| 64 |
+
return sess['success']
|
| 65 |
+
|
| 66 |
+
# Display the challenge grid
|
| 67 |
+
st.markdown(sess['grid_html'], unsafe_allow_html=True)
|
| 68 |
+
|
| 69 |
+
# Show direction mapping as reference if requested
|
| 70 |
+
if show_reference:
|
| 71 |
+
with st.expander("🧭 Your Direction Mapping Reference"):
|
| 72 |
+
col1, col2 = st.columns(2)
|
| 73 |
+
with col1:
|
| 74 |
+
for color in colors[:len(colors)//2]:
|
| 75 |
+
direction = direction_mapping.get(color, "Skip")
|
| 76 |
+
emoji_map = {"Up": "⬆️", "Down": "⬇️", "Left": "⬅️", "Right": "➡️", "Skip": "⏭️"}
|
| 77 |
+
st.markdown(f"**{color.title()}**: {direction} {emoji_map[direction]}")
|
| 78 |
+
with col2:
|
| 79 |
+
for color in colors[len(colors)//2:]:
|
| 80 |
+
direction = direction_mapping.get(color, "Skip")
|
| 81 |
+
emoji_map = {"Up": "⬆️", "Down": "⬇️", "Left": "⬅️", "Right": "➡️", "Skip": "⏭️"}
|
| 82 |
+
st.markdown(f"**{color.title()}**: {direction} {emoji_map[direction]}")
|
| 83 |
+
|
| 84 |
+
# Input for the direction
|
| 85 |
+
col1, col2 = st.columns([3, 1])
|
| 86 |
+
with col1:
|
| 87 |
+
user_input = st.radio(
|
| 88 |
+
"What direction do you see?",
|
| 89 |
+
options=["⬆️ Up", "⬇️ Down", "⬅️ Left", "➡️ Right", "⏭️ Skip"],
|
| 90 |
+
key=f"{session_key}_input",
|
| 91 |
+
horizontal=True
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
with col2:
|
| 95 |
+
st.markdown("<br>", unsafe_allow_html=True) # Spacing
|
| 96 |
+
if st.button("Submit", type="primary", key=f"{session_key}_submit_btn"):
|
| 97 |
+
# Map emoji selection to direction code
|
| 98 |
+
direction_code = {
|
| 99 |
+
"⬆️ Up": "U",
|
| 100 |
+
"⬇️ Down": "D",
|
| 101 |
+
"⬅️ Left": "L",
|
| 102 |
+
"➡️ Right": "R",
|
| 103 |
+
"⏭️ Skip": "S"
|
| 104 |
+
}[user_input]
|
| 105 |
+
|
| 106 |
+
# Check if the answer is correct
|
| 107 |
+
success = direction_code == sess['expected']
|
| 108 |
+
|
| 109 |
+
# Update session state
|
| 110 |
+
sess['completed'] = True
|
| 111 |
+
sess['success'] = success
|
| 112 |
+
|
| 113 |
+
# Call callbacks if provided
|
| 114 |
+
if success and on_success is not None:
|
| 115 |
+
on_success()
|
| 116 |
+
elif not success and on_failure is not None:
|
| 117 |
+
on_failure()
|
| 118 |
+
|
| 119 |
+
st.rerun()
|
| 120 |
+
|
| 121 |
+
return False
|
src/justfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
set shell := ["sh", "-c"]
|
| 2 |
+
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
| 3 |
+
#set allow-duplicate-recipe
|
| 4 |
+
#set positional-arguments
|
| 5 |
+
set dotenv-filename := ".env"
|
| 6 |
+
set export
|
| 7 |
+
|
| 8 |
+
import? "local.justfile"
|
| 9 |
+
|
| 10 |
+
setup:
|
| 11 |
+
uv pip install .
|
| 12 |
+
|
| 13 |
+
run:
|
| 14 |
+
uv run streamlit run app.py
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
src/local.justfile
ADDED
|
File without changes
|
src/pages/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
src/pages/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def initApp():
|
| 7 |
+
# Import here to avoid top-level circular import issues with Streamlit
|
| 8 |
+
from app import App
|
| 9 |
+
|
| 10 |
+
# If an App instance exists in session, reuse it. Otherwise create a fresh one.
|
| 11 |
+
if 'app' not in st.session_state:
|
| 12 |
+
st.session_state.app = App()
|
| 13 |
+
else:
|
| 14 |
+
# Ensure the session-backed fields are synchronized into the live App
|
| 15 |
+
try:
|
| 16 |
+
st.session_state.app.load_from_session()
|
| 17 |
+
except Exception:
|
| 18 |
+
# If loading fails, recreate a fresh App to avoid stale state
|
| 19 |
+
st.session_state.app = App()
|
| 20 |
+
|
| 21 |
+
return st.session_state.app
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
app = initApp()
|
src/pages/account.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Account Page - basic account details and reset
|
| 2 |
+
import streamlit as st
|
| 3 |
+
|
| 4 |
+
st.header("👤 Account")
|
| 5 |
+
|
| 6 |
+
from pages import app
|
| 7 |
+
|
| 8 |
+
if not app.wallet:
|
| 9 |
+
st.error("❌ No wallet connected")
|
| 10 |
+
st.info("Go to 'Import/Generate Wallet' to connect a wallet")
|
| 11 |
+
st.stop()
|
| 12 |
+
|
| 13 |
+
st.markdown("**Wallet Address:**")
|
| 14 |
+
st.code(str(app.wallet.address()))
|
| 15 |
+
|
| 16 |
+
st.markdown("**Selected Secret:**")
|
| 17 |
+
if app.selected_secret:
|
| 18 |
+
st.code(f"{app.selected_secret} (U+{ord(app.selected_secret):04X})")
|
| 19 |
+
else:
|
| 20 |
+
st.info("No secret selected yet")
|
| 21 |
+
|
| 22 |
+
st.markdown("---")
|
| 23 |
+
if st.button("🔄 Reset App State", type="secondary"):
|
| 24 |
+
# Minimal reset: clear registration/authentication and selected secret
|
| 25 |
+
app.is_registered = False
|
| 26 |
+
app.is_authenticated = False
|
| 27 |
+
app.selected_secret = None
|
| 28 |
+
app.direction_mapping = {}
|
| 29 |
+
st.session_state.app = app
|
| 30 |
+
st.success("App state reset. Please re-register or re-import wallet.")
|
| 31 |
+
st.rerun()
|
src/pages/authentication.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
# Authentication Page
|
| 4 |
+
st.header("🔐 Authentication")
|
| 5 |
+
|
| 6 |
+
from pages import app
|
| 7 |
+
|
| 8 |
+
if not app.wallet:
|
| 9 |
+
st.error("❌ Please connect a wallet first")
|
| 10 |
+
st.info("👈 Go to 'Import/Generate Wallet' to get started")
|
| 11 |
+
st.stop()
|
| 12 |
+
|
| 13 |
+
if not app.is_registered:
|
| 14 |
+
st.error("❌ Please register first")
|
| 15 |
+
st.info("👈 Go to 'Registration' to set up your 1P secret")
|
| 16 |
+
st.stop()
|
| 17 |
+
|
| 18 |
+
if app.is_authenticated:
|
| 19 |
+
st.success("✅ You are already authenticated!")
|
| 20 |
+
st.info("👈 Go to 'Manage Wallet' to access wallet functions")
|
| 21 |
+
|
| 22 |
+
if st.button("🔄 Re-authenticate", type="secondary"):
|
| 23 |
+
app.is_authenticated = False
|
| 24 |
+
app.save_to_session()
|
| 25 |
+
st.rerun()
|
| 26 |
+
st.stop()
|
| 27 |
+
|
| 28 |
+
st.markdown("""
|
| 29 |
+
### 1P Authentication Process:
|
| 30 |
+
1. **Visual Grid Challenge** - Find your secret character in the colored grid
|
| 31 |
+
2. **Direction Input** - Enter the direction based on your character's color
|
| 32 |
+
3. **Multiple Rounds** - Complete several rounds for security
|
| 33 |
+
4. **Verification** - System verifies your responses
|
| 34 |
+
""")
|
| 35 |
+
|
| 36 |
+
# Initialize 1P verifier and solver components
|
| 37 |
+
class OnePVerifier:
|
| 38 |
+
def __init__(self, secret: str, public_key_hex: str):
|
| 39 |
+
self.secret = secret
|
| 40 |
+
self.public_key = public_key_hex
|
| 41 |
+
self.session_state = SessionState()
|
| 42 |
+
self.nonce = None
|
| 43 |
+
self.entropy_layers = []
|
| 44 |
+
self.offsets = []
|
| 45 |
+
self.rotateds = []
|
| 46 |
+
self.color_maps = []
|
| 47 |
+
self.expected_solutions = []
|
| 48 |
+
self.skip_rounds = []
|
| 49 |
+
|
| 50 |
+
def start_session(self) -> tuple[str, List[str], int]:
|
| 51 |
+
self.nonce = generate_nonce()
|
| 52 |
+
difficulty = self.session_state.d
|
| 53 |
+
total_rounds = difficulty + (difficulty // 2)
|
| 54 |
+
self.entropy_layers = generate_entropy_layers(self.nonce, total_rounds)
|
| 55 |
+
rounds_range = list(range(total_rounds))
|
| 56 |
+
self.skip_rounds = sorted(random.sample(rounds_range, k=total_rounds - difficulty))
|
| 57 |
+
|
| 58 |
+
self.offsets = []
|
| 59 |
+
self.rotateds = []
|
| 60 |
+
self.color_maps = []
|
| 61 |
+
self.expected_solutions = []
|
| 62 |
+
grids = []
|
| 63 |
+
|
| 64 |
+
# Build combined alphabet from all selected domains
|
| 65 |
+
alphabet = ""
|
| 66 |
+
for domain_chars in DOMAINS.values():
|
| 67 |
+
alphabet += domain_chars
|
| 68 |
+
alphabet = ''.join(set(alphabet)) # Remove duplicates
|
| 69 |
+
|
| 70 |
+
for idx in range(total_rounds):
|
| 71 |
+
offset = self.entropy_layers[idx] % len(alphabet)
|
| 72 |
+
self.offsets.append(offset)
|
| 73 |
+
rotated = alphabet[offset:] + alphabet[:offset]
|
| 74 |
+
self.rotateds.append(rotated)
|
| 75 |
+
color_map = {rotated[i]: COLORS[i % 4] for i in range(len(rotated))}
|
| 76 |
+
self.color_maps.append(color_map)
|
| 77 |
+
|
| 78 |
+
if idx in self.skip_rounds:
|
| 79 |
+
expected = "S"
|
| 80 |
+
else:
|
| 81 |
+
assigned_color = color_map.get(self.secret, None)
|
| 82 |
+
if assigned_color is None:
|
| 83 |
+
expected = "S"
|
| 84 |
+
else:
|
| 85 |
+
direction = app.direction_mapping.get(assigned_color, "Skip")
|
| 86 |
+
expected = DIRECTION_MAP[direction]
|
| 87 |
+
|
| 88 |
+
self.expected_solutions.append(expected)
|
| 89 |
+
grids.append(self.display_grid(idx))
|
| 90 |
+
|
| 91 |
+
return self.nonce, grids, total_rounds
|
| 92 |
+
|
| 93 |
+
def display_grid(self, idx: int) -> str:
|
| 94 |
+
chars_by_color = defaultdict(list)
|
| 95 |
+
for ch, color in self.color_maps[idx].items():
|
| 96 |
+
chars_by_color[color].append(ch)
|
| 97 |
+
|
| 98 |
+
grid_html = f"""
|
| 99 |
+
<div style="border: 2px solid #333; padding: 15px; margin: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 100 |
+
<h4>🎯 Round {idx + 1}</h4>
|
| 101 |
+
<p><strong>Find your secret character and note its color!</strong></p>
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
color_hex_map = {"red": "#FF0000", "green": "#00AA00", "blue": "#0066FF", "yellow": "#FFD700"}
|
| 105 |
+
|
| 106 |
+
for color in COLORS:
|
| 107 |
+
chars = chars_by_color[color]
|
| 108 |
+
if chars:
|
| 109 |
+
grid_html += f'<div style="margin: 8px 0;"><strong style="color: {color_hex_map[color]};">{color.upper()}:</strong> '
|
| 110 |
+
for char in chars:
|
| 111 |
+
grid_html += f'<span style="color: {color_hex_map[color]}; font-size: 18px; margin: 2px; padding: 4px; background: white; border-radius: 4px;">{char}</span> '
|
| 112 |
+
grid_html += '</div>'
|
| 113 |
+
|
| 114 |
+
grid_html += '</div>'
|
| 115 |
+
return grid_html
|
| 116 |
+
|
| 117 |
+
def verify_solution(self, candidates: List[str]) -> bool:
|
| 118 |
+
allowed_skips = len(self.skip_rounds)
|
| 119 |
+
input_skips = candidates.count('S')
|
| 120 |
+
|
| 121 |
+
if input_skips > allowed_skips:
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
for idx, expected in enumerate(self.expected_solutions):
|
| 125 |
+
if expected == "S":
|
| 126 |
+
if candidates[idx] != "S":
|
| 127 |
+
return False
|
| 128 |
+
else:
|
| 129 |
+
if candidates[idx] == "S":
|
| 130 |
+
continue
|
| 131 |
+
if candidates[idx].upper() != expected:
|
| 132 |
+
return False
|
| 133 |
+
|
| 134 |
+
return True
|
| 135 |
+
|
| 136 |
+
# Start authentication session
|
| 137 |
+
st.markdown("---")
|
| 138 |
+
st.subheader("🎯 1P Challenge")
|
| 139 |
+
|
| 140 |
+
if app.auth_session is None:
|
| 141 |
+
st.info("Click 'Start Authentication' to begin the challenge")
|
| 142 |
+
|
| 143 |
+
if st.button("🚀 Start Authentication", type="primary"):
|
| 144 |
+
try:
|
| 145 |
+
# Create verifier with user's secret and public key
|
| 146 |
+
public_key_hex = app.wallet.public_key().to_bytes()[1:].hex()
|
| 147 |
+
verifier = OnePVerifier(app.selected_secret, public_key_hex)
|
| 148 |
+
nonce, grids, total_rounds = verifier.start_session()
|
| 149 |
+
|
| 150 |
+
app.auth_session = {
|
| 151 |
+
'verifier': verifier,
|
| 152 |
+
'grids': grids,
|
| 153 |
+
'total_rounds': total_rounds,
|
| 154 |
+
'current_round': 0,
|
| 155 |
+
'solutions': [],
|
| 156 |
+
'nonce': nonce
|
| 157 |
+
}
|
| 158 |
+
app.save_to_session()
|
| 159 |
+
st.rerun()
|
| 160 |
+
except Exception as e:
|
| 161 |
+
st.error(f"Failed to start authentication: {str(e)}")
|
| 162 |
+
|
| 163 |
+
else:
|
| 164 |
+
session = app.auth_session
|
| 165 |
+
current_round = session['current_round']
|
| 166 |
+
total_rounds = session['total_rounds']
|
| 167 |
+
|
| 168 |
+
if current_round < total_rounds:
|
| 169 |
+
st.progress((current_round) / total_rounds, f"Round {current_round + 1} of {total_rounds}")
|
| 170 |
+
|
| 171 |
+
# Display current grid
|
| 172 |
+
st.markdown(session['grids'][current_round], unsafe_allow_html=True)
|
| 173 |
+
|
| 174 |
+
# Show direction mapping as reference
|
| 175 |
+
with st.expander("🧭 Your Direction Mapping Reference"):
|
| 176 |
+
col1, col2 = st.columns(2)
|
| 177 |
+
with col1:
|
| 178 |
+
for color in COLORS[:2]:
|
| 179 |
+
direction = app.direction_mapping.get(color, "Skip")
|
| 180 |
+
emoji_map = {"Up": "⬆️", "Down": "⬇️", "Left": "⬅️", "Right": "➡️", "Skip": "⏭️"}
|
| 181 |
+
st.markdown(f"**{color.title()}**: {direction} {emoji_map[direction]}")
|
| 182 |
+
with col2:
|
| 183 |
+
for color in COLORS[2:]:
|
| 184 |
+
direction = app.direction_mapping.get(color, "Skip")
|
| 185 |
+
emoji_map = {"Up": "⬆️", "Down": "⬇️", "Left": "⬅️", "Right": "➡️", "Skip": "⏭️"}
|
| 186 |
+
st.markdown(f"**{color.title()}**: {direction} {emoji_map[direction]}")
|
| 187 |
+
|
| 188 |
+
# Input for current round
|
| 189 |
+
col1, col2 = st.columns([3, 1])
|
| 190 |
+
with col1:
|
| 191 |
+
user_input = st.radio(
|
| 192 |
+
f"What direction for Round {current_round + 1}?",
|
| 193 |
+
options=["⬆️ Up", "⬇️ Down", "⬅️ Left", "➡️ Right", "⏭️ Skip"],
|
| 194 |
+
key=f"round_{current_round}",
|
| 195 |
+
horizontal=True
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
with col2:
|
| 199 |
+
st.markdown("<br>", unsafe_allow_html=True) # Spacing
|
| 200 |
+
if st.button("Next Round ▶️", type="primary"):
|
| 201 |
+
# Map emoji selection to direction code
|
| 202 |
+
direction_code = {
|
| 203 |
+
"⬆️ Up": "U",
|
| 204 |
+
"⬇️ Down": "D",
|
| 205 |
+
"⬅️ Left": "L",
|
| 206 |
+
"➡️ Right": "R",
|
| 207 |
+
"⏭️ Skip": "S"
|
| 208 |
+
}[user_input]
|
| 209 |
+
|
| 210 |
+
session['solutions'].append(direction_code)
|
| 211 |
+
session['current_round'] += 1
|
| 212 |
+
app.auth_session = session
|
| 213 |
+
app.save_to_session()
|
| 214 |
+
st.rerun()
|
| 215 |
+
|
| 216 |
+
else:
|
| 217 |
+
# Authentication complete - verify solutions
|
| 218 |
+
st.success("🎉 All rounds completed!")
|
| 219 |
+
st.info("Verifying your responses...")
|
| 220 |
+
|
| 221 |
+
verifier = session['verifier']
|
| 222 |
+
solutions = session['solutions']
|
| 223 |
+
|
| 224 |
+
if verifier.verify_solution(solutions):
|
| 225 |
+
app.is_authenticated = True
|
| 226 |
+
app.auth_session = None # Clear session
|
| 227 |
+
app.save_to_session()
|
| 228 |
+
|
| 229 |
+
st.success("✅ Authentication successful!")
|
| 230 |
+
st.success("🎉 Welcome to your secure 1P wallet!")
|
| 231 |
+
st.balloons()
|
| 232 |
+
|
| 233 |
+
st.info("👈 Go to 'Manage Wallet' to access your wallet functions")
|
| 234 |
+
st.rerun()
|
| 235 |
+
|
| 236 |
+
else:
|
| 237 |
+
st.error("❌ Authentication failed!")
|
| 238 |
+
st.error("Your responses don't match the expected pattern.")
|
| 239 |
+
st.warning("Please try again or check your secret character and direction mapping.")
|
| 240 |
+
|
| 241 |
+
if st.button("🔄 Try Again", type="secondary"):
|
| 242 |
+
app.auth_session = None
|
| 243 |
+
app.save_to_session()
|
| 244 |
+
st.rerun()
|
src/pages/manage_wallet.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
# Manage Wallet Page
|
| 4 |
+
st.header("💰 Manage Wallet")
|
| 5 |
+
import streamlit as st
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# Manage Wallet Page
|
| 9 |
+
st.header("💰 Manage Wallet")
|
| 10 |
+
|
| 11 |
+
from pages import app
|
| 12 |
+
|
| 13 |
+
if not app.is_authenticated:
|
| 14 |
+
st.error("❌ Authentication required")
|
| 15 |
+
st.info("Please authenticate first to access wallet management")
|
| 16 |
+
st.stop()
|
| 17 |
+
|
| 18 |
+
st.success("🔐 Authenticated Session Active")
|
| 19 |
+
st.markdown(f"**Your Wallet Address:** `{str(app.wallet.address())[:20]}...`")
|
| 20 |
+
|
| 21 |
+
# Display wallet balance
|
| 22 |
+
st.markdown("---")
|
| 23 |
+
st.subheader("💰 Your Wallet Balance")
|
| 24 |
+
|
| 25 |
+
# Automatically check user balance
|
| 26 |
+
with st.spinner("Checking your wallet balance..."):
|
| 27 |
+
try:
|
| 28 |
+
# Use the sync helper method
|
| 29 |
+
apt_balance = app.get_account_balance_sync(app.wallet.address())
|
| 30 |
+
|
| 31 |
+
# Show the balance
|
| 32 |
+
st.metric("Current Balance", f"{apt_balance} APT")
|
| 33 |
+
|
| 34 |
+
# Add a refresh button
|
| 35 |
+
if st.button("🔄 Refresh Balance", type="secondary"):
|
| 36 |
+
st.rerun()
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
st.error(f"Error checking balance: {str(e)}")
|
| 40 |
+
st.info("Try refreshing the page if this persists.")
|
| 41 |
+
|
| 42 |
+
# Transaction functionality
|
| 43 |
+
st.markdown("---")
|
| 44 |
+
st.subheader("💸 Send Transaction")
|
| 45 |
+
st.info("💡 **Secure Transactions:** Send APT directly from your authenticated wallet")
|
| 46 |
+
|
| 47 |
+
if not app.system_wallet:
|
| 48 |
+
st.error("Wallet service not configured. Sending transactions is disabled.")
|
| 49 |
+
st.info("Please try again later when the service is available.")
|
| 50 |
+
else:
|
| 51 |
+
with st.form("send_transaction"):
|
| 52 |
+
recipient_address = st.text_input(
|
| 53 |
+
"Recipient Address",
|
| 54 |
+
placeholder="0x1234abcd...",
|
| 55 |
+
help="Enter the Aptos address to send funds to"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
amount = st.number_input(
|
| 59 |
+
"Amount (APT)",
|
| 60 |
+
min_value=0.00000001,
|
| 61 |
+
max_value=100.0,
|
| 62 |
+
value=0.1,
|
| 63 |
+
step=0.01,
|
| 64 |
+
format="%.8f"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
st.markdown("**Transaction Preview:**")
|
| 68 |
+
st.markdown(f"""
|
| 69 |
+
- **From:** Your Authenticated Wallet
|
| 70 |
+
- **To:** `{recipient_address[:20] + '...' if len(recipient_address) > 20 else recipient_address}`
|
| 71 |
+
- **Amount:** {amount} APT
|
| 72 |
+
- **Fee:** ~0.001 APT (estimated)
|
| 73 |
+
""")
|
| 74 |
+
|
| 75 |
+
send_transaction = st.form_submit_button("🚀 Send Transaction", type="primary")
|
| 76 |
+
|
| 77 |
+
if send_transaction:
|
| 78 |
+
if not recipient_address:
|
| 79 |
+
st.error("Please enter a recipient address")
|
| 80 |
+
elif len(recipient_address) < 10:
|
| 81 |
+
st.error("Invalid recipient address")
|
| 82 |
+
else:
|
| 83 |
+
with st.spinner("Processing transaction through system wallet..."):
|
| 84 |
+
try:
|
| 85 |
+
# Create transaction from system wallet
|
| 86 |
+
amount_in_octas = int(amount * 100000000)
|
| 87 |
+
|
| 88 |
+
# Create BCS serializer for the amount
|
| 89 |
+
serializer = Serializer()
|
| 90 |
+
serializer.u64(amount_in_octas)
|
| 91 |
+
serialized_amount = serializer.output()
|
| 92 |
+
|
| 93 |
+
# Make the transaction process more robust
|
| 94 |
+
try:
|
| 95 |
+
payload = EntryFunction.natural(
|
| 96 |
+
"0x1::coin",
|
| 97 |
+
"transfer",
|
| 98 |
+
["0x1::aptos_coin::AptosCoin"],
|
| 99 |
+
[recipient_address, serialized_amount]
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Create and submit transaction - handling potential async issues
|
| 103 |
+
from utils.aptos_sync import RestClientSync
|
| 104 |
+
# Use the sync wrapper to ensure compatibility with streamlit
|
| 105 |
+
sync_client = RestClientSync("https://testnet.aptoslabs.com/v1")
|
| 106 |
+
|
| 107 |
+
# Create and process the transaction
|
| 108 |
+
with st.spinner("Creating transaction..."):
|
| 109 |
+
txn = sync_client.create_transaction(app.system_wallet.address(), payload)
|
| 110 |
+
|
| 111 |
+
with st.spinner("Signing transaction..."):
|
| 112 |
+
signed_txn = app.system_wallet.sign_transaction(txn)
|
| 113 |
+
|
| 114 |
+
with st.spinner("Submitting transaction..."):
|
| 115 |
+
txn_hash = sync_client.submit_transaction(signed_txn)
|
| 116 |
+
|
| 117 |
+
with st.spinner("Waiting for confirmation..."):
|
| 118 |
+
sync_client.wait_for_transaction(txn_hash, timeout=30)
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
st.error(f"Transaction failed: {str(e)}")
|
| 122 |
+
st.warning("Please try again later.")
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
# Record transaction in our history
|
| 126 |
+
app.add_transaction(
|
| 127 |
+
txn_hash=txn_hash,
|
| 128 |
+
sender=str(app.system_wallet.address()),
|
| 129 |
+
recipient=recipient_address,
|
| 130 |
+
amount=amount,
|
| 131 |
+
is_credit=False,
|
| 132 |
+
status="completed",
|
| 133 |
+
description=f"Transfer to {recipient_address[:10]}..."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
st.session_state.app = app
|
| 137 |
+
app.save_to_session()
|
| 138 |
+
|
| 139 |
+
st.success("✅ Transaction sent successfully!")
|
| 140 |
+
st.success(f"📋 Transaction Hash: `{txn_hash}`")
|
| 141 |
+
st.markdown("📋 You can view this transaction in your **Transaction History** page")
|
| 142 |
+
|
| 143 |
+
# Show transaction details
|
| 144 |
+
with st.expander("Transaction Details", expanded=True):
|
| 145 |
+
st.markdown(f"""
|
| 146 |
+
- **Hash:** `{txn_hash}`
|
| 147 |
+
- **From:** Your Authenticated Wallet
|
| 148 |
+
- **To:** `{recipient_address}`
|
| 149 |
+
- **Amount:** {amount} APT
|
| 150 |
+
- **Status:** Confirmed ✅
|
| 151 |
+
""")
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
st.error(f"❌ Transaction failed: {str(e)}")
|
| 155 |
+
|
| 156 |
+
# Message signing
|
| 157 |
+
st.markdown("---")
|
| 158 |
+
st.subheader("✍️ Sign Message")
|
| 159 |
+
st.info("💡 **Secure signing:** Messages are signed using your authenticated session")
|
| 160 |
+
|
| 161 |
+
with st.form("sign_message"):
|
| 162 |
+
message_to_sign = st.text_area(
|
| 163 |
+
"Message to Sign",
|
| 164 |
+
placeholder="Enter your message here...",
|
| 165 |
+
height=100
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
sign_message = st.form_submit_button("✍️ Sign Message", type="secondary")
|
| 169 |
+
|
| 170 |
+
if sign_message:
|
| 171 |
+
if not message_to_sign:
|
| 172 |
+
st.error("Please enter a message to sign")
|
| 173 |
+
else:
|
| 174 |
+
try:
|
| 175 |
+
# Sign with original wallet for authenticity
|
| 176 |
+
signature = app.wallet.sign(message_to_sign.encode())
|
| 177 |
+
signature_hex = signature.hex()
|
| 178 |
+
|
| 179 |
+
st.success("✅ Message signed successfully!")
|
| 180 |
+
|
| 181 |
+
with st.expander("Signature Details", expanded=True):
|
| 182 |
+
st.markdown("**Original Message:**")
|
| 183 |
+
st.code(message_to_sign)
|
| 184 |
+
st.markdown("**Signature:**")
|
| 185 |
+
st.code(signature_hex)
|
| 186 |
+
st.markdown("**Signer Address:**")
|
| 187 |
+
st.code(str(app.wallet.address()))
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
st.error(f"❌ Signing failed: {str(e)}")
|
| 191 |
+
|
| 192 |
+
# Account management
|
| 193 |
+
st.markdown("---")
|
| 194 |
+
st.subheader("⚙️ Account Management")
|
| 195 |
+
|
| 196 |
+
col1, col2 = st.columns(2)
|
| 197 |
+
|
| 198 |
+
with col1:
|
| 199 |
+
st.markdown("**Session Control:**")
|
| 200 |
+
if st.button("🔄 Refresh Authentication", type="secondary"):
|
| 201 |
+
st.info("Session refreshed successfully")
|
| 202 |
+
st.rerun()
|
| 203 |
+
|
| 204 |
+
if st.button("🚪 Logout", type="secondary"):
|
| 205 |
+
app.is_authenticated = False
|
| 206 |
+
app.save_to_session()
|
| 207 |
+
st.success("Logged out successfully")
|
| 208 |
+
st.info("👈 Go to Authentication to login again")
|
| 209 |
+
st.rerun()
|
| 210 |
+
|
| 211 |
+
with col2:
|
| 212 |
+
st.markdown("**Account Info:**")
|
| 213 |
+
with st.expander("View Account Details"):
|
| 214 |
+
st.markdown(f"""
|
| 215 |
+
**Wallet Address:** `{app.wallet.address()}`
|
| 216 |
+
|
| 217 |
+
**Selected Secret:** {app.selected_secret} (U+{ord(app.selected_secret):04X})
|
| 218 |
+
|
| 219 |
+
**Registration Status:** ✅ Registered
|
| 220 |
+
|
| 221 |
+
**Authentication Status:** ✅ Active
|
| 222 |
+
""")
|
| 223 |
+
|
| 224 |
+
# Recent transactions (placeholder)
|
| 225 |
+
st.markdown("---")
|
| 226 |
+
st.subheader("📋 Recent Activity")
|
| 227 |
+
st.info("Transaction history feature coming soon...")
|
| 228 |
+
|
| 229 |
+
# Security notice
|
| 230 |
+
st.markdown("---")
|
| 231 |
+
st.warning("🔒 **Security Notice:** Your private key is securely managed by the 1P system. Never share your secret character or direction mapping with anyone.")
|
src/pages/registration.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from utils.transfer_utils import transfer_apt_sync
|
| 4 |
+
from components.auth_component import one_round_auth
|
| 5 |
+
|
| 6 |
+
from pages import app
|
| 7 |
+
|
| 8 |
+
# Registration Page
|
| 9 |
+
st.header("📝 Registration")
|
| 10 |
+
|
| 11 |
+
if not app.wallet:
|
| 12 |
+
st.error("❌ Please connect a wallet first")
|
| 13 |
+
st.info("👈 Go to 'Import/Generate Wallet' to get started")
|
| 14 |
+
st.stop()
|
| 15 |
+
|
| 16 |
+
if app.is_registered:
|
| 17 |
+
st.success("✅ You are already registered!")
|
| 18 |
+
st.info("👈 Go to 'Authentication' to verify your 1P secret")
|
| 19 |
+
st.stop()
|
| 20 |
+
|
| 21 |
+
st.markdown("""
|
| 22 |
+
### Registration Process:
|
| 23 |
+
1. **Select your 1P secret** - Choose one UTF-8 character elegantly
|
| 24 |
+
2. **Configure direction mapping** - Set your color-to-direction preferences
|
| 25 |
+
3. **Authenticate yourself** - Verify your 1P secret with a quick challenge
|
| 26 |
+
4. **Transfer minimum 1 APT** - Funds will be held in our secure system wallet
|
| 27 |
+
5. **Complete registration** - Your wallet will be registered for 1P authentication
|
| 28 |
+
""")
|
| 29 |
+
|
| 30 |
+
# Step 1: UTF-8 Character Selection
|
| 31 |
+
st.markdown("---")
|
| 32 |
+
st.subheader("🎨 Step 1: Select Your 1P Secret")
|
| 33 |
+
st.markdown("Choose **one character** that will be your secret. No keyboard typing required!")
|
| 34 |
+
|
| 35 |
+
# Language/Category filters
|
| 36 |
+
col1, col2 = st.columns(2)
|
| 37 |
+
with col1:
|
| 38 |
+
category_type = st.selectbox(
|
| 39 |
+
"Category Type",
|
| 40 |
+
options=["Emojis & Symbols", "Languages", "All Categories"],
|
| 41 |
+
index=0,
|
| 42 |
+
help="Filter by type of character categories"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Dynamically set options based on category type
|
| 46 |
+
if category_type == "Emojis & Symbols":
|
| 47 |
+
category_options = ['emojis', 'hearts', 'nature', 'food', 'animals', 'travel', 'sports', 'tech', 'music', 'weather', 'zodiac', 'numbers', 'symbols', 'ascii']
|
| 48 |
+
elif category_type == "Languages":
|
| 49 |
+
category_options = ['japanese', 'korean', 'chinese', 'arabic', 'cyrillic', 'ascii']
|
| 50 |
+
else:
|
| 51 |
+
category_options = list(DOMAINS.keys())
|
| 52 |
+
|
| 53 |
+
selected_categories = st.multiselect(
|
| 54 |
+
"Character Categories",
|
| 55 |
+
options=category_options,
|
| 56 |
+
default=['emojis'] if category_type == "Emojis & Symbols" else ['ascii'] if category_type == "Languages" else ['emojis'],
|
| 57 |
+
help="Select which types of characters to show"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
with col2:
|
| 61 |
+
col2_1, col2_2 = st.columns(2)
|
| 62 |
+
with col2_1:
|
| 63 |
+
chars_per_row = st.slider("Characters per row", 5, 20, 10)
|
| 64 |
+
with col2_2:
|
| 65 |
+
show_unicode = st.checkbox("Show Unicode codes", False)
|
| 66 |
+
|
| 67 |
+
search_term = st.text_input("Search (emoji description or character)", "",
|
| 68 |
+
placeholder="heart, food, smile, etc.",
|
| 69 |
+
help="Filter characters by description")
|
| 70 |
+
|
| 71 |
+
# Build character set based on selection
|
| 72 |
+
available_chars = ""
|
| 73 |
+
for category in selected_categories:
|
| 74 |
+
available_chars += DOMAINS[category]
|
| 75 |
+
|
| 76 |
+
if not available_chars:
|
| 77 |
+
st.warning("Please select at least one character category")
|
| 78 |
+
st.stop()
|
| 79 |
+
|
| 80 |
+
# Apply search filter if provided
|
| 81 |
+
chars_list = list(set(available_chars)) # Remove duplicates
|
| 82 |
+
|
| 83 |
+
if search_term:
|
| 84 |
+
# Simple filtering mechanism
|
| 85 |
+
search_term = search_term.lower()
|
| 86 |
+
|
| 87 |
+
# Define some common emoji descriptions for better search
|
| 88 |
+
emoji_descriptions = {
|
| 89 |
+
'smile': '😀😃😄😁😆',
|
| 90 |
+
'laugh': '😂🤣',
|
| 91 |
+
'heart': '❤️💖💝💘💗💓💕💞💜🧡💛💚💙',
|
| 92 |
+
'food': '🍎🍌🍇🍓🍈🍉🍊🍋🥭🍑🍒🥝🍍🥥🍅🥑🍆🥔🥕🌽',
|
| 93 |
+
'animal': '🐶🐱🐭🐹🐰🦊🐻🐼🐨🦁🐯🐮🐷🐸🐵🐔',
|
| 94 |
+
'flower': '🌸🌺🌻🌷🌹🌼',
|
| 95 |
+
'star': '⭐🌟💫✨',
|
| 96 |
+
'face': '😀😃😄😁😆😅😂🤣😊😇🙂🙃😉😌😍',
|
| 97 |
+
'hand': '👍👎👌✌️🤞🤟🤘👊✊🤛🤜👏',
|
| 98 |
+
'music': '🎵🎶🎸🎹🎷🎺🎻🥁🎼',
|
| 99 |
+
'sport': '⚽⚾🏀🏐🏈🏉🎾🏓🏸',
|
| 100 |
+
'travel': '✈️🚆🚂🚄🚘🚲',
|
| 101 |
+
'weather': '☀️🌤️⛅🌥️☁️🌦️🌧️⛈️'
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
filtered_chars = []
|
| 105 |
+
for char in chars_list:
|
| 106 |
+
# Check if char is in any of the emoji description groups that match the search term
|
| 107 |
+
in_description = False
|
| 108 |
+
for desc, emoji_group in emoji_descriptions.items():
|
| 109 |
+
if desc.lower().find(search_term) >= 0 and char in emoji_group:
|
| 110 |
+
in_description = True
|
| 111 |
+
break
|
| 112 |
+
|
| 113 |
+
# Add char if it matches search
|
| 114 |
+
if in_description or char.lower() == search_term.lower():
|
| 115 |
+
filtered_chars.append(char)
|
| 116 |
+
|
| 117 |
+
chars_list = filtered_chars if filtered_chars else chars_list
|
| 118 |
+
|
| 119 |
+
# Sort the characters
|
| 120 |
+
chars_list.sort()
|
| 121 |
+
|
| 122 |
+
# Create a pagination system for large character sets
|
| 123 |
+
chars_per_page = chars_per_row * 5 # 5 rows per page
|
| 124 |
+
total_chars = len(chars_list)
|
| 125 |
+
total_pages = (total_chars + chars_per_page - 1) // chars_per_page # Ceiling division
|
| 126 |
+
|
| 127 |
+
# Only show pagination if needed
|
| 128 |
+
if total_pages > 1:
|
| 129 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 130 |
+
with col2:
|
| 131 |
+
page_num = st.select_slider("Page", options=list(range(1, total_pages + 1)), value=1)
|
| 132 |
+
else:
|
| 133 |
+
page_num = 1
|
| 134 |
+
|
| 135 |
+
start_idx = (page_num - 1) * chars_per_page
|
| 136 |
+
end_idx = min(start_idx + chars_per_page, total_chars)
|
| 137 |
+
|
| 138 |
+
# Display character selection grid
|
| 139 |
+
st.markdown(f"**Available Characters:** ({total_chars} characters found)")
|
| 140 |
+
visible_chars = chars_list[start_idx:end_idx]
|
| 141 |
+
|
| 142 |
+
# Create grid display
|
| 143 |
+
rows = [visible_chars[i:i + chars_per_row] for i in range(0, len(visible_chars), chars_per_row)]
|
| 144 |
+
|
| 145 |
+
selected_secret = None
|
| 146 |
+
for row_idx, row in enumerate(rows):
|
| 147 |
+
cols = st.columns(len(row))
|
| 148 |
+
for col_idx, char in enumerate(row):
|
| 149 |
+
with cols[col_idx]:
|
| 150 |
+
unicode_info = f"\\nU+{ord(char):04X}" if show_unicode else ""
|
| 151 |
+
if st.button(f"{char}{unicode_info}",
|
| 152 |
+
key=f"char_{row_idx}_{col_idx}_p{page_num}",
|
| 153 |
+
use_container_width=True):
|
| 154 |
+
selected_secret = char
|
| 155 |
+
app.selected_secret = char
|
| 156 |
+
app.save_to_session()
|
| 157 |
+
st.rerun()
|
| 158 |
+
|
| 159 |
+
# Show recently used characters for quick selection
|
| 160 |
+
if not app.selected_secret and (app.recent_characters or app.favorite_characters):
|
| 161 |
+
st.markdown("---")
|
| 162 |
+
st.subheader("⭐ Quick Selection")
|
| 163 |
+
|
| 164 |
+
# Show favorites if available
|
| 165 |
+
if app.favorite_characters:
|
| 166 |
+
st.markdown("**Favorite Characters:**")
|
| 167 |
+
fav_cols = st.columns(min(10, len(app.favorite_characters)))
|
| 168 |
+
for idx, char in enumerate(app.favorite_characters):
|
| 169 |
+
with fav_cols[idx % len(fav_cols)]:
|
| 170 |
+
if st.button(f"{char}",
|
| 171 |
+
key=f"fav_{idx}",
|
| 172 |
+
use_container_width=True):
|
| 173 |
+
app.selected_secret = char
|
| 174 |
+
app.save_to_session()
|
| 175 |
+
st.rerun()
|
| 176 |
+
|
| 177 |
+
# Show recent characters if available
|
| 178 |
+
if app.recent_characters:
|
| 179 |
+
st.markdown("**Recently Used:**")
|
| 180 |
+
recent_cols = st.columns(min(10, len(app.recent_characters)))
|
| 181 |
+
for idx, char in enumerate(app.recent_characters):
|
| 182 |
+
with recent_cols[idx % len(recent_cols)]:
|
| 183 |
+
if st.button(f"{char}",
|
| 184 |
+
key=f"recent_{idx}",
|
| 185 |
+
use_container_width=True):
|
| 186 |
+
app.selected_secret = char
|
| 187 |
+
|
| 188 |
+
# Add to favorites
|
| 189 |
+
with recent_cols[idx % len(recent_cols)]:
|
| 190 |
+
if st.button("⭐", key=f"fav_add_{idx}", help="Add to favorites"):
|
| 191 |
+
if char not in app.favorite_characters:
|
| 192 |
+
app.favorite_characters.append(char)
|
| 193 |
+
if len(app.favorite_characters) > 10:
|
| 194 |
+
app.favorite_characters.pop(0) # Remove oldest if over limit
|
| 195 |
+
|
| 196 |
+
app.save_to_session()
|
| 197 |
+
st.rerun()
|
| 198 |
+
|
| 199 |
+
# Show selected secret
|
| 200 |
+
if app.selected_secret:
|
| 201 |
+
st.success(f"✅ Selected secret: **{app.selected_secret}** (U+{ord(app.selected_secret):04X})")
|
| 202 |
+
|
| 203 |
+
# Add selected character to recent list if not already there
|
| 204 |
+
if app.selected_secret not in app.recent_characters:
|
| 205 |
+
app.recent_characters.append(app.selected_secret)
|
| 206 |
+
# Keep only the last 10 characters
|
| 207 |
+
if len(app.recent_characters) > 10:
|
| 208 |
+
app.recent_characters.pop(0)
|
| 209 |
+
app.save_to_session()
|
| 210 |
+
|
| 211 |
+
# Option to add to favorites
|
| 212 |
+
col1, col2 = st.columns([3, 1])
|
| 213 |
+
with col2:
|
| 214 |
+
if app.selected_secret not in app.favorite_characters:
|
| 215 |
+
if st.button("⭐ Add to Favorites"):
|
| 216 |
+
app.favorite_characters.append(app.selected_secret)
|
| 217 |
+
if len(app.favorite_characters) > 10:
|
| 218 |
+
app.favorite_characters.pop(0) # Remove oldest if over limit
|
| 219 |
+
app.save_to_session()
|
| 220 |
+
st.rerun()
|
| 221 |
+
else:
|
| 222 |
+
if st.button("❌ Remove from Favorites"):
|
| 223 |
+
app.favorite_characters.remove(app.selected_secret)
|
| 224 |
+
app.save_to_session()
|
| 225 |
+
st.rerun()
|
| 226 |
+
|
| 227 |
+
# Step 2: Direction Mapping Configuration
|
| 228 |
+
if app.selected_secret:
|
| 229 |
+
st.markdown("---")
|
| 230 |
+
st.subheader("🧭 Step 2: Configure Direction Mapping")
|
| 231 |
+
st.markdown("Set your preferred directions for each color. This will be used during authentication.")
|
| 232 |
+
|
| 233 |
+
col1, col2 = st.columns(2)
|
| 234 |
+
|
| 235 |
+
direction_mapping = {}
|
| 236 |
+
with col1:
|
| 237 |
+
st.markdown("**Color → Direction Mapping:**")
|
| 238 |
+
for color in COLORS:
|
| 239 |
+
direction_mapping[color] = st.selectbox(
|
| 240 |
+
f"{color.title()} →",
|
| 241 |
+
options=DIRECTIONS,
|
| 242 |
+
key=f"direction_{color}",
|
| 243 |
+
index=DIRECTIONS.index(DIRECTIONS[COLORS.index(color)]) # Default mapping
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
with col2:
|
| 247 |
+
st.markdown("**Preview:**")
|
| 248 |
+
for color, direction in direction_mapping.items():
|
| 249 |
+
emoji_map = {"Up": "⬆️", "Down": "⬇️", "Left": "⬅️", "Right": "➡️", "Skip": "⏭️"}
|
| 250 |
+
st.text(f"{color.title()}: {direction} {emoji_map[direction]}")
|
| 251 |
+
|
| 252 |
+
app.direction_mapping = direction_mapping
|
| 253 |
+
app.save_to_session()
|
| 254 |
+
|
| 255 |
+
# Step 3: Authentication
|
| 256 |
+
if app.selected_secret and app.direction_mapping:
|
| 257 |
+
st.markdown("---")
|
| 258 |
+
st.subheader("🔐 Step 3: Authenticate Yourself")
|
| 259 |
+
|
| 260 |
+
st.markdown("""Verify your identity using the 1P visual grid system. This ensures you remember your secret
|
| 261 |
+
character and color-to-direction mapping before proceeding with fund transfer.""")
|
| 262 |
+
|
| 263 |
+
# Show authentication challenge using the component
|
| 264 |
+
auth_success = one_round_auth(
|
| 265 |
+
secret=app.selected_secret,
|
| 266 |
+
direction_mapping=app.direction_mapping,
|
| 267 |
+
colors=COLORS,
|
| 268 |
+
direction_map=DIRECTION_MAP,
|
| 269 |
+
domains=DOMAINS,
|
| 270 |
+
session_key="registration_auth"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if not auth_success:
|
| 274 |
+
# If auth is not completed or failed, stop here
|
| 275 |
+
if app.registration_auth and app.registration_auth.get("completed"):
|
| 276 |
+
if not app.registration_auth.get("success"):
|
| 277 |
+
st.error("❌ Authentication failed! Please try again.")
|
| 278 |
+
if st.button("🔄 Try Again", key="auth_retry"):
|
| 279 |
+
# Reset auth state
|
| 280 |
+
app.registration_auth = {
|
| 281 |
+
'started': False,
|
| 282 |
+
'completed': False,
|
| 283 |
+
'success': False,
|
| 284 |
+
'grid_html': None,
|
| 285 |
+
'expected': None
|
| 286 |
+
}
|
| 287 |
+
app.save_to_session()
|
| 288 |
+
st.rerun()
|
| 289 |
+
st.stop()
|
| 290 |
+
|
| 291 |
+
st.success("✅ Authentication successful!")
|
| 292 |
+
|
| 293 |
+
# Step 4: Balance Transfer
|
| 294 |
+
if app.selected_secret and app.direction_mapping and app.registration_auth and app.registration_auth.get("completed") and app.registration_auth.get("success"):
|
| 295 |
+
st.markdown("---")
|
| 296 |
+
st.subheader("💰 Step 4: Transfer Funds for Registration")
|
| 297 |
+
|
| 298 |
+
st.markdown("**Why transfer funds?**")
|
| 299 |
+
st.markdown("""
|
| 300 |
+
- Transfers 1 APT minimum to register for the 1P system
|
| 301 |
+
- Your funds are held securely in our system wallet
|
| 302 |
+
- Transactions are processed through our secure backend
|
| 303 |
+
- Your private key is never exposed after registration
|
| 304 |
+
""")
|
| 305 |
+
|
| 306 |
+
# Check current balance automatically
|
| 307 |
+
with st.spinner("Checking wallet balance..."):
|
| 308 |
+
try:
|
| 309 |
+
# Try to use the sync helper method
|
| 310 |
+
apt_balance = app.get_account_balance_sync(app.wallet.address())
|
| 311 |
+
|
| 312 |
+
# Display balance with colorful metric
|
| 313 |
+
col1, col2 = st.columns(2)
|
| 314 |
+
with col1:
|
| 315 |
+
st.metric("Current Wallet Balance", f"{apt_balance} APT")
|
| 316 |
+
with col2:
|
| 317 |
+
if apt_balance >= 1.0:
|
| 318 |
+
st.success("✅ Sufficient balance for registration")
|
| 319 |
+
else:
|
| 320 |
+
st.error("❌ Insufficient balance. Need at least 1 APT.")
|
| 321 |
+
st.warning("Please use the faucet in the wallet setup page.")
|
| 322 |
+
st.stop()
|
| 323 |
+
|
| 324 |
+
# Add refresh button
|
| 325 |
+
if st.button("🔄 Refresh Balance", type="secondary"):
|
| 326 |
+
st.rerun()
|
| 327 |
+
|
| 328 |
+
except Exception as e:
|
| 329 |
+
st.error(f"Balance check error: {str(e)}")
|
| 330 |
+
st.warning("Unable to check balance automatically. You can proceed if you know you have sufficient funds (at least 1 APT).")
|
| 331 |
+
|
| 332 |
+
# Add manual balance check option
|
| 333 |
+
if st.button("Try Again", type="secondary"):
|
| 334 |
+
st.rerun()
|
| 335 |
+
|
| 336 |
+
# Provide option to continue anyway
|
| 337 |
+
st.info("If you're certain you have at least 1 APT, you can continue with the registration.")
|
| 338 |
+
|
| 339 |
+
# Option to proceed anyway
|
| 340 |
+
if not st.checkbox("I understand the risks and want to proceed anyway"):
|
| 341 |
+
st.stop()
|
| 342 |
+
|
| 343 |
+
# Transfer amount selection
|
| 344 |
+
col1, col2 = st.columns(2)
|
| 345 |
+
with col1:
|
| 346 |
+
transfer_amount = st.number_input(
|
| 347 |
+
"Transfer Amount (APT)",
|
| 348 |
+
min_value=1.0,
|
| 349 |
+
max_value=100.0,
|
| 350 |
+
value=1.0,
|
| 351 |
+
step=0.1,
|
| 352 |
+
help="Minimum 1 APT required"
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
with col2:
|
| 356 |
+
st.markdown("<br>", unsafe_allow_html=True) # Spacing
|
| 357 |
+
leave_for_gas = st.checkbox("Leave 0.1 APT for gas fees", value=True)
|
| 358 |
+
|
| 359 |
+
# Step 5: Complete Registration
|
| 360 |
+
st.markdown("---")
|
| 361 |
+
st.subheader("✅ Step 5: Complete Registration")
|
| 362 |
+
|
| 363 |
+
st.warning("⚠️ **Final Check:**")
|
| 364 |
+
st.markdown(f"""
|
| 365 |
+
- **Secret Character:** {app.selected_secret}
|
| 366 |
+
- **Direction Mapping:** {len(app.direction_mapping)} colors configured
|
| 367 |
+
- **Transfer Amount:** {transfer_amount} APT
|
| 368 |
+
- **From Wallet:** `{app.wallet.address()[:10]}...`
|
| 369 |
+
""")
|
| 370 |
+
|
| 371 |
+
st.error("🔒 **Important:** After registration, your wallet's private key will be securely handled by our system. Make sure you're ready to proceed.")
|
| 372 |
+
|
| 373 |
+
confirm_registration = st.checkbox("I understand and want to proceed with registration")
|
| 374 |
+
|
| 375 |
+
if confirm_registration and st.button("🚀 Complete Registration", type="primary"):
|
| 376 |
+
with st.spinner("Processing registration..."):
|
| 377 |
+
try:
|
| 378 |
+
# Check user wallet balance first
|
| 379 |
+
apt_balance = app.get_account_balance_sync(app.wallet.address())
|
| 380 |
+
|
| 381 |
+
if apt_balance < transfer_amount:
|
| 382 |
+
st.error(f"❌ Insufficient balance: You have {apt_balance} APT but are trying to transfer {transfer_amount} APT")
|
| 383 |
+
st.warning("Please get more APT from the faucet or reduce the transfer amount.")
|
| 384 |
+
st.stop()
|
| 385 |
+
|
| 386 |
+
# Use our abstracted transfer function
|
| 387 |
+
with st.spinner("Creating and processing transaction..."):
|
| 388 |
+
success, txn_hash, error_msg = transfer_apt_sync(
|
| 389 |
+
sender_account=app.wallet,
|
| 390 |
+
recipient_address=SYSTEM_WALLET_ADDRESS,
|
| 391 |
+
amount_apt=transfer_amount
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
if not success:
|
| 395 |
+
st.error(f"Transaction failed: {error_msg}")
|
| 396 |
+
st.warning("Please check your balance and try again.")
|
| 397 |
+
st.stop()
|
| 398 |
+
|
| 399 |
+
# Mark as registered and record the transaction
|
| 400 |
+
app.is_registered = True
|
| 401 |
+
|
| 402 |
+
# Record transaction in our history
|
| 403 |
+
app.add_transaction(
|
| 404 |
+
txn_hash=txn_hash,
|
| 405 |
+
sender=str(app.wallet.address()),
|
| 406 |
+
recipient=SYSTEM_WALLET_ADDRESS,
|
| 407 |
+
amount=transfer_amount,
|
| 408 |
+
is_credit=False,
|
| 409 |
+
status="completed",
|
| 410 |
+
description="1P Wallet Registration"
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# Persist changes to session
|
| 414 |
+
app.save_to_session()
|
| 415 |
+
|
| 416 |
+
st.success("🎉 Registration completed successfully!")
|
| 417 |
+
st.success(f"✅ Transaction Hash: `{txn_hash}`")
|
| 418 |
+
st.info("**Next:** Go to 'Authentication' to verify your 1P secret")
|
| 419 |
+
st.markdown("📋 You can view this transaction in your **Transaction History** page")
|
| 420 |
+
|
| 421 |
+
# Show registration summary
|
| 422 |
+
with st.expander("Registration Summary", expanded=True):
|
| 423 |
+
st.markdown(f"""
|
| 424 |
+
- **Wallet:** `{app.wallet.address()}`
|
| 425 |
+
- **Secret:** {app.selected_secret} (U+{ord(app.selected_secret):04X})
|
| 426 |
+
- **Amount Transferred:** {transfer_amount} APT
|
| 427 |
+
- **Transaction:** `{txn_hash}`
|
| 428 |
+
- **System Wallet:** `{SYSTEM_WALLET_ADDRESS}`
|
| 429 |
+
""")
|
| 430 |
+
|
| 431 |
+
except Exception as e:
|
| 432 |
+
st.error(f"❌ Registration failed: {str(e)}")
|
| 433 |
+
st.error("Please check your balance and try again")
|
src/pages/transaction_history.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Transaction History Page
|
| 2 |
+
# This page displays the user's transaction history, showing credits and debits
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
from pages import app
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
st.header("📋 Transaction History")
|
| 10 |
+
|
| 11 |
+
if not app.wallet:
|
| 12 |
+
st.error("❌ Please connect a wallet first")
|
| 13 |
+
st.info("👈 Go to 'Import/Generate Wallet' to get started")
|
| 14 |
+
st.stop()
|
| 15 |
+
|
| 16 |
+
# Button to refresh transaction history
|
| 17 |
+
col1, col2 = st.columns([3, 1])
|
| 18 |
+
with col1:
|
| 19 |
+
st.markdown("View your transaction history, including credits and debits")
|
| 20 |
+
with col2:
|
| 21 |
+
if st.button("🔄 Refresh History", type="secondary"):
|
| 22 |
+
with st.spinner("Updating transaction history..."):
|
| 23 |
+
success = app.update_transaction_history()
|
| 24 |
+
if success:
|
| 25 |
+
st.success("Transaction history updated!")
|
| 26 |
+
else:
|
| 27 |
+
st.error("Failed to update transaction history")
|
| 28 |
+
st.rerun()
|
| 29 |
+
|
| 30 |
+
# If we don't have any transactions yet, try to fetch them
|
| 31 |
+
if not app.transactions:
|
| 32 |
+
with st.spinner("Fetching your transaction history..."):
|
| 33 |
+
app.update_transaction_history()
|
| 34 |
+
st.session_state.app = app
|
| 35 |
+
|
| 36 |
+
# Show transaction summary
|
| 37 |
+
st.markdown("---")
|
| 38 |
+
st.subheader("💰 Balance Summary")
|
| 39 |
+
|
| 40 |
+
# Calculate summary statistics
|
| 41 |
+
if app.transactions:
|
| 42 |
+
total_credits = sum(txn.amount for txn in app.transactions if txn.is_credit and txn.status == "completed")
|
| 43 |
+
total_debits = sum(txn.amount for txn in app.transactions if not txn.is_credit and txn.status == "completed")
|
| 44 |
+
net_balance = total_credits - total_debits
|
| 45 |
+
|
| 46 |
+
# Display summary
|
| 47 |
+
col1, col2, col3 = st.columns(3)
|
| 48 |
+
with col1:
|
| 49 |
+
st.metric("Total Credits", f"{total_credits:.4f} APT", delta=f"{total_credits:.2f}")
|
| 50 |
+
with col2:
|
| 51 |
+
st.metric("Total Debits", f"{total_debits:.4f} APT", delta=f"-{total_debits:.2f}", delta_color="inverse")
|
| 52 |
+
with col3:
|
| 53 |
+
st.metric("Net Balance", f"{net_balance:.4f} APT")
|
| 54 |
+
|
| 55 |
+
# Display current blockchain balance
|
| 56 |
+
st.markdown("---")
|
| 57 |
+
with st.spinner("Checking current blockchain balance..."):
|
| 58 |
+
try:
|
| 59 |
+
current_balance = app.get_account_balance_sync(app.wallet.address())
|
| 60 |
+
st.info(f"Current blockchain balance: **{current_balance:.4f} APT**")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
st.warning(f"Could not fetch current balance: {str(e)}")
|
| 63 |
+
else:
|
| 64 |
+
st.info("No transactions found. Your transaction history will appear here once you make transfers.")
|
| 65 |
+
|
| 66 |
+
# Display transaction list
|
| 67 |
+
st.markdown("---")
|
| 68 |
+
st.subheader("📝 Transaction List")
|
| 69 |
+
|
| 70 |
+
if app.transactions:
|
| 71 |
+
# Create tabs for all/credits/debits
|
| 72 |
+
tab1, tab2, tab3 = st.tabs(["All Transactions", "Credits (Received)", "Debits (Sent)"])
|
| 73 |
+
|
| 74 |
+
with tab1:
|
| 75 |
+
# All transactions
|
| 76 |
+
for idx, txn in enumerate(app.transactions):
|
| 77 |
+
with st.expander(
|
| 78 |
+
f"{'↘️ Received' if txn.is_credit else '↗️ Sent'} {txn.amount:.4f} APT - {time.strftime('%Y-%m-%d %H:%M', time.localtime(txn.timestamp))}",
|
| 79 |
+
expanded=(idx == 0) # Only expand first one by default
|
| 80 |
+
):
|
| 81 |
+
st.markdown(f"""
|
| 82 |
+
**Transaction:** `{txn.txn_hash[:10]}...{txn.txn_hash[-6:]}`
|
| 83 |
+
**From:** `{txn.sender[:10]}...{txn.sender[-6:]}`
|
| 84 |
+
**To:** `{txn.recipient[:10]}...{txn.recipient[-6:]}`
|
| 85 |
+
**Amount:** {txn.amount:.8f} APT
|
| 86 |
+
**Status:** {txn.status.title()}
|
| 87 |
+
**Date:** {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(txn.timestamp))}
|
| 88 |
+
""")
|
| 89 |
+
|
| 90 |
+
# Add link to explorer
|
| 91 |
+
st.markdown(f"[View on Explorer](https://explorer.aptoslabs.com/txn/{txn.txn_hash}?network=testnet)")
|
| 92 |
+
|
| 93 |
+
with tab2:
|
| 94 |
+
# Credits only
|
| 95 |
+
credits = [txn for txn in app.transactions if txn.is_credit]
|
| 96 |
+
if credits:
|
| 97 |
+
for idx, txn in enumerate(credits):
|
| 98 |
+
with st.expander(
|
| 99 |
+
f"↘️ Received {txn.amount:.4f} APT - {time.strftime('%Y-%m-%d %H:%M', time.localtime(txn.timestamp))}",
|
| 100 |
+
expanded=(idx == 0)
|
| 101 |
+
):
|
| 102 |
+
st.markdown(f"""
|
| 103 |
+
**Transaction:** `{txn.txn_hash[:10]}...{txn.txn_hash[-6:]}`
|
| 104 |
+
**From:** `{txn.sender[:10]}...{txn.sender[-6:]}`
|
| 105 |
+
**To:** `{txn.recipient[:10]}...{txn.recipient[-6:]}`
|
| 106 |
+
**Amount:** {txn.amount:.8f} APT
|
| 107 |
+
**Status:** {txn.status.title()}
|
| 108 |
+
**Date:** {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(txn.timestamp))}
|
| 109 |
+
""")
|
| 110 |
+
|
| 111 |
+
# Add link to explorer
|
| 112 |
+
st.markdown(f"[View on Explorer](https://explorer.aptoslabs.com/txn/{txn.txn_hash}?network=testnet)")
|
| 113 |
+
else:
|
| 114 |
+
st.info("No credits (received funds) found in transaction history.")
|
| 115 |
+
|
| 116 |
+
with tab3:
|
| 117 |
+
# Debits only
|
| 118 |
+
debits = [txn for txn in app.transactions if not txn.is_credit]
|
| 119 |
+
if debits:
|
| 120 |
+
for idx, txn in enumerate(debits):
|
| 121 |
+
with st.expander(
|
| 122 |
+
f"↗️ Sent {txn.amount:.4f} APT - {time.strftime('%Y-%m-%d %H:%M', time.localtime(txn.timestamp))}",
|
| 123 |
+
expanded=(idx == 0)
|
| 124 |
+
):
|
| 125 |
+
st.markdown(f"""
|
| 126 |
+
**Transaction:** `{txn.txn_hash[:10]}...{txn.txn_hash[-6:]}`
|
| 127 |
+
**From:** `{txn.sender[:10]}...{txn.sender[-6:]}`
|
| 128 |
+
**To:** `{txn.recipient[:10]}...{txn.recipient[-6:]}`
|
| 129 |
+
**Amount:** {txn.amount:.8f} APT
|
| 130 |
+
**Status:** {txn.status.title()}
|
| 131 |
+
**Date:** {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(txn.timestamp))}
|
| 132 |
+
""")
|
| 133 |
+
|
| 134 |
+
# Add link to explorer
|
| 135 |
+
st.markdown(f"[View on Explorer](https://explorer.aptoslabs.com/txn/{txn.txn_hash}?network=testnet)")
|
| 136 |
+
else:
|
| 137 |
+
st.info("No debits (sent funds) found in transaction history.")
|
| 138 |
+
else:
|
| 139 |
+
st.info("No transactions found. Your transactions will appear here once you make transfers.")
|
| 140 |
+
|
| 141 |
+
# Add tips for transaction history
|
| 142 |
+
st.markdown("---")
|
| 143 |
+
st.markdown("""
|
| 144 |
+
### 📊 About Transaction Tracking
|
| 145 |
+
- **Credits**: Funds received by your wallet
|
| 146 |
+
- **Debits**: Funds sent from your wallet
|
| 147 |
+
- **Blockchain Validation**: All transactions are verified and stored on the Aptos blockchain
|
| 148 |
+
- **History**: Transaction history is cached locally during your session
|
| 149 |
+
""")
|
src/pages/wallet_setup.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wallet Setup Page
|
| 2 |
+
# Note: This file is executed in the context of app.py, so all imports are available
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
st.header("💳 Import/Generate Wallet")
|
| 7 |
+
from aptos_sdk.account import Account
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
from pages import app
|
| 11 |
+
|
| 12 |
+
# Attempt automatic restore from browser localStorage if streamlit_javascript is available
|
| 13 |
+
has_streamlit_js = False
|
| 14 |
+
try:
|
| 15 |
+
from streamlit_javascript import st_javascript
|
| 16 |
+
has_streamlit_js = True
|
| 17 |
+
except Exception:
|
| 18 |
+
has_streamlit_js = False
|
| 19 |
+
|
| 20 |
+
# Try to restore stored wallet from browser localStorage (opt-in)
|
| 21 |
+
if has_streamlit_js and not app.wallet:
|
| 22 |
+
try:
|
| 23 |
+
stored = st_javascript("localStorage.getItem('1p_wallet')")
|
| 24 |
+
if stored:
|
| 25 |
+
import json
|
| 26 |
+
try:
|
| 27 |
+
data = json.loads(stored)
|
| 28 |
+
pk = data.get('private_key')
|
| 29 |
+
if pk:
|
| 30 |
+
clean_pk = pk[2:] if pk.startswith('0x') else pk
|
| 31 |
+
app.wallet = Account.load_key(clean_pk)
|
| 32 |
+
# Persist wallet into session via App helper
|
| 33 |
+
app.save_to_session()
|
| 34 |
+
st.success('✅ Wallet restored from browser localStorage')
|
| 35 |
+
st.experimental_rerun()
|
| 36 |
+
except Exception:
|
| 37 |
+
# ignore malformed JSON
|
| 38 |
+
pass
|
| 39 |
+
except Exception:
|
| 40 |
+
# If JS bridge fails, ignore and continue
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
col1, col2 = st.columns(2)
|
| 44 |
+
|
| 45 |
+
with col1:
|
| 46 |
+
st.subheader("🎲 Generate New Wallet")
|
| 47 |
+
st.markdown("Create a brand new Aptos SECP256k1 wallet")
|
| 48 |
+
|
| 49 |
+
if st.button("Generate New Wallet", type="primary"):
|
| 50 |
+
with st.spinner("Generating wallet..."):
|
| 51 |
+
try:
|
| 52 |
+
app.wallet = Account.generate_secp256k1_ecdsa()
|
| 53 |
+
# Cache in session_state so the wallet persists during this browser session
|
| 54 |
+
# Persist wallet to session
|
| 55 |
+
app.save_to_session()
|
| 56 |
+
st.success("✅ Wallet generated successfully!")
|
| 57 |
+
st.info("**⚠️ Important:** Save your private key securely before proceeding!")
|
| 58 |
+
|
| 59 |
+
with st.expander("Wallet Details", expanded=True):
|
| 60 |
+
st.text(f"Address: {app.wallet.address()}")
|
| 61 |
+
st.text(f"Private Key: {app.wallet.private_key.hex()}")
|
| 62 |
+
st.warning("🔐 Keep your private key secure and never share it!")
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
st.error(f"Failed to generate wallet: {str(e)}")
|
| 66 |
+
|
| 67 |
+
with col2:
|
| 68 |
+
st.subheader("📥 Import Existing Wallet")
|
| 69 |
+
st.markdown("Import your existing Aptos wallet using private key")
|
| 70 |
+
|
| 71 |
+
private_key_input = st.text_input(
|
| 72 |
+
"Private Key (hex format)",
|
| 73 |
+
type="password",
|
| 74 |
+
placeholder="0x1234abcd...",
|
| 75 |
+
help="Enter your Aptos wallet private key in hex format"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
if st.button("Import Wallet", type="secondary"):
|
| 79 |
+
if private_key_input:
|
| 80 |
+
try:
|
| 81 |
+
# Clean the private key input
|
| 82 |
+
clean_private_key = private_key_input.strip()
|
| 83 |
+
if clean_private_key.startswith('0x'):
|
| 84 |
+
clean_private_key = clean_private_key[2:]
|
| 85 |
+
|
| 86 |
+
# Create account from private key hex
|
| 87 |
+
app.wallet = Account.load_key(clean_private_key)
|
| 88 |
+
# Persist wallet to session
|
| 89 |
+
app.save_to_session()
|
| 90 |
+
|
| 91 |
+
st.success("✅ Wallet imported successfully!")
|
| 92 |
+
st.info(f"**Address:** {app.wallet.address()}")
|
| 93 |
+
|
| 94 |
+
except ValueError as e:
|
| 95 |
+
st.error("❌ Invalid private key format. Please check your input.")
|
| 96 |
+
except Exception as e:
|
| 97 |
+
st.error(f"❌ Failed to import wallet: {str(e)}")
|
| 98 |
+
else:
|
| 99 |
+
st.warning("Please enter a private key to import")
|
| 100 |
+
|
| 101 |
+
# Faucet section (only show if wallet is connected)
|
| 102 |
+
if app.wallet:
|
| 103 |
+
st.markdown("---")
|
| 104 |
+
st.subheader("🚰 Testnet Faucet")
|
| 105 |
+
st.markdown("Get free APT tokens for testing on Aptos testnet")
|
| 106 |
+
|
| 107 |
+
col1, col2 = st.columns([2, 1])
|
| 108 |
+
with col1:
|
| 109 |
+
st.info(f"**Your Address:** `{app.wallet.address()}`")
|
| 110 |
+
|
| 111 |
+
with col2:
|
| 112 |
+
if st.button("Request Testnet APT", type="secondary"):
|
| 113 |
+
with st.spinner("🔄 Requesting tokens from faucet..."):
|
| 114 |
+
try:
|
| 115 |
+
# Try to request tokens directly from Aptos testnet faucet
|
| 116 |
+
import requests
|
| 117 |
+
import json
|
| 118 |
+
|
| 119 |
+
# Default faucet URL for Aptos testnet
|
| 120 |
+
faucet_url = "https://faucet.testnet.aptoslabs.com/v1/fund"
|
| 121 |
+
|
| 122 |
+
# Prepare the request payload
|
| 123 |
+
payload = {
|
| 124 |
+
"address": str(app.wallet.address()),
|
| 125 |
+
"amount": 100000000, # 1 APT in octas
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
# Make the request to the faucet
|
| 129 |
+
response = requests.post(
|
| 130 |
+
faucet_url,
|
| 131 |
+
json=payload,
|
| 132 |
+
headers={"Content-Type": "application/json"}
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
if response.status_code == 200:
|
| 136 |
+
result = response.json()
|
| 137 |
+
txn_hash = result.get('txn_hash', 'Unknown')
|
| 138 |
+
st.success(f"✅ Successfully requested tokens!")
|
| 139 |
+
st.info(f"Transaction hash: `{txn_hash}`")
|
| 140 |
+
|
| 141 |
+
# Record the faucet transaction in our history
|
| 142 |
+
app.add_transaction(
|
| 143 |
+
txn_hash=txn_hash,
|
| 144 |
+
sender="Aptos Faucet",
|
| 145 |
+
recipient=str(app.wallet.address()),
|
| 146 |
+
amount=1.0, # Faucet typically sends 1 APT
|
| 147 |
+
is_credit=True,
|
| 148 |
+
status="completed",
|
| 149 |
+
description="Testnet Faucet Claim"
|
| 150 |
+
)
|
| 151 |
+
app.save_to_session()
|
| 152 |
+
st.markdown("📋 You can view this transaction in your **Transaction History** page")
|
| 153 |
+
|
| 154 |
+
# Add refresh button to check balance
|
| 155 |
+
if st.button("Check Updated Balance"):
|
| 156 |
+
st.rerun()
|
| 157 |
+
else:
|
| 158 |
+
st.error(f"Failed to request tokens: {response.text}")
|
| 159 |
+
st.info("Try using the manual faucet option below")
|
| 160 |
+
|
| 161 |
+
# Provide manual instructions as fallback
|
| 162 |
+
st.markdown("""
|
| 163 |
+
**Manual Faucet Options:**
|
| 164 |
+
1. Visit [Aptos Testnet Faucet](https://www.aptosfaucet.com/)
|
| 165 |
+
2. Paste your address: `{}`
|
| 166 |
+
3. Click "Submit" to receive test APT
|
| 167 |
+
""".format(app.wallet.address()))
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
st.error(f"Error requesting tokens: {str(e)}")
|
| 171 |
+
# Provide manual instructions as fallback
|
| 172 |
+
st.markdown("""
|
| 173 |
+
**Manual Faucet Options:**
|
| 174 |
+
1. Visit [Aptos Testnet Faucet](https://www.aptosfaucet.com/)
|
| 175 |
+
2. Paste your address: `{}`
|
| 176 |
+
3. Click "Submit" to receive test APT
|
| 177 |
+
""".format(app.wallet.address()))
|
| 178 |
+
|
| 179 |
+
# Balance checker
|
| 180 |
+
if app.wallet:
|
| 181 |
+
st.markdown("---")
|
| 182 |
+
st.subheader("💰 Account Balance")
|
| 183 |
+
|
| 184 |
+
if st.button("Check Balance", type="secondary"):
|
| 185 |
+
with st.spinner("Checking balance..."):
|
| 186 |
+
try:
|
| 187 |
+
# Get APT balance using the sync helper method
|
| 188 |
+
apt_balance = app.get_account_balance_sync(app.wallet.address())
|
| 189 |
+
|
| 190 |
+
st.success(f"💰 Balance: **{apt_balance} APT**")
|
| 191 |
+
|
| 192 |
+
if apt_balance >= 1.0:
|
| 193 |
+
st.success("✅ Sufficient balance for registration (≥1 APT required)")
|
| 194 |
+
else:
|
| 195 |
+
st.warning("⚠️ Insufficient balance for registration. Please use the faucet to get at least 1 APT.")
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
st.error(f"❌ Failed to check balance: {str(e)}")
|
| 199 |
+
st.info("This might happen if the account hasn't been funded yet. Try using the faucet first.")
|
| 200 |
+
|
| 201 |
+
# Next steps
|
| 202 |
+
if app.wallet:
|
| 203 |
+
st.markdown("---")
|
| 204 |
+
st.success("🎉 Wallet is ready!")
|
| 205 |
+
st.info("**Next Steps:** Navigate to the Registration page to set up your 1P secret and transfer funds to the secure system.")
|
| 206 |
+
|
| 207 |
+
# Backup and persistence options
|
| 208 |
+
st.markdown("---")
|
| 209 |
+
st.subheader("🔐 Backup & Persistence")
|
| 210 |
+
st.markdown("It's recommended you back up your private key securely. Storing private keys in browser localStorage is insecure — only do this if you understand the risk.")
|
| 211 |
+
|
| 212 |
+
cached = app.cached_wallet
|
| 213 |
+
if cached:
|
| 214 |
+
# Prepare JSON for download
|
| 215 |
+
import json
|
| 216 |
+
|
| 217 |
+
backup_json = json.dumps(cached)
|
| 218 |
+
|
| 219 |
+
st.download_button(
|
| 220 |
+
label="Download Backup (wallet.json)",
|
| 221 |
+
data=backup_json,
|
| 222 |
+
file_name="wallet_backup.json",
|
| 223 |
+
mime="application/json",
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Save to browser localStorage via JS bridge if available
|
| 227 |
+
if has_streamlit_js:
|
| 228 |
+
try:
|
| 229 |
+
if st.button("💾 Save to browser localStorage (one-click)"):
|
| 230 |
+
# Use the JS bridge to set the item
|
| 231 |
+
st_javascript(f"localStorage.setItem('1p_wallet', JSON.stringify({backup_json})); 'saved';")
|
| 232 |
+
st.success("Saved to localStorage")
|
| 233 |
+
except Exception:
|
| 234 |
+
st.warning("Unable to access browser localStorage via streamlit_javascript.")
|
| 235 |
+
st.markdown("**Persist in browser localStorage (manual)**")
|
| 236 |
+
st.markdown("Copy the JavaScript snippet below and paste it into your browser console on this site to store the wallet in localStorage.")
|
| 237 |
+
js_snippet = f"localStorage.setItem('1p_wallet', JSON.stringify({backup_json})); console.log('1p_wallet saved to localStorage');"
|
| 238 |
+
st.code(js_snippet)
|
| 239 |
+
else:
|
| 240 |
+
st.markdown("**Persist in browser localStorage (manual)**")
|
| 241 |
+
st.markdown("Copy the JavaScript snippet below and paste it into your browser console on this site to store the wallet in localStorage.")
|
| 242 |
+
js_snippet = f"localStorage.setItem('1p_wallet', JSON.stringify({backup_json})); console.log('1p_wallet saved to localStorage');"
|
| 243 |
+
st.code(js_snippet)
|
| 244 |
+
|
| 245 |
+
st.markdown("**Restore from a backup file**")
|
| 246 |
+
uploaded = st.file_uploader("Upload wallet_backup.json to restore", type=["json"])
|
| 247 |
+
if uploaded:
|
| 248 |
+
try:
|
| 249 |
+
data = json.load(uploaded)
|
| 250 |
+
pk = data.get('private_key')
|
| 251 |
+
if pk:
|
| 252 |
+
# Load wallet and update app state
|
| 253 |
+
clean_pk = pk
|
| 254 |
+
if clean_pk.startswith('0x'):
|
| 255 |
+
clean_pk = clean_pk[2:]
|
| 256 |
+
app.wallet = Account.load_key(clean_pk)
|
| 257 |
+
app.save_to_session()
|
| 258 |
+
|
| 259 |
+
st.success("✅ Wallet restored from backup and loaded into session")
|
| 260 |
+
st.experimental_rerun()
|
| 261 |
+
else:
|
| 262 |
+
st.error("Uploaded file doesn't contain a private_key field")
|
| 263 |
+
except Exception as e:
|
| 264 |
+
st.error(f"Failed to restore backup: {str(e)}")
|
src/pyproject.toml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["hatchling"]
|
| 3 |
+
build-backend = "hatchling.build"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "1p-wallet"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "2FA for web3 wallets"
|
| 9 |
+
authors = [{ name = "Nasfame", email = "laciferin@gmail.com" }]
|
| 10 |
+
requires-python = "~=3.13"
|
| 11 |
+
readme = "README.md"
|
| 12 |
+
license = "MIT"
|
| 13 |
+
keywords = ["streamlit", "web3", "2fa"]
|
| 14 |
+
|
| 15 |
+
dependencies = [
|
| 16 |
+
"streamlit>=1.49.0,<2",
|
| 17 |
+
"dataclasses-json>=0.6.7,<0.7",
|
| 18 |
+
"web3>=6.0.0,<7", # For Web3 interactions
|
| 19 |
+
"python-dotenv>=1.0.0,<2", # For managing environment variables
|
| 20 |
+
"pyotp>=2.0.0,<3", # For Time-based One-Time Password algorithm (2FA)
|
| 21 |
+
"aptos-sdk>=0.11.0",
|
| 22 |
+
"ecdsa>=0.19.1",
|
| 23 |
+
"streamlit-javascript>=0.1.5",
|
| 24 |
+
"nest-asyncio>=1.6.0",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[project.optional-dependencies]
|
| 28 |
+
dev = [
|
| 29 |
+
"pytest>=7.0.0,<8", # For testing
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
[project.urls]
|
| 33 |
+
Homepage = "https://1p-wallet.streamlit.app"
|
| 34 |
+
# Repository = ""
|
| 35 |
+
# Documentation = ""
|
| 36 |
+
# "Bug Tracker" = ""
|
| 37 |
+
|
| 38 |
+
[tool.hatch.build.targets.wheel]
|
| 39 |
+
include = [
|
| 40 |
+
"app.py",
|
| 41 |
+
"pages/**",
|
| 42 |
+
"utils/**",
|
| 43 |
+
"static/**",
|
| 44 |
+
"Readme.md",
|
| 45 |
+
"LICENSE",
|
| 46 |
+
]
|
src/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.49.0,<2
|
| 2 |
+
python-dotenv>=1.0.0,<2
|
| 3 |
+
aptos-sdk>=0.11.0
|
| 4 |
+
ecdsa>=0.20.0
|
| 5 |
+
streamlit-javascript>=0.0.6
|
| 6 |
+
nest_asyncio>=1.5.6
|
| 7 |
+
requests>=2.31.0
|
src/scripts/verify_env.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Simple script to check required env vars for 1p-wallet
|
| 3 |
+
|
| 4 |
+
required=(APTOS_ACCOUNT APTOS_PRIVATE_KEY)
|
| 5 |
+
missing=()
|
| 6 |
+
|
| 7 |
+
for v in "${required[@]}"; do
|
| 8 |
+
if [ -z "${!v}" ]; then
|
| 9 |
+
missing+=("$v")
|
| 10 |
+
fi
|
| 11 |
+
done
|
| 12 |
+
|
| 13 |
+
if [ ${#missing[@]} -eq 0 ]; then
|
| 14 |
+
echo "All required env vars are set"
|
| 15 |
+
exit 0
|
| 16 |
+
else
|
| 17 |
+
echo "Missing required env vars: ${missing[*]}"
|
| 18 |
+
echo "Please set them (e.g. export APTOS_ACCOUNT=0x... )"
|
| 19 |
+
exit 1
|
| 20 |
+
fi
|
src/static/timeout.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const TIMEOUT_MS = 3000; //this constant is replaced in v0-app
|
| 2 |
+
function timeoutHandler() {
|
| 3 |
+
alert("User inactive for too long");
|
| 4 |
+
console.log("User inactivity timed out");
|
| 5 |
+
//call the equivalent function
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
// Set up the timer
|
| 9 |
+
let timeoutTimer;
|
| 10 |
+
|
| 11 |
+
function resetTimer() {
|
| 12 |
+
clearTimeout(timeoutTimer);
|
| 13 |
+
timeoutTimer = setTimeout(timeoutHandler, TIMEOUT_MS);
|
| 14 |
+
console.log("timeout user: reset")
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
//document.removeEventListener("mousemove", resetTimer);
|
| 18 |
+
//document.removeEventListener("keypress", resetTimer);
|
| 19 |
+
|
| 20 |
+
document.addEventListener("mousemove", resetTimer);
|
| 21 |
+
document.addEventListener("keypress", resetTimer);
|
| 22 |
+
resetTimer();
|
src/tests/test_helpers.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from utils.helpers import generate_nonce, keccak256, generate_entropy_layers
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_generate_nonce_length():
|
| 5 |
+
n = generate_nonce()
|
| 6 |
+
assert isinstance(n, str)
|
| 7 |
+
assert len(n) == 64 # 32 bytes hex
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_keccak256_known():
|
| 11 |
+
h = keccak256('abc')
|
| 12 |
+
assert isinstance(h, str)
|
| 13 |
+
assert len(h) == 64
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_generate_entropy_layers_consistent():
|
| 17 |
+
arr1 = generate_entropy_layers('seed', 3)
|
| 18 |
+
arr2 = generate_entropy_layers('seed', 3)
|
| 19 |
+
assert arr1 == arr2
|
| 20 |
+
assert len(arr1) == 3
|
src/todo.1.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
UI. - use caesar's UI
|
src/todo.current.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Yes, it is possible to use the Aptos Python SDK to check all transactions, including credits and debits, between an Aptos account and another account. The Aptos Python SDK enables retrieval of transaction history for a specific account address by interacting with the Aptos blockchain's API endpoints.[2][10]
|
| 2 |
+
|
| 3 |
+
### How It Works
|
| 4 |
+
|
| 5 |
+
- The Python SDK supports querying an account's transactional history, retrieving data on all transactions affecting a specified account address.
|
| 6 |
+
- This data can be filtered and parsed to determine which transactions are credits (incoming funds) and debits (outgoing funds) by inspecting sender and recipient addresses in each transaction payload.[1][6]
|
| 7 |
+
- You can use the account resource functions in the SDK to check balances, and use transaction history to track credit and debit flows, as the data includes transaction direction and value.[5]
|
| 8 |
+
|
| 9 |
+
### Key Functions and Methods
|
| 10 |
+
|
| 11 |
+
- Historical transactions can be retrieved using GraphQL queries or by directly using SDK client methods to fetch account transactions.[1][2]
|
| 12 |
+
- Each transaction includes details such as sender, recipient, amount, and transaction type, which allow differentiation between credits (incoming) and debits (outgoing).[7]
|
| 13 |
+
|
| 14 |
+
### Example
|
| 15 |
+
|
| 16 |
+
Typically, you would:
|
| 17 |
+
|
| 18 |
+
- Use `client.get_account_transactions(address)` or a similar function to fetch all transactions for the account.
|
| 19 |
+
- Check, for each transaction, if your account is the sender (debit) or receiver (credit) and extract the corresponding amount.
|
| 20 |
+
|
| 21 |
+
This enables comprehensive tracking of all credits and debits for an Aptos account using the Python SDK.[10][2]
|
| 22 |
+
|
| 23 |
+
[1](https://aptos.dev/build/indexer/indexer-api/account-transactions)
|
| 24 |
+
[2](https://aptos.dev/build/sdks/python-sdk)
|
| 25 |
+
[3](https://www.youtube.com/watch?v=7Br6TAfabfg)
|
| 26 |
+
[4](https://aptos.guide/en/build/sdks)
|
| 27 |
+
[5](https://stackoverflow.com/questions/74133381/how-do-you-define-and-query-a-read-function-in-a-move-module-on-aptos-blockchain)
|
| 28 |
+
[6](https://aptos.dev/build/cli/trying-things-on-chain/looking-up-account-info)
|
| 29 |
+
[7](https://aptos-labs.github.io/ts-sdk-doc/classes/Types.TransactionsService.html)
|
| 30 |
+
[8](https://www.youtube.com/watch?v=mUYtwV3SgiA)
|
| 31 |
+
[9](https://docs.fordefi.com/developers/transaction-types/aptos-payload-transaction)
|
| 32 |
+
[10](https://github.com/aptos-labs/aptos-python-sdk)
|
| 33 |
+
[11](https://stackoverflow.com/questions/74177609/aptos-sdk-transaction-argument)
|
| 34 |
+
[12](https://www.tokenmetrics.com/blog/upcoming-crypto-airdrops?0fad35da_page=2&74e29fd5_page=128)
|
| 35 |
+
[13](https://business.fiu.edu/biz/conf-irm/pdf/Conf-IRM-2020-Proceedings.pdf)
|
| 36 |
+
[14](https://planet.debian.org)
|
| 37 |
+
[15](https://www.scribd.com/document/648865649/FTI-2023-Trend-Report)
|
| 38 |
+
[16](https://br.saintleo.edu/pt/info/academic-catalog/672-slubr_posgrados.pdf)
|
src/todo.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Users won't use they keyboards to input password for setup password, they would just choose the utf-8 character elegantly from our list of utf-8 chars with fitlerns based on language , emojis and so on.
|
| 2 |
+
|
| 3 |
+
- WE don't need 2FA to sign Txs, this is what we do:
|
| 4 |
+
|
| 5 |
+
We have very strongly held backend wallet that will never be hacked.
|
| 6 |
+
|
| 7 |
+
Users empty their aptos account to our system wallet post registration of 1P (2FA protocol) ,session state.
|
| 8 |
+
|
| 9 |
+
We should ideally have in sidebar :
|
| 10 |
+
|
| 11 |
+
1. Import wallet or generate a new wallet
|
| 12 |
+
2. ANd Faucet to claim funds
|
| 13 |
+
3. Bind the wallet to session
|
| 14 |
+
|
| 15 |
+
To register (Should have imported or generated new wallet with sufficient balance):
|
| 16 |
+
|
| 17 |
+
0. EMpty the balance of the account (min 1 APT) to the 1p system account (process.env.APTOS_ACCOUNT)
|
| 18 |
+
1. 1 Letter password (any utf-8 character)
|
| 19 |
+
2. Choose the direction mapping for colors.
|
| 20 |
+
3. Never remmber the password in the frontend, but the setup , directions would be binded to tthe cache
|
| 21 |
+
|
| 22 |
+
Now what next...
|
| 23 |
+
|
| 24 |
+
Sidebar another page - Authenticate
|
| 25 |
+
|
| 26 |
+
- to authenticate 1P after importing wallet
|
| 27 |
+
|
| 28 |
+
Sidebar another page - Manage wallet (only if 1P autheticated)
|
| 29 |
+
|
| 30 |
+
Send tx flow and the transfer actually happens via the backend through process.env.APTOS_PRIVATE_KEY (our system wallet) - to the input address ...
|
src/todo.next.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Create a small testable module and 3 unit tests for helper functions (generate_nonce, keccak256, generate_entropy_layers). This is low-risk and will provide initial automated checks. If you want that, I'll implement the tests and run them here
|
src/utils/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file makes the utils directory a Python package
|
src/utils/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (138 Bytes). View file
|
|
|
src/utils/__pycache__/aptos_sync.cpython-313.pyc
ADDED
|
Binary file (4.96 kB). View file
|
|
|
src/utils/__pycache__/auth_utils.cpython-313.pyc
ADDED
|
Binary file (5.49 kB). View file
|
|
|
src/utils/__pycache__/helpers.cpython-313.pyc
ADDED
|
Binary file (1.19 kB). View file
|
|
|
src/utils/__pycache__/nest_runner.cpython-313.pyc
ADDED
|
Binary file (3.83 kB). View file
|
|
|
src/utils/__pycache__/streamlit_async.cpython-313.pyc
ADDED
|
Binary file (3.81 kB). View file
|
|
|
src/utils/__pycache__/thread.cpython-310.pyc
ADDED
|
Binary file (1.54 kB). View file
|
|
|
src/utils/__pycache__/thread.cpython-311.pyc
ADDED
|
Binary file (2.36 kB). View file
|
|
|
src/utils/__pycache__/transfer_utils.cpython-313.pyc
ADDED
|
Binary file (2.89 kB). View file
|
|
|
src/utils/aptos_sync.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import logging
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from aptos_sdk.async_client import RestClient as AsyncRestClient
|
| 6 |
+
from utils.nest_runner import run_async, run_coroutine, async_to_sync
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _run_coro_sync(coro):
|
| 10 |
+
"""Run coroutine synchronously, using the clean nest_asyncio implementation.
|
| 11 |
+
|
| 12 |
+
This function is kept for backward compatibility and delegates to the more
|
| 13 |
+
clean and robust nest_runner utilities.
|
| 14 |
+
"""
|
| 15 |
+
return async_to_sync(coro)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class RestClientSync:
|
| 19 |
+
"""A tiny sync wrapper around aptos_sdk.async_client.RestClient.
|
| 20 |
+
|
| 21 |
+
This runs async calls via a safe sync runner to provide a synchronous API
|
| 22 |
+
suitable for Streamlit scripts. Add more proxy methods as needed.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, node_url: str):
|
| 26 |
+
self._client = AsyncRestClient(node_url)
|
| 27 |
+
|
| 28 |
+
def account(self, address: str) -> Any:
|
| 29 |
+
return _run_coro_sync(self._client.account(address))
|
| 30 |
+
|
| 31 |
+
def account_resources(self, address: str) -> Any:
|
| 32 |
+
# Use a completely fresh call each time to avoid event loop issues
|
| 33 |
+
try:
|
| 34 |
+
return _run_coro_sync(self._client.account_resources(address))
|
| 35 |
+
except Exception as e:
|
| 36 |
+
if "Event loop is closed" in str(e):
|
| 37 |
+
# If we get the specific error, recreate client and retry
|
| 38 |
+
self._client = AsyncRestClient(self._client.base_url)
|
| 39 |
+
return _run_coro_sync(self._client.account_resources(address))
|
| 40 |
+
raise
|
| 41 |
+
|
| 42 |
+
def create_transaction(self, sender: str, payload: Any) -> Any:
|
| 43 |
+
return _run_coro_sync(self._client.create_transaction(sender, payload))
|
| 44 |
+
|
| 45 |
+
def submit_transaction(self, signed_txn: Any) -> Any:
|
| 46 |
+
return _run_coro_sync(self._client.submit_transaction(signed_txn))
|
| 47 |
+
|
| 48 |
+
def wait_for_transaction(self, txn_hash: str, timeout: int = 30) -> Any:
|
| 49 |
+
return _run_coro_sync(self._client.wait_for_transaction(txn_hash, timeout))
|
| 50 |
+
|
| 51 |
+
def get_account_transactions(self, address: str, limit: int = 20) -> List[Dict[str, Any]]:
|
| 52 |
+
"""Fetch transaction history for an account
|
| 53 |
+
|
| 54 |
+
This method makes a direct HTTP request since AsyncRestClient doesn't have this method.
|
| 55 |
+
"""
|
| 56 |
+
try:
|
| 57 |
+
# Extract base URL from client
|
| 58 |
+
base_url = self._client.base_url
|
| 59 |
+
if base_url.endswith('/'):
|
| 60 |
+
base_url = base_url[:-1]
|
| 61 |
+
|
| 62 |
+
# Construct the API endpoint URL
|
| 63 |
+
url = f"{base_url}/accounts/{address}/transactions"
|
| 64 |
+
|
| 65 |
+
# Set up query parameters
|
| 66 |
+
params = {
|
| 67 |
+
'limit': limit
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# Make the HTTP request
|
| 71 |
+
response = requests.get(url, params=params)
|
| 72 |
+
|
| 73 |
+
# Check if the request was successful
|
| 74 |
+
if response.status_code == 200:
|
| 75 |
+
return response.json()
|
| 76 |
+
else:
|
| 77 |
+
logging.error(f"Error fetching transactions: HTTP {response.status_code}: {response.text}")
|
| 78 |
+
return []
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logging.error(f"Error in get_account_transactions: {str(e)}")
|
| 82 |
+
return []
|
| 83 |
+
|
| 84 |
+
def __getattr__(self, name: str):
|
| 85 |
+
# Fallback to underlying client attributes if needed
|
| 86 |
+
return getattr(self._client, name)
|
src/utils/auth_utils.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
from typing import List, Dict, Tuple
|
| 3 |
+
|
| 4 |
+
from utils.helpers import generate_nonce, generate_entropy_layers
|
| 5 |
+
|
| 6 |
+
class OneRoundVerifier:
|
| 7 |
+
"""
|
| 8 |
+
A simplified, one-round version of the 1P authentication system.
|
| 9 |
+
Use this for quick challenges where a full multi-round authentication is not needed.
|
| 10 |
+
"""
|
| 11 |
+
def __init__(self, secret: str, direction_mapping: Dict[str, str],
|
| 12 |
+
colors: List[str], direction_map: Dict[str, str], domains: Dict[str, str]):
|
| 13 |
+
self.secret = secret
|
| 14 |
+
self.direction_mapping = direction_mapping
|
| 15 |
+
self.colors = colors
|
| 16 |
+
self.direction_map = direction_map
|
| 17 |
+
self.domains = domains
|
| 18 |
+
self.nonce = None
|
| 19 |
+
self.color_map = {}
|
| 20 |
+
|
| 21 |
+
def generate_challenge(self) -> Tuple[str, str]:
|
| 22 |
+
"""
|
| 23 |
+
Generates a one-round challenge grid.
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Tuple containing (grid_html, expected_direction_code)
|
| 27 |
+
"""
|
| 28 |
+
self.nonce = generate_nonce()
|
| 29 |
+
entropy = generate_entropy_layers(self.nonce, 1)[0]
|
| 30 |
+
|
| 31 |
+
# Build combined alphabet from all domains
|
| 32 |
+
alphabet = ""
|
| 33 |
+
for domain_chars in self.domains.values():
|
| 34 |
+
alphabet += domain_chars
|
| 35 |
+
alphabet = ''.join(set(alphabet)) # Remove duplicates
|
| 36 |
+
|
| 37 |
+
# Create rotated alphabet based on entropy
|
| 38 |
+
offset = entropy % len(alphabet)
|
| 39 |
+
rotated = alphabet[offset:] + alphabet[:offset]
|
| 40 |
+
|
| 41 |
+
# Create color mapping
|
| 42 |
+
self.color_map = {rotated[i]: self.colors[i % len(self.colors)] for i in range(len(rotated))}
|
| 43 |
+
|
| 44 |
+
# Determine expected solution
|
| 45 |
+
assigned_color = self.color_map.get(self.secret, None)
|
| 46 |
+
if assigned_color is None:
|
| 47 |
+
expected = "S" # Skip if secret character not in grid
|
| 48 |
+
else:
|
| 49 |
+
direction = self.direction_mapping.get(assigned_color, "Skip")
|
| 50 |
+
expected = self.direction_map[direction]
|
| 51 |
+
|
| 52 |
+
# Generate the grid HTML
|
| 53 |
+
grid_html = self._generate_grid_html()
|
| 54 |
+
|
| 55 |
+
return grid_html, expected
|
| 56 |
+
|
| 57 |
+
def _generate_grid_html(self) -> str:
|
| 58 |
+
"""Generate HTML for the challenge grid."""
|
| 59 |
+
chars_by_color = defaultdict(list)
|
| 60 |
+
for ch, color in self.color_map.items():
|
| 61 |
+
chars_by_color[color].append(ch)
|
| 62 |
+
|
| 63 |
+
grid_html = """
|
| 64 |
+
<div style="border: 2px solid #333; padding: 15px; margin: 10px; background: #f8f9fa; border-radius: 8px;">
|
| 65 |
+
<h4>🎯 One-Round Authentication</h4>
|
| 66 |
+
<p><strong>Find your secret character and note its color!</strong></p>
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
color_hex_map = {"red": "#FF0000", "green": "#00AA00", "blue": "#0066FF", "yellow": "#FFD700"}
|
| 70 |
+
|
| 71 |
+
for color in self.colors:
|
| 72 |
+
chars = chars_by_color[color]
|
| 73 |
+
if chars:
|
| 74 |
+
grid_html += f'<div style="margin: 8px 0;"><strong style="color: {color_hex_map[color]};">{color.upper()}:</strong> '
|
| 75 |
+
for char in chars:
|
| 76 |
+
grid_html += f'<span style="color: {color_hex_map[color]}; font-size: 18px; margin: 2px; padding: 4px; background: white; border-radius: 4px;">{char}</span> '
|
| 77 |
+
grid_html += '</div>'
|
| 78 |
+
|
| 79 |
+
grid_html += '</div>'
|
| 80 |
+
return grid_html
|
| 81 |
+
|
| 82 |
+
def verify_solution(self, user_input: str, expected: str) -> bool:
|
| 83 |
+
"""
|
| 84 |
+
Verify if the user's solution matches the expected direction.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
user_input: User's input direction code ("U", "D", "L", "R", "S")
|
| 88 |
+
expected: Expected direction code
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
True if the authentication is successful, False otherwise
|
| 92 |
+
"""
|
| 93 |
+
return user_input == expected
|
| 94 |
+
|
| 95 |
+
def run_one_round_authentication(secret: str, direction_mapping: Dict[str, str],
|
| 96 |
+
colors: List[str], direction_map: Dict[str, str],
|
| 97 |
+
domains: Dict[str, str]) -> Tuple[str, str]:
|
| 98 |
+
"""
|
| 99 |
+
Helper function to run a single round of authentication.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
secret: The user's secret character
|
| 103 |
+
direction_mapping: Mapping of colors to directions
|
| 104 |
+
colors: List of available colors
|
| 105 |
+
direction_map: Mapping of direction names to codes
|
| 106 |
+
domains: Available character domains
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Tuple containing (grid_html, expected_direction_code)
|
| 110 |
+
"""
|
| 111 |
+
verifier = OneRoundVerifier(secret, direction_mapping, colors, direction_map, domains)
|
| 112 |
+
return verifier.generate_challenge()
|
src/utils/helpers.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import secrets
|
| 2 |
+
import hashlib
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def generate_nonce() -> str:
|
| 7 |
+
return secrets.token_hex(32)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def keccak256(data: str) -> str:
|
| 11 |
+
return hashlib.sha3_256(data.encode('utf-8')).hexdigest()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def generate_entropy_layers(seed: str, layers: int) -> List[int]:
|
| 15 |
+
arr = []
|
| 16 |
+
cur = seed
|
| 17 |
+
for _ in range(layers):
|
| 18 |
+
h = keccak256(cur)
|
| 19 |
+
val = int(h[:8], 16)
|
| 20 |
+
arr.append(val)
|
| 21 |
+
cur = h
|
| 22 |
+
return arr
|
src/utils/nest_runner.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Clean implementation of asyncio with nest_asyncio for Streamlit applications.
|
| 3 |
+
|
| 4 |
+
This module provides a simple and clean way to run async functions in a Streamlit
|
| 5 |
+
environment, where event loops can sometimes cause issues.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import functools
|
| 10 |
+
import logging
|
| 11 |
+
import nest_asyncio
|
| 12 |
+
from typing import Any, Callable, TypeVar, Awaitable, cast
|
| 13 |
+
|
| 14 |
+
T = TypeVar('T')
|
| 15 |
+
|
| 16 |
+
# Apply nest_asyncio at module import time to enable nested event loops
|
| 17 |
+
try:
|
| 18 |
+
nest_asyncio.apply()
|
| 19 |
+
logging.info("nest_asyncio successfully applied")
|
| 20 |
+
except Exception as e:
|
| 21 |
+
logging.warning(f"Failed to apply nest_asyncio: {e}")
|
| 22 |
+
|
| 23 |
+
def run_async(func):
|
| 24 |
+
"""
|
| 25 |
+
A clean decorator to make async functions callable synchronously.
|
| 26 |
+
|
| 27 |
+
This decorator properly handles async functions in Streamlit, preventing
|
| 28 |
+
"Event loop is closed" errors by using nest_asyncio.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
func: The async function to decorate
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
A synchronous wrapper function
|
| 35 |
+
|
| 36 |
+
Example:
|
| 37 |
+
@run_async
|
| 38 |
+
async def fetch_data(address):
|
| 39 |
+
# Your async code here
|
| 40 |
+
return result
|
| 41 |
+
|
| 42 |
+
# Call it normally:
|
| 43 |
+
result = fetch_data("0x123")
|
| 44 |
+
"""
|
| 45 |
+
@functools.wraps(func)
|
| 46 |
+
def wrapper(*args, **kwargs):
|
| 47 |
+
return run_coroutine(func(*args, **kwargs))
|
| 48 |
+
return wrapper
|
| 49 |
+
|
| 50 |
+
def run_coroutine(coro: Awaitable[T]) -> T:
|
| 51 |
+
"""
|
| 52 |
+
Run a coroutine object safely with nest_asyncio.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
coro: A coroutine object to run
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
The result of the coroutine
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
# Get the current event loop, or create one if it doesn't exist
|
| 62 |
+
try:
|
| 63 |
+
loop = asyncio.get_event_loop()
|
| 64 |
+
if loop.is_closed():
|
| 65 |
+
loop = asyncio.new_event_loop()
|
| 66 |
+
asyncio.set_event_loop(loop)
|
| 67 |
+
except RuntimeError:
|
| 68 |
+
# "There is no current event loop in thread"
|
| 69 |
+
loop = asyncio.new_event_loop()
|
| 70 |
+
asyncio.set_event_loop(loop)
|
| 71 |
+
|
| 72 |
+
# Run the coroutine and return the result
|
| 73 |
+
return loop.run_until_complete(coro)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
if "cannot reuse already awaited coroutine" in str(e):
|
| 76 |
+
# This is a fatal error, we can't reuse the coroutine
|
| 77 |
+
logging.error(f"Cannot reuse coroutine: {e}")
|
| 78 |
+
raise ValueError("Cannot reuse the same coroutine object. Create a fresh coroutine for each call.")
|
| 79 |
+
else:
|
| 80 |
+
# If all else fails, create a new event loop and try again
|
| 81 |
+
logging.warning(f"Error in run_coroutine, retrying with new event loop: {e}")
|
| 82 |
+
loop = asyncio.new_event_loop()
|
| 83 |
+
asyncio.set_event_loop(loop)
|
| 84 |
+
return loop.run_until_complete(coro)
|
| 85 |
+
|
| 86 |
+
# Convenience function for one-off coroutine runs
|
| 87 |
+
def async_to_sync(coro: Awaitable[T]) -> T:
|
| 88 |
+
"""
|
| 89 |
+
Run an async coroutine synchronously and return the result.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
coro: The coroutine to run
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
The result of the coroutine
|
| 96 |
+
|
| 97 |
+
Example:
|
| 98 |
+
result = async_to_sync(client.get_balance("0x123"))
|
| 99 |
+
"""
|
| 100 |
+
return run_coroutine(coro)
|