pythoneerHiro commited on
Commit
4585d4c
·
verified ·
1 Parent(s): 2b67412

Upload 54 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. src/.DS_Store +0 -0
  2. src/.claude/settings.local.json +23 -0
  3. src/.gitignore +77 -0
  4. src/.python-version +1 -0
  5. src/.run/run.run.xml +29 -0
  6. src/.streamlit/config.toml +40 -0
  7. src/Dockerfile +27 -0
  8. src/LICENSE +21 -0
  9. src/Makefile +5 -0
  10. src/Readme.md +40 -0
  11. src/aidocs/1p-draft.pdf +0 -0
  12. src/app.py +538 -0
  13. src/components/__init__.py +1 -0
  14. src/components/__pycache__/__init__.cpython-313.pyc +0 -0
  15. src/components/__pycache__/auth_component.cpython-313.pyc +0 -0
  16. src/components/auth_component.py +121 -0
  17. src/justfile +17 -0
  18. src/local.justfile +0 -0
  19. src/pages/.DS_Store +0 -0
  20. src/pages/__init__.py +24 -0
  21. src/pages/account.py +31 -0
  22. src/pages/authentication.py +244 -0
  23. src/pages/manage_wallet.py +231 -0
  24. src/pages/registration.py +433 -0
  25. src/pages/transaction_history.py +149 -0
  26. src/pages/wallet_setup.py +264 -0
  27. src/pyproject.toml +46 -0
  28. src/requirements.txt +7 -0
  29. src/scripts/verify_env.sh +20 -0
  30. src/static/timeout.js +22 -0
  31. src/tests/test_helpers.py +20 -0
  32. src/todo.1.md +1 -0
  33. src/todo.current.md +38 -0
  34. src/todo.md +30 -0
  35. src/todo.next.md +1 -0
  36. src/utils/.DS_Store +0 -0
  37. src/utils/__init__.py +1 -0
  38. src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  39. src/utils/__pycache__/aptos_sync.cpython-313.pyc +0 -0
  40. src/utils/__pycache__/auth_utils.cpython-313.pyc +0 -0
  41. src/utils/__pycache__/helpers.cpython-313.pyc +0 -0
  42. src/utils/__pycache__/nest_runner.cpython-313.pyc +0 -0
  43. src/utils/__pycache__/streamlit_async.cpython-313.pyc +0 -0
  44. src/utils/__pycache__/thread.cpython-310.pyc +0 -0
  45. src/utils/__pycache__/thread.cpython-311.pyc +0 -0
  46. src/utils/__pycache__/transfer_utils.cpython-313.pyc +0 -0
  47. src/utils/aptos_sync.py +86 -0
  48. src/utils/auth_utils.py +112 -0
  49. src/utils/helpers.py +22 -0
  50. 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)