├── .ci └── move_binary.py ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── Logo ├── horizontal.png ├── logomark.png └── vertical.png ├── README.md ├── action.yml ├── buildozer.spec ├── entrypoint.py ├── icons ├── black.png ├── brain.png ├── brainbow.png └── white.png ├── install.md ├── new_requirements.txt ├── pyproject.toml ├── requirements-dev.txt ├── requirements-kivy.txt ├── requirements.txt ├── screens ├── Screenshot_balance.png ├── Screenshot_login.png ├── Screenshot_recieve.png └── Screenshot_send.png └── src ├── aiosocks ├── README.rst ├── __init__.py ├── connector.py ├── constants.py ├── errors.py ├── helpers.py ├── protocols.py └── test_utils.py ├── android_utils.py ├── app.py ├── assets ├── RobotoMono-Regular.ttf ├── Use BIP39 instead.png ├── btc-own-node.png ├── dark-btc-own-node.png ├── dark-larry_offline.png ├── dark-offline.png ├── dark-onboarding_00.png ├── dark-onboarding_10.png ├── dark-onboarding_20.png ├── dark-onboarding_unlimited_wallets.png ├── dark-online.png ├── dark-playground.png ├── dark-strong_passphrase.png ├── dark-use-bip39.png ├── dark-what-salt-is.png ├── dark-when-you-go.png ├── larry_offline.png ├── offline.png ├── onboarding_00.png ├── onboarding_10.png ├── onboarding_20.png ├── onboarding_unlimited_wallets.png ├── online.png ├── playground.png ├── strong_passphrase.png ├── what-salt-is.png └── when-you-go.png ├── bip49.py ├── bottom_screens_address.py ├── bottom_screens_tx.py ├── brain.png ├── brain_icon.png ├── brain_nav.png ├── brainbow.kv ├── camerax_provider ├── camerax_src │ └── org │ │ └── kivy │ │ └── camerax │ │ ├── CallbackWrapper.java │ │ ├── CameraX.java │ │ ├── ImageAnalysisAnalyzer.java │ │ ├── ImageProxyOps.java │ │ ├── ImageSavedCallback.java │ │ ├── KivySurfaceProvider.java │ │ └── VideoSavedCallback.java └── gradle_options.py ├── connection.py ├── connectrum ├── __init__.py ├── client.py ├── constants.py ├── exc.py ├── findall.py ├── protocol.py ├── servers.json └── svr_info.py ├── exchange_rate.py ├── fee_estimate.py ├── history.py ├── keys.py ├── kivy_utils.py ├── label_store.py ├── logger.py ├── main.py ├── nowallet.py ├── nowallet_history_store.py ├── passphrase.py ├── python-for-android └── recipes │ ├── aiohttp │ └── __init__.py │ └── pbkdf2 │ └── __init__.py ├── qrreader.py ├── recovery.py ├── scrape.py ├── settings_json.py ├── socks_http.py ├── utils.py ├── version.py └── wallet_alias.py /.ci/move_binary.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import os 3 | import shutil 4 | import subprocess 5 | import sys 6 | from os import environ as env 7 | 8 | binary_filename = os.path.abspath(sys.argv[1]) 9 | master_repository_directory = os.path.abspath(sys.argv[2]) 10 | data_repository = sys.argv[3] 11 | data_repository_directory = os.path.abspath(data_repository) 12 | directory = sys.argv[4] 13 | 14 | os.chdir(master_repository_directory) 15 | 16 | filename = os.path.basename(binary_filename) 17 | # Include commit subject and hash to the new commit 18 | commit_hash = ( 19 | subprocess.check_output(["git", "rev-parse", "--verify", "--short", "HEAD"]) 20 | .decode("utf-8") 21 | .strip() 22 | ) 23 | commit_subject = ( 24 | subprocess.check_output(["git", "log", "-1", "--pretty=format:%s"]) 25 | .decode("utf-8") 26 | .strip() 27 | ) 28 | 29 | is_tag = env["GITHUB_EVENT_NAME"] == "push" and env["GITHUB_REF"].startswith( 30 | "refs/tags" 31 | ) 32 | is_pr = env["GITHUB_REF"].startswith("refs/pull") 33 | 34 | filename_split = filename.split("-") 35 | if is_tag: 36 | new_commit_message = ( 37 | f'Add binary for {filename_split[1]} {commit_hash}: "{commit_subject}"' 38 | ) 39 | elif is_pr: 40 | # Pull Request - prN (pr1) 41 | pr_number = env["GITHUB_REF"].split("/")[2] 42 | filename = "-".join( 43 | [*filename_split[:2], f"pr{pr_number}", *filename_split[2:]] 44 | ) 45 | directory = os.path.join(directory, "prs") 46 | new_commit_message = ( 47 | f'Add binary for #{pr_number} {commit_hash}: "{commit_subject}"' 48 | ) 49 | else: 50 | # Latest commit - short hash (20f2448) 51 | filename = "-".join([*filename_split[:2], commit_hash, *filename_split[2:]]) 52 | new_commit_message = f'Add binary for {commit_hash}: "{commit_subject}"' 53 | 54 | # Set author info to the latest commit author 55 | author_name = subprocess.check_output( 56 | ["git", "log", "-1", "--pretty=format:%an"] 57 | ).decode("utf-8") 58 | author_email = subprocess.check_output( 59 | ["git", "log", "-1", "--pretty=format:%ae"] 60 | ).decode("utf-8") 61 | 62 | # Prepare for pushing 63 | os.chdir(data_repository_directory) 64 | os.makedirs(directory, exist_ok=True) 65 | subprocess.check_call(["git", "config", "user.name", author_name]) 66 | subprocess.check_call(["git", "config", "user.email", author_email]) 67 | # Ensure that there are no changes 68 | subprocess.check_call( 69 | [ 70 | "git", 71 | "pull", 72 | "origin", 73 | data_repository, 74 | "--ff-only", 75 | "--allow-unrelated-histories", 76 | ] 77 | ) 78 | 79 | # Try to push several times 80 | for i in range(3): 81 | shutil.copy(binary_filename, os.path.join(directory, filename)) 82 | # Push changes 83 | subprocess.check_call(["git", "add", os.path.join(directory, filename)]) 84 | subprocess.check_call( 85 | ["git", "commit", "--amend", "-m", new_commit_message] 86 | ) 87 | try: 88 | subprocess.check_call( 89 | ["git", "push", "origin", data_repository, "--force"] 90 | ) 91 | except subprocess.CalledProcessError: # There are changes in repository 92 | # Undo local changes 93 | subprocess.check_call( 94 | ["git", "reset", f"origin/{data_repository}", "--hard"] 95 | ) 96 | # Pull new changes 97 | subprocess.check_call( 98 | [ 99 | "git", 100 | "pull", 101 | "origin", 102 | data_repository, 103 | "--force", 104 | "--ff-only", 105 | "--allow-unrelated-histories", 106 | ] 107 | ) 108 | else: 109 | break # Exit loop if there is no errors 110 | else: 111 | raise Exception("Cannot push binary") 112 | 113 | new_commit_hash = ( 114 | subprocess.check_output(["git", "rev-parse", "--verify", "--short", "HEAD"]) 115 | .decode("utf-8") 116 | .strip() 117 | ) 118 | print( 119 | f"Binary file: {env['GITHUB_SERVER_URL']}/{env['GITHUB_REPOSITORY']}/blob/" 120 | f"{new_commit_hash}/{directory}/{filename}" 121 | ) 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches-ignore: 5 | - data 6 | - gh-pages 7 | tags: 8 | - '**' 9 | pull_request: 10 | branches-ignore: 11 | - data 12 | - gh-pages 13 | 14 | jobs: 15 | # Build job. Builds app for Android with Buildozer 16 | build-android: 17 | name: Build for Android 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | path: master 25 | 26 | - name: Build with Buildozer 27 | uses: ./master # REPLACE WITH ArtemSBulgakov/buildozer-action@v1 28 | id: buildozer 29 | with: 30 | repository_root: master 31 | workdir: . 32 | buildozer_version: stable 33 | 34 | - name: Upload artifacts 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: package 38 | path: ${{ steps.buildozer.outputs.filename }} 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v2 42 | with: 43 | path: data 44 | ref: data # Branch name 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: 3.11.0 50 | architecture: x64 51 | 52 | - name: Push binary to data branch 53 | if: github.event_name == 'push' 54 | run: python master/.ci/move_binary.py "${{ steps.buildozer.outputs.filename }}" master data bin 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | nowallet.ini 3 | brainbow.ini 4 | *.egg-info 5 | .buildozer 6 | .eric6project 7 | .cache 8 | __pycache__ 9 | .mypy_cache 10 | .pytest_cache 11 | bin 12 | nowallet.e4p 13 | brainbow.e4p 14 | 15 | lint.txt 16 | type.txt 17 | .tox 18 | MANIFEST 19 | android-zbar-qrcode 20 | api_password.txt 21 | .vscode/ 22 | env 23 | env.old 24 | *.pyc 25 | -my-notes* 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Pre-commit hooks. 2 | # python3 -m pip install pre-commit 3 | # pre-commit install 4 | 5 | repos: 6 | 7 | # Format Python 8 | - repo: https://github.com/psf/black 9 | rev: stable 10 | hooks: 11 | - id: black 12 | 13 | # Sort imports 14 | - repo: https://github.com/pycqa/isort 15 | rev: 5.6.4 16 | hooks: 17 | - id: isort 18 | additional_dependencies: ["toml"] 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kivy/buildozer:latest 2 | # See https://github.com/kivy/buildozer/blob/master/Dockerfile 3 | 4 | # Buildozer will be installed in entrypoint.py 5 | # This is needed to install version specified by user 6 | RUN pip3 uninstall -y buildozer 7 | 8 | # Remove a lot of warnings 9 | # sudo: setrlimit(RLIMIT_CORE): Operation not permitted 10 | # See https://github.com/sudo-project/sudo/issues/42 11 | RUN echo "Set disable_coredump false" | sudo tee -a /etc/sudo.conf > /dev/null 12 | 13 | # By default Python buffers output and you see prints after execution 14 | # Set env variable to disable this behavior 15 | ENV PYTHONUNBUFFERED=1 16 | 17 | COPY entrypoint.py /action/entrypoint.py 18 | ENTRYPOINT ["/action/entrypoint.py"] 19 | -------------------------------------------------------------------------------- /Logo/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/Logo/horizontal.png -------------------------------------------------------------------------------- /Logo/logomark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/Logo/logomark.png -------------------------------------------------------------------------------- /Logo/vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/Logo/vertical.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Effective immediately, as of April 25, 2024, all work on Brainbow is suspended. The repository will remain archived and read-only. Please ensure compliance with local laws when accessing Brainbow resources or running the app. 2 | 3 |

4 | Brainbow 5 |

6 | 7 | ### Secure, private, and plausibly deniable 8 | #### Cross-platform Bitcoin brainwallet 9 | 10 | ### Introduction: 11 | 12 | This project, [Brainbow](https://github.com/Bitcoin-Brainbow/Brainbow/) is built on top of [nowallet](https://github.com/metamarcdw/nowallet), a project which was discontinued before it was completed.s 13 | 14 | The adopted nowallet sources have been completely reworked and adapted. 15 | 16 | 17 | ### What is Brainbow? ### 18 | 19 | Brainbow is a secure Bitcoin brainwallet app, a Bitcoin wallet also known as Bitcoin Client, that will ultimately be meant for desktop and mobile platforms. 20 | 21 | It was inspired by reports of incidents of Bitcoin being seized physically at border crossings. 22 | 23 | People need an option for a brainwallet that is **secure** and **easy to use**. 24 | 25 | It's written in Python and depends on the pycoin and Coinkite's connectrum libraries. 26 | 27 | It uses Electrum servers on the back end, and communicates exclusively over Tor. 28 | 29 | It uses a variant of the ['WarpWallet'](https://keybase.io/warp/) technique, combining PBKDF2 and scrypt with a salt for key derivation, rather than the typical, highly insecure SHA256(passphrase) method that your average brainwallet uses. 30 | 31 | Here's a basic explanation of the benefits of using the WarpWallet technique: 32 | 33 | 34 | ##### Quoted from https://keybase.io/warp/: 35 | >"WarpWallet is a deterministic bitcoin address generator. You never have 36 | >to save or store your private key anywhere. Just pick a really good 37 | >password and never use it for anything else. 38 | > 39 | >This is not an original idea. bitaddress.org's brainwallet is our 40 | >inspiration. 41 | > 42 | >WarpWallet adds two improvements: (1) WarpWallet uses scrypt to make 43 | >address generation both memory and time-intensive. And (2) you can "salt" 44 | >your passphrase with your email address. Though salting is optional, we 45 | >recommend it. Any attacker of WarpWallet addresses would have to target 46 | >you individually, rather than netting you in a wider, generic sweep. And 47 | >your email is trivial to remember, so why not?" 48 | 49 | (Note: Salting is not optional in our case.) 50 | 51 | ### Details: 52 | Basically, you get a secure brainwallet in a convenient app (now with 53 | SegWit address support) and only need to remember an email address/password 54 | combination rather than an entire 12/24 word seed. People are typically 55 | more accustomed to remembering a normal set of login info, which will 56 | protect users from forgetting or misremembering part of their seed and 57 | losing coins forever. 58 | 59 | We have also implemented a full HD wallet compatible with BIP32/44. The 60 | current working title is Nowallet, as in, "I'm sorry officer, I have no 61 | wallet!" We are currently in a pre-alpha state. All testers must be 62 | able to install dependencies and run from the simple command line interface. 63 | 64 | If you're interested in testing, you can get some testnet coins here: 65 | * https://coinfaucet.eu/en/btc-testnet/ bech32 ready 66 | * https://testnet-faucet.com/btc-testnet/ bech32 ready 67 | * https://bitcoinfaucet.uo1.net/ bech32 ready 68 | * https://onchain.io/bitcoin-testnet-faucet bech32 ready 69 | * https://kuttler.eu/de/bitcoin/btc/faucet/ bech32 ready 70 | * https://testnet.qc.to/ 71 | * https://testnet-faucet.mempool.co/ 72 | * https://tbtc.bitaps.com/ 73 | * https://testnet.help/en/btcfaucet/testnet 74 | 130 | 131 | 132 | 133 | Test wallets: 134 | 135 | 136 | test1 / test 137 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Buildozer action 2 | description: Build Python package with Kivy Buildozer 3 | author: Artem Bulgakov 4 | branding: 5 | icon: package 6 | color: yellow 7 | 8 | inputs: 9 | command: 10 | description: Command to start Buildozer. Set to `buildozer ios debug|release` for iOS 11 | required: true 12 | default: buildozer android debug 13 | repository_root: 14 | description: Path to cloned repository. Set if you specified path for `actions/checkout` action. 15 | required: true 16 | default: . 17 | workdir: 18 | description: Working directory where buildozer.spec is located. Set to `src` if buildozer.spec is in `src` directory 19 | required: true 20 | default: src 21 | buildozer_version: 22 | description: Version of Buildozer to install. By default installs latest release 23 | required: true 24 | default: stable 25 | 26 | outputs: 27 | filename: 28 | description: Filename of built package 29 | 30 | runs: 31 | using: docker 32 | image: Dockerfile 33 | -------------------------------------------------------------------------------- /entrypoint.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | """ 3 | Buildozer action 4 | ================ 5 | 6 | It sets some environment variables, installs Buildozer, runs Buildozer and finds 7 | output file. 8 | 9 | You can read this file top down because functions are ordered by their execution 10 | order. 11 | """ 12 | 13 | import os 14 | import subprocess 15 | import sys 16 | from os import environ as env 17 | 18 | 19 | def main(): 20 | repository_root = os.path.abspath(env["INPUT_REPOSITORY_ROOT"]) 21 | change_owner(env["USER"], repository_root) 22 | fix_home() 23 | install_buildozer(env["INPUT_BUILDOZER_VERSION"]) 24 | apply_buildozer_settings() 25 | change_directory(env["INPUT_REPOSITORY_ROOT"], env["INPUT_WORKDIR"]) 26 | apply_patches() 27 | run_command(env["INPUT_COMMAND"]) 28 | set_output(env["INPUT_REPOSITORY_ROOT"], env["INPUT_WORKDIR"]) 29 | change_owner("root", repository_root) 30 | 31 | 32 | def change_owner(user, repository_root): 33 | # GitHub sets root as owner of repository directory. Change it to user 34 | # And return to root after all commands 35 | subprocess.check_call(["sudo", "chown", "-R", user, repository_root]) 36 | 37 | 38 | def fix_home(): 39 | # GitHub sets HOME to /github/home, but Buildozer is installed to /home/user. Change HOME to user's home 40 | env["HOME"] = env["HOME_DIR"] 41 | 42 | 43 | def install_buildozer(buildozer_version): 44 | # Install required Buildozer version 45 | print("::group::Installing Buildozer") 46 | pip_install = [sys.executable] + "-m pip install --user --upgrade".split() 47 | if buildozer_version == "stable": 48 | # Install stable buildozer from PyPI 49 | subprocess.check_call([*pip_install, "buildozer"]) 50 | elif os.path.exists(buildozer_version) and os.path.exists( 51 | os.path.join(buildozer_version, "buildozer", "__init__.py") 52 | ): 53 | # Install from local directory 54 | subprocess.check_call([*pip_install, buildozer_version]) 55 | elif buildozer_version.startswith("git+"): 56 | # Install from specified git+ link 57 | subprocess.check_call([*pip_install, buildozer_version]) 58 | elif buildozer_version == "": 59 | # Just do nothing 60 | print( 61 | "::warning::Buildozer is not installed because " 62 | "specified buildozer_version is nothing." 63 | ) 64 | else: 65 | # Install specified ref from repository 66 | subprocess.check_call( 67 | [ 68 | *pip_install, 69 | f"git+https://github.com/kivy/buildozer.git@{buildozer_version}", 70 | ] 71 | ) 72 | print("::endgroup::") 73 | 74 | 75 | def apply_buildozer_settings(): 76 | # Buildozer settings to disable interactions 77 | env["BUILDOZER_WARN_ON_ROOT"] = "0" 78 | env["APP_ANDROID_ACCEPT_SDK_LICENSE"] = "1" 79 | # Do not allow to change directories 80 | env["BUILDOZER_BUILD_DIR"] = "./.buildozer" 81 | env["BUILDOZER_BIN"] = "./bin" 82 | 83 | 84 | def change_directory(repository_root, workdir): 85 | directory = os.path.join(repository_root, workdir) 86 | # Change directory to workir 87 | if not os.path.exists(directory): 88 | print("::error::Specified workdir is not exists.") 89 | exit(1) 90 | os.chdir(directory) 91 | 92 | 93 | def apply_patches(): 94 | # Apply patches 95 | print("::group::Applying patches to Buildozer") 96 | try: 97 | import importlib 98 | import site 99 | 100 | importlib.reload(site) 101 | globals()["buildozer"] = importlib.import_module("buildozer") 102 | except ImportError: 103 | print( 104 | "::error::Cannot apply patches to buildozer (ImportError). " 105 | "Update buildozer-action to new version or create a Bug Request" 106 | ) 107 | print("::endgroup::") 108 | return 109 | 110 | print("Changing global_buildozer_dir") 111 | source = open(buildozer.__file__, "r", encoding="utf-8").read() 112 | new_source = source.replace( 113 | """ 114 | @property 115 | def global_buildozer_dir(self): 116 | return join(expanduser('~'), '.buildozer') 117 | """, 118 | f""" 119 | @property 120 | def global_buildozer_dir(self): 121 | return '{env["GITHUB_WORKSPACE"]}/{env["INPUT_REPOSITORY_ROOT"]}/.buildozer_global' 122 | """, 123 | ) 124 | if new_source == source: 125 | print( 126 | "::warning::Cannot change global buildozer directory. " 127 | "Update buildozer-action to new version or create a Bug Request" 128 | ) 129 | open(buildozer.__file__, "w", encoding="utf-8").write(new_source) 130 | print("::endgroup::") 131 | 132 | 133 | def run_command(command): 134 | # Run command 135 | retcode = subprocess.check_call(command, shell=True) 136 | if retcode: 137 | print(f'::error::Error while executing command "{command}"') 138 | exit(1) 139 | 140 | 141 | def set_output(repository_root, workdir): 142 | if not os.path.exists("bin"): 143 | print( 144 | "::error::Output directory does not exist. See Buildozer log for error" 145 | ) 146 | exit(1) 147 | filename = [ 148 | file 149 | for file in os.listdir("bin") 150 | if os.path.isfile(os.path.join("bin", file)) 151 | ][0] 152 | path = os.path.normpath( 153 | os.path.join(repository_root, workdir, "bin", filename) 154 | ) 155 | print(f"::set-output name=filename::{path}") 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /icons/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/icons/black.png -------------------------------------------------------------------------------- /icons/brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/icons/brain.png -------------------------------------------------------------------------------- /icons/brainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/icons/brainbow.png -------------------------------------------------------------------------------- /icons/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/icons/white.png -------------------------------------------------------------------------------- /install.md: -------------------------------------------------------------------------------- 1 | # How to run the sources on your own machine 2 | 3 | ## Create a Python3 virtual environment 4 | 5 | ```shell 6 | python -m venv ~/brainbowenv/ 7 | ``` 8 | 9 | ## Activate the Python3 virtual environment 10 | 11 | ```shell 12 | source ~/brainbowenv/bin/activate 13 | ``` 14 | 15 | Note: you can deactivate the python environment at any time with the command `deactivate` 16 | read more about it [here](https://docs.python.org/3/library/venv.html) 17 | 18 | ## Clone Brainbow repository and install requirements 19 | 20 | ```shell 21 | git clone https://github.com/Bitcoin-Brainbow/Brainbow.git brainbow 22 | cd brainbow/ 23 | ``` 24 | 25 | Add the following to the new_requirements.txt 26 | 27 | kivy-garden 28 | kivy 29 | kivy_garden.graph 30 | kivy_garden.qrcode 31 | embit 32 | numpy 33 | camera4kivy 34 | pyzbar 35 | 36 | ```shell 37 | pip install -r new_requirements.txt 38 | ``` 39 | 40 | Note: for MacOS install zbar with brew 41 | 42 | ```shell 43 | brew install zbar 44 | ``` 45 | 46 | ## Start the app 47 | 48 | ```shell 49 | python main.py 50 | ``` 51 | 52 | ## NOTES: 53 | - Tor must be installed and running. 54 | -------------------------------------------------------------------------------- /new_requirements.txt: -------------------------------------------------------------------------------- 1 | aioconsole==0.1.10 2 | aiohttp==3.8.3 3 | aiosignal==1.2.0 4 | #git+https://github.com/nibrag/aiosocks.git@d4a85e73c9e3beadd7ab4c46b8f3ceb3b57338b2 5 | appdirs==1.4.4 6 | async-timeout==4.0.2 7 | asyncgui==0.5.5 8 | asynckivy==0.5.4 9 | attrs==17.4.0 10 | beautifulsoup4==4.6.0 11 | buildozer==1.4.0 12 | certifi==2018.1.18 13 | chardet==3.0.4 14 | charset-normalizer==2.1.1 15 | colorama==0.4.5 16 | #-e git+https://github.com/coinkite/connectrum@14735d7b77426f4fdefc001c67af1dc8cdf1c3a4#egg=connectrum 17 | Cython==0.29.32 18 | distlib==0.3.6 19 | docutils==0.14 20 | filelock==3.8.0 21 | frozenlist==1.3.1 22 | idna==2.6 23 | importlib-metadata==4.12.0 24 | Jinja2==3.1.2 25 | Kivy==2.1.0 26 | Kivy-Garden==0.1.5 27 | kivymd==1.0.2 28 | MarkupSafe==2.1.1 29 | multidict==6.0.2 30 | # -e git+https://github.com/xavierfiechter/nowallet.git@76ccef11b8f303fb8931b9ae8c3b7df4252bf508#egg=nowallet 31 | pbkdf2==1.3 32 | pep517==0.6.0 33 | pexpect==4.8.0 34 | Pillow==8.4.0 # 8.4.0 is needed, otherwise buildozer can't patch it as of 2022-10-12. See https://github.com/kivy/python-for-android/issues/2671 35 | platformdirs==2.5.2 36 | ptyprocess==0.7.0 37 | pycoin==0.80 38 | pycryptodome==3.15.0 39 | Pygments==2.2.0 40 | pytoml==0.1.21 41 | qrcode==5.3 42 | requests==2.20.0 43 | sh==1.14.3 44 | six==1.11.0 45 | toml==0.10.2 46 | urllib3==1.24.3 47 | virtualenv==20.16.4 48 | yarl==1.8.1 49 | zipp==3.8.1 50 | # embit==0.5.0 51 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Format Python 2 | [tool.black] 3 | line-length = 80 4 | target-version = ['py35', 'py36', 'py37', 'py38'] 5 | include = '(\.pyi?$|\.spec$)' 6 | exclude = ''' 7 | ( 8 | /( 9 | \.eggs 10 | | \.git 11 | | \.hg 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | _build 16 | | buck-out 17 | | build 18 | | dist 19 | )/ 20 | | buildozer\.spec 21 | ) 22 | ''' 23 | 24 | # Sort imports 25 | # Settings to fit Black formatting 26 | # Taken from https://github.com/timothycrosley/isort/issues/694 27 | [tool.isort] 28 | line_length = 80 29 | include_trailing_comma = true 30 | multi_line_output = 3 31 | use_parentheses = true 32 | force_grid_wrap = 0 33 | ensure_newline_before_comments = true 34 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | astroid==1.6.1 2 | async-generator==1.9 3 | attrs==17.4.0 4 | gunicorn==19.7.1 5 | isort==4.3.4 6 | lazy-object-proxy==1.3.1 7 | mccabe==0.6.1 8 | pluggy==0.6.0 9 | py==1.5.2 10 | pylint==1.8.2 11 | pytest==3.4.1 12 | pytest-asyncio==0.8.0 13 | six==1.11.0 14 | tox==2.9.1 15 | virtualenv==15.1.0 16 | wrapt==1.10.11 17 | -------------------------------------------------------------------------------- /requirements-kivy.txt: -------------------------------------------------------------------------------- 1 | certifi==2018.1.18 2 | chardet==3.0.4 3 | docutils==0.14 4 | idna==2.6 5 | #-e git+https://github.com/matham/kivy@async-loop#egg=kivy 6 | Kivy-Garden==0.1.4 7 | #-e git+https://gitlab.com/kivymd/KivyMD@master#egg=kivymd 8 | Pygments==2.2.0 9 | qrcode==5.3 10 | requests==2.20.0 11 | six==1.11.0 12 | urllib3>=1.24.2 13 | KivyMD==1.0.2 14 | kivy==2.1.0 15 | asynckivy==0.5.4 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioconsole==0.1.10 2 | aiohttp==2.2.5 3 | aiosocks==0.2.4 4 | async-timeout==2.0.0 5 | attrs==17.4.0 6 | beautifulsoup4==4.6.0 7 | chardet==3.0.4 8 | -e git+https://github.com/coinkite/connectrum@master#egg=connectrum 9 | idna==2.6 10 | multidict==4.1.0 11 | pbkdf2==1.3 12 | pycoin==0.80 13 | pycryptodome==3.6.6 14 | scrypt==0.8.6 15 | yarl==0.18.0 16 | -------------------------------------------------------------------------------- /screens/Screenshot_balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/screens/Screenshot_balance.png -------------------------------------------------------------------------------- /screens/Screenshot_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/screens/Screenshot_login.png -------------------------------------------------------------------------------- /screens/Screenshot_recieve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/screens/Screenshot_recieve.png -------------------------------------------------------------------------------- /screens/Screenshot_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/screens/Screenshot_send.png -------------------------------------------------------------------------------- /src/aiosocks/README.rst: -------------------------------------------------------------------------------- 1 | # 2 | # Copied aiosocks at commit d4a85e73c9e3beadd7ab4c46b8f3ceb3b57338b2 into the project repository. 3 | # git+https://github.com/nibrag/aiosocks.git@d4a85e73c9e3beadd7ab4c46b8f3ceb3b57338b2 4 | # 5 | 6 | 7 | SOCKS proxy client for asyncio and aiohttp 8 | ========================================== 9 | .. image:: https://travis-ci.org/nibrag/aiosocks.svg?branch=master 10 | :target: https://travis-ci.org/nibrag/aiosocks 11 | :align: right 12 | 13 | .. image:: https://coveralls.io/repos/github/nibrag/aiosocks/badge.svg?branch=master 14 | :target: https://coveralls.io/github/nibrag/aiosocks?branch=master 15 | :align: right 16 | 17 | .. image:: https://badge.fury.io/py/aiosocks.svg 18 | :target: https://badge.fury.io/py/aiosocks 19 | 20 | 21 | Dependencies 22 | ------------ 23 | python 3.5+ 24 | aiohttp 2.3.2+ 25 | 26 | Features 27 | -------- 28 | - SOCKS4, SOCKS4a and SOCKS5 version 29 | - ProxyConnector for aiohttp 30 | - SOCKS "CONNECT" command 31 | 32 | TODO 33 | ---- 34 | - UDP associate 35 | - TCP port binding 36 | 37 | Installation 38 | ------------ 39 | You can install it using Pip: 40 | 41 | .. code-block:: 42 | 43 | pip install aiosocks 44 | 45 | If you want the latest development version, you can install it from source: 46 | 47 | .. code-block:: 48 | 49 | git clone git@github.com:nibrag/aiosocks.git 50 | cd aiosocks 51 | python setup.py install 52 | 53 | Usage 54 | ----- 55 | direct usage 56 | ^^^^^^^^^^^^ 57 | 58 | .. code-block:: python 59 | 60 | import asyncio 61 | import aiosocks 62 | 63 | 64 | async def connect(): 65 | socks5_addr = aiosocks.Socks5Addr('127.0.0.1', 1080) 66 | socks4_addr = aiosocks.Socks4Addr('127.0.0.1', 1080) 67 | 68 | socks5_auth = aiosocks.Socks5Auth('login', 'pwd') 69 | socks4_auth = aiosocks.Socks4Auth('ident') 70 | 71 | dst = ('github.com', 80) 72 | 73 | # socks5 connect 74 | transport, protocol = await aiosocks.create_connection( 75 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 76 | 77 | # socks4 connect 78 | transport, protocol = await aiosocks.create_connection( 79 | lambda: Protocol, proxy=socks4_addr, proxy_auth=socks4_auth, dst=dst) 80 | 81 | # socks4 without auth and local domain name resolving 82 | transport, protocol = await aiosocks.create_connection( 83 | lambda: Protocol, proxy=socks4_addr, proxy_auth=None, dst=dst, remote_resolve=False) 84 | 85 | # use socks protocol 86 | transport, protocol = await aiosocks.create_connection( 87 | None, proxy=socks4_addr, proxy_auth=None, dst=dst) 88 | 89 | if __name__ == '__main__': 90 | loop = asyncio.get_event_loop() 91 | loop.run_until_complete(connect()) 92 | loop.close() 93 | 94 | 95 | **A wrapper for create_connection() returning a (reader, writer) pair** 96 | 97 | .. code-block:: python 98 | 99 | # StreamReader, StreamWriter 100 | reader, writer = await aiosocks.open_connection( 101 | proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst, remote_resolve=True) 102 | 103 | data = await reader.read(10) 104 | writer.write('data') 105 | 106 | error handling 107 | ^^^^^^^^^^^^^^ 108 | 109 | `SocksError` is a base class for: 110 | - `NoAcceptableAuthMethods` 111 | - `LoginAuthenticationFailed` 112 | - `InvalidServerVersion` 113 | - `InvalidServerReply` 114 | 115 | .. code-block:: python 116 | 117 | try: 118 | transport, protocol = await aiosocks.create_connection( 119 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 120 | except aiosocks.SocksConnectionError: 121 | # connection error 122 | except aiosocks.LoginAuthenticationFailed: 123 | # auth failed 124 | except aiosocks.NoAcceptableAuthMethods: 125 | # All offered SOCKS5 authentication methods were rejected 126 | except (aiosocks.InvalidServerVersion, aiosocks.InvalidServerReply): 127 | # something wrong 128 | except aiosocks.SocksError: 129 | # something other 130 | 131 | or 132 | 133 | .. code-block:: python 134 | 135 | try: 136 | transport, protocol = await aiosocks.create_connection( 137 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 138 | except aiosocks.SocksConnectionError: 139 | # connection error 140 | except aiosocks.SocksError: 141 | # socks error 142 | 143 | aiohttp usage 144 | ^^^^^^^^^^^^^ 145 | 146 | .. code-block:: python 147 | 148 | import asyncio 149 | import aiohttp 150 | import aiosocks 151 | from aiosocks.connector import ProxyConnector, ProxyClientRequest 152 | 153 | 154 | async def load_github_main(): 155 | auth5 = aiosocks.Socks5Auth('proxyuser1', password='pwd') 156 | auth4 = aiosocks.Socks4Auth('proxyuser1') 157 | ba = aiohttp.BasicAuth('login') 158 | 159 | # remote resolve 160 | conn = ProxyConnector(remote_resolve=True) 161 | 162 | # or locale resolve 163 | conn = ProxyConnector(remote_resolve=False) 164 | 165 | try: 166 | with aiohttp.ClientSession(connector=conn, request_class=ProxyClientRequest) as session: 167 | # socks5 proxy 168 | async with session.get('http://github.com/', proxy='socks5://127.0.0.1:1080', 169 | proxy_auth=auth5) as resp: 170 | if resp.status == 200: 171 | print(await resp.text()) 172 | 173 | # socks4 proxy 174 | async with session.get('http://github.com/', proxy='socks4://127.0.0.1:1081', 175 | proxy_auth=auth4) as resp: 176 | if resp.status == 200: 177 | print(await resp.text()) 178 | 179 | # http proxy 180 | async with session.get('http://github.com/', proxy='http://127.0.0.1:8080', 181 | proxy_auth=ba) as resp: 182 | if resp.status == 200: 183 | print(await resp.text()) 184 | except aiohttp.ClientProxyConnectionError: 185 | # connection problem 186 | except aiohttp.ClientConnectorError: 187 | # ssl error, certificate error, etc 188 | except aiosocks.SocksError: 189 | # communication problem 190 | 191 | 192 | if __name__ == '__main__': 193 | loop = asyncio.get_event_loop() 194 | loop.run_until_complete(load_github_main()) 195 | loop.close() 196 | -------------------------------------------------------------------------------- /src/aiosocks/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from .errors import ( 3 | SocksError, NoAcceptableAuthMethods, LoginAuthenticationFailed, 4 | SocksConnectionError, InvalidServerReply, InvalidServerVersion 5 | ) 6 | from .helpers import ( 7 | SocksAddr, Socks4Addr, Socks5Addr, Socks4Auth, Socks5Auth 8 | ) 9 | from .protocols import Socks4Protocol, Socks5Protocol, DEFAULT_LIMIT 10 | 11 | __version__ = '0.2.6' 12 | 13 | __all__ = ('Socks4Protocol', 'Socks5Protocol', 'Socks4Auth', 14 | 'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksError', 15 | 'NoAcceptableAuthMethods', 'LoginAuthenticationFailed', 16 | 'SocksConnectionError', 'InvalidServerVersion', 17 | 'InvalidServerReply', 'create_connection', 'open_connection') 18 | 19 | 20 | async def create_connection(protocol_factory, proxy, proxy_auth, dst, *, 21 | remote_resolve=True, loop=None, ssl=None, family=0, 22 | proto=0, flags=0, sock=None, local_addr=None, 23 | server_hostname=None, reader_limit=DEFAULT_LIMIT): 24 | assert isinstance(proxy, SocksAddr), ( 25 | 'proxy must be Socks4Addr() or Socks5Addr() tuple' 26 | ) 27 | 28 | assert proxy_auth is None or isinstance(proxy_auth, 29 | (Socks4Auth, Socks5Auth)), ( 30 | 'proxy_auth must be None or Socks4Auth() ' 31 | 'or Socks5Auth() tuple', proxy_auth 32 | ) 33 | assert isinstance(dst, (tuple, list)) and len(dst) == 2, ( 34 | 'invalid dst format, tuple("dst_host", dst_port))' 35 | ) 36 | 37 | if (isinstance(proxy, Socks4Addr) and not 38 | (proxy_auth is None or isinstance(proxy_auth, Socks4Auth))): 39 | raise ValueError( 40 | "proxy is Socks4Addr but proxy_auth is not Socks4Auth" 41 | ) 42 | 43 | if (isinstance(proxy, Socks5Addr) and not 44 | (proxy_auth is None or isinstance(proxy_auth, Socks5Auth))): 45 | raise ValueError( 46 | "proxy is Socks5Addr but proxy_auth is not Socks5Auth" 47 | ) 48 | 49 | if server_hostname is not None and not ssl: 50 | raise ValueError('server_hostname is only meaningful with ssl') 51 | 52 | if server_hostname is None and ssl: 53 | # read details: asyncio.create_connection 54 | server_hostname = dst[0] 55 | 56 | loop = loop or asyncio.get_event_loop() 57 | waiter = asyncio.Future(loop=loop) 58 | 59 | def socks_factory(): 60 | if isinstance(proxy, Socks4Addr): 61 | socks_proto = Socks4Protocol 62 | else: 63 | socks_proto = Socks5Protocol 64 | 65 | return socks_proto(proxy=proxy, proxy_auth=proxy_auth, dst=dst, 66 | app_protocol_factory=protocol_factory, 67 | waiter=waiter, remote_resolve=remote_resolve, 68 | loop=loop, ssl=ssl, server_hostname=server_hostname, 69 | reader_limit=reader_limit) 70 | 71 | try: 72 | transport, protocol = await loop.create_connection( 73 | socks_factory, proxy.host, proxy.port, family=family, 74 | proto=proto, flags=flags, sock=sock, local_addr=local_addr) 75 | except OSError as exc: 76 | raise SocksConnectionError( 77 | '[Errno %s] Can not connect to proxy %s:%d [%s]' % 78 | (exc.errno, proxy.host, proxy.port, exc.strerror)) from exc 79 | 80 | try: 81 | await waiter 82 | except: # noqa 83 | transport.close() 84 | raise 85 | 86 | return protocol.app_transport, protocol.app_protocol 87 | 88 | 89 | async def open_connection(proxy, proxy_auth, dst, *, remote_resolve=True, 90 | loop=None, limit=DEFAULT_LIMIT, **kwds): 91 | _, protocol = await create_connection( 92 | None, proxy, proxy_auth, dst, reader_limit=limit, 93 | remote_resolve=remote_resolve, loop=loop, **kwds) 94 | 95 | return protocol.reader, protocol.writer 96 | -------------------------------------------------------------------------------- /src/aiosocks/connector.py: -------------------------------------------------------------------------------- 1 | try: 2 | import aiohttp 3 | from aiohttp.client_exceptions import cert_errors, ssl_errors 4 | except ImportError: # pragma: no cover 5 | raise ImportError('aiosocks.SocksConnector require aiohttp library') 6 | 7 | from .errors import SocksConnectionError 8 | from .helpers import Socks4Auth, Socks5Auth, Socks4Addr, Socks5Addr 9 | from . import create_connection 10 | 11 | __all__ = ('ProxyConnector', 'ProxyClientRequest') 12 | 13 | 14 | class ProxyClientRequest(aiohttp.ClientRequest): 15 | def update_proxy(self, proxy, proxy_auth, proxy_headers): 16 | if proxy and proxy.scheme not in ['http', 'socks4', 'socks5']: 17 | raise ValueError( 18 | "Only http, socks4 and socks5 proxies are supported") 19 | if proxy and proxy_auth: 20 | if proxy.scheme == 'http' and \ 21 | not isinstance(proxy_auth, aiohttp.BasicAuth): 22 | raise ValueError("proxy_auth must be None or " 23 | "BasicAuth() tuple for http proxy") 24 | if proxy.scheme == 'socks4' and \ 25 | not isinstance(proxy_auth, Socks4Auth): 26 | raise ValueError("proxy_auth must be None or Socks4Auth() " 27 | "tuple for socks4 proxy") 28 | if proxy.scheme == 'socks5' and \ 29 | not isinstance(proxy_auth, Socks5Auth): 30 | raise ValueError("proxy_auth must be None or Socks5Auth() " 31 | "tuple for socks5 proxy") 32 | self.proxy = proxy 33 | self.proxy_auth = proxy_auth 34 | self.proxy_headers = proxy_headers 35 | 36 | 37 | class ProxyConnector(aiohttp.TCPConnector): 38 | def __init__(self, remote_resolve=True, **kwargs): 39 | super().__init__(**kwargs) 40 | 41 | self._remote_resolve = remote_resolve 42 | 43 | async def _create_proxy_connection(self, req, *args, **kwargs): 44 | if req.proxy.scheme == 'http': 45 | return await super()._create_proxy_connection(req, *args, **kwargs) 46 | else: 47 | return await self._create_socks_connection(req) 48 | 49 | async def _wrap_create_socks_connection(self, *args, req, **kwargs): 50 | try: 51 | return await create_connection(*args, **kwargs) 52 | except cert_errors as exc: 53 | raise aiohttp.ClientConnectorCertificateError( 54 | req.connection_key, exc) from exc 55 | except ssl_errors as exc: 56 | raise aiohttp.ClientConnectorSSLError( 57 | req.connection_key, exc) from exc 58 | except (OSError, SocksConnectionError) as exc: 59 | raise aiohttp.ClientProxyConnectionError( 60 | req.connection_key, exc) from exc 61 | 62 | def _get_fingerprint_and_hashfunc(self, req): 63 | base = super() 64 | if hasattr(base, '_get_fingerprint_and_hashfunc'): 65 | return base._get_fingerprint_and_hashfunc(req) 66 | 67 | fingerprint = self._get_fingerprint(req) 68 | if fingerprint: 69 | return (fingerprint.fingerprint, fingerprint._hashfunc) 70 | 71 | return (None, None) 72 | 73 | async def _create_socks_connection(self, req): 74 | sslcontext = self._get_ssl_context(req) 75 | fingerprint, hashfunc = self._get_fingerprint_and_hashfunc(req) 76 | 77 | if not self._remote_resolve: 78 | try: 79 | dst_hosts = list(await self._resolve_host(req.host, req.port)) 80 | dst = dst_hosts[0]['host'], dst_hosts[0]['port'] 81 | except OSError as exc: 82 | raise aiohttp.ClientConnectorError( 83 | req.connection_key, exc) from exc 84 | else: 85 | dst = req.host, req.port 86 | 87 | try: 88 | proxy_hosts = await self._resolve_host( 89 | req.proxy.host, req.proxy.port) 90 | except OSError as exc: 91 | raise aiohttp.ClientConnectorError( 92 | req.connection_key, exc) from exc 93 | 94 | last_exc = None 95 | 96 | for hinfo in proxy_hosts: 97 | if req.proxy.scheme == 'socks4': 98 | proxy = Socks4Addr(hinfo['host'], hinfo['port']) 99 | else: 100 | proxy = Socks5Addr(hinfo['host'], hinfo['port']) 101 | 102 | try: 103 | transp, proto = await self._wrap_create_socks_connection( 104 | self._factory, proxy, req.proxy_auth, dst, 105 | loop=self._loop, remote_resolve=self._remote_resolve, 106 | ssl=sslcontext, family=hinfo['family'], 107 | proto=hinfo['proto'], flags=hinfo['flags'], 108 | local_addr=self._local_addr, req=req, 109 | server_hostname=req.host if sslcontext else None) 110 | except aiohttp.ClientConnectorError as exc: 111 | last_exc = exc 112 | continue 113 | 114 | has_cert = transp.get_extra_info('sslcontext') 115 | if has_cert and fingerprint: 116 | sock = transp.get_extra_info('socket') 117 | if not hasattr(sock, 'getpeercert'): 118 | # Workaround for asyncio 3.5.0 119 | # Starting from 3.5.1 version 120 | # there is 'ssl_object' extra info in transport 121 | sock = transp._ssl_protocol._sslpipe.ssl_object 122 | # gives DER-encoded cert as a sequence of bytes (or None) 123 | cert = sock.getpeercert(binary_form=True) 124 | assert cert 125 | got = hashfunc(cert).digest() 126 | expected = fingerprint 127 | if got != expected: 128 | transp.close() 129 | if not self._cleanup_closed_disabled: 130 | self._cleanup_closed_transports.append(transp) 131 | last_exc = aiohttp.ServerFingerprintMismatch( 132 | expected, got, req.host, req.port) 133 | continue 134 | return transp, proto 135 | else: 136 | raise last_exc 137 | -------------------------------------------------------------------------------- /src/aiosocks/constants.py: -------------------------------------------------------------------------------- 1 | RSV = NULL = 0x00 2 | SOCKS_VER4 = 0x04 3 | SOCKS_VER5 = 0x05 4 | 5 | SOCKS_CMD_CONNECT = 0x01 6 | SOCKS_CMD_BIND = 0x02 7 | SOCKS_CMD_UDP_ASSOCIATE = 0x03 8 | SOCKS4_GRANTED = 0x5A 9 | SOCKS5_GRANTED = 0x00 10 | 11 | SOCKS5_AUTH_ANONYMOUS = 0x00 12 | SOCKS5_AUTH_UNAME_PWD = 0x02 13 | SOCKS5_AUTH_NO_ACCEPTABLE_METHODS = 0xFF 14 | 15 | SOCKS5_ATYP_IPv4 = 0x01 16 | SOCKS5_ATYP_DOMAIN = 0x03 17 | SOCKS5_ATYP_IPv6 = 0x04 18 | 19 | SOCKS4_ERRORS = { 20 | 0x5B: 'Request rejected or failed', 21 | 0x5C: 'Request rejected because SOCKS server ' 22 | 'cannot connect to identd on the client', 23 | 0x5D: 'Request rejected because the client program ' 24 | 'and identd report different user-ids' 25 | } 26 | 27 | SOCKS5_ERRORS = { 28 | 0x01: 'General SOCKS server failure', 29 | 0x02: 'Connection not allowed by ruleset', 30 | 0x03: 'Network unreachable', 31 | 0x04: 'Host unreachable', 32 | 0x05: 'Connection refused', 33 | 0x06: 'TTL expired', 34 | 0x07: 'Command not supported, or protocol error', 35 | 0x08: 'Address type not supported' 36 | } 37 | -------------------------------------------------------------------------------- /src/aiosocks/errors.py: -------------------------------------------------------------------------------- 1 | class SocksError(Exception): 2 | pass 3 | 4 | 5 | class NoAcceptableAuthMethods(SocksError): 6 | pass 7 | 8 | 9 | class LoginAuthenticationFailed(SocksError): 10 | pass 11 | 12 | 13 | class InvalidServerVersion(SocksError): 14 | pass 15 | 16 | 17 | class InvalidServerReply(SocksError): 18 | pass 19 | 20 | 21 | class SocksConnectionError(OSError): 22 | pass 23 | -------------------------------------------------------------------------------- /src/aiosocks/helpers.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | __all__ = ('Socks4Auth', 'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksAddr') 4 | 5 | 6 | class Socks4Auth(namedtuple('Socks4Auth', ['login', 'encoding'])): 7 | def __new__(cls, login, encoding='utf-8'): 8 | if login is None: 9 | raise ValueError('None is not allowed as login value') 10 | 11 | return super().__new__(cls, login.encode(encoding), encoding) 12 | 13 | 14 | class Socks5Auth(namedtuple('Socks5Auth', ['login', 'password', 'encoding'])): 15 | def __new__(cls, login, password, encoding='utf-8'): 16 | if login is None: 17 | raise ValueError('None is not allowed as login value') 18 | 19 | if password is None: 20 | raise ValueError('None is not allowed as password value') 21 | 22 | return super().__new__(cls, 23 | login.encode(encoding), 24 | password.encode(encoding), encoding) 25 | 26 | 27 | class SocksAddr(namedtuple('SocksServer', ['host', 'port'])): 28 | def __new__(cls, host, port=1080): 29 | if host is None: 30 | raise ValueError('None is not allowed as host value') 31 | 32 | if port is None: 33 | port = 1080 # default socks server port 34 | 35 | return super().__new__(cls, host, port) 36 | 37 | 38 | class Socks4Addr(SocksAddr): 39 | pass 40 | 41 | 42 | class Socks5Addr(SocksAddr): 43 | pass 44 | -------------------------------------------------------------------------------- /src/aiosocks/protocols.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import struct 4 | from asyncio import sslproto 5 | 6 | from . import constants as c 7 | from .helpers import ( 8 | Socks4Addr, Socks5Addr, Socks5Auth, Socks4Auth 9 | ) 10 | from .errors import ( 11 | SocksError, NoAcceptableAuthMethods, LoginAuthenticationFailed, 12 | InvalidServerReply, InvalidServerVersion 13 | ) 14 | 15 | 16 | DEFAULT_LIMIT = getattr(asyncio.streams, '_DEFAULT_LIMIT', 2**16) 17 | 18 | 19 | class BaseSocksProtocol(asyncio.StreamReaderProtocol): 20 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, *, 21 | remote_resolve=True, loop=None, ssl=False, 22 | server_hostname=None, negotiate_done_cb=None, 23 | reader_limit=DEFAULT_LIMIT): 24 | if not isinstance(dst, (tuple, list)) or len(dst) != 2: 25 | raise ValueError( 26 | 'Invalid dst format, tuple("dst_host", dst_port))' 27 | ) 28 | 29 | self._proxy = proxy 30 | self._auth = proxy_auth 31 | self._dst_host, self._dst_port = dst 32 | self._remote_resolve = remote_resolve 33 | self._waiter = waiter 34 | self._ssl = ssl 35 | self._server_hostname = server_hostname 36 | self._negotiate_done_cb = negotiate_done_cb 37 | self._loop = loop or asyncio.get_event_loop() 38 | 39 | self._transport = None 40 | self._negotiate_done = False 41 | self._proxy_peername = None 42 | self._proxy_sockname = None 43 | 44 | if app_protocol_factory: 45 | self._app_protocol = app_protocol_factory() 46 | else: 47 | self._app_protocol = self 48 | 49 | reader = asyncio.StreamReader(loop=self._loop, limit=reader_limit) 50 | 51 | super().__init__(stream_reader=reader, 52 | client_connected_cb=self.negotiate, loop=self._loop) 53 | 54 | async def negotiate(self, reader, writer): 55 | try: 56 | req = self.socks_request(c.SOCKS_CMD_CONNECT) 57 | self._proxy_peername, self._proxy_sockname = await req 58 | except SocksError as exc: 59 | exc = SocksError('Can not connect to %s:%s. %s' % 60 | (self._dst_host, self._dst_port, exc)) 61 | if not self._waiter.cancelled(): 62 | self._loop.call_soon(self._waiter.set_exception, exc) 63 | except Exception as exc: 64 | if not self._waiter.cancelled(): 65 | self._loop.call_soon(self._waiter.set_exception, exc) 66 | else: 67 | self._negotiate_done = True 68 | 69 | if self._ssl: 70 | # Creating a ssl transport needs to be reworked. 71 | # See details: http://bugs.python.org/issue23749 72 | self._tls_protocol = sslproto.SSLProtocol( 73 | app_protocol=self, sslcontext=self._ssl, server_side=False, 74 | server_hostname=self._server_hostname, waiter=self._waiter, 75 | loop=self._loop, call_connection_made=False) 76 | 77 | # starttls 78 | original_transport = self._transport 79 | self._transport.set_protocol(self._tls_protocol) 80 | self._transport = self._tls_protocol._app_transport 81 | 82 | self._tls_protocol.connection_made(original_transport) 83 | 84 | self._loop.call_soon(self._app_protocol.connection_made, 85 | self._transport) 86 | else: 87 | self._loop.call_soon(self._app_protocol.connection_made, 88 | self._transport) 89 | self._loop.call_soon(self._waiter.set_result, True) 90 | 91 | if self._negotiate_done_cb is not None: 92 | res = self._negotiate_done_cb(reader, writer) 93 | 94 | if asyncio.iscoroutine(res): 95 | self._loop.create_task(res) 96 | return res 97 | 98 | def connection_made(self, transport): 99 | # connection_made is called 100 | if self._transport: 101 | return 102 | 103 | super().connection_made(transport) 104 | self._transport = transport 105 | 106 | def connection_lost(self, exc): 107 | if self._negotiate_done and self._app_protocol is not self: 108 | self._loop.call_soon(self._app_protocol.connection_lost, exc) 109 | super().connection_lost(exc) 110 | 111 | def pause_writing(self): 112 | if self._negotiate_done and self._app_protocol is not self: 113 | self._app_protocol.pause_writing() 114 | else: 115 | super().pause_writing() 116 | 117 | def resume_writing(self): 118 | if self._negotiate_done and self._app_protocol is not self: 119 | self._app_protocol.resume_writing() 120 | else: 121 | super().resume_writing() 122 | 123 | def data_received(self, data): 124 | if self._negotiate_done and self._app_protocol is not self: 125 | self._app_protocol.data_received(data) 126 | else: 127 | super().data_received(data) 128 | 129 | def eof_received(self): 130 | if self._negotiate_done and self._app_protocol is not self: 131 | self._app_protocol.eof_received() 132 | super().eof_received() 133 | 134 | async def socks_request(self, cmd): 135 | raise NotImplementedError 136 | 137 | def write_request(self, request): 138 | bdata = bytearray() 139 | 140 | for item in request: 141 | if isinstance(item, int): 142 | bdata.append(item) 143 | elif isinstance(item, (bytearray, bytes)): 144 | bdata += item 145 | else: 146 | raise ValueError('Unsupported item') 147 | self._stream_writer.write(bdata) 148 | 149 | async def read_response(self, n): 150 | try: 151 | return (await self._stream_reader.readexactly(n)) 152 | except asyncio.IncompleteReadError as e: 153 | raise InvalidServerReply( 154 | 'Server sent fewer bytes than required (%s)' % str(e)) 155 | 156 | async def _get_dst_addr(self): 157 | infos = await self._loop.getaddrinfo( 158 | self._dst_host, self._dst_port, family=socket.AF_UNSPEC, 159 | type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP, 160 | flags=socket.AI_ADDRCONFIG) 161 | if not infos: 162 | raise OSError('getaddrinfo() returned empty list') 163 | return infos[0][0], infos[0][4][0] 164 | 165 | @property 166 | def app_protocol(self): 167 | return self._app_protocol 168 | 169 | @property 170 | def app_transport(self): 171 | return self._transport 172 | 173 | @property 174 | def proxy_sockname(self): 175 | """ 176 | Returns the bound IP address and port number at the proxy. 177 | """ 178 | return self._proxy_sockname 179 | 180 | @property 181 | def proxy_peername(self): 182 | """ 183 | Returns the IP and port number of the proxy. 184 | """ 185 | sock = self._transport.get_extra_info('socket') 186 | return sock.peername if sock else None 187 | 188 | @property 189 | def peername(self): 190 | """ 191 | Returns the IP address and port number of the destination 192 | machine (note: get_proxy_peername returns the proxy) 193 | """ 194 | return self._proxy_peername 195 | 196 | @property 197 | def reader(self): 198 | return self._stream_reader 199 | 200 | @property 201 | def writer(self): 202 | return self._stream_writer 203 | 204 | 205 | class Socks4Protocol(BaseSocksProtocol): 206 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, 207 | remote_resolve=True, loop=None, ssl=False, 208 | server_hostname=None, negotiate_done_cb=None, 209 | reader_limit=DEFAULT_LIMIT): 210 | proxy_auth = proxy_auth or Socks4Auth('') 211 | 212 | if not isinstance(proxy, Socks4Addr): 213 | raise ValueError('Invalid proxy format') 214 | 215 | if not isinstance(proxy_auth, Socks4Auth): 216 | raise ValueError('Invalid proxy_auth format') 217 | 218 | super().__init__(proxy, proxy_auth, dst, app_protocol_factory, 219 | waiter, remote_resolve=remote_resolve, loop=loop, 220 | ssl=ssl, server_hostname=server_hostname, 221 | reader_limit=reader_limit, 222 | negotiate_done_cb=negotiate_done_cb) 223 | 224 | async def socks_request(self, cmd): 225 | # prepare destination addr/port 226 | host, port = self._dst_host, self._dst_port 227 | port_bytes = struct.pack(b'>H', port) 228 | include_hostname = False 229 | 230 | try: 231 | host_bytes = socket.inet_aton(host) 232 | except socket.error: 233 | if self._remote_resolve: 234 | host_bytes = bytes([c.NULL, c.NULL, c.NULL, 0x01]) 235 | include_hostname = True 236 | else: 237 | # it's not an IP number, so it's probably a DNS name. 238 | family, host = await self._get_dst_addr() 239 | host_bytes = socket.inet_aton(host) 240 | 241 | # build and send connect command 242 | req = [c.SOCKS_VER4, cmd, port_bytes, 243 | host_bytes, self._auth.login, c.NULL] 244 | if include_hostname: 245 | req += [self._dst_host.encode('idna'), c.NULL] 246 | 247 | self.write_request(req) 248 | 249 | # read/process result 250 | resp = await self.read_response(8) 251 | 252 | if resp[0] != c.NULL: 253 | raise InvalidServerReply('SOCKS4 proxy server sent invalid data') 254 | if resp[1] != c.SOCKS4_GRANTED: 255 | error = c.SOCKS4_ERRORS.get(resp[1], 'Unknown error') 256 | raise SocksError('[Errno {0:#04x}]: {1}'.format(resp[1], error)) 257 | 258 | binded = socket.inet_ntoa(resp[4:]), struct.unpack('>H', resp[2:4])[0] 259 | return (host, port), binded 260 | 261 | 262 | class Socks5Protocol(BaseSocksProtocol): 263 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, 264 | remote_resolve=True, loop=None, ssl=False, 265 | server_hostname=None, negotiate_done_cb=None, 266 | reader_limit=DEFAULT_LIMIT): 267 | proxy_auth = proxy_auth or Socks5Auth('', '') 268 | 269 | if not isinstance(proxy, Socks5Addr): 270 | raise ValueError('Invalid proxy format') 271 | 272 | if not isinstance(proxy_auth, Socks5Auth): 273 | raise ValueError('Invalid proxy_auth format') 274 | 275 | super().__init__(proxy, proxy_auth, dst, app_protocol_factory, 276 | waiter, remote_resolve=remote_resolve, loop=loop, 277 | ssl=ssl, server_hostname=server_hostname, 278 | reader_limit=reader_limit, 279 | negotiate_done_cb=negotiate_done_cb) 280 | 281 | async def socks_request(self, cmd): 282 | await self.authenticate() 283 | 284 | # build and send command 285 | dst_addr, resolved = await self.build_dst_address( 286 | self._dst_host, self._dst_port) 287 | self.write_request([c.SOCKS_VER5, cmd, c.RSV] + dst_addr) 288 | 289 | # read/process command response 290 | resp = await self.read_response(3) 291 | 292 | if resp[0] != c.SOCKS_VER5: 293 | raise InvalidServerVersion( 294 | 'SOCKS5 proxy server sent invalid version' 295 | ) 296 | if resp[1] != c.SOCKS5_GRANTED: 297 | error = c.SOCKS5_ERRORS.get(resp[1], 'Unknown error') 298 | raise SocksError('[Errno {0:#04x}]: {1}'.format(resp[1], error)) 299 | 300 | binded = await self.read_address() 301 | 302 | return resolved, binded 303 | 304 | async def authenticate(self): 305 | # send available auth methods 306 | if self._auth.login and self._auth.password: 307 | req = [c.SOCKS_VER5, 0x02, 308 | c.SOCKS5_AUTH_ANONYMOUS, c.SOCKS5_AUTH_UNAME_PWD] 309 | else: 310 | req = [c.SOCKS_VER5, 0x01, c.SOCKS5_AUTH_ANONYMOUS] 311 | 312 | self.write_request(req) 313 | 314 | # read/process response and send auth data if necessary 315 | chosen_auth = await self.read_response(2) 316 | 317 | if chosen_auth[0] != c.SOCKS_VER5: 318 | raise InvalidServerVersion( 319 | 'SOCKS5 proxy server sent invalid version' 320 | ) 321 | 322 | if chosen_auth[1] == c.SOCKS5_AUTH_UNAME_PWD: 323 | req = [0x01, chr(len(self._auth.login)).encode(), self._auth.login, 324 | chr(len(self._auth.password)).encode(), self._auth.password] 325 | self.write_request(req) 326 | 327 | auth_status = await self.read_response(2) 328 | if auth_status[0] != 0x01: 329 | raise InvalidServerReply( 330 | 'SOCKS5 proxy server sent invalid data' 331 | ) 332 | if auth_status[1] != c.SOCKS5_GRANTED: 333 | raise LoginAuthenticationFailed( 334 | "SOCKS5 authentication failed" 335 | ) 336 | # offered auth methods rejected 337 | elif chosen_auth[1] != c.SOCKS5_AUTH_ANONYMOUS: 338 | if chosen_auth[1] == c.SOCKS5_AUTH_NO_ACCEPTABLE_METHODS: 339 | raise NoAcceptableAuthMethods( 340 | 'All offered SOCKS5 authentication methods were rejected' 341 | ) 342 | else: 343 | raise InvalidServerReply( 344 | 'SOCKS5 proxy server sent invalid data' 345 | ) 346 | 347 | async def build_dst_address(self, host, port): 348 | family_to_byte = {socket.AF_INET: c.SOCKS5_ATYP_IPv4, 349 | socket.AF_INET6: c.SOCKS5_ATYP_IPv6} 350 | port_bytes = struct.pack('>H', port) 351 | 352 | # if the given destination address is an IP address, we will 353 | # use the IP address request even if remote resolving was specified. 354 | for family in (socket.AF_INET, socket.AF_INET6): 355 | try: 356 | host_bytes = socket.inet_pton(family, host) 357 | req = [family_to_byte[family], host_bytes, port_bytes] 358 | return req, (host, port) 359 | except socket.error: 360 | pass 361 | 362 | # it's not an IP number, so it's probably a DNS name. 363 | if self._remote_resolve: 364 | host_bytes = host.encode('idna') 365 | req = [c.SOCKS5_ATYP_DOMAIN, chr(len(host_bytes)).encode(), 366 | host_bytes, port_bytes] 367 | else: 368 | family, host_bytes = await self._get_dst_addr() 369 | host_bytes = socket.inet_pton(family, host_bytes) 370 | req = [family_to_byte[family], host_bytes, port_bytes] 371 | host = socket.inet_ntop(family, host_bytes) 372 | 373 | return req, (host, port) 374 | 375 | async def read_address(self): 376 | atype = await self.read_response(1) 377 | 378 | if atype[0] == c.SOCKS5_ATYP_IPv4: 379 | addr = socket.inet_ntoa((await self.read_response(4))) 380 | elif atype[0] == c.SOCKS5_ATYP_DOMAIN: 381 | length = await self.read_response(1) 382 | addr = await self.read_response(ord(length)) 383 | elif atype[0] == c.SOCKS5_ATYP_IPv6: 384 | addr = await self.read_response(16) 385 | addr = socket.inet_ntop(socket.AF_INET6, addr) 386 | else: 387 | raise InvalidServerReply('SOCKS5 proxy server sent invalid data') 388 | 389 | port = await self.read_response(2) 390 | port = struct.unpack('>H', port)[0] 391 | 392 | return addr, port 393 | -------------------------------------------------------------------------------- /src/aiosocks/test_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import struct 3 | import socket 4 | from aiohttp.test_utils import unused_port 5 | 6 | 7 | class FakeSocksSrv: 8 | def __init__(self, loop, write_buff): 9 | self._loop = loop 10 | self._write_buff = write_buff 11 | self._transports = [] 12 | self._srv = None 13 | self.port = unused_port() 14 | 15 | async def __aenter__(self): 16 | transports = self._transports 17 | write_buff = self._write_buff 18 | 19 | class SocksPrimitiveProtocol(asyncio.Protocol): 20 | _transport = None 21 | 22 | def connection_made(self, transport): 23 | self._transport = transport 24 | transports.append(transport) 25 | 26 | def data_received(self, data): 27 | self._transport.write(write_buff) 28 | 29 | def factory(): 30 | return SocksPrimitiveProtocol() 31 | 32 | self._srv = await self._loop.create_server( 33 | factory, '127.0.0.1', self.port) 34 | 35 | return self 36 | 37 | async def __aexit__(self, exc_type, exc_val, exc_tb): 38 | for tr in self._transports: 39 | tr.close() 40 | 41 | self._srv.close() 42 | await self._srv.wait_closed() 43 | 44 | 45 | class FakeSocks4Srv: 46 | def __init__(self, loop): 47 | self._loop = loop 48 | self._transports = [] 49 | self._futures = [] 50 | self._srv = None 51 | self.port = unused_port() 52 | 53 | async def __aenter__(self): 54 | transports = self._transports 55 | futures = self._futures 56 | 57 | class Socks4Protocol(asyncio.StreamReaderProtocol): 58 | def __init__(self, _loop): 59 | self._loop = _loop 60 | reader = asyncio.StreamReader(loop=self._loop) 61 | super().__init__(reader, client_connected_cb=self.negotiate, 62 | loop=self._loop) 63 | 64 | def connection_made(self, transport): 65 | transports.append(transport) 66 | super().connection_made(transport) 67 | 68 | async def negotiate(self, reader, writer): 69 | writer.write(b'\x00\x5a\x04W\x01\x01\x01\x01') 70 | 71 | data = await reader.read(9) 72 | 73 | dst_port = struct.unpack('>H', data[2:4])[0] 74 | dst_addr = data[4:8] 75 | 76 | if data[-1] != 0x00: 77 | while True: 78 | byte = await reader.read(1) 79 | if byte == 0x00: 80 | break 81 | 82 | if dst_addr == b'\x00\x00\x00\x01': 83 | dst_addr = bytearray() 84 | 85 | while True: 86 | byte = await reader.read(1) 87 | if byte == 0x00: 88 | break 89 | dst_addr.append(byte) 90 | else: 91 | dst_addr = socket.inet_ntoa(dst_addr) 92 | 93 | cl_reader, cl_writer = await asyncio.open_connection( 94 | host=dst_addr, port=dst_port, loop=self._loop 95 | ) 96 | transports.append(cl_writer) 97 | 98 | cl_fut = asyncio.ensure_future( 99 | self.retranslator(reader, cl_writer), loop=self._loop) 100 | dst_fut = asyncio.ensure_future( 101 | self.retranslator(cl_reader, writer), loop=self._loop) 102 | 103 | futures.append(cl_fut) 104 | futures.append(dst_fut) 105 | 106 | async def retranslator(self, reader, writer): 107 | data = bytearray() 108 | while True: 109 | try: 110 | byte = await reader.read(10) 111 | if not byte: 112 | break 113 | data.append(byte[0]) 114 | writer.write(byte) 115 | await writer.drain() 116 | except: # noqa 117 | break 118 | 119 | def factory(): 120 | return Socks4Protocol(_loop=self._loop) 121 | 122 | self._srv = await self._loop.create_server( 123 | factory, '127.0.0.1', self.port) 124 | 125 | return self 126 | 127 | async def __aexit__(self, exc_type, exc_val, exc_tb): 128 | for tr in self._transports: 129 | tr.close() 130 | 131 | self._srv.close() 132 | await self._srv.wait_closed() 133 | 134 | for f in self._futures: 135 | if not f.cancelled() or not f.done(): 136 | f.cancel() 137 | -------------------------------------------------------------------------------- /src/android_utils.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | try: 4 | from android.runnable import run_on_ui_thread 5 | from jnius import autoclass 6 | PythonActivity = autoclass('org.kivy.android.PythonActivity') 7 | Params = autoclass('android.view.WindowManagerLayoutParams') 8 | except: 9 | def run_on_ui_thread(*args, **kwargs): 10 | pass 11 | 12 | def dark_mode(): 13 | return True 14 | # def dark_mode(): 15 | # """ Check for dark mode in Android. """ 16 | # try: 17 | # from jnius import autoclass 18 | # PythonActivity = autoclass('org.kivy.android.PythonActivity') 19 | # Configuration = autoclass('android.content.res.Configuration') 20 | # night_mode_flags = PythonActivity.mActivity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK 21 | # if night_mode_flags == Configuration.UI_MODE_NIGHT_YES: 22 | # return True 23 | # elif night_mode_flags in [Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED]: 24 | # return False 25 | # except Exception as ex: 26 | # print(ex) 27 | # print(traceback.format_exc()) 28 | 29 | 30 | def android_setflag(): 31 | try: 32 | PythonActivity.mActivity.getWindow().addFlags(Params.FLAG_KEEP_SCREEN_ON) 33 | except: 34 | pass 35 | 36 | 37 | 38 | def android_clearflag(): 39 | try: 40 | PythonActivity.mActivity.getWindow().clearFlags(Params.FLAG_KEEP_SCREEN_ON) 41 | except: 42 | pass 43 | 44 | 45 | 46 | try: 47 | run_on_ui_thread(android_setflag) 48 | run_on_ui_thread(android_clearflag) 49 | except: 50 | pass 51 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | 2 | def update_loading_small_text(text): 3 | from kivymd.app import MDApp 4 | 5 | app = MDApp.get_running_app() 6 | if app: 7 | app.root.ids.wait_text_small.text = text 8 | 9 | 10 | def update_waiting_texts(text="", small_text=""): 11 | from kivymd.app import MDApp 12 | app = MDApp.get_running_app() 13 | if app: 14 | app.root.ids.wait_text.text = text.upper() 15 | app.root.ids.wait_text_small.text = small_text 16 | -------------------------------------------------------------------------------- /src/assets/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/Use BIP39 instead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/Use BIP39 instead.png -------------------------------------------------------------------------------- /src/assets/btc-own-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/btc-own-node.png -------------------------------------------------------------------------------- /src/assets/dark-btc-own-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-btc-own-node.png -------------------------------------------------------------------------------- /src/assets/dark-larry_offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-larry_offline.png -------------------------------------------------------------------------------- /src/assets/dark-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-offline.png -------------------------------------------------------------------------------- /src/assets/dark-onboarding_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-onboarding_00.png -------------------------------------------------------------------------------- /src/assets/dark-onboarding_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-onboarding_10.png -------------------------------------------------------------------------------- /src/assets/dark-onboarding_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-onboarding_20.png -------------------------------------------------------------------------------- /src/assets/dark-onboarding_unlimited_wallets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-onboarding_unlimited_wallets.png -------------------------------------------------------------------------------- /src/assets/dark-online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-online.png -------------------------------------------------------------------------------- /src/assets/dark-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-playground.png -------------------------------------------------------------------------------- /src/assets/dark-strong_passphrase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-strong_passphrase.png -------------------------------------------------------------------------------- /src/assets/dark-use-bip39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-use-bip39.png -------------------------------------------------------------------------------- /src/assets/dark-what-salt-is.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-what-salt-is.png -------------------------------------------------------------------------------- /src/assets/dark-when-you-go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/dark-when-you-go.png -------------------------------------------------------------------------------- /src/assets/larry_offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/larry_offline.png -------------------------------------------------------------------------------- /src/assets/offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/offline.png -------------------------------------------------------------------------------- /src/assets/onboarding_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/onboarding_00.png -------------------------------------------------------------------------------- /src/assets/onboarding_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/onboarding_10.png -------------------------------------------------------------------------------- /src/assets/onboarding_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/onboarding_20.png -------------------------------------------------------------------------------- /src/assets/onboarding_unlimited_wallets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/onboarding_unlimited_wallets.png -------------------------------------------------------------------------------- /src/assets/online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/online.png -------------------------------------------------------------------------------- /src/assets/playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/playground.png -------------------------------------------------------------------------------- /src/assets/strong_passphrase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/strong_passphrase.png -------------------------------------------------------------------------------- /src/assets/what-salt-is.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/what-salt-is.png -------------------------------------------------------------------------------- /src/assets/when-you-go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/assets/when-you-go.png -------------------------------------------------------------------------------- /src/bip49.py: -------------------------------------------------------------------------------- 1 | from typing import Type, TypeVar, Tuple, List, Dict, Any 2 | from Crypto.Hash import SHA256 3 | 4 | from pycoin.tx.pay_to.ScriptPayToAddressWit import ScriptPayToAddressWit 5 | from pycoin.key.BIP32Node import BIP32Node 6 | from pycoin.ui import address_for_pay_to_script, standard_tx_out_script 7 | from pycoin.networks import bech32_hrp_for_netcode 8 | from pycoin.contrib import segwit_addr 9 | from pycoin.serialize import b2h_rev 10 | from pycoin.encoding import hash160 11 | 12 | 13 | class SegwitBIP32Node(BIP32Node): 14 | def bech32_p2wpkh_address(self) -> str: 15 | hrp = bech32_hrp_for_netcode(self.netcode()) 16 | witprog_version = 1 17 | p2aw_script = self.p2wpkh_script() 18 | return segwit_addr.encode(hrp, witprog_version, p2aw_script) 19 | 20 | def p2sh_p2wpkh_address(self) -> str: 21 | p2aw_script = self.p2wpkh_script() # type: bytes 22 | return address_for_pay_to_script(p2aw_script, netcode=self.netcode()) 23 | 24 | def p2wpkh_script_hash(self) -> bytes: 25 | p2aw_script = self.p2wpkh_script() # type: bytes 26 | return hash160(p2aw_script) 27 | 28 | def electrumx_script_hash(self, bech32: bool = False) -> str: 29 | addr = self.bech32_p2wpkh_address() if bech32 \ 30 | else self.p2sh_p2wpkh_address() # type: str 31 | script = standard_tx_out_script(addr) # type: bytes 32 | h = SHA256.new() 33 | h.update(script) 34 | return b2h_rev(h.digest()) 35 | 36 | def p2wpkh_script(self) -> bytes: 37 | hash160_c = self.hash160(use_uncompressed=False) # type: bytes 38 | return ScriptPayToAddressWit(b'\0', hash160_c).script() 39 | 40 | 41 | #def main(): 42 | # secret = "CORRECT HORSE BATTERY STAPLE" # type: str 43 | # mpk = SegwitBIP32Node.from_master_secret( 44 | # secret.encode("utf-8")) # type: SegwitBIP32Node 45 | # print(mpk.p2sh_p2wpkh_address()) 46 | # print(mpk.bech32_p2wpkh_address()) 47 | # 48 | 49 | #if __name__ == "__main__": 50 | # main() 51 | -------------------------------------------------------------------------------- /src/bottom_screens_address.py: -------------------------------------------------------------------------------- 1 | #from kivy.lang import Builder 2 | #screen = Builder.load_string(""" ... """) 3 | from kivymd.uix.gridlayout import MDGridLayout 4 | from kivymd.uix.label import MDLabel 5 | from kivy.core.window import Window 6 | from kivymd.uix.boxlayout import MDBoxLayout 7 | from kivymd.uix.bottomsheet import MDCustomBottomSheet 8 | from kivy_garden.qrcode import QRCodeWidget 9 | 10 | BG_COLOR = "#3E3E3C" # Battleship Gray 11 | 12 | # RGB: 132, 132, 130 - HSL: 0.17, 0.01, 0.51 13 | class AddressDetailInfo(MDGridLayout): 14 | def __init__(self, address, **var_args): 15 | super(AddressDetailInfo, self).__init__(**var_args) 16 | self.cols = 1 17 | self.col_default_width = Window.width 18 | self.col_width = Window.width 19 | self.background_color = BG_COLOR 20 | 21 | 22 | self.height = "400dp" 23 | self.width = Window.width 24 | self.size_hint_y = None 25 | #self.size_hint_x = None 26 | self.minimum_height = self.height 27 | self.padding = [ 0, "21dp", 0, 0] 28 | 29 | lbl1 = MDLabel(text ='Address') 30 | lbl1.size_hint_y = None 31 | lbl1.size_hint_x = Window.width 32 | lbl1.color: BG_COLOR 33 | lbl1.halign = "center" 34 | lbl1.font_size = '24sp' 35 | lbl1.theme_text_color = "Primary" #color: "#000000" 36 | lbl1.valign = "top" 37 | lbl1.bold = True 38 | self.add_widget(lbl1) 39 | 40 | chunk_size = 5 41 | chunked_address = [address[i:i+chunk_size] for i in range(0, len(address), chunk_size)] 42 | lbl2 = MDLabel(text = " ".join(chunked_address)) 43 | lbl2.size_hint_y = 1.65 44 | lbl2.size_hint_x = Window.width 45 | lbl2.color: BG_COLOR 46 | lbl2.halign = "center" 47 | lbl2.font_name = "RobotoMono" 48 | lbl2.valign = "top" 49 | 50 | spacer = MDBoxLayout() 51 | spacer.add_widget(lbl2) 52 | spacer.padding = ["42dp", 0, "42dp", 0] 53 | self.add_widget(spacer) 54 | 55 | # chip 56 | """ 57 | from kivymd.uix.chip.chip import MDChip 58 | chip_box = MDBoxLayout() 59 | chip_box.id = "chip_box" 60 | chip_box.adaptive_size = False 61 | chip_box.spacing = "8dp" 62 | chip_box.padding = ["21dp", "21dp", "21dp", "21dp"] 63 | chip_box.add_widget(MDChip(text="already used", icon_left='check', spacing = "8dp")) 64 | chip_box.add_widget(MDChip(text="KYC free", icon_left='check', spacing = "8dp")) 65 | chip_box.add_widget(MDChip(text="FU GREG", icon_left='check')) 66 | chip_box.add_widget(MDChip(text="AMMO FUND", icon_left='check')) 67 | chip_box.add_widget(MDChip(text="STEAK", icon_left='check')) 68 | chip_box.add_widget(MDChip(text="KYC free", icon_left='check')) 69 | 70 | self.add_widget(chip_box) 71 | """ 72 | # end chip 73 | 74 | 75 | qrcode_widget = QRCodeWidget() 76 | qrcode_widget.id = "addr_qrcode_list_btm_screen" 77 | qrcode_widget.show_border = False 78 | qrcode_widget.background_color = (0.98, 0.98, 0.98, 1) # = #fafafa 79 | qrcode_widget.data = "bitcoin:{}".format(address) 80 | 81 | spacer_btm = MDBoxLayout() 82 | spacer_btm.add_widget(qrcode_widget) 83 | spacer_btm.padding = [0, 0, 0, "42dp"] 84 | self.add_widget(spacer_btm) 85 | 86 | 87 | def open_address_bottom_sheet(address, qr=False): 88 | addr_btm_sheet = MDCustomBottomSheet(screen=AddressDetailInfo(address)) 89 | addr_btm_sheet.open() 90 | -------------------------------------------------------------------------------- /src/bottom_screens_tx.py: -------------------------------------------------------------------------------- 1 | #from kivy.lang import Builder 2 | #screen = Builder.load_string(""" ... """) 3 | from kivymd.uix.gridlayout import MDGridLayout 4 | from kivymd.uix.label import MDLabel 5 | from kivy.core.window import Window 6 | from kivymd.uix.boxlayout import MDBoxLayout 7 | from kivymd.uix.bottomsheet import MDCustomBottomSheet 8 | from kivymd.uix.button import MDRaisedButton 9 | from kivymd.uix.floatlayout import MDFloatLayout 10 | from kivymd.uix.tab import MDTabsBase 11 | from kivymd.uix.tab import MDTabs 12 | #from kivymd.uix.recycleview import MDRecycleView 13 | #from kivy.uix.recycleboxlayout import RecycleBoxLayout 14 | 15 | from kivymd.app import MDApp 16 | 17 | from kivymd.uix.scrollview import MDScrollView 18 | from kivymd.uix.list import TwoLineIconListItem 19 | from kivymd.uix.list import TwoLineListItem 20 | from kivymd.uix.list import TwoLineAvatarIconListItem 21 | from kivymd.uix.list import IconLeftWidget 22 | from kivymd.uix.list import IconRightWidget 23 | 24 | from kivymd.uix.list import MDList 25 | 26 | from kivy.properties import StringProperty 27 | #from kivy.properties import ObjectProperty 28 | 29 | class TXOTwoLineListItem(TwoLineListItem): 30 | txid = StringProperty() 31 | 32 | from kivy_utils import open_url 33 | 34 | 35 | from utils import format_txid 36 | from decimal import Decimal 37 | 38 | 39 | class BroadcastButton(MDRaisedButton): 40 | def do_broadcast_current_signed_tx(self): 41 | from asyncio import create_task as asyncio_create_task 42 | task1 = asyncio_create_task(MDApp.get_running_app().do_broadcast()) 43 | 44 | def __init__(self, **kwargs): 45 | self.on_release = self.do_broadcast_current_signed_tx 46 | return super(BroadcastButton, self).__init__(**kwargs) 47 | 48 | 49 | 50 | class ExplorerViewButton(MDRaisedButton): 51 | """ """ 52 | def do_view_txid_in_explorer(self): 53 | 54 | from nowallet import BTC as nowallet_BTC 55 | from nowallet import TBTC as nowallet_TBTC 56 | 57 | app = MDApp.get_running_app() 58 | base_url, chain = None, app.chain.chain_1209k 59 | if app.explorer == "blockcypher": 60 | base_url = "https://live.blockcypher.com/{}/tx/{}/" 61 | if app.chain == nowallet_TBTC: 62 | chain = "btc-testnet" 63 | elif app.explorer == "smartbit": 64 | base_url = "https://{}.smartbit.com.au/tx/{}/" 65 | if app.chain == nowallet_BTC: 66 | chain = "www" 67 | elif app.chain == nowallet_TBTC: 68 | chain = "testnet" 69 | elif app.explorer == "mempool.space": 70 | base_url = "https://mempool.space/{}tx/{}" 71 | if app.chain == nowallet_BTC: 72 | chain = "" 73 | elif app.chain == nowallet_TBTC: 74 | chain = "testnet/" 75 | url = base_url.format(chain, self.txid) 76 | open_url(url) 77 | 78 | def __init__(self, txid, **kwargs): 79 | self.txid = txid 80 | self.on_release = self.do_view_txid_in_explorer 81 | return super(ExplorerViewButton, self).__init__(**kwargs) 82 | 83 | 84 | 85 | 86 | 87 | class DetailTab(MDFloatLayout, MDTabsBase): 88 | pass 89 | 90 | 91 | class TxDetailInfo(MDGridLayout): 92 | def __init__(self, bg_color, tx, history, wallet, **var_args): 93 | super(TxDetailInfo, self).__init__(**var_args) 94 | in_out_sats_tx_value = 0.0 95 | 96 | #FIXME: 97 | #if wallet: 98 | # addrs = wallet.get_all_used_addresses() #TODO: FIXME: this is buggy, returns index of both (change and receive addresses) 99 | # for tx_out in signed_tx.txs_out: 100 | # if tx_out.address(netcode="XTN") in addrs: 101 | # in_out_sats_tx_value += tx_out.coin_value 102 | 103 | 104 | self.cols = 1 105 | self.col_default_width = Window.width 106 | self.col_width = Window.width 107 | 108 | self.height = "520dp" 109 | self.width = Window.width 110 | self.size_hint_y = None 111 | self.minimum_height = self.height 112 | self.md_bg_color = bg_color 113 | 114 | 115 | 116 | tabs = MDTabs() 117 | tabs.background_color = bg_color 118 | tabs.indicator_color = [0.97, 0.58, 0.10, 1.0] # #f7931a bitcoin orange 119 | 120 | 121 | overview_tab = DetailTab(title="OVERVIEW") 122 | overview_tab.background_color = bg_color 123 | if type(tx) == type(""): 124 | txid = tx # an txid was passed 125 | else: 126 | txid = tx.id() 127 | txid_short = format_txid(txid) 128 | stored_tx = wallet.history_store.get_tx(txid) 129 | 130 | overview_box = MDBoxLayout() 131 | overview_box.orientation = "vertical" 132 | overview_box.background_color = bg_color 133 | 134 | if False: 135 | lbl1 = MDLabel(text ='Amount') 136 | # lbl1.text_color: "#000000" 137 | lbl1.halign = "center" 138 | lbl1.font_size = '24sp' 139 | lbl1.theme_text_color = "Primary" #color: "#000000" 140 | lbl1.valign = "top" 141 | lbl1.bold = True 142 | overview_box.add_widget(lbl1) 143 | 144 | lbl2 = MDLabel(text = "{} sats or {} USD".format(in_out_sats_tx_value, "" )) 145 | # lbl2.text_color: "#000000" 146 | lbl2.halign = "center" 147 | #lbl2.font_name = "RobotoMono" 148 | lbl2.valign = "top" 149 | overview_box.add_widget(lbl2) 150 | 151 | #lbl3 = MDLabel(text = "{} {}".format("", "USD")) 152 | #lbl3.text_color: "#000000" 153 | #lbl3.halign = "center" 154 | #lbl3.font_name = "RobotoMono" 155 | #lbl3.valign = "top" 156 | #overview_box.add_widget(lbl3) 157 | 158 | 159 | lbl12 = MDLabel(text ='Transaction ID') 160 | # lbl12.text_color: "#000000" 161 | lbl12.halign = "center" 162 | lbl12.font_size = '24sp' 163 | lbl12.theme_text_color = "Primary" #color: "#000000" 164 | lbl12.valign = "top" 165 | lbl12.bold = True 166 | overview_box.add_widget(lbl12) 167 | 168 | lbl22 = MDLabel(text = txid_short) 169 | # lbl22.text_color: "#000000" 170 | lbl22.halign = "center" 171 | lbl22.font_name = "RobotoMono" 172 | lbl22.valign = "top" 173 | overview_box.add_widget(lbl22) 174 | 175 | if history and history.height > 0: 176 | 177 | lbl31 = MDLabel(text ='Block') 178 | # lbl31.text_color: "#000000" 179 | lbl31.halign = "center" 180 | lbl31.font_size = '24sp' 181 | lbl31.theme_text_color = "Primary" #color: "#000000" 182 | lbl31.valign = "top" 183 | lbl31.bold = True 184 | overview_box.add_widget(lbl31) 185 | 186 | lbl32 = MDLabel(text = "{}".format(history.height)) 187 | # lbl32.text_color: "#000000" 188 | lbl32.halign = "center" 189 | lbl32.valign = "top" 190 | overview_box.add_widget(lbl32) 191 | 192 | 193 | btn_box = MDBoxLayout() 194 | #btn_box.orientation = "horiziontal" 195 | btn_box.size_hint_x = 1 196 | #btn_box.size_hint_y = 0.6 197 | btn_box.spacing = 5 198 | btn_box.padding = ["42dp", 0, "42dp", "42dp"] 199 | 200 | 201 | if not history: 202 | broadcast_btn = BroadcastButton() 203 | broadcast_btn.text = "Broadcast" 204 | #broadcast_btn.size_hint_x = None 205 | broadcast_btn.size_hint_x = 0.5 206 | broadcast_btn.font_size = '18sp' 207 | # broadcast_btn.md_bg_color = "#252525" 208 | 209 | broadcast_btn_dl = MDRaisedButton(text="Download") 210 | #broadcast_btn_dl.size_hint_x = None 211 | broadcast_btn_dl.size_hint_x = 0.5 212 | broadcast_btn_dl.font_size = '18sp' 213 | # broadcast_btn_dl.md_bg_color = "#252525" 214 | 215 | # WIP: btn_box.add_widget(broadcast_btn_dl) 216 | btn_box.add_widget(broadcast_btn) 217 | overview_box.add_widget(btn_box) 218 | else: 219 | explorer_btn = ExplorerViewButton(txid) 220 | explorer_btn.text = "View on mempool.space" 221 | explorer_btn.size_hint_x = 1 222 | explorer_btn.font_size = '18sp' 223 | # explorer_btn.md_bg_color = "#252525" 224 | 225 | btn_box.add_widget(explorer_btn) 226 | overview_box.add_widget(btn_box) 227 | 228 | overview_tab.add_widget(overview_box) 229 | tabs.add_widget(overview_tab) 230 | 231 | # Tab Inputs 232 | inputs_tab = DetailTab(title="INPUTS") 233 | #inputs_tab.background_color = "#fafafa" 234 | inputs_box = MDBoxLayout() 235 | inputs_box.orientation = "vertical" 236 | 237 | 238 | scroll_input_list = MDList(id="scroll_view_list") 239 | scroll_input_view = MDScrollView(scroll_input_list) 240 | scroll_input_view.minimum_height = self.height 241 | 242 | app = MDApp.get_running_app() 243 | def sats2btc(val): 244 | return "{:.8f} {}".format(Decimal(coin_value) / 100000000, app.units) 245 | 246 | if stored_tx: 247 | for input in stored_tx.get('txs_in', []): 248 | previous_txo = input.get('previous_txo', None) 249 | if previous_txo: 250 | address = previous_txo.get('address', "(unknown)") 251 | coin_value = previous_txo.get('coin_value', "(unknown)") 252 | else: 253 | address = "(unknown)" 254 | coin_value = "(unknown)" 255 | if coin_value != "(unknown)": 256 | coin_value = sats2btc(coin_value) 257 | previous_hash = input.get('previous_hash', None) 258 | previous_index = input.get('previous_index', "(unknown)") 259 | scroll_input_list.add_widget( 260 | #TwoLineIconListItem( 261 | # IconLeftWidget( 262 | # icon="arrow-right-circle-outline" # = UTXO was spent (empty) 263 | # ), 264 | TwoLineListItem( 265 | text = coin_value, 266 | secondary_text = address, 267 | tertiary_text = "{}:{}".format( 268 | format_txid(previous_hash), 269 | previous_index), 270 | #on_release = app.open_txi_menu_items 271 | ) 272 | ) 273 | inputs_box.add_widget(scroll_input_view) 274 | 275 | 276 | 277 | inputs_tab.add_widget(inputs_box) 278 | tabs.add_widget(inputs_tab) 279 | 280 | # Tab Outputs 281 | outputs_tab = DetailTab(title="OUTPUTS") 282 | outputs_box = MDBoxLayout() 283 | outputs_box.orientation = "vertical" 284 | 285 | scroll_output_list = MDList(id="scroll_output_list") 286 | scroll_output_view = MDScrollView(scroll_output_list) 287 | scroll_output_view.minimum_height = self.height 288 | 289 | if stored_tx: 290 | for tx_out in stored_tx.get('txs_out', []): 291 | coin_value = tx_out.get('coin_value', "(unknown)") 292 | if coin_value != "(unknown)": 293 | coin_value = sats2btc(coin_value) 294 | 295 | scroll_output_list.add_widget( 296 | #TwoLineAvatarIconListItem( 297 | TXOTwoLineListItem( 298 | text=coin_value, 299 | secondary_text=str(tx_out.get('address', "(unknown)")), 300 | txid=txid, 301 | on_release = app.open_txo_menu_items 302 | ) 303 | ) 304 | 305 | outputs_box.add_widget(scroll_output_view) 306 | outputs_tab.add_widget(outputs_box) 307 | tabs.add_widget(outputs_tab) 308 | 309 | # # Tab Fee 310 | # fee_tab = DetailTab(title="Miner Fee") 311 | # fee_box = MDBoxLayout() 312 | # fee_box.orientation = "vertical" 313 | # try: 314 | # fee = signed_tx.fee() 315 | # except Exception as ex: 316 | # fee = str("(unknown)") 317 | # print (ex) 318 | # fee_lbl = MDLabel(text = "{}".format(fee)) 319 | # 320 | # fee_lbl.halign = "center" 321 | # fee_lbl.valign = "top" 322 | # fee_box.add_widget(fee_lbl) 323 | # 324 | # fee_tab.add_widget(fee_box) 325 | # tabs.add_widget(fee_tab) 326 | # 327 | # # Raw hex 328 | # raw_tx_hex_tab = DetailTab(title="Raw Transaction") 329 | # raw_tx_hex_box = MDBoxLayout() 330 | # raw_tx_hex_box.orientation = "vertical" 331 | # 332 | # hextx = stored_tx.get('stored_tx', None) 333 | # if hextx: 334 | # hextxtext = "{}...{}".format(hextx[:10], hextx[-10:]) 335 | # else: 336 | # hextxtext = "(unknown)" 337 | # 338 | # raw_tx_hex_lbl = MDLabel(text = hextxtext) 339 | # 340 | # raw_tx_hex_lbl.halign = "center" 341 | # raw_tx_hex_lbl.font_name = "RobotoMono" 342 | # raw_tx_hex_lbl.valign = "top" 343 | # raw_tx_hex_box.add_widget(raw_tx_hex_lbl) 344 | # 345 | # raw_tx_hex_tab.add_widget(raw_tx_hex_box) 346 | # tabs.add_widget(raw_tx_hex_tab) 347 | 348 | self.add_widget(tabs) 349 | 350 | 351 | 352 | def open_tx_preview_bottom_sheet(bg_color, tx, history, wallet): 353 | screen_box = MDBoxLayout() 354 | screen_box.orientation = "vertical" 355 | screen_box.size_hint_y = None 356 | screen_box.add_widget(TxDetailInfo(bg_color, tx, history, wallet)) 357 | 358 | tx_btm_sheet = MDCustomBottomSheet(screen=screen_box) 359 | tx_btm_sheet.open() 360 | return tx_btm_sheet 361 | -------------------------------------------------------------------------------- /src/brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/brain.png -------------------------------------------------------------------------------- /src/brain_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/brain_icon.png -------------------------------------------------------------------------------- /src/brain_nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-Brainbow/Brainbow/8847d349f60b2de93f88220a7cad578e2755a240/src/brain_nav.png -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/CallbackWrapper.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import androidx.camera.core.ImageProxy; 4 | import android.graphics.Rect; 5 | import android.util.Size; 6 | import java.util.Dictionary; 7 | 8 | public interface CallbackWrapper { 9 | public void callback_string(String filepath); 10 | public void callback_image(ImageProxy image); 11 | public void callback_config(Rect croprect, Size resolution, int rotation); 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/CameraX.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | //# General 4 | import java.io.File; 5 | import java.util.concurrent.Executor; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.ExecutionException; 8 | 9 | import com.google.common.util.concurrent.ListenableFuture; 10 | 11 | import org.kivy.android.PythonActivity; 12 | 13 | import android.app.Activity; 14 | import android.util.Size; 15 | import android.util.Rational; 16 | import android.graphics.Rect; 17 | import android.graphics.SurfaceTexture; 18 | import android.content.Context; 19 | import android.content.ContentResolver; 20 | import android.content.ContentValues; 21 | import android.net.Uri; 22 | import android.provider.MediaStore; 23 | 24 | //MediaActionSound = import android.media.MediaActionSound; 25 | 26 | import androidx.core.content.ContextCompat; 27 | import androidx.camera.core.Camera; 28 | import androidx.camera.core.CameraState; 29 | import androidx.camera.core.Preview; 30 | import androidx.camera.core.AspectRatio; 31 | import androidx.camera.core.ImageCapture; 32 | import androidx.camera.core.VideoCapture; 33 | import androidx.camera.core.FocusMeteringAction; 34 | import androidx.camera.core.MeteringPoint; 35 | import androidx.camera.core.SurfaceOrientedMeteringPointFactory; 36 | import androidx.camera.core.ImageAnalysis; 37 | import androidx.camera.lifecycle.ProcessCameraProvider; 38 | import androidx.camera.core.UseCaseGroup; 39 | import androidx.camera.core.CameraSelector; 40 | import androidx.camera.core.ZoomState; 41 | import androidx.camera.core.ViewPort; 42 | import androidx.lifecycle.ProcessLifecycleOwner; 43 | import androidx.lifecycle.LifecycleOwner; 44 | 45 | // Local Java 46 | import org.kivy.camerax.ImageSavedCallback; 47 | import org.kivy.camerax.VideoSavedCallback; 48 | import org.kivy.camerax.ImageAnalysisAnalyzer; 49 | import org.kivy.camerax.KivySurfaceProvider; 50 | import org.kivy.camerax.CallbackWrapper; 51 | 52 | class CameraX { 53 | 54 | // Initial State 55 | private boolean photo; 56 | private boolean video; 57 | private boolean analysis; 58 | private int lensFacing; 59 | private int [] cameraResolution; 60 | private int aspectRatio; 61 | private CallbackWrapper callbackClass; 62 | private int flashMode; 63 | private int imageOptimize; 64 | private float zoomScaleFront; 65 | private float zoomScaleBack; 66 | private int dataFormat; 67 | 68 | // Connect State 69 | private int viewPortWidth; 70 | private int viewPortHeight; 71 | private Rect cropRect; 72 | private Size resolution; 73 | private int rotation; 74 | 75 | // Persistent run time State 76 | // executor may persist after disconnect for capture to complete 77 | public static Executor executor = Executors.newSingleThreadExecutor(); 78 | 79 | // Run time State 80 | private UseCaseGroup useCaseGroup = null; 81 | private Preview preview = null; 82 | private ProcessCameraProvider cameraProvider = null; 83 | private ImageCapture imageCapture = null; 84 | private VideoCapture videoCapture = null; 85 | private Camera camera = null; 86 | private KivySurfaceProvider kivySurfaceProvider = null; 87 | private boolean videoIsRecording = false; 88 | private boolean selectingCamera = false; 89 | private boolean imageIsReady = false; 90 | 91 | public CameraX(boolean photo, 92 | boolean video, 93 | boolean analysis, 94 | String facing, 95 | int[] resolution, 96 | String aspect_ratio, 97 | CallbackWrapper callback_class, 98 | String flash, 99 | String optimize, 100 | float zoom_scale, 101 | String data_format) { 102 | 103 | this.photo = photo; 104 | this.video = video; 105 | this.analysis = analysis; 106 | if (this.analysis == true) { 107 | this.video = false; 108 | } 109 | if (facing.equals("front")) { 110 | this.lensFacing = CameraSelector.LENS_FACING_FRONT; 111 | } else { 112 | this.lensFacing = CameraSelector.LENS_FACING_BACK; 113 | } 114 | cameraResolution = resolution; 115 | 116 | if (aspect_ratio.equals("16:9")) { 117 | this.aspectRatio = AspectRatio.RATIO_16_9; 118 | } else { 119 | this.aspectRatio = AspectRatio.RATIO_4_3; 120 | } 121 | this.callbackClass = callback_class; 122 | 123 | if (flash.equals("on")) { 124 | this.flashMode = ImageCapture.FLASH_MODE_ON; 125 | } else if (flash.equals("auto")) { 126 | this.flashMode = ImageCapture.FLASH_MODE_AUTO; 127 | } else { 128 | this.flashMode = ImageCapture.FLASH_MODE_OFF; 129 | } 130 | 131 | if (optimize.equals("quality")) { 132 | this.imageOptimize = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY; 133 | } else { 134 | this.imageOptimize = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY; 135 | } 136 | 137 | this.zoomScaleFront = zoom_scale; 138 | this.zoomScaleBack = zoom_scale; 139 | 140 | if (data_format.equals("rgba")) { 141 | this.dataFormat = ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888; 142 | } else { 143 | this.dataFormat = ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888; 144 | } 145 | } 146 | 147 | //############################## 148 | //# Android CameraX 149 | //############################## 150 | 151 | public void setViewPort(int []view_port_size) { 152 | this.viewPortWidth = view_port_size[0]; 153 | this.viewPortHeight = view_port_size[1]; 154 | } 155 | 156 | public void startCamera() { 157 | Context context = PythonActivity.mActivity.getApplicationContext(); 158 | 159 | final ListenableFuture cameraProviderFuture = 160 | ProcessCameraProvider.getInstance(context); 161 | 162 | cameraProviderFuture.addListener(new Runnable() { 163 | @Override 164 | public void run() { 165 | try { 166 | ProcessCameraProvider cameraProvider = 167 | cameraProviderFuture.get(); 168 | CameraX.this.cameraProvider = cameraProvider; 169 | configureCamera(); 170 | } catch (ExecutionException | InterruptedException e) { 171 | // No errors need to be handled for this Future. 172 | // This should never be reached. 173 | } 174 | } 175 | }, ContextCompat.getMainExecutor(context)); 176 | } 177 | 178 | public void configureCameraMainThread() { 179 | Context context = PythonActivity.mActivity.getApplicationContext(); 180 | Executor executor = ContextCompat.getMainExecutor(context); 181 | executor.execute(new Runnable() { 182 | @Override 183 | public void run() { configureCamera(); } 184 | }); 185 | } 186 | 187 | private void configureCamera() { 188 | Activity mActivity = PythonActivity.mActivity; 189 | int rotation = 190 | mActivity.getWindowManager().getDefaultDisplay().getRotation(); 191 | 192 | // ImageAnalysis 193 | ImageAnalysis imageAnalysis = null; 194 | if (this.analysis) { 195 | int strategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST; 196 | ImageAnalysis.Builder ib = new ImageAnalysis.Builder(); 197 | if (cameraResolution.length != 0) { 198 | ib.setTargetResolution(new Size(cameraResolution[0], 199 | cameraResolution[1])); 200 | } else { 201 | ib.setTargetAspectRatio(this.aspectRatio); 202 | } 203 | ib.setOutputImageFormat(this.dataFormat); 204 | ib.setBackpressureStrategy(strategy); 205 | ib.setTargetRotation(rotation); 206 | imageAnalysis = ib.build(); 207 | ImageAnalysisAnalyzer iaa = 208 | new ImageAnalysisAnalyzer(this.callbackClass); 209 | imageAnalysis.setAnalyzer(executor, iaa); 210 | } 211 | 212 | // ImageCapture 213 | if (this.video) { 214 | VideoCapture.Builder cb = new VideoCapture.Builder(); 215 | if (cameraResolution.length != 0) { 216 | cb.setTargetResolution(new Size(cameraResolution[0], 217 | cameraResolution[1])); 218 | } else { 219 | cb.setTargetAspectRatio(this.aspectRatio); 220 | } 221 | cb.setTargetRotation(rotation); 222 | this.videoCapture = cb.build(); 223 | } 224 | 225 | if (this.photo) { 226 | ImageCapture.Builder cb = new ImageCapture.Builder(); 227 | cb.setFlashMode(this.flashMode); 228 | cb.setCaptureMode(this.imageOptimize); 229 | if (cameraResolution.length != 0) { 230 | cb.setTargetResolution(new Size(cameraResolution[0], 231 | cameraResolution[1])); 232 | } else { 233 | cb.setTargetAspectRatio(this.aspectRatio); 234 | } 235 | cb.setTargetRotation(rotation); 236 | this.imageCapture = cb.build(); 237 | } 238 | 239 | // Preview 240 | int aspect = this.aspectRatio; 241 | if (cameraResolution.length != 0) { 242 | if ((Math.max(cameraResolution[0],cameraResolution[1]) / 243 | Math.min(cameraResolution[0],cameraResolution[1]) > 1.5)) { 244 | aspect = AspectRatio.RATIO_16_9; 245 | } else { 246 | aspect = AspectRatio.RATIO_4_3; 247 | } 248 | } 249 | this.preview = new Preview.Builder() 250 | .setTargetAspectRatio(aspect) 251 | .build(); 252 | 253 | // ViewPort 254 | Rational vpAspect = new Rational(this.viewPortWidth, 255 | this.viewPortHeight); 256 | ViewPort viewPort = new ViewPort.Builder(vpAspect, rotation).build(); 257 | 258 | // UseCaseGroup 259 | UseCaseGroup.Builder ucgb = new UseCaseGroup.Builder(); 260 | ucgb.setViewPort(viewPort); 261 | ucgb.addUseCase(this.preview); 262 | if (this.video) { 263 | ucgb.addUseCase(this.videoCapture); 264 | } 265 | if (this.photo) { 266 | ucgb.addUseCase(this.imageCapture); 267 | } 268 | if (this.analysis) { 269 | ucgb.addUseCase(imageAnalysis); 270 | } 271 | this.useCaseGroup = ucgb.build(); 272 | 273 | bindPreview(); 274 | } 275 | 276 | private void bindPreview() { 277 | // CameraSelector 278 | CameraSelector cameraSelector; 279 | if (this.lensFacing == CameraSelector.LENS_FACING_BACK) { 280 | cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA; 281 | } else { 282 | cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA; 283 | } 284 | 285 | // Bind 286 | this.cameraProvider.unbindAll(); 287 | LifecycleOwner plo = ProcessLifecycleOwner.get(); 288 | this.camera = this.cameraProvider.bindToLifecycle(plo, 289 | cameraSelector, 290 | this.useCaseGroup); 291 | 292 | // Set touch state 293 | float zoom = zoomScaleFront; 294 | if (this.lensFacing == CameraSelector.LENS_FACING_BACK) { 295 | zoom = zoomScaleBack; 296 | } 297 | this.camera.getCameraControl().setLinearZoom(zoom); 298 | focus((float)0.5,(float)0.5); 299 | 300 | this.cropRect = this.preview.getResolutionInfo().getCropRect(); 301 | this.resolution = this.preview.getResolutionInfo().getResolution(); 302 | this.rotation = this.preview.getResolutionInfo().getRotationDegrees(); 303 | 304 | this.callbackClass.callback_config(this.cropRect, 305 | this.resolution, 306 | this.rotation); 307 | 308 | } 309 | 310 | public boolean imageReady() { 311 | if (this.imageIsReady) { 312 | if (this.kivySurfaceProvider != null) { 313 | kivySurfaceProvider.KivySurfaceTextureUpdate(); 314 | } 315 | return true; 316 | } 317 | return false; 318 | } 319 | 320 | public void setTexture(int texture_id, int[] size) { 321 | Context context = PythonActivity.mActivity.getApplicationContext(); 322 | Executor mainExecutor = ContextCompat.getMainExecutor(context); 323 | 324 | this.kivySurfaceProvider = new KivySurfaceProvider(texture_id, 325 | mainExecutor, 326 | size[0], 327 | size[1]); 328 | this.imageIsReady = false; 329 | this.kivySurfaceProvider.surfaceTexture.setOnFrameAvailableListener( 330 | new SurfaceTexture.OnFrameAvailableListener() { 331 | @Override 332 | public void onFrameAvailable(final SurfaceTexture surfaceTexture) { 333 | CameraX.this.imageIsReady = true; 334 | } 335 | }); 336 | } 337 | 338 | public void setSurfaceProvider(boolean enable) { 339 | if (enable) { 340 | this.preview.setSurfaceProvider(this.kivySurfaceProvider); 341 | } else { 342 | this.preview.setSurfaceProvider(null); 343 | } 344 | } 345 | 346 | public void unbind_camera() { 347 | if (this.cameraProvider != null) { 348 | this.cameraProvider.unbindAll(); 349 | this.cameraProvider = null; 350 | this.useCaseGroup = null; 351 | this.preview = null; 352 | this.camera = null; 353 | this.imageCapture = null; 354 | this.videoCapture = null; 355 | this.kivySurfaceProvider = null; 356 | this.videoIsRecording = false; 357 | this.selectingCamera = false; 358 | } 359 | } 360 | 361 | //############################## 362 | //# User Events 363 | //############################## 364 | 365 | public void select_camera(String facing) { 366 | if (!this.selectingCamera) { 367 | this.selectingCamera = true; 368 | this.lensFacing = CameraSelector.LENS_FACING_BACK; 369 | if (facing.equals("front")) { 370 | this.lensFacing = CameraSelector.LENS_FACING_FRONT; 371 | } 372 | bindPreview(); 373 | this.selectingCamera = false; 374 | } 375 | } 376 | 377 | 378 | public void focus(float x, float y) { 379 | SurfaceOrientedMeteringPointFactory factory = 380 | new SurfaceOrientedMeteringPointFactory((float)1.0,(float)1.0); 381 | MeteringPoint point = factory.createPoint(x / this.viewPortWidth, 382 | y / this.viewPortHeight); 383 | FocusMeteringAction action = 384 | new FocusMeteringAction.Builder(point).build(); 385 | if (this.camera != null) { 386 | this.camera.getCameraControl().startFocusAndMetering(action); 387 | } 388 | } 389 | 390 | public void zoom(float scale, boolean absolute) { 391 | if (this.camera != null) { 392 | ZoomState zs = 393 | this.camera.getCameraInfo().getZoomState().getValue(); 394 | float newScale = scale; 395 | if (absolute == false) { 396 | newScale = zs.getZoomRatio() * scale; 397 | } 398 | newScale = Math.min(newScale,zs.getMaxZoomRatio()); 399 | newScale = Math.max(newScale,zs.getMinZoomRatio()); 400 | this.camera.getCameraControl().setZoomRatio(newScale); 401 | zs = this.camera.getCameraInfo().getZoomState().getValue(); 402 | if (this.lensFacing == CameraSelector.LENS_FACING_BACK) { 403 | this.zoomScaleBack = zs.getLinearZoom(); 404 | } else { 405 | this.zoomScaleFront = zs.getLinearZoom(); 406 | } 407 | } 408 | } 409 | 410 | public String flash(String mode) { 411 | if (this.photo) { 412 | this.flashMode = ImageCapture.FLASH_MODE_OFF; 413 | if (mode.equals("on")) { 414 | this.flashMode = ImageCapture.FLASH_MODE_ON; 415 | } else if (mode == "auto") { 416 | this.flashMode = ImageCapture.FLASH_MODE_AUTO; 417 | } 418 | this.imageCapture.setFlashMode(this.flashMode); 419 | } 420 | return mode; 421 | } 422 | 423 | public void stop_capture_video() { 424 | if (this.video && this.videoIsRecording) { 425 | this.videoCapture.stopRecording(); 426 | this.videoIsRecording = false; 427 | } 428 | } 429 | 430 | public void capture_video(String location, String filename, 431 | boolean fileStorage) { 432 | if (this.video && !this.videoIsRecording) { 433 | this.videoIsRecording = true; 434 | VideoCapture.OutputFileOptions vcf; 435 | if (fileStorage) { 436 | String filePath = location + "/" + filename; 437 | File videoFile = new File(filePath); 438 | vcf = new VideoCapture.OutputFileOptions.Builder(videoFile) 439 | .build(); 440 | } else { 441 | ContentResolver cr = 442 | PythonActivity.mActivity.getContentResolver(); 443 | Uri collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 444 | ContentValues cv = new ContentValues(); 445 | cv.put(MediaStore.MediaColumns.DISPLAY_NAME, filename); 446 | cv.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4"); 447 | cv.put(MediaStore.MediaColumns.RELATIVE_PATH, location); 448 | vcf = new VideoCapture.OutputFileOptions.Builder(cr, 449 | collection, 450 | cv).build(); 451 | } 452 | VideoSavedCallback vsc = new VideoSavedCallback(this.callbackClass); 453 | this.videoCapture.startRecording(vcf,executor,vsc); 454 | } 455 | } 456 | 457 | public void capture_photo(String location, String filename, 458 | boolean fileStorage) { 459 | if (this.photo) { 460 | ImageCapture.OutputFileOptions icf; 461 | if (fileStorage) { 462 | String filePath = location + "/" + filename; 463 | File photoFile = new File(filePath); 464 | icf = new ImageCapture.OutputFileOptions.Builder(photoFile) 465 | .build(); 466 | } else { 467 | ContentResolver cr = PythonActivity.mActivity.getContentResolver(); 468 | Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 469 | ContentValues cv = new ContentValues(); 470 | cv.put(MediaStore.MediaColumns.DISPLAY_NAME, filename); 471 | cv.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); 472 | cv.put(MediaStore.MediaColumns.RELATIVE_PATH, location); 473 | icf = new ImageCapture.OutputFileOptions.Builder(cr, 474 | collection, 475 | cv).build(); 476 | } 477 | ImageSavedCallback isc = new ImageSavedCallback(this.callbackClass); 478 | this.imageCapture.takePicture(icf,executor,isc); 479 | } 480 | } 481 | } 482 | 483 | 484 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/ImageAnalysisAnalyzer.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import androidx.camera.core.ImageAnalysis; 4 | import androidx.camera.core.ImageProxy; 5 | import org.kivy.camerax.CallbackWrapper; 6 | 7 | public class ImageAnalysisAnalyzer implements ImageAnalysis.Analyzer { 8 | 9 | private CallbackWrapper callback_wrapper; 10 | 11 | public ImageAnalysisAnalyzer(CallbackWrapper callback_wrapper) { 12 | this.callback_wrapper = callback_wrapper; 13 | } 14 | 15 | public void analyze(ImageProxy image) { 16 | this.callback_wrapper.callback_image(image); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/ImageProxyOps.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import androidx.camera.core.ImageProxy; 4 | import android.graphics.ImageFormat; 5 | import android.graphics.PixelFormat; 6 | import java.lang.IllegalArgumentException; 7 | import java.nio.ByteBuffer; 8 | 9 | public class ImageProxyOps { 10 | 11 | private byte[] bytes; 12 | 13 | public ImageProxyOps() { 14 | this.bytes = new byte[0]; 15 | } 16 | 17 | public byte[] copyYUVtoBytes(ImageProxy image) { 18 | if (image.getFormat() != ImageFormat.YUV_420_888) { 19 | throw new IllegalArgumentException("Invalid image format"); 20 | } 21 | 22 | ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); 23 | ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); 24 | ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); 25 | 26 | int ySize = yBuffer.remaining(); 27 | int uSize = uBuffer.remaining(); 28 | int vSize = vBuffer.remaining(); 29 | 30 | if (this.bytes.length != ySize + uSize + vSize) { 31 | this.bytes = new byte[ySize + uSize + vSize]; 32 | } 33 | 34 | // U and V are swapped 35 | yBuffer.get(this.bytes, 0, ySize); 36 | vBuffer.get(this.bytes, ySize, vSize); 37 | uBuffer.get(this.bytes, ySize + vSize, uSize); 38 | 39 | return this.bytes; 40 | } 41 | 42 | public byte[] copyRGBAtoBytes(ImageProxy image) { 43 | if (image.getFormat() != PixelFormat.RGBA_8888) { 44 | throw new IllegalArgumentException("Invalid image format"); 45 | } 46 | 47 | // RGBA bytes are in plane zero 48 | ByteBuffer buffer = image.getPlanes()[0].getBuffer(); 49 | 50 | int size = buffer.remaining(); 51 | 52 | if (this.bytes.length != size ) { 53 | this.bytes = new byte[size]; 54 | } 55 | 56 | buffer.get(this.bytes, 0, size); 57 | 58 | return this.bytes; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/ImageSavedCallback.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import androidx.camera.core.ImageCapture.OnImageSavedCallback; 4 | import androidx.camera.core.ImageCapture.OutputFileResults; 5 | import androidx.camera.core.ImageCaptureException; 6 | import android.net.Uri; 7 | import android.content.Context; 8 | import android.database.Cursor; 9 | import android.provider.MediaStore.MediaColumns; 10 | import org.kivy.camerax.CallbackWrapper; 11 | import org.kivy.android.PythonActivity; 12 | 13 | public class ImageSavedCallback implements OnImageSavedCallback { 14 | 15 | private CallbackWrapper callback_wrapper; 16 | 17 | public ImageSavedCallback(CallbackWrapper callback_wrapper) { 18 | this.callback_wrapper = callback_wrapper; 19 | } 20 | 21 | public void onImageSaved(OutputFileResults outputFileResults){ 22 | Uri saveuri = outputFileResults.getSavedUri(); 23 | String result = ""; 24 | if (saveuri != null) { 25 | if (saveuri.getScheme().equals("content")) { 26 | Context context = 27 | PythonActivity.mActivity.getApplicationContext(); 28 | Cursor cursor = 29 | context.getContentResolver().query(saveuri, null, 30 | null, null, null); 31 | String dn = MediaColumns.DISPLAY_NAME; 32 | String rp = MediaColumns.RELATIVE_PATH; 33 | int nameIndex = cursor.getColumnIndex(dn); 34 | int pathIndex = cursor.getColumnIndex(rp); 35 | cursor.moveToFirst(); 36 | String file_name = cursor.getString(nameIndex); 37 | String file_path = cursor.getString(pathIndex); 38 | cursor.close(); 39 | result = file_path + file_name; 40 | } 41 | } 42 | this.callback_wrapper.callback_string(result); 43 | } 44 | 45 | public void onError(ImageCaptureException exception) { 46 | //int id = exception.getImageCaptureError(); 47 | String msg = "Image Capture terminated by early camera disconnect."; 48 | this.callback_wrapper.callback_string(msg); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/KivySurfaceProvider.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import java.util.concurrent.Executor; 4 | import androidx.camera.core.Preview; 5 | import androidx.camera.core.SurfaceRequest; 6 | import android.graphics.SurfaceTexture; 7 | import android.view.Surface; 8 | 9 | // Ref https://developer.android.com/reference/androidx/camera/core/Preview.SurfaceProvider?hl=zh-cn#onSurfaceRequested(androidx.camera.core.SurfaceRequest) 10 | 11 | class KivySurfaceProvider implements Preview.SurfaceProvider { 12 | // This executor must have also been used with Preview.setSurfaceProvider() 13 | // to ensure onSurfaceRequested() is called on our GL thread. 14 | Executor mGlExecutor; 15 | Surface surface; 16 | public SurfaceTexture surfaceTexture; 17 | 18 | public KivySurfaceProvider(int id, Executor self_te, 19 | int width, int height) { 20 | surfaceTexture = new SurfaceTexture(id); 21 | surfaceTexture.setDefaultBufferSize(width, height); 22 | 23 | surface = new Surface(surfaceTexture); 24 | mGlExecutor = self_te; 25 | } 26 | 27 | public void KivySurfaceTextureUpdate() { 28 | surfaceTexture.updateTexImage(); 29 | } 30 | 31 | @Override 32 | public void onSurfaceRequested(SurfaceRequest request) { 33 | request.provideSurface(surface, mGlExecutor, (result) -> {}); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/camerax_provider/camerax_src/org/kivy/camerax/VideoSavedCallback.java: -------------------------------------------------------------------------------- 1 | package org.kivy.camerax; 2 | 3 | import androidx.camera.core.VideoCapture.OnVideoSavedCallback; 4 | import androidx.camera.core.VideoCapture.OutputFileResults; 5 | import android.net.Uri; 6 | import android.content.Context; 7 | import android.database.Cursor; 8 | import android.provider.MediaStore.MediaColumns; 9 | import org.kivy.camerax.CallbackWrapper; 10 | import org.kivy.android.PythonActivity; 11 | 12 | public class VideoSavedCallback implements OnVideoSavedCallback { 13 | 14 | private CallbackWrapper callback_wrapper; 15 | 16 | public VideoSavedCallback(CallbackWrapper callback_wrapper) { 17 | this.callback_wrapper = callback_wrapper; 18 | } 19 | 20 | public void onVideoSaved(OutputFileResults outputFileResults){ 21 | Uri saveuri = outputFileResults.getSavedUri(); 22 | String result = ""; 23 | if (saveuri != null) { 24 | if (saveuri.getScheme().equals("content")) { 25 | Context context = 26 | PythonActivity.mActivity.getApplicationContext(); 27 | Cursor cursor = 28 | context.getContentResolver().query(saveuri, null, 29 | null, null, null); 30 | String dn = MediaColumns.DISPLAY_NAME; 31 | String rp = MediaColumns.RELATIVE_PATH; 32 | int nameIndex = cursor.getColumnIndex(dn); 33 | int pathIndex = cursor.getColumnIndex(rp); 34 | cursor.moveToFirst(); 35 | String file_name = cursor.getString(nameIndex); 36 | String file_path = cursor.getString(pathIndex); 37 | cursor.close(); 38 | result = file_path + file_name; 39 | } 40 | } 41 | this.callback_wrapper.callback_string(result); 42 | } 43 | 44 | public void onError(int videoCaptureError, String message, Throwable cause){ 45 | String msg = "Video Capture terminated by early camera disconnect."; 46 | this.callback_wrapper.callback_string(msg); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/camerax_provider/gradle_options.py: -------------------------------------------------------------------------------- 1 | # 2 | # Add gradle options for CameraX 3 | # 4 | from pythonforandroid.recipe import info 5 | from os.path import dirname, join, exists 6 | 7 | def before_apk_build(toolchain): 8 | unprocessed_args = toolchain.args.unknown_args 9 | 10 | if '--enable-androidx' not in unprocessed_args: 11 | unprocessed_args.append('--enable-androidx') 12 | info('Camerax Provider: Add android.enable_androidx = True') 13 | 14 | if 'CAMERA' not in unprocessed_args: 15 | unprocessed_args.append('--permission') 16 | unprocessed_args.append('CAMERA') 17 | info('Camerax Provider: Add android.permissions = CAMERA') 18 | 19 | if 'RECORD_AUDIO' not in unprocessed_args: 20 | unprocessed_args.append('--permission') 21 | unprocessed_args.append('RECORD_AUDIO') 22 | info('Camerax Provider: Add android.permissions = RECORD_AUDIO') 23 | 24 | # Check the current versions of these camera Gradle dependencies here: 25 | #https://developer.android.com/jetpack/androidx/releases/camera#dependencies 26 | # and the other packages at https://mvnrepository.com/ 27 | required_depends = ['androidx.camera:camera-core:1.1.0-beta01', 28 | 'androidx.camera:camera-camera2:1.1.0-beta01', 29 | 'androidx.camera:camera-lifecycle:1.1.0-beta01', 30 | 'androidx.lifecycle:lifecycle-process:2.4.0', 31 | 'androidx.core:core:1.6.0'] 32 | existing_depends = [] 33 | read_next = False 34 | for ua in unprocessed_args: 35 | if read_next: 36 | existing_depends.append(ua) 37 | read_next = False 38 | if ua == '--depend': 39 | read_next = True 40 | 41 | message = False 42 | for rd in required_depends: 43 | name, version = rd.rsplit(':',1) 44 | found = False 45 | for ed in existing_depends: 46 | if name in ed: 47 | found = True 48 | break 49 | if not found: 50 | unprocessed_args.append('--depend') 51 | unprocessed_args.append('{}:{}'.format(name,version)) 52 | message = True 53 | if message: 54 | info('Camerax Provider: Add android.gradle_dependencies reqired ' +\ 55 | 'for CameraX') 56 | 57 | # Add the Java source 58 | camerax_java = join(dirname(__file__), 'camerax_src') 59 | if exists(camerax_java): 60 | unprocessed_args.append('--add-source') 61 | unprocessed_args.append(camerax_java) 62 | info('Camerax Provider: Add android.add_src = ' +\ 63 | './camerax_provider/camerax_src') 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/connection.py: -------------------------------------------------------------------------------- 1 | from logger import logging 2 | 3 | import asyncio 4 | from typing import ( 5 | Tuple, List, Any, Callable, Awaitable, Dict 6 | ) 7 | 8 | from connectrum.client import StratumClient 9 | from connectrum.svr_info import ServerInfo 10 | from connectrum import ElectrumErrorResponse 11 | 12 | 13 | 14 | class Connection: 15 | """ Connection object. 16 | Connects to an Electrum server, and handles all Stratum protocol messages. 17 | """ 18 | # pylint: disable=E1111 19 | def __init__(self, 20 | loop: asyncio.AbstractEventLoop, 21 | server: str, 22 | port: int, 23 | proto: str, 24 | disconnect_callback=None) -> None: 25 | """ Connection object constructor. 26 | 27 | :param loop: an asyncio event loop 28 | :param server: a string containing a hostname 29 | :param port: port number that the server listens on 30 | :returns: A new Connection object 31 | """ 32 | logging.info("Connecting...") 33 | self.disconnect_callback = disconnect_callback 34 | self.server_info = ServerInfo(server, hostname=server, ports=port) # type: ServerInfo 35 | 36 | #for f in ['nickname', 'hostname', 'ports', 'version', 'pruning_limit' ]: 37 | # print("{}: {}".format(f, self.server_info.get(f))) 38 | 39 | logging.info(str(self.server_info.get_port(proto))) 40 | 41 | self.client = StratumClient(loop) # type: StratumClient 42 | self.connection = self.client.connect( 43 | self.server_info, 44 | proto_code=proto, 45 | use_tor=True, 46 | disable_cert_verify=(proto != "s"), 47 | disconnect_callback=self.disconnect_callback 48 | ) # type: asyncio.Future 49 | 50 | self.queue = None # type: asyncio.Queue 51 | 52 | self.methods = { 53 | "get": "blockchain.transaction.get", 54 | "get_balance": "blockchain.scripthash.get_balance", 55 | "listunspent": "blockchain.scripthash.listunspent", 56 | "get_history": "blockchain.scripthash.get_history", 57 | "get_header": "blockchain.block.header", # was "get_header" previously, removed in favor of header in electrum 58 | "subscribe": "blockchain.scripthash.subscribe", 59 | "subscribe_headers": "blockchain.headers.subscribe", 60 | "estimatefee": "blockchain.estimatefee", 61 | "relayfee": "blockchain.relayfee", 62 | "broadcast": "blockchain.transaction.broadcast" 63 | } # type: Dict[str, str] 64 | 65 | async def do_connect(self) -> None: 66 | """ Coroutine. Establishes a persistent connection to an Electrum server. 67 | Awaits the connection because AFAIK an init method can't be async. 68 | """ 69 | await self.connection 70 | logging.info("Connected to server") 71 | 72 | async def listen_rpc(self, method: str, args: List = None) -> Any: 73 | """ Coroutine. Sends a normal RPC message to the server and awaits response. 74 | 75 | :param method: The Electrum API method to use 76 | :param args: Params associated with current method 77 | :returns: Future. Response from server for this method(args) 78 | """ 79 | res = None 80 | method = self.methods.get(method, method) # lookup shortcuts or use "method" instead 81 | try: 82 | if args is None: 83 | res = await self.client.RPC(method) 84 | else: 85 | res = await self.client.RPC(method, *args) 86 | except Exception as ex: 87 | print("listen_rpc Exception {}".format(ex)) 88 | if method.endswith("broadcast"): 89 | return ex 90 | return res 91 | 92 | def listen_subscribe(self, method: str, args: List) -> None: 93 | """ Sends a "subscribe" message to the server and adds to the queue. 94 | Throws away the immediate future containing the "history" hash. 95 | 96 | :param method: The Electrum API method to use 97 | :param args: Params associated with current method 98 | """ 99 | method = self.methods.get(method, method) # lookup shortcuts or use "method" instead 100 | 101 | t = self.client.subscribe( 102 | method, *args 103 | ) # type: Tuple[asyncio.Future, asyncio.Queue] 104 | future, queue = t 105 | 106 | self.queue = queue 107 | return future 108 | 109 | async def consume_queue(self, queue_func: Callable[[List[str]], Awaitable[None]]) -> None: 110 | """ Coroutine. Infinite loop that consumes the current subscription queue. 111 | :param queue_func: A function to call when new responses arrive 112 | """ 113 | while True: 114 | logging.info("Awaiting queue..") 115 | result = await self.queue.get() # type: List[str] 116 | await queue_func(result) 117 | -------------------------------------------------------------------------------- /src/connectrum/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .exc import ElectrumErrorResponse 3 | 4 | __version__ = '0.8.1' 5 | -------------------------------------------------------------------------------- /src/connectrum/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Client connect to an Electrum server. 3 | # 4 | 5 | # Runtime check for optional modules 6 | from importlib import util as importutil 7 | import json, warnings, asyncio, ssl 8 | from .protocol import StratumProtocol 9 | from . import __version__ 10 | 11 | # Check if aiosocks is present, and load it if it is. 12 | if importutil.find_spec("aiosocks") is not None: 13 | import aiosocks 14 | have_aiosocks = True 15 | else: 16 | have_aiosocks = False 17 | 18 | from collections import defaultdict 19 | from .exc import ElectrumErrorResponse 20 | import logging 21 | 22 | logger = logging.getLogger('connectrum') 23 | 24 | class StratumClient: 25 | 26 | 27 | def __init__(self, loop=None): 28 | ''' 29 | Setup state needed to handle req/resp from a single Stratum server. 30 | Requires a transport (TransportABC) object to do the communication. 31 | ''' 32 | self.protocol = None 33 | 34 | self.next_id = 1 35 | self.inflight = {} 36 | self.subscriptions = defaultdict(list) 37 | 38 | # report our version, honestly; and indicate we only understand 1.4 39 | self.my_version_args = (f'Connectrum/{__version__}', '1.4') 40 | 41 | # these are valid after connection 42 | self.server_version = None # 'ElectrumX 1.13.0' or similar 43 | self.protocol_version = None # float(1.4) or similar 44 | 45 | self.actual_connection = {} 46 | 47 | self.ka_task = None 48 | 49 | self.loop = loop or asyncio.get_event_loop() 50 | 51 | self.reconnect = None # call connect() first 52 | 53 | # next step: call connect() 54 | 55 | def _connection_lost(self, protocol): 56 | # Ignore connection_lost for old connections 57 | self.disconnect_callback and self.disconnect_callback(self) 58 | if protocol is not self.protocol: 59 | return 60 | 61 | self.protocol = None 62 | logger.warn("Electrum server connection lost") 63 | 64 | # cleanup keep alive task 65 | if self.ka_task: 66 | self.ka_task.cancel() 67 | self.ka_task = None 68 | 69 | def close(self): 70 | if self.protocol: 71 | self.protocol.close() 72 | self.protocol = None 73 | if self.ka_task: 74 | self.ka_task.cancel() 75 | self.ka_task = None 76 | 77 | 78 | async def connect(self, server_info, proto_code=None, *, 79 | use_tor=False, disable_cert_verify=False, 80 | proxy=None, short_term=False, disconnect_callback=None): 81 | ''' 82 | Start connection process. 83 | Destination must be specified in a ServerInfo() record (first arg). 84 | ''' 85 | self.server_info = server_info 86 | self.disconnect_callback = disconnect_callback 87 | if not proto_code: 88 | proto_code,*_ = server_info.protocols 89 | self.proto_code = proto_code 90 | 91 | logger.debug("Connecting to: %r" % server_info) 92 | 93 | if proto_code == 'g': # websocket 94 | # to do this, we'll need a websockets implementation that 95 | # operates more like a asyncio.Transport 96 | # maybe: `asyncws` or `aiohttp` 97 | raise NotImplementedError('sorry no WebSocket transport yet') 98 | 99 | hostname, port, use_ssl = server_info.get_port(proto_code) 100 | 101 | if use_tor: 102 | if have_aiosocks: 103 | # Connect via Tor proxy proxy, assumed to be on localhost:9050 104 | # unless a tuple is given with another host/port combo. 105 | try: 106 | socks_host, socks_port = use_tor 107 | except TypeError: 108 | socks_host, socks_port = 'localhost', 9050 109 | 110 | # basically no-one has .onion SSL certificates, and 111 | # pointless anyway. 112 | disable_cert_verify = True 113 | 114 | assert not proxy, "Sorry not yet supporting proxy->tor->dest" 115 | 116 | logger.debug(" .. using TOR") 117 | 118 | proxy = aiosocks.Socks5Addr(socks_host, int(socks_port)) 119 | else: 120 | logger.debug("Error: want to use tor, but no aiosocks module.") 121 | 122 | if use_ssl == True and disable_cert_verify: 123 | # Create a more liberal SSL context that won't 124 | # object to self-signed certicates. This is 125 | # very bad on public Internet, but probably ok 126 | # over Tor 127 | use_ssl = ssl.create_default_context() 128 | use_ssl.check_hostname = False 129 | use_ssl.verify_mode = ssl.CERT_NONE 130 | 131 | logger.debug(" .. SSL cert check disabled") 132 | 133 | async def _reconnect(): 134 | if self.protocol: return # race/duplicate work 135 | 136 | if proxy: 137 | if have_aiosocks: 138 | transport, protocol = await aiosocks.create_connection( 139 | StratumProtocol, proxy=proxy, 140 | proxy_auth=None, 141 | remote_resolve=True, ssl=use_ssl, 142 | dst=(hostname, port)) 143 | else: 144 | logger.debug("Error: want to use proxy, but no aiosocks module.") 145 | else: 146 | transport, protocol = await self.loop.create_connection( 147 | StratumProtocol, host=hostname, 148 | port=port, ssl=use_ssl) 149 | 150 | self.protocol = protocol 151 | protocol.client = self 152 | 153 | # capture actual values used 154 | self.actual_connection = dict(hostname=hostname, port=int(port), 155 | ssl=bool(use_ssl), tor=bool(proxy)) 156 | self.actual_connection['ip_addr'] = transport.get_extra_info('peername', 157 | default=['unknown'])[0] 158 | 159 | # always report our version, and get server's version 160 | await self.get_server_version() 161 | logger.debug(f"Server version/protocol: {self.server_version} / {self.protocol_version}") 162 | 163 | if not short_term: 164 | self.ka_task = self.loop.create_task(self._keepalive()) 165 | 166 | logger.debug("Connected to: %r" % server_info) 167 | 168 | # close whatever we had 169 | if self.protocol: 170 | self.protocol.close() 171 | self.protocol = None 172 | 173 | self.reconnect = _reconnect 174 | await self.reconnect() 175 | 176 | async def get_server_version(self): 177 | # fetch version strings, save them 178 | # - can only be done once in v1.4 179 | self.server_version, pv = await self.RPC('server.version', *self.my_version_args) 180 | self.protocol_version = float(pv) 181 | 182 | async def _keepalive(self): 183 | ''' 184 | Keep our connect to server alive forever, with some 185 | pointless traffic. 186 | ''' 187 | while self.protocol: 188 | await self.RPC('server.ping') 189 | 190 | # Docs now say "The server may disconnect clients that have sent 191 | # no requests for roughly 10 minutes" ... so use 5 minutes here 192 | await asyncio.sleep(5*60) 193 | 194 | 195 | def _send_request(self, method, params=[], is_subscribe = False): 196 | ''' 197 | Send a new request to the server. Serialized the JSON and 198 | tracks id numbers and optional callbacks. 199 | ''' 200 | 201 | if method.startswith('blockchain.address.'): 202 | # these methods have changed, but we can patch them 203 | method, params = self.patch_addr_methods(method, params) 204 | 205 | # pick a new ID 206 | self.next_id += 1 207 | req_id = self.next_id 208 | 209 | # serialize as JSON 210 | msg = {'id': req_id, 'method': method, 'params': params} 211 | 212 | # subscriptions are a Q, normal requests are a future 213 | if is_subscribe: 214 | waitQ = asyncio.Queue() 215 | self.subscriptions[method].append(waitQ) 216 | 217 | fut = asyncio.Future(loop=self.loop) 218 | 219 | self.inflight[req_id] = (msg, fut) 220 | 221 | logger.debug(" REQ: %r" % msg) 222 | 223 | # send it via the transport, which serializes it 224 | if not self.protocol: 225 | logger.debug("Need to reconnect to server") 226 | 227 | async def connect_first(): 228 | await self.reconnect() 229 | self.protocol.send_data(msg) 230 | 231 | self.loop.create_task(connect_first()) 232 | else: 233 | # typical case, send request immediatedly, response is a future 234 | self.protocol.send_data(msg) 235 | 236 | return fut if not is_subscribe else (fut, waitQ) 237 | 238 | def _send_batch_requests(self, requests): 239 | ''' 240 | Send a new batch of requests to the server. 241 | ''' 242 | 243 | full_msg = [] 244 | 245 | for method, *params in requests: 246 | 247 | if method.startswith('blockchain.address.'): 248 | # these methods have changed, but we can patch them 249 | method, params = self.patch_addr_methods(method, params) 250 | 251 | # pick a new ID 252 | self.next_id += 1 253 | req_id = self.next_id 254 | 255 | # serialize as JSON 256 | msg = {'id': req_id, 'method': method, 'params': params} 257 | 258 | full_msg.append(msg) 259 | 260 | fut = asyncio.Future(loop=self.loop) 261 | first_msg = full_msg[0] 262 | 263 | self.inflight[first_msg['id']] = (full_msg, fut) 264 | 265 | logger.debug(" REQ: %r" % full_msg) 266 | 267 | # send it via the transport, which serializes it 268 | if not self.protocol: 269 | logger.debug("Need to reconnect to server") 270 | 271 | async def connect_first(): 272 | await self.reconnect() 273 | self.protocol.send_data(full_msg) 274 | 275 | self.loop.create_task(connect_first()) 276 | else: 277 | # typical case, send request immediately, response is a future 278 | self.protocol.send_data(full_msg) 279 | 280 | return fut 281 | 282 | def _got_response(self, msg): 283 | ''' 284 | Decode and dispatch responses from the server. 285 | 286 | Has already been unframed and deserialized into an object. 287 | ''' 288 | 289 | logger.debug("RESP: %r" % msg) 290 | 291 | if isinstance(msg, list): 292 | # we are dealing with a batch request 293 | 294 | inf = None 295 | for response in msg: 296 | resp_id = response.get('id', None) 297 | inf = self.inflight.pop(resp_id, None) 298 | if inf: 299 | break 300 | 301 | if not inf: 302 | first_msg = msg[0] 303 | logger.error("Incoming server message had unknown ID in it: %s" % first_msg['id']) 304 | return 305 | 306 | # it's a future which is done now 307 | full_req, rv = inf 308 | 309 | response_map = {resp['id']: resp for resp in msg} 310 | results = [] 311 | for request in full_req: 312 | req_id = request.get('id', None) 313 | 314 | response = response_map.get(req_id, None) 315 | if not response: 316 | logger.error("Incoming server message had missing ID: %s" % req_id) 317 | 318 | error = response.get('error', None) 319 | if error: 320 | logger.info("Error response: '%s'" % error) 321 | rv.set_exception(ElectrumErrorResponse(error, request)) 322 | 323 | result = response.get('result') 324 | results.append(result) 325 | 326 | rv.set_result(results) 327 | return 328 | 329 | resp_id = msg.get('id', None) 330 | 331 | if resp_id is None: 332 | # subscription traffic comes with method set, but no req id. 333 | method = msg.get('method', None) 334 | if not method: 335 | logger.error("Incoming server message had no ID nor method in it", msg) 336 | return 337 | 338 | # not obvious, but result is on params, not result, for subscriptions 339 | result = msg.get('params', None) 340 | 341 | logger.debug("Traffic on subscription: %s" % method) 342 | 343 | subs = self.subscriptions.get(method) 344 | for q in subs: 345 | self.loop.create_task(q.put(result)) 346 | 347 | return 348 | 349 | assert 'method' not in msg 350 | result = msg.get('result') 351 | 352 | # fetch and forget about the request 353 | inf = self.inflight.pop(resp_id) 354 | if not inf: 355 | logger.error("Incoming server message had unknown ID in it: %s" % resp_id) 356 | return 357 | 358 | # it's a future which is done now 359 | req, rv = inf 360 | 361 | if 'error' in msg: 362 | err = msg['error'] 363 | 364 | logger.info("Error response: '%s'" % err) 365 | rv.set_exception(ElectrumErrorResponse(err, req)) 366 | 367 | else: 368 | rv.set_result(result) 369 | 370 | def RPC(self, method, *params): 371 | ''' 372 | Perform a remote command. 373 | 374 | Expects a method name, which look like: 375 | 376 | blockchain.address.get_balance 377 | 378 | .. and sometimes take arguments, all of which are positional. 379 | 380 | Returns a future which will you should await for 381 | the result from the server. Failures are returned as exceptions. 382 | ''' 383 | assert '.' in method 384 | #assert not method.endswith('subscribe') 385 | 386 | return self._send_request(method, params) 387 | 388 | def batch_rpc(self, requests): 389 | ''' 390 | Perform a batch of remote commands. 391 | 392 | Expects a list of ("method name", params...) tuples, where the method name should look 393 | like: 394 | 395 | blockchain.address.get_balance 396 | 397 | .. and sometimes take arguments, all of which are positional. 398 | 399 | Returns a future which will you should await for the list of results for each command 400 | from the server. Failures are returned as exceptions. 401 | ''' 402 | for request in requests: 403 | assert isinstance(request, tuple) 404 | method, *params = request 405 | assert '.' in method 406 | 407 | return self._send_batch_requests(requests) 408 | 409 | def patch_addr_methods(self, method, params): 410 | # blockchain.address.get_balance(addr) => blockchain.scripthash.get_balance(sh) 411 | from hashlib import sha256 412 | from binascii import b2a_hex 413 | try: 414 | from pycoin.symbols.btc import network as BTC # bitcoin only! 415 | except ImportError: 416 | raise RuntimeError("we can patch obsolete protocol msgs, but need pycoin>=0.90") 417 | 418 | # convert from base58 into sha256(binary of script)? 419 | addr = BTC.parse(params[0]) 420 | sh = sha256(addr.script()).digest()[::-1] 421 | 422 | return method.replace('.address.', '.scripthash.'), \ 423 | [str(b2a_hex(sh), 'ascii')]+list(params[1:]) 424 | 425 | def subscribe(self, method, *params): 426 | ''' 427 | Perform a remote command which will stream events/data to us. 428 | 429 | Expects a method name, which look like: 430 | server.peers.subscribe 431 | .. and sometimes take arguments, all of which are positional. 432 | 433 | Returns a tuple: (Future, asyncio.Queue). 434 | The future will have the result of the initial 435 | call, and the queue will receive additional 436 | responses as they happen. 437 | ''' 438 | assert '.' in method 439 | assert method.endswith('subscribe') 440 | return self._send_request(method, params, is_subscribe=True) 441 | 442 | 443 | if __name__ == '__main__': 444 | from transport import SocketTransport 445 | from svr_info import KnownServers, ServerInfo 446 | 447 | logging.basicConfig(format="%(asctime)-11s %(message)s", datefmt="[%d/%m/%Y-%H:%M:%S]") 448 | 449 | loop = asyncio.get_event_loop() 450 | loop.set_debug(True) 451 | 452 | proto_code = 's' 453 | 454 | if 0: 455 | ks = KnownServers() 456 | ks.from_json('servers.json') 457 | which = ks.select(proto_code, is_onion=True, min_prune=1000)[0] 458 | else: 459 | which = ServerInfo({ 460 | "seen_at": 1465686119.022801, 461 | "ports": "t s", 462 | "nickname": "dunp", 463 | "pruning_limit": 10000, 464 | "version": "1.0", 465 | "hostname": "erbium1.sytes.net" }) 466 | 467 | c = StratumClient(loop=loop) 468 | 469 | loop.run_until_complete(c.connect(which, proto_code, disable_cert_verify=True, use_tor=True)) 470 | 471 | rv = loop.run_until_complete(c.RPC('server.peers.subscribe')) 472 | print("DONE!: this server has %d peers" % len(rv)) 473 | loop.close() 474 | 475 | #c.blockchain.address.get_balance(23) 476 | -------------------------------------------------------------------------------- /src/connectrum/constants.py: -------------------------------------------------------------------------------- 1 | 2 | # copied values from electrum source 3 | 4 | # IDK, maybe? 5 | ELECTRUM_VERSION = '2.6.4' # version of the client package 6 | PROTOCOL_VERSION = '0.10' # protocol version requested 7 | 8 | # note: 'v' and 'p' are effectively reserved as well. 9 | PROTOCOL_CODES = dict(t='TCP (plaintext)', h='HTTP (plaintext)', s='SSL', g='Websocket') 10 | 11 | # from electrum/lib/network.py at Jun/2016 12 | # 13 | DEFAULT_PORTS = { 't':50001, 's':50002, 'h':8081, 'g':8082} 14 | 15 | BOOTSTRAP_SERVERS = { 16 | 'erbium1.sytes.net': {'t':50001, 's':50002}, 17 | 'ecdsa.net': {'t':50001, 's':110}, 18 | 'electrum0.electricnewyear.net': {'t':50001, 's':50002}, 19 | 'VPS.hsmiths.com': {'t':50001, 's':50002}, 20 | 'ELECTRUM.jdubya.info': {'t':50001, 's':50002}, 21 | 'electrum.no-ip.org': {'t':50001, 's':50002, 'g':443}, 22 | 'us.electrum.be': DEFAULT_PORTS, 23 | 'bitcoins.sk': {'t':50001, 's':50002}, 24 | 'electrum.petrkr.net': {'t':50001, 's':50002}, 25 | 'electrum.dragonzone.net': DEFAULT_PORTS, 26 | 'Electrum.hsmiths.com': {'t':8080, 's':995}, 27 | 'electrum3.hachre.de': {'t':50001, 's':50002}, 28 | 'elec.luggs.co': {'t':80, 's':443}, 29 | 'btc.smsys.me': {'t':110, 's':995}, 30 | 'electrum.online': {'t':50001, 's':50002}, 31 | } 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/connectrum/exc.py: -------------------------------------------------------------------------------- 1 | # 2 | # Exceptions 3 | # 4 | 5 | 6 | class ElectrumErrorResponse(RuntimeError): 7 | pass 8 | -------------------------------------------------------------------------------- /src/connectrum/findall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | import bottom, random, time, asyncio 5 | from .svr_info import ServerInfo 6 | import logging 7 | 8 | logger = logging.getLogger('connectrum') 9 | 10 | class IrcListener(bottom.Client): 11 | def __init__(self, irc_nickname=None, irc_password=None, ssl=True): 12 | self.my_nick = irc_nickname or 'XC%d' % random.randint(1E11, 1E12) 13 | self.password = irc_password or None 14 | 15 | self.results = {} # by hostname 16 | self.servers = set() 17 | self.all_done = asyncio.Event() 18 | 19 | super(IrcListener, self).__init__(host='irc.freenode.net', port=6697 if ssl else 6667, ssl=ssl) 20 | 21 | # setup event handling 22 | self.on('CLIENT_CONNECT', self.connected) 23 | self.on('PING', self.keepalive) 24 | self.on('JOIN', self.joined) 25 | self.on('RPL_NAMREPLY', self.got_users) 26 | self.on('RPL_WHOREPLY', self.got_who_reply) 27 | self.on("client_disconnect", self.reconnect) 28 | self.on('RPL_ENDOFNAMES', self.got_end_of_names) 29 | 30 | async def collect_data(self): 31 | # start it process 32 | self.loop.create_task(self.connect()) 33 | 34 | # wait until done 35 | await self.all_done.wait() 36 | 37 | # return the results 38 | return self.results 39 | 40 | def connected(self, **kwargs): 41 | logger.debug("Connected") 42 | self.send('NICK', nick=self.my_nick) 43 | self.send('USER', user=self.my_nick, realname='Connectrum Client') 44 | # long delay here as it does an failing Ident probe (10 seconds min) 45 | self.send('JOIN', channel='#electrum') 46 | #self.send('WHO', mask='E_*') 47 | 48 | def keepalive(self, message, **kwargs): 49 | self.send('PONG', message=message) 50 | 51 | async def joined(self, nick=None, **kwargs): 52 | # happens when we or someone else joins the channel 53 | # seem to take 10 seconds or longer for me to join 54 | logger.debug('Joined: %r' % kwargs) 55 | 56 | if nick != self.my_nick: 57 | await self.add_server(nick) 58 | 59 | async def got_who_reply(self, nick=None, real_name=None, **kws): 60 | ''' 61 | Server replied to one of our WHO requests, with details. 62 | ''' 63 | #logger.debug('who reply: %r' % kws) 64 | 65 | nick = nick[2:] if nick[0:2] == 'E_' else nick 66 | host, ports = real_name.split(' ', 1) 67 | 68 | self.servers.remove(nick) 69 | 70 | logger.debug("Found: '%s' at %s with port list: %s",nick, host, ports) 71 | self.results[host.lower()] = ServerInfo(nick, host, ports) 72 | 73 | if not self.servers: 74 | self.all_done.set() 75 | 76 | async def got_users(self, users=[], **kws): 77 | # After successful join to channel, we are given a list of 78 | # users on the channel. Happens a few times for busy channels. 79 | logger.debug('Got %d (more) users in channel', len(users)) 80 | 81 | for nick in users: 82 | await self.add_server(nick) 83 | 84 | async def add_server(self, nick): 85 | # ignore everyone but electrum servers 86 | if nick.startswith('E_'): 87 | self.servers.add(nick[2:]) 88 | 89 | async def who_worker(self): 90 | # Fetch details on each Electrum server nick we see 91 | logger.debug('who task starts') 92 | copy = self.servers.copy() 93 | for nn in copy: 94 | logger.debug('do WHO for: ' + nn) 95 | self.send('WHO', mask='E_'+nn) 96 | 97 | logger.debug('who task done') 98 | 99 | def got_end_of_names(self, *a, **k): 100 | logger.debug('Got all the user names') 101 | 102 | assert self.servers, "No one on channel!" 103 | 104 | # ask for details on all of those users 105 | self.loop.create_task(self.who_worker()) 106 | 107 | 108 | async def reconnect(self, **kwargs): 109 | # Trigger an event that may cascade to a client_connect. 110 | # Don't continue until a client_connect occurs, which may be never. 111 | 112 | logger.warn("Disconnected (will reconnect)") 113 | 114 | # Note that we're not in a coroutine, so we don't have access 115 | # to await and asyncio.sleep 116 | time.sleep(3) 117 | 118 | # After this line we won't necessarily be connected. 119 | # We've simply scheduled the connect to happen in the future 120 | self.loop.create_task(self.connect()) 121 | 122 | logger.debug("Reconnect scheduled.") 123 | 124 | 125 | if __name__ == '__main__': 126 | 127 | 128 | import logging 129 | logging.getLogger('bottom').setLevel(logging.DEBUG) 130 | logging.getLogger('connectrum').setLevel(logging.DEBUG) 131 | logging.getLogger('asyncio').setLevel(logging.DEBUG) 132 | 133 | 134 | bot = IrcListener(ssl=False) 135 | bot.loop.set_debug(True) 136 | fut = bot.collect_data() 137 | #bot.loop.create_task(bot.connect()) 138 | rv = bot.loop.run_until_complete(fut) 139 | 140 | print(rv) 141 | 142 | -------------------------------------------------------------------------------- /src/connectrum/protocol.py: -------------------------------------------------------------------------------- 1 | # 2 | # Implement an asyncio.Protocol for Electrum (clients) 3 | # 4 | # 5 | import asyncio, json 6 | import logging 7 | 8 | logger = logging.getLogger('connectrum') 9 | 10 | class StratumProtocol(asyncio.Protocol): 11 | client = None 12 | closed = False 13 | transport = None 14 | buf = b"" 15 | 16 | def connection_made(self, transport): 17 | self.transport = transport 18 | logger.debug("Transport connected ok") 19 | 20 | def connection_lost(self, exc): 21 | if not self.closed: 22 | self.closed = True 23 | self.close() 24 | self.client._connection_lost(self) 25 | 26 | def data_received(self, data): 27 | self.buf += data 28 | 29 | # Unframe the mesage. Expecting JSON. 30 | *lines, self.buf = self.buf.split(b'\n') 31 | 32 | for line in lines: 33 | if not line: continue 34 | 35 | try: 36 | msg = line.decode('utf-8', "error").strip() 37 | except UnicodeError as exc: 38 | logger.exception("Encoding issue on %r" % line) 39 | self.connection_lost(exc) 40 | return 41 | 42 | try: 43 | msg = json.loads(msg) 44 | except ValueError as exc: 45 | logger.exception("Bad JSON received from server: %r" % msg) 46 | self.connection_lost(exc) 47 | return 48 | 49 | #logger.debug("RX:\n%s", json.dumps(msg, indent=2)) 50 | 51 | try: 52 | self.client._got_response(msg) 53 | except Exception as e: 54 | logger.exception("Trouble handling response! (%s)" % e) 55 | continue 56 | 57 | def send_data(self, message): 58 | ''' 59 | Given an object, encode as JSON and transmit to the server. 60 | ''' 61 | #logger.debug("TX:\n%s", json.dumps(message, indent=2)) 62 | data = json.dumps(message).encode('utf-8') + b'\n' 63 | self.transport.write(data) 64 | 65 | def close(self): 66 | if not self.closed: 67 | try: 68 | self.transport.close() 69 | finally: 70 | self.closed = True 71 | 72 | # EOF 73 | -------------------------------------------------------------------------------- /src/connectrum/svr_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Store information about servers. Filter and select based on their protocol support, etc. 3 | # 4 | 5 | # Runtime check for optional modules 6 | from importlib import util as importutil 7 | 8 | # Check if bottom is present. 9 | if importutil.find_spec("bottom") is not None: 10 | have_bottom = True 11 | else: 12 | have_bottom = False 13 | 14 | import time, random, json 15 | from .constants import DEFAULT_PORTS 16 | 17 | 18 | class ServerInfo(dict): 19 | ''' 20 | Information to be stored on a server. Originally based on IRC data published to a channel. 21 | 22 | ''' 23 | FIELDS = ['nickname', 'hostname', 'ports', 'version', 'pruning_limit' ] 24 | 25 | def __init__(self, nickname_or_dict, hostname=None, ports=None, 26 | version=None, pruning_limit=None, ip_addr=None): 27 | 28 | if not hostname and not ports: 29 | # promote a dict, or similar 30 | super(ServerInfo, self).__init__(nickname_or_dict) 31 | return 32 | 33 | self['nickname'] = nickname_or_dict or None 34 | self['hostname'] = hostname 35 | self['ip_addr'] = ip_addr or None 36 | 37 | # For 'ports', take 38 | # - a number (int), assumed to be TCP port, OR 39 | # - a list of codes, OR 40 | # - a string to be split apart. 41 | # Keep version and pruning limit separate 42 | # 43 | if isinstance(ports, int): 44 | ports = ['t%d' % ports] 45 | elif isinstance(ports, str): 46 | ports = ports.split() 47 | 48 | # check we don't have junk in the ports list 49 | for p in ports.copy(): 50 | if p[0] == 'v': 51 | version = p[1:] 52 | ports.remove(p) 53 | elif p[0] == 'p': 54 | try: 55 | pruning_limit = int(p[1:]) 56 | except ValueError: 57 | # ignore junk 58 | pass 59 | ports.remove(p) 60 | 61 | assert ports, "Must have at least one port/protocol" 62 | 63 | self['ports'] = ports 64 | self['version'] = version 65 | self['pruning_limit'] = int(pruning_limit or 0) 66 | 67 | @classmethod 68 | def from_response(cls, response_list): 69 | # Create a list of servers based on data from response from Stratum. 70 | # Give this the response to: "server.peers.subscribe" 71 | # 72 | # [... 73 | # "91.63.237.12", 74 | # "electrum3.hachre.de", 75 | # [ "v1.0", "p10000", "t", "s" ] 76 | # ...] 77 | # 78 | rv = [] 79 | 80 | for params in response_list: 81 | ip_addr, hostname, ports = params 82 | 83 | if ip_addr == hostname: 84 | ip_addr = None 85 | 86 | rv.append(ServerInfo(None, hostname, ports, ip_addr=ip_addr)) 87 | 88 | return rv 89 | 90 | @classmethod 91 | def from_dict(cls, d): 92 | n = d.pop('nickname', None) 93 | h = d.pop('hostname') 94 | p = d.pop('ports') 95 | rv = cls(n, h, p) 96 | rv.update(d) 97 | return rv 98 | 99 | 100 | @property 101 | def protocols(self): 102 | rv = set(i[0] for i in self['ports']) 103 | assert 'p' not in rv, 'pruning limit got in there' 104 | assert 'v' not in rv, 'version got in there' 105 | return rv 106 | 107 | @property 108 | def pruning_limit(self): 109 | return self.get('pruning_limit', 100) 110 | 111 | @property 112 | def hostname(self): 113 | return self.get('hostname') 114 | 115 | def get_port(self, for_protocol): 116 | ''' 117 | Return (hostname, port number, ssl) pair for the protocol. 118 | Assuming only one port per host. 119 | ''' 120 | assert len(for_protocol) == 1, "expect single letter code" 121 | 122 | use_ssl = for_protocol in ('s', 'g') 123 | 124 | if 'port' in self: return self['hostname'], int(self['port']), use_ssl 125 | 126 | print(self['ports']) 127 | try: 128 | rv = next(i for i in self['ports'] if i[0] == for_protocol) 129 | except Exception as ex: 130 | print (ex) 131 | rv = [] 132 | port = None 133 | if len(rv) >= 2: 134 | try: 135 | port = int(rv[1:]) 136 | except: 137 | pass 138 | port = port or DEFAULT_PORTS[for_protocol] 139 | 140 | return self['hostname'], port, use_ssl 141 | 142 | @property 143 | def is_onion(self): 144 | return self['hostname'].lower().endswith('.onion') 145 | 146 | def select(self, protocol='s', is_onion=None, min_prune=0): 147 | # predicate function for selection based on features/properties 148 | return ((protocol in self.protocols) 149 | and (self.is_onion == is_onion if is_onion is not None else True) 150 | and (self.pruning_limit >= min_prune)) 151 | 152 | def __repr__(self): 153 | return ''\ 154 | .format(**self) 155 | 156 | def __str__(self): 157 | # used as a dict key in a few places. 158 | return self['hostname'].lower() 159 | 160 | def __hash__(self): 161 | # this one-line allows use as a set member, which is really handy! 162 | return hash(self['hostname'].lower()) 163 | 164 | class KnownServers(dict): 165 | ''' 166 | Store a list of known servers and their port numbers, etc. 167 | 168 | - can add single entries 169 | - can read from a CSV for seeding/bootstrap 170 | - can read from IRC channel to find current hosts 171 | 172 | We are a dictionary, with key being the hostname (in lowercase) of the server. 173 | ''' 174 | 175 | def from_json(self, fname): 176 | ''' 177 | Read contents of a CSV containing a list of servers. 178 | ''' 179 | with open(fname, 'rt') as fp: 180 | for row in json.load(fp): 181 | nn = ServerInfo.from_dict(row) 182 | self[str(nn)] = nn 183 | 184 | def from_irc(self, irc_nickname=None, irc_password=None): 185 | ''' 186 | Connect to the IRC channel and find all servers presently connected. 187 | 188 | Slow; takes 30+ seconds but authoritative and current. 189 | 190 | OBSOLETE. 191 | ''' 192 | if have_bottom: 193 | from .findall import IrcListener 194 | 195 | # connect and fetch current set of servers who are 196 | # on #electrum channel at freenode 197 | 198 | bot = IrcListener(irc_nickname=irc_nickname, irc_password=irc_password) 199 | results = bot.loop.run_until_complete(bot.collect_data()) 200 | bot.loop.close() 201 | 202 | # merge by nick name 203 | self.update(results) 204 | else: 205 | return(False) 206 | 207 | def add_single(self, hostname, ports, nickname=None, **kws): 208 | ''' 209 | Explicitly add a single entry. 210 | Hostname is a FQDN and ports is either a single int (assumed to be TCP port) 211 | or Electrum protocol/port number specification with spaces in between. 212 | ''' 213 | nickname = nickname or hostname 214 | 215 | self[hostname.lower()] = ServerInfo(nickname, hostname, ports, **kws) 216 | 217 | def add_peer_response(self, response_list): 218 | # Update with response from Stratum (lacks the nickname value tho): 219 | # 220 | # "91.63.237.12", 221 | # "electrum3.hachre.de", 222 | # [ "v1.0", "p10000", "t", "s" ] 223 | # 224 | additions = set() 225 | for params in response_list: 226 | ip_addr, hostname, ports = params 227 | 228 | if ip_addr == hostname: 229 | ip_addr = None 230 | 231 | g = self.get(hostname.lower()) 232 | nickname = g['nickname'] if g else None 233 | 234 | here = ServerInfo(nickname, hostname, ports, ip_addr=ip_addr) 235 | self[str(here)] = here 236 | 237 | if not g: 238 | additions.add(str(here)) 239 | 240 | return additions 241 | 242 | def save_json(self, fname='servers.json'): 243 | ''' 244 | Write out to a CSV file. 245 | ''' 246 | rows = sorted(self.keys()) 247 | with open(fname, 'wt') as fp: 248 | json.dump([self[k] for k in rows], fp, indent=1) 249 | 250 | def dump(self): 251 | return '\n'.join(repr(i) for i in self.values()) 252 | 253 | def select(self, **kws): 254 | ''' 255 | Find all servers with indicated protocol support. Shuffled. 256 | 257 | Filter by TOR support, and pruning level. 258 | ''' 259 | lst = [i for i in self.values() if i.select(**kws)] 260 | 261 | random.shuffle(lst) 262 | 263 | return lst 264 | 265 | 266 | if __name__ == '__main__': 267 | 268 | ks = KnownServers() 269 | 270 | #ks.from_json('servers.json') 271 | ks.from_irc() 272 | 273 | #print (ks.dump()) 274 | 275 | from constants import PROTOCOL_CODES 276 | 277 | print ("%3d: servers in total" % len(ks)) 278 | 279 | for tor in [False, True]: 280 | for pp in PROTOCOL_CODES.keys(): 281 | ll = ks.select(pp, is_onion=tor) 282 | print ("%3d: %s" % (len(ll), PROTOCOL_CODES[pp] + (' [TOR]' if tor else ''))) 283 | 284 | # EOF 285 | -------------------------------------------------------------------------------- /src/exchange_rate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import json 4 | from typing import Dict, List, Any 5 | import numpy as np 6 | from socks_http import urlopen 7 | import requests 8 | 9 | CURRENCIES = [ 10 | "BTC", # This will disable some exchange rate logic if set. 11 | "USD", "CHF", "EUR", "GBP", "AUD", 12 | "CAD", "JPY", "CNY","RUB", "UAH", 13 | ] # type: List[str] 14 | 15 | CURRENCIES.sort() 16 | 17 | 18 | def reject_outliers(data, m=2.): 19 | """ """ 20 | data = np.array(data) 21 | d = np.abs(data - np.median(data)) 22 | mdev = np.median(d) 23 | s = d / (mdev if mdev else 1.) 24 | return data[s < m].tolist() 25 | 26 | async def fetch_from_api(base_url: str, chain_1209k: str, loop=None) -> Dict[str, Any]: 27 | fiats = ",".join(CURRENCIES) # type: str 28 | url = base_url.format(chain_1209k.upper(), fiats) # type: str 29 | logging.info("Fetching rates from URL: %s", url) 30 | return json.loads(await urlopen(url, loop=loop)) 31 | 32 | async def fetch_single_from_api(base_url: str, loop=None) -> Dict[str, Any]: 33 | logging.info("Fetching rates from URL: %s", base_url) 34 | response = requests.get(base_url, timeout=2.5) 35 | return response.json() 36 | 37 | async def fetch_exchange_rates(chain_1209k: str = "btc", loop=None) -> Dict[str, Dict]: 38 | """ """ 39 | all_rates = {} 40 | 41 | coingecko_url = ("https://api.coingecko.com/api/v3/simple/price" + 42 | "?ids=bitcoin%2C&vs_currencies=" + 43 | "%2C".join(CURRENCIES)) # type: str 44 | try: 45 | coingecko_json = await fetch_from_api(coingecko_url, chain_1209k, loop=loop) 46 | rates = {} 47 | for symbol, value in coingecko_json.get('bitcoin').items(): 48 | if symbol.upper() in CURRENCIES: 49 | rates[symbol.upper()] = value 50 | all_rates["coingecko"] = rates 51 | print (rates) 52 | except Exception as ex: 53 | logging.error("coingecko_url call failed with {}".format(ex), exc_info=True) 54 | 55 | ccomp_url = ("https://min-api.cryptocompare.com/data/price?fsym={}&tsyms={}") # type: str 56 | try: 57 | ccomp_json = await fetch_from_api( 58 | ccomp_url, chain_1209k, loop=loop) 59 | all_rates["ccomp"] = ccomp_json 60 | except Exception as ex: 61 | logging.error("coingecko_url call failed with {}".format(ex), exc_info=True) 62 | 63 | coinbase_url = ("https://api.coinbase.com/v2/exchange-rates?currency=BTC") # type: str 64 | try: 65 | coinbase_json = await fetch_single_from_api(coinbase_url, loop=loop) 66 | all_rates["coinbase"] = coinbase_json 67 | except Exception as ex: 68 | logging.error("coinbase call failed with {}".format(ex), exc_info=True) 69 | 70 | bitstamp_url = "https://www.bitstamp.net/api/v2/ticker/" 71 | try: 72 | bitstamp_json = await fetch_single_from_api(bitstamp_url, loop=loop) 73 | all_rates["bitstamp"] = bitstamp_json 74 | except Exception as ex: 75 | logging.error("bitstamp call failed with {}".format(ex), exc_info=True) 76 | 77 | res_with_avg_rate = {} 78 | for SYMBOL in CURRENCIES: 79 | try: 80 | all_symbol_rates = [] 81 | try: 82 | coingecko_rate = all_rates.get("coingecko", {}).get(SYMBOL, None) 83 | if coingecko_rate: 84 | all_symbol_rates.append(float(coingecko_rate)) 85 | except Exception as ex: 86 | print(ex) 87 | pass 88 | try: 89 | ccomp_rate = all_rates.get("ccomp", {}).get(SYMBOL, None) 90 | if ccomp_rate: 91 | all_symbol_rates.append(float(ccomp_rate)) 92 | except Exception as ex: 93 | print(ex) 94 | pass 95 | try: 96 | coinbase_rate = all_rates.get("coinbase", {}).get("data", {}).get("rates", {}).get(SYMBOL, None) 97 | if coinbase_rate: 98 | all_symbol_rates.append(float(coinbase_rate)) 99 | except Exception as ex: 100 | print(ex) 101 | pass 102 | try: 103 | for bitsamp_rate in all_rates.get("bitstamp", []): 104 | if bitsamp_rate.get('pair', "") == "BTC/"+SYMBOL: 105 | _bitsamp_rate = bitsamp_rate.get('last', None) 106 | if _bitsamp_rate: 107 | all_symbol_rates.append(float(_bitsamp_rate)) 108 | break 109 | except Exception as ex: 110 | print(ex) 111 | pass 112 | avg_rate = 1 113 | if len(all_symbol_rates): 114 | try: 115 | ans = reject_outliers(all_symbol_rates) 116 | avg_rate = sum(ans)/float(len(ans)) 117 | res_with_avg_rate[SYMBOL] = avg_rate 118 | except Exception as ex: 119 | print(ex) 120 | except Exception as ex: 121 | print(ex) 122 | pass 123 | return res_with_avg_rate 124 | -------------------------------------------------------------------------------- /src/fee_estimate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import json 4 | from typing import Dict, List, Any 5 | 6 | from socks_http import urlopen 7 | 8 | 9 | async def fetch_from_api(url, loop=None) -> Dict[str, Any]: 10 | ans = json.loads(await urlopen(url, loop=loop)) 11 | print(ans) 12 | return ans 13 | 14 | async def fetch_fee_estimate(loop=None): 15 | mempool_endpoint = "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" 16 | try: 17 | ans = await fetch_from_api(mempool_endpoint, loop=loop) 18 | print(ans) 19 | return ans 20 | except Exception as ex: 21 | logging.error("failed to get fee estimate from mempool with {}".format(ex), exc_info=True) 22 | -------------------------------------------------------------------------------- /src/history.py: -------------------------------------------------------------------------------- 1 | from logger import logging 2 | 3 | from decimal import Decimal 4 | from typing import Dict 5 | from typing import Any 6 | 7 | from pycoin.tx.Tx import Tx 8 | #from pycoin.tx.TxIn import TxIn 9 | #from pycoin.tx.TxOut import TxOut 10 | #from pycoin.tx.Spendable import Spendable 11 | from connectrum import ElectrumErrorResponse 12 | from connection import Connection 13 | 14 | from utils import get_timestamp_from_block_header 15 | 16 | class History: 17 | """ History object. Holds data relevant to a piece of 18 | our transaction history. 19 | """ 20 | 21 | def __init__(self, tx_obj: Tx, is_spend: bool, value: Decimal, height: int) -> None: 22 | """ History object constructor. 23 | 24 | :param tx_obj: a pycoin.Tx object representing the tx data 25 | :param is_spend: boolean, was this tx a spend from our wallet? 26 | :param value: the coin_value of this tx 27 | :param height: the height of the block this tx is included in 28 | :returns: A new History object 29 | """ 30 | self.tx_obj = tx_obj # type: Tx 31 | self.is_spend = is_spend # type: bool 32 | self.value = value # type: Decimal 33 | self.height = height # type: int 34 | self.timestamp = None # type: str 35 | 36 | print("\n\tHistory object constructor, adding {} {}".format(tx_obj.as_hex(), self.as_dict())) 37 | 38 | 39 | async def get_timestamp(self, connection: Connection) -> None: 40 | """ Coroutine. Gets the timestamp for this Tx based on the given height. 41 | :param connection: a Connection object for getting a block header from the server 42 | """ 43 | if self.height > 0: 44 | try: 45 | block_header = await connection.listen_rpc( 46 | connection.methods["get_header"], 47 | [self.height] 48 | ) # type: Dict[str, Any] 49 | except ElectrumErrorResponse as e: 50 | return 51 | self.timestamp = get_timestamp_from_block_header(block_header) 52 | logging.debug("Got timestamp {} for block at height {}".format(self.height, self.timestamp)) 53 | else: 54 | import datetime 55 | self.timestamp = datetime.datetime.now() 56 | logging.debug("Assuming timestamp %d from block at height %s", 57 | self.height, self.timestamp) 58 | 59 | def as_dict(self) -> Dict[str, Any]: 60 | """ Transforms this History object into a dictionary. 61 | :returns: A dictionary representation of this History object 62 | """ 63 | return { 64 | "txid": self.tx_obj.id(), 65 | "is_spend": self.is_spend, 66 | "value": str(self.value), 67 | "height": self.height, 68 | "timestamp": self.timestamp 69 | } 70 | 71 | def __str__(self) -> str: 72 | """ Special method __str__() 73 | :returns: The string representation of this History object 74 | """ 75 | return ( 76 | "" 77 | ).format(self.tx_obj.id(), self.is_spend, 78 | self.value, self.height, self.timestamp) 79 | 80 | def __repr__(self) -> str: 81 | return str(self) 82 | 83 | def __hash__(self) -> int: 84 | return hash(self.tx_obj.id()) 85 | 86 | def __eq__(self, other) -> bool: 87 | return self.tx_obj.id() == other.tx_obj.id() 88 | -------------------------------------------------------------------------------- /src/keys.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from typing import Tuple 3 | 4 | from Crypto.Hash import SHA256 5 | import pbkdf2 6 | 7 | from Crypto.Protocol.KDF import scrypt 8 | 9 | 10 | def and_split(bytes_: bytes) -> Tuple[bytes, bytes]: 11 | ba1 = bytearray() # type: bytearray 12 | ba2 = bytearray() # type: bytearray 13 | 14 | for byte in bytes_: 15 | ba1.append(byte & 0xF0) 16 | ba2.append(byte & 0x0F) 17 | return (bytes(ba1), bytes(ba2)) 18 | 19 | 20 | def xor_merge(bytes1: bytes, bytes2: bytes) -> bytes: 21 | if len(bytes1) != len(bytes2): 22 | raise ValueError("Length mismatch") 23 | 24 | byte_array = bytearray() # type: bytearray 25 | for i in range(len(bytes1)): 26 | byte_array.append(bytes1[i] ^ bytes2[i]) 27 | return bytes(byte_array) 28 | 29 | 30 | def derive_key(salt: str, passphrase: str) -> Union[int, Tuple[int, bytes]]: 31 | key_length = 64 # type: int 32 | t1 = and_split(bytes(salt, "utf-8")) # type: Tuple[bytes, bytes] 33 | salt1, salt2 = t1 34 | t2 = and_split(bytes(passphrase, "utf-8")) # type: Tuple[bytes, bytes] 35 | pass1, pass2 = t2 36 | 37 | N = 1<<18 # 1<<18 # == 2**18 == 262144 38 | 39 | scrypt_key = scrypt( 40 | pass1, 41 | salt=salt1, 42 | key_len=key_length, 43 | N=N, 44 | r=8, 45 | p=1, 46 | num_keys=1, 47 | ) 48 | 49 | pbkdf2_key = pbkdf2.PBKDF2( 50 | pass2, salt2, 51 | iterations=1 << 16, 52 | digestmodule=SHA256).read(key_length) # type: bytes 53 | keypair = xor_merge(scrypt_key, pbkdf2_key) # type: bytes 54 | secret_exp = int(keypair[0:32].hex(), 16) # type: int / a number in the range [1, curve_order]. 55 | chain_code = keypair[32:] # type: bytes 56 | return secret_exp, chain_code 57 | 58 | 59 | 60 | def main(): 61 | email = input("Enter email: ") # type: str 62 | passphrase = input("Enter passphrase: ") # type: str 63 | t = derive_key(email, passphrase) # type: Tuple[int, bytes] 64 | secret_exp, chain_code = t 65 | print("Secret exp: {}\nChain code: {}".format(secret_exp, chain_code)) 66 | 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /src/kivy_utils.py: -------------------------------------------------------------------------------- 1 | # Compatibility and utils 2 | from kivy.utils import platform 3 | from os.path import join as os_path_join 4 | 5 | def get_storage_path(filename=""): 6 | try: 7 | from android.storage import primary_external_storage_path 8 | ext_path = primary_external_storage_path() 9 | except ModuleNotFoundError: 10 | from os.path import expanduser 11 | ext_path = expanduser("~") 12 | return os_path_join(ext_path, 'Downloads', filename) 13 | 14 | 15 | def open_url(url): 16 | if platform == 'android': 17 | """ Open a webpage in the default Android browser. """ 18 | from jnius import autoclass, cast 19 | context = autoclass('org.kivy.android.PythonActivity').mActivity 20 | Uri = autoclass('android.net.Uri') 21 | Intent = autoclass('android.content.Intent') 22 | intent = Intent() 23 | intent.setAction(Intent.ACTION_VIEW) 24 | intent.setData(Uri.parse(url)) 25 | currentActivity = cast('android.app.Activity', context) 26 | currentActivity.startActivity(intent) 27 | else: 28 | import webbrowser 29 | webbrowser.open_new(url) 30 | -------------------------------------------------------------------------------- /src/label_store.py: -------------------------------------------------------------------------------- 1 | from kivy_utils import get_storage_path 2 | import os 3 | import shutil 4 | from functools import partial 5 | from kivymd.uix.button import MDFlatButton 6 | from kivymd.uix.dialog import MDDialog 7 | from kivy.metrics import dp 8 | 9 | class LabelStore: 10 | """ 11 | https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki 12 | """ 13 | def __init__(self, app, wallet): 14 | self.kivyapp = app 15 | self.wallet = wallet 16 | self.loaded_initally = False 17 | self.name = 'BIP329-labels-{}.jsonl'.format(self.wallet.fingerprint) 18 | self._unsynced_labels = 0 19 | self._types = ['tx','input', 'output', 'addr', 'xpub', 'pubkey'] 20 | self.store = [] 21 | self._dialog = None 22 | 23 | def _close_dialog(self, *args): 24 | """ """ 25 | if self._dialog: 26 | self._dialog.dismiss() 27 | 28 | def _import_close_dialog(self, *args): 29 | """ """ 30 | self.load_from_file() 31 | if self._dialog: 32 | self._dialog.dismiss() 33 | 34 | def _mark_synced(self): 35 | self._unsynced_labels = 0 36 | 37 | def check_for_import(self): 38 | """ """ 39 | if self.check_for_label_file(): 40 | self._dialog = MDDialog(title="Import labels from file?", 41 | text="Label file found", 42 | size_hint=(.8, None), 43 | height=dp(200), 44 | auto_dismiss=False, 45 | buttons=[ 46 | MDFlatButton(text="NO THANKS", 47 | on_release=partial(self._close_dialog)), 48 | MDFlatButton(text="YES", 49 | on_release=partial(self._import_close_dialog)), 50 | 51 | ] 52 | 53 | ) 54 | # WIP: self._dialog.open() 55 | 56 | 57 | def check_for_label_file(self, path=None): 58 | if path is None: 59 | path = get_storage_path(filename=self.name) 60 | if os.path.isfile(path): 61 | return True 62 | return False 63 | 64 | 65 | def load_from_file(self, path=None): 66 | if path is None: 67 | path = get_storage_path(filename=self.name) 68 | if os.path.isfile(path): 69 | print("BIP329 LOAD FROM FILE") 70 | self.loaded_initally = True 71 | 72 | 73 | def sync(self): 74 | self.save_to_file() 75 | self._mark_synced() 76 | 77 | 78 | 79 | def add_label(self, type, ref, label): 80 | print("{} {} {}".format(type, ref, label)) 81 | self.store.append({"type": type, "ref":ref, "label": label}) 82 | self._unsynced_labels += 1 83 | 84 | 85 | def save_to_file(self, path=None): 86 | """ Writes that file to the Downloads directly. """ 87 | if path is None: 88 | path = get_storage_path(filename=self.name) 89 | if os.path.isfile(path) and self.loaded_initally: 90 | # Don't overwrite the file we imported initally. 91 | shutil.copyfile(path, "{}-backup-{}".foramt(path, self.kivyapp.block_height)) 92 | f = open(path, "w") 93 | for entry in self.store: 94 | if all(item in list(entry.keys()) for item in ["ref", "label", "type"]): 95 | first = True 96 | line = "{" 97 | for k in entry.keys(): 98 | if line != "{": 99 | line += ", " 100 | line += "\"{}\": \"{}\"".format(k, entry[k]) 101 | line += "}\n" 102 | f.write(line) 103 | f.close() 104 | -------------------------------------------------------------------------------- /src/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import os 4 | 5 | 6 | FORMAT = "%(asctime)s %(levelname)s: %(message)s" # type: str 7 | 8 | stdout_hdlr = logging.StreamHandler(sys.stdout) # type: logging.StreamHandler 9 | stdout_hdlr.setFormatter(logging.Formatter(FORMAT)) 10 | stdout_hdlr.setLevel( 11 | logging.ERROR if os.environ.get("NW_LOG") == "ERR" else logging.INFO) 12 | 13 | file_hdlr = logging.FileHandler( 14 | filename="nowallet.log", mode="w") # type: logging.FileHandler 15 | file_hdlr.setFormatter(logging.Formatter(FORMAT)) 16 | file_hdlr.setLevel(logging.DEBUG) 17 | 18 | logging.basicConfig(level=logging.DEBUG, handlers=[stdout_hdlr, file_hdlr]) 19 | -------------------------------------------------------------------------------- /src/nowallet_history_store.py: -------------------------------------------------------------------------------- 1 | #from kivy.storage.jsonstore import JsonStore 2 | from logger import logging 3 | from pycoin.tx.Tx import Tx 4 | import json 5 | 6 | #TODO: what are mine and what are external addresses? 7 | 8 | #for i in range(anzahl_keys im store, anzahl_keys im store + Wallet._GAP_LIMIT): 9 | # for receive, change ... 10 | # addr = self.get_address(self.get_key(i, change)) # type: str 11 | 12 | class HistoryStore: 13 | """ """ 14 | def __init__(self, wallet): 15 | self.wallet = wallet 16 | self.name = 'brainbow-history-store-{}.json'.format(self.wallet.fingerprint) 17 | self.store = {} 18 | self.all_known_receive_addresses = [] 19 | self.all_known_change_addresses = [] 20 | 21 | 22 | def save_to_file(self): 23 | """ """ 24 | json_object = json.dumps(self.store, indent=4) 25 | with open(self.name, "w") as outfile: 26 | outfile.write(json_object) 27 | 28 | def get_txo_of_tx(self, txid, index): 29 | tx = self.store.get(txid, None) 30 | if tx: 31 | return tx['txs_out'][index] 32 | 33 | def get_tx(self, txid): 34 | return self.store.get(txid, None) 35 | 36 | def store_tx(self, tx_obj: Tx, history_obj = None): 37 | self.store[tx_obj.id()] = {} 38 | self.store[tx_obj.id()]['hextx'] = tx_obj.as_hex() 39 | self.store[tx_obj.id()]['txs_in'] = [] 40 | self.store[tx_obj.id()]['txs_out'] = [] 41 | if history_obj: 42 | self.store[tx_obj.id()]['height'] = history_obj.height 43 | self.store[tx_obj.id()]['timestamp'] = str(history_obj.timestamp) 44 | 45 | 46 | for i, txs_in in enumerate(tx_obj.txs_in): 47 | self.store[tx_obj.id()]['txs_in'].append({ 48 | 'previous_hash': str(txs_in.previous_hash), 49 | 'previous_index': int(txs_in.previous_index), 50 | 'previous_txo': self.get_txo_of_tx(str(txs_in.previous_hash), int(txs_in.previous_index)), 51 | 'in_index': i, 52 | }) 53 | 54 | for i, txout in enumerate(tx_obj.txs_out): 55 | address = txout.address(self.wallet.chain.netcode) 56 | 57 | if address in self.all_known_receive_addresses: 58 | is_mine = True 59 | is_change = False 60 | elif address in self.all_known_change_addresses: 61 | is_mine = True 62 | is_change = True 63 | else: 64 | is_mine = False 65 | is_change = False 66 | 67 | self.store[tx_obj.id()]['txs_out'].append({ 68 | 'address': str(address), 69 | 'out_index': i, 70 | 'coin_value': int(txout.coin_value), 71 | 'is_mine': is_mine, 72 | 'is_change': is_change 73 | }) 74 | 75 | # def mark_utxo_as_spend _in_block 76 | # 77 | #def update_tx(self, key, value): 78 | # """ 79 | # """ 80 | # pass 81 | def get_tx_by_txid(self, txid): 82 | _tx = self.store.find(txid=txid) 83 | print(_tx) 84 | return _tx 85 | """ 86 | is change? 87 | is ours ? 88 | is spend ? 89 | self.receive_addrs = self.wallet.get_all_known_addresses(change=False) 90 | self.change_addrs = self.wallet.get_all_known_addresses(change=True) 91 | """ 92 | -------------------------------------------------------------------------------- /src/passphrase.py: -------------------------------------------------------------------------------- 1 | import math 2 | import re 3 | 4 | def entropy_bits(passphrase): 5 | """ 6 | Ref: https://www.omnicalculator.com/other/password-entropy 7 | Check: https://timcutting.co.uk/tools/password-entropy 8 | """ 9 | pool_size = 0 10 | 11 | policies = {'Uppercase characters': 0, 12 | 'Lowercase characters': 0, 13 | 'Special characters': 0, 14 | 'Numbers': 0} 15 | 16 | entropies = {'Uppercase characters': 26, 17 | 'Lowercase characters': 26, 18 | 'Special characters': 32, # or more ... 19 | 'Numbers': 10 } 20 | 21 | passphrase_len = len(passphrase) 22 | 23 | for char in passphrase: 24 | if re.match("[0-9]", char): 25 | policies["Numbers"] += 1 26 | elif re.match("[a-z]", char): 27 | policies["Lowercase characters"] += 1 28 | elif re.match("[A-Z]", char): 29 | policies["Uppercase characters"] += 1 30 | else: # elif re.match("[\[\] !\"#$%&'()*+,-./:;<=>?@\\^_`{|}~]", char): # This regex can be used, but everything else should be considered special char 31 | policies["Special characters"] += 1 32 | del passphrase # Remove passphrase from memory 33 | 34 | for policy in policies.keys(): 35 | if policies[policy] > 0: 36 | pool_size += entropies[policy] 37 | return math.log2(math.pow(pool_size, passphrase_len)) 38 | -------------------------------------------------------------------------------- /src/python-for-android/recipes/aiohttp/__init__.py: -------------------------------------------------------------------------------- 1 | """Build AIOHTTP""" 2 | from typing import List 3 | from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe 4 | 5 | 6 | class AIOHTTPRecipe(CppCompiledComponentsPythonRecipe): # type: ignore # pylint: disable=R0903 7 | version = "3.8.3" 8 | url = "https://pypi.python.org/packages/source/a/aiohttp/aiohttp-{version}.tar.gz" 9 | name = "aiohttp" 10 | depends: List[str] = ["setuptools"] 11 | call_hostpython_via_targetpython = False 12 | install_in_hostpython = True 13 | 14 | def get_recipe_env(self, arch): 15 | env = super().get_recipe_env(arch) 16 | env['LDFLAGS'] += ' -lc++_shared' 17 | return env 18 | 19 | 20 | recipe = AIOHTTPRecipe() 21 | -------------------------------------------------------------------------------- /src/python-for-android/recipes/pbkdf2/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | from pythonforandroid.recipe import PythonRecipe 3 | 4 | 5 | class Pbkdf2Recipe(PythonRecipe): 6 | 7 | url = 'https://github.com/dlitz/python-pbkdf2/archive/refs/tags/v1.3.zip' 8 | #url = 'https://github.com/dlitz/python-pbkdf2/archive/master.zip' 9 | 10 | depends = ['setuptools'] 11 | 12 | recipe = Pbkdf2Recipe() 13 | -------------------------------------------------------------------------------- /src/qrreader.py: -------------------------------------------------------------------------------- 1 | # https://github.com/Android-for-Python/c4k_qr_example/blob/main/qrreader.py 2 | import webbrowser 3 | from kivy.clock import mainthread 4 | from kivy.metrics import dp 5 | from kivy.graphics import Line, Color, Rectangle 6 | from kivymd.app import MDApp 7 | from pyzbar import pyzbar 8 | from pyzbar.pyzbar import ZBarSymbol 9 | from PIL import Image 10 | 11 | from gestures4kivy import CommonGestures 12 | from camera4kivy import Preview 13 | 14 | 15 | class QRReader(Preview, CommonGestures): 16 | 17 | def __init__(self, cb, **kwargs): 18 | super().__init__(**kwargs) 19 | self.annotations = [] 20 | self.qrcode_decoded = None 21 | self.cb = cb 22 | 23 | #################################### 24 | # Analyze a Frame - NOT on UI Thread 25 | #################################### 26 | 27 | def analyze_pixels_callback(self, pixels, image_size, image_pos, scale, mirror): 28 | # pixels : Kivy Texture pixels 29 | # image_size : pixels size (w,h) 30 | # image_pos : location of Texture in Preview Widget (letterbox) 31 | # scale : scale from Analysis resolution to Preview resolution 32 | # mirror : true if Preview is mirrored 33 | pil_image = Image.frombytes(mode='RGBA', size=image_size, data= pixels) 34 | barcodes = pyzbar.decode(pil_image, symbols=[ZBarSymbol.QRCODE]) 35 | found = [] 36 | if barcodes: 37 | print("barcodes {} ".format(barcodes)) 38 | barcodes[0] 39 | self.disconnect_camera() 40 | #if self.cb: 41 | # self.cb(barcodes[0].data.decode('utf-8')) 42 | self.make_qrcode_decoded_thread_safe(barcodes[0].data.decode('utf-8')) 43 | return 44 | self.make_thread_safe(list(found)) ## A COPY of the list 45 | 46 | @mainthread 47 | def make_thread_safe(self, found): 48 | self.annotations = found 49 | 50 | @mainthread 51 | def make_qrcode_decoded_thread_safe(self, qrcode_decoded): 52 | self.qrcode_decoded = qrcode_decoded 53 | if self.cb: 54 | self.cb(self.qrcode_decoded) 55 | 56 | ################################ 57 | # Annotate Screen - on UI Thread 58 | ################################ 59 | 60 | def canvas_instructions_callback(self, texture, tex_size, tex_pos): 61 | # Add the analysis annotations 62 | Color(1,0,0,1) 63 | for r in self.annotations: 64 | Line(rectangle=(r['x'], r['y'], r['w'], r['h']), width = dp(2)) 65 | 66 | ################################# 67 | # User Touch Event - on UI Thread 68 | ################################# 69 | 70 | def cgb_select(self, touch, x, y, lp): 71 | self.open_browser(x, y) 72 | 73 | def open_browser(self, x, y): 74 | for r in self.annotations: 75 | if x >= r['x'] and x <= r['x'] + r['w'] and\ 76 | y >= r['y'] and y <= r['y'] + r['h']: 77 | webbrowser.open_new_tab(r['t']) 78 | -------------------------------------------------------------------------------- /src/recovery.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import getpass 3 | import nowallet 4 | 5 | if __name__ == "__main__": 6 | print("\n\tBRAINBOW RECOVERY") 7 | print("\n\tWARNING: Entering your salt and passphrase will print your 'BIP-32 Root Master Private Key' on the screen.") 8 | print("") 9 | ok = input("Type 'ok' to continue: ") 10 | if ok != "ok": 11 | print("Okay, 'ok' was not 'ok'. Good bye.") 12 | sys.exit(1) 13 | chain = input("Which chain [BTC or TBTC]: ") # type: str 14 | if chain.lower().strip() == "btc": 15 | chain = nowallet.BTC 16 | else: 17 | chain = nowallet.TBTC 18 | email = input("Enter salt: ") # type: str 19 | 20 | passphrase = getpass.getpass("Enter passphrase: ") # type: str 21 | confirm = getpass.getpass("Confirm your passphrase: ") # type: str 22 | assert passphrase == confirm, "Passphrase and confirmation did not match" 23 | assert email and passphrase, "Email and/or passphrase were blank" 24 | wallet = nowallet.Wallet(email, passphrase, None, None, chain) 25 | print("\n") 26 | print("\t{}".format("*"*120)) 27 | print("\n") 28 | print("\t WARNING: KEEP THIS KEY SECRET!") 29 | print("\n") 30 | print("\t BIP-32 Root Master Private Key for {}".format(wallet.fingerprint)) 31 | print("\t {}".format(wallet.private_BIP32_root_key)) 32 | print("\t") 33 | print("\t{}".format("*"*120)) 34 | print("\n") 35 | -------------------------------------------------------------------------------- /src/scrape.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from typing import List, Tuple 4 | 5 | from bs4 import BeautifulSoup 6 | from .socks_http import urlopen 7 | 8 | 9 | async def scrape_electrum_servers(chain_1209k: str = "tbtc", 10 | loop=None) -> List[Tuple[str, int, str]]: 11 | scrape_page = "https://1209k.com/bitcoin-eye/ele.php?chain={}" # type: str 12 | url = scrape_page.format(chain_1209k) # type: str 13 | logging.info("Scraping URL: %s", url) 14 | 15 | page = await urlopen(url, loop=loop) # type: str 16 | soup = BeautifulSoup(page, "html.parser") # type: BeautifulSoup 17 | table_data = soup.find_all("td") # type: List 18 | testnet_blacklist = ( 19 | "electrum.akinbo.org", 20 | "testnet.hsmiths.com", 21 | "testnet.qtornado.com" 22 | ) 23 | 24 | servers = list() # type: List[Tuple[str, int, str]] 25 | for i, data in enumerate(table_data): 26 | if i % 11 == 0 and "." in data.text: # Every new URL 27 | host = data.text # type: str 28 | port = int(table_data[i+1].text) # type: int 29 | proto = None # type: str 30 | 31 | if table_data[i+2].text == "ssl": 32 | proto = "s" 33 | elif table_data[i+2].text == "tcp": 34 | proto = "t" 35 | 36 | is_running = table_data[i+7].text == "open" # type: bool 37 | if is_running: 38 | if chain_1209k == "tbtc" and host in testnet_blacklist: 39 | continue 40 | servers.append((host, port, proto)) 41 | return servers 42 | 43 | 44 | def main(): 45 | loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop 46 | result = loop.run_until_complete( 47 | scrape_electrum_servers()) # type: List[Tuple[str, int, str]] 48 | print(result) 49 | loop.close() 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /src/settings_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | from exchange_rate import CURRENCIES 3 | 4 | def settings_json(coin="BTC"): 5 | return json.dumps( 6 | [ 7 | { 8 | "type": "bool", 9 | "title": "RBF", 10 | "desc": "Use opt in replace by fee?", 11 | "section": "nowallet", 12 | "key": "rbf", 13 | "default": "true" 14 | }, { 15 | "type": "bool", 16 | "title": "Auto-broadcast", 17 | "desc": "Broadcast transaction immediately?", 18 | "section": "nowallet", 19 | "key": "broadcast_tx", 20 | "default": "true" 21 | }, { 22 | "type": "options", 23 | "title": "Coin Units", 24 | "desc": "Preferred Bitcoin denomination", 25 | "section": "nowallet", 26 | "key": "units", 27 | "options": [coin, "sats"] 28 | }, { 29 | "type": "options", 30 | "title": "Currency", 31 | "desc": "Fiat currency for exchange rates", 32 | "section": "nowallet", 33 | "key": "currency", 34 | "options": CURRENCIES 35 | }, { 36 | "type": "options", 37 | "title": "Block Explorer", 38 | "desc": "Preferred block explorer", 39 | "section": "nowallet", 40 | "key": "explorer", 41 | "options": ["blockcypher", "smartbit"] 42 | }, { 43 | "type": "options", 44 | "title": "Price Provider", 45 | "desc": "Preferred price provider", 46 | "section": "nowallet", 47 | "key": "price_api", 48 | "options": ["CoinGecko", "CryptoCompare"] 49 | } 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /src/socks_http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import aiosocks 4 | from aiosocks.connector import ProxyConnector, ProxyClientRequest 5 | 6 | 7 | class SocksHTTPError(Exception): 8 | pass 9 | 10 | 11 | async def urlopen(url: str, bauth_tuple=None, loop=None) -> str: 12 | bauth = None 13 | if bauth_tuple: 14 | login, password = bauth_tuple 15 | bauth = aiohttp.BasicAuth(login, password=password, encoding='latin1') 16 | auth5 = aiosocks.Socks5Auth( 17 | 'proxyuser1', password='pwd') # type: aiosocks.Socks5Auth 18 | conn = ProxyConnector( 19 | remote_resolve=True, loop=loop) # type: ProxyConnector 20 | 21 | try: 22 | async with aiohttp.ClientSession( 23 | connector=conn, 24 | auth=bauth, 25 | request_class=ProxyClientRequest 26 | ) as session: 27 | async with session.get( 28 | url, 29 | proxy='socks5://127.0.0.1:9050', 30 | proxy_auth=auth5 31 | ) as resp: 32 | 33 | if resp.status == 200: 34 | return await resp.text() 35 | else: 36 | message = "HTTP response not OK: {}".format(resp.status) 37 | raise SocksHTTPError(message) 38 | 39 | except aiohttp.ClientProxyConnectionError: 40 | # connection problem 41 | pass 42 | except aiosocks.SocksError: 43 | # communication problem 44 | pass 45 | return "" # Should never happen 46 | 47 | 48 | def main(): 49 | loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop 50 | html = loop.run_until_complete(urlopen("https://github.com/")) # type: str 51 | print(html) 52 | loop.close() 53 | 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | from logger import logging 2 | import binascii 3 | import datetime 4 | import time 5 | import json 6 | from functools import wraps 7 | from typing import Callable 8 | from urllib import parse 9 | from pycoin.key import validate 10 | from embit import bech32 as embit_bech32 11 | 12 | from decimal import Decimal 13 | 14 | 15 | 16 | from typing import ( 17 | Tuple, List, Set, Dict, KeysView, Any, 18 | Union, Awaitable 19 | ) 20 | 21 | """ 22 | from pycoin.tx.Tx import Tx 23 | tx = Tx.from_hex(hextx) 24 | 25 | """ 26 | 27 | def format_txid(txid): 28 | return "{}...{}".format(txid[:11], txid[-11:]) 29 | 30 | def is_valid_address(addr, netcode): 31 | """ 32 | :param addr: "address" or "pay_to_script" 33 | :param netcode: BTC or XTN 34 | """ 35 | try: 36 | is_valid = addr.strip() and \ 37 | validate.is_address_valid( 38 | addr.strip(), ["address", "pay_to_script"], [netcode]) == netcode 39 | if not is_valid: 40 | addr = addr.lower() 41 | hrp = addr.split("1")[0] 42 | ver, prog = embit_bech32.decode(hrp, addr) 43 | if ver is not None: 44 | if 0 <= ver <= 16 and prog: 45 | is_valid = True 46 | except: 47 | is_valid = False 48 | return is_valid 49 | 50 | 51 | def get_payable_from_BIP21URI(uri: str, proto: str = "bitcoin", netcode="BTC") -> Tuple[str, Decimal]: 52 | """ Computes a 'payable' tuple from a given BIP21 encoded URI. 53 | 54 | :param uri: The BIP21 URI to decode 55 | :param proto: The expected protocol/scheme (case insensitive) 56 | :param netcode: BTC or XTN 57 | :returns: A payable (address, amount) corresponding to the given URI 58 | :raise: Raises s ValueError if there is no address given or if the 59 | protocol/scheme doesn't match what is expected 60 | """ 61 | obj = parse.urlparse(uri) # type: parse.ParseResult 62 | if not obj.path or obj.scheme.upper() != proto.upper(): 63 | try: 64 | if is_valid_address(uri, netcode): 65 | return uri, None 66 | except: 67 | pass 68 | raise ValueError("Malformed URI") 69 | if not obj.query: 70 | return obj.path, None 71 | query = parse.parse_qs(obj.query) # type: Dict 72 | return obj.path, Decimal(query["amount"][0]) 73 | 74 | 75 | def is_txid(txid): 76 | """ Quick and dirty check if this is a txid. """ 77 | if type(txid) == type("") and len(txid) == 64: 78 | return True 79 | return False 80 | 81 | def utxo_deduplication(utxos): 82 | dedup_utxos_text = [] 83 | dedup_utxos = [] 84 | for spendable in utxos: 85 | if spendable.as_text() not in dedup_utxos_text: 86 | dedup_utxos_text.append(spendable.as_text()) 87 | dedup_utxos.append(spendable) 88 | return dedup_utxos # type: pycoin.tx.Spendable.Spendable 89 | 90 | def log_time_elapsed(func: Callable) -> Callable: 91 | """ Decorator. Times completion of function and logs at level INFO. """ 92 | @wraps(func) 93 | def inner(*args, **kwargs) -> None: 94 | """ Decorator inner function. """ 95 | start_time = time.time() # type: float 96 | func(*args, **kwargs) 97 | end_time = time.time() # type: float 98 | seconds = end_time - start_time # type: float 99 | logging.info("Operation completed in {0:.3f} seconds".format(seconds)) 100 | return inner 101 | 102 | def get_timestamp_from_block_header(block_header): 103 | """ https://en.bitcoin.it/wiki/Protocol_documentation#Block_Headers 104 | """ 105 | raw_timestamp = block_header[(4+32+32)*2:(4+32+32+4)*2] 106 | byte_timestamp = bytes(raw_timestamp,'utf-8') 107 | ba = binascii.a2b_hex(byte_timestamp) 108 | int_timestamp = int.from_bytes(ba, byteorder='little', signed=True) 109 | return datetime.datetime.fromtimestamp(int_timestamp) 110 | -------------------------------------------------------------------------------- /src/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.148' 2 | -------------------------------------------------------------------------------- /src/wallet_alias.py: -------------------------------------------------------------------------------- 1 | PREFIX = [ 2 | 'active', 3 | 'adorable', 4 | 'agile', 5 | 'amazing', 6 | 'amiable', 7 | 'amusing', 8 | 'annoyed', 9 | 'aquatic', 10 | 'arctic', 11 | 'balanced', 12 | 'blue', 13 | 'bold', 14 | 'brave', 15 | 'bright', 16 | 'bronze', 17 | 'bubbly', 18 | 'busy', 19 | 'captain', 20 | 'caring', 21 | 'charming', 22 | 'cheeky', 23 | 'cheerful', 24 | 'chic', 25 | 'classy', 26 | 'clever', 27 | 'colorful', 28 | 'cool', 29 | 'couture', 30 | 'cozy', 31 | 'crafty', 32 | 'creative', 33 | 'credible', 34 | 'cultured', 35 | 'curious', 36 | 'cute', 37 | 'dandy', 38 | 'daring', 39 | 'dazzled', 40 | 'decisive', 41 | 'deep', 42 | 'delicate', 43 | 'deluxe', 44 | 'detailed', 45 | 'diligent', 46 | 'diplomat', 47 | 'direct', 48 | 'discreet', 49 | 'doctor', 50 | 'dramatic', 51 | 'dreamy', 52 | 'driven', 53 | 'dynamic', 54 | 'eager', 55 | 'elated', 56 | 'epic', 57 | 'ethereal', 58 | 'ethical', 59 | 'excited', 60 | 'expert', 61 | 'fabulous', 62 | 'fast', 63 | 'fearless', 64 | 'filthy', 65 | 'flat', 66 | 'fluffy', 67 | 'flying', 68 | 'focused', 69 | 'foolish', 70 | 'frantic', 71 | 'fresh', 72 | 'friendly', 73 | 'frothy', 74 | 'funny', 75 | 'fuzzy', 76 | 'gaudy', 77 | 'generous', 78 | 'genius', 79 | 'gentle', 80 | 'ghastly', 81 | 'giddy', 82 | 'giga', 83 | 'giving', 84 | 'glad', 85 | 'gleaming', 86 | 'glorious', 87 | 'glowing', 88 | 'golden', 89 | 'gorgeous', 90 | 'graceful', 91 | 'greasy', 92 | 'great', 93 | 'green', 94 | 'groovy', 95 | 'grumpy', 96 | 'hairy', 97 | 'handsome', 98 | 'happy', 99 | 'hard', 100 | 'healthy', 101 | 'helpful', 102 | 'helpless', 103 | 'hero', 104 | 'high', 105 | 'hollow', 106 | 'homely', 107 | 'honest', 108 | 'huge', 109 | 'hungry', 110 | 'hurt', 111 | 'icy', 112 | 'ideal', 113 | 'idyllic', 114 | 'immense', 115 | 'inspired', 116 | 'irate', 117 | 'itchy', 118 | 'jealous', 119 | 'jittery', 120 | 'jolly', 121 | 'joyful', 122 | 'joyous', 123 | 'jumpy', 124 | 'kind', 125 | 'knowing', 126 | 'large', 127 | 'lazy', 128 | 'legend', 129 | 'lethal', 130 | 'little', 131 | 'lively', 132 | 'livid', 133 | 'logical', 134 | 'lonely', 135 | 'loose', 136 | 'lost', 137 | 'lovely', 138 | 'loving', 139 | 'lucky', 140 | 'lyrical', 141 | 'macho', 142 | 'magic', 143 | 'majestic', 144 | 'massive', 145 | 'melodic', 146 | 'melted', 147 | 'mighty', 148 | 'minimal', 149 | 'misty', 150 | 'moody', 151 | 'mountain', 152 | 'muddy', 153 | 'mystery', 154 | 'narrow', 155 | 'nasty', 156 | 'nervous', 157 | 'nimble', 158 | 'noble', 159 | 'nutty', 160 | 'obedient', 161 | 'odd', 162 | 'panicky', 163 | 'patient', 164 | 'peaceful', 165 | 'perfect', 166 | 'petty', 167 | 'plain', 168 | 'pleasant', 169 | 'poetic', 170 | 'poised', 171 | 'polite', 172 | 'power', 173 | 'precious', 174 | 'precise', 175 | 'pretty', 176 | 'prickly', 177 | 'proud', 178 | 'pungent', 179 | 'puny', 180 | 'purple', 181 | 'quaint', 182 | 'quick', 183 | 'quiet', 184 | 'radiant', 185 | 'rapid', 186 | 'rational', 187 | 'ratty', 188 | 'red', 189 | 'relaxed', 190 | 'reliable', 191 | 'rich', 192 | 'ripe', 193 | 'robust', 194 | 'rocky', 195 | 'rotten', 196 | 'rotund', 197 | 'rough', 198 | 'round', 199 | 'royal', 200 | 'salty', 201 | 'scary', 202 | 'scrawny', 203 | 'secret', 204 | 'selfish', 205 | 'serene', 206 | 'serious', 207 | 'sexy', 208 | 'shaggy', 209 | 'shaky', 210 | 'shallow', 211 | 'sharp', 212 | 'shining', 213 | 'shiny', 214 | 'short', 215 | 'silky', 216 | 'silly', 217 | 'silver', 218 | 'skillful', 219 | 'skinny', 220 | 'slimy', 221 | 'small', 222 | 'smarmy', 223 | 'smart', 224 | 'smiling', 225 | 'smoggy', 226 | 'smooth', 227 | 'smug', 228 | 'snowy', 229 | 'soaring', 230 | 'social', 231 | 'soggy', 232 | 'solid', 233 | 'sore', 234 | 'sour', 235 | 'space', 236 | 'speedy', 237 | 'spicy', 238 | 'spiky', 239 | 'square', 240 | 'stacked', 241 | 'stale', 242 | 'steady', 243 | 'steep', 244 | 'stellar', 245 | 'strong', 246 | 'sturdy', 247 | 'super', 248 | 'swift', 249 | 'tender', 250 | 'tropical', 251 | 'weaving', 252 | 'winged', 253 | 'wise', 254 | 'witty', 255 | 'wonder', 256 | 'yellow', 257 | 'zany'] 258 | 259 | SUFFIX = ['alpaca', 260 | 'ant', 261 | 'anteater', 262 | 'antelope', 263 | 'ape', 264 | 'apollo', 265 | 'baboon', 266 | 'badger', 267 | 'basilisk', 268 | 'bat', 269 | 'bear', 270 | 'beauty', 271 | 'beaver', 272 | 'bee', 273 | 'bigfoot', 274 | 'bimbo', 275 | 'bison', 276 | 'boa', 277 | 'boar', 278 | 'bobcat', 279 | 'buffalo', 280 | 'bull', 281 | 'bunny', 282 | 'camel', 283 | 'captain', 284 | 'capybara', 285 | 'cat', 286 | 'catch', 287 | 'chamois', 288 | 'champion', 289 | 'cheetah', 290 | 'chicken', 291 | 'chough', 292 | 'clam', 293 | 'cobra', 294 | 'cod', 295 | 'comet', 296 | 'condor', 297 | 'cookie', 298 | 'coyote', 299 | 'crab', 300 | 'crane', 301 | 'crow', 302 | 'curlew', 303 | 'deer', 304 | 'dingo', 305 | 'dinosaur', 306 | 'dog', 307 | 'dogfish', 308 | 'dolphin', 309 | 'donkey', 310 | 'dotterel', 311 | 'dove', 312 | 'dragon', 313 | 'dreamer', 314 | 'duck', 315 | 'dugong', 316 | 'dunlin', 317 | 'eagle', 318 | 'echidna', 319 | 'eel', 320 | 'egret', 321 | 'eland', 322 | 'elephant', 323 | 'elk', 324 | 'emu', 325 | 'fairy', 326 | 'falcon', 327 | 'ferret', 328 | 'fighter', 329 | 'finch', 330 | 'fish', 331 | 'flamingo', 332 | 'fly', 333 | 'fogg', 334 | 'fox', 335 | 'frog', 336 | 'galaxy', 337 | 'gator', 338 | 'gaur', 339 | 'gazelle', 340 | 'gecko', 341 | 'gerbil', 342 | 'giraffe', 343 | 'glider', 344 | 'gnat', 345 | 'gnome', 346 | 'gnu', 347 | 'goat', 348 | 'goldfish', 349 | 'goose', 350 | 'gorilla', 351 | 'goshawk', 352 | 'griffin', 353 | 'grouse', 354 | 'guanaco', 355 | 'gull', 356 | 'hamster', 357 | 'hare', 358 | 'hawk', 359 | 'hedgehog', 360 | 'hera', 361 | 'hermes', 362 | 'heron', 363 | 'herring', 364 | 'hippo', 365 | 'hornet', 366 | 'horse', 367 | 'human', 368 | 'hunter', 369 | 'hyena', 370 | 'ibex', 371 | 'ibis', 372 | 'iguana', 373 | 'jackal', 374 | 'jay', 375 | 'jupiter', 376 | 'kangaroo', 377 | 'king', 378 | 'kitten', 379 | 'koala', 380 | 'kouprey', 381 | 'kudu', 382 | 'lapwing', 383 | 'lark', 384 | 'lemming', 385 | 'lemur', 386 | 'leopard', 387 | 'lion', 388 | 'lizard', 389 | 'llama', 390 | 'lobster', 391 | 'locust', 392 | 'loris', 393 | 'louse', 394 | 'lyrebird', 395 | 'macaw', 396 | 'magpie', 397 | 'mallard', 398 | 'manatee', 399 | 'mandrill', 400 | 'mantis', 401 | 'mars', 402 | 'marten', 403 | 'meerkat', 404 | 'mercury', 405 | 'mermaid', 406 | 'mink', 407 | 'mole', 408 | 'mongoose', 409 | 'monkey', 410 | 'moon', 411 | 'moose', 412 | 'mosquito', 413 | 'mouse', 414 | 'mule', 415 | 'narwhal', 416 | 'newt', 417 | 'octopus', 418 | 'okapi', 419 | 'opossum', 420 | 'oryx', 421 | 'ostrich', 422 | 'otter', 423 | 'owl', 424 | 'oyster', 425 | 'panda', 426 | 'panther', 427 | 'parrot', 428 | 'peafowl', 429 | 'pelican', 430 | 'penguin', 431 | 'pheasant', 432 | 'phoenix', 433 | 'pig', 434 | 'pigeon', 435 | 'piranha', 436 | 'planet', 437 | 'pony', 438 | 'porpoise', 439 | 'poseidon', 440 | 'possum', 441 | 'puffin', 442 | 'quail', 443 | 'queen', 444 | 'quelea', 445 | 'quetzal', 446 | 'rabbit', 447 | 'raccoon', 448 | 'rail', 449 | 'ram', 450 | 'rat', 451 | 'raven', 452 | 'realist', 453 | 'reindeer', 454 | 'robo', 455 | 'rockstar', 456 | 'rook', 457 | 'sable', 458 | 'salmon', 459 | 'sardine', 460 | 'saturn', 461 | 'scorpion', 462 | 'seahorse', 463 | 'seal', 464 | 'shark', 465 | 'sheep', 466 | 'shrew', 467 | 'skunk', 468 | 'sky', 469 | 'snail', 470 | 'snake', 471 | 'sparrow', 472 | 'sphinx', 473 | 'spider', 474 | 'squid', 475 | 'squirrel', 476 | 'star', 477 | 'starling', 478 | 'stingray', 479 | 'stinkbug', 480 | 'stork', 481 | 'swallow', 482 | 'swan', 483 | 'tapir', 484 | 'tarsier', 485 | 'taurus', 486 | 'termite', 487 | 'tiger', 488 | 'toad', 489 | 'tree', 490 | 'trout', 491 | 'turkey', 492 | 'turtle', 493 | 'unicorn', 494 | 'urchin', 495 | 'venus', 496 | 'viper', 497 | 'vulture', 498 | 'wallaby', 499 | 'walrus', 500 | 'wasp', 501 | 'weasel', 502 | 'werewolf', 503 | 'whale', 504 | 'wildcat', 505 | 'wolf', 506 | 'wombat', 507 | 'woodcock', 508 | 'worm', 509 | 'wren', 510 | 'yak', 511 | 'yeti', 512 | 'zebra', 513 | 'zeus', 514 | 'zombie'] 515 | 516 | 517 | def wallet_alias(first_byte, second_byte): 518 | first = PREFIX[int(first_byte, base=16)] 519 | second = SUFFIX[int(second_byte, base=16)] 520 | return("{}{}".format(first.capitalize(), second.capitalize())) 521 | --------------------------------------------------------------------------------