├── .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 |
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 |
--------------------------------------------------------------------------------