Compare commits
405 Commits
wow
...
cryptoguar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631ccea626 | ||
|
|
a5c3c692a0 | ||
|
|
b2df4ea80d | ||
|
|
18a7105f20 | ||
|
|
fcdb2e7dfe | ||
|
|
3c5e8481cd | ||
|
|
97bb615176 | ||
|
|
f1c2b41714 | ||
|
|
8d317e4b67 | ||
|
|
45ed2cdb87 | ||
|
|
d64e3f4be9 | ||
|
|
57d885bc0c | ||
|
|
205c6e2b58 | ||
|
|
d95f3ccd24 | ||
|
|
d6a9425b22 | ||
|
|
e4cc5da490 | ||
|
|
e177d36bd4 | ||
|
|
05ffa5e3ac | ||
|
|
6165cbc4c3 | ||
|
|
7e6f94319d | ||
|
|
71fd3d10aa | ||
|
|
e4ed9aebdf | ||
|
|
b97a9f4a27 | ||
|
|
510eff6163 | ||
|
|
efb84f58af | ||
|
|
831ef40977 | ||
|
|
a0456cb689 | ||
|
|
c7818f5fac | ||
|
|
713577d868 | ||
|
|
37be3bcab5 | ||
|
|
4ae97790aa | ||
|
|
8928451af0 | ||
|
|
473e4fd400 | ||
|
|
ff2fc35f72 | ||
|
|
edb3b19dcf | ||
|
|
aac2f51b88 | ||
|
|
57b96cd985 | ||
|
|
4d5551cd84 | ||
|
|
7ee4720738 | ||
|
|
c76fe79848 | ||
|
|
f13c481b51 | ||
|
|
6f776971b1 | ||
|
|
c79ed493aa | ||
|
|
b6709d0cdc | ||
|
|
c945e267e7 | ||
|
|
ef65420978 | ||
|
|
6da4bf6aaf | ||
|
|
6e56b7f421 | ||
|
|
f084c6f538 | ||
|
|
443bd6917f | ||
|
|
b55d126a0a | ||
|
|
586ff3288f | ||
|
|
0398fce5a8 | ||
|
|
ef082ff7be | ||
|
|
168284ce25 | ||
|
|
e797e23625 | ||
|
|
d3fcdc8052 | ||
|
|
2c176a8c86 | ||
|
|
e92d5560af | ||
|
|
0171ad6889 | ||
|
|
5d381d4b73 | ||
|
|
9e24d9a12a | ||
|
|
21ef6f3129 | ||
|
|
67d808cbe4 | ||
|
|
5d1bed6423 | ||
|
|
edc11b4c96 | ||
|
|
5daf591985 | ||
|
|
aee66712b8 | ||
|
|
8de365f9d3 | ||
|
|
765ef9571a | ||
|
|
c575625097 | ||
|
|
fe02441619 | ||
|
|
c992ef571a | ||
|
|
5f275132de | ||
|
|
64151f4203 | ||
|
|
734214af53 | ||
|
|
1cb8ffb632 | ||
|
|
40d06df325 | ||
|
|
62031173f5 | ||
|
|
f473d66de5 | ||
|
|
e548cf2b3b | ||
|
|
d1baf4bc10 | ||
|
|
3b8e084b2e | ||
|
|
0a697c61e8 | ||
|
|
5af59dd8da | ||
|
|
a75cd28995 | ||
|
|
f40d98ef23 | ||
|
|
b14fba0e1f | ||
|
|
4d928dc98e | ||
|
|
1845f802a2 | ||
|
|
7ec9dfa35a | ||
|
|
b70e46ffc1 | ||
|
|
07de2d61af | ||
|
|
65fbcda556 | ||
|
|
40f334ed0e | ||
|
|
77bb3e6353 | ||
|
|
3b60472c04 | ||
|
|
b87e034719 | ||
|
|
def7aae1ec | ||
|
|
294595adbd | ||
|
|
ab04f27497 | ||
|
|
159974d414 | ||
|
|
110b91bb75 | ||
|
|
3cea5449c9 | ||
|
|
07ed0af468 | ||
|
|
feabc619ae | ||
|
|
e3f7b5b79b | ||
|
|
35bede48b0 | ||
|
|
af6154705c | ||
|
|
f010fc0c83 | ||
|
|
3da9221d43 | ||
|
|
a7f0f257b8 | ||
|
|
c095e22fdb | ||
|
|
0c98dff044 | ||
|
|
69cc56e4a7 | ||
|
|
a54e6daaa1 | ||
|
|
3009cacdb2 | ||
|
|
b9bacb9988 | ||
|
|
6905c6a131 | ||
|
|
ce7b94a878 | ||
|
|
c49cdb2e98 | ||
|
|
0ae4651a78 | ||
|
|
12d24800b8 | ||
|
|
c09eab71cc | ||
|
|
5bbafbdb3c | ||
|
|
157b63a5d0 | ||
|
|
341d39a6a3 | ||
|
|
bb8dad1607 | ||
|
|
20bcef1891 | ||
|
|
f9bf29e68c | ||
|
|
820e5af5fb | ||
|
|
681122bcca | ||
|
|
9418ea4385 | ||
|
|
73ab5e7391 | ||
|
|
bf6d07a726 | ||
|
|
e4849d6dfe | ||
|
|
2002fcb31b | ||
|
|
21c828051c | ||
|
|
c7e84e2249 | ||
|
|
a3645c286d | ||
|
|
618df98abf | ||
|
|
4bbf739786 | ||
|
|
878a145420 | ||
|
|
32bd44b19a | ||
|
|
c5ced6994a | ||
|
|
2929e74c78 | ||
|
|
0c01dcf2f5 | ||
|
|
9eacd35319 | ||
|
|
ca6af04eba | ||
|
|
691e3f1b82 | ||
|
|
80dbbd3d12 | ||
|
|
28d99c4c0f | ||
|
|
3f8012f0d0 | ||
|
|
a53de511ce | ||
|
|
34eb5900fb | ||
|
|
514f7efc6e | ||
|
|
de81ec5d75 | ||
|
|
4b23834af8 | ||
|
|
0e2be676db | ||
|
|
3be72b3c71 | ||
|
|
889ffaaa33 | ||
|
|
50515568d8 | ||
|
|
56f96291e4 | ||
|
|
f5db8cf7ce | ||
|
|
ea91647862 | ||
|
|
d7a5467f4f | ||
|
|
95db6655e7 | ||
|
|
36ec1e8683 | ||
|
|
1797db97a0 | ||
|
|
10964f0f51 | ||
|
|
d2733b704d | ||
|
|
b1401ee00b | ||
|
|
e71589a292 | ||
|
|
54f56e0e2c | ||
|
|
73543a5477 | ||
|
|
7ad92b1bbd | ||
|
|
a1e2592965 | ||
|
|
ff29100fd4 | ||
|
|
059356ccd8 | ||
|
|
5d0c7d28e4 | ||
|
|
75d0ca926f | ||
|
|
8582dc479b | ||
|
|
b7383d99dc | ||
|
|
d88f5728a4 | ||
|
|
6d66ee8653 | ||
|
|
ec21ea05bf | ||
|
|
bba517c8b7 | ||
|
|
ebcc4ccb06 | ||
|
|
656335b541 | ||
|
|
e39613f49d | ||
|
|
706d251ef4 | ||
|
|
80e17c739e | ||
|
|
69ca41c68d | ||
|
|
6f61c7d26d | ||
|
|
b4a08ce15e | ||
|
|
744ad7988a | ||
|
|
56cd6da556 | ||
|
|
42955af42c | ||
|
|
f20a9fd75b | ||
|
|
4942f23de6 | ||
|
|
037851a002 | ||
|
|
cf92c5635d | ||
|
|
5f7abbb2eb | ||
|
|
fdb02d10d6 | ||
|
|
0dc55fc449 | ||
|
|
790a550e7f | ||
|
|
d19a7538fd | ||
|
|
913cdfa984 | ||
|
|
289b2a53db | ||
|
|
5941f2952e | ||
|
|
414947cbb5 | ||
|
|
435e74f83a | ||
|
|
0ca5aefc12 | ||
|
|
938c641736 | ||
|
|
68b066a2d1 | ||
|
|
c5508fe9be | ||
|
|
418e863d26 | ||
|
|
1bb721edf5 | ||
|
|
801006fa70 | ||
|
|
1763dec981 | ||
|
|
128291a36a | ||
|
|
31ead537c9 | ||
|
|
25cfcc7cee | ||
|
|
e7a70f1e26 | ||
|
|
ad43ce4095 | ||
|
|
b1b00b5342 | ||
|
|
a4dc9af301 | ||
|
|
eefaab1752 | ||
|
|
c16dd1bba3 | ||
|
|
08df0ceae0 | ||
|
|
26de907185 | ||
|
|
e90800884a | ||
|
|
bebbba49ff | ||
|
|
d417a46e67 | ||
|
|
1b36154142 | ||
|
|
f4f64423a4 | ||
|
|
3ba2145cc9 | ||
|
|
9386544b3d | ||
|
|
3e3a83e6d4 | ||
|
|
33105a832f | ||
|
|
01f6a1d877 | ||
|
|
4143f1a8ce | ||
|
|
ea41c4b41a | ||
|
|
bd571702cb | ||
|
|
fa8764342e | ||
|
|
757f8f2762 | ||
|
|
0bd626d659 | ||
|
|
5db8d6ccbe | ||
|
|
eb30ef22fc | ||
|
|
2e4be0274a | ||
|
|
28af80873a | ||
|
|
8795ecc231 | ||
|
|
5bf20370eb | ||
|
|
893fc87b28 | ||
|
|
6e0f6dabe4 | ||
|
|
2983238ef5 | ||
|
|
3b86985ae3 | ||
|
|
732c87b013 | ||
|
|
6be9a14335 | ||
|
|
f93fae6696 | ||
|
|
373525b364 | ||
|
|
7b03ce4769 | ||
|
|
b484827c15 | ||
|
|
b5f6eb6526 | ||
|
|
e28d41ed0c | ||
|
|
aefb094694 | ||
|
|
cf1811cec3 | ||
|
|
273da833db | ||
|
|
f6916f093b | ||
|
|
51c1179326 | ||
|
|
6f0123e13e | ||
|
|
ca5b9e5e00 | ||
|
|
283cfc7c59 | ||
|
|
e05aaeba26 | ||
|
|
8d96ea7fcf | ||
|
|
b400669919 | ||
|
|
fc17fa41ff | ||
|
|
a214866b08 | ||
|
|
62e6978be1 | ||
|
|
00d70f8cc7 | ||
|
|
60cca03d31 | ||
|
|
52ae633c21 | ||
|
|
f02920721f | ||
|
|
765fadb0ed | ||
|
|
ccc90ccb67 | ||
|
|
bff3c45976 | ||
|
|
17308f9a66 | ||
|
|
ed80caf532 | ||
|
|
465f910812 | ||
|
|
a7c2fbba1f | ||
|
|
b7da4f2096 | ||
|
|
b73907bb84 | ||
|
|
499b086b57 | ||
|
|
b83f289013 | ||
|
|
3ea832bd03 | ||
|
|
1b43806d51 | ||
|
|
745d1460e5 | ||
|
|
15e7a6efda | ||
|
|
602682a2f4 | ||
|
|
2296198b44 | ||
|
|
cc3ef1c065 | ||
|
|
1d5d6004bc | ||
|
|
3345d56f5b | ||
|
|
e23216df07 | ||
|
|
3cab753398 | ||
|
|
5e71367c21 | ||
|
|
1eca1b60ab | ||
|
|
01c8130535 | ||
|
|
062cc6dbdc | ||
|
|
72bfcd3521 | ||
|
|
33cf81a76d | ||
|
|
445aa116ad | ||
|
|
bdc173187d | ||
|
|
87ad321987 | ||
|
|
19c6cff7d3 | ||
|
|
996c67beea | ||
|
|
e2fe0697ee | ||
|
|
0a5680da13 | ||
|
|
264f4d209f | ||
|
|
3ee69ea11a | ||
|
|
c523754516 | ||
|
|
13015e3da9 | ||
|
|
f7141dd0c9 | ||
|
|
3430776ffc | ||
|
|
48a46aea47 | ||
|
|
eb7f3b54ec | ||
|
|
c43d46c7e8 | ||
|
|
7d77d46fa2 | ||
|
|
934e809ac3 | ||
|
|
8081f22e92 | ||
|
|
c9b99dd67a | ||
|
|
fe83736ec7 | ||
|
|
817d2c1e9c | ||
|
|
c0d9b7c161 | ||
|
|
014ee22b79 | ||
|
|
cbd0898eb1 | ||
|
|
63d27b4a6f | ||
|
|
fdfa03eaaf | ||
|
|
c9d1129e93 | ||
|
|
376b485261 | ||
|
|
75fa008f0a | ||
|
|
54983913e1 | ||
|
|
c6f3c684a8 | ||
|
|
39aad231cd | ||
|
|
bd06c435e9 | ||
|
|
fb1caea4de | ||
|
|
d8430f4ca9 | ||
|
|
f7315d405d | ||
|
|
bcd251c4df | ||
|
|
3f963f3329 | ||
|
|
4117c461bb | ||
|
|
8c6ea947ba | ||
|
|
f2a3fc1da1 | ||
|
|
771ad2586a | ||
|
|
60a3956c07 | ||
|
|
d097846756 | ||
|
|
1209d1b269 | ||
|
|
209dea52b3 | ||
|
|
f954822d74 | ||
|
|
dbdb89cd10 | ||
|
|
c53e426989 | ||
|
|
484ad0ca38 | ||
|
|
ac7f24daff | ||
|
|
c1f724ac5e | ||
|
|
d5f643aab9 | ||
|
|
72fc065928 | ||
|
|
25b479fdb6 | ||
|
|
4b18ddfe79 | ||
|
|
bdea7de27e | ||
|
|
bcd9d5c9af | ||
|
|
fe976810e3 | ||
|
|
033167a451 | ||
|
|
cdfb9132ad | ||
|
|
b6d29a33d2 | ||
|
|
003d7b85ab | ||
|
|
1b585ea5c9 | ||
|
|
47a80dc603 | ||
|
|
b29d37a8be | ||
|
|
60369fc2a4 | ||
|
|
3927d823c0 | ||
|
|
1d58dfdc94 | ||
|
|
7ac75f7344 | ||
|
|
80c43056cc | ||
|
|
1564655777 | ||
|
|
2d243fc310 | ||
|
|
75ad5a5b4d | ||
|
|
5e69bf172c | ||
|
|
f307332409 | ||
|
|
56378d168b | ||
|
|
274be9d716 | ||
|
|
eec0760e44 | ||
|
|
6ed108e741 | ||
|
|
7429dc5b2d | ||
|
|
9a900a5bac | ||
|
|
ba4796c763 | ||
|
|
57f238d48e | ||
|
|
d93a73c29e | ||
|
|
94303cff93 | ||
|
|
a977cfe857 | ||
|
|
00912b277a | ||
|
|
40eff0ce0f | ||
|
|
9835d33d12 | ||
|
|
64165e387e | ||
|
|
34d94760a3 | ||
|
|
713990f57b |
17
.cirrus.yml
@@ -3,11 +3,10 @@ container:
|
|||||||
|
|
||||||
lint_task:
|
lint_task:
|
||||||
setup_script:
|
setup_script:
|
||||||
- pip install flake8
|
- pip install flake8 codespell
|
||||||
- pip install codespell
|
|
||||||
script:
|
script:
|
||||||
- flake8 --version
|
- flake8 --version
|
||||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||||
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||||
|
|
||||||
test_task:
|
test_task:
|
||||||
@@ -17,25 +16,21 @@ test_task:
|
|||||||
- BIN_DIR: /tmp/cached_bin
|
- BIN_DIR: /tmp/cached_bin
|
||||||
- PARTICL_BINDIR: ${BIN_DIR}/particl
|
- PARTICL_BINDIR: ${BIN_DIR}/particl
|
||||||
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
|
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
|
||||||
|
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
|
||||||
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
|
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
|
||||||
- XMR_BINDIR: ${BIN_DIR}/monero
|
- XMR_BINDIR: ${BIN_DIR}/monero
|
||||||
setup_script:
|
setup_script:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y wget python3-pip gnupg unzip automake libtool pkg-config
|
- apt-get install -y python3-pip pkg-config
|
||||||
- pip install tox pytest
|
- pip install tox pytest
|
||||||
- python3 setup.py install
|
- pip install .
|
||||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
|
||||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
|
||||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
|
||||||
- cd coincurve-anonswap
|
|
||||||
- python3 setup.py install --force
|
|
||||||
bins_cache:
|
bins_cache:
|
||||||
folder: /tmp/cached_bin
|
folder: /tmp/cached_bin
|
||||||
reupload_on_changes: false
|
reupload_on_changes: false
|
||||||
fingerprint_script:
|
fingerprint_script:
|
||||||
- basicswap-prepare -v
|
- basicswap-prepare -v
|
||||||
populate_script:
|
populate_script:
|
||||||
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
|
||||||
script:
|
script:
|
||||||
- cd "${CIRRUS_WORKING_DIR}"
|
- cd "${CIRRUS_WORKING_DIR}"
|
||||||
- export DATADIRS="${TEST_DIR}"
|
- export DATADIRS="${TEST_DIR}"
|
||||||
|
|||||||
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 20
|
||||||
|
target-branch: "dev"
|
||||||
71
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
BIN_DIR: /tmp/cached_bin
|
||||||
|
TEST_RELOAD_PATH: /tmp/test_basicswap
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.12"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install flake8 codespell pytest
|
||||||
|
pip install -r requirements.txt --require-hashes
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
pip install .
|
||||||
|
# Print the core versions to a file for caching
|
||||||
|
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
|
||||||
|
cat core_versions.txt
|
||||||
|
- name: Running flake8
|
||||||
|
run: |
|
||||||
|
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||||
|
- name: Running codespell
|
||||||
|
run: |
|
||||||
|
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||||
|
- name: Running test_other
|
||||||
|
run: |
|
||||||
|
pytest tests/basicswap/test_other.py
|
||||||
|
- name: Cache coin cores
|
||||||
|
id: cache-cores
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-cores
|
||||||
|
with:
|
||||||
|
path: /tmp/cached_bin
|
||||||
|
key: cores-${{ runner.os }}-${{ hashFiles('**/core_versions.txt') }}
|
||||||
|
|
||||||
|
- if: ${{ steps.cache-cores.outputs.cache-hit != 'true' }}
|
||||||
|
name: Running basicswap-prepare
|
||||||
|
run: |
|
||||||
|
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
|
||||||
|
- name: Running test_xmr
|
||||||
|
run: |
|
||||||
|
export PYTHONPATH=$(pwd)
|
||||||
|
export PARTICL_BINDIR="$BIN_DIR/particl"
|
||||||
|
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
|
||||||
|
export XMR_BINDIR="$BIN_DIR/monero"
|
||||||
|
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
|
||||||
|
- name: Running test_encrypted_xmr_reload
|
||||||
|
run: |
|
||||||
|
export PYTHONPATH=$(pwd)
|
||||||
|
export TEST_PATH=${TEST_RELOAD_PATH}
|
||||||
|
mkdir -p ${TEST_PATH}/bin
|
||||||
|
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
|
||||||
|
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
|
||||||
3
.gitignore
vendored
@@ -13,3 +13,6 @@ __pycache__
|
|||||||
# geckodriver.log
|
# geckodriver.log
|
||||||
*.log
|
*.log
|
||||||
docker/.env
|
docker/.env
|
||||||
|
|
||||||
|
# vscode dev container settings
|
||||||
|
compose-dev.yaml
|
||||||
60
.travis.yml
@@ -1,60 +0,0 @@
|
|||||||
dist: bionic
|
|
||||||
os: linux
|
|
||||||
language: python
|
|
||||||
python: '3.7'
|
|
||||||
stages:
|
|
||||||
- lint
|
|
||||||
- test
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- TEST_DIR=${HOME}/test_basicswap2
|
|
||||||
- TEST_RELOAD_PATH=~/test_basicswap1
|
|
||||||
- BIN_DIR=~/cached_bin
|
|
||||||
- PARTICL_BINDIR=${BIN_DIR}/particl
|
|
||||||
- BITCOIN_BINDIR=${BIN_DIR}/bitcoin
|
|
||||||
- LITECOIN_BINDIR=${BIN_DIR}/litecoin
|
|
||||||
- XMR_BINDIR=${BIN_DIR}/monero
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- "$BIN_DIR"
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get install -y wget python3-pip gnupg unzip automake libtool pkg-config
|
|
||||||
install:
|
|
||||||
- travis_retry pip install tox pytest
|
|
||||||
before_script:
|
|
||||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
|
||||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
|
||||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
|
||||||
- cd coincurve-anonswap
|
|
||||||
- python3 setup.py install --force
|
|
||||||
script:
|
|
||||||
- cd $TRAVIS_BUILD_DIR
|
|
||||||
- python3 setup.py install
|
|
||||||
- basicswap-prepare --bindir=${BIN_DIR} --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
|
||||||
- export DATADIRS="${TEST_DIR}"
|
|
||||||
- mkdir -p "${DATADIRS}/bin"
|
|
||||||
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
|
|
||||||
- mkdir -p "${TEST_RELOAD_PATH}/bin"
|
|
||||||
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
|
|
||||||
- # tox
|
|
||||||
- pytest tests/basicswap/test_xmr.py
|
|
||||||
- pytest tests/basicswap/test_xmr_reload.py
|
|
||||||
- pytest tests/basicswap/test_xmr_bids_offline.py
|
|
||||||
after_success:
|
|
||||||
- echo "End test"
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: lint
|
|
||||||
env:
|
|
||||||
cache: false
|
|
||||||
install:
|
|
||||||
- travis_retry pip install flake8==3.7.0
|
|
||||||
- travis_retry pip install codespell==1.15.0
|
|
||||||
before_script:
|
|
||||||
script:
|
|
||||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
|
||||||
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
|
||||||
after_success:
|
|
||||||
- echo "End lint"
|
|
||||||
- stage: test
|
|
||||||
env:
|
|
||||||
12
Dockerfile
@@ -5,18 +5,12 @@ ENV LANG=C.UTF-8 \
|
|||||||
DATADIRS="/coindata"
|
DATADIRS="/coindata"
|
||||||
|
|
||||||
RUN apt-get update; \
|
RUN apt-get update; \
|
||||||
apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata;
|
apt-get install -y --no-install-recommends \
|
||||||
|
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata cmake ninja-build;
|
||||||
ARG COINCURVE_VERSION=v0.2
|
|
||||||
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
|
|
||||||
unzip coincurve-anonswap.zip && \
|
|
||||||
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
|
|
||||||
cd coincurve-anonswap && \
|
|
||||||
python3 setup.py install --force
|
|
||||||
|
|
||||||
# Install requirements first so as to skip in subsequent rebuilds
|
# Install requirements first so as to skip in subsequent rebuilds
|
||||||
COPY ./requirements.txt requirements.txt
|
COPY ./requirements.txt requirements.txt
|
||||||
RUN pip3 install -r requirements.txt
|
RUN pip3 install -r requirements.txt --require-hashes
|
||||||
|
|
||||||
COPY . basicswap-master
|
COPY . basicswap-master
|
||||||
RUN cd basicswap-master; \
|
RUN cd basicswap-master; \
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
include *.md LICENSE
|
|
||||||
|
|
||||||
recursive-include doc *
|
|
||||||
recursive-include basicswap/templates *
|
|
||||||
recursive-include basicswap/static *
|
|
||||||
20
README.md
@@ -64,6 +64,12 @@ BasicSwap is compatible with the following digital assets.
|
|||||||
<td>XMR
|
<td>XMR
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Bitcoin Cash
|
||||||
|
</td>
|
||||||
|
<td>BCH
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Dash
|
<td>Dash
|
||||||
</td>
|
</td>
|
||||||
@@ -106,6 +112,18 @@ BasicSwap is compatible with the following digital assets.
|
|||||||
<td>PART
|
<td>PART
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dogecoin
|
||||||
|
</td>
|
||||||
|
<td>DOGE
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Namecoin
|
||||||
|
</td>
|
||||||
|
<td>NMC
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
If you’d like to add a cryptocurrency to BasicSwap, refer to how other cryptocurrencies have been integrated to the DEX by following [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html).
|
If you’d like to add a cryptocurrency to BasicSwap, refer to how other cryptocurrencies have been integrated to the DEX by following [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html).
|
||||||
@@ -122,7 +140,7 @@ If you’d like to add a cryptocurrency to BasicSwap, refer to how other cryptoc
|
|||||||
|
|
||||||
Follow the guides on [Particl Academy](https://academy.particl.io) for tutorials and guides on how BasicSwap works.
|
Follow the guides on [Particl Academy](https://academy.particl.io) for tutorials and guides on how BasicSwap works.
|
||||||
|
|
||||||
* [Download BasicSwapDEX](https://github.com/tecnovert/basicswap/tree/master/doc)
|
* [Download BasicSwapDEX](https://github.com/basicswap/basicswap/tree/master/doc)
|
||||||
|
|
||||||
#### Community chat support
|
#### Community chat support
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
name = "basicswap"
|
name = "basicswap"
|
||||||
|
|
||||||
__version__ = "0.13.2"
|
__version__ = "0.14.3"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2024 tecnovert
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -18,12 +19,18 @@ import subprocess
|
|||||||
|
|
||||||
from sockshandler import SocksiPyHandler
|
from sockshandler import SocksiPyHandler
|
||||||
|
|
||||||
|
from .db import (
|
||||||
|
DBMethods,
|
||||||
|
)
|
||||||
from .rpc import (
|
from .rpc import (
|
||||||
callrpc,
|
callrpc,
|
||||||
)
|
)
|
||||||
from .util import (
|
from .util import (
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
)
|
)
|
||||||
|
from .util.logging import (
|
||||||
|
BSXLogger,
|
||||||
|
)
|
||||||
from .chainparams import (
|
from .chainparams import (
|
||||||
Coins,
|
Coins,
|
||||||
chainparams,
|
chainparams,
|
||||||
@@ -34,8 +41,8 @@ def getaddrinfo_tor(*args):
|
|||||||
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
|
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
|
||||||
|
|
||||||
|
|
||||||
class BaseApp:
|
class BaseApp(DBMethods):
|
||||||
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
|
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
|
||||||
self.log_name = log_name
|
self.log_name = log_name
|
||||||
self.fp = fp
|
self.fp = fp
|
||||||
self.fail_code = 0
|
self.fail_code = 0
|
||||||
@@ -47,19 +54,19 @@ class BaseApp:
|
|||||||
self.coin_clients = {}
|
self.coin_clients = {}
|
||||||
self.coin_interfaces = {}
|
self.coin_interfaces = {}
|
||||||
self.mxDB = threading.Lock()
|
self.mxDB = threading.Lock()
|
||||||
self.debug = self.settings.get('debug', False)
|
self.debug = self.settings.get("debug", False)
|
||||||
self.delay_event = threading.Event()
|
self.delay_event = threading.Event()
|
||||||
self.chainstate_delay_event = threading.Event()
|
self.chainstate_delay_event = threading.Event()
|
||||||
|
|
||||||
self._network = None
|
self._network = None
|
||||||
self.prepareLogging()
|
self.prepareLogging()
|
||||||
self.log.info('Network: {}'.format(self.chain))
|
self.log.info("Network: {}".format(self.chain))
|
||||||
|
|
||||||
self.use_tor_proxy = self.settings.get('use_tor', False)
|
self.use_tor_proxy = self.settings.get("use_tor", False)
|
||||||
self.tor_proxy_host = self.settings.get('tor_proxy_host', '127.0.0.1')
|
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
|
||||||
self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050)
|
self.tor_proxy_port = self.settings.get("tor_proxy_port", 9050)
|
||||||
self.tor_control_password = self.settings.get('tor_control_password', None)
|
self.tor_control_password = self.settings.get("tor_control_password", None)
|
||||||
self.tor_control_port = self.settings.get('tor_control_port', 9051)
|
self.tor_control_port = self.settings.get("tor_control_port", 9051)
|
||||||
self.default_socket = socket.socket
|
self.default_socket = socket.socket
|
||||||
self.default_socket_timeout = socket.getdefaulttimeout()
|
self.default_socket_timeout = socket.getdefaulttimeout()
|
||||||
self.default_socket_getaddrinfo = socket.getaddrinfo
|
self.default_socket_getaddrinfo = socket.getaddrinfo
|
||||||
@@ -71,16 +78,24 @@ class BaseApp:
|
|||||||
self.delay_event.set()
|
self.delay_event.set()
|
||||||
|
|
||||||
def prepareLogging(self):
|
def prepareLogging(self):
|
||||||
|
logging.setLoggerClass(BSXLogger)
|
||||||
self.log = logging.getLogger(self.log_name)
|
self.log = logging.getLogger(self.log_name)
|
||||||
self.log.propagate = False
|
self.log.propagate = False
|
||||||
|
|
||||||
# Remove any existing handlers
|
# Remove any existing handlers
|
||||||
self.log.handlers = []
|
self.log.handlers = []
|
||||||
|
|
||||||
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S')
|
formatter = logging.Formatter(
|
||||||
|
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
stream_stdout = logging.StreamHandler()
|
stream_stdout = logging.StreamHandler()
|
||||||
if self.log_name != 'BasicSwap':
|
if self.log_name != "BasicSwap":
|
||||||
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S'))
|
stream_stdout.setFormatter(
|
||||||
|
logging.Formatter(
|
||||||
|
"%(asctime)s %(name)s %(levelname)s : %(message)s",
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
stream_stdout.setFormatter(formatter)
|
stream_stdout.setFormatter(formatter)
|
||||||
stream_fp = logging.StreamHandler(self.fp)
|
stream_fp = logging.StreamHandler(self.fp)
|
||||||
@@ -92,67 +107,91 @@ class BaseApp:
|
|||||||
|
|
||||||
def getChainClientSettings(self, coin):
|
def getChainClientSettings(self, coin):
|
||||||
try:
|
try:
|
||||||
return self.settings['chainclients'][chainparams[coin]['name']]
|
return self.settings["chainclients"][chainparams[coin]["name"]]
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def setDaemonPID(self, name, pid) -> None:
|
def setDaemonPID(self, name, pid) -> None:
|
||||||
if isinstance(name, Coins):
|
if isinstance(name, Coins):
|
||||||
self.coin_clients[name]['pid'] = pid
|
self.coin_clients[name]["pid"] = pid
|
||||||
return
|
return
|
||||||
for c, v in self.coin_clients.items():
|
for c, v in self.coin_clients.items():
|
||||||
if v['name'] == name:
|
if v["name"] == name:
|
||||||
v['pid'] = pid
|
v["pid"] = pid
|
||||||
|
|
||||||
def getChainDatadirPath(self, coin) -> str:
|
def getChainDatadirPath(self, coin) -> str:
|
||||||
datadir = self.coin_clients[coin]['datadir']
|
datadir = self.coin_clients[coin]["datadir"]
|
||||||
testnet_name = '' if self.chain == 'mainnet' else chainparams[coin][self.chain].get('name', self.chain)
|
testnet_name = (
|
||||||
|
""
|
||||||
|
if self.chain == "mainnet"
|
||||||
|
else chainparams[coin][self.chain].get("name", self.chain)
|
||||||
|
)
|
||||||
return os.path.join(datadir, testnet_name)
|
return os.path.join(datadir, testnet_name)
|
||||||
|
|
||||||
def getCoinIdFromName(self, coin_name: str):
|
def getCoinIdFromName(self, coin_name: str):
|
||||||
for c, params in chainparams.items():
|
for c, params in chainparams.items():
|
||||||
if coin_name.lower() == params['name'].lower():
|
if coin_name.lower() == params["name"].lower():
|
||||||
return c
|
return c
|
||||||
raise ValueError('Unknown coin: {}'.format(coin_name))
|
raise ValueError("Unknown coin: {}".format(coin_name))
|
||||||
|
|
||||||
def callrpc(self, method, params=[], wallet=None):
|
def callrpc(self, method, params=[], wallet=None):
|
||||||
cc = self.coin_clients[Coins.PART]
|
cc = self.coin_clients[Coins.PART]
|
||||||
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
|
return callrpc(
|
||||||
|
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
|
||||||
|
)
|
||||||
|
|
||||||
def callcoinrpc(self, coin, method, params=[], wallet=None):
|
def callcoinrpc(self, coin, method, params=[], wallet=None):
|
||||||
cc = self.coin_clients[coin]
|
cc = self.coin_clients[coin]
|
||||||
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
|
return callrpc(
|
||||||
|
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
|
||||||
|
)
|
||||||
|
|
||||||
def callcoincli(self, coin_type, params, wallet=None, timeout=None):
|
def callcoincli(self, coin_type, params, wallet=None, timeout=None):
|
||||||
bindir = self.coin_clients[coin_type]['bindir']
|
bindir = self.coin_clients[coin_type]["bindir"]
|
||||||
datadir = self.coin_clients[coin_type]['datadir']
|
datadir = self.coin_clients[coin_type]["datadir"]
|
||||||
command_cli = os.path.join(bindir, chainparams[coin_type]['name'] + '-cli' + ('.exe' if os.name == 'nt' else ''))
|
cli_bin: str = chainparams[coin_type].get(
|
||||||
args = [command_cli, ]
|
"cli_binname", chainparams[coin_type]["name"] + "-cli"
|
||||||
if self.chain != 'mainnet':
|
)
|
||||||
args.append('-' + self.chain)
|
command_cli = os.path.join(
|
||||||
args.append('-datadir=' + datadir)
|
bindir, cli_bin + (".exe" if os.name == "nt" else "")
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
command_cli,
|
||||||
|
]
|
||||||
|
if self.chain != "mainnet":
|
||||||
|
args.append("-" + self.chain)
|
||||||
|
args.append("-datadir=" + datadir)
|
||||||
args += shlex.split(params)
|
args += shlex.split(params)
|
||||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(
|
||||||
|
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
out = p.communicate(timeout=timeout)
|
out = p.communicate(timeout=timeout)
|
||||||
if len(out[1]) > 0:
|
if len(out[1]) > 0:
|
||||||
raise ValueError('CLI error ' + str(out[1]))
|
raise ValueError("CLI error " + str(out[1]))
|
||||||
return out[0].decode('utf-8').strip()
|
return out[0].decode("utf-8").strip()
|
||||||
|
|
||||||
def is_transient_error(self, ex) -> bool:
|
def is_transient_error(self, ex) -> bool:
|
||||||
if isinstance(ex, TemporaryError):
|
if isinstance(ex, TemporaryError):
|
||||||
return True
|
return True
|
||||||
str_error = str(ex).lower()
|
str_error = str(ex).lower()
|
||||||
return 'read timed out' in str_error or 'no connection to daemon' in str_error
|
return "read timed out" in str_error or "no connection to daemon" in str_error
|
||||||
|
|
||||||
def setConnectionParameters(self, timeout=120):
|
def setConnectionParameters(self, timeout=120):
|
||||||
opener = urllib.request.build_opener()
|
opener = urllib.request.build_opener()
|
||||||
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
opener.addheaders = [("User-agent", "Mozilla/5.0")]
|
||||||
urllib.request.install_opener(opener)
|
urllib.request.install_opener(opener)
|
||||||
|
|
||||||
if self.use_tor_proxy:
|
if self.use_tor_proxy:
|
||||||
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port, rdns=True)
|
socks.setdefaultproxy(
|
||||||
|
socks.PROXY_TYPE_SOCKS5,
|
||||||
|
self.tor_proxy_host,
|
||||||
|
self.tor_proxy_port,
|
||||||
|
rdns=True,
|
||||||
|
)
|
||||||
socket.socket = socks.socksocket
|
socket.socket = socks.socksocket
|
||||||
socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail
|
socket.getaddrinfo = (
|
||||||
|
getaddrinfo_tor # Without this accessing .onion links would fail
|
||||||
|
)
|
||||||
|
|
||||||
socket.setdefaulttimeout(timeout)
|
socket.setdefaulttimeout(timeout)
|
||||||
|
|
||||||
@@ -162,12 +201,19 @@ class BaseApp:
|
|||||||
socket.getaddrinfo = self.default_socket_getaddrinfo
|
socket.getaddrinfo = self.default_socket_getaddrinfo
|
||||||
socket.setdefaulttimeout(self.default_socket_timeout)
|
socket.setdefaulttimeout(self.default_socket_timeout)
|
||||||
|
|
||||||
def readURL(self, url: str, timeout: int = 120, headers=None) -> bytes:
|
def readURL(self, url: str, timeout: int = 120, headers={}) -> bytes:
|
||||||
open_handler = None
|
open_handler = None
|
||||||
if self.use_tor_proxy:
|
if self.use_tor_proxy:
|
||||||
open_handler = SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port)
|
open_handler = SocksiPyHandler(
|
||||||
opener = urllib.request.build_opener(open_handler) if self.use_tor_proxy else urllib.request.build_opener()
|
socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port
|
||||||
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
)
|
||||||
|
opener = (
|
||||||
|
urllib.request.build_opener(open_handler)
|
||||||
|
if self.use_tor_proxy
|
||||||
|
else urllib.request.build_opener()
|
||||||
|
)
|
||||||
|
if headers is None:
|
||||||
|
opener.addheaders = [("User-agent", "Mozilla/5.0")]
|
||||||
request = urllib.request.Request(url, headers=headers)
|
request = urllib.request.Request(url, headers=headers)
|
||||||
return opener.open(request, timeout=timeout).read()
|
return opener.open(request, timeout=timeout).read()
|
||||||
|
|
||||||
@@ -178,7 +224,9 @@ class BaseApp:
|
|||||||
|
|
||||||
def torControl(self, query):
|
def torControl(self, query):
|
||||||
try:
|
try:
|
||||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(self.tor_control_password, query).encode('utf-8')
|
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
|
||||||
|
self.tor_control_password, query
|
||||||
|
).encode("utf-8")
|
||||||
c = socket.create_connection((self.tor_proxy_host, self.tor_control_port))
|
c = socket.create_connection((self.tor_proxy_host, self.tor_control_port))
|
||||||
c.send(command)
|
c.send(command)
|
||||||
response = bytearray()
|
response = bytearray()
|
||||||
@@ -190,23 +238,23 @@ class BaseApp:
|
|||||||
c.close()
|
c.close()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f'torControl {e}')
|
self.log.error(f"torControl {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def getTime(self) -> int:
|
def getTime(self) -> int:
|
||||||
return int(time.time()) + self.mock_time_offset
|
return int(time.time()) + self.mock_time_offset
|
||||||
|
|
||||||
def setMockTimeOffset(self, new_offset: int) -> None:
|
def setMockTimeOffset(self, new_offset: int) -> None:
|
||||||
self.log.warning(f'Setting mocktime to {new_offset}')
|
self.log.warning(f"Setting mocktime to {new_offset}")
|
||||||
self.mock_time_offset = new_offset
|
self.mock_time_offset = new_offset
|
||||||
|
|
||||||
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
|
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
|
||||||
value: int = self.settings.get(name, default_v)
|
value: int = self.settings.get(name, default_v)
|
||||||
if value < min_v:
|
if value < min_v:
|
||||||
self.log.warning(f'Setting {name} to {min_v}')
|
self.log.warning(f"Setting {name} to {min_v}")
|
||||||
value = min_v
|
value = min_v
|
||||||
if value > max_v:
|
if value > max_v:
|
||||||
self.log.warning(f'Setting {name} to {max_v}')
|
self.log.warning(f"Setting {name} to {max_v}")
|
||||||
value = max_v
|
value = max_v
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2021-2024 tecnovert
|
# Copyright (c) 2021-2024 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ class SwapTypes(IntEnum):
|
|||||||
SELLER_FIRST_2MSG = auto()
|
SELLER_FIRST_2MSG = auto()
|
||||||
BUYER_FIRST_2MSG = auto()
|
BUYER_FIRST_2MSG = auto()
|
||||||
XMR_SWAP = auto()
|
XMR_SWAP = auto()
|
||||||
|
XMR_BCH_SWAP = auto()
|
||||||
|
|
||||||
|
|
||||||
class OfferStates(IntEnum):
|
class OfferStates(IntEnum):
|
||||||
@@ -105,6 +107,8 @@ class BidStates(IntEnum):
|
|||||||
BID_REQUEST_SENT = 29
|
BID_REQUEST_SENT = 29
|
||||||
BID_REQUEST_ACCEPTED = 30
|
BID_REQUEST_ACCEPTED = 30
|
||||||
BID_EXPIRED = 31
|
BID_EXPIRED = 31
|
||||||
|
BID_AACCEPT_DELAY = 32
|
||||||
|
BID_AACCEPT_FAIL = 33
|
||||||
|
|
||||||
|
|
||||||
class TxStates(IntEnum):
|
class TxStates(IntEnum):
|
||||||
@@ -136,6 +140,8 @@ class TxTypes(IntEnum):
|
|||||||
|
|
||||||
ITX_PRE_FUNDED = auto()
|
ITX_PRE_FUNDED = auto()
|
||||||
|
|
||||||
|
BCH_MERCY = auto()
|
||||||
|
|
||||||
|
|
||||||
class ActionTypes(IntEnum):
|
class ActionTypes(IntEnum):
|
||||||
ACCEPT_BID = auto()
|
ACCEPT_BID = auto()
|
||||||
@@ -183,6 +189,8 @@ class EventLogTypes(IntEnum):
|
|||||||
PTX_REDEEM_PUBLISHED = auto()
|
PTX_REDEEM_PUBLISHED = auto()
|
||||||
PTX_REFUND_PUBLISHED = auto()
|
PTX_REFUND_PUBLISHED = auto()
|
||||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||||
|
BCH_MERCY_TX_PUBLISHED = auto()
|
||||||
|
BCH_MERCY_TX_FOUND = auto()
|
||||||
|
|
||||||
|
|
||||||
class XmrSplitMsgTypes(IntEnum):
|
class XmrSplitMsgTypes(IntEnum):
|
||||||
@@ -194,6 +202,7 @@ class DebugTypes(IntEnum):
|
|||||||
NONE = 0
|
NONE = 0
|
||||||
BID_STOP_AFTER_COIN_A_LOCK = auto()
|
BID_STOP_AFTER_COIN_A_LOCK = auto()
|
||||||
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
|
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
|
||||||
|
BID_DONT_SPEND_COIN_A_LOCK_REFUND2 = auto() # continues
|
||||||
CREATE_INVALID_COIN_B_LOCK = auto()
|
CREATE_INVALID_COIN_B_LOCK = auto()
|
||||||
BUYER_STOP_AFTER_ITX = auto()
|
BUYER_STOP_AFTER_ITX = auto()
|
||||||
MAKE_INVALID_PTX = auto()
|
MAKE_INVALID_PTX = auto()
|
||||||
@@ -204,6 +213,10 @@ class DebugTypes(IntEnum):
|
|||||||
DUPLICATE_ACTIONS = auto()
|
DUPLICATE_ACTIONS = auto()
|
||||||
DONT_CONFIRM_PTX = auto()
|
DONT_CONFIRM_PTX = auto()
|
||||||
OFFER_LOCK_2_VALUE_INC = auto()
|
OFFER_LOCK_2_VALUE_INC = auto()
|
||||||
|
BID_STOP_AFTER_COIN_B_LOCK = auto()
|
||||||
|
BID_DONT_SPEND_COIN_B_LOCK = auto()
|
||||||
|
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
|
||||||
|
BID_DONT_SPEND_COIN_A_LOCK = auto()
|
||||||
|
|
||||||
|
|
||||||
class NotificationTypes(IntEnum):
|
class NotificationTypes(IntEnum):
|
||||||
@@ -221,12 +234,12 @@ class AutomationOverrideOptions(IntEnum):
|
|||||||
|
|
||||||
def strAutomationOverrideOption(option):
|
def strAutomationOverrideOption(option):
|
||||||
if option == AutomationOverrideOptions.DEFAULT:
|
if option == AutomationOverrideOptions.DEFAULT:
|
||||||
return 'Default'
|
return "Default"
|
||||||
if option == AutomationOverrideOptions.ALWAYS_ACCEPT:
|
if option == AutomationOverrideOptions.ALWAYS_ACCEPT:
|
||||||
return 'Always Accept'
|
return "Always Accept"
|
||||||
if option == AutomationOverrideOptions.NEVER_ACCEPT:
|
if option == AutomationOverrideOptions.NEVER_ACCEPT:
|
||||||
return 'Never Accept'
|
return "Never Accept"
|
||||||
return 'Unknown'
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
class VisibilityOverrideOptions(IntEnum):
|
class VisibilityOverrideOptions(IntEnum):
|
||||||
@@ -237,244 +250,257 @@ class VisibilityOverrideOptions(IntEnum):
|
|||||||
|
|
||||||
def strVisibilityOverrideOption(option):
|
def strVisibilityOverrideOption(option):
|
||||||
if option == VisibilityOverrideOptions.DEFAULT:
|
if option == VisibilityOverrideOptions.DEFAULT:
|
||||||
return 'Default'
|
return "Default"
|
||||||
if option == VisibilityOverrideOptions.HIDE:
|
if option == VisibilityOverrideOptions.HIDE:
|
||||||
return 'Hide'
|
return "Hide"
|
||||||
if option == VisibilityOverrideOptions.BLOCK:
|
if option == VisibilityOverrideOptions.BLOCK:
|
||||||
return 'Block'
|
return "Block"
|
||||||
return 'Unknown'
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def strOfferState(state):
|
def strOfferState(state):
|
||||||
if state == OfferStates.OFFER_SENT:
|
if state == OfferStates.OFFER_SENT:
|
||||||
return 'Sent'
|
return "Sent"
|
||||||
if state == OfferStates.OFFER_RECEIVED:
|
if state == OfferStates.OFFER_RECEIVED:
|
||||||
return 'Received'
|
return "Received"
|
||||||
if state == OfferStates.OFFER_ABANDONED:
|
if state == OfferStates.OFFER_ABANDONED:
|
||||||
return 'Abandoned'
|
return "Abandoned"
|
||||||
if state == OfferStates.OFFER_EXPIRED:
|
if state == OfferStates.OFFER_EXPIRED:
|
||||||
return 'Expired'
|
return "Expired"
|
||||||
return 'Unknown'
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def strBidState(state):
|
def strBidState(state):
|
||||||
if state == BidStates.BID_SENT:
|
if state == BidStates.BID_SENT:
|
||||||
return 'Sent'
|
return "Sent"
|
||||||
if state == BidStates.BID_RECEIVING:
|
if state == BidStates.BID_RECEIVING:
|
||||||
return 'Receiving'
|
return "Receiving"
|
||||||
if state == BidStates.BID_RECEIVING_ACC:
|
if state == BidStates.BID_RECEIVING_ACC:
|
||||||
return 'Receiving accept'
|
return "Receiving accept"
|
||||||
if state == BidStates.BID_RECEIVED:
|
if state == BidStates.BID_RECEIVED:
|
||||||
return 'Received'
|
return "Received"
|
||||||
if state == BidStates.BID_ACCEPTED:
|
if state == BidStates.BID_ACCEPTED:
|
||||||
return 'Accepted'
|
return "Accepted"
|
||||||
if state == BidStates.SWAP_INITIATED:
|
if state == BidStates.SWAP_INITIATED:
|
||||||
return 'Initiated'
|
return "Initiated"
|
||||||
if state == BidStates.SWAP_PARTICIPATING:
|
if state == BidStates.SWAP_PARTICIPATING:
|
||||||
return 'Participating'
|
return "Participating"
|
||||||
if state == BidStates.SWAP_COMPLETED:
|
if state == BidStates.SWAP_COMPLETED:
|
||||||
return 'Completed'
|
return "Completed"
|
||||||
if state == BidStates.SWAP_TIMEDOUT:
|
if state == BidStates.SWAP_TIMEDOUT:
|
||||||
return 'Timed-out'
|
return "Timed-out"
|
||||||
if state == BidStates.BID_ABANDONED:
|
if state == BidStates.BID_ABANDONED:
|
||||||
return 'Abandoned'
|
return "Abandoned"
|
||||||
if state == BidStates.BID_STALLED_FOR_TEST:
|
if state == BidStates.BID_STALLED_FOR_TEST:
|
||||||
return 'Stalled (debug)'
|
return "Stalled (debug)"
|
||||||
if state == BidStates.BID_ERROR:
|
if state == BidStates.BID_ERROR:
|
||||||
return 'Error'
|
return "Error"
|
||||||
if state == BidStates.BID_REJECTED:
|
if state == BidStates.BID_REJECTED:
|
||||||
return 'Rejected'
|
return "Rejected"
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
|
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
|
||||||
return 'Script coin locked'
|
return "Script coin locked"
|
||||||
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
|
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
|
||||||
return 'Script coin spend tx valid'
|
return "Script coin spend tx valid"
|
||||||
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
|
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
|
||||||
return 'Scriptless coin locked'
|
return "Scriptless coin locked"
|
||||||
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
|
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
|
||||||
return 'Script coin lock released'
|
return "Script coin lock released"
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
||||||
return 'Script tx redeemed'
|
return "Script tx redeemed"
|
||||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
||||||
return 'Script pre-refund tx in chain'
|
return "Script pre-refund tx in chain"
|
||||||
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
|
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
|
||||||
return 'Scriptless tx redeemed'
|
return "Scriptless tx redeemed"
|
||||||
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
|
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
|
||||||
return 'Scriptless tx recovered'
|
return "Scriptless tx recovered"
|
||||||
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
|
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
|
||||||
return 'Failed, refunded'
|
return "Failed, refunded"
|
||||||
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
|
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
|
||||||
return 'Failed, swiped'
|
return "Failed, swiped"
|
||||||
if state == BidStates.XMR_SWAP_FAILED:
|
if state == BidStates.XMR_SWAP_FAILED:
|
||||||
return 'Failed'
|
return "Failed"
|
||||||
if state == BidStates.SWAP_DELAYING:
|
if state == BidStates.SWAP_DELAYING:
|
||||||
return 'Delaying'
|
return "Delaying"
|
||||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
|
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
|
||||||
return 'Exchanged script lock tx sigs msg'
|
return "Exchanged script lock tx sigs msg"
|
||||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
|
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
|
||||||
return 'Exchanged script lock spend tx msg'
|
return "Exchanged script lock spend tx msg"
|
||||||
if state == BidStates.BID_REQUEST_SENT:
|
if state == BidStates.BID_REQUEST_SENT:
|
||||||
return 'Request sent'
|
return "Request sent"
|
||||||
if state == BidStates.BID_REQUEST_ACCEPTED:
|
if state == BidStates.BID_REQUEST_ACCEPTED:
|
||||||
return 'Request accepted'
|
return "Request accepted"
|
||||||
if state == BidStates.BID_STATE_UNKNOWN:
|
if state == BidStates.BID_STATE_UNKNOWN:
|
||||||
return 'Unknown bid state'
|
return "Unknown bid state"
|
||||||
if state == BidStates.BID_EXPIRED:
|
if state == BidStates.BID_EXPIRED:
|
||||||
return 'Expired'
|
return "Expired"
|
||||||
return 'Unknown' + ' ' + str(state)
|
if state == BidStates.BID_AACCEPT_DELAY:
|
||||||
|
return "Auto accept delay"
|
||||||
|
if state == BidStates.BID_AACCEPT_FAIL:
|
||||||
|
return "Auto accept failed"
|
||||||
|
return "Unknown" + " " + str(state)
|
||||||
|
|
||||||
|
|
||||||
def strTxState(state):
|
def strTxState(state):
|
||||||
if state == TxStates.TX_NONE:
|
if state == TxStates.TX_NONE:
|
||||||
return 'None'
|
return "None"
|
||||||
if state == TxStates.TX_SENT:
|
if state == TxStates.TX_SENT:
|
||||||
return 'Sent'
|
return "Sent"
|
||||||
if state == TxStates.TX_CONFIRMED:
|
if state == TxStates.TX_CONFIRMED:
|
||||||
return 'Confirmed'
|
return "Confirmed"
|
||||||
if state == TxStates.TX_REDEEMED:
|
if state == TxStates.TX_REDEEMED:
|
||||||
return 'Redeemed'
|
return "Redeemed"
|
||||||
if state == TxStates.TX_REFUNDED:
|
if state == TxStates.TX_REFUNDED:
|
||||||
return 'Refunded'
|
return "Refunded"
|
||||||
if state == TxStates.TX_IN_MEMPOOL:
|
if state == TxStates.TX_IN_MEMPOOL:
|
||||||
return 'In Mempool'
|
return "In Mempool"
|
||||||
if state == TxStates.TX_IN_CHAIN:
|
if state == TxStates.TX_IN_CHAIN:
|
||||||
return 'In Chain'
|
return "In Chain"
|
||||||
return 'Unknown'
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def strTxType(tx_type):
|
def strTxType(tx_type):
|
||||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
|
||||||
return 'Chain A Lock Tx'
|
return "Chain A Lock Tx"
|
||||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
|
||||||
return 'Chain A Lock Spend Tx'
|
return "Chain A Lock Spend Tx"
|
||||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
|
||||||
return 'Chain A Lock Refund Tx'
|
return "Chain A Lock Refund Tx"
|
||||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
|
||||||
return 'Chain A Lock Refund Spend Tx'
|
return "Chain A Lock Refund Spend Tx"
|
||||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
|
||||||
return 'Chain A Lock Refund Swipe Tx'
|
return "Chain A Lock Refund Swipe Tx"
|
||||||
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
|
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
|
||||||
return 'Chain B Lock Tx'
|
return "Chain B Lock Tx"
|
||||||
if tx_type == TxTypes.ITX_PRE_FUNDED:
|
if tx_type == TxTypes.ITX_PRE_FUNDED:
|
||||||
return 'Funded mock initiate tx'
|
return "Funded mock initiate Tx"
|
||||||
return 'Unknown'
|
if tx_type == TxTypes.BCH_MERCY:
|
||||||
|
return "BCH Mercy Tx"
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def strAddressType(addr_type):
|
def strAddressType(addr_type):
|
||||||
if addr_type == AddressTypes.OFFER:
|
if addr_type == AddressTypes.OFFER:
|
||||||
return 'Offer'
|
return "Offer"
|
||||||
if addr_type == AddressTypes.BID:
|
if addr_type == AddressTypes.BID:
|
||||||
return 'Bid'
|
return "Bid"
|
||||||
if addr_type == AddressTypes.RECV_OFFER:
|
if addr_type == AddressTypes.RECV_OFFER:
|
||||||
return 'Offer recv'
|
return "Offer recv"
|
||||||
if addr_type == AddressTypes.SEND_OFFER:
|
if addr_type == AddressTypes.SEND_OFFER:
|
||||||
return 'Offer send'
|
return "Offer send"
|
||||||
return 'Unknown'
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def getLockName(lock_type):
|
def getLockName(lock_type):
|
||||||
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
|
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
|
||||||
return 'Sequence lock, blocks'
|
return "Sequence lock, blocks"
|
||||||
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
|
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
|
||||||
return 'Sequence lock, time'
|
return "Sequence lock, time"
|
||||||
if lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
|
if lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
|
||||||
return 'blocks'
|
return "blocks"
|
||||||
if lock_type == TxLockTypes.ABS_LOCK_TIME:
|
if lock_type == TxLockTypes.ABS_LOCK_TIME:
|
||||||
return 'time'
|
return "time"
|
||||||
|
|
||||||
|
|
||||||
def describeEventEntry(event_type, event_msg):
|
def describeEventEntry(event_type, event_msg):
|
||||||
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
|
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
|
||||||
return 'Failed to publish lock tx B'
|
return "Failed to publish lock tx B"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
|
||||||
return 'Lock tx A published'
|
return "Lock tx A published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
|
||||||
return 'Lock tx B published'
|
return "Lock tx B published"
|
||||||
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
||||||
return 'Failed to publish lock tx B spend: ' + event_msg
|
return "Failed to publish lock tx B spend: " + event_msg
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
||||||
return 'Lock tx A seen in chain'
|
return "Lock tx A seen in chain"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
||||||
return 'Lock tx A confirmed in chain'
|
return "Lock tx A confirmed in chain"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
|
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
|
||||||
return 'Lock tx B seen in chain'
|
return "Lock tx B seen in chain"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
|
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
|
||||||
return 'Lock tx B confirmed in chain'
|
return "Lock tx B confirmed in chain"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_IN_MEMPOOL:
|
if event_type == EventLogTypes.LOCK_TX_B_IN_MEMPOOL:
|
||||||
return 'Lock tx B seen in mempool'
|
return "Lock tx B seen in mempool"
|
||||||
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
|
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
|
||||||
return 'Debug tweak applied ' + event_msg
|
return "Debug tweak applied " + event_msg
|
||||||
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
|
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
|
||||||
return 'Failed to publish lock tx B refund'
|
return "Failed to publish lock tx B refund"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
||||||
return 'Detected invalid lock Tx B'
|
return "Detected invalid lock Tx B"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
||||||
return 'Lock tx A refund tx published'
|
return "Lock tx A refund tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
||||||
return 'Lock tx A refund spend tx published'
|
return "Lock tx A refund spend tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
|
||||||
return 'Lock tx A refund swipe tx published'
|
return "Lock tx A refund swipe tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
|
||||||
return 'Lock tx B refund tx published'
|
return "Lock tx B refund tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED:
|
||||||
return 'Lock tx A spend tx published'
|
return "Lock tx A spend tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
|
||||||
return 'Lock tx B spend tx published'
|
return "Lock tx B spend tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
|
||||||
return 'Lock tx A refund tx seen in chain'
|
return "Lock tx A refund tx seen in chain"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
|
||||||
return 'Lock tx A refund spend tx seen in chain'
|
return "Lock tx A refund spend tx seen in chain"
|
||||||
if event_type == EventLogTypes.SYSTEM_WARNING:
|
if event_type == EventLogTypes.SYSTEM_WARNING:
|
||||||
return 'Warning: ' + event_msg
|
return "Warning: " + event_msg
|
||||||
if event_type == EventLogTypes.ERROR:
|
if event_type == EventLogTypes.ERROR:
|
||||||
return 'Error: ' + event_msg
|
return "Error: " + event_msg
|
||||||
if event_type == EventLogTypes.AUTOMATION_CONSTRAINT:
|
if event_type == EventLogTypes.AUTOMATION_CONSTRAINT:
|
||||||
return 'Failed auto accepting'
|
return "Failed auto accepting"
|
||||||
if event_type == EventLogTypes.AUTOMATION_ACCEPTING_BID:
|
if event_type == EventLogTypes.AUTOMATION_ACCEPTING_BID:
|
||||||
return 'Auto accepting'
|
return "Auto accepting"
|
||||||
if event_type == EventLogTypes.ITX_PUBLISHED:
|
if event_type == EventLogTypes.ITX_PUBLISHED:
|
||||||
return 'Initiate tx published'
|
return "Initiate tx published"
|
||||||
if event_type == EventLogTypes.ITX_REDEEM_PUBLISHED:
|
if event_type == EventLogTypes.ITX_REDEEM_PUBLISHED:
|
||||||
return 'Initiate tx redeem tx published'
|
return "Initiate tx redeem tx published"
|
||||||
if event_type == EventLogTypes.ITX_REFUND_PUBLISHED:
|
if event_type == EventLogTypes.ITX_REFUND_PUBLISHED:
|
||||||
return 'Initiate tx refund tx published'
|
return "Initiate tx refund tx published"
|
||||||
if event_type == EventLogTypes.PTX_PUBLISHED:
|
if event_type == EventLogTypes.PTX_PUBLISHED:
|
||||||
return 'Participate tx published'
|
return "Participate tx published"
|
||||||
if event_type == EventLogTypes.PTX_REDEEM_PUBLISHED:
|
if event_type == EventLogTypes.PTX_REDEEM_PUBLISHED:
|
||||||
return 'Participate tx redeem tx published'
|
return "Participate tx redeem tx published"
|
||||||
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
|
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
|
||||||
return 'Participate tx refund tx published'
|
return "Participate tx refund tx published"
|
||||||
|
if event_type == EventLogTypes.BCH_MERCY_TX_FOUND:
|
||||||
|
return "BCH mercy tx found"
|
||||||
|
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
|
||||||
|
return "Lock tx B mercy tx published"
|
||||||
|
|
||||||
|
|
||||||
def getVoutByAddress(txjs, p2sh):
|
def getVoutByAddress(txjs, p2sh):
|
||||||
for o in txjs['vout']:
|
for o in txjs["vout"]:
|
||||||
try:
|
try:
|
||||||
if 'address' in o['scriptPubKey'] and o['scriptPubKey']['address'] == p2sh:
|
if "address" in o["scriptPubKey"] and o["scriptPubKey"]["address"] == p2sh:
|
||||||
return o['n']
|
return o["n"]
|
||||||
if p2sh in o['scriptPubKey']['addresses']:
|
if p2sh in o["scriptPubKey"]["addresses"]:
|
||||||
return o['n']
|
return o["n"]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise ValueError('Address output not found in txn')
|
raise ValueError("Address output not found in txn")
|
||||||
|
|
||||||
|
|
||||||
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
|
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
|
||||||
for o in txjs['vout']:
|
for o in txjs["vout"]:
|
||||||
try:
|
try:
|
||||||
if scriptPubKey_hex == o['scriptPubKey']['hex']:
|
if scriptPubKey_hex == o["scriptPubKey"]["hex"]:
|
||||||
return o['n']
|
return o["n"]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise ValueError('scriptPubKey output not found in txn')
|
raise ValueError("scriptPubKey output not found in txn")
|
||||||
|
|
||||||
|
|
||||||
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
|
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type="pubkey_address"):
|
||||||
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
|
return encodeAddress(
|
||||||
|
bytes((chainparams[coin_type][chain_name][addr_type],))
|
||||||
|
+ decodeAddress(addr)[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def getOfferProofOfFundsHash(offer_msg, offer_addr):
|
def getOfferProofOfFundsHash(offer_msg, offer_addr):
|
||||||
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
|
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
h.update(offer_addr.encode('utf-8'))
|
h.update(offer_addr.encode("utf-8"))
|
||||||
offer_bytes = offer_msg.to_bytes()
|
offer_bytes = offer_msg.to_bytes()
|
||||||
h.update(offer_bytes)
|
h.update(offer_bytes)
|
||||||
return h.digest()
|
return h.digest()
|
||||||
@@ -484,33 +510,48 @@ def getLastBidState(packed_states):
|
|||||||
num_states = len(packed_states) // 12
|
num_states = len(packed_states) // 12
|
||||||
if num_states < 2:
|
if num_states < 2:
|
||||||
return BidStates.BID_STATE_UNKNOWN
|
return BidStates.BID_STATE_UNKNOWN
|
||||||
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
|
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
|
||||||
try:
|
try:
|
||||||
num_states = len(packed_states) // 12
|
num_states = len(packed_states) // 12
|
||||||
if num_states < 2:
|
if num_states < 2:
|
||||||
return BidStates.BID_STATE_UNKNOWN
|
return BidStates.BID_STATE_UNKNOWN
|
||||||
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
|
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
return BidStates.BID_STATE_UNKNOWN
|
return BidStates.BID_STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
def strSwapType(swap_type):
|
def strSwapType(swap_type):
|
||||||
if swap_type == SwapTypes.SELLER_FIRST:
|
if swap_type == SwapTypes.SELLER_FIRST:
|
||||||
return 'seller_first'
|
return "seller_first"
|
||||||
if swap_type == SwapTypes.XMR_SWAP:
|
if swap_type == SwapTypes.XMR_SWAP:
|
||||||
return 'xmr_swap'
|
return "xmr_swap"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def strSwapDesc(swap_type):
|
def strSwapDesc(swap_type):
|
||||||
if swap_type == SwapTypes.SELLER_FIRST:
|
if swap_type == SwapTypes.SELLER_FIRST:
|
||||||
return 'Secret Hash'
|
return "Secret Hash"
|
||||||
if swap_type == SwapTypes.XMR_SWAP:
|
if swap_type == SwapTypes.XMR_SWAP:
|
||||||
return 'Adaptor Sig'
|
return "Adaptor Sig"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED]
|
inactive_states = [
|
||||||
|
BidStates.SWAP_COMPLETED,
|
||||||
|
BidStates.BID_ERROR,
|
||||||
|
BidStates.BID_REJECTED,
|
||||||
|
BidStates.SWAP_TIMEDOUT,
|
||||||
|
BidStates.BID_ABANDONED,
|
||||||
|
BidStates.BID_EXPIRED,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def canAcceptBidState(state):
|
||||||
|
return state in (
|
||||||
|
BidStates.BID_RECEIVED,
|
||||||
|
BidStates.BID_AACCEPT_DELAY,
|
||||||
|
BidStates.BID_AACCEPT_FAIL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def isActiveBidState(state):
|
def isActiveBidState(state):
|
||||||
|
|||||||
1
basicswap/bin/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
name = "bin"
|
||||||
2952
basicswap/bin/prepare.py
Executable file
649
basicswap/bin/run.py
Executable file
@@ -0,0 +1,649 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import basicswap.config as cfg
|
||||||
|
from basicswap import __version__
|
||||||
|
from basicswap.ui.util import getCoinName
|
||||||
|
from basicswap.basicswap import BasicSwap
|
||||||
|
from basicswap.chainparams import chainparams, Coins
|
||||||
|
from basicswap.http_server import HttpThread
|
||||||
|
from basicswap.contrib.websocket_server import WebsocketServer
|
||||||
|
|
||||||
|
|
||||||
|
initial_logger = logging.getLogger()
|
||||||
|
initial_logger.level = logging.DEBUG
|
||||||
|
if not len(initial_logger.handlers):
|
||||||
|
initial_logger.addHandler(initial_logger.StreamHandler(sys.stdout))
|
||||||
|
logger = initial_logger
|
||||||
|
|
||||||
|
swap_client = None
|
||||||
|
|
||||||
|
|
||||||
|
class Daemon:
|
||||||
|
__slots__ = ("handle", "files")
|
||||||
|
|
||||||
|
def __init__(self, handle, files):
|
||||||
|
self.handle = handle
|
||||||
|
self.files = files
|
||||||
|
|
||||||
|
|
||||||
|
def is_known_coin(coin_name: str) -> bool:
|
||||||
|
for k, v in chainparams.items():
|
||||||
|
if coin_name == v["name"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
os.write(
|
||||||
|
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
||||||
|
)
|
||||||
|
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||||
|
swap_client.stopRunning()
|
||||||
|
|
||||||
|
|
||||||
|
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||||
|
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||||
|
datadir_path = os.path.expanduser(node_dir)
|
||||||
|
|
||||||
|
# Rewrite litecoin.conf
|
||||||
|
# TODO: Remove
|
||||||
|
needs_rewrite: bool = False
|
||||||
|
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
|
||||||
|
if os.path.exists(ltc_conf_path):
|
||||||
|
with open(ltc_conf_path) as fp:
|
||||||
|
for line in fp:
|
||||||
|
line = line.strip()
|
||||||
|
if line.endswith("=onion"):
|
||||||
|
needs_rewrite = True
|
||||||
|
break
|
||||||
|
if needs_rewrite:
|
||||||
|
logger.info("Rewriting litecoin.conf")
|
||||||
|
shutil.copyfile(ltc_conf_path, ltc_conf_path + ".last")
|
||||||
|
with (
|
||||||
|
open(ltc_conf_path + ".last") as fp_from,
|
||||||
|
open(ltc_conf_path, "w") as fp_to,
|
||||||
|
):
|
||||||
|
for line in fp_from:
|
||||||
|
if line.strip().endswith("=onion"):
|
||||||
|
fp_to.write(line.strip()[:-6] + "\n")
|
||||||
|
else:
|
||||||
|
fp_to.write(line)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
daemon_bin,
|
||||||
|
]
|
||||||
|
add_datadir: bool = extra_config.get("add_datadir", True)
|
||||||
|
if add_datadir:
|
||||||
|
args.append("-datadir=" + datadir_path)
|
||||||
|
args += opts
|
||||||
|
logger.info("Starting node {}".format(daemon_bin))
|
||||||
|
logger.debug("Arguments {}".format(" ".join(args)))
|
||||||
|
|
||||||
|
opened_files = []
|
||||||
|
if extra_config.get("stdout_to_file", False):
|
||||||
|
stdout_dest = open(
|
||||||
|
os.path.join(
|
||||||
|
datadir_path, extra_config.get("stdout_filename", "core_stdout.log")
|
||||||
|
),
|
||||||
|
"w",
|
||||||
|
)
|
||||||
|
opened_files.append(stdout_dest)
|
||||||
|
stderr_dest = stdout_dest
|
||||||
|
else:
|
||||||
|
stdout_dest = subprocess.PIPE
|
||||||
|
stderr_dest = subprocess.PIPE
|
||||||
|
|
||||||
|
shell: bool = False
|
||||||
|
if extra_config.get("use_shell", False):
|
||||||
|
args = " ".join(args)
|
||||||
|
shell = True
|
||||||
|
return Daemon(
|
||||||
|
subprocess.Popen(
|
||||||
|
args,
|
||||||
|
shell=shell,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=stdout_dest,
|
||||||
|
stderr=stderr_dest,
|
||||||
|
cwd=datadir_path,
|
||||||
|
),
|
||||||
|
opened_files,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||||
|
daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||||
|
|
||||||
|
datadir_path = os.path.expanduser(node_dir)
|
||||||
|
config_filename = (
|
||||||
|
"wownerod.conf" if daemon_bin.startswith("wow") else "monerod.conf"
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
daemon_path,
|
||||||
|
"--non-interactive",
|
||||||
|
"--config-file=" + os.path.join(datadir_path, config_filename),
|
||||||
|
] + opts
|
||||||
|
logger.info("Starting node {}".format(daemon_bin))
|
||||||
|
logger.debug("Arguments {}".format(" ".join(args)))
|
||||||
|
|
||||||
|
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
file_stdout = open(os.path.join(datadir_path, "core_stdout.log"), "w")
|
||||||
|
file_stderr = open(os.path.join(datadir_path, "core_stderr.log"), "w")
|
||||||
|
return Daemon(
|
||||||
|
subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=file_stdout,
|
||||||
|
stderr=file_stderr,
|
||||||
|
cwd=datadir_path,
|
||||||
|
),
|
||||||
|
[file_stdout, file_stderr],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||||
|
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
|
||||||
|
args = [daemon_path, "--non-interactive"]
|
||||||
|
|
||||||
|
needs_rewrite: bool = False
|
||||||
|
config_to_remove = [
|
||||||
|
"daemon-address=",
|
||||||
|
"untrusted-daemon=",
|
||||||
|
"trusted-daemon=",
|
||||||
|
"proxy=",
|
||||||
|
]
|
||||||
|
|
||||||
|
data_dir = os.path.expanduser(node_dir)
|
||||||
|
|
||||||
|
wallet_config_filename = (
|
||||||
|
"wownero-wallet-rpc.conf"
|
||||||
|
if wallet_bin.startswith("wow")
|
||||||
|
else "monero_wallet.conf"
|
||||||
|
)
|
||||||
|
config_path = os.path.join(data_dir, wallet_config_filename)
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
args += ["--config-file=" + config_path]
|
||||||
|
with open(config_path) as fp:
|
||||||
|
for line in fp:
|
||||||
|
if any(
|
||||||
|
line.startswith(config_line) for config_line in config_to_remove
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Found old config in monero_wallet.conf: {}".format(
|
||||||
|
line.strip()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
needs_rewrite = True
|
||||||
|
args += opts
|
||||||
|
|
||||||
|
if needs_rewrite:
|
||||||
|
logger.info("Rewriting wallet config")
|
||||||
|
shutil.copyfile(config_path, config_path + ".last")
|
||||||
|
with open(config_path + ".last") as fp_from, open(config_path, "w") as fp_to:
|
||||||
|
for line in fp_from:
|
||||||
|
if not any(
|
||||||
|
line.startswith(config_line) for config_line in config_to_remove
|
||||||
|
):
|
||||||
|
fp_to.write(line)
|
||||||
|
|
||||||
|
logger.info("Starting wallet daemon {}".format(wallet_bin))
|
||||||
|
logger.debug("Arguments {}".format(" ".join(args)))
|
||||||
|
|
||||||
|
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
|
||||||
|
wallet_stdout = open(os.path.join(data_dir, "wallet_stdout.log"), "w")
|
||||||
|
wallet_stderr = open(os.path.join(data_dir, "wallet_stderr.log"), "w")
|
||||||
|
return Daemon(
|
||||||
|
subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=wallet_stdout,
|
||||||
|
stderr=wallet_stderr,
|
||||||
|
cwd=data_dir,
|
||||||
|
),
|
||||||
|
[wallet_stdout, wallet_stderr],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ws_new_client(client, server):
|
||||||
|
if swap_client:
|
||||||
|
swap_client.log.debug(f'ws_new_client {client["id"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def ws_client_left(client, server):
|
||||||
|
if client is None:
|
||||||
|
return
|
||||||
|
if swap_client:
|
||||||
|
swap_client.log.debug(f'ws_client_left {client["id"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def ws_message_received(client, server, message):
|
||||||
|
if len(message) > 200:
|
||||||
|
message = message[:200] + ".."
|
||||||
|
if swap_client:
|
||||||
|
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
|
||||||
|
|
||||||
|
|
||||||
|
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
|
||||||
|
return coin_settings.get(
|
||||||
|
"core_binname", chainparams[coin_id].get("core_binname", default_name)
|
||||||
|
) + (".exe" if os.name == "nt" else "")
|
||||||
|
|
||||||
|
|
||||||
|
def getWalletBinName(coin_id: int, coin_settings, default_name: str) -> str:
|
||||||
|
return coin_settings.get(
|
||||||
|
"wallet_binname", chainparams[coin_id].get("wallet_binname", default_name)
|
||||||
|
) + (".exe" if os.name == "nt" else "")
|
||||||
|
|
||||||
|
|
||||||
|
def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=False):
|
||||||
|
extra_args = []
|
||||||
|
if "config_filename" in coin_settings:
|
||||||
|
extra_args.append("--conf=" + coin_settings["config_filename"])
|
||||||
|
if "port" in coin_settings and coin_id != Coins.BTC:
|
||||||
|
if prepare is False and use_tor_proxy:
|
||||||
|
if coin_id == Coins.BCH:
|
||||||
|
# Without this BCH (27.1) will bind to the default BTC port, even with proxy set
|
||||||
|
extra_args.append("--bind=127.0.0.1:" + str(int(coin_settings["port"])))
|
||||||
|
else:
|
||||||
|
extra_args.append("--port=" + str(int(coin_settings["port"])))
|
||||||
|
|
||||||
|
# BTC versions from v28 fail to start if the onionport is in use.
|
||||||
|
# As BCH may use port 8334, disable it here.
|
||||||
|
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
|
||||||
|
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
|
||||||
|
if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
|
||||||
|
port: int = coin_settings.get("port", 8333)
|
||||||
|
extra_args.append(f"--bind=0.0.0.0:{port}")
|
||||||
|
return extra_args
|
||||||
|
|
||||||
|
|
||||||
|
def runClient(fp, data_dir, chain, start_only_coins):
|
||||||
|
global swap_client, logger
|
||||||
|
daemons = []
|
||||||
|
pids = []
|
||||||
|
threads = []
|
||||||
|
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||||
|
pids_path = os.path.join(data_dir, ".pids")
|
||||||
|
|
||||||
|
if os.getenv("WALLET_ENCRYPTION_PWD", "") != "":
|
||||||
|
if "decred" in start_only_coins:
|
||||||
|
# Workaround for dcrwallet requiring password for initial startup
|
||||||
|
logger.warning(
|
||||||
|
"Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Please unset the WALLET_ENCRYPTION_PWD environment variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(settings_path):
|
||||||
|
raise ValueError("Settings file not found: " + str(settings_path))
|
||||||
|
|
||||||
|
with open(settings_path) as fs:
|
||||||
|
settings = json.load(fs)
|
||||||
|
|
||||||
|
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
||||||
|
logger = swap_client.log
|
||||||
|
|
||||||
|
if os.path.exists(pids_path):
|
||||||
|
with open(pids_path) as fd:
|
||||||
|
for ln in fd:
|
||||||
|
# TODO: try close
|
||||||
|
logger.warning("Found pid for daemon {} ".format(ln.strip()))
|
||||||
|
|
||||||
|
# Ensure daemons are stopped
|
||||||
|
swap_client.stopDaemons()
|
||||||
|
|
||||||
|
# Settings may have been modified
|
||||||
|
settings = swap_client.settings
|
||||||
|
try:
|
||||||
|
# Try start daemons
|
||||||
|
for c, v in settings["chainclients"].items():
|
||||||
|
if len(start_only_coins) > 0 and c not in start_only_coins:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
coin_id = swap_client.getCoinIdFromName(c)
|
||||||
|
display_name = getCoinName(coin_id)
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
logger.warning("Not starting unknown coin: {}".format(c))
|
||||||
|
continue
|
||||||
|
if c in ("monero", "wownero"):
|
||||||
|
if v["manage_daemon"] is True:
|
||||||
|
swap_client.log.info(f"Starting {display_name} daemon")
|
||||||
|
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||||
|
|
||||||
|
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
|
||||||
|
pid = daemons[-1].handle.pid
|
||||||
|
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||||
|
|
||||||
|
if v["manage_wallet_daemon"] is True:
|
||||||
|
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||||
|
daemon_addr = "{}:{}".format(v["rpchost"], v["rpcport"])
|
||||||
|
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
|
||||||
|
coin_id, v["rpchost"]
|
||||||
|
)
|
||||||
|
opts = [
|
||||||
|
"--daemon-address",
|
||||||
|
daemon_addr,
|
||||||
|
]
|
||||||
|
|
||||||
|
proxy_log_str = ""
|
||||||
|
proxy_host, proxy_port = swap_client.getXMRWalletProxy(
|
||||||
|
coin_id, v["rpchost"]
|
||||||
|
)
|
||||||
|
if proxy_host:
|
||||||
|
proxy_log_str = " through proxy"
|
||||||
|
opts += [
|
||||||
|
"--proxy",
|
||||||
|
f"{proxy_host}:{proxy_port}",
|
||||||
|
"--daemon-ssl-allow-any-cert",
|
||||||
|
]
|
||||||
|
|
||||||
|
swap_client.log.info(
|
||||||
|
"daemon-address: {} ({}){}".format(
|
||||||
|
daemon_addr,
|
||||||
|
"trusted" if trusted_daemon else "untrusted",
|
||||||
|
proxy_log_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
daemon_rpcuser = v.get("rpcuser", "")
|
||||||
|
daemon_rpcpass = v.get("rpcpassword", "")
|
||||||
|
if daemon_rpcuser != "":
|
||||||
|
opts.append("--daemon-login")
|
||||||
|
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
|
||||||
|
|
||||||
|
opts.append(
|
||||||
|
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
|
||||||
|
)
|
||||||
|
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
|
||||||
|
|
||||||
|
daemons.append(
|
||||||
|
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
|
||||||
|
)
|
||||||
|
pid = daemons[-1].handle.pid
|
||||||
|
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||||
|
|
||||||
|
continue # /monero
|
||||||
|
|
||||||
|
if c == "decred":
|
||||||
|
appdata = v["datadir"]
|
||||||
|
extra_opts = [
|
||||||
|
f'--appdata="{appdata}"',
|
||||||
|
]
|
||||||
|
use_shell: bool = True if os.name == "nt" else False
|
||||||
|
if v["manage_daemon"] is True:
|
||||||
|
swap_client.log.info(f"Starting {display_name} daemon")
|
||||||
|
filename: str = getCoreBinName(coin_id, v, "dcrd")
|
||||||
|
|
||||||
|
extra_config = {
|
||||||
|
"add_datadir": False,
|
||||||
|
"stdout_to_file": True,
|
||||||
|
"stdout_filename": "dcrd_stdout.log",
|
||||||
|
"use_shell": use_shell,
|
||||||
|
}
|
||||||
|
daemons.append(
|
||||||
|
startDaemon(
|
||||||
|
appdata,
|
||||||
|
v["bindir"],
|
||||||
|
filename,
|
||||||
|
opts=extra_opts,
|
||||||
|
extra_config=extra_config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pid = daemons[-1].handle.pid
|
||||||
|
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||||
|
|
||||||
|
if v["manage_wallet_daemon"] is True:
|
||||||
|
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||||
|
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
|
||||||
|
|
||||||
|
wallet_pwd = v["wallet_pwd"]
|
||||||
|
if wallet_pwd == "":
|
||||||
|
# Only set when in startonlycoin mode
|
||||||
|
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
|
||||||
|
if wallet_pwd != "":
|
||||||
|
extra_opts.append(f'--pass="{wallet_pwd}"')
|
||||||
|
extra_config = {
|
||||||
|
"add_datadir": False,
|
||||||
|
"stdout_to_file": True,
|
||||||
|
"stdout_filename": "dcrwallet_stdout.log",
|
||||||
|
"use_shell": use_shell,
|
||||||
|
}
|
||||||
|
daemons.append(
|
||||||
|
startDaemon(
|
||||||
|
appdata,
|
||||||
|
v["bindir"],
|
||||||
|
filename,
|
||||||
|
opts=extra_opts,
|
||||||
|
extra_config=extra_config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pid = daemons[-1].handle.pid
|
||||||
|
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||||
|
|
||||||
|
continue # /decred
|
||||||
|
|
||||||
|
if v["manage_daemon"] is True:
|
||||||
|
swap_client.log.info(f"Starting {display_name} daemon")
|
||||||
|
|
||||||
|
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||||
|
extra_opts = getCoreBinArgs(
|
||||||
|
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
|
||||||
|
)
|
||||||
|
daemons.append(
|
||||||
|
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
|
||||||
|
)
|
||||||
|
pid = daemons[-1].handle.pid
|
||||||
|
pids.append((c, pid))
|
||||||
|
swap_client.setDaemonPID(c, pid)
|
||||||
|
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||||
|
if len(pids) > 0:
|
||||||
|
with open(pids_path, "w") as fd:
|
||||||
|
for p in pids:
|
||||||
|
fd.write("{}:{}\n".format(*p))
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGHUP, signal_handler)
|
||||||
|
|
||||||
|
if len(start_only_coins) > 0:
|
||||||
|
logger.info(
|
||||||
|
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
|
||||||
|
)
|
||||||
|
while not swap_client.delay_event.wait(0.5):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
swap_client.start()
|
||||||
|
if "htmlhost" in settings:
|
||||||
|
swap_client.log.info(
|
||||||
|
"Starting http server at http://%s:%d."
|
||||||
|
% (settings["htmlhost"], settings["htmlport"])
|
||||||
|
)
|
||||||
|
allow_cors = (
|
||||||
|
settings["allowcors"]
|
||||||
|
if "allowcors" in settings
|
||||||
|
else cfg.DEFAULT_ALLOW_CORS
|
||||||
|
)
|
||||||
|
thread_http = HttpThread(
|
||||||
|
fp,
|
||||||
|
settings["htmlhost"],
|
||||||
|
settings["htmlport"],
|
||||||
|
allow_cors,
|
||||||
|
swap_client,
|
||||||
|
)
|
||||||
|
threads.append(thread_http)
|
||||||
|
thread_http.start()
|
||||||
|
|
||||||
|
if "wshost" in settings:
|
||||||
|
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
|
||||||
|
swap_client.log.info(f"Starting ws server at {ws_url}.")
|
||||||
|
|
||||||
|
swap_client.ws_server = WebsocketServer(
|
||||||
|
host=settings["wshost"], port=settings["wsport"]
|
||||||
|
)
|
||||||
|
swap_client.ws_server.client_port = settings.get(
|
||||||
|
"wsclientport", settings["wsport"]
|
||||||
|
)
|
||||||
|
swap_client.ws_server.set_fn_new_client(ws_new_client)
|
||||||
|
swap_client.ws_server.set_fn_client_left(ws_client_left)
|
||||||
|
swap_client.ws_server.set_fn_message_received(ws_message_received)
|
||||||
|
swap_client.ws_server.run_forever(threaded=True)
|
||||||
|
|
||||||
|
logger.info("Exit with Ctrl + c.")
|
||||||
|
while not swap_client.delay_event.wait(0.5):
|
||||||
|
swap_client.update()
|
||||||
|
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if swap_client.ws_server:
|
||||||
|
try:
|
||||||
|
swap_client.log.info("Stopping websocket server.")
|
||||||
|
swap_client.ws_server.shutdown_gracefully()
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
swap_client.finalise()
|
||||||
|
swap_client.log.info("Stopping HTTP threads.")
|
||||||
|
for t in threads:
|
||||||
|
try:
|
||||||
|
t.stop()
|
||||||
|
t.join()
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
closed_pids = []
|
||||||
|
for d in daemons:
|
||||||
|
swap_client.log.info("Interrupting {}".format(d.handle.pid))
|
||||||
|
try:
|
||||||
|
d.handle.send_signal(
|
||||||
|
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
|
||||||
|
for d in daemons:
|
||||||
|
try:
|
||||||
|
d.handle.wait(timeout=120)
|
||||||
|
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
|
||||||
|
if fp:
|
||||||
|
fp.close()
|
||||||
|
closed_pids.append(d.handle.pid)
|
||||||
|
except Exception as e:
|
||||||
|
swap_client.log.error(f"Error: {e}")
|
||||||
|
|
||||||
|
if os.path.exists(pids_path):
|
||||||
|
with open(pids_path) as fd:
|
||||||
|
lines = fd.read().split("\n")
|
||||||
|
still_running = ""
|
||||||
|
for ln in lines:
|
||||||
|
try:
|
||||||
|
if int(ln.split(":")[1]) not in closed_pids:
|
||||||
|
still_running += ln + "\n"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
with open(pids_path, "w") as fd:
|
||||||
|
fd.write(still_running)
|
||||||
|
|
||||||
|
|
||||||
|
def printVersion():
|
||||||
|
logger.info("Basicswap version: %s", __version__)
|
||||||
|
|
||||||
|
|
||||||
|
def printHelp():
|
||||||
|
print("Usage: basicswap-run ")
|
||||||
|
print("\n--help, -h Print help.")
|
||||||
|
print("--version, -v Print version.")
|
||||||
|
print(
|
||||||
|
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
|
||||||
|
cfg.BASICSWAP_DATADIR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("--mainnet Run in mainnet mode.")
|
||||||
|
print("--testnet Run in testnet mode.")
|
||||||
|
print("--regtest Run in regtest mode.")
|
||||||
|
print(
|
||||||
|
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data_dir = None
|
||||||
|
chain = "mainnet"
|
||||||
|
start_only_coins = set()
|
||||||
|
|
||||||
|
for v in sys.argv[1:]:
|
||||||
|
if len(v) < 2 or v[0] != "-":
|
||||||
|
logger.warning("Unknown argument %s", v)
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = v.split("=")
|
||||||
|
name = s[0].strip()
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
if name[0] == "-":
|
||||||
|
name = name[1:]
|
||||||
|
|
||||||
|
if name == "v" or name == "version":
|
||||||
|
printVersion()
|
||||||
|
return 0
|
||||||
|
if name == "h" or name == "help":
|
||||||
|
printHelp()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if name in ("mainnet", "testnet", "regtest"):
|
||||||
|
chain = name
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(s) == 2:
|
||||||
|
if name == "datadir":
|
||||||
|
data_dir = os.path.expanduser(s[1])
|
||||||
|
continue
|
||||||
|
if name == "startonlycoin":
|
||||||
|
for coin in [s.lower() for s in s[1].split(",")]:
|
||||||
|
if is_known_coin(coin) is False:
|
||||||
|
raise ValueError(f"Unknown coin: {coin}")
|
||||||
|
start_only_coins.add(coin)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.warning("Unknown argument %s", v)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
logger.warning(
|
||||||
|
"Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
if data_dir is None:
|
||||||
|
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
|
||||||
|
logger.info("Using datadir: %s", data_dir)
|
||||||
|
logger.info("Chain: %s", chain)
|
||||||
|
|
||||||
|
if not os.path.exists(data_dir):
|
||||||
|
os.makedirs(data_dir)
|
||||||
|
|
||||||
|
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
|
||||||
|
logger.info(
|
||||||
|
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
|
||||||
|
)
|
||||||
|
runClient(fp, data_dir, chain, start_only_coins)
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
|
return swap_client.fail_code if swap_client is not None else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2024 tecnovert
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -30,419 +31,522 @@ class Coins(IntEnum):
|
|||||||
NAV = 14
|
NAV = 14
|
||||||
LTC_MWEB = 15
|
LTC_MWEB = 15
|
||||||
# ZANO = 16
|
# ZANO = 16
|
||||||
|
BCH = 17
|
||||||
|
DOGE = 18
|
||||||
|
|
||||||
|
|
||||||
chainparams = {
|
chainparams = {
|
||||||
Coins.PART: {
|
Coins.PART: {
|
||||||
'name': 'particl',
|
"name": "particl",
|
||||||
'ticker': 'PART',
|
"ticker": "PART",
|
||||||
'message_magic': 'Bitcoin Signed Message:\n',
|
"message_magic": "Bitcoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 2,
|
"blocks_target": 60 * 2,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 51735,
|
"rpcport": 51735,
|
||||||
'pubkey_address': 0x38,
|
"pubkey_address": 0x38,
|
||||||
'script_address': 0x3c,
|
"script_address": 0x3C,
|
||||||
'key_prefix': 0x6c,
|
"key_prefix": 0x6C,
|
||||||
'stealth_key_prefix': 0x14,
|
"stealth_key_prefix": 0x14,
|
||||||
'hrp': 'pw',
|
"hrp": "pw",
|
||||||
'bip44': 44,
|
"bip44": 44,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 51935,
|
"rpcport": 51935,
|
||||||
'pubkey_address': 0x76,
|
"pubkey_address": 0x76,
|
||||||
'script_address': 0x7a,
|
"script_address": 0x7A,
|
||||||
'key_prefix': 0x2e,
|
"key_prefix": 0x2E,
|
||||||
'stealth_key_prefix': 0x15,
|
"stealth_key_prefix": 0x15,
|
||||||
'hrp': 'tpw',
|
"hrp": "tpw",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 51936,
|
||||||
|
"pubkey_address": 0x76,
|
||||||
|
"script_address": 0x7A,
|
||||||
|
"key_prefix": 0x2E,
|
||||||
|
"stealth_key_prefix": 0x15,
|
||||||
|
"hrp": "rtpw",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 51936,
|
|
||||||
'pubkey_address': 0x76,
|
|
||||||
'script_address': 0x7a,
|
|
||||||
'key_prefix': 0x2e,
|
|
||||||
'stealth_key_prefix': 0x15,
|
|
||||||
'hrp': 'rtpw',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.BTC: {
|
Coins.BTC: {
|
||||||
'name': 'bitcoin',
|
"name": "bitcoin",
|
||||||
'ticker': 'BTC',
|
"ticker": "BTC",
|
||||||
'message_magic': 'Bitcoin Signed Message:\n',
|
"message_magic": "Bitcoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 10,
|
"blocks_target": 60 * 10,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 8332,
|
"rpcport": 8332,
|
||||||
'pubkey_address': 0,
|
"pubkey_address": 0,
|
||||||
'script_address': 5,
|
"script_address": 5,
|
||||||
'key_prefix': 128,
|
"key_prefix": 128,
|
||||||
'hrp': 'bc',
|
"hrp": "bc",
|
||||||
'bip44': 0,
|
"bip44": 0,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
|
"ext_public_key_prefix": 0x0488B21E,
|
||||||
|
"ext_secret_key_prefix": 0x0488ADE4,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 18332,
|
"rpcport": 18332,
|
||||||
'pubkey_address': 111,
|
"pubkey_address": 111,
|
||||||
'script_address': 196,
|
"script_address": 196,
|
||||||
'key_prefix': 239,
|
"key_prefix": 239,
|
||||||
'hrp': 'tb',
|
"hrp": "tb",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
'name': 'testnet3',
|
"name": "testnet3",
|
||||||
|
"ext_public_key_prefix": 0x043587CF,
|
||||||
|
"ext_secret_key_prefix": 0x04358394,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18443,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "bcrt",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
"ext_public_key_prefix": 0x043587CF,
|
||||||
|
"ext_secret_key_prefix": 0x04358394,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 18443,
|
|
||||||
'pubkey_address': 111,
|
|
||||||
'script_address': 196,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': 'bcrt',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.LTC: {
|
Coins.LTC: {
|
||||||
'name': 'litecoin',
|
"name": "litecoin",
|
||||||
'ticker': 'LTC',
|
"ticker": "LTC",
|
||||||
'message_magic': 'Litecoin Signed Message:\n',
|
"message_magic": "Litecoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 1,
|
"blocks_target": 60 * 1,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 9332,
|
"rpcport": 9332,
|
||||||
'pubkey_address': 48,
|
"pubkey_address": 48,
|
||||||
'script_address': 5,
|
"script_address": 5,
|
||||||
'script_address2': 50,
|
"script_address2": 50,
|
||||||
'key_prefix': 176,
|
"key_prefix": 176,
|
||||||
'hrp': 'ltc',
|
"hrp": "ltc",
|
||||||
'bip44': 2,
|
"bip44": 2,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 19332,
|
"rpcport": 19332,
|
||||||
'pubkey_address': 111,
|
"pubkey_address": 111,
|
||||||
'script_address': 196,
|
"script_address": 196,
|
||||||
'script_address2': 58,
|
"script_address2": 58,
|
||||||
'key_prefix': 239,
|
"key_prefix": 239,
|
||||||
'hrp': 'tltc',
|
"hrp": "tltc",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
'name': 'testnet4',
|
"name": "testnet4",
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 19443,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"script_address2": 58,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "rltc",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Coins.DOGE: {
|
||||||
|
"name": "dogecoin",
|
||||||
|
"ticker": "DOGE",
|
||||||
|
"message_magic": "Dogecoin Signed Message:\n",
|
||||||
|
"blocks_target": 60 * 1,
|
||||||
|
"decimal_places": 8,
|
||||||
|
"mainnet": {
|
||||||
|
"rpcport": 22555,
|
||||||
|
"pubkey_address": 30,
|
||||||
|
"script_address": 22,
|
||||||
|
"key_prefix": 158,
|
||||||
|
"hrp": "doge",
|
||||||
|
"bip44": 3,
|
||||||
|
"min_amount": 100000, # TODO increase above fee
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"testnet": {
|
||||||
|
"rpcport": 44555,
|
||||||
|
"pubkey_address": 113,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 241,
|
||||||
|
"hrp": "tdge",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
"name": "testnet4",
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18332,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "rdge",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 19443,
|
|
||||||
'pubkey_address': 111,
|
|
||||||
'script_address': 196,
|
|
||||||
'script_address2': 58,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': 'rltc',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.DCR: {
|
Coins.DCR: {
|
||||||
'name': 'decred',
|
"name": "decred",
|
||||||
'ticker': 'DCR',
|
"ticker": "DCR",
|
||||||
'message_magic': 'Decred Signed Message:\n',
|
"message_magic": "Decred Signed Message:\n",
|
||||||
'blocks_target': 60 * 5,
|
"blocks_target": 60 * 5,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'mainnet': {
|
"has_multiwallet": False,
|
||||||
'rpcport': 9109,
|
"mainnet": {
|
||||||
'pubkey_address': 0x073f,
|
"rpcport": 9109,
|
||||||
'script_address': 0x071a,
|
"pubkey_address": 0x073F,
|
||||||
'key_prefix': 0x22de,
|
"script_address": 0x071A,
|
||||||
'bip44': 42,
|
"key_prefix": 0x22DE,
|
||||||
'min_amount': 1000,
|
"bip44": 42,
|
||||||
'max_amount': 100000 * COIN,
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 19109,
|
"rpcport": 19109,
|
||||||
'pubkey_address': 0x0f21,
|
"pubkey_address": 0x0F21,
|
||||||
'script_address': 0x0efc,
|
"script_address": 0x0EFC,
|
||||||
'key_prefix': 0x230e,
|
"key_prefix": 0x230E,
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
'name': 'testnet3',
|
"name": "testnet3",
|
||||||
|
},
|
||||||
|
"regtest": { # simnet
|
||||||
|
"rpcport": 18656,
|
||||||
|
"pubkey_address": 0x0E91,
|
||||||
|
"script_address": 0x0E6C,
|
||||||
|
"key_prefix": 0x2307,
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': { # simnet
|
|
||||||
'rpcport': 18656,
|
|
||||||
'pubkey_address': 0x0e91,
|
|
||||||
'script_address': 0x0e6c,
|
|
||||||
'key_prefix': 0x2307,
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.NMC: {
|
Coins.NMC: {
|
||||||
'name': 'namecoin',
|
"name": "namecoin",
|
||||||
'ticker': 'NMC',
|
"ticker": "NMC",
|
||||||
'message_magic': 'Namecoin Signed Message:\n',
|
"message_magic": "Namecoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 10,
|
"blocks_target": 60 * 10,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 8336,
|
"rpcport": 8336,
|
||||||
'pubkey_address': 52,
|
"pubkey_address": 52,
|
||||||
'script_address': 13,
|
"script_address": 13,
|
||||||
'hrp': 'nc',
|
"hrp": "nc",
|
||||||
'bip44': 7,
|
"bip44": 7,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 18336,
|
"rpcport": 18336,
|
||||||
'pubkey_address': 111,
|
"pubkey_address": 111,
|
||||||
'script_address': 196,
|
"script_address": 196,
|
||||||
'hrp': 'tn',
|
"hrp": "tn",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
'name': 'testnet3',
|
"name": "testnet3",
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18443,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"hrp": "ncrt",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 18443,
|
|
||||||
'pubkey_address': 111,
|
|
||||||
'script_address': 196,
|
|
||||||
'hrp': 'ncrt',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.XMR: {
|
Coins.XMR: {
|
||||||
'name': 'monero',
|
"name": "monero",
|
||||||
'ticker': 'XMR',
|
"ticker": "XMR",
|
||||||
'client': 'xmr',
|
"client": "xmr",
|
||||||
'decimal_places': 12,
|
"decimal_places": 12,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 18081,
|
"rpcport": 18081,
|
||||||
'walletrpcport': 18082,
|
"walletrpcport": 18082,
|
||||||
'min_amount': 100000,
|
"min_amount": 1000000000,
|
||||||
'max_amount': 10000 * XMR_COIN,
|
"max_amount": 10000000 * XMR_COIN,
|
||||||
'address_prefix': 18,
|
"address_prefix": 18,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 28081,
|
"rpcport": 28081,
|
||||||
'walletrpcport': 28082,
|
"walletrpcport": 28082,
|
||||||
'min_amount': 100000,
|
"min_amount": 1000000000,
|
||||||
'max_amount': 10000 * XMR_COIN,
|
"max_amount": 10000000 * XMR_COIN,
|
||||||
'address_prefix': 18,
|
"address_prefix": 18,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18081,
|
||||||
|
"walletrpcport": 18082,
|
||||||
|
"min_amount": 1000000000,
|
||||||
|
"max_amount": 10000000 * XMR_COIN,
|
||||||
|
"address_prefix": 18,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 18081,
|
|
||||||
'walletrpcport': 18082,
|
|
||||||
'min_amount': 100000,
|
|
||||||
'max_amount': 10000 * XMR_COIN,
|
|
||||||
'address_prefix': 18,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.WOW: {
|
Coins.WOW: {
|
||||||
'name': 'wownero',
|
"name": "wownero",
|
||||||
'ticker': 'WOW',
|
"ticker": "WOW",
|
||||||
'client': 'wow',
|
"client": "wow",
|
||||||
'decimal_places': 11,
|
"decimal_places": 11,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 34568,
|
"rpcport": 34568,
|
||||||
'walletrpcport': 34572, # todo
|
"walletrpcport": 34572, # todo
|
||||||
'min_amount': 100000,
|
"min_amount": 100000000,
|
||||||
'max_amount': 10000 * WOW_COIN,
|
"max_amount": 10000000 * WOW_COIN,
|
||||||
'address_prefix': 4146,
|
"address_prefix": 4146,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 44568,
|
"rpcport": 44568,
|
||||||
'walletrpcport': 44572,
|
"walletrpcport": 44572,
|
||||||
'min_amount': 100000,
|
"min_amount": 100000000,
|
||||||
'max_amount': 10000 * WOW_COIN,
|
"max_amount": 10000000 * WOW_COIN,
|
||||||
'address_prefix': 4146,
|
"address_prefix": 4146,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 54568,
|
||||||
|
"walletrpcport": 54572,
|
||||||
|
"min_amount": 100000000,
|
||||||
|
"max_amount": 10000000 * WOW_COIN,
|
||||||
|
"address_prefix": 4146,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 54568,
|
|
||||||
'walletrpcport': 54572,
|
|
||||||
'min_amount': 100000,
|
|
||||||
'max_amount': 10000 * WOW_COIN,
|
|
||||||
'address_prefix': 4146,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.PIVX: {
|
Coins.PIVX: {
|
||||||
'name': 'pivx',
|
"name": "pivx",
|
||||||
'ticker': 'PIVX',
|
"ticker": "PIVX",
|
||||||
'message_magic': 'DarkNet Signed Message:\n',
|
"display_name": "PIVX",
|
||||||
'blocks_target': 60 * 1,
|
"message_magic": "DarkNet Signed Message:\n",
|
||||||
'decimal_places': 8,
|
"blocks_target": 60 * 1,
|
||||||
'has_cltv': True,
|
"decimal_places": 8,
|
||||||
'has_csv': False,
|
"has_cltv": True,
|
||||||
'has_segwit': False,
|
"has_csv": False,
|
||||||
'use_ticker_as_name': True,
|
"has_segwit": False,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 51473,
|
"rpcport": 51473,
|
||||||
'pubkey_address': 30,
|
"pubkey_address": 30,
|
||||||
'script_address': 13,
|
"script_address": 13,
|
||||||
'key_prefix': 212,
|
"key_prefix": 212,
|
||||||
'bip44': 119,
|
"bip44": 119,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 51475,
|
"rpcport": 51475,
|
||||||
'pubkey_address': 139,
|
"pubkey_address": 139,
|
||||||
'script_address': 19,
|
"script_address": 19,
|
||||||
'key_prefix': 239,
|
"key_prefix": 239,
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
'name': 'testnet4',
|
"name": "testnet4",
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 51477,
|
||||||
|
"pubkey_address": 139,
|
||||||
|
"script_address": 19,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 51477,
|
|
||||||
'pubkey_address': 139,
|
|
||||||
'script_address': 19,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.DASH: {
|
Coins.DASH: {
|
||||||
'name': 'dash',
|
"name": "dash",
|
||||||
'ticker': 'DASH',
|
"ticker": "DASH",
|
||||||
'message_magic': 'DarkCoin Signed Message:\n',
|
"message_magic": "DarkCoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 2.5,
|
"blocks_target": 60 * 2.5,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'has_csv': True,
|
"has_csv": True,
|
||||||
'has_segwit': False,
|
"has_segwit": False,
|
||||||
'mainnet': {
|
"mainnet": {
|
||||||
'rpcport': 9998,
|
"rpcport": 9998,
|
||||||
'pubkey_address': 76,
|
"pubkey_address": 76,
|
||||||
'script_address': 16,
|
"script_address": 16,
|
||||||
'key_prefix': 204,
|
"key_prefix": 204,
|
||||||
'hrp': '',
|
"hrp": "",
|
||||||
'bip44': 5,
|
"bip44": 5,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 19998,
|
"rpcport": 19998,
|
||||||
'pubkey_address': 140,
|
"pubkey_address": 140,
|
||||||
'script_address': 19,
|
"script_address": 19,
|
||||||
'key_prefix': 239,
|
"key_prefix": 239,
|
||||||
'hrp': '',
|
"hrp": "",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18332,
|
||||||
|
"pubkey_address": 140,
|
||||||
|
"script_address": 19,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 18332,
|
|
||||||
'pubkey_address': 140,
|
|
||||||
'script_address': 19,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': '',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.FIRO: {
|
Coins.FIRO: {
|
||||||
'name': 'firo',
|
"name": "firo",
|
||||||
'ticker': 'FIRO',
|
"ticker": "FIRO",
|
||||||
'message_magic': 'Zcoin Signed Message:\n',
|
"message_magic": "Zcoin Signed Message:\n",
|
||||||
'blocks_target': 60 * 10,
|
"blocks_target": 60 * 10,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'has_cltv': False,
|
"has_cltv": False,
|
||||||
'has_csv': False,
|
"has_csv": False,
|
||||||
'has_segwit': False,
|
"has_segwit": False,
|
||||||
'mainnet': {
|
"has_multiwallet": False,
|
||||||
'rpcport': 8888,
|
"mainnet": {
|
||||||
'pubkey_address': 82,
|
"rpcport": 8888,
|
||||||
'script_address': 7,
|
"pubkey_address": 82,
|
||||||
'key_prefix': 210,
|
"script_address": 7,
|
||||||
'hrp': '',
|
"key_prefix": 210,
|
||||||
'bip44': 136,
|
"hrp": "",
|
||||||
'min_amount': 1000,
|
"bip44": 136,
|
||||||
'max_amount': 100000 * COIN,
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
"testnet": {
|
||||||
'rpcport': 18888,
|
"rpcport": 18888,
|
||||||
'pubkey_address': 65,
|
"pubkey_address": 65,
|
||||||
'script_address': 178,
|
"script_address": 178,
|
||||||
'key_prefix': 185,
|
"key_prefix": 185,
|
||||||
'hrp': '',
|
"hrp": "",
|
||||||
'bip44': 1,
|
"bip44": 1,
|
||||||
'min_amount': 1000,
|
"min_amount": 100000,
|
||||||
'max_amount': 100000 * COIN,
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 28888,
|
||||||
|
"pubkey_address": 65,
|
||||||
|
"script_address": 178,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 28888,
|
|
||||||
'pubkey_address': 65,
|
|
||||||
'script_address': 178,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': '',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Coins.NAV: {
|
Coins.NAV: {
|
||||||
'name': 'navcoin',
|
"name": "navcoin",
|
||||||
'ticker': 'NAV',
|
"ticker": "NAV",
|
||||||
'message_magic': 'Navcoin Signed Message:\n',
|
"message_magic": "Navcoin Signed Message:\n",
|
||||||
'blocks_target': 30,
|
"blocks_target": 30,
|
||||||
'decimal_places': 8,
|
"decimal_places": 8,
|
||||||
'has_csv': True,
|
"has_csv": True,
|
||||||
'has_segwit': True,
|
"has_segwit": True,
|
||||||
'mainnet': {
|
"has_multiwallet": False,
|
||||||
'rpcport': 44444,
|
"mainnet": {
|
||||||
'pubkey_address': 53,
|
"rpcport": 44444,
|
||||||
'script_address': 85,
|
"pubkey_address": 53,
|
||||||
'key_prefix': 150,
|
"script_address": 85,
|
||||||
'hrp': '',
|
"key_prefix": 150,
|
||||||
'bip44': 130,
|
"hrp": "",
|
||||||
'min_amount': 1000,
|
"bip44": 130,
|
||||||
'max_amount': 100000 * COIN,
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"testnet": {
|
||||||
|
"rpcport": 44445,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 44446,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Coins.BCH: {
|
||||||
|
"name": "bitcoincash",
|
||||||
|
"ticker": "BCH",
|
||||||
|
"display_name": "Bitcoin Cash",
|
||||||
|
"message_magic": "Bitcoin Signed Message:\n",
|
||||||
|
"blocks_target": 60 * 2,
|
||||||
|
"decimal_places": 8,
|
||||||
|
"has_cltv": True,
|
||||||
|
"has_csv": True,
|
||||||
|
"has_segwit": False,
|
||||||
|
"cli_binname": "bitcoin-cli",
|
||||||
|
"core_binname": "bitcoind",
|
||||||
|
"mainnet": {
|
||||||
|
"rpcport": 8332,
|
||||||
|
"pubkey_address": 0,
|
||||||
|
"script_address": 5,
|
||||||
|
"key_prefix": 128,
|
||||||
|
"hrp": "bitcoincash",
|
||||||
|
"bip44": 0,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
},
|
||||||
|
"testnet": {
|
||||||
|
"rpcport": 18332,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "bchtest",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
|
"name": "testnet3",
|
||||||
|
},
|
||||||
|
"regtest": {
|
||||||
|
"rpcport": 18443,
|
||||||
|
"pubkey_address": 111,
|
||||||
|
"script_address": 196,
|
||||||
|
"key_prefix": 239,
|
||||||
|
"hrp": "bchreg",
|
||||||
|
"bip44": 1,
|
||||||
|
"min_amount": 100000,
|
||||||
|
"max_amount": 10000000 * COIN,
|
||||||
},
|
},
|
||||||
'testnet': {
|
|
||||||
'rpcport': 44445,
|
|
||||||
'pubkey_address': 111,
|
|
||||||
'script_address': 196,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': '',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
},
|
},
|
||||||
'regtest': {
|
|
||||||
'rpcport': 44446,
|
|
||||||
'pubkey_address': 111,
|
|
||||||
'script_address': 196,
|
|
||||||
'key_prefix': 239,
|
|
||||||
'hrp': '',
|
|
||||||
'bip44': 1,
|
|
||||||
'min_amount': 1000,
|
|
||||||
'max_amount': 100000 * COIN,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name_map = {}
|
||||||
ticker_map = {}
|
ticker_map = {}
|
||||||
|
|
||||||
|
|
||||||
for c, params in chainparams.items():
|
for c, params in chainparams.items():
|
||||||
ticker_map[params['ticker'].lower()] = c
|
name_map[params["name"].lower()] = c
|
||||||
|
ticker_map[params["ticker"].lower()] = c
|
||||||
|
|
||||||
|
|
||||||
def getCoinIdFromTicker(ticker):
|
def getCoinIdFromTicker(ticker: str) -> str:
|
||||||
try:
|
try:
|
||||||
return ticker_map[ticker.lower()]
|
return ticker_map[ticker.lower()]
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError('Unknown coin')
|
raise ValueError(f"Unknown coin {ticker}")
|
||||||
|
|
||||||
|
|
||||||
|
def getCoinIdFromName(name: str) -> str:
|
||||||
|
try:
|
||||||
|
return name_map[name.lower()]
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(f"Unknown coin {name}")
|
||||||
|
|||||||
@@ -1,38 +1,56 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2023 tecnovert
|
# Copyright (c) 2019-2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
CONFIG_FILENAME = 'basicswap.json'
|
CONFIG_FILENAME = "basicswap.json"
|
||||||
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', os.path.join('~', '.basicswap'))
|
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
||||||
DEFAULT_ALLOW_CORS = False
|
DEFAULT_ALLOW_CORS = False
|
||||||
TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
|
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
|
||||||
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', os.path.join('~', '.basicswap', 'bin')))
|
DEFAULT_TEST_BINDIR = os.path.expanduser(
|
||||||
|
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
|
||||||
|
)
|
||||||
|
|
||||||
bin_suffix = ('.exe' if os.name == 'nt' else '')
|
bin_suffix = ".exe" if os.name == "nt" else ""
|
||||||
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
|
PARTICL_BINDIR = os.path.expanduser(
|
||||||
PARTICLD = os.getenv('PARTICLD', 'particld' + bin_suffix)
|
os.getenv("PARTICL_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "particl"))
|
||||||
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + bin_suffix)
|
)
|
||||||
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + bin_suffix)
|
PARTICLD = os.getenv("PARTICLD", "particld" + bin_suffix)
|
||||||
|
PARTICL_CLI = os.getenv("PARTICL_CLI", "particl-cli" + bin_suffix)
|
||||||
|
PARTICL_TX = os.getenv("PARTICL_TX", "particl-tx" + bin_suffix)
|
||||||
|
|
||||||
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoin')))
|
BITCOIN_BINDIR = os.path.expanduser(
|
||||||
BITCOIND = os.getenv('BITCOIND', 'bitcoind' + bin_suffix)
|
os.getenv("BITCOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "bitcoin"))
|
||||||
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + bin_suffix)
|
)
|
||||||
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + bin_suffix)
|
BITCOIND = os.getenv("BITCOIND", "bitcoind" + bin_suffix)
|
||||||
|
BITCOIN_CLI = os.getenv("BITCOIN_CLI", "bitcoin-cli" + bin_suffix)
|
||||||
|
BITCOIN_TX = os.getenv("BITCOIN_TX", "bitcoin-tx" + bin_suffix)
|
||||||
|
|
||||||
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'litecoin')))
|
LITECOIN_BINDIR = os.path.expanduser(
|
||||||
LITECOIND = os.getenv('LITECOIND', 'litecoind' + bin_suffix)
|
os.getenv("LITECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "litecoin"))
|
||||||
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + bin_suffix)
|
)
|
||||||
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + bin_suffix)
|
LITECOIND = os.getenv("LITECOIND", "litecoind" + bin_suffix)
|
||||||
|
LITECOIN_CLI = os.getenv("LITECOIN_CLI", "litecoin-cli" + bin_suffix)
|
||||||
|
LITECOIN_TX = os.getenv("LITECOIN_TX", "litecoin-tx" + bin_suffix)
|
||||||
|
|
||||||
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'namecoin')))
|
DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
|
||||||
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + bin_suffix)
|
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
|
||||||
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + bin_suffix)
|
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix)
|
||||||
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
|
|
||||||
|
|
||||||
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
|
NAMECOIN_BINDIR = os.path.expanduser(
|
||||||
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
|
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
|
||||||
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)
|
)
|
||||||
|
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
|
||||||
|
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
|
||||||
|
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
|
||||||
|
|
||||||
|
XMR_BINDIR = os.path.expanduser(
|
||||||
|
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
|
||||||
|
)
|
||||||
|
XMRD = os.getenv("XMRD", "monerod" + bin_suffix)
|
||||||
|
XMR_WALLET_RPC = os.getenv("XMR_WALLET_RPC", "monero-wallet-rpc" + bin_suffix)
|
||||||
|
|
||||||
|
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.
|
||||||
|
|||||||
64
basicswap/contrib/test_framework/descriptors.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2019 Pieter Wuille
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Utility functions related to output descriptors"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
||||||
|
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||||
|
GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd]
|
||||||
|
|
||||||
|
def descsum_polymod(symbols):
|
||||||
|
"""Internal function that computes the descriptor checksum."""
|
||||||
|
chk = 1
|
||||||
|
for value in symbols:
|
||||||
|
top = chk >> 35
|
||||||
|
chk = (chk & 0x7ffffffff) << 5 ^ value
|
||||||
|
for i in range(5):
|
||||||
|
chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
|
||||||
|
return chk
|
||||||
|
|
||||||
|
def descsum_expand(s):
|
||||||
|
"""Internal function that does the character to symbol expansion"""
|
||||||
|
groups = []
|
||||||
|
symbols = []
|
||||||
|
for c in s:
|
||||||
|
if not c in INPUT_CHARSET:
|
||||||
|
return None
|
||||||
|
v = INPUT_CHARSET.find(c)
|
||||||
|
symbols.append(v & 31)
|
||||||
|
groups.append(v >> 5)
|
||||||
|
if len(groups) == 3:
|
||||||
|
symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
|
||||||
|
groups = []
|
||||||
|
if len(groups) == 1:
|
||||||
|
symbols.append(groups[0])
|
||||||
|
elif len(groups) == 2:
|
||||||
|
symbols.append(groups[0] * 3 + groups[1])
|
||||||
|
return symbols
|
||||||
|
|
||||||
|
def descsum_create(s):
|
||||||
|
"""Add a checksum to a descriptor without"""
|
||||||
|
symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
checksum = descsum_polymod(symbols) ^ 1
|
||||||
|
return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))
|
||||||
|
|
||||||
|
def descsum_check(s, require=True):
|
||||||
|
"""Verify that the checksum is correct in a descriptor"""
|
||||||
|
if not '#' in s:
|
||||||
|
return not require
|
||||||
|
if s[-9] != '#':
|
||||||
|
return False
|
||||||
|
if not all(x in CHECKSUM_CHARSET for x in s[-8:]):
|
||||||
|
return False
|
||||||
|
symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]]
|
||||||
|
return descsum_polymod(symbols) == 1
|
||||||
|
|
||||||
|
def drop_origins(s):
|
||||||
|
'''Drop the key origins from a descriptor'''
|
||||||
|
desc = re.sub(r'\[.+?\]', '', s)
|
||||||
|
if '#' in s:
|
||||||
|
desc = desc[:desc.index('#')]
|
||||||
|
return descsum_create(desc)
|
||||||
1148
basicswap/db.py
@@ -1,27 +1,46 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022-2024 tecnovert
|
# Copyright (c) 2022-2024 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from sqlalchemy.orm import scoped_session
|
|
||||||
from .db import (
|
from .db import (
|
||||||
|
AutomationStrategy,
|
||||||
BidState,
|
BidState,
|
||||||
Concepts,
|
Concepts,
|
||||||
AutomationStrategy,
|
CURRENT_DB_DATA_VERSION,
|
||||||
CURRENT_DB_VERSION,
|
CURRENT_DB_VERSION,
|
||||||
CURRENT_DB_DATA_VERSION)
|
)
|
||||||
|
|
||||||
from .basicswap_util import (
|
from .basicswap_util import (
|
||||||
BidStates,
|
BidStates,
|
||||||
strBidState,
|
canAcceptBidState,
|
||||||
isActiveBidState,
|
isActiveBidState,
|
||||||
isErrorBidState,
|
isErrorBidState,
|
||||||
isFailingBidState,
|
isFailingBidState,
|
||||||
isFinalBidState,
|
isFinalBidState,
|
||||||
|
strBidState,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def addBidState(self, state, now, cursor):
|
||||||
|
self.add(
|
||||||
|
BidState(
|
||||||
|
active_ind=1,
|
||||||
|
state_id=int(state),
|
||||||
|
in_progress=isActiveBidState(state),
|
||||||
|
in_error=isErrorBidState(state),
|
||||||
|
swap_failed=isFailingBidState(state),
|
||||||
|
swap_ended=isFinalBidState(state),
|
||||||
|
can_accept=canAcceptBidState(state),
|
||||||
|
label=strBidState(state),
|
||||||
|
created_at=now,
|
||||||
|
),
|
||||||
|
cursor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -29,99 +48,128 @@ def upgradeDatabaseData(self, data_version):
|
|||||||
if data_version >= CURRENT_DB_DATA_VERSION:
|
if data_version >= CURRENT_DB_DATA_VERSION:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log.info('Upgrading database records from version %d to %d.', data_version, CURRENT_DB_DATA_VERSION)
|
self.log.info(
|
||||||
with self.mxDB:
|
"Upgrading database records from version %d to %d.",
|
||||||
|
data_version,
|
||||||
|
CURRENT_DB_DATA_VERSION,
|
||||||
|
)
|
||||||
|
cursor = self.openDB()
|
||||||
try:
|
try:
|
||||||
session = scoped_session(self.session_factory)
|
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
|
|
||||||
if data_version < 1:
|
if data_version < 1:
|
||||||
session.add(AutomationStrategy(
|
self.add(
|
||||||
|
AutomationStrategy(
|
||||||
active_ind=1,
|
active_ind=1,
|
||||||
label='Accept All',
|
label="Accept All",
|
||||||
type_ind=Concepts.OFFER,
|
type_ind=Concepts.OFFER,
|
||||||
data=json.dumps({'exact_rate_only': True,
|
data=json.dumps(
|
||||||
'max_concurrent_bids': 5}).encode('utf-8'),
|
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||||
|
).encode("utf-8"),
|
||||||
only_known_identities=False,
|
only_known_identities=False,
|
||||||
created_at=now))
|
created_at=now,
|
||||||
session.add(AutomationStrategy(
|
),
|
||||||
|
cursor,
|
||||||
|
)
|
||||||
|
self.add(
|
||||||
|
AutomationStrategy(
|
||||||
active_ind=1,
|
active_ind=1,
|
||||||
label='Accept Known',
|
label="Accept Known",
|
||||||
type_ind=Concepts.OFFER,
|
type_ind=Concepts.OFFER,
|
||||||
data=json.dumps({'exact_rate_only': True,
|
data=json.dumps(
|
||||||
'max_concurrent_bids': 5}).encode('utf-8'),
|
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||||
|
).encode("utf-8"),
|
||||||
only_known_identities=True,
|
only_known_identities=True,
|
||||||
note='Accept bids from identities with previously successful swaps only',
|
note="Accept bids from identities with previously successful swaps only",
|
||||||
created_at=now))
|
created_at=now,
|
||||||
|
),
|
||||||
|
cursor,
|
||||||
|
)
|
||||||
|
|
||||||
for state in BidStates:
|
for state in BidStates:
|
||||||
session.add(BidState(
|
addBidState(self, state, now, cursor)
|
||||||
active_ind=1,
|
|
||||||
state_id=int(state),
|
|
||||||
in_progress=isActiveBidState(state),
|
|
||||||
in_error=isErrorBidState(state),
|
|
||||||
swap_failed=isFailingBidState(state),
|
|
||||||
swap_ended=isFinalBidState(state),
|
|
||||||
label=strBidState(state),
|
|
||||||
created_at=now))
|
|
||||||
|
|
||||||
if data_version > 0 and data_version < 2:
|
if data_version > 0 and data_version < 2:
|
||||||
for state in (BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX):
|
for state in (
|
||||||
session.add(BidState(
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||||
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||||
|
):
|
||||||
|
self.add(
|
||||||
|
BidState(
|
||||||
active_ind=1,
|
active_ind=1,
|
||||||
state_id=int(state),
|
state_id=int(state),
|
||||||
in_progress=isActiveBidState(state),
|
in_progress=isActiveBidState(state),
|
||||||
label=strBidState(state),
|
label=strBidState(state),
|
||||||
created_at=now))
|
created_at=now,
|
||||||
|
),
|
||||||
|
cursor,
|
||||||
|
)
|
||||||
if data_version > 0 and data_version < 3:
|
if data_version > 0 and data_version < 3:
|
||||||
for state in BidStates:
|
for state in BidStates:
|
||||||
in_error = isErrorBidState(state)
|
in_error = isErrorBidState(state)
|
||||||
swap_failed = isFailingBidState(state)
|
swap_failed = isFailingBidState(state)
|
||||||
swap_ended = isFinalBidState(state)
|
swap_ended = isFinalBidState(state)
|
||||||
session.execute('UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id', {'in_error': in_error, 'swap_failed': swap_failed, 'swap_ended': swap_ended, 'state_id': int(state)})
|
cursor.execute(
|
||||||
|
"UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||||
|
{
|
||||||
|
"in_error": in_error,
|
||||||
|
"swap_failed": swap_failed,
|
||||||
|
"swap_ended": swap_ended,
|
||||||
|
"state_id": int(state),
|
||||||
|
},
|
||||||
|
)
|
||||||
if data_version > 0 and data_version < 4:
|
if data_version > 0 and data_version < 4:
|
||||||
for state in (BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_ACCEPTED):
|
for state in (
|
||||||
session.add(BidState(
|
BidStates.BID_REQUEST_SENT,
|
||||||
active_ind=1,
|
BidStates.BID_REQUEST_ACCEPTED,
|
||||||
state_id=int(state),
|
):
|
||||||
in_progress=isActiveBidState(state),
|
addBidState(self, state, now, cursor)
|
||||||
in_error=isErrorBidState(state),
|
|
||||||
swap_failed=isFailingBidState(state),
|
if data_version > 0 and data_version < 5:
|
||||||
swap_ended=isFinalBidState(state),
|
for state in (
|
||||||
label=strBidState(state),
|
BidStates.BID_EXPIRED,
|
||||||
created_at=now))
|
BidStates.BID_AACCEPT_DELAY,
|
||||||
|
BidStates.BID_AACCEPT_FAIL,
|
||||||
|
):
|
||||||
|
addBidState(self, state, now, cursor)
|
||||||
|
|
||||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||||
self.setIntKV('db_data_version', self.db_data_version, session)
|
self.setIntKV("db_data_version", self.db_data_version, cursor)
|
||||||
session.commit()
|
self.commitDB()
|
||||||
self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
|
self.log.info(
|
||||||
|
"Upgraded database records to version {}".format(self.db_data_version)
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
self.closeDB(cursor, commit=False)
|
||||||
session.remove()
|
|
||||||
|
|
||||||
|
|
||||||
def upgradeDatabase(self, db_version):
|
def upgradeDatabase(self, db_version):
|
||||||
if db_version >= CURRENT_DB_VERSION:
|
if db_version >= CURRENT_DB_VERSION:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log.info('Upgrading database from version %d to %d.', db_version, CURRENT_DB_VERSION)
|
self.log.info(
|
||||||
|
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
|
||||||
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
session = scoped_session(self.session_factory)
|
try:
|
||||||
|
cursor = self.openDB()
|
||||||
|
|
||||||
current_version = db_version
|
current_version = db_version
|
||||||
if current_version == 6:
|
if current_version == 6:
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN security_token BLOB')
|
cursor.execute("ALTER TABLE bids ADD COLUMN security_token BLOB")
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN security_token BLOB')
|
cursor.execute("ALTER TABLE offers ADD COLUMN security_token BLOB")
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 7:
|
elif current_version == 7:
|
||||||
session.execute('ALTER TABLE transactions ADD COLUMN block_hash BLOB')
|
cursor.execute("ALTER TABLE transactions ADD COLUMN block_hash BLOB")
|
||||||
session.execute('ALTER TABLE transactions ADD COLUMN block_height INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE transactions ADD COLUMN block_time INTEGER')
|
"ALTER TABLE transactions ADD COLUMN block_height INTEGER"
|
||||||
|
)
|
||||||
|
cursor.execute("ALTER TABLE transactions ADD COLUMN block_time INTEGER")
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 8:
|
elif current_version == 8:
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE wallets (
|
CREATE TABLE wallets (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
coin_id INTEGER,
|
coin_id INTEGER,
|
||||||
@@ -129,30 +177,42 @@ def upgradeDatabase(self, db_version):
|
|||||||
wallet_data VARCHAR,
|
wallet_data VARCHAR,
|
||||||
balance_type INTEGER,
|
balance_type INTEGER,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 9:
|
elif current_version == 9:
|
||||||
session.execute('ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR')
|
cursor.execute("ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR")
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 10:
|
elif current_version == 10:
|
||||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER')
|
"ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER"
|
||||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR')
|
)
|
||||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR')
|
cursor.execute(
|
||||||
session.execute('UPDATE smsgaddresses SET active_ind = 1, created_at = 1')
|
"ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER"
|
||||||
|
)
|
||||||
|
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR")
|
||||||
|
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR")
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE smsgaddresses SET active_ind = 1, created_at = 1"
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN addr_to VARCHAR')
|
cursor.execute("ALTER TABLE offers ADD COLUMN addr_to VARCHAR")
|
||||||
session.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
|
cursor.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 11:
|
elif current_version == 11:
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER')
|
"ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER"
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN protocol_version INTEGER')
|
)
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN protocol_version INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE transactions ADD COLUMN tx_data BLOB')
|
"ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER"
|
||||||
|
)
|
||||||
|
cursor.execute("ALTER TABLE bids ADD COLUMN protocol_version INTEGER")
|
||||||
|
cursor.execute("ALTER TABLE offers ADD COLUMN protocol_version INTEGER")
|
||||||
|
cursor.execute("ALTER TABLE transactions ADD COLUMN tx_data BLOB")
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 12:
|
elif current_version == 12:
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE knownidentities (
|
CREATE TABLE knownidentities (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
address VARCHAR,
|
address VARCHAR,
|
||||||
@@ -167,15 +227,19 @@ def upgradeDatabase(self, db_version):
|
|||||||
note VARCHAR,
|
note VARCHAR,
|
||||||
updated_at BIGINT,
|
updated_at BIGINT,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN reject_code INTEGER')
|
)
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN rate INTEGER')
|
cursor.execute("ALTER TABLE bids ADD COLUMN reject_code INTEGER")
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER')
|
cursor.execute("ALTER TABLE bids ADD COLUMN rate INTEGER")
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER')
|
cursor.execute(
|
||||||
|
"ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER"
|
||||||
|
)
|
||||||
|
cursor.execute("ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER")
|
||||||
db_version += 1
|
db_version += 1
|
||||||
elif current_version == 13:
|
elif current_version == 13:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE automationstrategies (
|
CREATE TABLE automationstrategies (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
@@ -187,9 +251,11 @@ def upgradeDatabase(self, db_version):
|
|||||||
|
|
||||||
note VARCHAR,
|
note VARCHAR,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE automationlinks (
|
CREATE TABLE automationlinks (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
@@ -204,9 +270,11 @@ def upgradeDatabase(self, db_version):
|
|||||||
|
|
||||||
note VARCHAR,
|
note VARCHAR,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE history (
|
CREATE TABLE history (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
concept_type INTEGER,
|
concept_type INTEGER,
|
||||||
@@ -215,9 +283,11 @@ def upgradeDatabase(self, db_version):
|
|||||||
|
|
||||||
note VARCHAR,
|
note VARCHAR,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE bidstates (
|
CREATE TABLE bidstates (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
@@ -227,31 +297,47 @@ def upgradeDatabase(self, db_version):
|
|||||||
|
|
||||||
note VARCHAR,
|
note VARCHAR,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('ALTER TABLE wallets ADD COLUMN active_ind INTEGER')
|
cursor.execute("ALTER TABLE wallets ADD COLUMN active_ind INTEGER")
|
||||||
session.execute('ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE eventqueue RENAME TO actions')
|
"ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER"
|
||||||
session.execute('ALTER TABLE actions RENAME COLUMN event_id TO action_id')
|
)
|
||||||
session.execute('ALTER TABLE actions RENAME COLUMN event_type TO action_type')
|
cursor.execute("ALTER TABLE eventqueue RENAME TO actions")
|
||||||
session.execute('ALTER TABLE actions RENAME COLUMN event_data TO action_data')
|
cursor.execute(
|
||||||
|
"ALTER TABLE actions RENAME COLUMN event_id TO action_id"
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE actions RENAME COLUMN event_type TO action_type"
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE actions RENAME COLUMN event_data TO action_data"
|
||||||
|
)
|
||||||
elif current_version == 14:
|
elif current_version == 14:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id')
|
"ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB"
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id"
|
||||||
|
)
|
||||||
elif current_version == 15:
|
elif current_version == 15:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE notifications (
|
CREATE TABLE notifications (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
event_type INTEGER,
|
event_type INTEGER,
|
||||||
event_data BLOB,
|
event_data BLOB,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
elif current_version == 16:
|
elif current_version == 16:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE prefunded_transactions (
|
CREATE TABLE prefunded_transactions (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
@@ -261,25 +347,31 @@ def upgradeDatabase(self, db_version):
|
|||||||
tx_type INTEGER,
|
tx_type INTEGER,
|
||||||
tx_data BLOB,
|
tx_data BLOB,
|
||||||
used_by BLOB,
|
used_by BLOB,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
|
)
|
||||||
elif current_version == 17:
|
elif current_version == 17:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER')
|
cursor.execute(
|
||||||
session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER')
|
"ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER"
|
||||||
session.execute('ALTER TABLE knownidentities ADD COLUMN data BLOB')
|
)
|
||||||
session.execute('UPDATE knownidentities SET active_ind = 1')
|
cursor.execute(
|
||||||
|
"ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER"
|
||||||
|
)
|
||||||
|
cursor.execute("ALTER TABLE knownidentities ADD COLUMN data BLOB")
|
||||||
|
cursor.execute("UPDATE knownidentities SET active_ind = 1")
|
||||||
elif current_version == 18:
|
elif current_version == 18:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING')
|
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING")
|
||||||
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING')
|
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING")
|
||||||
elif current_version == 19:
|
elif current_version == 19:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE bidstates ADD COLUMN in_error INTEGER')
|
cursor.execute("ALTER TABLE bidstates ADD COLUMN in_error INTEGER")
|
||||||
session.execute('ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER')
|
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER")
|
||||||
session.execute('ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER')
|
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER")
|
||||||
elif current_version == 20:
|
elif current_version == 20:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE message_links (
|
CREATE TABLE message_links (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
active_ind INTEGER,
|
active_ind INTEGER,
|
||||||
@@ -291,18 +383,20 @@ def upgradeDatabase(self, db_version):
|
|||||||
msg_type INTEGER,
|
msg_type INTEGER,
|
||||||
msg_sequence INTEGER,
|
msg_sequence INTEGER,
|
||||||
msg_id BLOB,
|
msg_id BLOB,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER')
|
)
|
||||||
|
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
|
||||||
elif current_version == 21:
|
elif current_version == 21:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
|
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
|
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
|
||||||
elif current_version == 22:
|
elif current_version == 22:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
|
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
|
||||||
elif current_version == 23:
|
elif current_version == 23:
|
||||||
db_version += 1
|
db_version += 1
|
||||||
session.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE checkedblocks (
|
CREATE TABLE checkedblocks (
|
||||||
record_id INTEGER NOT NULL,
|
record_id INTEGER NOT NULL,
|
||||||
created_at BIGINT,
|
created_at BIGINT,
|
||||||
@@ -310,17 +404,24 @@ def upgradeDatabase(self, db_version):
|
|||||||
block_height INTEGER,
|
block_height INTEGER,
|
||||||
block_hash BLOB,
|
block_hash BLOB,
|
||||||
block_time INTEGER,
|
block_time INTEGER,
|
||||||
PRIMARY KEY (record_id))''')
|
PRIMARY KEY (record_id))"""
|
||||||
session.execute('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB')
|
)
|
||||||
|
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
|
||||||
|
elif current_version == 24:
|
||||||
|
db_version += 1
|
||||||
|
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
|
||||||
if current_version != db_version:
|
if current_version != db_version:
|
||||||
self.db_version = db_version
|
self.db_version = db_version
|
||||||
self.setIntKV('db_version', db_version, session)
|
self.setIntKV("db_version", db_version, cursor)
|
||||||
session.commit()
|
self.commitDB()
|
||||||
session.close()
|
self.log.info("Upgraded database to version {}".format(self.db_version))
|
||||||
session.remove()
|
|
||||||
self.log.info('Upgraded database to version {}'.format(self.db_version))
|
|
||||||
continue
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Upgrade failed {}".format(e))
|
||||||
|
self.rollbackDB()
|
||||||
|
finally:
|
||||||
|
self.closeDB(cursor, commit=False)
|
||||||
break
|
break
|
||||||
|
|
||||||
if db_version != CURRENT_DB_VERSION:
|
if db_version != CURRENT_DB_VERSION:
|
||||||
raise ValueError('Unable to upgrade database.')
|
raise ValueError("Unable to upgrade database.")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2023 The BSX Developers
|
# Copyright (c) 2023-2024 The Basicswap Developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -12,47 +12,122 @@ from .db import (
|
|||||||
def remove_expired_data(self, time_offset: int = 0):
|
def remove_expired_data(self, time_offset: int = 0):
|
||||||
now: int = self.getTime()
|
now: int = self.getTime()
|
||||||
try:
|
try:
|
||||||
session = self.openSession()
|
cursor = self.openDB()
|
||||||
|
|
||||||
active_bids_insert = self.activeBidsQueryStr(now, '', 'b2')
|
active_bids_insert: str = self.activeBidsQueryStr("", "b2")
|
||||||
query_str = f'''
|
query_str = f"""
|
||||||
SELECT o.offer_id FROM offers o
|
SELECT o.offer_id FROM offers o
|
||||||
WHERE o.expire_at <= :expired_at AND 0 = (SELECT COUNT(*) FROM bids b2 WHERE b2.offer_id = o.offer_id AND {active_bids_insert})
|
WHERE o.expire_at <= :expired_at AND 0 = (SELECT COUNT(*) FROM bids b2 WHERE b2.offer_id = o.offer_id AND {active_bids_insert})
|
||||||
'''
|
"""
|
||||||
num_offers = 0
|
num_offers = 0
|
||||||
num_bids = 0
|
num_bids = 0
|
||||||
offer_rows = session.execute(query_str, {'expired_at': now - time_offset})
|
offer_rows = cursor.execute(
|
||||||
|
query_str, {"now": now, "expired_at": now - time_offset}
|
||||||
|
)
|
||||||
for offer_row in offer_rows:
|
for offer_row in offer_rows:
|
||||||
num_offers += 1
|
num_offers += 1
|
||||||
bid_rows = session.execute('SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
bid_rows = cursor.execute(
|
||||||
|
"SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id",
|
||||||
|
{"offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
for bid_row in bid_rows:
|
for bid_row in bid_rows:
|
||||||
num_bids += 1
|
num_bids += 1
|
||||||
session.execute('DELETE FROM transactions WHERE transactions.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
cursor.execute(
|
||||||
session.execute('DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
"DELETE FROM transactions WHERE transactions.bid_id = :bid_id",
|
||||||
session.execute('DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
{"bid_id": bid_row[0]},
|
||||||
session.execute('DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
)
|
||||||
session.execute('DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
cursor.execute(
|
||||||
session.execute('DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id",
|
||||||
session.execute('DELETE FROM actions WHERE actions.linked_id = :bid_id', {'bid_id': bid_row[0]})
|
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||||
session.execute('DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
)
|
||||||
session.execute('DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
cursor.execute(
|
||||||
session.execute('DELETE FROM bids WHERE bids.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id",
|
||||||
session.execute('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id', {'type_ind': int(Concepts.BID), 'linked_id': bid_row[0]})
|
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id",
|
||||||
|
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id",
|
||||||
|
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id",
|
||||||
|
{"bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM actions WHERE actions.linked_id = :bid_id",
|
||||||
|
{"bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id",
|
||||||
|
{"bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id",
|
||||||
|
{"bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM bids WHERE bids.bid_id = :bid_id",
|
||||||
|
{"bid_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||||
|
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
cursor.execute(
|
||||||
session.execute('DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
|
||||||
session.execute('DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||||
session.execute('DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
)
|
||||||
session.execute('DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
cursor.execute(
|
||||||
session.execute('DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
|
||||||
session.execute('DELETE FROM actions WHERE actions.linked_id = :offer_id', {'offer_id': offer_row[0]})
|
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||||
session.execute('DELETE FROM offers WHERE offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
)
|
||||||
session.execute('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
cursor.execute(
|
||||||
|
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
|
||||||
|
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
|
||||||
|
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
|
||||||
|
{"offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
|
||||||
|
{"offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
|
||||||
|
{"offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
|
||||||
|
{"offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||||
|
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||||
|
)
|
||||||
|
|
||||||
if num_offers > 0 or num_bids > 0:
|
if num_offers > 0 or num_bids > 0:
|
||||||
self.log.info('Removed data for {} expired offer{} and {} bid{}.'.format(num_offers, 's' if num_offers != 1 else '', num_bids, 's' if num_bids != 1 else ''))
|
self.log.info(
|
||||||
|
"Removed data for {} expired offer{} and {} bid{}.".format(
|
||||||
|
num_offers,
|
||||||
|
"s" if num_offers != 1 else "",
|
||||||
|
num_bids,
|
||||||
|
"s" if num_bids != 1 else "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
session.execute('DELETE FROM checkedblocks WHERE created_at <= :expired_at', {'expired_at': now - time_offset})
|
cursor.execute(
|
||||||
|
"DELETE FROM checkedblocks WHERE created_at <= :expired_at",
|
||||||
|
{"expired_at": now - time_offset},
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.closeSession(session)
|
self.closeDB(cursor)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ def encodepoint(P):
|
|||||||
zi = edf.inv(P[2])
|
zi = edf.inv(P[2])
|
||||||
x = (P[0] * zi) % edf.q
|
x = (P[0] * zi) % edf.q
|
||||||
y = (P[1] * zi) % edf.q
|
y = (P[1] * zi) % edf.q
|
||||||
y += ((x & 1) << 255)
|
y += (x & 1) << 255
|
||||||
return y.to_bytes(32, byteorder='little')
|
return y.to_bytes(32, byteorder="little")
|
||||||
|
|
||||||
|
|
||||||
def hashToEd25519(bytes_in):
|
def hashToEd25519(bytes_in):
|
||||||
@@ -22,8 +22,8 @@ def hashToEd25519(bytes_in):
|
|||||||
for i in range(1000):
|
for i in range(1000):
|
||||||
h255 = bytearray(hashed)
|
h255 = bytearray(hashed)
|
||||||
x_sign = 0 if h255[31] & 0x80 == 0 else 1
|
x_sign = 0 if h255[31] & 0x80 == 0 else 1
|
||||||
h255[31] &= 0x7f # Clear top bit
|
h255[31] &= 0x7F # Clear top bit
|
||||||
y = int.from_bytes(h255, byteorder='little')
|
y = int.from_bytes(h255, byteorder="little")
|
||||||
x = edf.xrecover(y, x_sign)
|
x = edf.xrecover(y, x_sign)
|
||||||
if x == 0 and y == 1: # Skip infinity point
|
if x == 0 and y == 1: # Skip infinity point
|
||||||
continue
|
continue
|
||||||
@@ -33,4 +33,4 @@ def hashToEd25519(bytes_in):
|
|||||||
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
||||||
return P
|
return P
|
||||||
hashed = hashlib.sha256(hashed).digest()
|
hashed = hashlib.sha256(hashed).digest()
|
||||||
raise ValueError('hashToEd25519 failed')
|
raise ValueError("hashToEd25519 failed")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
class Explorer():
|
class Explorer:
|
||||||
def __init__(self, swapclient, coin_type, base_url):
|
def __init__(self, swapclient, coin_type, base_url):
|
||||||
self.swapclient = swapclient
|
self.swapclient = swapclient
|
||||||
self.coin_type = coin_type
|
self.coin_type = coin_type
|
||||||
@@ -15,82 +15,94 @@ class Explorer():
|
|||||||
self.log = self.swapclient.log
|
self.log = self.swapclient.log
|
||||||
|
|
||||||
def readURL(self, url):
|
def readURL(self, url):
|
||||||
self.log.debug('Explorer url: {}'.format(url))
|
self.log.debug("Explorer url: {}".format(url))
|
||||||
return self.swapclient.readURL(url)
|
return self.swapclient.readURL(url)
|
||||||
|
|
||||||
|
|
||||||
class ExplorerInsight(Explorer):
|
class ExplorerInsight(Explorer):
|
||||||
def getChainHeight(self):
|
def getChainHeight(self):
|
||||||
return json.loads(self.readURL(self.base_url + '/sync'))['blockChainHeight']
|
return json.loads(self.readURL(self.base_url + "/sync"))["blockChainHeight"]
|
||||||
|
|
||||||
def getBlock(self, block_hash):
|
def getBlock(self, block_hash):
|
||||||
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
|
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def getTransaction(self, txid):
|
def getTransaction(self, txid):
|
||||||
data = json.loads(self.readURL(self.base_url + '/tx/{}'.format(txid)))
|
data = json.loads(self.readURL(self.base_url + "/tx/{}".format(txid)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def getBalance(self, address):
|
def getBalance(self, address):
|
||||||
data = json.loads(self.readURL(self.base_url + '/addr/{}/balance'.format(address)))
|
data = json.loads(
|
||||||
|
self.readURL(self.base_url + "/addr/{}/balance".format(address))
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def lookupUnspentByAddress(self, address):
|
def lookupUnspentByAddress(self, address):
|
||||||
data = json.loads(self.readURL(self.base_url + '/addr/{}/utxo'.format(address)))
|
data = json.loads(self.readURL(self.base_url + "/addr/{}/utxo".format(address)))
|
||||||
rv = []
|
rv = []
|
||||||
for utxo in data:
|
for utxo in data:
|
||||||
rv.append({
|
rv.append(
|
||||||
'txid': utxo['txid'],
|
{
|
||||||
'index': utxo['vout'],
|
"txid": utxo["txid"],
|
||||||
'height': utxo['height'],
|
"index": utxo["vout"],
|
||||||
'n_conf': utxo['confirmations'],
|
"height": utxo["height"],
|
||||||
'value': utxo['satoshis'],
|
"n_conf": utxo["confirmations"],
|
||||||
})
|
"value": utxo["satoshis"],
|
||||||
|
}
|
||||||
|
)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
class ExplorerBitAps(Explorer):
|
class ExplorerBitAps(Explorer):
|
||||||
def getChainHeight(self):
|
def getChainHeight(self):
|
||||||
return json.loads(self.readURL(self.base_url + '/block/last'))['data']['block']['height']
|
return json.loads(self.readURL(self.base_url + "/block/last"))["data"]["block"][
|
||||||
|
"height"
|
||||||
|
]
|
||||||
|
|
||||||
def getBlock(self, block_hash):
|
def getBlock(self, block_hash):
|
||||||
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
|
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def getTransaction(self, txid):
|
def getTransaction(self, txid):
|
||||||
data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(txid)))
|
data = json.loads(self.readURL(self.base_url + "/transaction/{}".format(txid)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def getBalance(self, address):
|
def getBalance(self, address):
|
||||||
data = json.loads(self.readURL(self.base_url + '/address/state/' + address))
|
data = json.loads(self.readURL(self.base_url + "/address/state/" + address))
|
||||||
return data['data']['balance']
|
return data["data"]["balance"]
|
||||||
|
|
||||||
def lookupUnspentByAddress(self, address):
|
def lookupUnspentByAddress(self, address):
|
||||||
# Can't get unspents return only if exactly one transaction exists
|
# Can't get unspents return only if exactly one transaction exists
|
||||||
data = json.loads(self.readURL(self.base_url + '/address/transactions/' + address))
|
data = json.loads(
|
||||||
|
self.readURL(self.base_url + "/address/transactions/" + address)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
assert data['data']['list'] == 1
|
assert data["data"]["list"] == 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.debug('Explorer error: {}'.format(str(ex)))
|
self.log.debug("Explorer error: {}".format(str(ex)))
|
||||||
return None
|
return None
|
||||||
tx = data['data']['list'][0]
|
tx = data["data"]["list"][0]
|
||||||
tx_data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(tx['txId'])))['data']
|
tx_data = json.loads(
|
||||||
|
self.readURL(self.base_url + "/transaction/{}".format(tx["txId"]))
|
||||||
|
)["data"]
|
||||||
|
|
||||||
for i, vout in tx_data['vOut'].items():
|
for i, vout in tx_data["vOut"].items():
|
||||||
if vout['address'] == address:
|
if vout["address"] == address:
|
||||||
return [{
|
return [
|
||||||
'txid': tx_data['txId'],
|
{
|
||||||
'index': int(i),
|
"txid": tx_data["txId"],
|
||||||
'height': tx_data['blockHeight'],
|
"index": int(i),
|
||||||
'n_conf': tx_data['confirmations'],
|
"height": tx_data["blockHeight"],
|
||||||
'value': vout['value'],
|
"n_conf": tx_data["confirmations"],
|
||||||
}]
|
"value": vout["value"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ExplorerChainz(Explorer):
|
class ExplorerChainz(Explorer):
|
||||||
def getChainHeight(self):
|
def getChainHeight(self):
|
||||||
return int(self.readURL(self.base_url + '?q=getblockcount'))
|
return int(self.readURL(self.base_url + "?q=getblockcount"))
|
||||||
|
|
||||||
def lookupUnspentByAddress(self, address):
|
def lookupUnspentByAddress(self, address):
|
||||||
chain_height = self.getChainHeight()
|
chain_height = self.getChainHeight()
|
||||||
self.log.debug('[rm] chain_height %d', chain_height)
|
self.log.debug("[rm] chain_height %d", chain_height)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019-2024 tecnovert
|
# Copyright (c) 2019-2024 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -56,12 +57,12 @@ from .ui.page_identity import page_identity
|
|||||||
from .ui.page_smsgaddresses import page_smsgaddresses
|
from .ui.page_smsgaddresses import page_smsgaddresses
|
||||||
from .ui.page_debug import page_debug
|
from .ui.page_debug import page_debug
|
||||||
|
|
||||||
env = Environment(loader=PackageLoader('basicswap', 'templates'))
|
env = Environment(loader=PackageLoader("basicswap", "templates"))
|
||||||
env.filters['formatts'] = format_timestamp
|
env.filters["formatts"] = format_timestamp
|
||||||
|
|
||||||
|
|
||||||
def extractDomain(url):
|
def extractDomain(url):
|
||||||
return url.split('://', 1)[1].split('/', 1)[0]
|
return url.split("://", 1)[1].split("/", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
def listAvailableExplorers(swap_client):
|
def listAvailableExplorers(swap_client):
|
||||||
@@ -69,40 +70,47 @@ def listAvailableExplorers(swap_client):
|
|||||||
for c in Coins:
|
for c in Coins:
|
||||||
if c not in chainparams:
|
if c not in chainparams:
|
||||||
continue
|
continue
|
||||||
for i, e in enumerate(swap_client.coin_clients[c]['explorers']):
|
for i, e in enumerate(swap_client.coin_clients[c]["explorers"]):
|
||||||
explorers.append(('{}_{}'.format(int(c), i), getCoinName(c) + ' - ' + extractDomain(e.base_url)))
|
explorers.append(
|
||||||
|
(
|
||||||
|
"{}_{}".format(int(c), i),
|
||||||
|
getCoinName(c) + " - " + extractDomain(e.base_url),
|
||||||
|
)
|
||||||
|
)
|
||||||
return explorers
|
return explorers
|
||||||
|
|
||||||
|
|
||||||
def listExplorerActions(swap_client):
|
def listExplorerActions(swap_client):
|
||||||
actions = [('height', 'Chain Height'),
|
actions = [
|
||||||
('block', 'Get Block'),
|
("height", "Chain Height"),
|
||||||
('tx', 'Get Transaction'),
|
("block", "Get Block"),
|
||||||
('balance', 'Address Balance'),
|
("tx", "Get Transaction"),
|
||||||
('unspent', 'List Unspent')]
|
("balance", "Address Balance"),
|
||||||
|
("unspent", "List Unspent"),
|
||||||
|
]
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
def parse_cmd(cmd: str, type_map: str):
|
def parse_cmd(cmd: str, type_map: str):
|
||||||
params = shlex.split(cmd)
|
params = shlex.split(cmd)
|
||||||
if len(params) < 1:
|
if len(params) < 1:
|
||||||
return '', []
|
return "", []
|
||||||
method = params[0]
|
method = params[0]
|
||||||
typed_params = []
|
typed_params = []
|
||||||
params = params[1:]
|
params = params[1:]
|
||||||
|
|
||||||
for i, param in enumerate(params):
|
for i, param in enumerate(params):
|
||||||
if i >= len(type_map):
|
if i >= len(type_map):
|
||||||
type_ind = 's'
|
type_ind = "s"
|
||||||
else:
|
else:
|
||||||
type_ind = type_map[i]
|
type_ind = type_map[i]
|
||||||
if type_ind == 'i':
|
if type_ind == "i":
|
||||||
typed_params.append(int(param))
|
typed_params.append(int(param))
|
||||||
elif type_ind == 'f':
|
elif type_ind == "f":
|
||||||
typed_params.append(float(param))
|
typed_params.append(float(param))
|
||||||
elif type_ind == 'b':
|
elif type_ind == "b":
|
||||||
typed_params.append(toBool(param))
|
typed_params.append(toBool(param))
|
||||||
elif type_ind == 'j':
|
elif type_ind == "j":
|
||||||
typed_params.append(json.loads(param))
|
typed_params.append(json.loads(param))
|
||||||
else:
|
else:
|
||||||
typed_params.append(param)
|
typed_params.append(param)
|
||||||
@@ -111,7 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
|
|||||||
|
|
||||||
|
|
||||||
class HttpHandler(BaseHTTPRequestHandler):
|
class HttpHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
def log_error(self, format, *args):
|
def log_error(self, format, *args):
|
||||||
super().log_message(format, *args)
|
super().log_message(format, *args)
|
||||||
|
|
||||||
@@ -123,96 +130,112 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
return os.urandom(8).hex()
|
return os.urandom(8).hex()
|
||||||
|
|
||||||
def checkForm(self, post_string, name, messages):
|
def checkForm(self, post_string, name, messages):
|
||||||
if post_string == '':
|
if post_string == "":
|
||||||
return None
|
return None
|
||||||
form_data = parse.parse_qs(post_string)
|
form_data = parse.parse_qs(post_string)
|
||||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||||
if self.server.last_form_id.get(name, None) == form_id:
|
if self.server.last_form_id.get(name, None) == form_id:
|
||||||
messages.append('Prevented double submit for form {}.'.format(form_id))
|
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||||
return None
|
return None
|
||||||
self.server.last_form_id[name] = form_id
|
self.server.last_form_id[name] = form_id
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
def render_template(self, template, args_dict, status_code=200, version=__version__):
|
def render_template(
|
||||||
|
self, template, args_dict, status_code=200, version=__version__
|
||||||
|
):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
if swap_client.ws_server:
|
if swap_client.ws_server:
|
||||||
args_dict['ws_url'] = swap_client.ws_server.url
|
args_dict["ws_port"] = swap_client.ws_server.client_port
|
||||||
if swap_client.debug:
|
if swap_client.debug:
|
||||||
args_dict['debug_mode'] = True
|
args_dict["debug_mode"] = True
|
||||||
if swap_client.debug_ui:
|
if swap_client.debug_ui:
|
||||||
args_dict['debug_ui_mode'] = True
|
args_dict["debug_ui_mode"] = True
|
||||||
if swap_client.use_tor_proxy:
|
if swap_client.use_tor_proxy:
|
||||||
args_dict['use_tor_proxy'] = True
|
args_dict["use_tor_proxy"] = True
|
||||||
# TODO: Cache value?
|
# TODO: Cache value?
|
||||||
try:
|
try:
|
||||||
args_dict['tor_established'] = True if get_tor_established_state(swap_client) == '1' else False
|
tor_state = get_tor_established_state(swap_client)
|
||||||
except Exception:
|
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||||
|
except Exception as e:
|
||||||
|
args_dict["tor_established"] = False
|
||||||
if swap_client.debug:
|
if swap_client.debug:
|
||||||
|
swap_client.log.error(f"Error getting Tor state: {str(e)}")
|
||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
if swap_client._show_notifications:
|
if swap_client._show_notifications:
|
||||||
args_dict['notifications'] = swap_client.getNotifications()
|
args_dict["notifications"] = swap_client.getNotifications()
|
||||||
|
|
||||||
if 'messages' in args_dict:
|
if "messages" in args_dict:
|
||||||
messages_with_ids = []
|
messages_with_ids = []
|
||||||
for msg in args_dict['messages']:
|
for msg in args_dict["messages"]:
|
||||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
self.server.msg_id_counter += 1
|
self.server.msg_id_counter += 1
|
||||||
args_dict['messages'] = messages_with_ids
|
args_dict["messages"] = messages_with_ids
|
||||||
if 'err_messages' in args_dict:
|
if "err_messages" in args_dict:
|
||||||
err_messages_with_ids = []
|
err_messages_with_ids = []
|
||||||
for msg in args_dict['err_messages']:
|
for msg in args_dict["err_messages"]:
|
||||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
self.server.msg_id_counter += 1
|
self.server.msg_id_counter += 1
|
||||||
args_dict['err_messages'] = err_messages_with_ids
|
args_dict["err_messages"] = err_messages_with_ids
|
||||||
|
|
||||||
shutdown_token = os.urandom(8).hex()
|
shutdown_token = os.urandom(8).hex()
|
||||||
self.server.session_tokens['shutdown'] = shutdown_token
|
self.server.session_tokens["shutdown"] = shutdown_token
|
||||||
args_dict['shutdown_token'] = shutdown_token
|
args_dict["shutdown_token"] = shutdown_token
|
||||||
|
|
||||||
encrypted, locked = swap_client.getLockedState()
|
encrypted, locked = swap_client.getLockedState()
|
||||||
args_dict['encrypted'] = encrypted
|
args_dict["encrypted"] = encrypted
|
||||||
args_dict['locked'] = locked
|
args_dict["locked"] = locked
|
||||||
|
|
||||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||||
self.server.msg_id_counter = 0
|
self.server.msg_id_counter = 0
|
||||||
|
|
||||||
args_dict['version'] = version
|
args_dict["version"] = version
|
||||||
|
|
||||||
self.putHeaders(status_code, 'text/html')
|
self.putHeaders(status_code, "text/html")
|
||||||
return bytes(template.render(
|
return bytes(
|
||||||
|
template.render(
|
||||||
title=self.server.title,
|
title=self.server.title,
|
||||||
h2=self.server.title,
|
h2=self.server.title,
|
||||||
form_id=self.generate_form_id(),
|
form_id=self.generate_form_id(),
|
||||||
**args_dict,
|
**args_dict,
|
||||||
), 'UTF-8')
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
|
||||||
def render_simple_template(self, template, args_dict):
|
def render_simple_template(self, template, args_dict):
|
||||||
swap_client = self.server.swap_client
|
return bytes(
|
||||||
return bytes(template.render(
|
template.render(
|
||||||
title=self.server.title,
|
title=self.server.title,
|
||||||
**args_dict,
|
**args_dict,
|
||||||
), 'UTF-8')
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
|
||||||
def page_info(self, info_str, post_string=None):
|
def page_info(self, info_str, post_string=None):
|
||||||
template = env.get_template('info.html')
|
template = env.get_template("info.html")
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'title_str': 'BasicSwap Info',
|
template,
|
||||||
'message_str': info_str,
|
{
|
||||||
'summary': summary,
|
"title_str": "BasicSwap Info",
|
||||||
})
|
"message_str": info_str,
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_error(self, error_str, post_string=None):
|
def page_error(self, error_str, post_string=None):
|
||||||
template = env.get_template('error.html')
|
template = env.get_template("error.html")
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'title_str': 'BasicSwap Error',
|
template,
|
||||||
'message_str': error_str,
|
{
|
||||||
'summary': summary,
|
"title_str": "BasicSwap Error",
|
||||||
})
|
"message_str": error_str,
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_explorers(self, url_split, post_string):
|
def page_explorers(self, url_split, post_string):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
@@ -224,42 +247,49 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
action = -1
|
action = -1
|
||||||
messages = []
|
messages = []
|
||||||
err_messages = []
|
err_messages = []
|
||||||
form_data = self.checkForm(post_string, 'explorers', err_messages)
|
form_data = self.checkForm(post_string, "explorers", err_messages)
|
||||||
if form_data:
|
if form_data:
|
||||||
|
|
||||||
explorer = form_data[b'explorer'][0].decode('utf-8')
|
explorer = form_data[b"explorer"][0].decode("utf-8")
|
||||||
action = form_data[b'action'][0].decode('utf-8')
|
action = form_data[b"action"][0].decode("utf-8")
|
||||||
|
|
||||||
args = '' if b'args' not in form_data else form_data[b'args'][0].decode('utf-8')
|
args = (
|
||||||
|
""
|
||||||
|
if b"args" not in form_data
|
||||||
|
else form_data[b"args"][0].decode("utf-8")
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
c, e = explorer.split('_')
|
c, e = explorer.split("_")
|
||||||
exp = swap_client.coin_clients[Coins(int(c))]['explorers'][int(e)]
|
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
|
||||||
if action == 'height':
|
if action == "height":
|
||||||
result = str(exp.getChainHeight())
|
result = str(exp.getChainHeight())
|
||||||
elif action == 'block':
|
elif action == "block":
|
||||||
result = dumpj(exp.getBlock(args))
|
result = dumpj(exp.getBlock(args))
|
||||||
elif action == 'tx':
|
elif action == "tx":
|
||||||
result = dumpj(exp.getTransaction(args))
|
result = dumpj(exp.getTransaction(args))
|
||||||
elif action == 'balance':
|
elif action == "balance":
|
||||||
result = dumpj(exp.getBalance(args))
|
result = dumpj(exp.getBalance(args))
|
||||||
elif action == 'unspent':
|
elif action == "unspent":
|
||||||
result = dumpj(exp.lookupUnspentByAddress(args))
|
result = dumpj(exp.lookupUnspentByAddress(args))
|
||||||
else:
|
else:
|
||||||
result = 'Unknown action'
|
result = "Unknown action"
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
result = str(ex)
|
result = str(ex)
|
||||||
|
|
||||||
template = env.get_template('explorers.html')
|
template = env.get_template("explorers.html")
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'messages': messages,
|
template,
|
||||||
'err_messages': err_messages,
|
{
|
||||||
'explorers': listAvailableExplorers(swap_client),
|
"messages": messages,
|
||||||
'explorer': explorer,
|
"err_messages": err_messages,
|
||||||
'actions': listExplorerActions(swap_client),
|
"explorers": listAvailableExplorers(swap_client),
|
||||||
'action': action,
|
"explorer": explorer,
|
||||||
'result': result,
|
"actions": listExplorerActions(swap_client),
|
||||||
'summary': summary,
|
"action": action,
|
||||||
})
|
"result": result,
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_rpc(self, url_split, post_string):
|
def page_rpc(self, url_split, post_string):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
@@ -267,34 +297,33 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
cmd = ''
|
cmd = ""
|
||||||
coin_type_selected = -1
|
coin_type_selected = -1
|
||||||
coin_type = -1
|
coin_type = -1
|
||||||
coin_id = -1
|
call_type = "cli"
|
||||||
call_type = 'cli'
|
type_map = ""
|
||||||
type_map = ''
|
|
||||||
messages = []
|
messages = []
|
||||||
err_messages = []
|
err_messages = []
|
||||||
form_data = self.checkForm(post_string, 'rpc', err_messages)
|
form_data = self.checkForm(post_string, "rpc", err_messages)
|
||||||
if form_data:
|
if form_data:
|
||||||
try:
|
try:
|
||||||
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
|
call_type = get_data_entry_or(form_data, "call_type", "cli")
|
||||||
type_map = get_data_entry_or(form_data, 'type_map', '')
|
type_map = get_data_entry_or(form_data, "type_map", "")
|
||||||
try:
|
try:
|
||||||
coin_type_selected = get_data_entry(form_data, 'coin_type')
|
coin_type_selected = get_data_entry(form_data, "coin_type")
|
||||||
coin_type_split = coin_type_selected.split(',')
|
coin_type_split = coin_type_selected.split(",")
|
||||||
coin_type = Coins(int(coin_type_split[0]))
|
coin_type = Coins(int(coin_type_split[0]))
|
||||||
coin_variant = int(coin_type_split[1])
|
coin_variant = int(coin_type_split[1])
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError('Unknown Coin Type')
|
raise ValueError("Unknown Coin Type")
|
||||||
|
|
||||||
if coin_type in (Coins.DCR,):
|
if coin_type in (Coins.DCR,):
|
||||||
call_type = 'http'
|
call_type = "http"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd = get_data_entry(form_data, 'cmd')
|
cmd = get_data_entry(form_data, "cmd")
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError('Invalid command')
|
raise ValueError("Invalid command")
|
||||||
if coin_type in (Coins.XMR, Coins.WOW):
|
if coin_type in (Coins.XMR, Coins.WOW):
|
||||||
ci = swap_client.ci(coin_type)
|
ci = swap_client.ci(coin_type)
|
||||||
arr = cmd.split(None, 1)
|
arr = cmd.split(None, 1)
|
||||||
@@ -309,10 +338,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
params = None
|
params = None
|
||||||
rv = ci.rpc2(method, params)
|
rv = ci.rpc2(method, params)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown RPC variant')
|
raise ValueError("Unknown RPC variant")
|
||||||
result = json.dumps(rv, indent=4)
|
result = json.dumps(rv, indent=4)
|
||||||
else:
|
else:
|
||||||
if call_type == 'http':
|
if call_type == "http":
|
||||||
ci = swap_client.ci(coin_type)
|
ci = swap_client.ci(coin_type)
|
||||||
method, params = parse_cmd(cmd, type_map)
|
method, params = parse_cmd(cmd, type_map)
|
||||||
if coin_variant == 1:
|
if coin_variant == 1:
|
||||||
@@ -326,44 +355,50 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
rv = ci.rpc_wallet(method, params)
|
rv = ci.rpc_wallet(method, params)
|
||||||
if not isinstance(rv, str):
|
if not isinstance(rv, str):
|
||||||
rv = json.dumps(rv, indent=4)
|
rv = json.dumps(rv, indent=4)
|
||||||
result = cmd + '\n' + rv
|
result = cmd + "\n" + rv
|
||||||
else:
|
else:
|
||||||
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
|
result = cmd + "\n" + swap_client.callcoincli(coin_type, cmd)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
result = cmd + '\n' + str(ex)
|
result = cmd + "\n" + str(ex)
|
||||||
if self.server.swap_client.debug is True:
|
if self.server.swap_client.debug is True:
|
||||||
self.server.swap_client.log.error(traceback.format_exc())
|
self.server.swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
template = env.get_template('rpc.html')
|
template = env.get_template("rpc.html")
|
||||||
|
|
||||||
coin_available = listAvailableCoins(swap_client, with_variants=False)
|
coin_available = listAvailableCoins(swap_client, with_variants=False)
|
||||||
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
|
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
|
||||||
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
|
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
|
||||||
coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, Coins.WOW)]
|
coins = [
|
||||||
|
(str(c[0]) + ",0", c[1])
|
||||||
|
for c in coin_available
|
||||||
|
if c[0] not in (Coins.XMR, Coins.WOW)
|
||||||
|
]
|
||||||
|
|
||||||
if any(c[0] == Coins.DCR for c in coin_available):
|
if any(c[0] == Coins.DCR for c in coin_available):
|
||||||
coins.append((str(int(Coins.DCR)) + ',1', 'Decred Wallet'))
|
coins.append((str(int(Coins.DCR)) + ",1", "Decred Wallet"))
|
||||||
if any(c[0] == Coins.LTC for c in coin_available):
|
if any(c[0] == Coins.LTC for c in coin_available):
|
||||||
coins.append((str(int(Coins.LTC)) + ',2', 'Litecoin MWEB Wallet'))
|
coins.append((str(int(Coins.LTC)) + ",2", "Litecoin MWEB Wallet"))
|
||||||
if with_xmr:
|
if with_xmr:
|
||||||
coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
|
coins.append((str(int(Coins.XMR)) + ",0", "Monero"))
|
||||||
coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
|
coins.append((str(int(Coins.XMR)) + ",1", "Monero JSON"))
|
||||||
coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
|
coins.append((str(int(Coins.XMR)) + ",2", "Monero Wallet"))
|
||||||
if with_wow:
|
if with_wow:
|
||||||
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
|
coins.append((str(int(Coins.WOW)) + ",0", "Wownero"))
|
||||||
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
|
coins.append((str(int(Coins.WOW)) + ",1", "Wownero JSON"))
|
||||||
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
|
coins.append((str(int(Coins.WOW)) + ",2", "Wownero Wallet"))
|
||||||
|
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'messages': messages,
|
template,
|
||||||
'err_messages': err_messages,
|
{
|
||||||
'coins': coins,
|
"messages": messages,
|
||||||
'coin_type': coin_type_selected,
|
"err_messages": err_messages,
|
||||||
'call_type': call_type,
|
"coins": coins,
|
||||||
'result': result,
|
"coin_type": coin_type_selected,
|
||||||
'messages': messages,
|
"call_type": call_type,
|
||||||
'summary': summary,
|
"result": result,
|
||||||
})
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_active(self, url_split, post_string):
|
def page_active(self, url_split, post_string):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
@@ -371,12 +406,24 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
active_swaps = swap_client.listSwapsInProgress()
|
active_swaps = swap_client.listSwapsInProgress()
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
template = env.get_template('active.html')
|
template = env.get_template("active.html")
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'refresh': 30,
|
template,
|
||||||
'active_swaps': [(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
|
{
|
||||||
'summary': summary,
|
"refresh": 30,
|
||||||
})
|
"active_swaps": [
|
||||||
|
(
|
||||||
|
s[0].hex(),
|
||||||
|
s[1],
|
||||||
|
strBidState(s[2]),
|
||||||
|
strTxState(s[3]),
|
||||||
|
strTxState(s[4]),
|
||||||
|
)
|
||||||
|
for s in active_swaps
|
||||||
|
],
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_watched(self, url_split, post_string):
|
def page_watched(self, url_split, post_string):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
@@ -384,62 +431,68 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
|
|
||||||
template = env.get_template('watched.html')
|
template = env.get_template("watched.html")
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'refresh': 30,
|
template,
|
||||||
'last_scanned': [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
{
|
||||||
'watched_outputs': [(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
|
"refresh": 30,
|
||||||
'summary': summary,
|
"last_scanned": [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
||||||
})
|
"watched_outputs": [
|
||||||
|
(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4]))
|
||||||
|
for wo in watched_outputs
|
||||||
|
],
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def page_shutdown(self, url_split, post_string):
|
def page_shutdown(self, url_split, post_string):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
|
|
||||||
if len(url_split) > 2:
|
if len(url_split) > 2:
|
||||||
token = url_split[2]
|
token = url_split[2]
|
||||||
expect_token = self.server.session_tokens.get('shutdown', None)
|
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||||
if token != expect_token:
|
if token != expect_token:
|
||||||
return self.page_info('Unexpected token, still running.')
|
return self.page_info("Unexpected token, still running.")
|
||||||
|
|
||||||
swap_client.stopRunning()
|
swap_client.stopRunning()
|
||||||
|
|
||||||
return self.page_info('Shutting down')
|
return self.page_info("Shutting down")
|
||||||
|
|
||||||
def page_index(self, url_split):
|
def page_index(self, url_split):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
swap_client.checkSystemStatus()
|
swap_client.checkSystemStatus()
|
||||||
summary = swap_client.getSummary()
|
self.send_response(302)
|
||||||
template = env.get_template('index.html')
|
self.send_header("Location", "/offers")
|
||||||
return self.render_template(template, {
|
self.end_headers()
|
||||||
'refresh': 30,
|
return b""
|
||||||
'summary': summary,
|
|
||||||
'use_tor_proxy': swap_client.use_tor_proxy
|
|
||||||
})
|
|
||||||
|
|
||||||
def page_404(self, url_split):
|
def page_404(self, url_split):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
summary = swap_client.getSummary()
|
summary = swap_client.getSummary()
|
||||||
template = env.get_template('404.html')
|
template = env.get_template("404.html")
|
||||||
return self.render_template(template, {
|
return self.render_template(
|
||||||
'summary': summary,
|
template,
|
||||||
})
|
{
|
||||||
|
"summary": summary,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def putHeaders(self, status_code, content_type):
|
def putHeaders(self, status_code, content_type):
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
if self.server.allow_cors:
|
if self.server.allow_cors:
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header('Content-Type', content_type)
|
self.send_header("Content-Type", content_type)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def handle_http(self, status_code, path, post_string='', is_json=False):
|
def handle_http(self, status_code, path, post_string="", is_json=False):
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
parsed = parse.urlparse(self.path)
|
parsed = parse.urlparse(self.path)
|
||||||
url_split = parsed.path.split('/')
|
url_split = parsed.path.split("/")
|
||||||
if post_string == '' and len(parsed.query) > 0:
|
if post_string == "" and len(parsed.query) > 0:
|
||||||
post_string = parsed.query
|
post_string = parsed.query
|
||||||
if len(url_split) > 1 and url_split[1] == 'json':
|
if len(url_split) > 1 and url_split[1] == "json":
|
||||||
try:
|
try:
|
||||||
self.putHeaders(status_code, 'text/plain')
|
self.putHeaders(status_code, "text/plain")
|
||||||
func = js_url_to_function(url_split)
|
func = js_url_to_function(url_split)
|
||||||
return func(self, url_split, post_string, is_json)
|
return func(self, url_split, post_string, is_json)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -447,37 +500,42 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
return js_error(self, str(ex))
|
return js_error(self, str(ex))
|
||||||
|
|
||||||
if len(url_split) > 1 and url_split[1] == 'static':
|
if len(url_split) > 1 and url_split[1] == "static":
|
||||||
try:
|
try:
|
||||||
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||||
if len(url_split) > 3 and url_split[2] == 'sequence_diagrams':
|
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||||
with open(os.path.join(static_path, 'sequence_diagrams', url_split[3]), 'rb') as fp:
|
with open(
|
||||||
self.putHeaders(status_code, 'image/svg+xml')
|
os.path.join(static_path, "sequence_diagrams", url_split[3]),
|
||||||
|
"rb",
|
||||||
|
) as fp:
|
||||||
|
self.putHeaders(status_code, "image/svg+xml")
|
||||||
return fp.read()
|
return fp.read()
|
||||||
elif len(url_split) > 3 and url_split[2] == 'images':
|
elif len(url_split) > 3 and url_split[2] == "images":
|
||||||
filename = os.path.join(*url_split[3:])
|
filename = os.path.join(*url_split[3:])
|
||||||
_, extension = os.path.splitext(filename)
|
_, extension = os.path.splitext(filename)
|
||||||
mime_type = {
|
mime_type = {
|
||||||
'.svg': 'image/svg+xml',
|
".svg": "image/svg+xml",
|
||||||
'.png': 'image/png',
|
".png": "image/png",
|
||||||
'.jpg': 'image/jpeg',
|
".jpg": "image/jpeg",
|
||||||
'.gif': 'image/gif',
|
".gif": "image/gif",
|
||||||
'.ico': 'image/x-icon',
|
".ico": "image/x-icon",
|
||||||
}.get(extension, '')
|
}.get(extension, "")
|
||||||
if mime_type == '':
|
if mime_type == "":
|
||||||
raise ValueError('Unknown file type ' + filename)
|
raise ValueError("Unknown file type " + filename)
|
||||||
with open(os.path.join(static_path, 'images', filename), 'rb') as fp:
|
with open(
|
||||||
|
os.path.join(static_path, "images", filename), "rb"
|
||||||
|
) as fp:
|
||||||
self.putHeaders(status_code, mime_type)
|
self.putHeaders(status_code, mime_type)
|
||||||
return fp.read()
|
return fp.read()
|
||||||
elif len(url_split) > 3 and url_split[2] == 'css':
|
elif len(url_split) > 3 and url_split[2] == "css":
|
||||||
filename = os.path.join(*url_split[3:])
|
filename = os.path.join(*url_split[3:])
|
||||||
with open(os.path.join(static_path, 'css', filename), 'rb') as fp:
|
with open(os.path.join(static_path, "css", filename), "rb") as fp:
|
||||||
self.putHeaders(status_code, 'text/css; charset=utf-8')
|
self.putHeaders(status_code, "text/css; charset=utf-8")
|
||||||
return fp.read()
|
return fp.read()
|
||||||
elif len(url_split) > 3 and url_split[2] == 'js':
|
elif len(url_split) > 3 and url_split[2] == "js":
|
||||||
filename = os.path.join(*url_split[3:])
|
filename = os.path.join(*url_split[3:])
|
||||||
with open(os.path.join(static_path, 'js', filename), 'rb') as fp:
|
with open(os.path.join(static_path, "js", filename), "rb") as fp:
|
||||||
self.putHeaders(status_code, 'application/javascript')
|
self.putHeaders(status_code, "application/javascript")
|
||||||
return fp.read()
|
return fp.read()
|
||||||
else:
|
else:
|
||||||
return self.page_404(url_split)
|
return self.page_404(url_split)
|
||||||
@@ -492,63 +550,61 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if len(url_split) > 1:
|
if len(url_split) > 1:
|
||||||
page = url_split[1]
|
page = url_split[1]
|
||||||
|
|
||||||
if page == 'active':
|
if page == "active":
|
||||||
return self.page_active(url_split, post_string)
|
return self.page_active(url_split, post_string)
|
||||||
if page == 'wallets':
|
if page == "wallets":
|
||||||
return page_wallets(self, url_split, post_string)
|
return page_wallets(self, url_split, post_string)
|
||||||
if page == 'wallet':
|
if page == "wallet":
|
||||||
return page_wallet(self, url_split, post_string)
|
return page_wallet(self, url_split, post_string)
|
||||||
if page == 'settings':
|
if page == "settings":
|
||||||
return page_settings(self, url_split, post_string)
|
return page_settings(self, url_split, post_string)
|
||||||
if page == 'error':
|
if page == "error":
|
||||||
return self.page_error(url_split, post_string)
|
return self.page_error(url_split, post_string)
|
||||||
if page == 'info':
|
if page == "info":
|
||||||
return self.page_info(url_split, post_string)
|
return self.page_info(url_split, post_string)
|
||||||
if page == 'rpc':
|
if page == "rpc":
|
||||||
return self.page_rpc(url_split, post_string)
|
return self.page_rpc(url_split, post_string)
|
||||||
if page == 'debug':
|
if page == "debug":
|
||||||
return page_debug(self, url_split, post_string)
|
return page_debug(self, url_split, post_string)
|
||||||
if page == 'explorers':
|
if page == "explorers":
|
||||||
return self.page_explorers(url_split, post_string)
|
return self.page_explorers(url_split, post_string)
|
||||||
if page == 'offer':
|
if page == "offer":
|
||||||
return page_offer(self, url_split, post_string)
|
return page_offer(self, url_split, post_string)
|
||||||
if page == 'offers':
|
if page == "offers":
|
||||||
return page_offers(self, url_split, post_string)
|
return page_offers(self, url_split, post_string)
|
||||||
if page == 'newoffer':
|
if page == "newoffer":
|
||||||
return page_newoffer(self, url_split, post_string)
|
return page_newoffer(self, url_split, post_string)
|
||||||
if page == 'sentoffers':
|
if page == "sentoffers":
|
||||||
return page_offers(self, url_split, post_string, sent=True)
|
return page_offers(self, url_split, post_string, sent=True)
|
||||||
if page == 'bid':
|
if page == "bid":
|
||||||
return page_bid(self, url_split, post_string)
|
return page_bid(self, url_split, post_string)
|
||||||
if page == 'receivedbids':
|
if page == "bids":
|
||||||
return page_bids(self, url_split, post_string, received=True)
|
return page_bids(self, url_split, post_string)
|
||||||
if page == 'sentbids':
|
if page == "availablebids":
|
||||||
return page_bids(self, url_split, post_string, sent=True)
|
|
||||||
if page == 'availablebids':
|
|
||||||
return page_bids(self, url_split, post_string, available=True)
|
return page_bids(self, url_split, post_string, available=True)
|
||||||
if page == 'watched':
|
if page == "watched":
|
||||||
return self.page_watched(url_split, post_string)
|
return self.page_watched(url_split, post_string)
|
||||||
if page == 'smsgaddresses':
|
if page == "smsgaddresses":
|
||||||
return page_smsgaddresses(self, url_split, post_string)
|
return page_smsgaddresses(self, url_split, post_string)
|
||||||
if page == 'identity':
|
if page == "identity":
|
||||||
return page_identity(self, url_split, post_string)
|
return page_identity(self, url_split, post_string)
|
||||||
if page == 'tor':
|
if page == "tor":
|
||||||
return page_tor(self, url_split, post_string)
|
return page_tor(self, url_split, post_string)
|
||||||
if page == 'automation':
|
if page == "automation":
|
||||||
return page_automation_strategies(self, url_split, post_string)
|
return page_automation_strategies(self, url_split, post_string)
|
||||||
if page == 'automationstrategy':
|
if page == "automationstrategy":
|
||||||
return page_automation_strategy(self, url_split, post_string)
|
return page_automation_strategy(self, url_split, post_string)
|
||||||
if page == 'newautomationstrategy':
|
if page == "newautomationstrategy":
|
||||||
return page_automation_strategy_new(self, url_split, post_string)
|
return page_automation_strategy_new(self, url_split, post_string)
|
||||||
if page == 'shutdown':
|
if page == "shutdown":
|
||||||
return self.page_shutdown(url_split, post_string)
|
return self.page_shutdown(url_split, post_string)
|
||||||
if page == 'changepassword':
|
if page == "changepassword":
|
||||||
return page_changepassword(self, url_split, post_string)
|
return page_changepassword(self, url_split, post_string)
|
||||||
if page == 'unlock':
|
if page == "unlock":
|
||||||
return page_unlock(self, url_split, post_string)
|
return page_unlock(self, url_split, post_string)
|
||||||
if page == 'lock':
|
if page == "lock":
|
||||||
return page_lock(self, url_split, post_string)
|
return page_lock(self, url_split, post_string)
|
||||||
if page != '':
|
if page != "":
|
||||||
return self.page_404(url_split)
|
return self.page_404(url_split)
|
||||||
return self.page_index(url_split)
|
return self.page_index(url_split)
|
||||||
except LockedCoinError:
|
except LockedCoinError:
|
||||||
@@ -563,20 +619,20 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(response)
|
self.wfile.write(response)
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
post_string = self.rfile.read(int(self.headers.get('Content-Length')))
|
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
|
||||||
|
|
||||||
is_json = True if 'json' in self.headers.get('Content-Type', '') else False
|
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||||
response = self.handle_http(200, self.path, post_string, is_json)
|
response = self.handle_http(200, self.path, post_string, is_json)
|
||||||
self.wfile.write(response)
|
self.wfile.write(response)
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
self.putHeaders(200, 'text/html')
|
self.putHeaders(200, "text/html")
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
self.send_response(200, 'ok')
|
self.send_response(200, "ok")
|
||||||
if self.server.allow_cors:
|
if self.server.allow_cors:
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header('Access-Control-Allow-Headers', '*')
|
self.send_header("Access-Control-Allow-Headers", "*")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
|
||||||
@@ -590,7 +646,7 @@ class HttpThread(threading.Thread, HTTPServer):
|
|||||||
self.port_no = port_no
|
self.port_no = port_no
|
||||||
self.allow_cors = allow_cors
|
self.allow_cors = allow_cors
|
||||||
self.swap_client = swap_client
|
self.swap_client = swap_client
|
||||||
self.title = 'BasicSwap - ' + __version__
|
self.title = "BasicSwap - " + __version__
|
||||||
self.last_form_id = dict()
|
self.last_form_id = dict()
|
||||||
self.session_tokens = dict()
|
self.session_tokens = dict()
|
||||||
self.env = env
|
self.env = env
|
||||||
@@ -605,9 +661,9 @@ class HttpThread(threading.Thread, HTTPServer):
|
|||||||
# Send fake request
|
# Send fake request
|
||||||
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
||||||
conn.connect()
|
conn.connect()
|
||||||
conn.request('GET', '/none')
|
conn.request("GET", "/none")
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
_ = response.read()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def serve_forever(self):
|
def serve_forever(self):
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2024 tecnovert
|
# Copyright (c) 2024 tecnovert
|
||||||
|
# Copyright (c) 2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ from basicswap.chainparams import (
|
|||||||
)
|
)
|
||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
ensure,
|
ensure,
|
||||||
i2b, b2i,
|
i2b,
|
||||||
|
b2i,
|
||||||
make_int,
|
make_int,
|
||||||
format_amount,
|
format_amount,
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
@@ -26,9 +28,7 @@ from basicswap.util.ecc import (
|
|||||||
ep,
|
ep,
|
||||||
getSecretInt,
|
getSecretInt,
|
||||||
)
|
)
|
||||||
from coincurve.dleag import (
|
from coincurve.dleag import verify_secp256k1_point
|
||||||
verify_secp256k1_point
|
|
||||||
)
|
|
||||||
from coincurve.keys import (
|
from coincurve.keys import (
|
||||||
PublicKey,
|
PublicKey,
|
||||||
)
|
)
|
||||||
@@ -52,6 +52,11 @@ class CoinInterface:
|
|||||||
self.setDefaults()
|
self.setDefaults()
|
||||||
self._network = network
|
self._network = network
|
||||||
self._mx_wallet = threading.Lock()
|
self._mx_wallet = threading.Lock()
|
||||||
|
self._altruistic = True
|
||||||
|
|
||||||
|
def interface_type(self) -> int:
|
||||||
|
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.
|
||||||
|
return self.coin_type()
|
||||||
|
|
||||||
def setDefaults(self):
|
def setDefaults(self):
|
||||||
self._unknown_wallet_seed = True
|
self._unknown_wallet_seed = True
|
||||||
@@ -66,33 +71,33 @@ class CoinInterface:
|
|||||||
|
|
||||||
def coin_name(self) -> str:
|
def coin_name(self) -> str:
|
||||||
coin_chainparams = chainparams[self.coin_type()]
|
coin_chainparams = chainparams[self.coin_type()]
|
||||||
if coin_chainparams.get('use_ticker_as_name', False):
|
if "display_name" in coin_chainparams:
|
||||||
return coin_chainparams['ticker']
|
return coin_chainparams["display_name"]
|
||||||
return coin_chainparams['name'].capitalize()
|
return coin_chainparams["name"].capitalize()
|
||||||
|
|
||||||
def ticker(self) -> str:
|
def ticker(self) -> str:
|
||||||
ticker = chainparams[self.coin_type()]['ticker']
|
ticker = chainparams[self.coin_type()]["ticker"]
|
||||||
if self._network == 'testnet':
|
if self._network == "testnet":
|
||||||
ticker = 't' + ticker
|
ticker = "t" + ticker
|
||||||
elif self._network == 'regtest':
|
elif self._network == "regtest":
|
||||||
ticker = 'rt' + ticker
|
ticker = "rt" + ticker
|
||||||
return ticker
|
return ticker
|
||||||
|
|
||||||
def getExchangeTicker(self, exchange_name: str) -> str:
|
def getExchangeTicker(self, exchange_name: str) -> str:
|
||||||
return chainparams[self.coin_type()]['ticker']
|
return chainparams[self.coin_type()]["ticker"]
|
||||||
|
|
||||||
def getExchangeName(self, exchange_name: str) -> str:
|
def getExchangeName(self, exchange_name: str) -> str:
|
||||||
return chainparams[self.coin_type()]['name']
|
return chainparams[self.coin_type()]["name"]
|
||||||
|
|
||||||
def ticker_mainnet(self) -> str:
|
def ticker_mainnet(self) -> str:
|
||||||
ticker = chainparams[self.coin_type()]['ticker']
|
ticker = chainparams[self.coin_type()]["ticker"]
|
||||||
return ticker
|
return ticker
|
||||||
|
|
||||||
def min_amount(self) -> int:
|
def min_amount(self) -> int:
|
||||||
return chainparams[self.coin_type()][self._network]['min_amount']
|
return chainparams[self.coin_type()][self._network]["min_amount"]
|
||||||
|
|
||||||
def max_amount(self) -> int:
|
def max_amount(self) -> int:
|
||||||
return chainparams[self.coin_type()][self._network]['max_amount']
|
return chainparams[self.coin_type()][self._network]["max_amount"]
|
||||||
|
|
||||||
def setWalletSeedWarning(self, value: bool) -> None:
|
def setWalletSeedWarning(self, value: bool) -> None:
|
||||||
self._unknown_wallet_seed = value
|
self._unknown_wallet_seed = value
|
||||||
@@ -110,7 +115,7 @@ class CoinInterface:
|
|||||||
return chainparams[self.coin_type()][self._network]
|
return chainparams[self.coin_type()][self._network]
|
||||||
|
|
||||||
def has_segwit(self) -> bool:
|
def has_segwit(self) -> bool:
|
||||||
return chainparams[self.coin_type()].get('has_segwit', True)
|
return chainparams[self.coin_type()].get("has_segwit", True)
|
||||||
|
|
||||||
def use_p2shp2wsh(self) -> bool:
|
def use_p2shp2wsh(self) -> bool:
|
||||||
# p2sh-p2wsh
|
# p2sh-p2wsh
|
||||||
@@ -120,24 +125,26 @@ class CoinInterface:
|
|||||||
if isinstance(ex, TemporaryError):
|
if isinstance(ex, TemporaryError):
|
||||||
return True
|
return True
|
||||||
str_error: str = str(ex).lower()
|
str_error: str = str(ex).lower()
|
||||||
if 'not enough unlocked money' in str_error:
|
if "not enough unlocked money" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'no unlocked balance' in str_error:
|
if "no unlocked balance" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'transaction was rejected by daemon' in str_error:
|
if "transaction was rejected by daemon" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'invalid unlocked_balance' in str_error:
|
if "invalid unlocked_balance" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'daemon is busy' in str_error:
|
if "daemon is busy" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'timed out' in str_error:
|
if "timed out" in str_error:
|
||||||
return True
|
return True
|
||||||
if 'request-sent' in str_error:
|
if "request-sent" in str_error:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def setConfTarget(self, new_conf_target: int) -> None:
|
def setConfTarget(self, new_conf_target: int) -> None:
|
||||||
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
|
ensure(
|
||||||
|
new_conf_target >= 1 and new_conf_target < 33, "Invalid conf_target value"
|
||||||
|
)
|
||||||
self._conf_target = new_conf_target
|
self._conf_target = new_conf_target
|
||||||
|
|
||||||
def walletRestoreHeight(self) -> int:
|
def walletRestoreHeight(self) -> int:
|
||||||
@@ -166,31 +173,19 @@ class CoinInterface:
|
|||||||
def checkWallets(self) -> int:
|
def checkWallets(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def altruistic(self) -> bool:
|
||||||
|
return self._altruistic
|
||||||
|
|
||||||
class AdaptorSigInterface():
|
|
||||||
|
class AdaptorSigInterface:
|
||||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||||
return [
|
return [b"", bytes(72), bytes(72), bytes(len(script))]
|
||||||
b'',
|
|
||||||
bytes(72),
|
|
||||||
bytes(72),
|
|
||||||
bytes(len(script))
|
|
||||||
]
|
|
||||||
|
|
||||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||||
return [
|
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||||
b'',
|
|
||||||
bytes(72),
|
|
||||||
bytes(72),
|
|
||||||
bytes((1,)),
|
|
||||||
bytes(len(script))
|
|
||||||
]
|
|
||||||
|
|
||||||
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
|
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
|
||||||
return [
|
return [bytes(72), b"", bytes(len(script))]
|
||||||
bytes(72),
|
|
||||||
b'',
|
|
||||||
bytes(len(script))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
||||||
@@ -198,7 +193,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
|||||||
def curve_type():
|
def curve_type():
|
||||||
return Curves.secp256k1
|
return Curves.secp256k1
|
||||||
|
|
||||||
def getNewSecretKey(self) -> bytes:
|
def getNewRandomKey(self) -> bytes:
|
||||||
return i2b(getSecretInt())
|
return i2b(getSecretInt())
|
||||||
|
|
||||||
def getPubkey(self, privkey: bytes) -> bytes:
|
def getPubkey(self, privkey: bytes) -> bytes:
|
||||||
@@ -209,7 +204,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
|||||||
|
|
||||||
def verifyKey(self, k: bytes) -> bool:
|
def verifyKey(self, k: bytes) -> bool:
|
||||||
i = b2i(k)
|
i = b2i(k)
|
||||||
return (i < ep.o and i > 0)
|
return i < ep.o and i > 0
|
||||||
|
|
||||||
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
|
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
|
||||||
return verify_secp256k1_point(pubkey_bytes)
|
return verify_secp256k1_point(pubkey_bytes)
|
||||||
|
|||||||
1136
basicswap/interface/bch.py
Normal file
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||||
|
|
||||||
|
def polymod(values):
|
||||||
|
chk = 1
|
||||||
|
generator = [
|
||||||
|
(0x01, 0x98F2BC8E61),
|
||||||
|
(0x02, 0x79B76D99E2),
|
||||||
|
(0x04, 0xF33E5FB3C4),
|
||||||
|
(0x08, 0xAE2EABE2A8),
|
||||||
|
(0x10, 0x1E4F43E470),
|
||||||
|
]
|
||||||
|
for value in values:
|
||||||
|
top = chk >> 35
|
||||||
|
chk = ((chk & 0x07FFFFFFFF) << 5) ^ value
|
||||||
|
for i in generator:
|
||||||
|
if top & i[0] != 0:
|
||||||
|
chk ^= i[1]
|
||||||
|
return chk ^ 1
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_checksum(prefix, payload):
|
||||||
|
poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
|
||||||
|
out = list()
|
||||||
|
for i in range(8):
|
||||||
|
out.append((poly >> 5 * (7 - i)) & 0x1F)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def verify_checksum(prefix, payload):
|
||||||
|
return polymod(prefix_expand(prefix) + payload) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def b32decode(inputs):
|
||||||
|
out = list()
|
||||||
|
for letter in inputs:
|
||||||
|
out.append(CHARSET.find(letter))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def b32encode(inputs):
|
||||||
|
out = ""
|
||||||
|
for char_code in inputs:
|
||||||
|
out += CHARSET[char_code]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def convertbits(data, frombits, tobits, pad=True):
|
||||||
|
acc = 0
|
||||||
|
bits = 0
|
||||||
|
ret = []
|
||||||
|
maxv = (1 << tobits) - 1
|
||||||
|
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||||
|
for value in data:
|
||||||
|
if value < 0 or (value >> frombits):
|
||||||
|
return None
|
||||||
|
acc = ((acc << frombits) | value) & max_acc
|
||||||
|
bits += frombits
|
||||||
|
while bits >= tobits:
|
||||||
|
bits -= tobits
|
||||||
|
ret.append((acc >> bits) & maxv)
|
||||||
|
if pad:
|
||||||
|
if bits:
|
||||||
|
ret.append((acc << (tobits - bits)) & maxv)
|
||||||
|
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||||
|
return None
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_expand(prefix):
|
||||||
|
return [ord(x) & 0x1F for x in prefix] + [0]
|
||||||
|
|
||||||
|
|
||||||
|
class Address:
|
||||||
|
"""
|
||||||
|
Class to handle CashAddr.
|
||||||
|
|
||||||
|
:param version: Version of CashAddr
|
||||||
|
:type version: ``str``
|
||||||
|
:param payload: Payload of CashAddr as int list of the bytearray
|
||||||
|
:type payload: ``list`` of ``int``
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSIONS = {
|
||||||
|
"P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"},
|
||||||
|
"P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"},
|
||||||
|
"P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"},
|
||||||
|
"P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"},
|
||||||
|
"P2SH32-TESTNET": {
|
||||||
|
"prefix": "bchtest",
|
||||||
|
"version_bit": 11,
|
||||||
|
"network": "testnet",
|
||||||
|
},
|
||||||
|
"P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"},
|
||||||
|
"P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"},
|
||||||
|
"P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"},
|
||||||
|
"P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"},
|
||||||
|
"P2SH20-CATKN": {
|
||||||
|
"prefix": "bitcoincash",
|
||||||
|
"version_bit": 24,
|
||||||
|
"network": "mainnet",
|
||||||
|
},
|
||||||
|
"P2SH32-CATKN": {
|
||||||
|
"prefix": "bitcoincash",
|
||||||
|
"version_bit": 27,
|
||||||
|
"network": "mainnet",
|
||||||
|
},
|
||||||
|
"P2PKH-CATKN": {
|
||||||
|
"prefix": "bitcoincash",
|
||||||
|
"version_bit": 16,
|
||||||
|
"network": "mainnet",
|
||||||
|
},
|
||||||
|
"P2SH20-CATKN-TESTNET": {
|
||||||
|
"prefix": "bchtest",
|
||||||
|
"version_bit": 24,
|
||||||
|
"network": "testnet",
|
||||||
|
},
|
||||||
|
"P2SH32-CATKN-TESTNET": {
|
||||||
|
"prefix": "bchtest",
|
||||||
|
"version_bit": 27,
|
||||||
|
"network": "testnet",
|
||||||
|
},
|
||||||
|
"P2PKH-CATKN-TESTNET": {
|
||||||
|
"prefix": "bchtest",
|
||||||
|
"version_bit": 16,
|
||||||
|
"network": "testnet",
|
||||||
|
},
|
||||||
|
"P2SH20-CATKN-REGTEST": {
|
||||||
|
"prefix": "bchreg",
|
||||||
|
"version_bit": 24,
|
||||||
|
"network": "regtest",
|
||||||
|
},
|
||||||
|
"P2SH32-CATKN-REGTEST": {
|
||||||
|
"prefix": "bchreg",
|
||||||
|
"version_bit": 27,
|
||||||
|
"network": "regtest",
|
||||||
|
},
|
||||||
|
"P2PKH-CATKN-REGTEST": {
|
||||||
|
"prefix": "bchreg",
|
||||||
|
"version_bit": 16,
|
||||||
|
"network": "regtest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"}
|
||||||
|
|
||||||
|
ADDRESS_TYPES = {
|
||||||
|
0: "P2PKH",
|
||||||
|
8: "P2SH20",
|
||||||
|
11: "P2SH32",
|
||||||
|
16: "P2PKH-CATKN",
|
||||||
|
24: "P2SH20-CATKN",
|
||||||
|
27: "P2SH32-CATKN",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, version, payload):
|
||||||
|
if version not in Address.VERSIONS:
|
||||||
|
raise ValueError("Invalid address version provided")
|
||||||
|
|
||||||
|
self.version = version
|
||||||
|
self.payload = payload
|
||||||
|
self.prefix = Address.VERSIONS[self.version]["prefix"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Address('{self.cash_address()}')"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, str):
|
||||||
|
return self.cash_address() == other
|
||||||
|
elif isinstance(other, Address):
|
||||||
|
return self.cash_address() == other.cash_address()
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Address can be compared to a string address"
|
||||||
|
" or an instance of Address"
|
||||||
|
)
|
||||||
|
|
||||||
|
def cash_address(self):
|
||||||
|
"""
|
||||||
|
Generate CashAddr of the Address
|
||||||
|
|
||||||
|
:rtype: ``str``
|
||||||
|
"""
|
||||||
|
version_bit = Address.VERSIONS[self.version]["version_bit"]
|
||||||
|
payload = [version_bit] + list(self.payload)
|
||||||
|
payload = convertbits(payload, 8, 5)
|
||||||
|
checksum = calculate_checksum(self.prefix, payload)
|
||||||
|
return self.prefix + ":" + b32encode(payload + checksum)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_string(address):
|
||||||
|
"""
|
||||||
|
Generate Address from a cashadress string
|
||||||
|
|
||||||
|
:param scriptcode: The cashaddress string
|
||||||
|
:type scriptcode: ``str``
|
||||||
|
:returns: Instance of :class:~bitcash.cashaddress.Address
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
address = str(address)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("Expected string as input")
|
||||||
|
|
||||||
|
if address.upper() != address and address.lower() != address:
|
||||||
|
raise ValueError(
|
||||||
|
"Cash address contains uppercase and lowercase characters: " + address
|
||||||
|
)
|
||||||
|
|
||||||
|
address = address.lower()
|
||||||
|
colon_count = address.count(":")
|
||||||
|
if colon_count == 0:
|
||||||
|
raise ValueError("Cash address is missing prefix")
|
||||||
|
if colon_count > 1:
|
||||||
|
raise ValueError("Cash address contains more than one colon character")
|
||||||
|
|
||||||
|
prefix, base32string = address.split(":")
|
||||||
|
decoded = b32decode(base32string)
|
||||||
|
|
||||||
|
if not verify_checksum(prefix, decoded):
|
||||||
|
raise ValueError(
|
||||||
|
"Bad cash address checksum for address {}".format(address)
|
||||||
|
)
|
||||||
|
converted = convertbits(decoded, 5, 8)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = Address.ADDRESS_TYPES[converted[0]]
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("Could not determine address version")
|
||||||
|
|
||||||
|
version += Address.VERSION_SUFFIXES[prefix]
|
||||||
|
|
||||||
|
payload = converted[1:-6]
|
||||||
|
return Address(version, payload)
|
||||||
|
|
||||||
|
class TestFrameworkScript(unittest.TestCase):
|
||||||
|
def test_base58encodedecode(self):
|
||||||
|
def check_cashaddress(address: str):
|
||||||
|
self.assertEqual(Address.from_string(address).cash_address(), address)
|
||||||
|
|
||||||
|
check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6")
|
||||||
43
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
|
||||||
|
from basicswap.contrib.test_framework.script import CScriptOp
|
||||||
|
|
||||||
|
|
||||||
|
OP_TXINPUTCOUNT = CScriptOp(0xc3)
|
||||||
|
OP_1 = CScriptOp(0x51)
|
||||||
|
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
|
||||||
|
OP_TXOUTPUTCOUNT = CScriptOp(0xc4)
|
||||||
|
OP_0 = CScriptOp(0x00)
|
||||||
|
OP_UTXOVALUE = CScriptOp(0xc6)
|
||||||
|
OP_OUTPUTVALUE = CScriptOp(0xcc)
|
||||||
|
OP_SUB = CScriptOp(0x94)
|
||||||
|
OP_UTXOTOKENCATEGORY = CScriptOp(0xce)
|
||||||
|
OP_OUTPUTTOKENCATEGORY = CScriptOp(0xd1)
|
||||||
|
OP_EQUALVERIFY = CScriptOp(0x88)
|
||||||
|
OP_UTXOTOKENCOMMITMENT = CScriptOp(0xcf)
|
||||||
|
OP_OUTPUTTOKENCOMMITMENT = CScriptOp(0xd2)
|
||||||
|
OP_UTXOTOKENAMOUNT = CScriptOp(0xd0)
|
||||||
|
OP_OUTPUTTOKENAMOUNT = CScriptOp(0xd3)
|
||||||
|
OP_INPUTSEQUENCENUMBER = CScriptOp(0xcb)
|
||||||
|
OP_NOTIF = CScriptOp(0x64)
|
||||||
|
OP_OUTPUTBYTECODE = CScriptOp(0xcd)
|
||||||
|
OP_OVER = CScriptOp(0x78)
|
||||||
|
OP_CHECKDATASIG = CScriptOp(0xba)
|
||||||
|
OP_CHECKDATASIGVERIFY = CScriptOp(0xbb)
|
||||||
|
OP_ELSE = CScriptOp(0x67)
|
||||||
|
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
|
||||||
|
OP_DROP = CScriptOp(0x75)
|
||||||
|
OP_EQUAL = CScriptOp(0x87)
|
||||||
|
OP_ENDIF = CScriptOp(0x68)
|
||||||
|
OP_HASH256 = CScriptOp(0xaa)
|
||||||
|
OP_PUSHBYTES_32 = CScriptOp(0x20)
|
||||||
|
OP_DUP = CScriptOp(0x76)
|
||||||
|
OP_HASH160 = CScriptOp(0xa9)
|
||||||
|
OP_CHECKSIG = CScriptOp(0xac)
|
||||||
|
OP_SHA256 = CScriptOp(0xa8)
|
||||||
|
OP_VERIFY = CScriptOp(0x69)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022 tecnovert
|
# Copyright (c) 2022-2024 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -11,7 +12,10 @@ from basicswap.util.address import decodeAddress
|
|||||||
from basicswap.contrib.mnemonic import Mnemonic
|
from basicswap.contrib.mnemonic import Mnemonic
|
||||||
from basicswap.contrib.test_framework.script import (
|
from basicswap.contrib.test_framework.script import (
|
||||||
CScript,
|
CScript,
|
||||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
OP_DUP,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
OP_CHECKSIG,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,41 +26,62 @@ class DASHInterface(BTCInterface):
|
|||||||
|
|
||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super().__init__(coin_settings, network, swap_client)
|
super().__init__(coin_settings, network, swap_client)
|
||||||
self._wallet_passphrase = ''
|
self._wallet_passphrase = ""
|
||||||
self._have_checked_seed = False
|
self._have_checked_seed = False
|
||||||
|
|
||||||
def entropyToMnemonic(self, key: bytes) -> str:
|
self._wallet_v20_compatible = (
|
||||||
return Mnemonic('english').to_mnemonic(key)
|
False
|
||||||
|
if not swap_client
|
||||||
def initialiseWallet(self, key: bytes):
|
else swap_client.getChainClientSettings(self.coin_type()).get(
|
||||||
words = self.entropyToMnemonic(key)
|
"wallet_v20_compatible", False
|
||||||
|
)
|
||||||
mnemonic_passphrase = ''
|
)
|
||||||
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
|
|
||||||
self._have_checked_seed = False
|
|
||||||
if self._wallet_passphrase != '':
|
|
||||||
self.unlockWallet(self._wallet_passphrase)
|
|
||||||
|
|
||||||
def decodeAddress(self, address: str) -> bytes:
|
def decodeAddress(self, address: str) -> bytes:
|
||||||
return decodeAddress(address)[1:]
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
def checkExpectedSeed(self, key_hash: str):
|
def getWalletSeedID(self) -> str:
|
||||||
try:
|
hdseed: str = self.rpc_wallet("dumphdinfo")["hdseed"]
|
||||||
rv = self.rpc_wallet('dumphdinfo')
|
return self.getSeedHash(bytes.fromhex(hdseed)).hex()
|
||||||
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
|
|
||||||
|
def entropyToMnemonic(self, key: bytes) -> None:
|
||||||
|
return Mnemonic("english").to_mnemonic(key)
|
||||||
|
|
||||||
|
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||||
|
self._have_checked_seed = False
|
||||||
|
if self._wallet_v20_compatible:
|
||||||
|
self._log.warning("Generating wallet compatible with v20 seed.")
|
||||||
|
words = self.entropyToMnemonic(key_bytes)
|
||||||
|
mnemonic_passphrase = ""
|
||||||
|
self.rpc_wallet(
|
||||||
|
"upgradetohd", [words, mnemonic_passphrase, self._wallet_passphrase]
|
||||||
|
)
|
||||||
|
self._have_checked_seed = False
|
||||||
|
if self._wallet_passphrase != "":
|
||||||
|
self.unlockWallet(self._wallet_passphrase)
|
||||||
|
return
|
||||||
|
|
||||||
|
key_wif = self.encodeKey(key_bytes)
|
||||||
|
self.rpc_wallet("sethdseed", [True, key_wif])
|
||||||
|
|
||||||
|
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||||
|
self._expect_seedid_hex = expect_seedid
|
||||||
|
rv = self.rpc_wallet("dumphdinfo")
|
||||||
|
if rv["mnemonic"] != "":
|
||||||
|
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
|
||||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||||
|
have_expected_seed: bool = expect_seedid == entropy_hash
|
||||||
|
else:
|
||||||
|
have_expected_seed: bool = expect_seedid == self.getWalletSeedID()
|
||||||
self._have_checked_seed = True
|
self._have_checked_seed = True
|
||||||
return entropy_hash == key_hash
|
return have_expected_seed
|
||||||
except Exception as e:
|
|
||||||
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def withdrawCoin(self, value, addr_to, subfee):
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
|
params = [addr_to, value, "", "", subfee, False, False, self._conf_target]
|
||||||
return self.rpc_wallet('sendtoaddress', params)
|
return self.rpc_wallet("sendtoaddress", params)
|
||||||
|
|
||||||
def getSpendableBalance(self) -> int:
|
def getSpendableBalance(self) -> int:
|
||||||
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
|
return self.make_int(self.rpc_wallet("getwalletinfo")["balance"])
|
||||||
|
|
||||||
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
# Return P2PKH
|
# Return P2PKH
|
||||||
@@ -66,29 +91,38 @@ class DASHInterface(BTCInterface):
|
|||||||
add_bytes = 107
|
add_bytes = 107
|
||||||
size = len(tx.serialize_with_witness()) + add_bytes
|
size = len(tx.serialize_with_witness()) + add_bytes
|
||||||
pay_fee = round(fee_rate * size / 1000)
|
pay_fee = round(fee_rate * size / 1000)
|
||||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
self._log.info(
|
||||||
|
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||||
|
)
|
||||||
return pay_fee
|
return pay_fee
|
||||||
|
|
||||||
def findTxnByHash(self, txid_hex: str):
|
def findTxnByHash(self, txid_hex: str):
|
||||||
# Only works for wallet txns
|
# Only works for wallet txns
|
||||||
try:
|
try:
|
||||||
rv = self.rpc_wallet('gettransaction', [txid_hex])
|
rv = self.rpc_wallet("gettransaction", [txid_hex])
|
||||||
except Exception as ex:
|
except Exception as e: # noqa: F841
|
||||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
self._log.debug(
|
||||||
|
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def unlockWallet(self, password: str):
|
def unlockWallet(self, password: str):
|
||||||
super().unlockWallet(password)
|
super().unlockWallet(password)
|
||||||
|
if self._wallet_v20_compatible:
|
||||||
# Store password for initialiseWallet
|
# Store password for initialiseWallet
|
||||||
self._wallet_passphrase = password
|
self._wallet_passphrase = password
|
||||||
if not self._have_checked_seed:
|
if not self._have_checked_seed:
|
||||||
|
try:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
except Exception as ex:
|
||||||
|
# dumphdinfo can fail if the wallet is not initialised
|
||||||
|
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
|
||||||
|
|
||||||
def lockWallet(self):
|
def lockWallet(self):
|
||||||
super().lockWallet()
|
super().lockWallet()
|
||||||
self._wallet_passphrase = ''
|
self._wallet_passphrase = ""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
from .dcr import DCRInterface
|
from .dcr import DCRInterface
|
||||||
|
|
||||||
__all__ = ['DCRInterface',]
|
__all__ = [
|
||||||
|
"DCRInterface",
|
||||||
|
]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class SigHashType(IntEnum):
|
|||||||
SigHashSingle = 0x3
|
SigHashSingle = 0x3
|
||||||
SigHashAnyOneCanPay = 0x80
|
SigHashAnyOneCanPay = 0x80
|
||||||
|
|
||||||
SigHashMask = 0x1f
|
SigHashMask = 0x1F
|
||||||
|
|
||||||
|
|
||||||
class SignatureType(IntEnum):
|
class SignatureType(IntEnum):
|
||||||
@@ -33,7 +33,7 @@ class SignatureType(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class COutPoint:
|
class COutPoint:
|
||||||
__slots__ = ('hash', 'n', 'tree')
|
__slots__ = ("hash", "n", "tree")
|
||||||
|
|
||||||
def __init__(self, hash=0, n=0, tree=0):
|
def __init__(self, hash=0, n=0, tree=0):
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
@@ -41,24 +41,30 @@ class COutPoint:
|
|||||||
self.tree = tree
|
self.tree = tree
|
||||||
|
|
||||||
def get_hash(self) -> bytes:
|
def get_hash(self) -> bytes:
|
||||||
return self.hash.to_bytes(32, 'big')
|
return self.hash.to_bytes(32, "big")
|
||||||
|
|
||||||
|
|
||||||
class CTxIn:
|
class CTxIn:
|
||||||
__slots__ = ('prevout', 'sequence',
|
__slots__ = (
|
||||||
'value_in', 'block_height', 'block_index', 'signature_script') # Witness
|
"prevout",
|
||||||
|
"sequence",
|
||||||
|
"value_in",
|
||||||
|
"block_height",
|
||||||
|
"block_index",
|
||||||
|
"signature_script",
|
||||||
|
) # Witness
|
||||||
|
|
||||||
def __init__(self, prevout=COutPoint(), sequence=0):
|
def __init__(self, prevout=COutPoint(), sequence=0):
|
||||||
self.prevout = prevout
|
self.prevout = prevout
|
||||||
self.sequence = sequence
|
self.sequence = sequence
|
||||||
self.value_in = -1
|
self.value_in = -1
|
||||||
self.block_height = 0
|
self.block_height = 0
|
||||||
self.block_index = 0xffffffff
|
self.block_index = 0xFFFFFFFF
|
||||||
self.signature_script = bytes()
|
self.signature_script = bytes()
|
||||||
|
|
||||||
|
|
||||||
class CTxOut:
|
class CTxOut:
|
||||||
__slots__ = ('value', 'version', 'script_pubkey')
|
__slots__ = ("value", "version", "script_pubkey")
|
||||||
|
|
||||||
def __init__(self, value=0, script_pubkey=bytes()):
|
def __init__(self, value=0, script_pubkey=bytes()):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -67,7 +73,7 @@ class CTxOut:
|
|||||||
|
|
||||||
|
|
||||||
class CTransaction:
|
class CTransaction:
|
||||||
__slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry')
|
__slots__ = ("hash", "version", "vin", "vout", "locktime", "expiry")
|
||||||
|
|
||||||
def __init__(self, tx=None):
|
def __init__(self, tx=None):
|
||||||
if tx is None:
|
if tx is None:
|
||||||
@@ -85,8 +91,8 @@ class CTransaction:
|
|||||||
|
|
||||||
def deserialize(self, data: bytes) -> None:
|
def deserialize(self, data: bytes) -> None:
|
||||||
|
|
||||||
version = int.from_bytes(data[:4], 'little')
|
version = int.from_bytes(data[:4], "little")
|
||||||
self.version = version & 0xffff
|
self.version = version & 0xFFFF
|
||||||
ser_type: int = version >> 16
|
ser_type: int = version >> 16
|
||||||
o = 4
|
o = 4
|
||||||
|
|
||||||
@@ -97,13 +103,13 @@ class CTransaction:
|
|||||||
for i in range(num_txin):
|
for i in range(num_txin):
|
||||||
txi = CTxIn()
|
txi = CTxIn()
|
||||||
txi.prevout = COutPoint()
|
txi.prevout = COutPoint()
|
||||||
txi.prevout.hash = int.from_bytes(data[o:o + 32], 'little')
|
txi.prevout.hash = int.from_bytes(data[o : o + 32], "little")
|
||||||
o += 32
|
o += 32
|
||||||
txi.prevout.n = int.from_bytes(data[o:o + 4], 'little')
|
txi.prevout.n = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
txi.prevout.tree = data[o]
|
txi.prevout.tree = data[o]
|
||||||
o += 1
|
o += 1
|
||||||
txi.sequence = int.from_bytes(data[o:o + 4], 'little')
|
txi.sequence = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
self.vin.append(txi)
|
self.vin.append(txi)
|
||||||
|
|
||||||
@@ -112,9 +118,9 @@ class CTransaction:
|
|||||||
|
|
||||||
for i in range(num_txout):
|
for i in range(num_txout):
|
||||||
txo = CTxOut()
|
txo = CTxOut()
|
||||||
txo.value = int.from_bytes(data[o:o + 8], 'little')
|
txo.value = int.from_bytes(data[o : o + 8], "little")
|
||||||
o += 8
|
o += 8
|
||||||
txo.version = int.from_bytes(data[o:o + 2], 'little')
|
txo.version = int.from_bytes(data[o : o + 2], "little")
|
||||||
o += 2
|
o += 2
|
||||||
script_bytes, nb = decode_compactsize(data, o)
|
script_bytes, nb = decode_compactsize(data, o)
|
||||||
o += nb
|
o += nb
|
||||||
@@ -122,9 +128,9 @@ class CTransaction:
|
|||||||
o += script_bytes
|
o += script_bytes
|
||||||
self.vout.append(txo)
|
self.vout.append(txo)
|
||||||
|
|
||||||
self.locktime = int.from_bytes(data[o:o + 4], 'little')
|
self.locktime = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
self.expiry = int.from_bytes(data[o:o + 4], 'little')
|
self.expiry = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
|
|
||||||
if ser_type == TxSerializeType.NoWitness:
|
if ser_type == TxSerializeType.NoWitness:
|
||||||
@@ -137,15 +143,15 @@ class CTransaction:
|
|||||||
self.vin = [CTxIn() for _ in range(num_wit_scripts)]
|
self.vin = [CTxIn() for _ in range(num_wit_scripts)]
|
||||||
else:
|
else:
|
||||||
if num_wit_scripts != len(self.vin):
|
if num_wit_scripts != len(self.vin):
|
||||||
raise ValueError('non equal witness and prefix txin quantities')
|
raise ValueError("non equal witness and prefix txin quantities")
|
||||||
|
|
||||||
for i in range(num_wit_scripts):
|
for i in range(num_wit_scripts):
|
||||||
txi = self.vin[i]
|
txi = self.vin[i]
|
||||||
txi.value_in = int.from_bytes(data[o:o + 8], 'little')
|
txi.value_in = int.from_bytes(data[o : o + 8], "little")
|
||||||
o += 8
|
o += 8
|
||||||
txi.block_height = int.from_bytes(data[o:o + 4], 'little')
|
txi.block_height = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
txi.block_index = int.from_bytes(data[o:o + 4], 'little')
|
txi.block_index = int.from_bytes(data[o : o + 4], "little")
|
||||||
o += 4
|
o += 4
|
||||||
script_bytes, nb = decode_compactsize(data, o)
|
script_bytes, nb = decode_compactsize(data, o)
|
||||||
o += nb
|
o += nb
|
||||||
@@ -154,34 +160,36 @@ class CTransaction:
|
|||||||
|
|
||||||
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
|
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
|
||||||
data = bytes()
|
data = bytes()
|
||||||
version = (self.version & 0xffff) | (ser_type << 16)
|
version = (self.version & 0xFFFF) | (ser_type << 16)
|
||||||
data += version.to_bytes(4, 'little')
|
data += version.to_bytes(4, "little")
|
||||||
|
|
||||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
|
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
|
||||||
data += encode_compactsize(len(self.vin))
|
data += encode_compactsize(len(self.vin))
|
||||||
for txi in self.vin:
|
for txi in self.vin:
|
||||||
data += txi.prevout.hash.to_bytes(32, 'little')
|
data += txi.prevout.hash.to_bytes(32, "little")
|
||||||
data += txi.prevout.n.to_bytes(4, 'little')
|
data += txi.prevout.n.to_bytes(4, "little")
|
||||||
data += txi.prevout.tree.to_bytes(1, 'little')
|
data += txi.prevout.tree.to_bytes(1, "little")
|
||||||
data += txi.sequence.to_bytes(4, 'little')
|
data += txi.sequence.to_bytes(4, "little")
|
||||||
|
|
||||||
data += encode_compactsize(len(self.vout))
|
data += encode_compactsize(len(self.vout))
|
||||||
for txo in self.vout:
|
for txo in self.vout:
|
||||||
data += txo.value.to_bytes(8, 'little')
|
data += txo.value.to_bytes(8, "little")
|
||||||
data += txo.version.to_bytes(2, 'little')
|
data += txo.version.to_bytes(2, "little")
|
||||||
data += encode_compactsize(len(txo.script_pubkey))
|
data += encode_compactsize(len(txo.script_pubkey))
|
||||||
data += txo.script_pubkey
|
data += txo.script_pubkey
|
||||||
|
|
||||||
data += self.locktime.to_bytes(4, 'little')
|
data += self.locktime.to_bytes(4, "little")
|
||||||
data += self.expiry.to_bytes(4, 'little')
|
data += self.expiry.to_bytes(4, "little")
|
||||||
|
|
||||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
|
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
|
||||||
data += encode_compactsize(len(self.vin))
|
data += encode_compactsize(len(self.vin))
|
||||||
for txi in self.vin:
|
for txi in self.vin:
|
||||||
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
|
tc_value_in = (
|
||||||
data += tc_value_in.to_bytes(8, 'little')
|
txi.value_in & 0xFFFFFFFFFFFFFFFF
|
||||||
data += txi.block_height.to_bytes(4, 'little')
|
) # Convert negative values
|
||||||
data += txi.block_index.to_bytes(4, 'little')
|
data += tc_value_in.to_bytes(8, "little")
|
||||||
|
data += txi.block_height.to_bytes(4, "little")
|
||||||
|
data += txi.block_index.to_bytes(4, "little")
|
||||||
data += encode_compactsize(len(txi.signature_script))
|
data += encode_compactsize(len(txi.signature_script))
|
||||||
data += txi.signature_script
|
data += txi.signature_script
|
||||||
|
|
||||||
@@ -191,10 +199,10 @@ class CTransaction:
|
|||||||
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
|
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
|
||||||
|
|
||||||
def TxHashWitness(self) -> bytes:
|
def TxHashWitness(self) -> bytes:
|
||||||
raise ValueError('todo')
|
raise ValueError("todo")
|
||||||
|
|
||||||
def TxHashFull(self) -> bytes:
|
def TxHashFull(self) -> bytes:
|
||||||
raise ValueError('todo')
|
raise ValueError("todo")
|
||||||
|
|
||||||
|
|
||||||
def findOutput(tx, script_pk: bytes):
|
def findOutput(tx, script_pk: bytes):
|
||||||
|
|||||||
@@ -9,34 +9,34 @@ import traceback
|
|||||||
from basicswap.rpc import Jsonrpc
|
from basicswap.rpc import Jsonrpc
|
||||||
|
|
||||||
|
|
||||||
def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
|
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
|
||||||
try:
|
try:
|
||||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
x = Jsonrpc(url)
|
x = Jsonrpc(url)
|
||||||
x.__handler = None
|
x.__handler = None
|
||||||
v = x.json_request(method, params)
|
v = x.json_request(method, params)
|
||||||
x.close()
|
x.close()
|
||||||
r = json.loads(v.decode('utf-8'))
|
r = json.loads(v.decode("utf-8"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
|
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||||
|
|
||||||
if 'error' in r and r['error'] is not None:
|
if "error" in r and r["error"] is not None:
|
||||||
raise ValueError('RPC error ' + str(r['error']))
|
raise ValueError("RPC error " + str(r["error"]))
|
||||||
|
|
||||||
return r['result']
|
return r["result"]
|
||||||
|
|
||||||
|
|
||||||
def openrpc(rpc_port, auth, host='127.0.0.1'):
|
def openrpc(rpc_port, auth, host="127.0.0.1"):
|
||||||
try:
|
try:
|
||||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
return Jsonrpc(url)
|
return Jsonrpc(url)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise ValueError('RPC error ' + str(ex))
|
raise ValueError("RPC error " + str(ex))
|
||||||
|
|
||||||
|
|
||||||
def make_rpc_func(port, auth, host='127.0.0.1'):
|
def make_rpc_func(port, auth, host="127.0.0.1"):
|
||||||
port = port
|
port = port
|
||||||
auth = auth
|
auth = auth
|
||||||
host = host
|
host = host
|
||||||
@@ -44,4 +44,5 @@ def make_rpc_func(port, auth, host='127.0.0.1'):
|
|||||||
def rpc_func(method, params=None):
|
def rpc_func(method, params=None):
|
||||||
nonlocal port, auth, host
|
nonlocal port, auth, host
|
||||||
return callrpc(port, auth, method, params, host)
|
return callrpc(port, auth, method, params, host)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
OP_0 = 0x00
|
OP_0 = 0x00
|
||||||
OP_DATA_1 = 0x01
|
OP_DATA_1 = 0x01
|
||||||
OP_1NEGATE = 0x4f
|
OP_1NEGATE = 0x4F
|
||||||
OP_1 = 0x51
|
OP_1 = 0x51
|
||||||
OP_IF = 0x63
|
OP_IF = 0x63
|
||||||
OP_ELSE = 0x67
|
OP_ELSE = 0x67
|
||||||
@@ -16,13 +16,13 @@ OP_DROP = 0x75
|
|||||||
OP_DUP = 0x76
|
OP_DUP = 0x76
|
||||||
OP_EQUAL = 0x87
|
OP_EQUAL = 0x87
|
||||||
OP_EQUALVERIFY = 0x88
|
OP_EQUALVERIFY = 0x88
|
||||||
OP_PUSHDATA1 = 0x4c
|
OP_PUSHDATA1 = 0x4C
|
||||||
OP_PUSHDATA2 = 0x4d
|
OP_PUSHDATA2 = 0x4D
|
||||||
OP_PUSHDATA4 = 0x4e
|
OP_PUSHDATA4 = 0x4E
|
||||||
OP_HASH160 = 0xa9
|
OP_HASH160 = 0xA9
|
||||||
OP_CHECKSIG = 0xac
|
OP_CHECKSIG = 0xAC
|
||||||
OP_CHECKMULTISIG = 0xae
|
OP_CHECKMULTISIG = 0xAE
|
||||||
OP_CHECKSEQUENCEVERIFY = 0xb2
|
OP_CHECKSEQUENCEVERIFY = 0xB2
|
||||||
|
|
||||||
|
|
||||||
def push_script_data(data_array: bytearray, data: bytes) -> None:
|
def push_script_data(data_array: bytearray, data: bytes) -> None:
|
||||||
@@ -39,12 +39,12 @@ def push_script_data(data_array: bytearray, data: bytes) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if len_data < OP_PUSHDATA1:
|
if len_data < OP_PUSHDATA1:
|
||||||
data_array += len_data.to_bytes(1, 'little')
|
data_array += len_data.to_bytes(1, "little")
|
||||||
elif len_data <= 0xff:
|
elif len_data <= 0xFF:
|
||||||
data_array += bytes((OP_PUSHDATA1, len_data))
|
data_array += bytes((OP_PUSHDATA1, len_data))
|
||||||
elif len_data <= 0xffff:
|
elif len_data <= 0xFFFF:
|
||||||
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
|
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, "little")
|
||||||
else:
|
else:
|
||||||
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
|
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, "little")
|
||||||
|
|
||||||
data_array += data
|
data_array += data
|
||||||
|
|||||||
@@ -10,46 +10,48 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
def createDCRWallet(args, hex_seed, logging, delay_event):
|
def createDCRWallet(args, hex_seed, logging, delay_event):
|
||||||
logging.info('Creating DCR wallet')
|
logging.info("Creating DCR wallet")
|
||||||
|
|
||||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == "nt":
|
||||||
str_args = ' '.join(args)
|
str_args = " ".join(args)
|
||||||
p = subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
p = subprocess.Popen(
|
||||||
|
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||||
|
|
||||||
def readOutput():
|
def readOutput():
|
||||||
buf = os.read(pipe_r, 1024).decode('utf-8')
|
buf = os.read(pipe_r, 1024).decode("utf-8")
|
||||||
response = None
|
response = None
|
||||||
if 'Opened wallet' in buf:
|
if "Opened wallet" in buf:
|
||||||
pass
|
pass
|
||||||
elif 'Use the existing configured private passphrase' in buf:
|
elif "Use the existing configured private passphrase" in buf:
|
||||||
response = b'y\n'
|
response = b"y\n"
|
||||||
elif 'Do you want to add an additional layer of encryption' in buf:
|
elif "Do you want to add an additional layer of encryption" in buf:
|
||||||
response = b'n\n'
|
response = b"n\n"
|
||||||
elif 'Do you have an existing wallet seed' in buf:
|
elif "Do you have an existing wallet seed" in buf:
|
||||||
response = b'y\n'
|
response = b"y\n"
|
||||||
elif 'Enter existing wallet seed' in buf:
|
elif "Enter existing wallet seed" in buf:
|
||||||
response = (hex_seed + '\n').encode('utf-8')
|
response = (hex_seed + "\n").encode("utf-8")
|
||||||
elif 'Seed input successful' in buf:
|
elif "Seed input successful" in buf:
|
||||||
pass
|
pass
|
||||||
elif 'Upgrading database from version' in buf:
|
elif "Upgrading database from version" in buf:
|
||||||
pass
|
pass
|
||||||
elif 'Ticket commitments db upgrade done' in buf:
|
elif "Ticket commitments db upgrade done" in buf:
|
||||||
pass
|
pass
|
||||||
elif 'The wallet has been created successfully' in buf:
|
elif "The wallet has been created successfully" in buf:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unexpected output: {buf}')
|
raise ValueError(f"Unexpected output: {buf}")
|
||||||
if response is not None:
|
if response is not None:
|
||||||
p.stdin.write(response)
|
p.stdin.write(response)
|
||||||
p.stdin.flush()
|
p.stdin.flush()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
if os.name == 'nt':
|
if os.name == "nt":
|
||||||
readOutput()
|
readOutput()
|
||||||
delay_event.wait(0.1)
|
delay_event.wait(0.1)
|
||||||
continue
|
continue
|
||||||
@@ -57,7 +59,7 @@ def createDCRWallet(args, hex_seed, logging, delay_event):
|
|||||||
readOutput()
|
readOutput()
|
||||||
delay_event.wait(0.1)
|
delay_event.wait(0.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f'dcrwallet --create failed: {e}')
|
logging.error(f"dcrwallet --create failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
if p.poll() is None:
|
if p.poll() is None:
|
||||||
p.terminate()
|
p.terminate()
|
||||||
|
|||||||
62
basicswap/interface/doge.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024 The BasicSwap developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from .btc import BTCInterface
|
||||||
|
from basicswap.chainparams import Coins
|
||||||
|
from basicswap.util.crypto import hash160
|
||||||
|
|
||||||
|
from basicswap.contrib.test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_DUP,
|
||||||
|
OP_CHECKSIG,
|
||||||
|
OP_HASH160,
|
||||||
|
OP_EQUAL,
|
||||||
|
OP_EQUALVERIFY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DOGEInterface(BTCInterface):
|
||||||
|
@staticmethod
|
||||||
|
def coin_type():
|
||||||
|
return Coins.DOGE
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def est_lock_tx_vsize() -> int:
|
||||||
|
return 192
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
|
return 192
|
||||||
|
|
||||||
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
|
super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
|
|
||||||
|
def getScriptDest(self, script: bytearray) -> bytearray:
|
||||||
|
# P2SH
|
||||||
|
|
||||||
|
script_hash = hash160(script)
|
||||||
|
assert len(script_hash) == 20
|
||||||
|
|
||||||
|
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||||
|
|
||||||
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
|
# Return P2PKH
|
||||||
|
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||||
|
|
||||||
|
def encodeScriptDest(self, script_dest: bytes) -> str:
|
||||||
|
# Extract hash from script
|
||||||
|
script_hash = script_dest[2:-1]
|
||||||
|
return self.sh_to_address(script_hash)
|
||||||
|
|
||||||
|
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||||
|
add_bytes = 107
|
||||||
|
size = len(tx.serialize_with_witness()) + add_bytes
|
||||||
|
pay_fee = round(fee_rate * size / 1000)
|
||||||
|
self._log.info(
|
||||||
|
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||||
|
)
|
||||||
|
return pay_fee
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022-2023 tecnovert
|
# Copyright (c) 2022-2023 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -40,10 +41,15 @@ class FIROInterface(BTCInterface):
|
|||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
|
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
# No multiwallet support
|
# No multiwallet support
|
||||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
self.rpc_wallet = make_rpc_func(
|
||||||
|
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||||
|
)
|
||||||
|
|
||||||
def getExchangeName(self, exchange_name):
|
if "wallet_name" in coin_settings:
|
||||||
return 'zcoin'
|
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||||
|
|
||||||
|
def getExchangeName(self, exchange_name: str) -> str:
|
||||||
|
return "zcoin"
|
||||||
|
|
||||||
def initialiseWallet(self, key):
|
def initialiseWallet(self, key):
|
||||||
# load with -hdseed= parameter
|
# load with -hdseed= parameter
|
||||||
@@ -52,8 +58,8 @@ class FIROInterface(BTCInterface):
|
|||||||
def checkWallets(self) -> int:
|
def checkWallets(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
def getNewAddress(self, use_segwit, label="swap_receive"):
|
||||||
return self.rpc('getnewaddress', [label])
|
return self.rpc("getnewaddress", [label])
|
||||||
# addr_plain = self.rpc('getnewaddress', [label])
|
# addr_plain = self.rpc('getnewaddress', [label])
|
||||||
# return self.rpc('addwitnessaddress', [addr_plain])
|
# return self.rpc('addwitnessaddress', [addr_plain])
|
||||||
|
|
||||||
@@ -61,20 +67,20 @@ class FIROInterface(BTCInterface):
|
|||||||
return decodeAddress(address)[1:]
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
def encodeSegwitAddress(self, script):
|
def encodeSegwitAddress(self, script):
|
||||||
raise ValueError('TODO')
|
raise ValueError("TODO")
|
||||||
|
|
||||||
def decodeSegwitAddress(self, addr):
|
def decodeSegwitAddress(self, addr):
|
||||||
raise ValueError('TODO')
|
raise ValueError("TODO")
|
||||||
|
|
||||||
def isWatchOnlyAddress(self, address):
|
def isWatchOnlyAddress(self, address):
|
||||||
addr_info = self.rpc('validateaddress', [address])
|
addr_info = self.rpc("validateaddress", [address])
|
||||||
return addr_info['iswatchonly']
|
return addr_info["iswatchonly"]
|
||||||
|
|
||||||
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
||||||
addr_info = self.rpc('validateaddress', [address])
|
addr_info = self.rpc("validateaddress", [address])
|
||||||
if not or_watch_only:
|
if not or_watch_only:
|
||||||
return addr_info['ismine']
|
return addr_info["ismine"]
|
||||||
return addr_info['ismine'] or addr_info['iswatchonly']
|
return addr_info["ismine"] or addr_info["iswatchonly"]
|
||||||
|
|
||||||
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||||
lock_tx_dest = self.getScriptDest(lock_script)
|
lock_tx_dest = self.getScriptDest(lock_script)
|
||||||
@@ -82,58 +88,85 @@ class FIROInterface(BTCInterface):
|
|||||||
|
|
||||||
if not self.isAddressMine(address, or_watch_only=True):
|
if not self.isAddressMine(address, or_watch_only=True):
|
||||||
# Expects P2WSH nested in BIP16_P2SH
|
# Expects P2WSH nested in BIP16_P2SH
|
||||||
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
|
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True])
|
||||||
addr_info = self.rpc('validateaddress', [address])
|
|
||||||
|
|
||||||
return address
|
return address
|
||||||
|
|
||||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
def getLockTxHeight(
|
||||||
|
self,
|
||||||
|
txid,
|
||||||
|
dest_address,
|
||||||
|
bid_amount,
|
||||||
|
rescan_from,
|
||||||
|
find_index: bool = False,
|
||||||
|
vout: int = -1,
|
||||||
|
):
|
||||||
# Add watchonly address and rescan if required
|
# Add watchonly address and rescan if required
|
||||||
|
|
||||||
if not self.isAddressMine(dest_address, or_watch_only=True):
|
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||||
self.importWatchOnlyAddress(dest_address, 'bid')
|
self.importWatchOnlyAddress(dest_address, "bid")
|
||||||
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
self._log.info(
|
||||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
|
||||||
|
)
|
||||||
|
self._log.info(
|
||||||
|
"Rescanning {} chain from height: {}".format(
|
||||||
|
self.coin_name(), rescan_from
|
||||||
|
)
|
||||||
|
)
|
||||||
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||||
|
|
||||||
return_txid = True if txid is None else False
|
return_txid = True if txid is None else False
|
||||||
if txid is None:
|
if txid is None:
|
||||||
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
txns = self.rpc(
|
||||||
|
"listunspent",
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
9999999,
|
||||||
|
[
|
||||||
|
dest_address,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
for tx in txns:
|
for tx in txns:
|
||||||
if self.make_int(tx['amount']) == bid_amount:
|
if self.make_int(tx["amount"]) == bid_amount:
|
||||||
txid = bytes.fromhex(tx['txid'])
|
txid = bytes.fromhex(tx["txid"])
|
||||||
break
|
break
|
||||||
|
|
||||||
if txid is None:
|
if txid is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tx = self.rpc('gettransaction', [txid.hex()])
|
tx = self.rpc("gettransaction", [txid.hex()])
|
||||||
|
|
||||||
block_height = 0
|
block_height = 0
|
||||||
if 'blockhash' in tx:
|
if "blockhash" in tx:
|
||||||
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
block_header = self.rpc("getblockheader", [tx["blockhash"]])
|
||||||
block_height = block_header['height']
|
block_height = block_header["height"]
|
||||||
|
|
||||||
rv = {
|
rv = {
|
||||||
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
"depth": 0 if "confirmations" not in tx else tx["confirmations"],
|
||||||
'height': block_height}
|
"height": block_height,
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
self._log.debug(
|
||||||
|
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if find_index:
|
if find_index:
|
||||||
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
|
||||||
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||||
|
|
||||||
if return_txid:
|
if return_txid:
|
||||||
rv['txid'] = txid.hex()
|
rv["txid"] = txid.hex()
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
def createSCLockTx(
|
||||||
|
self, value: int, script: bytearray, vkbv: bytes = None
|
||||||
|
) -> bytes:
|
||||||
tx = CTransaction()
|
tx = CTransaction()
|
||||||
tx.nVersion = self.txVersion()
|
tx.nVersion = self.txVersion()
|
||||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||||
@@ -144,24 +177,36 @@ class FIROInterface(BTCInterface):
|
|||||||
return self.fundTx(tx_bytes, feerate)
|
return self.fundTx(tx_bytes, feerate)
|
||||||
|
|
||||||
def signTxWithWallet(self, tx):
|
def signTxWithWallet(self, tx):
|
||||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||||
return bytes.fromhex(rv['hex'])
|
return bytes.fromhex(rv["hex"])
|
||||||
|
|
||||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
def createRawFundedTransaction(
|
||||||
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
self,
|
||||||
|
addr_to: str,
|
||||||
|
amount: int,
|
||||||
|
sub_fee: bool = False,
|
||||||
|
lock_unspents: bool = True,
|
||||||
|
) -> str:
|
||||||
|
txn = self.rpc(
|
||||||
|
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
|
||||||
|
)
|
||||||
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
||||||
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
self._log.debug(
|
||||||
|
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
|
||||||
|
)
|
||||||
options = {
|
options = {
|
||||||
'lockUnspents': lock_unspents,
|
"lockUnspents": lock_unspents,
|
||||||
'feeRate': fee_rate,
|
"feeRate": fee_rate,
|
||||||
}
|
}
|
||||||
if sub_fee:
|
if sub_fee:
|
||||||
options['subtractFeeFromOutputs'] = [0,]
|
options["subtractFeeFromOutputs"] = [
|
||||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
0,
|
||||||
|
]
|
||||||
|
return self.rpc("fundrawtransaction", [txn, options])["hex"]
|
||||||
|
|
||||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
return self.rpc("signrawtransaction", [txn_funded])["hex"]
|
||||||
|
|
||||||
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||||
# Return P2PKH
|
# Return P2PKH
|
||||||
@@ -188,60 +233,75 @@ class FIROInterface(BTCInterface):
|
|||||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||||
|
|
||||||
def withdrawCoin(self, value, addr_to, subfee):
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
params = [addr_to, value, '', '', subfee]
|
params = [addr_to, value, "", "", subfee]
|
||||||
return self.rpc('sendtoaddress', params)
|
return self.rpc("sendtoaddress", params)
|
||||||
|
|
||||||
def getWalletSeedID(self):
|
def getWalletSeedID(self):
|
||||||
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||||
|
|
||||||
def getSpendableBalance(self) -> int:
|
def getSpendableBalance(self) -> int:
|
||||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
return self.make_int(self.rpc("getwalletinfo")["balance"])
|
||||||
|
|
||||||
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||||
add_bytes = 107
|
add_bytes = 107
|
||||||
size = len(tx.serialize_with_witness()) + add_bytes
|
size = len(tx.serialize_with_witness()) + add_bytes
|
||||||
pay_fee = round(fee_rate * size / 1000)
|
pay_fee = round(fee_rate * size / 1000)
|
||||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
self._log.info(
|
||||||
|
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||||
|
)
|
||||||
return pay_fee
|
return pay_fee
|
||||||
|
|
||||||
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||||
key_wif = self.encodeKey(key)
|
key_wif = self.encodeKey(key)
|
||||||
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
rv = self.rpc(
|
||||||
return bytes.fromhex(rv['hex'])
|
"signrawtransaction",
|
||||||
|
[
|
||||||
|
tx.hex(),
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
key_wif,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return bytes.fromhex(rv["hex"])
|
||||||
|
|
||||||
def findTxnByHash(self, txid_hex: str):
|
def findTxnByHash(self, txid_hex: str):
|
||||||
# Only works for wallet txns
|
# Only works for wallet txns
|
||||||
try:
|
try:
|
||||||
rv = self.rpc('gettransaction', [txid_hex])
|
rv = self.rpc("gettransaction", [txid_hex])
|
||||||
except Exception as ex:
|
except Exception as e: # noqa: F841
|
||||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
self._log.debug(
|
||||||
|
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||||
# TODO: Lock unspent and use same output/s to fund bid
|
# TODO: Lock unspent and use same output/s to fund bid
|
||||||
|
|
||||||
unspents_by_addr = dict()
|
unspents_by_addr = dict()
|
||||||
unspents = self.rpc('listunspent')
|
unspents = self.rpc("listunspent")
|
||||||
for u in unspents:
|
for u in unspents:
|
||||||
if u['spendable'] is not True:
|
if u["spendable"] is not True:
|
||||||
continue
|
continue
|
||||||
if u['address'] not in unspents_by_addr:
|
if u["address"] not in unspents_by_addr:
|
||||||
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
|
unspents_by_addr[u["address"]] = {"total": 0, "utxos": []}
|
||||||
utxo_amount: int = self.make_int(u['amount'], r=1)
|
utxo_amount: int = self.make_int(u["amount"], r=1)
|
||||||
unspents_by_addr[u['address']]['total'] += utxo_amount
|
unspents_by_addr[u["address"]]["total"] += utxo_amount
|
||||||
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
|
unspents_by_addr[u["address"]]["utxos"].append(
|
||||||
|
(utxo_amount, u["txid"], u["vout"])
|
||||||
|
)
|
||||||
|
|
||||||
max_utxos: int = 4
|
max_utxos: int = 4
|
||||||
|
|
||||||
viable_addrs = []
|
viable_addrs = []
|
||||||
for addr, data in unspents_by_addr.items():
|
for addr, data in unspents_by_addr.items():
|
||||||
if data['total'] >= amount_for:
|
if data["total"] >= amount_for:
|
||||||
# Sort from largest to smallest amount
|
# Sort from largest to smallest amount
|
||||||
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
|
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
|
||||||
|
|
||||||
# Max outputs required to reach amount_for
|
# Max outputs required to reach amount_for
|
||||||
utxos_req: int = 0
|
utxos_req: int = 0
|
||||||
@@ -256,13 +316,17 @@ class FIROInterface(BTCInterface):
|
|||||||
viable_addrs.append(addr)
|
viable_addrs.append(addr)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
|
ensure(
|
||||||
|
len(viable_addrs) > 0, "Could not find address with enough funds for proof"
|
||||||
|
)
|
||||||
|
|
||||||
sign_for_addr: str = random.choice(viable_addrs)
|
sign_for_addr: str = random.choice(viable_addrs)
|
||||||
self._log.debug('sign_for_addr %s', sign_for_addr)
|
self._log.debug("sign_for_addr %s", sign_for_addr)
|
||||||
|
|
||||||
prove_utxos = []
|
prove_utxos = []
|
||||||
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
|
sorted_utxos = sorted(
|
||||||
|
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
|
||||||
|
)
|
||||||
|
|
||||||
hasher = hashlib.sha256()
|
hasher = hashlib.sha256()
|
||||||
|
|
||||||
@@ -272,18 +336,29 @@ class FIROInterface(BTCInterface):
|
|||||||
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
||||||
prove_utxos.append(outpoint)
|
prove_utxos.append(outpoint)
|
||||||
hasher.update(outpoint[0])
|
hasher.update(outpoint[0])
|
||||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||||
if sum_value >= amount_for:
|
if sum_value >= amount_for:
|
||||||
break
|
break
|
||||||
utxos_hash = hasher.digest()
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
if (
|
||||||
|
self.using_segwit()
|
||||||
|
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||||
# 'Address does not refer to key' for non p2pkh
|
# 'Address does not refer to key' for non p2pkh
|
||||||
pkh = self.decodeAddress(sign_for_addr)
|
pkh = self.decodeAddress(sign_for_addr)
|
||||||
sign_for_addr = self.pkh_to_address(pkh)
|
sign_for_addr = self.pkh_to_address(pkh)
|
||||||
self._log.debug('sign_for_addr converted %s', sign_for_addr)
|
self._log.debug("sign_for_addr converted %s", sign_for_addr)
|
||||||
|
|
||||||
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
|
signature = self.rpc(
|
||||||
|
"signmessage",
|
||||||
|
[
|
||||||
|
sign_for_addr,
|
||||||
|
sign_for_addr
|
||||||
|
+ "_swap_proof_"
|
||||||
|
+ utxos_hash.hex()
|
||||||
|
+ extra_commit_bytes.hex(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
return (sign_for_addr, signature, prove_utxos)
|
return (sign_for_addr, signature, prove_utxos)
|
||||||
|
|
||||||
@@ -292,19 +367,23 @@ class FIROInterface(BTCInterface):
|
|||||||
sum_value: int = 0
|
sum_value: int = 0
|
||||||
for outpoint in utxos:
|
for outpoint in utxos:
|
||||||
hasher.update(outpoint[0])
|
hasher.update(outpoint[0])
|
||||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||||
utxos_hash = hasher.digest()
|
utxos_hash = hasher.digest()
|
||||||
|
|
||||||
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
|
passed = self.verifyMessage(
|
||||||
ensure(passed is True, 'Proof of funds signature invalid')
|
address,
|
||||||
|
address + "_swap_proof_" + utxos_hash.hex() + extra_commit_bytes.hex(),
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
ensure(passed is True, "Proof of funds signature invalid")
|
||||||
|
|
||||||
if self.using_segwit():
|
if self.using_segwit():
|
||||||
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
|
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
|
||||||
|
|
||||||
sum_value: int = 0
|
sum_value: int = 0
|
||||||
for outpoint in utxos:
|
for outpoint in utxos:
|
||||||
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
|
txout = self.rpc("gettxout", [outpoint[0].hex(), outpoint[1]])
|
||||||
sum_value += self.make_int(txout['value'])
|
sum_value += self.make_int(txout["value"])
|
||||||
|
|
||||||
return sum_value
|
return sum_value
|
||||||
|
|
||||||
@@ -314,15 +393,15 @@ class FIROInterface(BTCInterface):
|
|||||||
chain_blocks: int = self.getChainHeight()
|
chain_blocks: int = self.getChainHeight()
|
||||||
|
|
||||||
current_height: int = chain_blocks
|
current_height: int = chain_blocks
|
||||||
block_hash = self.rpc('getblockhash', [current_height])
|
block_hash = self.rpc("getblockhash", [current_height])
|
||||||
|
|
||||||
script_hash: bytes = self.decodeAddress(addr_find)
|
script_hash: bytes = self.decodeAddress(addr_find)
|
||||||
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
||||||
|
|
||||||
while current_height > height_start:
|
while current_height > height_start:
|
||||||
block_hash = self.rpc('getblockhash', [current_height])
|
block_hash = self.rpc("getblockhash", [current_height])
|
||||||
|
|
||||||
block = self.rpc('getblock', [block_hash, False])
|
block = self.rpc("getblock", [block_hash, False])
|
||||||
decoded_block = CBlock()
|
decoded_block = CBlock()
|
||||||
decoded_block = FromHex(decoded_block, block)
|
decoded_block = FromHex(decoded_block, block)
|
||||||
for tx in decoded_block.vtx:
|
for tx in decoded_block.vtx:
|
||||||
@@ -330,38 +409,46 @@ class FIROInterface(BTCInterface):
|
|||||||
if txo.scriptPubKey == find_scriptPubKey:
|
if txo.scriptPubKey == find_scriptPubKey:
|
||||||
tx.rehash()
|
tx.rehash()
|
||||||
txid = i2b(tx.sha256)
|
txid = i2b(tx.sha256)
|
||||||
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
|
self._log.info(
|
||||||
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
|
"Found output to addr: {} in tx {} in block {}".format(
|
||||||
self.rpc('invalidateblock', [block_hash])
|
addr_find, txid.hex(), block_hash
|
||||||
self.rpc('reconsiderblock', [block_hash])
|
)
|
||||||
|
)
|
||||||
|
self._log.info(
|
||||||
|
"rescanblockchain hack invalidateblock {}".format(
|
||||||
|
block_hash
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.rpc("invalidateblock", [block_hash])
|
||||||
|
self.rpc("reconsiderblock", [block_hash])
|
||||||
return
|
return
|
||||||
current_height -= 1
|
current_height -= 1
|
||||||
|
|
||||||
def getBlockWithTxns(self, block_hash: str):
|
def getBlockWithTxns(self, block_hash: str):
|
||||||
# TODO: Bypass decoderawtransaction and getblockheader
|
# TODO: Bypass decoderawtransaction and getblockheader
|
||||||
block = self.rpc('getblock', [block_hash, False])
|
block = self.rpc("getblock", [block_hash, False])
|
||||||
block_header = self.rpc('getblockheader', [block_hash])
|
block_header = self.rpc("getblockheader", [block_hash])
|
||||||
decoded_block = CBlock()
|
decoded_block = CBlock()
|
||||||
decoded_block = FromHex(decoded_block, block)
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
|
||||||
tx_rv = []
|
tx_rv = []
|
||||||
for tx in decoded_block.vtx:
|
for tx in decoded_block.vtx:
|
||||||
tx_hex = tx.serialize_with_witness().hex()
|
tx_hex = tx.serialize_with_witness().hex()
|
||||||
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
tx_dec = self.rpc("decoderawtransaction", [tx_hex])
|
||||||
if 'hex' not in tx_dec:
|
if "hex" not in tx_dec:
|
||||||
tx_dec['hex'] = tx_hex
|
tx_dec["hex"] = tx_hex
|
||||||
|
|
||||||
tx_rv.append(tx_dec)
|
tx_rv.append(tx_dec)
|
||||||
|
|
||||||
block_rv = {
|
block_rv = {
|
||||||
'hash': block_hash,
|
"hash": block_hash,
|
||||||
'previousblockhash': block_header['previousblockhash'],
|
"previousblockhash": block_header["previousblockhash"],
|
||||||
'tx': tx_rv,
|
"tx": tx_rv,
|
||||||
'confirmations': block_header['confirmations'],
|
"confirmations": block_header["confirmations"],
|
||||||
'height': block_header['height'],
|
"height": block_header["height"],
|
||||||
'time': block_header['time'],
|
"time": block_header["time"],
|
||||||
'version': block_header['version'],
|
"version": block_header["version"],
|
||||||
'merkleroot': block_header['merkleroot'],
|
"merkleroot": block_header["merkleroot"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return block_rv
|
return block_rv
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2023 tecnovert
|
# Copyright (c) 2020-2023 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -17,75 +18,86 @@ class LTCInterface(BTCInterface):
|
|||||||
|
|
||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
|
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
self._rpc_wallet_mweb = 'mweb'
|
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
|
||||||
self.rpc_wallet_mweb = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet_mweb)
|
self.rpc_wallet_mweb = make_rpc_func(
|
||||||
|
self._rpcport,
|
||||||
|
self._rpcauth,
|
||||||
|
host=self._rpc_host,
|
||||||
|
wallet=self._rpc_wallet_mweb,
|
||||||
|
)
|
||||||
|
|
||||||
def getNewMwebAddress(self, use_segwit=False, label='swap_receive') -> str:
|
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||||
return self.rpc_wallet_mweb('getnewaddress', [label, 'mweb'])
|
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
|
||||||
|
|
||||||
def getNewStealthAddress(self, label=''):
|
def getNewStealthAddress(self, label=""):
|
||||||
return self.getNewMwebAddress(False, label)
|
return self.getNewMwebAddress(False, label)
|
||||||
|
|
||||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||||
params = [addr_to, value, '', '', subfee, True, self._conf_target]
|
params = [addr_to, value, "", "", subfee, True, self._conf_target]
|
||||||
if type_from == 'mweb':
|
if type_from == "mweb":
|
||||||
return self.rpc_wallet_mweb('sendtoaddress', params)
|
return self.rpc_wallet_mweb("sendtoaddress", params)
|
||||||
return self.rpc_wallet('sendtoaddress', params)
|
return self.rpc_wallet("sendtoaddress", params)
|
||||||
|
|
||||||
def createUTXO(self, value_sats: int):
|
def createUTXO(self, value_sats: int):
|
||||||
# Create a new address and send value_sats to it
|
# Create a new address and send value_sats to it
|
||||||
|
|
||||||
spendable_balance = self.getSpendableBalance()
|
spendable_balance = self.getSpendableBalance()
|
||||||
if spendable_balance < value_sats:
|
if spendable_balance < value_sats:
|
||||||
raise ValueError('Balance too low')
|
raise ValueError("Balance too low")
|
||||||
|
|
||||||
address = self.getNewAddress(self._use_segwit, 'create_utxo')
|
address = self.getNewAddress(self._use_segwit, "create_utxo")
|
||||||
return self.withdrawCoin(self.format_amount(value_sats), 'plain', address, False), address
|
return (
|
||||||
|
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
|
||||||
def getWalletInfo(self):
|
def getWalletInfo(self):
|
||||||
rv = super(LTCInterface, self).getWalletInfo()
|
rv = super(LTCInterface, self).getWalletInfo()
|
||||||
|
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
||||||
mweb_info = self.rpc_wallet_mweb('getwalletinfo')
|
rv["mweb_balance"] = mweb_info["balance"]
|
||||||
rv['mweb_balance'] = mweb_info['balance']
|
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
||||||
rv['mweb_unconfirmed'] = mweb_info['unconfirmed_balance']
|
rv["mweb_immature"] = mweb_info["immature_balance"]
|
||||||
rv['mweb_immature'] = mweb_info['immature_balance']
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def getUnspentsByAddr(self):
|
def getUnspentsByAddr(self):
|
||||||
unspent_addr = dict()
|
unspent_addr = dict()
|
||||||
unspent = self.rpc_wallet('listunspent')
|
unspent = self.rpc_wallet("listunspent")
|
||||||
for u in unspent:
|
for u in unspent:
|
||||||
if u.get('spendable', False) is False:
|
if u.get("spendable", False) is False:
|
||||||
continue
|
continue
|
||||||
if u.get('solvable', False) is False: # Filter out mweb outputs
|
if u.get("solvable", False) is False: # Filter out mweb outputs
|
||||||
continue
|
continue
|
||||||
if 'address' not in u:
|
if "address" not in u:
|
||||||
continue
|
continue
|
||||||
if 'desc' in u:
|
if "desc" in u:
|
||||||
desc = u['desc']
|
desc = u["desc"]
|
||||||
if self.using_segwit:
|
if self.using_segwit:
|
||||||
if self.use_p2shp2wsh():
|
if self.use_p2shp2wsh():
|
||||||
if not desc.startswith('sh(wpkh'):
|
if not desc.startswith("sh(wpkh"):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if not desc.startswith('wpkh'):
|
if not desc.startswith("wpkh"):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if not desc.startswith('pkh'):
|
if not desc.startswith("pkh"):
|
||||||
continue
|
continue
|
||||||
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
|
unspent_addr[u["address"]] = unspent_addr.get(
|
||||||
|
u["address"], 0
|
||||||
|
) + self.make_int(u["amount"], r=1)
|
||||||
return unspent_addr
|
return unspent_addr
|
||||||
|
|
||||||
|
|
||||||
class LTCInterfaceMWEB(LTCInterface):
|
class LTCInterfaceMWEB(LTCInterface):
|
||||||
@staticmethod
|
|
||||||
def coin_type():
|
def interface_type(self) -> int:
|
||||||
return Coins.LTC_MWEB
|
return Coins.LTC_MWEB
|
||||||
|
|
||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
|
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
|
||||||
self._rpc_wallet = 'mweb'
|
self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb")
|
||||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
|
self.rpc_wallet = make_rpc_func(
|
||||||
|
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
|
||||||
|
)
|
||||||
|
|
||||||
def chainparams(self):
|
def chainparams(self):
|
||||||
return chainparams[Coins.LTC]
|
return chainparams[Coins.LTC]
|
||||||
@@ -95,56 +107,54 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
|
|
||||||
def coin_name(self) -> str:
|
def coin_name(self) -> str:
|
||||||
coin_chainparams = chainparams[Coins.LTC]
|
coin_chainparams = chainparams[Coins.LTC]
|
||||||
if coin_chainparams.get('use_ticker_as_name', False):
|
return coin_chainparams["name"].capitalize() + " MWEB"
|
||||||
return coin_chainparams['ticker'] + ' MWEB'
|
|
||||||
return coin_chainparams['name'].capitalize() + ' MWEB'
|
|
||||||
|
|
||||||
def ticker(self) -> str:
|
def ticker(self) -> str:
|
||||||
ticker = chainparams[Coins.LTC]['ticker']
|
ticker = chainparams[Coins.LTC]["ticker"]
|
||||||
if self._network == 'testnet':
|
if self._network == "testnet":
|
||||||
ticker = 't' + ticker
|
ticker = "t" + ticker
|
||||||
elif self._network == 'regtest':
|
elif self._network == "regtest":
|
||||||
ticker = 'rt' + ticker
|
ticker = "rt" + ticker
|
||||||
return ticker + '_MWEB'
|
return ticker + "_MWEB"
|
||||||
|
|
||||||
def getNewAddress(self, use_segwit=False, label='swap_receive') -> str:
|
def getNewAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||||
return self.getNewMwebAddress()
|
return self.getNewMwebAddress()
|
||||||
|
|
||||||
def has_mweb_wallet(self) -> bool:
|
def has_mweb_wallet(self) -> bool:
|
||||||
return 'mweb' in self.rpc('listwallets')
|
return "mweb" in self.rpc("listwallets")
|
||||||
|
|
||||||
def init_wallet(self, password=None):
|
def init_wallet(self, password=None):
|
||||||
# If system is encrypted mweb wallet will be created at first unlock
|
# If system is encrypted mweb wallet will be created at first unlock
|
||||||
|
|
||||||
self._log.info('init_wallet - {}'.format(self.ticker()))
|
self._log.info("init_wallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
self._log.info('Creating mweb wallet for {}.'.format(self.coin_name()))
|
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
|
||||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||||
self.rpc('createwallet', ['mweb', False, True, password, False, False, True])
|
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||||
|
|
||||||
if self.getWalletSeedID() == 'Not found':
|
if self.getWalletSeedID() == "Not found":
|
||||||
self._sc.initialiseWallet(self.coin_type())
|
self._sc.initialiseWallet(self.interface_type())
|
||||||
|
|
||||||
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
|
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
|
||||||
self.rpc('unloadwallet', ['mweb'])
|
self.rpc("unloadwallet", ["mweb"])
|
||||||
self.rpc('loadwallet', ['mweb'])
|
self.rpc("loadwallet", ["mweb"])
|
||||||
if password is not None:
|
if password is not None:
|
||||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||||
self.rpc_wallet('keypoolrefill')
|
self.rpc_wallet("keypoolrefill")
|
||||||
|
|
||||||
def unlockWallet(self, password: str):
|
def unlockWallet(self, password: str):
|
||||||
if password == '':
|
if password == "":
|
||||||
return
|
return
|
||||||
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
if not self.has_mweb_wallet():
|
if not self.has_mweb_wallet():
|
||||||
self.init_wallet(password)
|
self.init_wallet(password)
|
||||||
else:
|
else:
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||||
|
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|||||||
@@ -14,26 +14,38 @@ class NMCInterface(BTCInterface):
|
|||||||
def coin_type():
|
def coin_type():
|
||||||
return Coins.NMC
|
return Coins.NMC
|
||||||
|
|
||||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
def getLockTxHeight(
|
||||||
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
|
self,
|
||||||
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
|
txid,
|
||||||
self._log.debug('[rm] scantxoutset end')
|
dest_address,
|
||||||
|
bid_amount,
|
||||||
|
rescan_from,
|
||||||
|
find_index: bool = False,
|
||||||
|
vout: int = -1,
|
||||||
|
):
|
||||||
|
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
|
||||||
|
ro = self.rpc(
|
||||||
|
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
|
||||||
|
) # TODO: Use combo(address) where possible
|
||||||
|
self._log.debug("[rm] scantxoutset end")
|
||||||
return_txid = True if txid is None else False
|
return_txid = True if txid is None else False
|
||||||
for o in ro['unspents']:
|
for o in ro["unspents"]:
|
||||||
if txid and o['txid'] != txid.hex():
|
if txid and o["txid"] != txid.hex():
|
||||||
continue
|
continue
|
||||||
# Verify amount
|
# Verify amount
|
||||||
if self.make_int(o['amount']) != int(bid_amount):
|
if self.make_int(o["amount"]) != int(bid_amount):
|
||||||
self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
|
self._log.warning(
|
||||||
|
"Found output to lock tx address of incorrect value: %s, %s",
|
||||||
|
str(o["amount"]),
|
||||||
|
o["txid"],
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rv = {
|
rv = {"depth": 0, "height": o["height"]}
|
||||||
'depth': 0,
|
if o["height"] > 0:
|
||||||
'height': o['height']}
|
rv["depth"] = ro["height"] - o["height"]
|
||||||
if o['height'] > 0:
|
|
||||||
rv['depth'] = ro['height'] - o['height']
|
|
||||||
if find_index:
|
if find_index:
|
||||||
rv['index'] = o['vout']
|
rv["index"] = o["vout"]
|
||||||
if return_txid:
|
if return_txid:
|
||||||
rv['txid'] = o['txid']
|
rv["txid"] = o["txid"]
|
||||||
return rv
|
return rv
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
from .btc import BTCInterface
|
from .btc import BTCInterface
|
||||||
from basicswap.contrib.test_framework.messages import (
|
from basicswap.contrib.test_framework.messages import CTxOut
|
||||||
CTxOut)
|
|
||||||
|
|
||||||
|
|
||||||
class PassthroughBTCInterface(BTCInterface):
|
class PassthroughBTCInterface(BTCInterface):
|
||||||
@@ -15,5 +14,5 @@ class PassthroughBTCInterface(BTCInterface):
|
|||||||
super().__init__(coin_settings, network)
|
super().__init__(coin_settings, network)
|
||||||
self.txoType = CTxOut
|
self.txoType = CTxOut
|
||||||
self._network = network
|
self._network = network
|
||||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
self.blocks_confirmed = coin_settings["blocks_confirmed"]
|
||||||
self.setConfTarget(coin_settings['conf_target'])
|
self.setConfTarget(coin_settings["conf_target"])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022 tecnovert
|
# Copyright (c) 2022 tecnovert
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -11,11 +12,7 @@ from .btc import BTCInterface
|
|||||||
from basicswap.rpc import make_rpc_func
|
from basicswap.rpc import make_rpc_func
|
||||||
from basicswap.chainparams import Coins
|
from basicswap.chainparams import Coins
|
||||||
from basicswap.util.address import decodeAddress
|
from basicswap.util.address import decodeAddress
|
||||||
from .contrib.pivx_test_framework.messages import (
|
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
|
||||||
CBlock,
|
|
||||||
ToHex,
|
|
||||||
FromHex,
|
|
||||||
CTransaction)
|
|
||||||
from basicswap.contrib.test_framework.script import (
|
from basicswap.contrib.test_framework.script import (
|
||||||
CScript,
|
CScript,
|
||||||
OP_DUP,
|
OP_DUP,
|
||||||
@@ -33,65 +30,79 @@ class PIVXInterface(BTCInterface):
|
|||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
|
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
|
||||||
# No multiwallet support
|
# No multiwallet support
|
||||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
self.rpc_wallet = make_rpc_func(
|
||||||
|
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||||
|
)
|
||||||
|
|
||||||
def checkWallets(self) -> int:
|
def checkWallets(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def signTxWithWallet(self, tx):
|
def signTxWithWallet(self, tx):
|
||||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||||
return bytes.fromhex(rv['hex'])
|
return bytes.fromhex(rv["hex"])
|
||||||
|
|
||||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
def createRawFundedTransaction(
|
||||||
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
self,
|
||||||
|
addr_to: str,
|
||||||
|
amount: int,
|
||||||
|
sub_fee: bool = False,
|
||||||
|
lock_unspents: bool = True,
|
||||||
|
) -> str:
|
||||||
|
txn = self.rpc(
|
||||||
|
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
|
||||||
|
)
|
||||||
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
||||||
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
self._log.debug(
|
||||||
|
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
|
||||||
|
)
|
||||||
options = {
|
options = {
|
||||||
'lockUnspents': lock_unspents,
|
"lockUnspents": lock_unspents,
|
||||||
'feeRate': fee_rate,
|
"feeRate": fee_rate,
|
||||||
}
|
}
|
||||||
if sub_fee:
|
if sub_fee:
|
||||||
options['subtractFeeFromOutputs'] = [0,]
|
options["subtractFeeFromOutputs"] = [
|
||||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
0,
|
||||||
|
]
|
||||||
|
return self.rpc("fundrawtransaction", [txn, options])["hex"]
|
||||||
|
|
||||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
return self.rpc("signrawtransaction", [txn_funded])["hex"]
|
||||||
|
|
||||||
def decodeAddress(self, address):
|
def decodeAddress(self, address):
|
||||||
return decodeAddress(address)[1:]
|
return decodeAddress(address)[1:]
|
||||||
|
|
||||||
def getBlockWithTxns(self, block_hash):
|
def getBlockWithTxns(self, block_hash):
|
||||||
# TODO: Bypass decoderawtransaction and getblockheader
|
# TODO: Bypass decoderawtransaction and getblockheader
|
||||||
block = self.rpc('getblock', [block_hash, False])
|
block = self.rpc("getblock", [block_hash, False])
|
||||||
block_header = self.rpc('getblockheader', [block_hash])
|
block_header = self.rpc("getblockheader", [block_hash])
|
||||||
decoded_block = CBlock()
|
decoded_block = CBlock()
|
||||||
decoded_block = FromHex(decoded_block, block)
|
decoded_block = FromHex(decoded_block, block)
|
||||||
|
|
||||||
tx_rv = []
|
tx_rv = []
|
||||||
for tx in decoded_block.vtx:
|
for tx in decoded_block.vtx:
|
||||||
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
|
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
|
||||||
tx_rv.append(tx_dec)
|
tx_rv.append(tx_dec)
|
||||||
|
|
||||||
block_rv = {
|
block_rv = {
|
||||||
'hash': block_hash,
|
"hash": block_hash,
|
||||||
'previousblockhash': block_header['previousblockhash'],
|
"previousblockhash": block_header["previousblockhash"],
|
||||||
'tx': tx_rv,
|
"tx": tx_rv,
|
||||||
'confirmations': block_header['confirmations'],
|
"confirmations": block_header["confirmations"],
|
||||||
'height': block_header['height'],
|
"height": block_header["height"],
|
||||||
'time': block_header['time'],
|
"time": block_header["time"],
|
||||||
'version': block_header['version'],
|
"version": block_header["version"],
|
||||||
'merkleroot': block_header['merkleroot'],
|
"merkleroot": block_header["merkleroot"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return block_rv
|
return block_rv
|
||||||
|
|
||||||
def withdrawCoin(self, value, addr_to, subfee):
|
def withdrawCoin(self, value, addr_to, subfee):
|
||||||
params = [addr_to, value, '', '', subfee]
|
params = [addr_to, value, "", "", subfee]
|
||||||
return self.rpc('sendtoaddress', params)
|
return self.rpc("sendtoaddress", params)
|
||||||
|
|
||||||
def getSpendableBalance(self) -> int:
|
def getSpendableBalance(self) -> int:
|
||||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
return self.make_int(self.rpc("getwalletinfo")["balance"])
|
||||||
|
|
||||||
def loadTx(self, tx_bytes):
|
def loadTx(self, tx_bytes):
|
||||||
# Load tx from bytes to internal representation
|
# Load tx from bytes to internal representation
|
||||||
@@ -107,22 +118,35 @@ class PIVXInterface(BTCInterface):
|
|||||||
add_bytes = 107
|
add_bytes = 107
|
||||||
size = len(tx.serialize_with_witness()) + add_bytes
|
size = len(tx.serialize_with_witness()) + add_bytes
|
||||||
pay_fee = round(fee_rate * size / 1000)
|
pay_fee = round(fee_rate * size / 1000)
|
||||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
self._log.info(
|
||||||
|
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||||
|
)
|
||||||
return pay_fee
|
return pay_fee
|
||||||
|
|
||||||
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||||
key_wif = self.encodeKey(key)
|
key_wif = self.encodeKey(key)
|
||||||
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
rv = self.rpc(
|
||||||
return bytes.fromhex(rv['hex'])
|
"signrawtransaction",
|
||||||
|
[
|
||||||
|
tx.hex(),
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
key_wif,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return bytes.fromhex(rv["hex"])
|
||||||
|
|
||||||
def findTxnByHash(self, txid_hex: str):
|
def findTxnByHash(self, txid_hex: str):
|
||||||
# Only works for wallet txns
|
# Only works for wallet txns
|
||||||
try:
|
try:
|
||||||
rv = self.rpc('gettransaction', [txid_hex])
|
rv = self.rpc("gettransaction", [txid_hex])
|
||||||
except Exception as ex:
|
except Exception as e: # noqa: F841
|
||||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
self._log.debug(
|
||||||
|
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -25,3 +26,7 @@ class WOWInterface(XMRInterface):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def exp() -> int:
|
def exp() -> int:
|
||||||
return 11
|
return 11
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def depth_spendable() -> int:
|
||||||
|
return 3
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2024 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import basicswap.contrib.ed25519_fast as edf
|
import basicswap.contrib.ed25519_fast as edf
|
||||||
@@ -27,16 +27,9 @@ from coincurve.dleag import (
|
|||||||
from basicswap.interface.base import (
|
from basicswap.interface.base import (
|
||||||
Curves,
|
Curves,
|
||||||
)
|
)
|
||||||
from basicswap.util import (
|
from basicswap.util import i2b, b2i, b2h, dumpj, ensure, TemporaryError
|
||||||
i2b, b2i, b2h,
|
from basicswap.util.network import is_private_ip_address
|
||||||
dumpj,
|
from basicswap.rpc_xmr import make_xmr_rpc_func, make_xmr_rpc2_func
|
||||||
ensure,
|
|
||||||
TemporaryError)
|
|
||||||
from basicswap.util.network import (
|
|
||||||
is_private_ip_address)
|
|
||||||
from basicswap.rpc_xmr import (
|
|
||||||
make_xmr_rpc_func,
|
|
||||||
make_xmr_rpc2_func)
|
|
||||||
from basicswap.chainparams import XMR_COIN, Coins
|
from basicswap.chainparams import XMR_COIN, Coins
|
||||||
from basicswap.interface.base import CoinInterface
|
from basicswap.interface.base import CoinInterface
|
||||||
|
|
||||||
@@ -76,95 +69,167 @@ class XMRInterface(CoinInterface):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||||
raise ValueError('Not possible')
|
raise ValueError("Not possible")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def est_lock_tx_vsize() -> int:
|
||||||
|
# TODO: Estimate with ringsize
|
||||||
|
return 1604
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
# TODO: Estimate with ringsize
|
# TODO: Estimate with ringsize
|
||||||
return 1604
|
return 1604
|
||||||
|
|
||||||
|
def is_transient_error(self, ex) -> bool:
|
||||||
|
str_error: str = str(ex).lower()
|
||||||
|
if "failed to get earliest fork height" in str_error:
|
||||||
|
return True
|
||||||
|
return super().is_transient_error(ex)
|
||||||
|
|
||||||
def __init__(self, coin_settings, network, swap_client=None):
|
def __init__(self, coin_settings, network, swap_client=None):
|
||||||
super().__init__(network)
|
super().__init__(network)
|
||||||
|
|
||||||
self._addr_prefix = self.chainparams_network()['address_prefix']
|
self._addr_prefix = self.chainparams_network()["address_prefix"]
|
||||||
|
|
||||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
self.blocks_confirmed = coin_settings["blocks_confirmed"]
|
||||||
self._restore_height = coin_settings.get('restore_height', 0)
|
self._restore_height = coin_settings.get("restore_height", 0)
|
||||||
self.setFeePriority(coin_settings.get('fee_priority', 0))
|
self.setFeePriority(coin_settings.get("fee_priority", 0))
|
||||||
self._sc = swap_client
|
self._sc = swap_client
|
||||||
self._log = self._sc.log if self._sc and self._sc.log else logging
|
self._log = self._sc.log if self._sc and self._sc.log else logging
|
||||||
self._wallet_password = None
|
self._wallet_password = None
|
||||||
self._have_checked_seed = False
|
self._have_checked_seed = False
|
||||||
|
self._wallet_filename = coin_settings.get("wallet_name", "swap_wallet")
|
||||||
|
|
||||||
daemon_login = None
|
daemon_login = None
|
||||||
if coin_settings.get('rpcuser', '') != '':
|
if coin_settings.get("rpcuser", "") != "":
|
||||||
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
|
daemon_login = (
|
||||||
|
coin_settings.get("rpcuser", ""),
|
||||||
|
coin_settings.get("rpcpassword", ""),
|
||||||
|
)
|
||||||
|
|
||||||
rpchost = coin_settings.get('rpchost', '127.0.0.1')
|
rpchost = coin_settings.get("rpchost", "127.0.0.1")
|
||||||
proxy_host = None
|
proxy_host = None
|
||||||
proxy_port = None
|
proxy_port = None
|
||||||
# Connect to the daemon over a proxy if not running locally
|
# Connect to the daemon over a proxy if not running locally
|
||||||
if swap_client:
|
if swap_client:
|
||||||
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
|
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
|
||||||
manage_daemon: bool = chain_client_settings['manage_daemon']
|
manage_daemon: bool = chain_client_settings["manage_daemon"]
|
||||||
if swap_client.use_tor_proxy:
|
if swap_client.use_tor_proxy:
|
||||||
if manage_daemon is False:
|
if manage_daemon is False:
|
||||||
log_str: str = ''
|
log_str: str = ""
|
||||||
have_cc_tor_opt = 'use_tor' in chain_client_settings
|
have_cc_tor_opt = "use_tor" in chain_client_settings
|
||||||
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
|
if have_cc_tor_opt and chain_client_settings["use_tor"] is False:
|
||||||
log_str = ' bypassing proxy (use_tor false for XMR)'
|
log_str = (
|
||||||
|
f" bypassing proxy (use_tor false for {self.coin_name()})"
|
||||||
|
)
|
||||||
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
|
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
|
||||||
log_str = ' bypassing proxy (private ip address)'
|
log_str = " bypassing proxy (private ip address)"
|
||||||
else:
|
else:
|
||||||
proxy_host = swap_client.tor_proxy_host
|
proxy_host = swap_client.tor_proxy_host
|
||||||
proxy_port = swap_client.tor_proxy_port
|
proxy_port = swap_client.tor_proxy_port
|
||||||
log_str = f' through proxy at {proxy_host}'
|
log_str = f" through proxy at {proxy_host}"
|
||||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
|
self._log.info(
|
||||||
|
f"Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
|
self._log.info(
|
||||||
|
f"Not connecting to local {self.coin_name()} daemon through proxy."
|
||||||
|
)
|
||||||
elif manage_daemon is False:
|
elif manage_daemon is False:
|
||||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
|
self._log.info(
|
||||||
|
f"Connecting to remote {self.coin_name()} daemon at {rpchost}."
|
||||||
|
)
|
||||||
|
|
||||||
self._rpctimeout = coin_settings.get('rpctimeout', 60)
|
self._rpctimeout = coin_settings.get("rpctimeout", 60)
|
||||||
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
|
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
|
||||||
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600)
|
# walletrpctimeoutlong likely unneeded
|
||||||
|
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
|
||||||
|
|
||||||
self.rpc = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ')
|
self.rpc = make_xmr_rpc_func(
|
||||||
self.rpc2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint
|
coin_settings["rpcport"],
|
||||||
self.rpc_wallet = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ')
|
daemon_login,
|
||||||
|
host=rpchost,
|
||||||
|
proxy_host=proxy_host,
|
||||||
|
proxy_port=proxy_port,
|
||||||
|
default_timeout=self._rpctimeout,
|
||||||
|
tag="Node(j) ",
|
||||||
|
)
|
||||||
|
self.rpc2 = make_xmr_rpc2_func(
|
||||||
|
coin_settings["rpcport"],
|
||||||
|
daemon_login,
|
||||||
|
host=rpchost,
|
||||||
|
proxy_host=proxy_host,
|
||||||
|
proxy_port=proxy_port,
|
||||||
|
default_timeout=self._rpctimeout,
|
||||||
|
tag="Node ",
|
||||||
|
) # non-json endpoint
|
||||||
|
self.rpc_wallet = make_xmr_rpc_func(
|
||||||
|
coin_settings["walletrpcport"],
|
||||||
|
coin_settings["walletrpcauth"],
|
||||||
|
host=coin_settings.get("walletrpchost", "127.0.0.1"),
|
||||||
|
default_timeout=self._walletrpctimeout,
|
||||||
|
tag="Wallet ",
|
||||||
|
)
|
||||||
|
|
||||||
def setFeePriority(self, new_priority):
|
def setFeePriority(self, new_priority):
|
||||||
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
|
ensure(new_priority >= 0 and new_priority < 4, "Invalid fee_priority value")
|
||||||
self._fee_priority = new_priority
|
self._fee_priority = new_priority
|
||||||
|
|
||||||
def setWalletFilename(self, wallet_filename):
|
|
||||||
self._wallet_filename = wallet_filename
|
|
||||||
|
|
||||||
def createWallet(self, params):
|
def createWallet(self, params):
|
||||||
if self._wallet_password is not None:
|
if self._wallet_password is not None:
|
||||||
params['password'] = self._wallet_password
|
params["password"] = self._wallet_password
|
||||||
rv = self.rpc_wallet('generate_from_keys', params)
|
rv = self.rpc_wallet("generate_from_keys", params)
|
||||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
if "address" in rv:
|
||||||
|
new_address: str = rv["address"]
|
||||||
|
is_watch_only: bool = "Watch-only" in rv.get("info", "")
|
||||||
|
self._log.info(
|
||||||
|
"Generated{} {} wallet: {}".format(
|
||||||
|
" watch-only" if is_watch_only else "",
|
||||||
|
self.coin_name(),
|
||||||
|
self._log.addr(new_address),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._log.debug("generate_from_keys %s", dumpj(rv))
|
||||||
|
raise ValueError("generate_from_keys failed")
|
||||||
|
|
||||||
def openWallet(self, filename):
|
def openWallet(self, filename):
|
||||||
params = {'filename': filename}
|
params = {"filename": filename}
|
||||||
if self._wallet_password is not None:
|
if self._wallet_password is not None:
|
||||||
params['password'] = self._wallet_password
|
params["password"] = self._wallet_password
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Can't reopen the same wallet in windows, !is_keys_file_locked()
|
self.rpc_wallet("open_wallet", params)
|
||||||
self.rpc_wallet('close_wallet')
|
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||||
except Exception:
|
self.rpc_wallet("refresh")
|
||||||
pass
|
except Exception as e:
|
||||||
self.rpc_wallet('open_wallet', params)
|
if "no connection to daemon" in str(e):
|
||||||
|
self._log.debug(f"{self.coin_name()} {e}")
|
||||||
|
return # bypass refresh error to allow startup with a busy daemon
|
||||||
|
|
||||||
def initialiseWallet(self, key_view: bytes, key_spend: bytes, restore_height=None) -> None:
|
try:
|
||||||
|
# TODO Remove `store` after upstream fix to autosave on close_wallet
|
||||||
|
self.rpc_wallet("store")
|
||||||
|
self.rpc_wallet("close_wallet")
|
||||||
|
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.rpc_wallet("open_wallet", params)
|
||||||
|
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||||
|
self.rpc_wallet("refresh")
|
||||||
|
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
|
||||||
|
|
||||||
|
def initialiseWallet(
|
||||||
|
self, key_view: bytes, key_spend: bytes, restore_height=None
|
||||||
|
) -> None:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
try:
|
try:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
# TODO: Check address
|
# TODO: Check address
|
||||||
return # Wallet exists
|
return # Wallet exists
|
||||||
except Exception as e:
|
except Exception as e: # noqa: F841
|
||||||
pass
|
pass
|
||||||
|
|
||||||
Kbv = self.getPubkey(key_view)
|
Kbv = self.getPubkey(key_view)
|
||||||
@@ -172,11 +237,11 @@ class XMRInterface(CoinInterface):
|
|||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'filename': self._wallet_filename,
|
"filename": self._wallet_filename,
|
||||||
'address': address_b58,
|
"address": address_b58,
|
||||||
'viewkey': b2h(key_view[::-1]),
|
"viewkey": b2h(key_view[::-1]),
|
||||||
'spendkey': b2h(key_spend[::-1]),
|
"spendkey": b2h(key_spend[::-1]),
|
||||||
'restore_height': self._restore_height,
|
"restore_height": self._restore_height,
|
||||||
}
|
}
|
||||||
self.createWallet(params)
|
self.createWallet(params)
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
@@ -186,86 +251,97 @@ class XMRInterface(CoinInterface):
|
|||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
|
|
||||||
def testDaemonRPC(self, with_wallet=True) -> None:
|
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||||
self.rpc_wallet('get_languages')
|
self.rpc_wallet("get_languages")
|
||||||
|
|
||||||
def getDaemonVersion(self):
|
def getDaemonVersion(self):
|
||||||
return self.rpc_wallet('get_version')['version']
|
return self.rpc_wallet("get_version")["version"]
|
||||||
|
|
||||||
def getBlockchainInfo(self):
|
def getBlockchainInfo(self):
|
||||||
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
|
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||||
rv = {
|
rv = {
|
||||||
'blocks': get_height['height'],
|
"blocks": get_height["height"],
|
||||||
'verificationprogress': 0.0,
|
"verificationprogress": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
|
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
|
||||||
# get_block_count returns "Internal error" if bootstrap-daemon is active
|
# get_block_count returns "Internal error" if bootstrap-daemon is active
|
||||||
if get_height['untrusted'] is True:
|
if get_height["untrusted"] is True:
|
||||||
rv['bootstrapping'] = True
|
rv["bootstrapping"] = True
|
||||||
get_info = self.rpc2('get_info', timeout=self._rpctimeout)
|
get_info = self.rpc2("get_info", timeout=self._rpctimeout)
|
||||||
if 'height_without_bootstrap' in get_info:
|
if "height_without_bootstrap" in get_info:
|
||||||
rv['blocks'] = get_info['height_without_bootstrap']
|
rv["blocks"] = get_info["height_without_bootstrap"]
|
||||||
|
|
||||||
rv['known_block_count'] = get_info['height']
|
rv["known_block_count"] = get_info["height"]
|
||||||
if rv['known_block_count'] > rv['blocks']:
|
if rv["known_block_count"] > rv["blocks"]:
|
||||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
|
||||||
else:
|
else:
|
||||||
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
|
rv["known_block_count"] = self.rpc(
|
||||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
"get_block_count", timeout=self._rpctimeout
|
||||||
|
)["count"]
|
||||||
|
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.warning(f'{self.ticker_str()} get_block_count failed with: {e}')
|
self._log.warning(f"{self.ticker_str()} get_block_count failed with: {e}")
|
||||||
rv['verificationprogress'] = 0.0
|
rv["verificationprogress"] = 0.0
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def getChainHeight(self):
|
def getChainHeight(self):
|
||||||
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
|
||||||
|
|
||||||
def getWalletInfo(self):
|
def getWalletInfo(self):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
try:
|
try:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'Failed to open wallet' in str(e):
|
if "Failed to open wallet" in str(e):
|
||||||
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0}
|
rv = {
|
||||||
|
"encrypted": True,
|
||||||
|
"locked": True,
|
||||||
|
"balance": 0,
|
||||||
|
"unconfirmed_balance": 0,
|
||||||
|
}
|
||||||
return rv
|
return rv
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
rv = {}
|
rv = {}
|
||||||
self.rpc_wallet('refresh')
|
balance_info = self.rpc_wallet("get_balance")
|
||||||
balance_info = self.rpc_wallet('get_balance')
|
|
||||||
|
|
||||||
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
|
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
|
||||||
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
|
rv["balance"] = self.format_amount(balance_info["unlocked_balance"])
|
||||||
rv['encrypted'] = False if self._wallet_password is None else True
|
rv["unconfirmed_balance"] = self.format_amount(
|
||||||
rv['locked'] = False
|
balance_info["balance"] - balance_info["unlocked_balance"]
|
||||||
|
)
|
||||||
|
rv["encrypted"] = False if self._wallet_password is None else True
|
||||||
|
rv["locked"] = False
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def getMainWalletAddress(self) -> str:
|
def getMainWalletAddress(self) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
return self.rpc_wallet('get_address')['address']
|
return self.rpc_wallet("get_address")["address"]
|
||||||
|
|
||||||
def getNewAddress(self, placeholder) -> str:
|
def getNewAddress(self, placeholder) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
|
new_address = self.rpc_wallet("create_address", {"account_index": 0})[
|
||||||
self.rpc_wallet('store')
|
"address"
|
||||||
|
]
|
||||||
|
self.rpc_wallet("store")
|
||||||
return new_address
|
return new_address
|
||||||
|
|
||||||
def get_fee_rate(self, conf_target: int = 2):
|
def get_fee_rate(self, conf_target: int = 2):
|
||||||
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
|
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
|
||||||
fee_est = self.rpc('get_fee_estimate')
|
fee_est = self.rpc("get_fee_estimate")
|
||||||
if conf_target <= 1:
|
if conf_target <= 1:
|
||||||
conf_target = 1 # normal
|
conf_target = 1 # normal
|
||||||
else:
|
else:
|
||||||
conf_target = 0 # slow
|
conf_target = 0 # slow
|
||||||
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000
|
fee_per_k_bytes = fee_est["fees"][conf_target] * 1000
|
||||||
|
|
||||||
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate'
|
return float(self.format_amount(fee_per_k_bytes)), "get_fee_estimate"
|
||||||
|
|
||||||
def getNewSecretKey(self) -> bytes:
|
def getNewRandomKey(self) -> bytes:
|
||||||
# Note: Returned bytes are in big endian order
|
# Note: Returned bytes are in big endian order
|
||||||
return i2b(edu.get_secret())
|
return i2b(edu.get_secret())
|
||||||
|
|
||||||
@@ -294,7 +370,7 @@ class XMRInterface(CoinInterface):
|
|||||||
|
|
||||||
def verifyKey(self, k: int) -> bool:
|
def verifyKey(self, k: int) -> bool:
|
||||||
i = b2i(k)
|
i = b2i(k)
|
||||||
return (i < edf.l and i > 8)
|
return i < edf.l and i > 8
|
||||||
|
|
||||||
def verifyPubkey(self, pubkey_bytes):
|
def verifyPubkey(self, pubkey_bytes):
|
||||||
# Calls ed25519_decode_check_point() in secp256k1
|
# Calls ed25519_decode_check_point() in secp256k1
|
||||||
@@ -320,45 +396,58 @@ class XMRInterface(CoinInterface):
|
|||||||
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
||||||
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
|
|
||||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
|
def publishBLockTx(
|
||||||
|
self,
|
||||||
|
kbv: bytes,
|
||||||
|
Kbs: bytes,
|
||||||
|
output_amount: int,
|
||||||
|
feerate: int,
|
||||||
|
unlock_time: int = 0,
|
||||||
|
) -> bytes:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet('refresh')
|
|
||||||
|
|
||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
|
|
||||||
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
|
params = {
|
||||||
|
"destinations": [{"amount": output_amount, "address": shared_addr}],
|
||||||
|
"unlock_time": unlock_time,
|
||||||
|
}
|
||||||
if self._fee_priority > 0:
|
if self._fee_priority > 0:
|
||||||
params['priority'] = self._fee_priority
|
params["priority"] = self._fee_priority
|
||||||
rv = self.rpc_wallet('transfer', params)
|
rv = self.rpc_wallet("transfer", params)
|
||||||
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
|
self._log.info(
|
||||||
tx_hash = bytes.fromhex(rv['tx_hash'])
|
"publishBLockTx %s to address_b58 %s",
|
||||||
|
self._log.id(rv["tx_hash"]),
|
||||||
|
self._log.addr(shared_addr),
|
||||||
|
)
|
||||||
|
tx_hash = bytes.fromhex(rv["tx_hash"])
|
||||||
|
|
||||||
return tx_hash
|
return tx_hash
|
||||||
|
|
||||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
def findTxB(
|
||||||
|
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||||
|
):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
|
|
||||||
kbv_le = kbv[::-1]
|
kbv_le = kbv[::-1]
|
||||||
params = {
|
params = {
|
||||||
'restore_height': restore_height,
|
"restore_height": restore_height,
|
||||||
'filename': address_b58,
|
"filename": address_b58,
|
||||||
'address': address_b58,
|
"address": address_b58,
|
||||||
'viewkey': b2h(kbv_le),
|
"viewkey": b2h(kbv_le),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.openWallet(address_b58)
|
self.openWallet(address_b58)
|
||||||
except Exception as e:
|
except Exception as e: # noqa: F841
|
||||||
self.createWallet(params)
|
self.createWallet(params)
|
||||||
self.openWallet(address_b58)
|
self.openWallet(address_b58)
|
||||||
|
|
||||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
"""
|
||||||
|
|
||||||
'''
|
|
||||||
# Debug
|
# Debug
|
||||||
try:
|
try:
|
||||||
current_height = self.rpc_wallet('get_height')['height']
|
current_height = self.rpc_wallet('get_height')['height']
|
||||||
@@ -367,136 +456,219 @@ class XMRInterface(CoinInterface):
|
|||||||
self._log.info('rpc failed %s', str(e))
|
self._log.info('rpc failed %s', str(e))
|
||||||
current_height = None # If the transfer is available it will be deep enough
|
current_height = None # If the transfer is available it will be deep enough
|
||||||
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
|
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
|
||||||
'''
|
"""
|
||||||
params = {'transfer_type': 'available'}
|
params = {"transfer_type": "available"}
|
||||||
transfers = self.rpc_wallet('incoming_transfers', params)
|
transfers = self.rpc_wallet("incoming_transfers", params)
|
||||||
rv = None
|
rv = None
|
||||||
if 'transfers' in transfers:
|
if "transfers" in transfers:
|
||||||
for transfer in transfers['transfers']:
|
for transfer in transfers["transfers"]:
|
||||||
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
|
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
|
||||||
if not transfer['unlocked']:
|
if not transfer["unlocked"]:
|
||||||
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
|
full_tx = self.rpc_wallet(
|
||||||
unlock_time = full_tx['transfer']['unlock_time']
|
"get_transfer_by_txid", {"txid": transfer["tx_hash"]}
|
||||||
|
)
|
||||||
|
unlock_time = full_tx["transfer"]["unlock_time"]
|
||||||
if unlock_time != 0:
|
if unlock_time != 0:
|
||||||
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
|
self._log.warning(
|
||||||
|
"Coin b lock txn is locked: {}, unlock_time {}".format(
|
||||||
|
transfer["tx_hash"], unlock_time
|
||||||
|
)
|
||||||
|
)
|
||||||
rv = -1
|
rv = -1
|
||||||
continue
|
continue
|
||||||
if transfer['amount'] == cb_swap_value:
|
if transfer["amount"] == cb_swap_value:
|
||||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
|
return {
|
||||||
|
"txid": transfer["tx_hash"],
|
||||||
|
"amount": transfer["amount"],
|
||||||
|
"height": (
|
||||||
|
0
|
||||||
|
if "block_height" not in transfer
|
||||||
|
else transfer["block_height"]
|
||||||
|
),
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
|
self._log.warning(
|
||||||
|
"Incorrect amount detected for coin b lock txn: {}".format(
|
||||||
|
transfer["tx_hash"]
|
||||||
|
)
|
||||||
|
)
|
||||||
rv = -1
|
rv = -1
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def findTxnByHash(self, txid):
|
def findTxnByHash(self, txid):
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
|
||||||
self._log.info(f'findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}')
|
"height"
|
||||||
|
]
|
||||||
|
self._log.info(
|
||||||
|
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.info('rpc failed %s', str(e))
|
self._log.info("rpc failed %s", str(e))
|
||||||
current_height = None # If the transfer is available it will be deep enough
|
current_height = (
|
||||||
|
None # If the transfer is available it will be deep enough
|
||||||
|
)
|
||||||
|
|
||||||
params = {'transfer_type': 'available'}
|
params = {"transfer_type": "available"}
|
||||||
rv = self.rpc_wallet('incoming_transfers', params)
|
rv = self.rpc_wallet("incoming_transfers", params)
|
||||||
if 'transfers' in rv:
|
if "transfers" in rv:
|
||||||
for transfer in rv['transfers']:
|
for transfer in rv["transfers"]:
|
||||||
if transfer['tx_hash'] == txid \
|
if transfer["tx_hash"] == txid and (
|
||||||
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed):
|
current_height is None
|
||||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']}
|
or current_height - transfer["block_height"]
|
||||||
|
> self.blocks_confirmed
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"txid": transfer["tx_hash"],
|
||||||
|
"amount": transfer["amount"],
|
||||||
|
"height": transfer["block_height"],
|
||||||
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
|
def spendBLockTx(
|
||||||
'''
|
self,
|
||||||
|
chain_b_lock_txid: bytes,
|
||||||
|
address_to: str,
|
||||||
|
kbv: bytes,
|
||||||
|
kbs: bytes,
|
||||||
|
cb_swap_value: int,
|
||||||
|
b_fee_rate: int,
|
||||||
|
restore_height: int,
|
||||||
|
spend_actual_balance: bool = False,
|
||||||
|
lock_tx_vout=None,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
Notes:
|
Notes:
|
||||||
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
|
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
|
||||||
'''
|
"""
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
Kbs = self.getPubkey(kbs)
|
Kbs = self.getPubkey(kbs)
|
||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
|
|
||||||
wallet_filename = address_b58 + '_spend'
|
wallet_filename = address_b58 + "_spend"
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'filename': wallet_filename,
|
"filename": wallet_filename,
|
||||||
'address': address_b58,
|
"address": address_b58,
|
||||||
'viewkey': b2h(kbv[::-1]),
|
"viewkey": b2h(kbv[::-1]),
|
||||||
'spendkey': b2h(kbs[::-1]),
|
"spendkey": b2h(kbs[::-1]),
|
||||||
'restore_height': restore_height,
|
"restore_height": restore_height,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.openWallet(wallet_filename)
|
self.openWallet(wallet_filename)
|
||||||
except Exception as e:
|
except Exception as e: # noqa: F841
|
||||||
self.createWallet(params)
|
self.createWallet(params)
|
||||||
self.openWallet(wallet_filename)
|
self.openWallet(wallet_filename)
|
||||||
|
|
||||||
self.rpc_wallet('refresh')
|
rv = self.rpc_wallet("get_balance")
|
||||||
rv = self.rpc_wallet('get_balance')
|
if rv["balance"] < cb_swap_value:
|
||||||
if rv['balance'] < cb_swap_value:
|
self._log.warning("Balance is too low, checking for existing spend.")
|
||||||
self._log.warning('Balance is too low, checking for existing spend.')
|
txns = self.rpc_wallet("get_transfers", {"out": True})
|
||||||
txns = self.rpc_wallet('get_transfers', {'out': True})
|
if "out" in txns:
|
||||||
if 'out' in txns:
|
txns = txns["out"]
|
||||||
txns = txns['out']
|
|
||||||
if len(txns) > 0:
|
if len(txns) > 0:
|
||||||
txid = txns[0]['txid']
|
txid = txns[0]["txid"]
|
||||||
self._log.warning(f'spendBLockTx detected spending tx: {txid}.')
|
self._log.warning(f"spendBLockTx detected spending tx: {txid}.")
|
||||||
if txns[0]['address'] == address_b58:
|
|
||||||
|
# Should check for address_to, but only the from address is found in the output
|
||||||
|
if txns[0]["address"] == address_b58:
|
||||||
return bytes.fromhex(txid)
|
return bytes.fromhex(txid)
|
||||||
|
|
||||||
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
|
self._log.error(
|
||||||
|
"wallet {} balance {}, expected {}".format(
|
||||||
|
wallet_filename, rv["balance"], cb_swap_value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not spend_actual_balance:
|
if not spend_actual_balance:
|
||||||
raise TemporaryError('Invalid balance')
|
raise TemporaryError("Invalid balance")
|
||||||
|
|
||||||
if spend_actual_balance and rv['balance'] != cb_swap_value:
|
if spend_actual_balance and rv["balance"] != cb_swap_value:
|
||||||
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value))
|
self._log.warning(
|
||||||
cb_swap_value = rv['balance']
|
"Spending actual balance {}, not swap value {}.".format(
|
||||||
if rv['unlocked_balance'] < cb_swap_value:
|
rv["balance"], cb_swap_value
|
||||||
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock']))
|
)
|
||||||
raise TemporaryError('Invalid unlocked_balance')
|
)
|
||||||
|
cb_swap_value = rv["balance"]
|
||||||
|
if rv["unlocked_balance"] < cb_swap_value:
|
||||||
|
self._log.error(
|
||||||
|
"wallet {} balance {}, expected {}, blocks_to_unlock {}".format(
|
||||||
|
wallet_filename,
|
||||||
|
rv["unlocked_balance"],
|
||||||
|
cb_swap_value,
|
||||||
|
rv["blocks_to_unlock"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise TemporaryError("Invalid unlocked_balance")
|
||||||
|
|
||||||
params = {'address': address_to}
|
params = {"address": address_to}
|
||||||
if self._fee_priority > 0:
|
if self._fee_priority > 0:
|
||||||
params['priority'] = self._fee_priority
|
params["priority"] = self._fee_priority
|
||||||
|
|
||||||
rv = self.rpc_wallet('sweep_all', params)
|
rv = self.rpc_wallet("sweep_all", params)
|
||||||
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
|
|
||||||
|
|
||||||
return bytes.fromhex(rv['tx_hash_list'][0])
|
return bytes.fromhex(rv["tx_hash_list"][0])
|
||||||
|
|
||||||
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str:
|
def withdrawCoin(
|
||||||
|
self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False
|
||||||
|
) -> str:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet('refresh')
|
|
||||||
|
|
||||||
if sweepall:
|
if sweepall:
|
||||||
balance = self.rpc_wallet('get_balance')
|
balance = self.rpc_wallet("get_balance")
|
||||||
if balance['balance'] != balance['unlocked_balance']:
|
if balance["balance"] != balance["unlocked_balance"]:
|
||||||
raise ValueError('Balance must be fully confirmed to use sweep all.')
|
raise ValueError(
|
||||||
self._log.info('{} {} sweep_all.'.format(self.ticker_str(), 'estimate fee' if estimate_fee else 'withdraw'))
|
"Balance must be fully confirmed to use sweep all."
|
||||||
self._log.debug('{} balance: {}'.format(self.ticker_str(), balance['balance']))
|
)
|
||||||
params = {'address': addr_to, 'do_not_relay': estimate_fee}
|
self._log.info(
|
||||||
|
"{} {} sweep_all.".format(
|
||||||
|
self.ticker_str(),
|
||||||
|
"estimate fee" if estimate_fee else "withdraw",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._log.debug(
|
||||||
|
"{} balance: {}".format(self.ticker_str(), balance["balance"])
|
||||||
|
)
|
||||||
|
params = {
|
||||||
|
"address": addr_to,
|
||||||
|
"do_not_relay": estimate_fee,
|
||||||
|
"subaddr_indices_all": True,
|
||||||
|
}
|
||||||
if self._fee_priority > 0:
|
if self._fee_priority > 0:
|
||||||
params['priority'] = self._fee_priority
|
params["priority"] = self._fee_priority
|
||||||
rv = self.rpc_wallet('sweep_all', params)
|
rv = self.rpc_wallet("sweep_all", params)
|
||||||
if estimate_fee:
|
if estimate_fee:
|
||||||
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])}
|
return {
|
||||||
return rv['tx_hash_list'][0]
|
"num_txns": len(rv["fee_list"]),
|
||||||
|
"sum_amount": sum(rv["amount_list"]),
|
||||||
|
"sum_fee": sum(rv["fee_list"]),
|
||||||
|
"sum_weight": sum(rv["weight_list"]),
|
||||||
|
}
|
||||||
|
return rv["tx_hash_list"][0]
|
||||||
|
|
||||||
value_sats: int = self.make_int(value)
|
value_sats: int = self.make_int(value)
|
||||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee}
|
params = {
|
||||||
|
"destinations": [{"amount": value_sats, "address": addr_to}],
|
||||||
|
"do_not_relay": estimate_fee,
|
||||||
|
}
|
||||||
if self._fee_priority > 0:
|
if self._fee_priority > 0:
|
||||||
params['priority'] = self._fee_priority
|
params["priority"] = self._fee_priority
|
||||||
rv = self.rpc_wallet('transfer', params)
|
rv = self.rpc_wallet("transfer", params)
|
||||||
if estimate_fee:
|
if estimate_fee:
|
||||||
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']}
|
return {
|
||||||
return rv['tx_hash']
|
"num_txns": 1,
|
||||||
|
"sum_amount": rv["amount"],
|
||||||
|
"sum_fee": rv["fee"],
|
||||||
|
"sum_weight": rv["weight"],
|
||||||
|
}
|
||||||
|
return rv["tx_hash"]
|
||||||
|
|
||||||
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
|
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
|
||||||
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
|
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
|
||||||
@@ -506,7 +678,7 @@ class XMRInterface(CoinInterface):
|
|||||||
try:
|
try:
|
||||||
Kbv = self.getPubkey(kbv)
|
Kbv = self.getPubkey(kbv)
|
||||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||||
wallet_file = address_b58 + '_spend'
|
wallet_file = address_b58 + "_spend"
|
||||||
try:
|
try:
|
||||||
self.openWallet(wallet_file)
|
self.openWallet(wallet_file)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -514,54 +686,59 @@ class XMRInterface(CoinInterface):
|
|||||||
try:
|
try:
|
||||||
self.openWallet(wallet_file)
|
self.openWallet(wallet_file)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.')
|
self._log.info(
|
||||||
|
f"showLockTransfers trying to create wallet for address {address_b58}."
|
||||||
|
)
|
||||||
kbv_le = kbv[::-1]
|
kbv_le = kbv[::-1]
|
||||||
params = {
|
params = {
|
||||||
'restore_height': restore_height,
|
"restore_height": restore_height,
|
||||||
'filename': address_b58,
|
"filename": address_b58,
|
||||||
'address': address_b58,
|
"address": address_b58,
|
||||||
'viewkey': b2h(kbv_le),
|
"viewkey": b2h(kbv_le),
|
||||||
}
|
}
|
||||||
self.createWallet(params)
|
self.createWallet(params)
|
||||||
self.openWallet(address_b58)
|
self.openWallet(address_b58)
|
||||||
|
|
||||||
self.rpc_wallet('refresh')
|
rv = self.rpc_wallet(
|
||||||
|
"get_transfers",
|
||||||
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
|
{"in": True, "out": True, "pending": True, "failed": True},
|
||||||
rv['filename'] = wallet_file
|
)
|
||||||
|
rv["filename"] = wallet_file
|
||||||
return rv
|
return rv
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'error': str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
def getSpendableBalance(self) -> int:
|
def getSpendableBalance(self) -> int:
|
||||||
with self._mx_wallet:
|
with self._mx_wallet:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
|
|
||||||
self.rpc_wallet('refresh')
|
balance_info = self.rpc_wallet("get_balance")
|
||||||
balance_info = self.rpc_wallet('get_balance')
|
return balance_info["unlocked_balance"]
|
||||||
return balance_info['unlocked_balance']
|
|
||||||
|
|
||||||
def changeWalletPassword(self, old_password, new_password):
|
def changeWalletPassword(self, old_password, new_password):
|
||||||
self._log.info('changeWalletPassword - {}'.format(self.ticker()))
|
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||||
orig_password = self._wallet_password
|
orig_password = self._wallet_password
|
||||||
if old_password != '':
|
if old_password != "":
|
||||||
self._wallet_password = old_password
|
self._wallet_password = old_password
|
||||||
try:
|
try:
|
||||||
self.openWallet(self._wallet_filename)
|
self.openWallet(self._wallet_filename)
|
||||||
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
|
self.rpc_wallet(
|
||||||
|
"change_wallet_password",
|
||||||
|
{"old_password": old_password, "new_password": new_password},
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._wallet_password = orig_password
|
self._wallet_password = orig_password
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unlockWallet(self, password: str) -> None:
|
def unlockWallet(self, password: str) -> None:
|
||||||
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
self._wallet_password = password
|
self._wallet_password = password
|
||||||
|
|
||||||
if not self._have_checked_seed:
|
if not self._have_checked_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
def lockWallet(self) -> None:
|
def lockWallet(self) -> None:
|
||||||
self._log.info('lockWallet - {}'.format(self.ticker()))
|
self._log.info("lockWallet - {}".format(self.ticker()))
|
||||||
self._wallet_password = None
|
self._wallet_password = None
|
||||||
|
|
||||||
def isAddressMine(self, address):
|
def isAddressMine(self, address):
|
||||||
@@ -570,7 +747,14 @@ class XMRInterface(CoinInterface):
|
|||||||
|
|
||||||
def ensureFunds(self, amount: int) -> None:
|
def ensureFunds(self, amount: int) -> None:
|
||||||
if self.getSpendableBalance() < amount:
|
if self.getSpendableBalance() < amount:
|
||||||
raise ValueError('Balance too low')
|
raise ValueError("Balance too low")
|
||||||
|
|
||||||
def getTransaction(self, txid: bytes):
|
def getTransaction(self, txid: bytes):
|
||||||
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
return self.rpc2(
|
||||||
|
"get_transactions",
|
||||||
|
{
|
||||||
|
"txs_hashes": [
|
||||||
|
txid.hex(),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
|
||||||
'''
|
"""
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||||
@@ -18,12 +18,12 @@ Don't encode fields of default values.
|
|||||||
When decoding initialise all fields not set from data.
|
When decoding initialise all fields not set from data.
|
||||||
|
|
||||||
protobuf ParseFromString would reset the whole object, from_bytes won't.
|
protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
from basicswap.util.integer import encode_varint, decode_varint
|
from basicswap.util.integer import encode_varint, decode_varint
|
||||||
|
|
||||||
|
|
||||||
class NonProtobufClass():
|
class NonProtobufClass:
|
||||||
def __init__(self, init_all: bool = True, **kwargs):
|
def __init__(self, init_all: bool = True, **kwargs):
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
found_field: bool = False
|
found_field: bool = False
|
||||||
@@ -34,7 +34,7 @@ class NonProtobufClass():
|
|||||||
found_field = True
|
found_field = True
|
||||||
break
|
break
|
||||||
if found_field is False:
|
if found_field is False:
|
||||||
raise ValueError(f'got an unexpected keyword argument \'{key}\'')
|
raise ValueError(f"got an unexpected keyword argument '{key}'")
|
||||||
|
|
||||||
if init_all:
|
if init_all:
|
||||||
self.init_fields()
|
self.init_fields()
|
||||||
@@ -53,7 +53,7 @@ class NonProtobufClass():
|
|||||||
else:
|
else:
|
||||||
setattr(self, field_name, bytes())
|
setattr(self, field_name, bytes())
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def to_bytes(self) -> bytes:
|
||||||
rv = bytes()
|
rv = bytes()
|
||||||
@@ -74,11 +74,11 @@ class NonProtobufClass():
|
|||||||
continue
|
continue
|
||||||
rv += encode_varint(tag)
|
rv += encode_varint(tag)
|
||||||
if isinstance(field_value, str):
|
if isinstance(field_value, str):
|
||||||
field_value = field_value.encode('utf-8')
|
field_value = field_value.encode("utf-8")
|
||||||
rv += encode_varint(len(field_value))
|
rv += encode_varint(len(field_value))
|
||||||
rv += field_value
|
rv += field_value
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
|
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
|
||||||
@@ -92,7 +92,9 @@ class NonProtobufClass():
|
|||||||
|
|
||||||
field_name, wire_type_expect, field_type = self._map[field_num]
|
field_name, wire_type_expect, field_type = self._map[field_num]
|
||||||
if wire_type != wire_type_expect:
|
if wire_type != wire_type_expect:
|
||||||
raise ValueError(f'Unexpected wire_type {wire_type} for field {field_num}')
|
raise ValueError(
|
||||||
|
f"Unexpected wire_type {wire_type} for field {field_num}"
|
||||||
|
)
|
||||||
|
|
||||||
if wire_type == 0:
|
if wire_type == 0:
|
||||||
field_value, lv = decode_varint(b, o)
|
field_value, lv = decode_varint(b, o)
|
||||||
@@ -103,9 +105,9 @@ class NonProtobufClass():
|
|||||||
field_value = b[o : o + field_len]
|
field_value = b[o : o + field_len]
|
||||||
o += field_len
|
o += field_len
|
||||||
if field_type == 1:
|
if field_type == 1:
|
||||||
field_value = field_value.decode('utf-8')
|
field_value = field_value.decode("utf-8")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||||
|
|
||||||
setattr(self, field_name, field_value)
|
setattr(self, field_name, field_value)
|
||||||
|
|
||||||
@@ -115,151 +117,150 @@ class NonProtobufClass():
|
|||||||
|
|
||||||
class OfferMessage(NonProtobufClass):
|
class OfferMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('protocol_version', 0, 0),
|
1: ("protocol_version", 0, 0),
|
||||||
2: ('coin_from', 0, 0),
|
2: ("coin_from", 0, 0),
|
||||||
3: ('coin_to', 0, 0),
|
3: ("coin_to", 0, 0),
|
||||||
4: ('amount_from', 0, 0),
|
4: ("amount_from", 0, 0),
|
||||||
5: ('amount_to', 0, 0),
|
5: ("amount_to", 0, 0),
|
||||||
6: ('min_bid_amount', 0, 0),
|
6: ("min_bid_amount", 0, 0),
|
||||||
7: ('time_valid', 0, 0),
|
7: ("time_valid", 0, 0),
|
||||||
8: ('lock_type', 0, 0),
|
8: ("lock_type", 0, 0),
|
||||||
9: ('lock_value', 0, 0),
|
9: ("lock_value", 0, 0),
|
||||||
10: ('swap_type', 0, 0),
|
10: ("swap_type", 0, 0),
|
||||||
11: ('proof_address', 2, 1),
|
11: ("proof_address", 2, 1),
|
||||||
12: ('proof_signature', 2, 1),
|
12: ("proof_signature", 2, 1),
|
||||||
13: ('pkhash_seller', 2, 0),
|
13: ("pkhash_seller", 2, 0),
|
||||||
14: ('secret_hash', 2, 0),
|
14: ("secret_hash", 2, 0),
|
||||||
15: ('fee_rate_from', 0, 0),
|
15: ("fee_rate_from", 0, 0),
|
||||||
16: ('fee_rate_to', 0, 0),
|
16: ("fee_rate_to", 0, 0),
|
||||||
17: ('amount_negotiable', 0, 2),
|
17: ("amount_negotiable", 0, 2),
|
||||||
18: ('rate_negotiable', 0, 2),
|
18: ("rate_negotiable", 0, 2),
|
||||||
19: ('proof_utxos', 2, 0),
|
19: ("proof_utxos", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BidMessage(NonProtobufClass):
|
class BidMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('protocol_version', 0, 0),
|
1: ("protocol_version", 0, 0),
|
||||||
2: ('offer_msg_id', 2, 0),
|
2: ("offer_msg_id", 2, 0),
|
||||||
3: ('time_valid', 0, 0),
|
3: ("time_valid", 0, 0),
|
||||||
4: ('amount', 0, 0),
|
4: ("amount", 0, 0),
|
||||||
5: ('amount_to', 0, 0),
|
5: ("amount_to", 0, 0),
|
||||||
6: ('pkhash_buyer', 2, 0),
|
6: ("pkhash_buyer", 2, 0),
|
||||||
7: ('proof_address', 2, 1),
|
7: ("proof_address", 2, 1),
|
||||||
8: ('proof_signature', 2, 1),
|
8: ("proof_signature", 2, 1),
|
||||||
9: ('proof_utxos', 2, 0),
|
9: ("proof_utxos", 2, 0),
|
||||||
10: ('pkhash_buyer_to', 2, 0),
|
10: ("pkhash_buyer_to", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BidAcceptMessage(NonProtobufClass):
|
class BidAcceptMessage(NonProtobufClass):
|
||||||
# Step 3, seller -> buyer
|
# Step 3, seller -> buyer
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('initiate_txid', 2, 0),
|
2: ("initiate_txid", 2, 0),
|
||||||
3: ('contract_script', 2, 0),
|
3: ("contract_script", 2, 0),
|
||||||
4: ('pkhash_seller', 2, 0),
|
4: ("pkhash_seller", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OfferRevokeMessage(NonProtobufClass):
|
class OfferRevokeMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('offer_msg_id', 2, 0),
|
1: ("offer_msg_id", 2, 0),
|
||||||
2: ('signature', 2, 0),
|
2: ("signature", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BidRejectMessage(NonProtobufClass):
|
class BidRejectMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('reject_code', 0, 0),
|
2: ("reject_code", 0, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrBidMessage(NonProtobufClass):
|
class XmrBidMessage(NonProtobufClass):
|
||||||
# MSG1L, F -> L
|
# MSG1L, F -> L
|
||||||
_map = {
|
_map = {
|
||||||
1: ('protocol_version', 0, 0),
|
1: ("protocol_version", 0, 0),
|
||||||
2: ('offer_msg_id', 2, 0),
|
2: ("offer_msg_id", 2, 0),
|
||||||
3: ('time_valid', 0, 0),
|
3: ("time_valid", 0, 0),
|
||||||
4: ('amount', 0, 0),
|
4: ("amount", 0, 0),
|
||||||
5: ('amount_to', 0, 0),
|
5: ("amount_to", 0, 0),
|
||||||
6: ('pkaf', 2, 0),
|
6: ("pkaf", 2, 0),
|
||||||
7: ('kbvf', 2, 0),
|
7: ("kbvf", 2, 0),
|
||||||
8: ('kbsf_dleag', 2, 0),
|
8: ("kbsf_dleag", 2, 0),
|
||||||
9: ('dest_af', 2, 0),
|
9: ("dest_af", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrSplitMessage(NonProtobufClass):
|
class XmrSplitMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('msg_id', 2, 0),
|
1: ("msg_id", 2, 0),
|
||||||
2: ('msg_type', 0, 0),
|
2: ("msg_type", 0, 0),
|
||||||
3: ('sequence', 0, 0),
|
3: ("sequence", 0, 0),
|
||||||
4: ('dleag', 2, 0),
|
4: ("dleag", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrBidAcceptMessage(NonProtobufClass):
|
class XmrBidAcceptMessage(NonProtobufClass):
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('pkal', 2, 0),
|
2: ("pkal", 2, 0),
|
||||||
3: ('kbvl', 2, 0),
|
3: ("kbvl", 2, 0),
|
||||||
4: ('kbsl_dleag', 2, 0),
|
4: ("kbsl_dleag", 2, 0),
|
||||||
|
|
||||||
# MSG2F
|
# MSG2F
|
||||||
5: ('a_lock_tx', 2, 0),
|
5: ("a_lock_tx", 2, 0),
|
||||||
6: ('a_lock_tx_script', 2, 0),
|
6: ("a_lock_tx_script", 2, 0),
|
||||||
7: ('a_lock_refund_tx', 2, 0),
|
7: ("a_lock_refund_tx", 2, 0),
|
||||||
8: ('a_lock_refund_tx_script', 2, 0),
|
8: ("a_lock_refund_tx_script", 2, 0),
|
||||||
9: ('a_lock_refund_spend_tx', 2, 0),
|
9: ("a_lock_refund_spend_tx", 2, 0),
|
||||||
10: ('al_lock_refund_tx_sig', 2, 0),
|
10: ("al_lock_refund_tx_sig", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
||||||
# MSG3L
|
# MSG3L
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('af_lock_refund_spend_tx_esig', 2, 0),
|
2: ("af_lock_refund_spend_tx_esig", 2, 0),
|
||||||
3: ('af_lock_refund_tx_sig', 2, 0),
|
3: ("af_lock_refund_tx_sig", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
||||||
# MSG4F
|
# MSG4F
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('a_lock_spend_tx', 2, 0),
|
2: ("a_lock_spend_tx", 2, 0),
|
||||||
3: ('kal_sig', 2, 0),
|
3: ("kal_sig", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||||
# MSG5F
|
# MSG5F
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('al_lock_spend_tx_esig', 2, 0),
|
2: ("al_lock_spend_tx_esig", 2, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ADSBidIntentMessage(NonProtobufClass):
|
class ADSBidIntentMessage(NonProtobufClass):
|
||||||
# L -> F Sent from bidder, construct a reverse bid
|
# L -> F Sent from bidder, construct a reverse bid
|
||||||
_map = {
|
_map = {
|
||||||
1: ('protocol_version', 0, 0),
|
1: ("protocol_version", 0, 0),
|
||||||
2: ('offer_msg_id', 2, 0),
|
2: ("offer_msg_id", 2, 0),
|
||||||
3: ('time_valid', 0, 0),
|
3: ("time_valid", 0, 0),
|
||||||
4: ('amount_from', 0, 0),
|
4: ("amount_from", 0, 0),
|
||||||
5: ('amount_to', 0, 0),
|
5: ("amount_to", 0, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
||||||
# F -> L Sent from offerer, construct a reverse bid
|
# F -> L Sent from offerer, construct a reverse bid
|
||||||
_map = {
|
_map = {
|
||||||
1: ('bid_msg_id', 2, 0),
|
1: ("bid_msg_id", 2, 0),
|
||||||
2: ('pkaf', 2, 0),
|
2: ("pkaf", 2, 0),
|
||||||
3: ('kbvf', 2, 0),
|
3: ("kbvf", 2, 0),
|
||||||
4: ('kbsf_dleag', 2, 0),
|
4: ("kbsf_dleag", 2, 0),
|
||||||
5: ('dest_af', 2, 0),
|
5: ("dest_af", 2, 0),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||||
|
|
||||||
Handshake procedure:
|
Handshake procedure:
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
Both nodes are initialised
|
Both nodes are initialised
|
||||||
|
|
||||||
XChaCha20_Poly1305 mac is 16bytes
|
XChaCha20_Poly1305 mac is 16bytes
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import queue
|
import queue
|
||||||
@@ -36,11 +36,12 @@ from Crypto.Cipher import ChaCha20_Poly1305 # TODO: Add to libsecp256k1/coincur
|
|||||||
from coincurve.keys import PrivateKey, PublicKey
|
from coincurve.keys import PrivateKey, PublicKey
|
||||||
from basicswap.contrib.rfc6979 import (
|
from basicswap.contrib.rfc6979 import (
|
||||||
rfc6979_hmac_sha256_initialize,
|
rfc6979_hmac_sha256_initialize,
|
||||||
rfc6979_hmac_sha256_generate)
|
rfc6979_hmac_sha256_generate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
START_TOKEN = 0xabcd
|
START_TOKEN = 0xABCD
|
||||||
MSG_START_TOKEN = START_TOKEN.to_bytes(2, 'big')
|
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
|
||||||
|
|
||||||
MSG_MAX_SIZE = 0x200000 # 2MB
|
MSG_MAX_SIZE = 0x200000 # 2MB
|
||||||
|
|
||||||
@@ -63,35 +64,37 @@ class NetMessageTypes(IntEnum):
|
|||||||
return value in cls._value2member_map_
|
return value in cls._value2member_map_
|
||||||
|
|
||||||
|
|
||||||
'''
|
"""
|
||||||
class NetMessage:
|
class NetMessage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._msg_class = None # 2 bytes
|
self._msg_class = None # 2 bytes
|
||||||
self._len = None # 4 bytes
|
self._len = None # 4 bytes
|
||||||
self._msg_type = None # 2 bytes
|
self._msg_type = None # 2 bytes
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Ensure handshake keys are not reused by including the time in the msg, mac and key hash
|
# Ensure handshake keys are not reused by including the time in the msg, mac and key hash
|
||||||
# Verify timestamp is not too old
|
# Verify timestamp is not too old
|
||||||
# Add keys to db to catch concurrent attempts, records can be cleared periodically, the timestamp should catch older replay attempts
|
# Add keys to db to catch concurrent attempts, records can be cleared periodically, the timestamp should catch older replay attempts
|
||||||
class MsgHandshake:
|
class MsgHandshake:
|
||||||
__slots__ = ('_timestamp', '_ephem_pk', '_ct', '_mac')
|
__slots__ = ("_timestamp", "_ephem_pk", "_ct", "_mac")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def encode_aad(self): # Additional Authenticated Data
|
def encode_aad(self): # Additional Authenticated Data
|
||||||
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
|
return (
|
||||||
self._timestamp.to_bytes(8, 'big') + \
|
int(NetMessageTypes.HANDSHAKE).to_bytes(2, "big")
|
||||||
self._ephem_pk
|
+ self._timestamp.to_bytes(8, "big")
|
||||||
|
+ self._ephem_pk
|
||||||
|
)
|
||||||
|
|
||||||
def encode(self):
|
def encode(self):
|
||||||
return self.encode_aad() + self._ct + self._mac
|
return self.encode_aad() + self._ct + self._mac
|
||||||
|
|
||||||
def decode(self, msg_mv):
|
def decode(self, msg_mv):
|
||||||
o = 2
|
o = 2
|
||||||
self._timestamp = int.from_bytes(msg_mv[o: o + 8], 'big')
|
self._timestamp = int.from_bytes(msg_mv[o : o + 8], "big")
|
||||||
o += 8
|
o += 8
|
||||||
self._ephem_pk = bytes(msg_mv[o : o + 33])
|
self._ephem_pk = bytes(msg_mv[o : o + 33])
|
||||||
o += 33
|
o += 33
|
||||||
@@ -101,11 +104,31 @@ class MsgHandshake:
|
|||||||
|
|
||||||
class Peer:
|
class Peer:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'_mx', '_pubkey', '_address', '_socket', '_version', '_ready', '_incoming',
|
"_mx",
|
||||||
'_connected_at', '_last_received_at', '_bytes_sent', '_bytes_received',
|
"_pubkey",
|
||||||
'_receiving_length', '_receiving_buffer', '_recv_messages', '_misbehaving_score',
|
"_address",
|
||||||
'_ke', '_km', '_dir', '_sent_nonce', '_recv_nonce', '_last_handshake_at',
|
"_socket",
|
||||||
'_ping_nonce', '_last_ping_at', '_last_ping_rtt')
|
"_version",
|
||||||
|
"_ready",
|
||||||
|
"_incoming",
|
||||||
|
"_connected_at",
|
||||||
|
"_last_received_at",
|
||||||
|
"_bytes_sent",
|
||||||
|
"_bytes_received",
|
||||||
|
"_receiving_length",
|
||||||
|
"_receiving_buffer",
|
||||||
|
"_recv_messages",
|
||||||
|
"_misbehaving_score",
|
||||||
|
"_ke",
|
||||||
|
"_km",
|
||||||
|
"_dir",
|
||||||
|
"_sent_nonce",
|
||||||
|
"_recv_nonce",
|
||||||
|
"_last_handshake_at",
|
||||||
|
"_ping_nonce",
|
||||||
|
"_last_ping_at",
|
||||||
|
"_last_ping_rtt",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, address, socket, pubkey):
|
def __init__(self, address, socket, pubkey):
|
||||||
self._mx = threading.Lock()
|
self._mx = threading.Lock()
|
||||||
@@ -141,14 +164,16 @@ def listen_thread(cls):
|
|||||||
max_bytes = 0x10000
|
max_bytes = 0x10000
|
||||||
while cls._running:
|
while cls._running:
|
||||||
# logging.info('[rm] network loop %d', cls._running)
|
# logging.info('[rm] network loop %d', cls._running)
|
||||||
readable, writable, errored = select.select(cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout)
|
readable, writable, errored = select.select(
|
||||||
|
cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout
|
||||||
|
)
|
||||||
cls._mx.acquire()
|
cls._mx.acquire()
|
||||||
try:
|
try:
|
||||||
disconnected_peers = []
|
disconnected_peers = []
|
||||||
for s in readable:
|
for s in readable:
|
||||||
if s == cls._socket:
|
if s == cls._socket:
|
||||||
peer_socket, address = cls._socket.accept()
|
peer_socket, address = cls._socket.accept()
|
||||||
logging.info('Connection from %s', address)
|
logging.info("Connection from %s", address)
|
||||||
new_peer = Peer(address, peer_socket, None)
|
new_peer = Peer(address, peer_socket, None)
|
||||||
new_peer._incoming = True
|
new_peer._incoming = True
|
||||||
cls._peers.append(new_peer)
|
cls._peers.append(new_peer)
|
||||||
@@ -161,11 +186,11 @@ def listen_thread(cls):
|
|||||||
bytes_recv = s.recv(max_bytes, socket.MSG_DONTWAIT)
|
bytes_recv = s.recv(max_bytes, socket.MSG_DONTWAIT)
|
||||||
except socket.error as se:
|
except socket.error as se:
|
||||||
if se.args[0] not in (socket.EWOULDBLOCK,):
|
if se.args[0] not in (socket.EWOULDBLOCK,):
|
||||||
logging.error('Receive error %s', str(se))
|
logging.error("Receive error %s", str(se))
|
||||||
disconnected_peers.append(peer)
|
disconnected_peers.append(peer)
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('Receive error %s', str(e))
|
logging.error("Receive error %s", str(e))
|
||||||
disconnected_peers.append(peer)
|
disconnected_peers.append(peer)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -175,7 +200,7 @@ def listen_thread(cls):
|
|||||||
cls.receive_bytes(peer, bytes_recv)
|
cls.receive_bytes(peer, bytes_recv)
|
||||||
|
|
||||||
for s in errored:
|
for s in errored:
|
||||||
logging.warning('Socket error')
|
logging.warning("Socket error")
|
||||||
|
|
||||||
for peer in disconnected_peers:
|
for peer in disconnected_peers:
|
||||||
cls.disconnect(peer)
|
cls.disconnect(peer)
|
||||||
@@ -193,7 +218,9 @@ def msg_thread(cls):
|
|||||||
try:
|
try:
|
||||||
now_us = time.time_ns() // 1000
|
now_us = time.time_ns() // 1000
|
||||||
if peer._ready is True:
|
if peer._ready is True:
|
||||||
if now_us - peer._last_ping_at >= 5000000: # 5 seconds TODO: Make variable
|
if (
|
||||||
|
now_us - peer._last_ping_at >= 5000000
|
||||||
|
): # 5 seconds TODO: Make variable
|
||||||
cls.send_ping(peer)
|
cls.send_ping(peer)
|
||||||
msg = peer._recv_messages.get(False)
|
msg = peer._recv_messages.get(False)
|
||||||
cls.process_message(peer, msg)
|
cls.process_message(peer, msg)
|
||||||
@@ -201,7 +228,7 @@ def msg_thread(cls):
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning('process message error %s', str(e))
|
logging.warning("process message error %s", str(e))
|
||||||
if cls._sc.debug:
|
if cls._sc.debug:
|
||||||
logging.error(traceback.format_exc())
|
logging.error(traceback.format_exc())
|
||||||
|
|
||||||
@@ -211,9 +238,24 @@ def msg_thread(cls):
|
|||||||
|
|
||||||
class Network:
|
class Network:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'_p2p_host', '_p2p_port', '_network_key', '_network_pubkey',
|
"_p2p_host",
|
||||||
'_sc', '_peers', '_max_connections', '_running', '_network_thread', '_msg_thread',
|
"_p2p_port",
|
||||||
'_mx', '_socket', '_read_sockets', '_write_sockets', '_error_sockets', '_csprng', '_seen_ephem_keys')
|
"_network_key",
|
||||||
|
"_network_pubkey",
|
||||||
|
"_sc",
|
||||||
|
"_peers",
|
||||||
|
"_max_connections",
|
||||||
|
"_running",
|
||||||
|
"_network_thread",
|
||||||
|
"_msg_thread",
|
||||||
|
"_mx",
|
||||||
|
"_socket",
|
||||||
|
"_read_sockets",
|
||||||
|
"_write_sockets",
|
||||||
|
"_error_sockets",
|
||||||
|
"_csprng",
|
||||||
|
"_seen_ephem_keys",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, p2p_host, p2p_port, network_key, swap_client):
|
def __init__(self, p2p_host, p2p_port, network_key, swap_client):
|
||||||
self._p2p_host = p2p_host
|
self._p2p_host = p2p_host
|
||||||
@@ -278,7 +320,13 @@ class Network:
|
|||||||
self._mx.release()
|
self._mx.release()
|
||||||
|
|
||||||
def add_connection(self, host, port, peer_pubkey):
|
def add_connection(self, host, port, peer_pubkey):
|
||||||
self._sc.log.info('Connecting from %s to %s at %s %d', self._network_pubkey.hex(), peer_pubkey.hex(), host, port)
|
self._sc.log.info(
|
||||||
|
"Connecting from %s to %s at %s %d",
|
||||||
|
self._network_pubkey.hex(),
|
||||||
|
peer_pubkey.hex(),
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
)
|
||||||
self._mx.acquire()
|
self._mx.acquire()
|
||||||
try:
|
try:
|
||||||
address = (host, port)
|
address = (host, port)
|
||||||
@@ -294,7 +342,7 @@ class Network:
|
|||||||
self.send_handshake(peer)
|
self.send_handshake(peer)
|
||||||
|
|
||||||
def disconnect(self, peer):
|
def disconnect(self, peer):
|
||||||
self._sc.log.info('Closing peer socket %s', peer._address)
|
self._sc.log.info("Closing peer socket %s", peer._address)
|
||||||
self._read_sockets.pop(self._read_sockets.index(peer._socket))
|
self._read_sockets.pop(self._read_sockets.index(peer._socket))
|
||||||
self._error_sockets.pop(self._error_sockets.index(peer._socket))
|
self._error_sockets.pop(self._error_sockets.index(peer._socket))
|
||||||
peer.close()
|
peer.close()
|
||||||
@@ -305,7 +353,11 @@ class Network:
|
|||||||
|
|
||||||
used = self._seen_ephem_keys.get(ephem_pk)
|
used = self._seen_ephem_keys.get(ephem_pk)
|
||||||
if used:
|
if used:
|
||||||
raise ValueError('Handshake ephem_pk reused %s peer %s', 'for' if direction == 1 else 'by', used[0])
|
raise ValueError(
|
||||||
|
"Handshake ephem_pk reused %s peer %s",
|
||||||
|
"for" if direction == 1 else "by",
|
||||||
|
used[0],
|
||||||
|
)
|
||||||
|
|
||||||
self._seen_ephem_keys[ephem_pk] = (peer._address, timestamp)
|
self._seen_ephem_keys[ephem_pk] = (peer._address, timestamp)
|
||||||
|
|
||||||
@@ -313,12 +365,14 @@ class Network:
|
|||||||
self._seen_ephem_keys.popitem(last=False)
|
self._seen_ephem_keys.popitem(last=False)
|
||||||
|
|
||||||
def send_handshake(self, peer):
|
def send_handshake(self, peer):
|
||||||
self._sc.log.debug('send_handshake %s', peer._address)
|
self._sc.log.debug("send_handshake %s", peer._address)
|
||||||
peer._mx.acquire()
|
peer._mx.acquire()
|
||||||
try:
|
try:
|
||||||
# TODO: Drain peer._recv_messages
|
# TODO: Drain peer._recv_messages
|
||||||
if not peer._recv_messages.empty():
|
if not peer._recv_messages.empty():
|
||||||
self._sc.log.warning('send_handshake %s - Receive queue dumped.', peer._address)
|
self._sc.log.warning(
|
||||||
|
"send_handshake %s - Receive queue dumped.", peer._address
|
||||||
|
)
|
||||||
while not peer._recv_messages.empty():
|
while not peer._recv_messages.empty():
|
||||||
peer._recv_messages.get(False)
|
peer._recv_messages.get(False)
|
||||||
|
|
||||||
@@ -332,7 +386,7 @@ class Network:
|
|||||||
|
|
||||||
ss = k.ecdh(peer._pubkey)
|
ss = k.ecdh(peer._pubkey)
|
||||||
|
|
||||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
|
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
|
||||||
peer._ke = hashed[:32]
|
peer._ke = hashed[:32]
|
||||||
peer._km = hashed[32:]
|
peer._km = hashed[32:]
|
||||||
|
|
||||||
@@ -361,11 +415,13 @@ class Network:
|
|||||||
peer._mx.release()
|
peer._mx.release()
|
||||||
|
|
||||||
def process_handshake(self, peer, msg_mv):
|
def process_handshake(self, peer, msg_mv):
|
||||||
self._sc.log.debug('process_handshake %s', peer._address)
|
self._sc.log.debug("process_handshake %s", peer._address)
|
||||||
|
|
||||||
# TODO: Drain peer._recv_messages
|
# TODO: Drain peer._recv_messages
|
||||||
if not peer._recv_messages.empty():
|
if not peer._recv_messages.empty():
|
||||||
self._sc.log.warning('process_handshake %s - Receive queue dumped.', peer._address)
|
self._sc.log.warning(
|
||||||
|
"process_handshake %s - Receive queue dumped.", peer._address
|
||||||
|
)
|
||||||
while not peer._recv_messages.empty():
|
while not peer._recv_messages.empty():
|
||||||
peer._recv_messages.get(False)
|
peer._recv_messages.get(False)
|
||||||
|
|
||||||
@@ -375,17 +431,19 @@ class Network:
|
|||||||
try:
|
try:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
if now - peer._last_handshake_at < 30:
|
if now - peer._last_handshake_at < 30:
|
||||||
raise ValueError('Too many handshakes from peer %s', peer._address)
|
raise ValueError("Too many handshakes from peer %s", peer._address)
|
||||||
|
|
||||||
if abs(msg._timestamp - now) > TIMESTAMP_LEEWAY:
|
if abs(msg._timestamp - now) > TIMESTAMP_LEEWAY:
|
||||||
raise ValueError('Bad handshake timestamp from peer %s', peer._address)
|
raise ValueError("Bad handshake timestamp from peer %s", peer._address)
|
||||||
|
|
||||||
self.check_handshake_ephem_key(peer, msg._timestamp, msg._ephem_pk, direction=2)
|
self.check_handshake_ephem_key(
|
||||||
|
peer, msg._timestamp, msg._ephem_pk, direction=2
|
||||||
|
)
|
||||||
|
|
||||||
nk = PrivateKey(self._network_key)
|
nk = PrivateKey(self._network_key)
|
||||||
ss = nk.ecdh(msg._ephem_pk)
|
ss = nk.ecdh(msg._ephem_pk)
|
||||||
|
|
||||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
|
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
|
||||||
peer._ke = hashed[:32]
|
peer._ke = hashed[:32]
|
||||||
peer._km = hashed[32:]
|
peer._km = hashed[32:]
|
||||||
|
|
||||||
@@ -395,7 +453,9 @@ class Network:
|
|||||||
aad += nonce
|
aad += nonce
|
||||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||||
cipher.update(aad)
|
cipher.update(aad)
|
||||||
plaintext = cipher.decrypt_and_verify(msg._ct, msg._mac) # Will raise error if mac doesn't match
|
plaintext = cipher.decrypt_and_verify(
|
||||||
|
msg._ct, msg._mac
|
||||||
|
) # Will raise error if mac doesn't match
|
||||||
|
|
||||||
peer._version = plaintext[:6]
|
peer._version = plaintext[:6]
|
||||||
sig = plaintext[6:]
|
sig = plaintext[6:]
|
||||||
@@ -414,7 +474,7 @@ class Network:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: misbehaving
|
# TODO: misbehaving
|
||||||
self._sc.log.debug('[rm] process_handshake %s', str(e))
|
self._sc.log.debug("[rm] process_handshake %s", str(e))
|
||||||
|
|
||||||
def process_ping(self, peer, msg_mv):
|
def process_ping(self, peer, msg_mv):
|
||||||
nonce = peer._recv_nonce[:24]
|
nonce = peer._recv_nonce[:24]
|
||||||
@@ -426,14 +486,18 @@ class Network:
|
|||||||
mac = msg_mv[-16:]
|
mac = msg_mv[-16:]
|
||||||
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
||||||
|
|
||||||
ping_nonce = int.from_bytes(plaintext[:4], 'big')
|
ping_nonce = int.from_bytes(plaintext[:4], "big")
|
||||||
# Version is added to a ping following a handshake message
|
# Version is added to a ping following a handshake message
|
||||||
if len(plaintext) >= 10:
|
if len(plaintext) >= 10:
|
||||||
peer._ready = True
|
peer._ready = True
|
||||||
version = plaintext[4:10]
|
version = plaintext[4:10]
|
||||||
if peer._version is None:
|
if peer._version is None:
|
||||||
peer._version = version
|
peer._version = version
|
||||||
self._sc.log.debug('Set version from ping %s, %s', peer._pubkey.hex(), peer._version.hex())
|
self._sc.log.debug(
|
||||||
|
"Set version from ping %s, %s",
|
||||||
|
peer._pubkey.hex(),
|
||||||
|
peer._version.hex(),
|
||||||
|
)
|
||||||
|
|
||||||
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
||||||
|
|
||||||
@@ -449,26 +513,26 @@ class Network:
|
|||||||
mac = msg_mv[-16:]
|
mac = msg_mv[-16:]
|
||||||
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
||||||
|
|
||||||
pong_nonce = int.from_bytes(plaintext[:4], 'big')
|
pong_nonce = int.from_bytes(plaintext[:4], "big")
|
||||||
|
|
||||||
if pong_nonce == peer._ping_nonce:
|
if pong_nonce == peer._ping_nonce:
|
||||||
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
|
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
|
||||||
else:
|
else:
|
||||||
self._sc.log.debug('Pong received out of order %s', peer._address)
|
self._sc.log.debug("Pong received out of order %s", peer._address)
|
||||||
|
|
||||||
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
||||||
|
|
||||||
def send_ping(self, peer):
|
def send_ping(self, peer):
|
||||||
ping_nonce = random.getrandbits(32)
|
ping_nonce = random.getrandbits(32)
|
||||||
|
|
||||||
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, 'big')
|
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, "big")
|
||||||
nonce = peer._sent_nonce[:24]
|
nonce = peer._sent_nonce[:24]
|
||||||
|
|
||||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||||
cipher.update(msg_bytes)
|
cipher.update(msg_bytes)
|
||||||
cipher.update(nonce)
|
cipher.update(nonce)
|
||||||
|
|
||||||
payload = ping_nonce.to_bytes(4, 'big')
|
payload = ping_nonce.to_bytes(4, "big")
|
||||||
if peer._last_ping_at == 0:
|
if peer._last_ping_at == 0:
|
||||||
payload += self._sc._version
|
payload += self._sc._version
|
||||||
ct, mac = cipher.encrypt_and_digest(payload)
|
ct, mac = cipher.encrypt_and_digest(payload)
|
||||||
@@ -483,14 +547,14 @@ class Network:
|
|||||||
self.send_msg(peer, msg_bytes)
|
self.send_msg(peer, msg_bytes)
|
||||||
|
|
||||||
def send_pong(self, peer, ping_nonce):
|
def send_pong(self, peer, ping_nonce):
|
||||||
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, 'big')
|
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, "big")
|
||||||
nonce = peer._sent_nonce[:24]
|
nonce = peer._sent_nonce[:24]
|
||||||
|
|
||||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||||
cipher.update(msg_bytes)
|
cipher.update(msg_bytes)
|
||||||
cipher.update(nonce)
|
cipher.update(nonce)
|
||||||
|
|
||||||
payload = ping_nonce.to_bytes(4, 'big')
|
payload = ping_nonce.to_bytes(4, "big")
|
||||||
ct, mac = cipher.encrypt_and_digest(payload)
|
ct, mac = cipher.encrypt_and_digest(payload)
|
||||||
msg_bytes += ct + mac
|
msg_bytes += ct + mac
|
||||||
|
|
||||||
@@ -502,19 +566,21 @@ class Network:
|
|||||||
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
|
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
|
||||||
len_encoded = len(msg_encoded)
|
len_encoded = len(msg_encoded)
|
||||||
|
|
||||||
msg_packed = bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, 'big') + msg_encoded
|
msg_packed = (
|
||||||
|
bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, "big") + msg_encoded
|
||||||
|
)
|
||||||
peer._socket.sendall(msg_packed)
|
peer._socket.sendall(msg_packed)
|
||||||
|
|
||||||
peer._bytes_sent += len_encoded
|
peer._bytes_sent += len_encoded
|
||||||
|
|
||||||
def process_message(self, peer, msg_bytes):
|
def process_message(self, peer, msg_bytes):
|
||||||
logging.info('[rm] process_message %s len %d', peer._address, len(msg_bytes))
|
logging.info("[rm] process_message %s len %d", peer._address, len(msg_bytes))
|
||||||
|
|
||||||
peer._mx.acquire()
|
peer._mx.acquire()
|
||||||
try:
|
try:
|
||||||
mv = memoryview(msg_bytes)
|
mv = memoryview(msg_bytes)
|
||||||
o = 0
|
o = 0
|
||||||
msg_type = int.from_bytes(mv[o: o + 2], 'big')
|
msg_type = int.from_bytes(mv[o : o + 2], "big")
|
||||||
if msg_type == NetMessageTypes.HANDSHAKE:
|
if msg_type == NetMessageTypes.HANDSHAKE:
|
||||||
self.process_handshake(peer, mv)
|
self.process_handshake(peer, mv)
|
||||||
elif msg_type == NetMessageTypes.PING:
|
elif msg_type == NetMessageTypes.PING:
|
||||||
@@ -522,7 +588,7 @@ class Network:
|
|||||||
elif msg_type == NetMessageTypes.PONG:
|
elif msg_type == NetMessageTypes.PONG:
|
||||||
self.process_pong(peer, mv)
|
self.process_pong(peer, mv)
|
||||||
else:
|
else:
|
||||||
self._sc.log.debug('Unknown message type %d', msg_type)
|
self._sc.log.debug("Unknown message type %d", msg_type)
|
||||||
finally:
|
finally:
|
||||||
peer._mx.release()
|
peer._mx.release()
|
||||||
|
|
||||||
@@ -533,7 +599,6 @@ class Network:
|
|||||||
peer._last_received_at = time.time()
|
peer._last_received_at = time.time()
|
||||||
peer._bytes_received += len_received
|
peer._bytes_received += len_received
|
||||||
|
|
||||||
invalid_msg = False
|
|
||||||
mv = memoryview(bytes_recv)
|
mv = memoryview(bytes_recv)
|
||||||
|
|
||||||
o = 0
|
o = 0
|
||||||
@@ -541,32 +606,32 @@ class Network:
|
|||||||
while o < len_received:
|
while o < len_received:
|
||||||
if peer._receiving_length == 0:
|
if peer._receiving_length == 0:
|
||||||
if len(bytes_recv) < MSG_HEADER_LEN:
|
if len(bytes_recv) < MSG_HEADER_LEN:
|
||||||
raise ValueError('Msg too short')
|
raise ValueError("Msg too short")
|
||||||
|
|
||||||
if mv[o : o + 2] != MSG_START_TOKEN:
|
if mv[o : o + 2] != MSG_START_TOKEN:
|
||||||
raise ValueError('Invalid start token')
|
raise ValueError("Invalid start token")
|
||||||
o += 2
|
o += 2
|
||||||
|
|
||||||
msg_len = int.from_bytes(mv[o: o + 4], 'big')
|
msg_len = int.from_bytes(mv[o : o + 4], "big")
|
||||||
o += 4
|
o += 4
|
||||||
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
|
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
|
||||||
raise ValueError('Invalid data length')
|
raise ValueError("Invalid data length")
|
||||||
|
|
||||||
# Precheck msg_type
|
# Precheck msg_type
|
||||||
msg_type = int.from_bytes(mv[o: o + 2], 'big')
|
msg_type = int.from_bytes(mv[o : o + 2], "big")
|
||||||
# o += 2 # Don't inc offset, msg includes type
|
# o += 2 # Don't inc offset, msg includes type
|
||||||
if not NetMessageTypes.has_value(msg_type):
|
if not NetMessageTypes.has_value(msg_type):
|
||||||
raise ValueError('Invalid msg type')
|
raise ValueError("Invalid msg type")
|
||||||
|
|
||||||
peer._receiving_length = msg_len
|
peer._receiving_length = msg_len
|
||||||
len_pkt = (len_received - o)
|
len_pkt = len_received - o
|
||||||
nc = msg_len if len_pkt > msg_len else len_pkt
|
nc = msg_len if len_pkt > msg_len else len_pkt
|
||||||
|
|
||||||
peer._receiving_buffer = mv[o : o + nc]
|
peer._receiving_buffer = mv[o : o + nc]
|
||||||
o += nc
|
o += nc
|
||||||
else:
|
else:
|
||||||
len_to_go = peer._receiving_length - len(peer._receiving_buffer)
|
len_to_go = peer._receiving_length - len(peer._receiving_buffer)
|
||||||
len_pkt = (len_received - o)
|
len_pkt = len_received - o
|
||||||
nc = len_to_go if len_pkt > len_to_go else len_pkt
|
nc = len_to_go if len_pkt > len_to_go else len_pkt
|
||||||
peer._receiving_buffer = mv[o : o + nc]
|
peer._receiving_buffer = mv[o : o + nc]
|
||||||
o += nc
|
o += nc
|
||||||
@@ -576,11 +641,13 @@ class Network:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self._sc.debug:
|
if self._sc.debug:
|
||||||
self._sc.log.error('Invalid message received from %s %s', peer._address, str(e))
|
self._sc.log.error(
|
||||||
|
"Invalid message received from %s %s", peer._address, str(e)
|
||||||
|
)
|
||||||
# TODO: misbehaving
|
# TODO: misbehaving
|
||||||
|
|
||||||
def test_onion(self, path):
|
def test_onion(self, path):
|
||||||
self._sc.log.debug('test_onion packet')
|
self._sc.log.debug("test_onion packet")
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
rv = {}
|
rv = {}
|
||||||
@@ -589,14 +656,14 @@ class Network:
|
|||||||
with self._mx:
|
with self._mx:
|
||||||
for peer in self._peers:
|
for peer in self._peers:
|
||||||
peer_info = {
|
peer_info = {
|
||||||
'pubkey': 'Unknown' if not peer._pubkey else peer._pubkey.hex(),
|
"pubkey": "Unknown" if not peer._pubkey else peer._pubkey.hex(),
|
||||||
'address': '{}:{}'.format(peer._address[0], peer._address[1]),
|
"address": "{}:{}".format(peer._address[0], peer._address[1]),
|
||||||
'bytessent': peer._bytes_sent,
|
"bytessent": peer._bytes_sent,
|
||||||
'bytesrecv': peer._bytes_received,
|
"bytesrecv": peer._bytes_received,
|
||||||
'ready': peer._ready,
|
"ready": peer._ready,
|
||||||
'incoming': peer._incoming,
|
"incoming": peer._incoming,
|
||||||
}
|
}
|
||||||
peers.append(peer_info)
|
peers.append(peer_info)
|
||||||
|
|
||||||
rv['peers'] = peers
|
rv["peers"] = peers
|
||||||
return rv
|
return rv
|
||||||
|
|||||||
1257
basicswap/pgp/keys/bitcoin_laanwj.pgp
Normal file
166
basicswap/pgp/keys/dash_pasta.pgp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBF1ULyUBEADFFliU0Hr+PRCQNT9/9ZEhZtLmMMu7tai3VCxhmrHrOpNJJHqX
|
||||||
|
f1/CeUyBhmCvXpKIpAAbH66l/Uc9GH5UgMZ19gMyGa3q3QJn9A6RR9ud4ALRg60P
|
||||||
|
fmYTAci+6Luko7bqTzkS+fYOUSy/LY57s5ANTpveE+iTsBd5grXczCxaYYnthKKA
|
||||||
|
ecmTs8GzQH8XEUgy6fduHcGySzMBj87daZBmPl2zninbTmOYkzev38HXFpr6KinJ
|
||||||
|
t3vRkhw4AOMSdgaTiNr6gALKoKLyCbhvHuDsVoDBQtIzBXtOeIGyzwBFdHlN2bFG
|
||||||
|
CcH2vWOzg/Yp1qYleWWV7KYHOVKcxrIycPM0tNueLlvrqVrI59QXMVRJHtBs8eQg
|
||||||
|
dH9rZNbO0vuv6rCP7e0nt2ACVT/fExdvrwuHHYZ/7IlwOBlFhab3QYpl/WWep2+X
|
||||||
|
95BSbDOXFrLWwEE9gND+douDG1DExVa3aSNXQJdi4/Mh7bMFiq2FsbXqu+TFSCTg
|
||||||
|
ae33WKl/AOmHVirgtipnq70PW9hHViaSg3rz0NyYHHczNVaCROHE8YdIM/bAmKY/
|
||||||
|
IYVBXJtT+6Mn8N87isK2TR7zMM3FvDJ4Dsqm1UTGwtDvMtB0sNa5IROaUCHdlMFu
|
||||||
|
rG8n+Bq/oGBFjk9Ay/twH4uOpxyr91aGoGtytw/jhd1+LOb0TGhFGpdc8QARAQAB
|
||||||
|
tBtQYXN0YSA8cGFzdGFAZGFzaGJvb3N0Lm9yZz6JAlQEEwEIAD4WIQQpWQNi7IeK
|
||||||
|
gf08ICtSUnvtq+h5hAUCXVQvJQIbAwUJA8PHawULCQgHAgYVCgkICwIEFgIDAQIe
|
||||||
|
AQIXgAAKCRBSUnvtq+h5hMqeEACQteY571XK50dW1oQzjgPq5tVuchoRQI727pr7
|
||||||
|
5145o2rOe0e0xrWzVNnhd9ZDzC4j8dh6wWVQWErHr+3Hhn8sCUW2PNU+o3GvhGR6
|
||||||
|
aqPl0Oh5gt4wHZalrcUnZ5u/RtFbDmGilobdASL/mpZge8ymLBj2lKiRR2X/JQe/
|
||||||
|
KAzr/7QW1zLh2oEUOOGVas6Ev+ziosAE0b3upGTHJFPQPMFv4za22MbeTKYeqyJ6
|
||||||
|
W6LdQDDssC/RBQKZXj3pRweA6RQFGOqw44CbtIHuQu/PV8ZDTpE+v9cWAzoNCMcQ
|
||||||
|
2fm5tCM8zYytt3perbA3VPwZNXcsITcRpIS5FgoeOntgIwzzKVmY+4GD8uWM/DHt
|
||||||
|
JPxyry7LpSa8CNyx+oN+Z2qCChn03ycJzO3UFsaCMG/CMAEkLxbg0AcxNyQ8kvIG
|
||||||
|
lcEDLINaz1xuHAtAxqTQKMYCP1xtd5rhGOe1FkGfVYEJX97+JgMGa8+2nD5+A6wG
|
||||||
|
0+JaJllqzfXY1VhNoVmfS/hFPQ+t/84jNSGR5Kn956C5MvTK65VumH+NRE59kpt1
|
||||||
|
nsIQNKu/v6fZUnbRtCFC05BSwIjoTzFvKXycJkCVjdSYARWkagki4bbFC1WZQuA9
|
||||||
|
BOF5TOUAYt6zaEBfAJgjeRT71Mr03eNExXaLm9k/hmvapGpmtJQhLY6NKPm/ctyf
|
||||||
|
IaEz/YkCVwQTAQgAQQIbAwIXgAUJDS2jLwULCQgHAgYVCgkICwIEFgIDAQIeBRYh
|
||||||
|
BClZA2Lsh4qB/TwgK1JSe+2r6HmEBQJlrVMsAhkBAAoJEFJSe+2r6HmE0KcP/2EG
|
||||||
|
b4CWvsmn3q6NoBmZ+u+rCitaX33+kXc4US6vRvAfhe0YiOWr5tNd4lg2JID+6jsN
|
||||||
|
2NkAZYgzm4TXXJLkjXkrB+s0sFkCjyG1/wBfZlPUSfxoDFusJry87N/7E9yMX7A+
|
||||||
|
YV2Hh/yOXbR+/jSINfmjC+3ttjWDUsUWT9m1yN8SBNg6h66TLffFyXgGFkRKYE27
|
||||||
|
eprP0cuVkI6Fks68ocSQ5FQ7gmdMCC4JFtOI4e1ax6mfvTFz2e2f5DlohPjW9w4e
|
||||||
|
KTn+k98Nuev+s3WGiDXjxSABoehAdwz2mbEjPsuz0jLeYKn6ialHh+hruYZozx8d
|
||||||
|
xpUIWEVlMwLDBteWCuwTp+XPmOvaKkgYLxkfjjeIqUy17f6py17GrDZFHLeiopcJ
|
||||||
|
qyQJ0XLQI/qAKXkySBpvGD86nrM1i+5X7nLxZ0YfjKQ7cI+fp5A6SsQPUk9SI95P
|
||||||
|
XRssx481zNse5wxFMP8J9oIB6nger39lpRRmvaSUJDNWjfsRZ/XK4mfib2OlLXoo
|
||||||
|
WuU5lCwqtQ+Jw9Zr/Gby2kTNIjrfIpdNyThTnth+uTwcA8KCJRJY2BrPBtWNWqPL
|
||||||
|
xLv9RLR3/N1siyJcichExIBKEzOhzzi/i/PTU8dK2OBXrSaJ8DXhPwyNTB2l7jnX
|
||||||
|
BO0hxeO4gmzAFQpM7QXXVDguL0b594y05UNOM/ljiQIcBBMBAgAGBQJeut/oAAoJ
|
||||||
|
ECqAP87D6bin7ZMP/3be6BDv/zf0gCTmgjD6StvPHu+F17op4VPj2cHYCgFP1ZHF
|
||||||
|
H2RjqRVhSN6Wk+hbmR5PDHoVA2ncxITv/DddKRjYc7fPRlrje7H19+urJgqqkWzm
|
||||||
|
uUbNlxKiXiVW/OPmCjjI89Okt3dZGCTicEAPzJ6LTpoVgo4n/Eu81nMm6caf++Pz
|
||||||
|
z1vEI3bJdPHPYyI+gN64mEhfP4OJu8v2XTbj+0ua3JxYWilxF7haytApmaPqeT7u
|
||||||
|
OEBrX7EV1M+DlQCSM61u2EC5eIwAoDba/ENXNyg5Z1JbFe3DxqE6ZVcAcZWXGdtP
|
||||||
|
otayuEy6WL3LB2UUsM4UB4FPSUwcFvnkV8YzBSV8Rqx+mkOFM6BhxzwK0zPvY+vv
|
||||||
|
+rXSwz7uE/yrToqO9KvGhFxMwMwzTRAJXI870fJQ9c5z2LzxoNg5gOUQH4vPG6YQ
|
||||||
|
T1ev04fj7IGYch9EhrSjuLCm94BApOEA+h/TTN6+xVLemUSB/l+Obm5701PP/naV
|
||||||
|
prCJcCqIU3tH5HU3BXpZH++AzWo0pmgbtd7ECsR/y0NR4Mxoef677q9YGJEG/psY
|
||||||
|
C0GZlzWsY5zjala+bEVn5gvbw6Lh4Q2gwpvVXdygb6PSPwRSkpgHtUxdvIQsDEaB
|
||||||
|
BGg/ae0x3O55z2/z95acnhIMRqQpUpnPmDZUBKlsDJ8tivw/2r8o16YtAlJ0iQEz
|
||||||
|
BBABCAAdFiEEYKz3C/cSZFBJ7m8V7+rxZoYiX2QFAmWp9dIACgkQ7+rxZoYiX2St
|
||||||
|
Mwf8CdL0fhz2TM1R79n+FW7QCSaINBzIE1lN2TbdVEZeyiwQLn9cbqOvVPFavj4v
|
||||||
|
xWFIXfAYzitLDHkikmg5Qzj7OXB2plFnqJxZ1tZSC1EdMHuNX1j55FDAggV/U/yv
|
||||||
|
2PDY2XuwJbj/hLj80oNzIL5qLnNco0CLggB8QLLleFw4BTKycGDrzQCk4AGQ8tDR
|
||||||
|
NoyI6Q/oFQtWQgQdm9Cs02Myr51QZBe09XXA4wpyqv9BM+E0o8SLp/x/wZXM99vD
|
||||||
|
Na7Df0nsRIQukFy5HqJJTufP1b6QFVMY1ouweyLxABXO4cvtYpOAUwQroY4U/q9Z
|
||||||
|
nRzxj8Sq+reAt8O/wwJ8ujy9ILR8UGFzdGEgKFNlZSBrZXliYXNlLmlvL3Bhc3Rh
|
||||||
|
IGZvciBwcm9vZnMgb24gbXkgaWRlbnRpZnkuIDYwQUNGNzBCRjcxMjY0NTA0OUVF
|
||||||
|
NkYxNUVGRUFGMTY2ODYyMjVGNjQgaXMgbXkgb2ZmbGluZSBvbmx5IEdQRyBrZXku
|
||||||
|
KYkCVAQTAQgAPgIbAwUJDS2jLwIXgBYhBClZA2Lsh4qB/TwgK1JSe+2r6HmEBQJl
|
||||||
|
qf1lBQsJCAcCBhUKCQgLAgQWAgMBAh4FAAoJEFJSe+2r6HmEhQMP/jiIGD9/Zzwa
|
||||||
|
GeBtrCD46WNT7Gxs9g/Lo+OsHqKzieN/H8EW61uS0kmkP7kKJdJHnpL7e8Q280OC
|
||||||
|
+YxV5YMG4byHmtOSvAbDNCTG8Eg3C7QW79ECIZaJldp5Bv6yrbwqsJyeDNfR61Zq
|
||||||
|
6lyG2Atvgt6fKjeHpxnDUfr0a9DqfkN8DLADzy1srwWlwilSAzhGBRsS7OV6gsbi
|
||||||
|
ZrQ/4sh/ZNtf/4lo3X/vyhKStTjh9UEEJykwkDyV+Ih3htrUAjHkKl60wHUKobxB
|
||||||
|
Jhsarye+DmrN+FIrHfvywpuGv+Xp6EXxGlbzlTUtTaDFF9b71AuGDFOjprbDaNJA
|
||||||
|
recDj8WwxW9rwyrRH52TBAAtLJNkk7Yt7rruVocDgwJo0h9WP8OIzerZDn0sUNpN
|
||||||
|
OGtdnbWRkAVgSCgoFVgeRWX4UpT120vDTEuwkhp7r8MhNqE96LGpBBRUhk1tSrKl
|
||||||
|
+ewKgP1f/px+hO+0er9f+tTFP5vH9RQ3v+VpjzwVK2e2mez/nRwkdj0OVubUD0rU
|
||||||
|
cXiIt7rGNSSjGDvPKrRFsApYIGIfeDg9y/c0L0PCBqiZ6XEi46NEDYJGutg/ChbM
|
||||||
|
9wI3D1WLC3oKP4Z+2z96FyiOkvj7sYM23jAVii7YT18dpJSw6B7jV4FBpE7mrlFU
|
||||||
|
qBlsSJck6gb0qXkmfNTtgRP0/8De+8p9iQEzBBABCAAdFiEEYKz3C/cSZFBJ7m8V
|
||||||
|
7+rxZoYiX2QFAmWp9ocACgkQ7+rxZoYiX2SLEQf+MXqtD4WGMiGgKg9eaVCGMJn8
|
||||||
|
N+Y0nqxwpCVq6RAJGdjYcT4BCfNTwjdYKqBEPRfK5JP+VZ6RZ6nBfZxUTfzomWWF
|
||||||
|
L6M+A6A1+4Y8++SJvnSn+CqlvIOjFAUx37lf7KwXRDWKK9pmQn1+iZ0IwowXvRzl
|
||||||
|
DIfwlc5phTq7YUNZLgmytP1j0yhmdFHzaTUcq5waZIwIKDtaVORUyOCpUYc0sevz
|
||||||
|
Z3j1uLx8aWQXXfVYTQVNv1hmoarTZru0w0q5KTuJYyCX4quBjIutIoJ+N80OJ3SU
|
||||||
|
dAkCHFo4YEQAKubC/G7BHS4Q1btfqjkGF2kDX9e4amIQnrF3wcimESqi5xpn67QW
|
||||||
|
UGFzdGEgPHBhc3RhQGRhc2gub3JnPokCVAQTAQgAPgIbAwUJA8PHawIXgBYhBClZ
|
||||||
|
A2Lsh4qB/TwgK1JSe+2r6HmEBQJlqf1lBQsJCAcCBhUKCQgLAgQWAgMBAh4FAAoJ
|
||||||
|
EFJSe+2r6HmECFwQAIDwX6fe0y6bc42zNU3Sqtd+Q3OgZfW0Rg23viI1ujyJE1uk
|
||||||
|
mmGR0i0b2luM+lSw1xOpr+pEsRX0dfaqAbbyUVIgyIZ5viXDZyWyJXr7NuBQZalX
|
||||||
|
k4njNfAELnQN2MPy/dqpelb6/J+kn6q4TC4DN95bJtSzPLK16rI94sSO+XUAJaiU
|
||||||
|
pr++cUelALoa5yHBL0mGuhlkNgCNdTE0eVwBLRQDrAywcUOEb6f2eNHyK6UY7WLy
|
||||||
|
0/LZZv2SzG/ZNQEQNY15/vrDwsQvD1ZueY5haCRK0Ga5o3GWZACU/+/c4VL2Ew7K
|
||||||
|
odxAjhVHBz50wIe35DUKVkYOQDIx9y+e50CPJicKOsnwjpC+NzQCk462ixCO9DFI
|
||||||
|
+9AFTJ6TD2BxVRHxLyUY7J21Mes4EILKFAV2dAOSZnd6LgqiYzqovJl6FmaLJyRM
|
||||||
|
JEfqvTi6Vy38Ns/6PCVGJTWKVsKz2lDas6U3/71jS0FSEwEJ9Rv9Yo75uErypNlJ
|
||||||
|
MiEahwy7kxqs8BKLtuPrF6QKRB7RgWgVxxU7z92VKCBzKDD0Oe3CDu4Lfva0487d
|
||||||
|
+TwNIGJdDeJ+ywhhFXIoGmeRm1YZferx1u5PCphiDLVkDDlLEolbp3bxKnN+l4wC
|
||||||
|
OUvhabciX46H3sM6KGMSoDRjh5n0UPr2+67qBq/rNJRCkALEFrG46i/+mNrYiQEz
|
||||||
|
BBABCAAdFiEEYKz3C/cSZFBJ7m8V7+rxZoYiX2QFAmWp9dIACgkQ7+rxZoYiX2Se
|
||||||
|
cQf+IKiMpD8+D93HtmmwG0twBbPMOVta0NU90Gvjxkw/v/JIDEWlZECClUW6Se8Z
|
||||||
|
Icq+WRZeDP6UZharGAg2GfRpfrKIwVt/aP16LsCqq+SiP4xaohmpcXQxacS5u813
|
||||||
|
G9FFuxmHud3x7/sXtxKSVQRkhgQlq+RRG/s5CodNvjliM5OQiiXGr+q1tWy5QhRs
|
||||||
|
xCXj4CTc2CiV0ycWB36Cx9tkx+/s0pf7X4778wCrhzT6Ds5fT0W9uZifcglfI/p5
|
||||||
|
jYYQkGpOrnOiHkBU3F80iFowIGsiv8pfaSqBP8yBAOtNBSVo5ksqSaH+TpVeIb0/
|
||||||
|
pfGrM1BOzpTVfTmEj77qSE2tvrkCDQRdVC8lARAAu64IaLWAvMStxVsC/+NnwBBx
|
||||||
|
YPef4Iq5gB5P1NgkmkD+tyohWVnzdN/hwVDX3BAXevF8M+y6MouUA9IxJRt2W9PK
|
||||||
|
06ArTdwhFpiam2NAO5OOUhuJ1F8eAhRQ5VvI8MbVttZKSk3LiCmXGSj5UUXEFKS1
|
||||||
|
B7WztZVwqG6YswoAPwbNerZuwYbH2gfa9LK+av1cdZ8tnDaVmZWL8z1xSCyfRa/U
|
||||||
|
AtZht/CEoTvAwXJ6CxVUBngIlqVnK0KvOrNzol2m5x4NgPcdtdDlrTQE+SpqTKjy
|
||||||
|
roRe27D+atiO6pFG/TOTkx4TWXR07YTeZQJT/fntV409daIxEgShD0md7nJ7rVYy
|
||||||
|
8u+9Z4JLlt2mtnsUKHezo1Axrlri05cewPVYQLuJND/5e2X9UzSTpY3NubQAtkD1
|
||||||
|
PpM5JeCbslT9PcMnRuUydZbhn7ieW0b57uWpOpE11s2eIJ5ixSci4mSJE9kW+IcC
|
||||||
|
ic/PPoD1Rh2CvFTBPl/bsw6Bzw64LMflPjgWkR7NVQb1DETfXo5C2A/QU6Z/o7O4
|
||||||
|
JaAeAoGki/sCmeAi5W+F1kcjPk/L/TXM6ZccMytVQOECYBOYVUxZ2VbhknKOcSFQ
|
||||||
|
cpk8bj2xsD1xX2EYhkXcCQkvutIgHGz/dt6dtvcaaL85krWD/y8h68TTFjQXK0+g
|
||||||
|
8gcpexfqTMcLnF7pqEEAEQEAAYkCPAQYAQgAJhYhBClZA2Lsh4qB/TwgK1JSe+2r
|
||||||
|
6HmEBQJdVC8lAhsMBQkDw8drAAoJEFJSe+2r6HmEDzEP/A8H3JkeSa/03kWvudFl
|
||||||
|
oVbGbfvP+XkKvGnAZPGHz3ne/SV2tcXljNgU15xHvLktI4GluEfJxRPUqvUal1zO
|
||||||
|
R9hqpas0vX8gsf0r0d3om2DHCyMY8GscfDF05Y8fqf0nU5/oLDlwwp11IyW8BDLS
|
||||||
|
wwANsTLZ1ysukfYc4hoopU71/wdAl85fae7I2QRduImWlMADfUtc9Orfb1tAhPta
|
||||||
|
CJVZj5vgfUNSZOTUJ73RGbdL3Z2dc42lO3mRMyDkPdykkq0EgOo6zZLuHZQFhxTz
|
||||||
|
WIWeUT8vWNjpkdTeRHLvv3cwPRx1k1atrM+pE9YkhCg0EOMTcmN+FMekgnU+ee0c
|
||||||
|
ibn5wWOvE05zwRKYROx34va2U6TUU6KkV3fFuq3qqkXaiMFauhI1lSFGgccg7BCN
|
||||||
|
MhbBpOBkfGI3croFGSm2pTydJ87/+P9C9ecOZSqCE7Zt5IfDs/xV7DjxBK99Z5+R
|
||||||
|
GxtsIpNlxpsUvlMSsxUNhOWyiCKr6NIOfOzdLYDkhHcKMqWGmc1zC3HHHuZvX5u6
|
||||||
|
orTyYXWqc8X5p3Kh7Qjf/ChtN2P6SCOUQquEvpiY5J1TdmQSuoqHzg3ZrN+7EOKd
|
||||||
|
nUH7y1KB7iTvgQ07lcHnAMbkFDcpQA+tAMd99LVNSXh8urXhJ/AtxaJbNbCSvpkO
|
||||||
|
GB4WHLy/V+JdomFC9Pb3oPeiiQI8BBgBCAAmAhsMFiEEKVkDYuyHioH9PCArUlJ7
|
||||||
|
7avoeYQFAmEb0RAFCQ0to2sACgkQUlJ77avoeYRHuxAAigKlhF2q7RYOxcCIsA+z
|
||||||
|
Af4jJCCkpdOWwWhjqgjtbFrS/39/FoRSC9TClO2CU4j5FIAkPKdv7EFiAXaMIDur
|
||||||
|
tpN4Ps+l6wUX/tS+xaGDVseRoAdhVjp7ilG9WIvmV3UMqxge6hbam3H5JhiVlmS+
|
||||||
|
DAxG07dbHiFrdqeHrVZU/3649K8JOO9/xSs7Qzf6XJqepfzCjQ4ZRnGy4A/0hhYT
|
||||||
|
yzGeJOcTNigSjsPHl5PNipG0xbnAn7mxFm2i5XdVmTMCqsThkH6Ac3OBbLgRBvBh
|
||||||
|
VRWUR1Fbod7ypLTjOrXFW3Yvm7mtbZU8oqLKgcaACyXaIvwAoBY9dIXgrws6Z1dg
|
||||||
|
wvFH+1N7V2A+mVkbjPzS7Iko9lC1e5WBAJ7VkW20/5Ki08JXpLmd7UyglCcioQTM
|
||||||
|
d7YyE/Aho3zQbo/9A10REC4kOsl/Ou6IeEURa+mfb9MYPgoVGTcKZnaX0d40auRJ
|
||||||
|
ptosuoYLenXciRdUmfsADAb2pVdm5b2H3+NLXf+TnbyY/zm24ZFGPXBRSj7tQgaV
|
||||||
|
6kn9NPSg32Z1WcR+pAn3Jwqts3f1PNuYCrZvWv66NohJRrdCZc1wV4dkYvl2M1s+
|
||||||
|
zf8iTVti4IifNjn57slXtEsH36miQy2vN6Cp9I3A7m5WeL07i27P8bvhxOg9q6r3
|
||||||
|
NAgNcAK3mOfpQ/ej25jgI5y4MwRm9a42FgkrBgEEAdpHDwEBB0AqRGVWZSZaVkMJ
|
||||||
|
2QwXfknlrvSgrc8SagU0r0oDKsOsPIkCswQYAQgAJhYhBClZA2Lsh4qB/TwgK1JS
|
||||||
|
e+2r6HmEBQJm9a42AhsCBQkDwmcAAIEJEFJSe+2r6HmEdiAEGRYIAB0WIQQCuOfQ
|
||||||
|
AhZ8i0Ua8F/i89eRbnItOAUCZvWuNgAKCRDi89eRbnItOFVdAPwK6OXfnljdVrDx
|
||||||
|
akjecvA1HXCuRzzkyLPkTcYTCIqyXQD/aG664lvKWApb8z6DzPdi2ZGXvE4UgSYc
|
||||||
|
bFtju14RWguf7Q//TgaDjrbuPs6fbdXZdT/Glh2PbTtpJzY2QZQRnuXjn7nx6Nao
|
||||||
|
jBGMsQCHaI8kycmtZtU1uu1E4kEy5uzpXoRUJoZzHMOqntWxwpWoCypAKDrHsAJe
|
||||||
|
/JV/7PlPpqBsMdoCWbkj4THbgLwzkOPjWkvYIrbPNc/HmMIXXvUjBmgU6weG1mho
|
||||||
|
s7eHc+MhaNLT9L0m1AjnxN39EjwLVLu9K7KzTelJKIxQnXNM6IIH3PFcyTqR7b2e
|
||||||
|
E+Ds+J8H9DMfBnf7D6pl4M45IyvZlUzTPWNFddNcNEqVIlMCnyaSczjZVtPVmFfj
|
||||||
|
/b5zrQd+kWZEne3a5/JFkdnpyJW4yvRaqFUuLdypTJa4TklJ/z/lu1/x/DCbMmyB
|
||||||
|
XxChnOVwoqYyTiLD05VAD2+zoLZ630JC1i/BXl6vrhwGUJEcF7A1XDwPSQ4VFNwU
|
||||||
|
45dVVP+iMWYGjx5WlL/n/tmwXOT7TmhvXTsaYz0rlhEujrt//PTcIn0wLfHSPhbh
|
||||||
|
Dr34OnZdo366FkRGcMi/j1ViFRB7Z2bDaVGpI6zEXC2DqKcplYNFqXnlmqGp89/I
|
||||||
|
Yn9Ng1DdVbuZSaAITJ+cWyt/XQDwNpUSwe2H7FtJUyZs697I05wJdBqDgPOlWk+d
|
||||||
|
w7ITptFnGG93750xYBA1k9T0OYpNwJB8IZDIRaIJ1G16qe19PfNcHyK1PbS4MwRm
|
||||||
|
9bROFgkrBgEEAdpHDwEBB0B92inq37NVcsS1Ls23yNdXE2nz3BXfscywSVXBqNZN
|
||||||
|
bIkCswQYAQgAJhYhBClZA2Lsh4qB/TwgK1JSe+2r6HmEBQJm9bROAhsCBQkDwmcA
|
||||||
|
AIEJEFJSe+2r6HmEdiAEGRYKAB0WIQRHpeVRP4vUB1Zsqy7N3qfpETFgUwUCZvW0
|
||||||
|
TgAKCRDN3qfpETFgUz3EAP9xNJ/BQGkvD7uZCkE+mUg0EPtrL9RU1DCKmNHY9h3P
|
||||||
|
IAD7B6v4nvM01lOBaxLnXxcESbV/eY9wcl8W/33L5fYBpQ9vvQ/+IlVEdqugj+0W
|
||||||
|
PBO5fbWOegpFR9ujNWIT7GUHY+kgiNXncNY2zXHpNAz/k/TKrAQHuNjMzLIL2Zhf
|
||||||
|
NuFTRPZ2qyzJUY+tFfMwqYUG9dW/oY5IydTVQLrkEDffGob7S7p/+aXs7/L0Dmp/
|
||||||
|
u5z3pX5GJxUlmjXedx/tyNZEQeqFquCmIABUh2XGCW7IQ2nXMTJUjgMuphtQ8JkS
|
||||||
|
n2de2HwVTkx6RonebA5fHQP07IfUiVFpSAZqZJvQ6HNVwTMaP9lU3JzvmexJSL74
|
||||||
|
zmm7YEoH1C+Cz6jGi3mlsIY8y+xSQ14vOoO6I+TulF9vEFNoQO5l9IYbqNMTGA7r
|
||||||
|
2Ukq8GH0n9rfAxJEM7OkaX4pZNKXXG2d0DbvoJjSNTyctQkGrl1EKYL8rRY5CKpz
|
||||||
|
/X1akcKXaJ6mYoLeYamTsZzXEsO7r10nKGKhZMt1cpvf8qy6PsSTCEhbo+YE///L
|
||||||
|
0ppFGugsl1QqDgjYaLci7Wcz7kHgYdHttsXT2bq1q0AvHsTt9TjFNFKwnGDGsw28
|
||||||
|
XHYJkZs5vJOQj46glPxEsHMdkdZzUIyCC3HT/KfvArfdDgZZQ4QhzTsG4Becsrfx
|
||||||
|
ch6p/gvyxN9gielc/pQZhqqUtB5PF9pv9f/OnQf8uGqbhPHr6i4GfwQCov7LTJhc
|
||||||
|
t8FIucvlOdt4EqKaSmoBQZk0Aj/N5q4=
|
||||||
|
=vjZr
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
31
basicswap/pgp/keys/dogecoin_patricklodder.pgp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQENBF8V/EkBCAC8YTo6YJLNY0To+25b+dcSRcMCo/g9TJlraoJagO9Hr0Njbryg
|
||||||
|
jG5iptxi6UjDD+8xPK7YYRhaKyzJq1yTjGe5u5WEEtMfNaiVgA6dSEOXTdH4xT6q
|
||||||
|
v3VundebzZ7TFue7kj7fzEh7t9x2k5+RI2RvOs26ANEBKgJliQIZDXKOLcQuW7k9
|
||||||
|
9pWvqMWqRyn8WVGNf/UGBoFDcXQ1wo3h6m/LMJIO5L2IGlQWPmc8WT3uHJ/X/5Ln
|
||||||
|
slQ1ml7h+JjNwN0rAY/ZaJHSEi2y0RtLRzISP0EsA6EbqvJNGI8jqs5rpImgUn9U
|
||||||
|
8Q8Xz6hLPAiVTmteF63LlKo03wRcH8d/FVSvABEBAAG0N1BhdHJpY2sgTG9kZGVy
|
||||||
|
IDxwYXRyaWNrbG9kZGVyQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbT6JAVQEEwEI
|
||||||
|
AD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTcbvSov58bHk3h7lItOjRb
|
||||||
|
mNDcHwUCYtNqvwUJB3/VdgAKCRAtOjRbmNDcH+sVB/9jGPwrd1Om6L3ALzkZniR7
|
||||||
|
ODYFN4m8MRC4LPH2Ngt1Ea3/5DA68hEzQVGAFF+m7i7ZH9bmTvGB9R+qqF9WLTRc
|
||||||
|
aoO0XvYI8YrRLuhZFazafsLFRD5/c6QfpkBAjiDuxNIjEg2i+nY3avraxicKQKBY
|
||||||
|
PWWY0TFbz8K+CgIBh8Dnv7lqcxCFWHit/KHHjGAOvIPD5sLtv42dYk4TBEff4MVK
|
||||||
|
CzuCQtU8viy5doQPYHwfNADpOguskiNtFZmG2iPwgIE2tzHpLG2kidzZvJbHDcXY
|
||||||
|
XP13FnLvONf2bkS11gZSRm8pa6uay8/KfBNlCeMOYQDVoCuBbD5/2MwuV6o6OfSI
|
||||||
|
uQENBF8V/EkBCADN8eWUf0OtQdthNoWhRgotz/EzLI9r3sVv2SqbA++rHW9TC7mB
|
||||||
|
Wl/3e5emXWgKI1EK1Poz5HeKnL3SRx3xizgBTK6+RNQK6svvaLwcx06y8pZP9RqX
|
||||||
|
jLaRR67fXZCL+ulPtTcbt/JwlaTaokwWsgfy3UZRcK33llLbvWFjht2OGfx8B6Z9
|
||||||
|
UFRxW4sP0HuE3RrnMATGymWvOZlwYDr73HltksnOEFkz4lVP5VK9kdbndQjIB3Cf
|
||||||
|
zw/waTqjX+xXjJsFMYZhEDARhP5BQIoQvEv8KRtptNoLJGFZ9RGf+fIHiar2GAZL
|
||||||
|
4WZbZ0IuGLj419TkgvsUkI83Bx97DkS5Xa+jABEBAAGJATwEGAEIACYCGwwWIQTc
|
||||||
|
bvSov58bHk3h7lItOjRbmNDcHwUCYtNq0AUJB3/VhwAKCRAtOjRbmNDcH8cfB/4q
|
||||||
|
Puoir46sAGHBJt4TVe+R5ErVmGfGVUc3n6svguJnRMTAi1gpb6EapjdR9gUx+3Ja
|
||||||
|
wUE1keJuw5xeFi2JGp/XHt+8LAhsRAaLA4YViho8KL3yjzARvqrkYfl+FuO6kZIj
|
||||||
|
FEPJjRI1hOx5pWtPa3L3GZOexYDhRVdIJDci3gbFmU8HjgFx0G50zAysGR4DLVXj
|
||||||
|
FQBPvt4asUTdx30HU/pxWqFEzAeJPOVyjoxotdsMcIYXVBDhte5eADJ4OSMmc7k3
|
||||||
|
k46yHnbD4wyqqGtWqxHitTrl2U+M5MO5rlOZpGtIMtHz186OyMySZ5Gc886vPlOG
|
||||||
|
XgtNHT7E4rDrhySwy6Yk
|
||||||
|
=DQYN
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
41
basicswap/pgp/keys/dogecoin_xanimo.pgp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
tS35OCTSJzNMoXtTZqCkDLc9
|
||||||
|
=Z8rt
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
161
basicswap/pgp/keys/nicolasdorier.asc
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Comment: https://keybase.io/nicolasdorier
|
||||||
|
Version: Keybase Go 2.6.0 (linux)
|
||||||
|
|
||||||
|
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
|
||||||
|
=sNb2
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Comment: https://keybase.io/nicolasdorier
|
||||||
|
Version: Keybase Go 5.0.0 (windows)
|
||||||
|
|
||||||
|
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
|
||||||
|
=ut7t
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Comment: https://keybase.io/nicolasdorier
|
||||||
|
Version: Keybase Go 5.0.0 (windows)
|
||||||
|
|
||||||
|
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
|
||||||
|
=n82A
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmbbRWEACgkQE/E2UcnP
|
||||||
|
DWsVcxAArcHDK8mzY9RkiWuaW9l3PItU/n6NH7XCPqchnkum3IXHWUn/hU3Dzq8M
|
||||||
|
pZCEksI4jm3tGVxz+RvjWHCQBk0e40LOmYfUrAXFiyrchw0VQKbzF81q0HL7jUB3
|
||||||
|
0GlwvSX7o4B2TNjEAcD6x6ztwHLZKEAFwHBtUlSxkqAPkTzkrxMBptKUwhGj3uWW
|
||||||
|
p9NDsRKAmzitpg+H/+Puh5HcC05bqhsMZ2h3iyswmrP9DJvsKjAYHEwLECzoFn72
|
||||||
|
On+AkV/msvJSkp4O8gr2j4IZ/Mm/zOe1PAq7UdVMWGdxfjn37Ci0adY7ZJd/9tsk
|
||||||
|
VcaHRzJ91iTItol0FFX+ytOxKNyMq311EWaWR2lecx/cagt8nA4oELrKGwTy4dDV
|
||||||
|
kSXUF8PYXtFLAPhow42ARQdk4bgOolwT4AYjHMUoK25RiYbY4dFyuiOv/OHg6Wvs
|
||||||
|
Ostfpy+VKD9qi3NMBf5i0QoxtJZQuWA6PaR7W+idLRgXA6LJB4OEtd5WmCvG10zX
|
||||||
|
hkToSGPN6AMPSta1BIoFnDbTBYQvsslxwSWM4wmQgJbkqSYU4vg8JRJ58spUXn5/
|
||||||
|
DIRU8slQuIvKek9D67aahpdA9W0csoOhHGnnWk/JS/Hyzk6v9b3N3L82ct8tzhH6
|
||||||
|
MhnH7vVrRDxGAsWDeGkCAH2/KxSaafqWTjXoSDrftPueI07BeAY=
|
||||||
|
=TH7+
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmeP/40ACgkQE/E2UcnP
|
||||||
|
DWuraw/9HCuAZG+D6xSLWmjA9Dtj9OZMEOIxqvxw+1e2KQ5ek4d1waL63NWFQfMi
|
||||||
|
fDlKKeFbZoL6Dfjbx0GoUJKTfrIVKog6DlVzIi5PuUwPOCBFuLl0g5kHlC20jbPw
|
||||||
|
nu7T6fj6/oD/lqo0rzFDkbsX7Fk4GGC7rYLKfdtYhDgMq9ro7QhSxAOJanRyqzXL
|
||||||
|
dvPNxlyksOyttJLSAZI9BOkrpTWoyb3asOli5oHgdcheHd/2fjby69huS3UWEjdO
|
||||||
|
9Bm73UFlxF2hxCTc2Fqvvb3SBDmNCLlFM0f+DDJNMJGUQViVCar0YRw3R+/NBo83
|
||||||
|
ptutp3bpabHijQFEEpIx/19nh9RQMJjaHHHqdPcTeg8bU/Yeq36TI7gsCenK0mQT
|
||||||
|
75MscvJAG0enoKVrTZez5ner9ZwLOevAKzRe4huRJZZjM8gM6sb2OKslJLqTxEVt
|
||||||
|
G3b8BLB9IUAxCeyuvGSG/3RV3MgZLnLy5MLYjh72+Kmo6HpuajJwPuvUck5ZYcGE
|
||||||
|
jjeRFZmqZj0FtCrcfStau/0liyAxU5k/43RwMvujO1uTTgOVHw1QhhMEkZ9bYhhO
|
||||||
|
JgeCEkwL1Bjjved1NSySjZbt2sFbG89as14ezHxgc4HaujJ6bGkINnkPOPWM1tk4
|
||||||
|
DjjEO/0PY9i0m/ivQUXf5ZPSnlkAR8x6Ve2S2MvQd7nFoS/YfLs=
|
||||||
|
=0pTn
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
21
basicswap/pgp/sigs/utxo-snapshot-bitcoin-mainnet-hashes.asc
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA256
|
||||||
|
|
||||||
|
725a049bc5a9fd60b05bba4d4825d35115d99f05ab5b7716d4507c295d05172d utxo-snapshot-bitcoin-mainnet-820852.tar
|
||||||
|
744c42885df700513331a978b289d9c9d5b27e0cf1147f2f5a287b4492ff940c utxo-snapshot-bitcoin-mainnet-867690.tar
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAEBCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmedUAYACgkQE/E2UcnP
|
||||||
|
DWs1Vw/+P3CGP9LLVv2deNocBFunUz+7aDZsQiykSI8ws50ssJ5PsAg5VSl4CbCl
|
||||||
|
owWOdQVJiDUh7daP0jr+bt3X2FY5ORBb1TGlvfCHE+vLfEFDnTpLXouSCclP0cv8
|
||||||
|
Ci8zQFKSI5Pf6uSMpALgQZxBgNU/0IegAQbpuJI4nrQXTKHJcMqtw1LtnmcreESO
|
||||||
|
MsSiGCXnC1R+xGQjptfvbzXaQVrin7ctYA9zjN4CGbjNChzr+ywT8dht2RKoLYyP
|
||||||
|
OrEys7d8EIaw/ktRvRmyk6O7KmnvUhf0uuFlDq+eTiBIpQoUEovCow1YYKaWkIRB
|
||||||
|
r4JBJJ34AB+XC2hgi5jpJNub/wKgVBm0iy79zZOSILP3ymbn3iJGg4ifUF0YeZCU
|
||||||
|
ufYkYi3iTJDpwYr0tylZmBiwsWNcbUhB+WTNX7ogCW70ZuhrF0PJQRPmhI34vsE/
|
||||||
|
qg3n0/hNNsypy0epRd33KSOvrSmaoTKLtCax9Osnt+F+yTYjD5EPqkQuzlJl+fDe
|
||||||
|
VvjWO5XHuaRvzijBrJQz6r5V4e/0ioNa8FTRqWmMTO1wHmxF5glpozyKycv9+bsB
|
||||||
|
IL9F1IQjhPkSVI7Hw8bsURpfH4mV+9eZJJDIvBf1/0gDctsBdsI5+5jxZjup769Q
|
||||||
|
AmMsGeZoplm/eUofQ9hItWcVitPhisDmC3wDR71UKM0b9FF6IUY=
|
||||||
|
=YUjt
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
@@ -16,19 +16,26 @@ class ProtocolInterface:
|
|||||||
swap_type = None
|
swap_type = None
|
||||||
|
|
||||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
raise ValueError('base class')
|
raise ValueError("base class")
|
||||||
|
|
||||||
def getMockScript(self) -> bytearray:
|
def getMockScript(self) -> bytearray:
|
||||||
return bytearray([
|
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
|
||||||
OpCodes.OP_RETURN, OpCodes.OP_1])
|
|
||||||
|
|
||||||
def getMockScriptScriptPubkey(self, ci) -> bytearray:
|
def getMockScriptScriptPubkey(self, ci) -> bytearray:
|
||||||
script = self.getMockScript()
|
script = self.getMockScript()
|
||||||
return ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
return (
|
||||||
|
ci.getScriptDest(script)
|
||||||
|
if ci._use_segwit
|
||||||
|
else ci.get_p2sh_script_pubkey(script)
|
||||||
|
)
|
||||||
|
|
||||||
def getMockAddrTo(self, ci):
|
def getMockAddrTo(self, ci):
|
||||||
script = self.getMockScript()
|
script = self.getMockScript()
|
||||||
return ci.encodeScriptDest(ci.getScriptDest(script)) if ci._use_segwit else ci.encode_p2sh(script)
|
return (
|
||||||
|
ci.encodeScriptDest(ci.getScriptDest(script))
|
||||||
|
if ci._use_segwit
|
||||||
|
else ci.encode_p2sh(script)
|
||||||
|
)
|
||||||
|
|
||||||
def findMockVout(self, ci, itx_decoded):
|
def findMockVout(self, ci, itx_decoded):
|
||||||
mock_addr = self.getMockAddrTo(ci)
|
mock_addr = self.getMockAddrTo(ci)
|
||||||
|
|||||||
@@ -26,52 +26,66 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
|
|||||||
ABS_LOCK_TIME_LEEWAY = 10 * 60
|
ABS_LOCK_TIME_LEEWAY = 10 * 60
|
||||||
|
|
||||||
|
|
||||||
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256) -> bytearray:
|
def buildContractScript(
|
||||||
script = bytearray([
|
lock_val: int,
|
||||||
|
secret_hash: bytes,
|
||||||
|
pkh_redeem: bytes,
|
||||||
|
pkh_refund: bytes,
|
||||||
|
op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY,
|
||||||
|
op_hash=OpCodes.OP_SHA256,
|
||||||
|
) -> bytearray:
|
||||||
|
script = (
|
||||||
|
bytearray(
|
||||||
|
[
|
||||||
OpCodes.OP_IF,
|
OpCodes.OP_IF,
|
||||||
OpCodes.OP_SIZE,
|
OpCodes.OP_SIZE,
|
||||||
0x01, 0x20, # 32
|
0x01,
|
||||||
|
0x20, # 32
|
||||||
OpCodes.OP_EQUALVERIFY,
|
OpCodes.OP_EQUALVERIFY,
|
||||||
op_hash,
|
op_hash,
|
||||||
0x20]) \
|
0x20,
|
||||||
+ secret_hash \
|
]
|
||||||
+ bytearray([
|
)
|
||||||
OpCodes.OP_EQUALVERIFY,
|
+ secret_hash
|
||||||
OpCodes.OP_DUP,
|
+ bytearray([OpCodes.OP_EQUALVERIFY, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14])
|
||||||
OpCodes.OP_HASH160,
|
+ pkh_redeem
|
||||||
0x14]) \
|
+ bytearray(
|
||||||
+ pkh_redeem \
|
[
|
||||||
+ bytearray([OpCodes.OP_ELSE, ]) \
|
OpCodes.OP_ELSE,
|
||||||
+ SerialiseNum(lock_val) \
|
]
|
||||||
+ bytearray([
|
)
|
||||||
op_lock,
|
+ SerialiseNum(lock_val)
|
||||||
OpCodes.OP_DROP,
|
+ bytearray(
|
||||||
OpCodes.OP_DUP,
|
[op_lock, OpCodes.OP_DROP, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14]
|
||||||
OpCodes.OP_HASH160,
|
)
|
||||||
0x14]) \
|
+ pkh_refund
|
||||||
+ pkh_refund \
|
+ bytearray([OpCodes.OP_ENDIF, OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG])
|
||||||
+ bytearray([
|
)
|
||||||
OpCodes.OP_ENDIF,
|
|
||||||
OpCodes.OP_EQUALVERIFY,
|
|
||||||
OpCodes.OP_CHECKSIG])
|
|
||||||
return script
|
return script
|
||||||
|
|
||||||
|
|
||||||
def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256):
|
def verifyContractScript(
|
||||||
if script[0] != OpCodes.OP_IF or \
|
script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256
|
||||||
script[1] != OpCodes.OP_SIZE or \
|
):
|
||||||
script[2] != 0x01 or script[3] != 0x20 or \
|
if (
|
||||||
script[4] != OpCodes.OP_EQUALVERIFY or \
|
script[0] != OpCodes.OP_IF
|
||||||
script[5] != op_hash or \
|
or script[1] != OpCodes.OP_SIZE
|
||||||
script[6] != 0x20:
|
or script[2] != 0x01
|
||||||
|
or script[3] != 0x20
|
||||||
|
or script[4] != OpCodes.OP_EQUALVERIFY
|
||||||
|
or script[5] != op_hash
|
||||||
|
or script[6] != 0x20
|
||||||
|
):
|
||||||
return False, None, None, None, None
|
return False, None, None, None, None
|
||||||
o = 7
|
o = 7
|
||||||
script_hash = script[o : o + 32]
|
script_hash = script[o : o + 32]
|
||||||
o += 32
|
o += 32
|
||||||
if script[o] != OpCodes.OP_EQUALVERIFY or \
|
if (
|
||||||
script[o + 1] != OpCodes.OP_DUP or \
|
script[o] != OpCodes.OP_EQUALVERIFY
|
||||||
script[o + 2] != OpCodes.OP_HASH160 or \
|
or script[o + 1] != OpCodes.OP_DUP
|
||||||
script[o + 3] != 0x14:
|
or script[o + 2] != OpCodes.OP_HASH160
|
||||||
|
or script[o + 3] != 0x14
|
||||||
|
):
|
||||||
return False, script_hash, None, None, None
|
return False, script_hash, None, None, None
|
||||||
o += 4
|
o += 4
|
||||||
pkh_redeem = script[o : o + 20]
|
pkh_redeem = script[o : o + 20]
|
||||||
@@ -81,18 +95,22 @@ def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash
|
|||||||
o += 1
|
o += 1
|
||||||
lock_val, nb = decodeScriptNum(script, o)
|
lock_val, nb = decodeScriptNum(script, o)
|
||||||
o += nb
|
o += nb
|
||||||
if script[o] != op_lock or \
|
if (
|
||||||
script[o + 1] != OpCodes.OP_DROP or \
|
script[o] != op_lock
|
||||||
script[o + 2] != OpCodes.OP_DUP or \
|
or script[o + 1] != OpCodes.OP_DROP
|
||||||
script[o + 3] != OpCodes.OP_HASH160 or \
|
or script[o + 2] != OpCodes.OP_DUP
|
||||||
script[o + 4] != 0x14:
|
or script[o + 3] != OpCodes.OP_HASH160
|
||||||
|
or script[o + 4] != 0x14
|
||||||
|
):
|
||||||
return False, script_hash, pkh_redeem, lock_val, None
|
return False, script_hash, pkh_redeem, lock_val, None
|
||||||
o += 5
|
o += 5
|
||||||
pkh_refund = script[o : o + 20]
|
pkh_refund = script[o : o + 20]
|
||||||
o += 20
|
o += 20
|
||||||
if script[o] != OpCodes.OP_ENDIF or \
|
if (
|
||||||
script[o + 1] != OpCodes.OP_EQUALVERIFY or \
|
script[o] != OpCodes.OP_ENDIF
|
||||||
script[o + 2] != OpCodes.OP_CHECKSIG:
|
or script[o + 1] != OpCodes.OP_EQUALVERIFY
|
||||||
|
or script[o + 2] != OpCodes.OP_CHECKSIG
|
||||||
|
):
|
||||||
return False, script_hash, pkh_redeem, lock_val, pkh_refund
|
return False, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||||
return True, script_hash, pkh_redeem, lock_val, pkh_refund
|
return True, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||||
|
|
||||||
@@ -101,16 +119,23 @@ def extractScriptSecretHash(script):
|
|||||||
return script[7:39]
|
return script[7:39]
|
||||||
|
|
||||||
|
|
||||||
def redeemITx(self, bid_id: bytes, session):
|
def redeemITx(self, bid_id: bytes, cursor):
|
||||||
bid, offer = self.getBidAndOffer(bid_id, session)
|
bid, offer = self.getBidAndOffer(bid_id, cursor)
|
||||||
ci_from = self.ci(offer.coin_from)
|
ci_from = self.ci(offer.coin_from)
|
||||||
|
|
||||||
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate', session=session)
|
txn = self.createRedeemTxn(
|
||||||
|
ci_from.coin_type(), bid, for_txn_type="initiate", cursor=cursor
|
||||||
|
)
|
||||||
txid = ci_from.publishTx(bytes.fromhex(txn))
|
txid = ci_from.publishTx(bytes.fromhex(txn))
|
||||||
|
|
||||||
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
|
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
|
||||||
self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex())
|
self.log.debug(
|
||||||
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, '', session)
|
"Submitted initiate redeem txn %s to %s chain for bid %s",
|
||||||
|
txid,
|
||||||
|
ci_from.coin_name(),
|
||||||
|
bid_id.hex(),
|
||||||
|
)
|
||||||
|
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
|
||||||
|
|
||||||
|
|
||||||
class AtomicSwapInterface(ProtocolInterface):
|
class AtomicSwapInterface(ProtocolInterface):
|
||||||
@@ -118,13 +143,19 @@ class AtomicSwapInterface(ProtocolInterface):
|
|||||||
|
|
||||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
addr_to = self.getMockAddrTo(ci)
|
addr_to = self.getMockAddrTo(ci)
|
||||||
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
funded_tx = ci.createRawFundedTransaction(
|
||||||
|
addr_to, amount, sub_fee, lock_unspents=False
|
||||||
|
)
|
||||||
|
|
||||||
return bytes.fromhex(funded_tx)
|
return bytes.fromhex(funded_tx)
|
||||||
|
|
||||||
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
|
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
|
||||||
mock_txo_script = self.getMockScriptScriptPubkey(ci)
|
mock_txo_script = self.getMockScriptScriptPubkey(ci)
|
||||||
real_txo_script = ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
real_txo_script = (
|
||||||
|
ci.getScriptDest(script)
|
||||||
|
if ci._use_segwit
|
||||||
|
else ci.get_p2sh_script_pubkey(script)
|
||||||
|
)
|
||||||
|
|
||||||
found: int = 0
|
found: int = 0
|
||||||
ctx = ci.loadTx(mock_tx)
|
ctx = ci.loadTx(mock_tx)
|
||||||
@@ -134,9 +165,9 @@ class AtomicSwapInterface(ProtocolInterface):
|
|||||||
found += 1
|
found += 1
|
||||||
|
|
||||||
if found < 1:
|
if found < 1:
|
||||||
raise ValueError('Mocked output not found')
|
raise ValueError("Mocked output not found")
|
||||||
if found > 1:
|
if found > 1:
|
||||||
raise ValueError('Too many mocked outputs found')
|
raise ValueError("Too many mocked outputs found")
|
||||||
ctx.nLockTime = 0
|
ctx.nLockTime = 0
|
||||||
|
|
||||||
funded_tx = ctx.serialize()
|
funded_tx = ctx.serialize()
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2024 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
|
# Copyright (c) 2024-2025 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
from basicswap.util import (
|
from basicswap.util import (
|
||||||
ensure,
|
ensure,
|
||||||
)
|
)
|
||||||
@@ -12,23 +15,23 @@ from basicswap.chainparams import (
|
|||||||
Coins,
|
Coins,
|
||||||
)
|
)
|
||||||
from basicswap.basicswap_util import (
|
from basicswap.basicswap_util import (
|
||||||
|
EventLogTypes,
|
||||||
KeyTypes,
|
KeyTypes,
|
||||||
SwapTypes,
|
SwapTypes,
|
||||||
EventLogTypes,
|
TxTypes,
|
||||||
)
|
)
|
||||||
from . import ProtocolInterface
|
from . import ProtocolInterface
|
||||||
from basicswap.contrib.test_framework.script import (
|
from basicswap.contrib.test_framework.script import CScript, CScriptOp, OP_CHECKMULTISIG
|
||||||
CScript, CScriptOp,
|
|
||||||
OP_CHECKMULTISIG
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def addLockRefundSigs(self, xmr_swap, ci):
|
def addLockRefundSigs(self, xmr_swap, ci):
|
||||||
self.log.debug('Setting lock refund tx sigs')
|
self.log.debug("Setting lock refund tx sigs")
|
||||||
|
|
||||||
witness_stack = []
|
witness_stack = []
|
||||||
if ci.coin_type() not in (Coins.DCR,):
|
if ci.coin_type() not in (Coins.DCR,):
|
||||||
witness_stack += [b'', ]
|
witness_stack += [
|
||||||
|
b"",
|
||||||
|
]
|
||||||
witness_stack += [
|
witness_stack += [
|
||||||
xmr_swap.al_lock_refund_tx_sig,
|
xmr_swap.al_lock_refund_tx_sig,
|
||||||
xmr_swap.af_lock_refund_tx_sig,
|
xmr_swap.af_lock_refund_tx_sig,
|
||||||
@@ -36,63 +39,125 @@ def addLockRefundSigs(self, xmr_swap, ci):
|
|||||||
]
|
]
|
||||||
|
|
||||||
signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack)
|
signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack)
|
||||||
ensure(signed_tx, 'setTxSignature failed')
|
ensure(signed_tx, "setTxSignature failed")
|
||||||
xmr_swap.a_lock_refund_tx = signed_tx
|
xmr_swap.a_lock_refund_tx = signed_tx
|
||||||
|
|
||||||
|
|
||||||
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
|
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
|
||||||
self.log.info('Manually recovering %s', bid_id.hex())
|
self.log.info(f"Manually recovering {self.log.id(bid_id)}")
|
||||||
# Manually recover txn if other key is known
|
# Manually recover txn if other key is known
|
||||||
session = self.openSession()
|
|
||||||
try:
|
try:
|
||||||
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
|
use_cursor = self.openDB(cursor)
|
||||||
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
|
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
|
||||||
ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
|
ensure(bid, "Bid not found: {}.".format(bid_id.hex()))
|
||||||
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
|
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex()))
|
||||||
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
|
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
|
||||||
ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
|
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex()))
|
||||||
ci_to = self.ci(offer.coin_to)
|
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex()))
|
||||||
|
|
||||||
for_ed25519 = True if Coins(offer.coin_to) == Coins.XMR else False
|
# The no-script coin is always the follower
|
||||||
|
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
|
||||||
|
ci_from = self.ci(Coins(offer.coin_from))
|
||||||
|
ci_to = self.ci(Coins(offer.coin_to))
|
||||||
|
ci_follower = ci_from if reverse_bid else ci_to
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decoded_key_half = ci_to.decodeKey(encoded_key)
|
decoded_key_half = ci_follower.decodeKey(encoded_key)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError('Failed to decode provided key-half: ', str(e))
|
raise ValueError("Failed to decode provided key-half: ", str(e))
|
||||||
|
|
||||||
if bid.was_sent:
|
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
|
||||||
|
|
||||||
|
localkeyhalf = ci_follower.decodeKey(
|
||||||
|
getChainBSplitKey(self, bid, xmr_swap, offer)
|
||||||
|
)
|
||||||
|
if was_sent:
|
||||||
kbsl = decoded_key_half
|
kbsl = decoded_key_half
|
||||||
kbsf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519)
|
kbsf = localkeyhalf
|
||||||
else:
|
else:
|
||||||
kbsl = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519)
|
kbsl = localkeyhalf
|
||||||
kbsf = decoded_key_half
|
kbsf = decoded_key_half
|
||||||
ensure(ci_to.verifyKey(kbsl), 'Invalid kbsl')
|
|
||||||
ensure(ci_to.verifyKey(kbsf), 'Invalid kbsf')
|
|
||||||
vkbs = ci_to.sumKeys(kbsl, kbsf)
|
|
||||||
|
|
||||||
if offer.coin_to == Coins.XMR:
|
ensure(ci_follower.verifyKey(kbsl), "Invalid kbsl")
|
||||||
address_to = self.getCachedMainWalletAddress(ci_to)
|
ensure(ci_follower.verifyKey(kbsf), "Invalid kbsf")
|
||||||
|
if kbsl == kbsf:
|
||||||
|
raise ValueError("Provided key matches local key")
|
||||||
|
vkbs = ci_follower.sumKeys(kbsl, kbsf)
|
||||||
|
|
||||||
|
ensure(ci_follower.verifyPubkey(xmr_swap.pkbs), "Invalid pkbs") # Sanity check
|
||||||
|
|
||||||
|
# Ensure summed key matches the expected pubkey
|
||||||
|
summed_pkbs = ci_follower.getPubkey(vkbs)
|
||||||
|
if summed_pkbs != xmr_swap.pkbs:
|
||||||
|
err_msg: str = "Summed key does not match expected wallet spend pubkey"
|
||||||
|
self.log.error(
|
||||||
|
f"{err_msg}. Got: {summed_pkbs.hex()}, Expect: {xmr_swap.pkbs.hex()}"
|
||||||
|
)
|
||||||
|
raise ValueError(err_msg)
|
||||||
|
|
||||||
|
coin_to: int = ci_follower.interface_type()
|
||||||
|
base_coin_to: int = ci_follower.coin_type()
|
||||||
|
if coin_to in (Coins.XMR, Coins.WOW):
|
||||||
|
address_to = self.getCachedMainWalletAddress(ci_follower, use_cursor)
|
||||||
|
elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
|
||||||
|
address_to = self.getCachedStealthAddressForCoin(base_coin_to, use_cursor)
|
||||||
else:
|
else:
|
||||||
address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
|
address_to = self.getReceiveAddressFromPool(
|
||||||
|
base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor
|
||||||
|
)
|
||||||
amount = bid.amount_to
|
amount = bid.amount_to
|
||||||
lock_tx_vout = bid.getLockTXBVout()
|
lock_tx_vout = bid.getLockTXBVout()
|
||||||
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout)
|
txid = ci_follower.spendBLockTx(
|
||||||
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
|
xmr_swap.b_lock_tx_id,
|
||||||
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), session)
|
address_to,
|
||||||
session.commit()
|
xmr_swap.vkbv,
|
||||||
|
vkbs,
|
||||||
|
amount,
|
||||||
|
xmr_offer.b_fee_rate,
|
||||||
|
bid.chain_b_height_start,
|
||||||
|
spend_actual_balance=True,
|
||||||
|
lock_tx_vout=lock_tx_vout,
|
||||||
|
)
|
||||||
|
self.log.debug(
|
||||||
|
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
|
||||||
|
)
|
||||||
|
self.logBidEvent(
|
||||||
|
bid.bid_id,
|
||||||
|
EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED,
|
||||||
|
txid.hex(),
|
||||||
|
use_cursor,
|
||||||
|
)
|
||||||
|
self.commitDB()
|
||||||
|
|
||||||
return txid
|
return txid
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(traceback.format_exc())
|
||||||
|
raise (e)
|
||||||
finally:
|
finally:
|
||||||
self.closeSession(session, commit=False)
|
if cursor is None:
|
||||||
|
self.closeDB(use_cursor, commit=False)
|
||||||
|
|
||||||
|
|
||||||
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
||||||
reverse_bid: bool = offer.bid_reversed
|
reverse_bid: bool = offer.bid_reversed
|
||||||
|
ci_leader = swap_client.ci(offer.coin_to if reverse_bid else offer.coin_from)
|
||||||
ci_follower = swap_client.ci(offer.coin_from if reverse_bid else offer.coin_to)
|
ci_follower = swap_client.ci(offer.coin_from if reverse_bid else offer.coin_to)
|
||||||
|
|
||||||
key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL
|
for_ed25519: bool = True if ci_follower.curve_type() == Curves.ed25519 else False
|
||||||
return ci_follower.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, key_type, True if ci_follower.coin_type() == Coins.XMR else False))
|
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
|
||||||
|
|
||||||
|
key_type = KeyTypes.KBSF if was_sent else KeyTypes.KBSL
|
||||||
|
|
||||||
|
return ci_follower.encodeKey(
|
||||||
|
swap_client.getPathKey(
|
||||||
|
ci_leader.interface_type(),
|
||||||
|
ci_follower.interface_type(),
|
||||||
|
bid.created_at,
|
||||||
|
xmr_swap.contract_count,
|
||||||
|
key_type,
|
||||||
|
for_ed25519,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
||||||
@@ -102,13 +167,21 @@ def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
|||||||
|
|
||||||
if bid.was_sent:
|
if bid.was_sent:
|
||||||
if xmr_swap.a_lock_refund_spend_tx:
|
if xmr_swap.a_lock_refund_spend_tx:
|
||||||
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx)
|
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(
|
||||||
kbsl = ci_leader.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl)
|
xmr_swap.a_lock_refund_spend_tx
|
||||||
|
)
|
||||||
|
kbsl = ci_leader.recoverEncKey(
|
||||||
|
xmr_swap.af_lock_refund_spend_tx_esig,
|
||||||
|
af_lock_refund_spend_tx_sig,
|
||||||
|
xmr_swap.pkasl,
|
||||||
|
)
|
||||||
return ci_follower.encodeKey(kbsl)
|
return ci_follower.encodeKey(kbsl)
|
||||||
else:
|
else:
|
||||||
if xmr_swap.a_lock_spend_tx:
|
if xmr_swap.a_lock_spend_tx:
|
||||||
al_lock_spend_tx_sig = ci_leader.extractLeaderSig(xmr_swap.a_lock_spend_tx)
|
al_lock_spend_tx_sig = ci_leader.extractLeaderSig(xmr_swap.a_lock_spend_tx)
|
||||||
kbsf = ci_leader.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf)
|
kbsf = ci_leader.recoverEncKey(
|
||||||
|
xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf
|
||||||
|
)
|
||||||
return ci_follower.encodeKey(kbsf)
|
return ci_follower.encodeKey(kbsf)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -119,21 +192,29 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
|
|||||||
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
|
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
|
||||||
elif ci_to.curve_type() == Curves.secp256k1:
|
elif ci_to.curve_type() == Curves.secp256k1:
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, 'proof kbsf owned for swap')
|
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
|
||||||
pk_recovered: bytes = ci_to.verifySigAndRecover(xmr_swap.kbsf_dleag, 'proof kbsf owned for swap')
|
kbsf, "proof kbsf owned for swap"
|
||||||
|
)
|
||||||
|
pk_recovered: bytes = ci_to.verifySigAndRecover(
|
||||||
|
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
|
||||||
|
)
|
||||||
if pk_recovered == xmr_swap.pkbsf:
|
if pk_recovered == xmr_swap.pkbsf:
|
||||||
break
|
break
|
||||||
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
|
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
|
||||||
assert (pk_recovered == xmr_swap.pkbsf)
|
assert pk_recovered == xmr_swap.pkbsf
|
||||||
xmr_swap.pkasf = xmr_swap.pkbsf
|
xmr_swap.pkasf = xmr_swap.pkbsf
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown curve')
|
raise ValueError("Unknown curve")
|
||||||
|
|
||||||
|
|
||||||
class XmrSwapInterface(ProtocolInterface):
|
class XmrSwapInterface(ProtocolInterface):
|
||||||
swap_type = SwapTypes.XMR_SWAP
|
swap_type = SwapTypes.XMR_SWAP
|
||||||
|
|
||||||
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript:
|
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
|
||||||
|
# fallthrough to ci if genScriptLockTxScript is implemented there
|
||||||
|
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
|
||||||
|
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
|
||||||
|
|
||||||
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
||||||
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
||||||
|
|
||||||
@@ -141,7 +222,9 @@ class XmrSwapInterface(ProtocolInterface):
|
|||||||
|
|
||||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||||
addr_to = self.getMockAddrTo(ci)
|
addr_to = self.getMockAddrTo(ci)
|
||||||
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
funded_tx = ci.createRawFundedTransaction(
|
||||||
|
addr_to, amount, sub_fee, lock_unspents=False
|
||||||
|
)
|
||||||
|
|
||||||
return bytes.fromhex(funded_tx)
|
return bytes.fromhex(funded_tx)
|
||||||
|
|
||||||
@@ -157,9 +240,9 @@ class XmrSwapInterface(ProtocolInterface):
|
|||||||
found += 1
|
found += 1
|
||||||
|
|
||||||
if found < 1:
|
if found < 1:
|
||||||
raise ValueError('Mocked output not found')
|
raise ValueError("Mocked output not found")
|
||||||
if found > 1:
|
if found > 1:
|
||||||
raise ValueError('Too many mocked outputs found')
|
raise ValueError("Too many mocked outputs found")
|
||||||
ctx.nLockTime = 0
|
ctx.nLockTime = 0
|
||||||
|
|
||||||
return ctx.serialize()
|
return ctx.serialize()
|
||||||
|
|||||||
114
basicswap/rpc.py
@@ -18,31 +18,42 @@ from xmlrpc.client import (
|
|||||||
from .util import jsonDecimal
|
from .util import jsonDecimal
|
||||||
|
|
||||||
|
|
||||||
class Jsonrpc():
|
class Jsonrpc:
|
||||||
# __getattr__ complicates extending ServerProxy
|
# __getattr__ complicates extending ServerProxy
|
||||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
def __init__(
|
||||||
allow_none=False, use_datetime=False, use_builtin_types=False,
|
self,
|
||||||
*, context=None):
|
uri,
|
||||||
|
transport=None,
|
||||||
|
encoding=None,
|
||||||
|
verbose=False,
|
||||||
|
allow_none=False,
|
||||||
|
use_datetime=False,
|
||||||
|
use_builtin_types=False,
|
||||||
|
*,
|
||||||
|
context=None,
|
||||||
|
):
|
||||||
# establish a "logical" server connection
|
# establish a "logical" server connection
|
||||||
|
|
||||||
# get the url
|
# get the url
|
||||||
parsed = urllib.parse.urlparse(uri)
|
parsed = urllib.parse.urlparse(uri)
|
||||||
if parsed.scheme not in ('http', 'https'):
|
if parsed.scheme not in ("http", "https"):
|
||||||
raise OSError('unsupported XML-RPC protocol')
|
raise OSError("unsupported XML-RPC protocol")
|
||||||
self.__host = parsed.netloc
|
self.__host = parsed.netloc
|
||||||
self.__handler = parsed.path
|
self.__handler = parsed.path
|
||||||
if not self.__handler:
|
if not self.__handler:
|
||||||
self.__handler = '/RPC2'
|
self.__handler = "/RPC2"
|
||||||
|
|
||||||
if transport is None:
|
if transport is None:
|
||||||
handler = SafeTransport if parsed.scheme == 'https' else Transport
|
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
transport = handler(use_datetime=use_datetime,
|
transport = handler(
|
||||||
|
use_datetime=use_datetime,
|
||||||
use_builtin_types=use_builtin_types,
|
use_builtin_types=use_builtin_types,
|
||||||
**extra_kwargs)
|
**extra_kwargs,
|
||||||
|
)
|
||||||
self.__transport = transport
|
self.__transport = transport
|
||||||
|
|
||||||
self.__encoding = encoding or 'utf-8'
|
self.__encoding = encoding or "utf-8"
|
||||||
self.__verbose = verbose
|
self.__verbose = verbose
|
||||||
self.__allow_none = allow_none
|
self.__allow_none = allow_none
|
||||||
|
|
||||||
@@ -57,17 +68,16 @@ class Jsonrpc():
|
|||||||
connection = self.__transport.make_connection(self.__host)
|
connection = self.__transport.make_connection(self.__host)
|
||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
|
|
||||||
request_body = {
|
request_body = {"method": method, "params": params, "id": self.__request_id}
|
||||||
'method': method,
|
|
||||||
'params': params,
|
|
||||||
'id': self.__request_id
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.putrequest('POST', self.__handler)
|
connection.putrequest("POST", self.__handler)
|
||||||
headers.append(('Content-Type', 'application/json'))
|
headers.append(("Content-Type", "application/json"))
|
||||||
headers.append(('User-Agent', 'jsonrpc'))
|
headers.append(("User-Agent", "jsonrpc"))
|
||||||
self.__transport.send_headers(connection, headers)
|
self.__transport.send_headers(connection, headers)
|
||||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
|
self.__transport.send_content(
|
||||||
|
connection,
|
||||||
|
json.dumps(request_body, default=jsonDecimal).encode("utf-8"),
|
||||||
|
)
|
||||||
self.__request_id += 1
|
self.__request_id += 1
|
||||||
|
|
||||||
resp = connection.getresponse()
|
resp = connection.getresponse()
|
||||||
@@ -82,55 +92,59 @@ class Jsonrpc():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
|
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||||
try:
|
try:
|
||||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
url += 'wallet/' + urllib.parse.quote(wallet)
|
url += "wallet/" + urllib.parse.quote(wallet)
|
||||||
x = Jsonrpc(url)
|
x = Jsonrpc(url)
|
||||||
|
|
||||||
v = x.json_request(method, params)
|
v = x.json_request(method, params)
|
||||||
x.close()
|
x.close()
|
||||||
r = json.loads(v.decode('utf-8'))
|
r = json.loads(v.decode("utf-8"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
|
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||||
|
|
||||||
if 'error' in r and r['error'] is not None:
|
if "error" in r and r["error"] is not None:
|
||||||
raise ValueError('RPC error ' + str(r['error']))
|
raise ValueError("RPC error " + str(r["error"]))
|
||||||
|
|
||||||
return r['result']
|
return r["result"]
|
||||||
|
|
||||||
|
|
||||||
def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'):
|
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||||
try:
|
try:
|
||||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
url += 'wallet/' + urllib.parse.quote(wallet)
|
url += "wallet/" + urllib.parse.quote(wallet)
|
||||||
return Jsonrpc(url)
|
return Jsonrpc(url)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise ValueError('RPC error ' + str(ex))
|
raise ValueError("RPC error " + str(ex))
|
||||||
|
|
||||||
|
|
||||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None):
|
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None):
|
||||||
cli_bin = os.path.join(bindir, cli_bin)
|
cli_bin = os.path.join(bindir, cli_bin)
|
||||||
|
|
||||||
args = [cli_bin, ]
|
args = [
|
||||||
if chain != 'mainnet':
|
cli_bin,
|
||||||
args.append('-' + chain)
|
]
|
||||||
args.append('-datadir=' + datadir)
|
if chain != "mainnet":
|
||||||
|
args.append("-" + chain)
|
||||||
|
args.append("-datadir=" + datadir)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
args.append('-rpcwallet=' + wallet)
|
args.append("-rpcwallet=" + wallet)
|
||||||
args += shlex.split(cmd)
|
args += shlex.split(cmd)
|
||||||
|
|
||||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(
|
||||||
|
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
out = p.communicate()
|
out = p.communicate()
|
||||||
|
|
||||||
if len(out[1]) > 0:
|
if len(out[1]) > 0:
|
||||||
raise ValueError('RPC error ' + str(out[1]))
|
raise ValueError("RPC error " + str(out[1]))
|
||||||
|
|
||||||
r = out[0].decode('utf-8').strip()
|
r = out[0].decode("utf-8").strip()
|
||||||
try:
|
try:
|
||||||
r = json.loads(r)
|
r = json.loads(r)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -138,7 +152,7 @@ def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None)
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
|
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||||
port = port
|
port = port
|
||||||
auth = auth
|
auth = auth
|
||||||
wallet = wallet
|
wallet = wallet
|
||||||
@@ -146,11 +160,19 @@ def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
|
|||||||
|
|
||||||
def rpc_func(method, params=None, wallet_override=None):
|
def rpc_func(method, params=None, wallet_override=None):
|
||||||
nonlocal port, auth, wallet, host
|
nonlocal port, auth, wallet, host
|
||||||
return callrpc(port, auth, method, params, wallet if wallet_override is None else wallet_override, host)
|
return callrpc(
|
||||||
|
port,
|
||||||
|
auth,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
wallet if wallet_override is None else wallet_override,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|
||||||
|
|
||||||
def escape_rpcauth(auth_str: str) -> str:
|
def escape_rpcauth(auth_str: str) -> str:
|
||||||
username, password = auth_str.split(':', 1)
|
username, password = auth_str.split(":", 1)
|
||||||
password = urllib.parse.quote(password, safe='')
|
password = urllib.parse.quote(password, safe="")
|
||||||
return f'{username}:{password}'
|
return f"{username}:{password}"
|
||||||
|
|||||||
@@ -33,31 +33,50 @@ class SocksTransport(Transport):
|
|||||||
return self._connection[1]
|
return self._connection[1]
|
||||||
# create a HTTP connection object from a host descriptor
|
# create a HTTP connection object from a host descriptor
|
||||||
chost, self._extra_headers, x509 = self.get_host_info(host)
|
chost, self._extra_headers, x509 = self.get_host_info(host)
|
||||||
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost)
|
self._connection = host, SocksiPyConnection(
|
||||||
|
self.proxy_type,
|
||||||
|
self.proxy_host,
|
||||||
|
self.proxy_port,
|
||||||
|
self.proxy_rdns,
|
||||||
|
self.proxy_username,
|
||||||
|
self.proxy_password,
|
||||||
|
chost,
|
||||||
|
)
|
||||||
return self._connection[1]
|
return self._connection[1]
|
||||||
|
|
||||||
|
|
||||||
class JsonrpcDigest():
|
class JsonrpcDigest:
|
||||||
# __getattr__ complicates extending ServerProxy
|
# __getattr__ complicates extending ServerProxy
|
||||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
def __init__(
|
||||||
allow_none=False, use_datetime=False, use_builtin_types=False,
|
self,
|
||||||
*, context=None):
|
uri,
|
||||||
|
transport=None,
|
||||||
|
encoding=None,
|
||||||
|
verbose=False,
|
||||||
|
allow_none=False,
|
||||||
|
use_datetime=False,
|
||||||
|
use_builtin_types=False,
|
||||||
|
*,
|
||||||
|
context=None,
|
||||||
|
):
|
||||||
|
|
||||||
parsed = urllib.parse.urlparse(uri)
|
parsed = urllib.parse.urlparse(uri)
|
||||||
if parsed.scheme not in ('http', 'https'):
|
if parsed.scheme not in ("http", "https"):
|
||||||
raise OSError('unsupported XML-RPC protocol')
|
raise OSError("unsupported XML-RPC protocol")
|
||||||
self.__host = parsed.netloc
|
self.__host = parsed.netloc
|
||||||
self.__handler = parsed.path
|
self.__handler = parsed.path
|
||||||
|
|
||||||
if transport is None:
|
if transport is None:
|
||||||
handler = SafeTransport if parsed.scheme == 'https' else Transport
|
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
transport = handler(use_datetime=use_datetime,
|
transport = handler(
|
||||||
|
use_datetime=use_datetime,
|
||||||
use_builtin_types=use_builtin_types,
|
use_builtin_types=use_builtin_types,
|
||||||
**extra_kwargs)
|
**extra_kwargs,
|
||||||
|
)
|
||||||
self.__transport = transport
|
self.__transport = transport
|
||||||
|
|
||||||
self.__encoding = encoding or 'utf-8'
|
self.__encoding = encoding or "utf-8"
|
||||||
self.__verbose = verbose
|
self.__verbose = verbose
|
||||||
self.__allow_none = allow_none
|
self.__allow_none = allow_none
|
||||||
|
|
||||||
@@ -77,11 +96,18 @@ class JsonrpcDigest():
|
|||||||
connection.timeout = timeout
|
connection.timeout = timeout
|
||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
|
|
||||||
connection.putrequest('POST', self.__handler)
|
connection.putrequest("POST", self.__handler)
|
||||||
headers.append(('Content-Type', 'application/json'))
|
headers.append(("Content-Type", "application/json"))
|
||||||
headers.append(('User-Agent', 'jsonrpc'))
|
headers.append(("User-Agent", "jsonrpc"))
|
||||||
self.__transport.send_headers(connection, headers)
|
self.__transport.send_headers(connection, headers)
|
||||||
self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8'))
|
self.__transport.send_content(
|
||||||
|
connection,
|
||||||
|
(
|
||||||
|
""
|
||||||
|
if params is None
|
||||||
|
else json.dumps(params, default=jsonDecimal).encode("utf-8")
|
||||||
|
),
|
||||||
|
)
|
||||||
self.__request_id += 1
|
self.__request_id += 1
|
||||||
|
|
||||||
resp = connection.getresponse()
|
resp = connection.getresponse()
|
||||||
@@ -93,7 +119,7 @@ class JsonrpcDigest():
|
|||||||
self.__transport.close()
|
self.__transport.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def json_request(self, request_body, username='', password='', timeout=None):
|
def json_request(self, request_body, username="", password="", timeout=None):
|
||||||
try:
|
try:
|
||||||
connection = self.__transport.make_connection(self.__host)
|
connection = self.__transport.make_connection(self.__host)
|
||||||
if timeout:
|
if timeout:
|
||||||
@@ -101,65 +127,82 @@ class JsonrpcDigest():
|
|||||||
|
|
||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
|
|
||||||
connection.putrequest('POST', self.__handler)
|
connection.putrequest("POST", self.__handler)
|
||||||
headers.append(('Content-Type', 'application/json'))
|
headers.append(("Content-Type", "application/json"))
|
||||||
headers.append(('Connection', 'keep-alive'))
|
headers.append(("Connection", "keep-alive"))
|
||||||
self.__transport.send_headers(connection, headers)
|
self.__transport.send_headers(connection, headers)
|
||||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
self.__transport.send_content(
|
||||||
|
connection,
|
||||||
|
(
|
||||||
|
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
|
||||||
|
if request_body
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
resp = connection.getresponse()
|
resp = connection.getresponse()
|
||||||
|
|
||||||
if resp.status == 401:
|
if resp.status == 401:
|
||||||
resp_headers = resp.getheaders()
|
resp_headers = resp.getheaders()
|
||||||
v = resp.read()
|
_ = resp.read()
|
||||||
|
|
||||||
algorithm = ''
|
realm = ""
|
||||||
realm = ''
|
nonce = ""
|
||||||
nonce = ''
|
|
||||||
for h in resp_headers:
|
for h in resp_headers:
|
||||||
if h[0] != 'WWW-authenticate':
|
if h[0] != "WWW-authenticate":
|
||||||
continue
|
continue
|
||||||
fields = h[1].split(',')
|
fields = h[1].split(",")
|
||||||
for f in fields:
|
for f in fields:
|
||||||
key, value = f.split('=', 1)
|
key, value = f.split("=", 1)
|
||||||
if key == 'algorithm' and value != 'MD5':
|
if key == "algorithm" and value != "MD5":
|
||||||
break
|
break
|
||||||
if key == 'realm':
|
if key == "realm":
|
||||||
realm = value.strip('"')
|
realm = value.strip('"')
|
||||||
if key == 'nonce':
|
if key == "nonce":
|
||||||
nonce = value.strip('"')
|
nonce = value.strip('"')
|
||||||
if realm != '' and nonce != '':
|
if realm != "" and nonce != "":
|
||||||
break
|
break
|
||||||
|
|
||||||
if realm == '' or nonce == '':
|
if realm == "" or nonce == "":
|
||||||
raise ValueError('Authenticate header not found.')
|
raise ValueError("Authenticate header not found.")
|
||||||
|
|
||||||
path = self.__handler
|
path = self.__handler
|
||||||
HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest()
|
HA1 = hashlib.md5(
|
||||||
|
f"{username}:{realm}:{password}".encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
http_method = 'POST'
|
http_method = "POST"
|
||||||
HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest()
|
HA2 = hashlib.md5(f"{http_method}:{path}".encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
ncvalue = '{:08x}'.format(1)
|
ncvalue = "{:08x}".format(1)
|
||||||
s = ncvalue.encode('utf-8')
|
s = ncvalue.encode("utf-8")
|
||||||
s += nonce.encode('utf-8')
|
s += nonce.encode("utf-8")
|
||||||
s += time.ctime().encode('utf-8')
|
s += time.ctime().encode("utf-8")
|
||||||
s += os.urandom(8)
|
s += os.urandom(8)
|
||||||
cnonce = (hashlib.sha1(s).hexdigest()[:16])
|
cnonce = hashlib.sha1(s).hexdigest()[:16]
|
||||||
|
|
||||||
# MD5-SESS
|
# MD5-SESS
|
||||||
HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest()
|
HA1 = hashlib.md5(f"{HA1}:{nonce}:{cnonce}".encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest()
|
respdig = hashlib.md5(
|
||||||
|
f"{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}".encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
|
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
|
||||||
headers = self.__transport._extra_headers[:]
|
headers = self.__transport._extra_headers[:]
|
||||||
headers.append(('Authorization', header_value))
|
headers.append(("Authorization", header_value))
|
||||||
|
|
||||||
connection.putrequest('POST', self.__handler)
|
connection.putrequest("POST", self.__handler)
|
||||||
headers.append(('Content-Type', 'application/json'))
|
headers.append(("Content-Type", "application/json"))
|
||||||
headers.append(('Connection', 'keep-alive'))
|
headers.append(("Connection", "keep-alive"))
|
||||||
self.__transport.send_headers(connection, headers)
|
self.__transport.send_headers(connection, headers)
|
||||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
self.__transport.send_content(
|
||||||
|
connection,
|
||||||
|
(
|
||||||
|
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
|
||||||
|
if request_body
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
resp = connection.getresponse()
|
resp = connection.getresponse()
|
||||||
|
|
||||||
self.__request_id += 1
|
self.__request_id += 1
|
||||||
@@ -172,57 +215,88 @@ class JsonrpcDigest():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''):
|
def callrpc_xmr(
|
||||||
|
rpc_port,
|
||||||
|
method,
|
||||||
|
params=[],
|
||||||
|
rpc_host="127.0.0.1",
|
||||||
|
path="json_rpc",
|
||||||
|
auth=None,
|
||||||
|
timeout=120,
|
||||||
|
transport=None,
|
||||||
|
tag="",
|
||||||
|
):
|
||||||
# auth is a tuple: (username, password)
|
# auth is a tuple: (username, password)
|
||||||
try:
|
try:
|
||||||
if rpc_host.count('://') > 0:
|
if rpc_host.count("://") > 0:
|
||||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, path)
|
url = "{}:{}/{}".format(rpc_host, rpc_port, path)
|
||||||
else:
|
else:
|
||||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
url = "http://{}:{}/{}".format(rpc_host, rpc_port, path)
|
||||||
|
|
||||||
x = JsonrpcDigest(url, transport=transport)
|
x = JsonrpcDigest(url, transport=transport)
|
||||||
request_body = {
|
request_body = {
|
||||||
'method': method,
|
"method": method,
|
||||||
'params': params,
|
"params": params,
|
||||||
'jsonrpc': '2.0',
|
"jsonrpc": "2.0",
|
||||||
'id': x.request_id()
|
"id": x.request_id(),
|
||||||
}
|
}
|
||||||
if auth:
|
if auth:
|
||||||
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
|
v = x.json_request(
|
||||||
|
request_body, username=auth[0], password=auth[1], timeout=timeout
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
v = x.json_request(request_body, timeout=timeout)
|
v = x.json_request(request_body, timeout=timeout)
|
||||||
x.close()
|
x.close()
|
||||||
r = json.loads(v.decode('utf-8'))
|
r = json.loads(v.decode("utf-8"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
|
||||||
|
|
||||||
if 'error' in r and r['error'] is not None:
|
if "error" in r and r["error"] is not None:
|
||||||
raise ValueError(tag + 'RPC error ' + str(r['error']))
|
raise ValueError(tag + "RPC error " + str(r["error"]))
|
||||||
|
|
||||||
return r['result']
|
return r["result"]
|
||||||
|
|
||||||
|
|
||||||
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''):
|
def callrpc_xmr2(
|
||||||
|
rpc_port: int,
|
||||||
|
method: str,
|
||||||
|
params=None,
|
||||||
|
auth=None,
|
||||||
|
rpc_host="127.0.0.1",
|
||||||
|
timeout=120,
|
||||||
|
transport=None,
|
||||||
|
tag="",
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if rpc_host.count('://') > 0:
|
if rpc_host.count("://") > 0:
|
||||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
|
url = "{}:{}/{}".format(rpc_host, rpc_port, method)
|
||||||
else:
|
else:
|
||||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
|
url = "http://{}:{}/{}".format(rpc_host, rpc_port, method)
|
||||||
|
|
||||||
x = JsonrpcDigest(url, transport=transport)
|
x = JsonrpcDigest(url, transport=transport)
|
||||||
if auth:
|
if auth:
|
||||||
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
|
v = x.json_request(
|
||||||
|
params, username=auth[0], password=auth[1], timeout=timeout
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
v = x.json_request(params, timeout=timeout)
|
v = x.json_request(params, timeout=timeout)
|
||||||
x.close()
|
x.close()
|
||||||
r = json.loads(v.decode('utf-8'))
|
r = json.loads(v.decode("utf-8"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
|
def make_xmr_rpc2_func(
|
||||||
|
port,
|
||||||
|
auth,
|
||||||
|
host="127.0.0.1",
|
||||||
|
proxy_host=None,
|
||||||
|
proxy_port=None,
|
||||||
|
default_timeout=120,
|
||||||
|
tag="",
|
||||||
|
):
|
||||||
port = port
|
port = port
|
||||||
auth = auth
|
auth = auth
|
||||||
host = host
|
host = host
|
||||||
@@ -236,11 +310,29 @@ def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port
|
|||||||
|
|
||||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||||
nonlocal port, auth, host, transport, tag
|
nonlocal port, auth, host, transport, tag
|
||||||
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
|
return callrpc_xmr2(
|
||||||
|
port,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
auth=auth,
|
||||||
|
rpc_host=host,
|
||||||
|
timeout=timeout,
|
||||||
|
transport=transport,
|
||||||
|
tag=tag,
|
||||||
|
)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|
||||||
|
|
||||||
def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
|
def make_xmr_rpc_func(
|
||||||
|
port,
|
||||||
|
auth,
|
||||||
|
host="127.0.0.1",
|
||||||
|
proxy_host=None,
|
||||||
|
proxy_port=None,
|
||||||
|
default_timeout=120,
|
||||||
|
tag="",
|
||||||
|
):
|
||||||
port = port
|
port = port
|
||||||
auth = auth
|
auth = auth
|
||||||
host = host
|
host = host
|
||||||
@@ -254,5 +346,15 @@ def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=
|
|||||||
|
|
||||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||||
nonlocal port, auth, host, transport, tag
|
nonlocal port, auth, host, transport, tag
|
||||||
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
|
return callrpc_xmr(
|
||||||
|
port,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
rpc_host=host,
|
||||||
|
auth=auth,
|
||||||
|
timeout=timeout,
|
||||||
|
transport=transport,
|
||||||
|
tag=tag,
|
||||||
|
)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|||||||
@@ -8,23 +8,23 @@ from enum import IntEnum
|
|||||||
|
|
||||||
|
|
||||||
class OpCodes(IntEnum):
|
class OpCodes(IntEnum):
|
||||||
OP_0 = 0x00,
|
OP_0 = (0x00,)
|
||||||
OP_PUSHDATA1 = 0x4c,
|
OP_PUSHDATA1 = (0x4C,)
|
||||||
OP_1 = 0x51,
|
OP_1 = (0x51,)
|
||||||
OP_16 = 0x60,
|
OP_16 = (0x60,)
|
||||||
OP_IF = 0x63,
|
OP_IF = (0x63,)
|
||||||
OP_ELSE = 0x67,
|
OP_ELSE = (0x67,)
|
||||||
OP_ENDIF = 0x68,
|
OP_ENDIF = (0x68,)
|
||||||
OP_RETURN = 0x6a,
|
OP_RETURN = (0x6A,)
|
||||||
OP_DROP = 0x75,
|
OP_DROP = (0x75,)
|
||||||
OP_DUP = 0x76,
|
OP_DUP = (0x76,)
|
||||||
OP_SIZE = 0x82,
|
OP_SIZE = (0x82,)
|
||||||
OP_EQUAL = 0x87,
|
OP_EQUAL = (0x87,)
|
||||||
OP_EQUALVERIFY = 0x88,
|
OP_EQUALVERIFY = (0x88,)
|
||||||
OP_SHA256 = 0xa8,
|
OP_SHA256 = (0xA8,)
|
||||||
OP_HASH160 = 0xa9,
|
OP_HASH160 = (0xA9,)
|
||||||
OP_CHECKSIG = 0xac,
|
OP_CHECKSIG = (0xAC,)
|
||||||
OP_CHECKLOCKTIMEVERIFY = 0xb1,
|
OP_CHECKLOCKTIMEVERIFY = (0xB1,)
|
||||||
OP_CHECKSEQUENCEVERIFY = 0xb2,
|
OP_CHECKSEQUENCEVERIFY = (0xB2,)
|
||||||
|
|
||||||
OP_SHA256_DECRED = 0xc0,
|
OP_SHA256_DECRED = (0xC0,)
|
||||||
|
|||||||
@@ -1,42 +1,39 @@
|
|||||||
|
/* General Styles */
|
||||||
.padded_row td
|
.bold {
|
||||||
{
|
|
||||||
padding-top:1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold
|
|
||||||
{
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monospace
|
.monospace {
|
||||||
{
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floatright
|
.floatright {
|
||||||
{
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 1.25rem;
|
top: 1.25rem;
|
||||||
right: 1.25rem;
|
right: 1.25rem;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error_msg
|
/* Table Styles */
|
||||||
{
|
.padded_row td {
|
||||||
|
padding-top: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-highest {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
#hide {
|
#hide {
|
||||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
/* Firefox */
|
|
||||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
/* Safari and Chrome */
|
|
||||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
/* Opera */
|
|
||||||
animation: cssAnimation 0s ease-in 15s forwards;
|
animation: cssAnimation 0s ease-in 15s forwards;
|
||||||
-webkit-animation-fill-mode: forwards;
|
-webkit-animation-fill-mode: forwards;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes cssAnimation {
|
@keyframes cssAnimation {
|
||||||
to {
|
to {
|
||||||
width: 0;
|
width: 0;
|
||||||
@@ -44,6 +41,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes cssAnimation {
|
@-webkit-keyframes cssAnimation {
|
||||||
to {
|
to {
|
||||||
width: 0;
|
width: 0;
|
||||||
@@ -52,6 +50,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom Select Styles */
|
||||||
.custom-select .select {
|
.custom-select .select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-image: url('/static/images/other/coin.png');
|
background-image: url('/static/images/other/coin.png');
|
||||||
@@ -95,8 +94,9 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Blur and Overlay Styles */
|
||||||
.blurred {
|
.blurred {
|
||||||
filter: blur(4px);
|
filter: blur(3px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
user-select: auto;
|
user-select: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable opacity on disabled form elements in Chrome */
|
/* Form Element Styles */
|
||||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
select:disabled,
|
select:disabled,
|
||||||
input:disabled,
|
input:disabled,
|
||||||
@@ -120,6 +120,7 @@
|
|||||||
border: 1px solid red !important;
|
border: 1px solid red !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active Container Styles */
|
||||||
.active-container {
|
.active-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -137,6 +138,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Center Spin Animation */
|
||||||
.center-spin {
|
.center-spin {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -144,23 +146,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-container:hover #coin_to_button, .hover-container:hover #coin_to {
|
/* Hover Container Styles */
|
||||||
border-color: #3b82f6;
|
.hover-container:hover #coin_to_button,
|
||||||
}
|
.hover-container:hover #coin_to,
|
||||||
.hover-container:hover #coin_from_button, .hover-container:hover #coin_from {
|
.hover-container:hover #coin_from_button,
|
||||||
|
.hover-container:hover #coin_from {
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#coin_to_button, #coin_from_button {
|
#coin_to_button,
|
||||||
|
#coin_from_button {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Input-like Container Styles */
|
||||||
.input-like-container {
|
.input-like-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@@ -172,41 +181,45 @@
|
|||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
height: 90px;
|
overflow-wrap: break-word;
|
||||||
color: #374151;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
outline: none;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
height: auto;
|
||||||
|
min-height: 90px;
|
||||||
|
max-height: 150px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-like-container.dark {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-like-container.copying {
|
.input-like-container.copying {
|
||||||
width: inherit;
|
width: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* QR Code Styles */
|
||||||
.qrcode {
|
.qrcode {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-border {
|
.qrcode-border {
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-color: rgba(59, 130, 246, var(--tw-border-opacity));
|
background-color: #ffffff;
|
||||||
border-radius: 20px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode img {
|
.qrcode img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 15px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#showQR {
|
#showQR {
|
||||||
@@ -217,59 +230,138 @@
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode-container {
|
||||||
select.select-disabled {
|
margin-top: 25px;
|
||||||
opacity: 0.40 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disabled Element Styles */
|
||||||
.disabled-input-enabled {
|
select.select-disabled,
|
||||||
opacity: 0.40 !important;
|
.disabled-input-enabled,
|
||||||
}
|
|
||||||
select.disabled-select-enabled {
|
select.disabled-select-enabled {
|
||||||
opacity: 0.40 !important;
|
opacity: 0.40 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shutdown Modal Styles */
|
||||||
|
#shutdownModal {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-select .select {
|
#shutdownModal > div:first-child {
|
||||||
appearance: none;
|
z-index: 40;
|
||||||
background-position: 10px center;
|
}
|
||||||
background-repeat: no-repeat;
|
|
||||||
|
#shutdownModal > div:last-child {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shutdownModal > div {
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shutdownModal.hidden > div {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shutdownModal:not(.hidden) > div {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutdown-button {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutdown-button.shutdown-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutdown-button.shutdown-disabled:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutdown-button.shutdown-disabled svg {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Line Animation */
|
||||||
|
.loading-line {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #ccc;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-select select::-webkit-scrollbar {
|
.loading-line::before {
|
||||||
width: 0;
|
content: '';
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select option {
|
|
||||||
padding-left: 0;
|
|
||||||
text-indent: 0;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select option.no-space {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select option[data-image] {
|
|
||||||
background-image: url('');
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 10px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select-image {
|
|
||||||
display: none;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select .select:focus+.select-dropdown .select-image {
|
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to right, transparent, #007bff, transparent);
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usd-value:not(.loading) .loading-line,
|
||||||
|
.profit-loss:not(.loading) .loading-line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resolution Button Styles */
|
||||||
|
.resolution-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #4B5563;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-button:hover {
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-button:focus {
|
||||||
|
outline: 2px solid #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-button.active {
|
||||||
|
color: #3B82F6;
|
||||||
|
outline: 2px solid #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .resolution-button {
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .resolution-button:hover {
|
||||||
|
color: #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .resolution-button.active {
|
||||||
|
color: #60A5FA;
|
||||||
|
outline-color: #60A5FA;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Button Styles */
|
||||||
|
#toggle-volume.active {
|
||||||
|
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggle-auto-refresh[data-enabled="true"] {
|
||||||
|
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||||
|
}
|
||||||
|
|||||||
BIN
basicswap/static/images/coins/Bitcoin%20Cash.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
basicswap/static/images/coins/Bitcoin-Cash-20.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
basicswap/static/images/coins/Bitcoin-Cash.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 9.0 KiB |
872
basicswap/static/js/active.js
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
// Constants and State
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
const COIN_NAME_TO_SYMBOL = {
|
||||||
|
'Bitcoin': 'BTC',
|
||||||
|
'Litecoin': 'LTC',
|
||||||
|
'Monero': 'XMR',
|
||||||
|
'Particl': 'PART',
|
||||||
|
'Particl Blind': 'PART',
|
||||||
|
'Particl Anon': 'PART',
|
||||||
|
'PIVX': 'PIVX',
|
||||||
|
'Firo': 'FIRO',
|
||||||
|
'Dash': 'DASH',
|
||||||
|
'Decred': 'DCR',
|
||||||
|
'Wownero': 'WOW',
|
||||||
|
'Bitcoin Cash': 'BCH',
|
||||||
|
'Dogecoin': 'DOGE'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
const state = {
|
||||||
|
identities: new Map(),
|
||||||
|
currentPage: 1,
|
||||||
|
wsConnected: false,
|
||||||
|
swapsData: [],
|
||||||
|
isLoading: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
refreshPromise: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM
|
||||||
|
const elements = {
|
||||||
|
swapsBody: document.getElementById('active-swaps-body'),
|
||||||
|
prevPageButton: document.getElementById('prevPage'),
|
||||||
|
nextPageButton: document.getElementById('nextPage'),
|
||||||
|
currentPageSpan: document.getElementById('currentPage'),
|
||||||
|
paginationControls: document.getElementById('pagination-controls'),
|
||||||
|
activeSwapsCount: document.getElementById('activeSwapsCount'),
|
||||||
|
refreshSwapsButton: document.getElementById('refreshSwaps'),
|
||||||
|
statusDot: document.getElementById('status-dot'),
|
||||||
|
statusText: document.getElementById('status-text')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identity Manager
|
||||||
|
const IdentityManager = {
|
||||||
|
cache: new Map(),
|
||||||
|
pendingRequests: new Map(),
|
||||||
|
retryDelay: 2000,
|
||||||
|
maxRetries: 3,
|
||||||
|
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
||||||
|
|
||||||
|
async getIdentityData(address) {
|
||||||
|
if (!address) {
|
||||||
|
return { address: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedData = this.getCachedIdentity(address);
|
||||||
|
if (cachedData) {
|
||||||
|
return { ...cachedData, address };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingRequests.has(address)) {
|
||||||
|
const pendingData = await this.pendingRequests.get(address);
|
||||||
|
return { ...pendingData, address };
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = this.fetchWithRetry(address);
|
||||||
|
this.pendingRequests.set(address, request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await request;
|
||||||
|
this.cache.set(address, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
return { ...data, address };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error fetching identity for ${address}:`, error);
|
||||||
|
return { address };
|
||||||
|
} finally {
|
||||||
|
this.pendingRequests.delete(address);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCachedIdentity(address) {
|
||||||
|
const cached = this.cache.get(address);
|
||||||
|
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
if (cached) {
|
||||||
|
this.cache.delete(address);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchWithRetry(address, attempt = 1) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/json/identities/${address}`, {
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
address,
|
||||||
|
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
|
||||||
|
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
|
||||||
|
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
|
||||||
|
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
|
||||||
|
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
|
||||||
|
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
|
||||||
|
label: data.label || '',
|
||||||
|
note: data.note || '',
|
||||||
|
automation_override: safeParseInt(data.automation_override)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt >= this.maxRetries) {
|
||||||
|
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
num_sent_bids_successful: 0,
|
||||||
|
num_recv_bids_successful: 0,
|
||||||
|
num_sent_bids_failed: 0,
|
||||||
|
num_recv_bids_failed: 0,
|
||||||
|
num_sent_bids_rejected: 0,
|
||||||
|
num_recv_bids_rejected: 0,
|
||||||
|
label: '',
|
||||||
|
note: '',
|
||||||
|
automation_override: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
||||||
|
return this.fetchWithRetry(address, attempt + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeParseInt = (value) => {
|
||||||
|
const parsed = parseInt(value);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status, tx_a, tx_b) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'Completed':
|
||||||
|
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
|
||||||
|
case 'Expired':
|
||||||
|
case 'Timed-out':
|
||||||
|
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-white';
|
||||||
|
case 'Error':
|
||||||
|
case 'Failed':
|
||||||
|
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
|
||||||
|
case 'Failed, swiped':
|
||||||
|
case 'Failed, refunded':
|
||||||
|
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-red-500';
|
||||||
|
case 'InProgress':
|
||||||
|
case 'Script coin locked':
|
||||||
|
case 'Scriptless coin locked':
|
||||||
|
case 'Script coin lock released':
|
||||||
|
case 'SendingInitialTx':
|
||||||
|
case 'SendingPaymentTx':
|
||||||
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
||||||
|
case 'Received':
|
||||||
|
case 'Exchanged script lock tx sigs msg':
|
||||||
|
case 'Exchanged script lock spend tx msg':
|
||||||
|
case 'Script tx redeemed':
|
||||||
|
case 'Scriptless tx redeemed':
|
||||||
|
case 'Scriptless tx recovered':
|
||||||
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
||||||
|
case 'Accepted':
|
||||||
|
case 'Request accepted':
|
||||||
|
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
|
||||||
|
case 'Delaying':
|
||||||
|
case 'Auto accept delay':
|
||||||
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
||||||
|
case 'Abandoned':
|
||||||
|
case 'Rejected':
|
||||||
|
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTxStatusClass = (status) => {
|
||||||
|
if (!status || status === 'None') return 'text-gray-400';
|
||||||
|
|
||||||
|
if (status.includes('Complete') || status.includes('Confirmed')) {
|
||||||
|
return 'text-green-500';
|
||||||
|
}
|
||||||
|
if (status.includes('Error') || status.includes('Failed')) {
|
||||||
|
return 'text-red-500';
|
||||||
|
}
|
||||||
|
if (status.includes('Progress') || status.includes('Sending')) {
|
||||||
|
return 'text-yellow-500';
|
||||||
|
}
|
||||||
|
return 'text-blue-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Util
|
||||||
|
const formatTimeAgo = (timestamp) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const diff = now - timestamp;
|
||||||
|
|
||||||
|
if (diff < 60) return `${diff} seconds ago`;
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
||||||
|
return `${Math.floor(diff / 86400)} days ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAddress = (address, displayLength = 15) => {
|
||||||
|
if (!address) return '';
|
||||||
|
if (address.length <= displayLength) return address;
|
||||||
|
return `${address.slice(0, displayLength)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const statusColors = {
|
||||||
|
'Received': 'text-blue-500',
|
||||||
|
'Accepted': 'text-green-500',
|
||||||
|
'InProgress': 'text-yellow-500',
|
||||||
|
'Complete': 'text-green-600',
|
||||||
|
'Failed': 'text-red-500',
|
||||||
|
'Expired': 'text-gray-500'
|
||||||
|
};
|
||||||
|
return statusColors[status] || 'text-gray-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeStrokeColor = (expireTime) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const timeLeft = expireTime - now;
|
||||||
|
|
||||||
|
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||||
|
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||||
|
return '#10B981'; // More than 30 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket Manager
|
||||||
|
const WebSocketManager = {
|
||||||
|
ws: null,
|
||||||
|
processingQueue: false,
|
||||||
|
reconnectTimeout: null,
|
||||||
|
maxReconnectAttempts: 5,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
reconnectDelay: 5000,
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.connect();
|
||||||
|
this.startHealthCheck();
|
||||||
|
},
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wsPort = window.ws_port || '11700';
|
||||||
|
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||||
|
this.setupEventHandlers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket connection error:', error);
|
||||||
|
this.handleReconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupEventHandlers() {
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
state.wsConnected = true;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
updateConnectionStatus('connected');
|
||||||
|
console.log('🟢 WebSocket connection established for Swaps in Progress');
|
||||||
|
updateSwapsTable({ resetPage: true, refreshData: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = () => {
|
||||||
|
if (!this.processingQueue) {
|
||||||
|
this.processingQueue = true;
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
if (!state.isRefreshing) {
|
||||||
|
await updateSwapsTable({ resetPage: false, refreshData: true });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.processingQueue = false;
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
state.wsConnected = false;
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
|
this.handleReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = () => {
|
||||||
|
updateConnectionStatus('error');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
startHealthCheck() {
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||||
|
this.handleReconnect();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleReconnect() {
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||||
|
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||||
|
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
||||||
|
} else {
|
||||||
|
updateConnectionStatus('error');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.connect();
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI
|
||||||
|
const updateConnectionStatus = (status) => {
|
||||||
|
const { statusDot, statusText } = elements;
|
||||||
|
if (!statusDot || !statusText) return;
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
connected: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2',
|
||||||
|
textClass: 'text-sm text-green-500',
|
||||||
|
message: 'Connected'
|
||||||
|
},
|
||||||
|
disconnected: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2',
|
||||||
|
textClass: 'text-sm text-red-500',
|
||||||
|
message: 'Disconnected - Reconnecting...'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2',
|
||||||
|
textClass: 'text-sm text-yellow-500',
|
||||||
|
message: 'Connection Error'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2',
|
||||||
|
textClass: 'text-sm text-gray-500',
|
||||||
|
message: 'Connecting...'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] || statusConfig.default;
|
||||||
|
statusDot.className = config.dotClass;
|
||||||
|
statusText.className = config.textClass;
|
||||||
|
statusText.textContent = config.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLoadingState = (isLoading) => {
|
||||||
|
state.isLoading = isLoading;
|
||||||
|
if (elements.refreshSwapsButton) {
|
||||||
|
elements.refreshSwapsButton.disabled = isLoading;
|
||||||
|
elements.refreshSwapsButton.classList.toggle('opacity-75', isLoading);
|
||||||
|
elements.refreshSwapsButton.classList.toggle('cursor-wait', isLoading);
|
||||||
|
|
||||||
|
const refreshIcon = elements.refreshSwapsButton.querySelector('svg');
|
||||||
|
const refreshText = elements.refreshSwapsButton.querySelector('#refreshText');
|
||||||
|
|
||||||
|
if (refreshIcon) {
|
||||||
|
refreshIcon.style.transition = 'transform 0.3s ease';
|
||||||
|
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshText) {
|
||||||
|
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processIdentityStats = (identity) => {
|
||||||
|
if (!identity) return null;
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
sentSuccessful: safeParseInt(identity.num_sent_bids_successful),
|
||||||
|
recvSuccessful: safeParseInt(identity.num_recv_bids_successful),
|
||||||
|
sentFailed: safeParseInt(identity.num_sent_bids_failed),
|
||||||
|
recvFailed: safeParseInt(identity.num_recv_bids_failed),
|
||||||
|
sentRejected: safeParseInt(identity.num_sent_bids_rejected),
|
||||||
|
recvRejected: safeParseInt(identity.num_recv_bids_rejected)
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful;
|
||||||
|
stats.totalFailed = stats.sentFailed + stats.recvFailed;
|
||||||
|
stats.totalRejected = stats.sentRejected + stats.recvRejected;
|
||||||
|
stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected;
|
||||||
|
|
||||||
|
stats.successRate = stats.totalBids > 0
|
||||||
|
? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1)
|
||||||
|
: '0.0';
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createIdentityTooltip = (identity) => {
|
||||||
|
if (!identity) return '';
|
||||||
|
|
||||||
|
const stats = processIdentityStats(identity);
|
||||||
|
if (!stats) return '';
|
||||||
|
|
||||||
|
const getSuccessRateColor = (rate) => {
|
||||||
|
const numRate = parseFloat(rate);
|
||||||
|
if (numRate >= 80) return 'text-green-600';
|
||||||
|
if (numRate >= 60) return 'text-yellow-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="identity-info space-y-2">
|
||||||
|
${identity.label ? `
|
||||||
|
<div class="border-b border-gray-400 pb-2">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
|
||||||
|
<div class="text-white">${identity.label}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Address:</div>
|
||||||
|
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
|
||||||
|
${identity.address || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${identity.note ? `
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
|
||||||
|
<div class="text-white text-sm italic">${identity.note}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="pt-2 mt-2">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||||||
|
<div class="text-lg font-bold ${getSuccessRateColor(stats.successRate)}">
|
||||||
|
${stats.successRate}%
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-white">Success Rate</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||||||
|
<div class="text-lg font-bold text-blue-500">${stats.totalBids}</div>
|
||||||
|
<div class="text-xs text-white">Total Trades</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
|
||||||
|
<div>
|
||||||
|
<div class="text-green-600 font-semibold">
|
||||||
|
${stats.totalSuccessful}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Successful</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-yellow-600 font-semibold">
|
||||||
|
${stats.totalRejected}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Rejected</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-red-600 font-semibold">
|
||||||
|
${stats.totalFailed}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSwapTableRow = async (swap) => {
|
||||||
|
if (!swap || !swap.bid_id) {
|
||||||
|
console.warn('Invalid swap data:', swap);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await IdentityManager.getIdentityData(swap.addr_from);
|
||||||
|
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
|
||||||
|
const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from;
|
||||||
|
const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to;
|
||||||
|
const timeColor = getTimeStrokeColor(swap.expire_at);
|
||||||
|
const fromAmount = parseFloat(swap.amount_from) || 0;
|
||||||
|
const toAmount = parseFloat(swap.amount_to) || 0;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${swap.bid_id}">
|
||||||
|
<td class="relative w-0 p-0 m-0">
|
||||||
|
<div class="absolute top-0 bottom-0 left-0 w-1"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Time Column -->
|
||||||
|
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="relative" data-tooltip-target="tooltip-time-${uniqueId}">
|
||||||
|
<svg class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${timeColor}" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col hidden xl:block">
|
||||||
|
<div class="text-xs whitespace-nowrap">
|
||||||
|
<span class="bold">Posted:</span> ${formatTimeAgo(swap.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Details Column -->
|
||||||
|
<td class="py-8 px-4 text-xs text-left hidden xl:block">
|
||||||
|
<div class="flex flex-col gap-2 relative">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="/identity/${swap.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
|
||||||
|
${identity?.label || formatAddress(swap.addr_from)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Bid ID:</span>
|
||||||
|
<a href="/bid/${swap.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(swap.bid_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Offer ID:</span>
|
||||||
|
<a href="/offer/${swap.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(swap.offer_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<!-- You Send Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-left">
|
||||||
|
<div class="items-center monospace">
|
||||||
|
<div class="pr-2">
|
||||||
|
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Swap Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-center">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
|
<img class="h-12"
|
||||||
|
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
|
||||||
|
alt="${swap.coin_from}"
|
||||||
|
onerror="this.src='/static/images/coins/default.png'">
|
||||||
|
</span>
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
|
<img class="h-12"
|
||||||
|
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
|
||||||
|
alt="${swap.coin_to}"
|
||||||
|
onerror="this.src='/static/images/coins/default.png'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- You Receive Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-right">
|
||||||
|
<div class="items-center monospace">
|
||||||
|
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<div data-tooltip-target="tooltip-status-${uniqueId}" class="flex justify-center">
|
||||||
|
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
|
||||||
|
${swap.bid_state}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<a href="/bid/${swap.bid_id}"
|
||||||
|
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Tooltips -->
|
||||||
|
<div id="tooltip-time-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="active-revoked-expired">
|
||||||
|
<span class="bold">
|
||||||
|
<div class="text-xs"><span class="bold">Posted:</span> ${formatTimeAgo(swap.created_at)}</div>
|
||||||
|
<div class="text-xs"><span class="bold">Expires in:</span> ${formatTime(swap.expire_at)}</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 text-xs">
|
||||||
|
<p class="font-bold mb-3">Time Indicator Colors:</p>
|
||||||
|
<p class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Green: More than 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Blue: Between 5 and 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3 mb-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Grey: Less than 5 minutes left or expired
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
${createIdentityTooltip(identity)}
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Offer ID:</div>
|
||||||
|
<div class="monospace text-xs break-all">
|
||||||
|
${swap.offer_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-bid-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Bid ID:</div>
|
||||||
|
<div class="monospace text-xs break-all">
|
||||||
|
${swap.bid_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="text-white">
|
||||||
|
<p class="font-bold mb-2">Transaction Status</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="bg-gray-500 p-2 rounded">
|
||||||
|
<p class="text-xs font-bold">ITX:</p>
|
||||||
|
<p>${swap.tx_state_a || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-500 p-2 rounded">
|
||||||
|
<p class="text-xs font-bold">PTX:</p>
|
||||||
|
<p>${swap.tx_state_b || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function updateSwapsTable(options = {}) {
|
||||||
|
const { resetPage = false, refreshData = true } = options;
|
||||||
|
|
||||||
|
if (state.refreshPromise) {
|
||||||
|
await state.refreshPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateLoadingState(true);
|
||||||
|
|
||||||
|
if (refreshData) {
|
||||||
|
state.refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/json/active', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sort_by: "created_at",
|
||||||
|
sort_dir: "desc"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
state.swapsData = Array.isArray(data) ? data : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching swap data:', error);
|
||||||
|
state.swapsData = [];
|
||||||
|
} finally {
|
||||||
|
state.refreshPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await state.refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.activeSwapsCount) {
|
||||||
|
elements.activeSwapsCount.textContent = state.swapsData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(state.swapsData.length / PAGE_SIZE);
|
||||||
|
|
||||||
|
if (resetPage && state.swapsData.length > 0) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentPage = Math.min(Math.max(1, state.currentPage), Math.max(1, totalPages));
|
||||||
|
|
||||||
|
const startIndex = (state.currentPage - 1) * PAGE_SIZE;
|
||||||
|
const endIndex = startIndex + PAGE_SIZE;
|
||||||
|
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (elements.swapsBody) {
|
||||||
|
if (currentPageSwaps.length > 0) {
|
||||||
|
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
|
||||||
|
const rows = await Promise.all(rowPromises);
|
||||||
|
elements.swapsBody.innerHTML = rows.join('');
|
||||||
|
|
||||||
|
// Initialize tooltips
|
||||||
|
if (window.TooltipManager) {
|
||||||
|
window.TooltipManager.cleanup();
|
||||||
|
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
|
||||||
|
tooltipTriggers.forEach(trigger => {
|
||||||
|
const targetId = trigger.getAttribute('data-tooltip-target');
|
||||||
|
const tooltipContent = document.getElementById(targetId);
|
||||||
|
if (tooltipContent) {
|
||||||
|
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
|
||||||
|
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.swapsBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
||||||
|
No active swaps found
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.paginationControls) {
|
||||||
|
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.currentPageSpan) {
|
||||||
|
elements.currentPageSpan.textContent = state.currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.prevPageButton) {
|
||||||
|
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nextPageButton) {
|
||||||
|
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating swaps table:', error);
|
||||||
|
if (elements.swapsBody) {
|
||||||
|
elements.swapsBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-red-500">
|
||||||
|
Error loading active swaps. Please try again later.
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
if (elements.refreshSwapsButton) {
|
||||||
|
elements.refreshSwapsButton.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.isRefreshing) return;
|
||||||
|
|
||||||
|
updateLoadingState(true);
|
||||||
|
try {
|
||||||
|
await updateSwapsTable({ resetPage: true, refreshData: true });
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.prevPageButton) {
|
||||||
|
elements.prevPageButton.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.isLoading) return;
|
||||||
|
if (state.currentPage > 1) {
|
||||||
|
state.currentPage--;
|
||||||
|
await updateSwapsTable({ resetPage: false, refreshData: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nextPageButton) {
|
||||||
|
elements.nextPageButton.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.isLoading) return;
|
||||||
|
const totalPages = Math.ceil(state.swapsData.length / PAGE_SIZE);
|
||||||
|
if (state.currentPage < totalPages) {
|
||||||
|
state.currentPage++;
|
||||||
|
await updateSwapsTable({ resetPage: false, refreshData: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
WebSocketManager.initialize();
|
||||||
|
setupEventListeners();
|
||||||
|
});
|
||||||
899
basicswap/static/js/bids_available.js
Normal file
@@ -0,0 +1,899 @@
|
|||||||
|
// Constants and State
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
const COIN_NAME_TO_SYMBOL = {
|
||||||
|
'Bitcoin': 'BTC',
|
||||||
|
'Litecoin': 'LTC',
|
||||||
|
'Monero': 'XMR',
|
||||||
|
'Particl': 'PART',
|
||||||
|
'Particl Blind': 'PART',
|
||||||
|
'Particl Anon': 'PART',
|
||||||
|
'PIVX': 'PIVX',
|
||||||
|
'Firo': 'FIRO',
|
||||||
|
'Dash': 'DASH',
|
||||||
|
'Decred': 'DCR',
|
||||||
|
'Wownero': 'WOW',
|
||||||
|
'Bitcoin Cash': 'BCH',
|
||||||
|
'Dogecoin': 'DOGE'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
const state = {
|
||||||
|
dentities: new Map(),
|
||||||
|
currentPage: 1,
|
||||||
|
wsConnected: false,
|
||||||
|
jsonData: [],
|
||||||
|
isLoading: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
refreshPromise: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM
|
||||||
|
const elements = {
|
||||||
|
bidsBody: document.getElementById('bids-body'),
|
||||||
|
prevPageButton: document.getElementById('prevPage'),
|
||||||
|
nextPageButton: document.getElementById('nextPage'),
|
||||||
|
currentPageSpan: document.getElementById('currentPage'),
|
||||||
|
paginationControls: document.getElementById('pagination-controls'),
|
||||||
|
availableBidsCount: document.getElementById('availableBidsCount'),
|
||||||
|
refreshBidsButton: document.getElementById('refreshBids'),
|
||||||
|
statusDot: document.getElementById('status-dot'),
|
||||||
|
statusText: document.getElementById('status-text')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identity Manager
|
||||||
|
const IdentityManager = {
|
||||||
|
cache: new Map(),
|
||||||
|
pendingRequests: new Map(),
|
||||||
|
retryDelay: 2000,
|
||||||
|
maxRetries: 3,
|
||||||
|
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
||||||
|
|
||||||
|
async getIdentityData(address) {
|
||||||
|
if (!address) {
|
||||||
|
return { address: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedData = this.getCachedIdentity(address);
|
||||||
|
if (cachedData) {
|
||||||
|
return { ...cachedData, address };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingRequests.has(address)) {
|
||||||
|
const pendingData = await this.pendingRequests.get(address);
|
||||||
|
return { ...pendingData, address };
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = this.fetchWithRetry(address);
|
||||||
|
this.pendingRequests.set(address, request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await request;
|
||||||
|
this.cache.set(address, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
return { ...data, address };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error fetching identity for ${address}:`, error);
|
||||||
|
return { address };
|
||||||
|
} finally {
|
||||||
|
this.pendingRequests.delete(address);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCachedIdentity(address) {
|
||||||
|
const cached = this.cache.get(address);
|
||||||
|
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
if (cached) {
|
||||||
|
this.cache.delete(address);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchWithRetry(address, attempt = 1) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/json/identities/${address}`, {
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
address,
|
||||||
|
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
|
||||||
|
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
|
||||||
|
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
|
||||||
|
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
|
||||||
|
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
|
||||||
|
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
|
||||||
|
label: data.label || '',
|
||||||
|
note: data.note || '',
|
||||||
|
automation_override: safeParseInt(data.automation_override)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt >= this.maxRetries) {
|
||||||
|
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
num_sent_bids_successful: 0,
|
||||||
|
num_recv_bids_successful: 0,
|
||||||
|
num_sent_bids_failed: 0,
|
||||||
|
num_recv_bids_failed: 0,
|
||||||
|
num_sent_bids_rejected: 0,
|
||||||
|
num_recv_bids_rejected: 0,
|
||||||
|
label: '',
|
||||||
|
note: '',
|
||||||
|
automation_override: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
||||||
|
return this.fetchWithRetry(address, attempt + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
this.cache.clear();
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromCache(address) {
|
||||||
|
this.cache.delete(address);
|
||||||
|
this.pendingRequests.delete(address);
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [address, cached] of this.cache.entries()) {
|
||||||
|
if (now - cached.timestamp >= this.cacheTimeout) {
|
||||||
|
this.cache.delete(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Util
|
||||||
|
const formatTimeAgo = (timestamp) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const diff = now - timestamp;
|
||||||
|
|
||||||
|
if (diff < 60) return `${diff} seconds ago`;
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
||||||
|
return `${Math.floor(diff / 86400)} days ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const diff = timestamp - now;
|
||||||
|
|
||||||
|
if (diff <= 0) return "Expired";
|
||||||
|
if (diff < 60) return `${diff} seconds`;
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)} minutes`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)} hours`;
|
||||||
|
return `${Math.floor(diff / 86400)} days`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAddress = (address, displayLength = 15) => {
|
||||||
|
if (!address) return '';
|
||||||
|
if (address.length <= displayLength) return address;
|
||||||
|
return `${address.slice(0, displayLength)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeStrokeColor = (expireTime) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const timeLeft = expireTime - now;
|
||||||
|
|
||||||
|
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||||
|
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||||
|
return '#10B981'; // More than 30 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTimeTooltip = (bid) => {
|
||||||
|
const postedTime = formatTimeAgo(bid.created_at);
|
||||||
|
const expiresIn = formatTime(bid.expire_at);
|
||||||
|
return `
|
||||||
|
<div class="active-revoked-expired">
|
||||||
|
<span class="bold">
|
||||||
|
<div class="text-xs"><span class="bold">Posted:</span> ${postedTime}</div>
|
||||||
|
<div class="text-xs"><span class="bold">Expires in:</span> ${expiresIn}</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 text-xs">
|
||||||
|
<p class="font-bold mb-3">Time Indicator Colors:</p>
|
||||||
|
<p class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Green: More than 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Blue: Between 5 and 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3 mb-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Grey: Less than 5 minutes left or expired
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeParseInt = (value) => {
|
||||||
|
const parsed = parseInt(value);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processIdentityStats = (identity) => {
|
||||||
|
if (!identity) return null;
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
sentSuccessful: safeParseInt(identity.num_sent_bids_successful),
|
||||||
|
recvSuccessful: safeParseInt(identity.num_recv_bids_successful),
|
||||||
|
sentFailed: safeParseInt(identity.num_sent_bids_failed),
|
||||||
|
recvFailed: safeParseInt(identity.num_recv_bids_failed),
|
||||||
|
sentRejected: safeParseInt(identity.num_sent_bids_rejected),
|
||||||
|
recvRejected: safeParseInt(identity.num_recv_bids_rejected)
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful;
|
||||||
|
stats.totalFailed = stats.sentFailed + stats.recvFailed;
|
||||||
|
stats.totalRejected = stats.sentRejected + stats.recvRejected;
|
||||||
|
stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected;
|
||||||
|
|
||||||
|
stats.successRate = stats.totalBids > 0
|
||||||
|
? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1)
|
||||||
|
: '0.0';
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createIdentityTooltip = (identity) => {
|
||||||
|
if (!identity) return '';
|
||||||
|
|
||||||
|
const stats = processIdentityStats(identity);
|
||||||
|
if (!stats) return '';
|
||||||
|
|
||||||
|
const getSuccessRateColor = (rate) => {
|
||||||
|
const numRate = parseFloat(rate);
|
||||||
|
if (numRate >= 80) return 'text-green-600';
|
||||||
|
if (numRate >= 60) return 'text-yellow-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="identity-info space-y-2">
|
||||||
|
${identity.label ? `
|
||||||
|
<div class="border-b border-gray-400 pb-2">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
|
||||||
|
<div class="text-white">${identity.label}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
|
||||||
|
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
|
||||||
|
${identity.address || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${identity.note ? `
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
|
||||||
|
<div class="text-white text-sm italic">${identity.note}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="pt-2 mt-2">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||||||
|
<div class="text-lg font-bold ${getSuccessRateColor(stats.successRate)}">
|
||||||
|
${stats.successRate}%
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-white">Success Rate</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||||||
|
<div class="text-lg font-bold text-blue-500">${stats.totalBids}</div>
|
||||||
|
<div class="text-xs text-white">Total Trades</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
|
||||||
|
<div>
|
||||||
|
<div class="text-green-600 font-semibold">
|
||||||
|
${stats.totalSuccessful}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Successful</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-yellow-600 font-semibold">
|
||||||
|
${stats.totalRejected}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Rejected</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-red-600 font-semibold">
|
||||||
|
${stats.totalFailed}
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket Manager
|
||||||
|
const WebSocketManager = {
|
||||||
|
ws: null,
|
||||||
|
processingQueue: false,
|
||||||
|
reconnectTimeout: null,
|
||||||
|
maxReconnectAttempts: 5,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
reconnectDelay: 5000,
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.connect();
|
||||||
|
this.startHealthCheck();
|
||||||
|
},
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wsPort = window.ws_port || '11700';
|
||||||
|
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||||
|
this.setupEventHandlers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket connection error:', error);
|
||||||
|
this.handleReconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupEventHandlers() {
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
state.wsConnected = true;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
updateConnectionStatus('connected');
|
||||||
|
console.log('🟢 WebSocket connection established for Bid Requests');
|
||||||
|
updateBidsTable({ resetPage: true, refreshData: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = () => {
|
||||||
|
if (!this.processingQueue) {
|
||||||
|
this.processingQueue = true;
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
if (!state.isRefreshing) {
|
||||||
|
await updateBidsTable({ resetPage: false, refreshData: true });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.processingQueue = false;
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
state.wsConnected = false;
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
|
this.handleReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = () => {
|
||||||
|
updateConnectionStatus('error');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
startHealthCheck() {
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||||
|
this.handleReconnect();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleReconnect() {
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||||
|
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||||
|
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
||||||
|
} else {
|
||||||
|
updateConnectionStatus('error');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.connect();
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI
|
||||||
|
const updateConnectionStatus = (status) => {
|
||||||
|
const { statusDot, statusText } = elements;
|
||||||
|
if (!statusDot || !statusText) return;
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
connected: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2',
|
||||||
|
textClass: 'text-sm text-green-500',
|
||||||
|
message: 'Connected'
|
||||||
|
},
|
||||||
|
disconnected: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2',
|
||||||
|
textClass: 'text-sm text-red-500',
|
||||||
|
message: 'Disconnected - Reconnecting...'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2',
|
||||||
|
textClass: 'text-sm text-yellow-500',
|
||||||
|
message: 'Connection Error'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2',
|
||||||
|
textClass: 'text-sm text-gray-500',
|
||||||
|
message: 'Connecting...'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] || statusConfig.default;
|
||||||
|
statusDot.className = config.dotClass;
|
||||||
|
statusText.className = config.textClass;
|
||||||
|
statusText.textContent = config.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLoadingState = (isLoading) => {
|
||||||
|
state.isLoading = isLoading;
|
||||||
|
if (elements.refreshBidsButton) {
|
||||||
|
elements.refreshBidsButton.disabled = isLoading;
|
||||||
|
elements.refreshBidsButton.classList.toggle('opacity-75', isLoading);
|
||||||
|
elements.refreshBidsButton.classList.toggle('cursor-wait', isLoading);
|
||||||
|
|
||||||
|
const refreshIcon = elements.refreshBidsButton.querySelector('svg');
|
||||||
|
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
|
||||||
|
|
||||||
|
if (refreshIcon) {
|
||||||
|
// Add CSS transition for smoother animation
|
||||||
|
refreshIcon.style.transition = 'transform 0.3s ease';
|
||||||
|
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshText) {
|
||||||
|
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBidTableRow = async (bid) => {
|
||||||
|
if (!bid || !bid.bid_id) {
|
||||||
|
console.error('Invalid bid data:', bid);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await IdentityManager.getIdentityData(bid.addr_from);
|
||||||
|
const fromAmount = parseFloat(bid.amount_from) || 0;
|
||||||
|
const toAmount = parseFloat(bid.amount_to) || 0;
|
||||||
|
const rate = toAmount > 0 ? toAmount / fromAmount : 0;
|
||||||
|
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
|
||||||
|
|
||||||
|
const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from;
|
||||||
|
const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to;
|
||||||
|
|
||||||
|
const timeColor = getTimeStrokeColor(bid.expire_at);
|
||||||
|
const uniqueId = `${bid.bid_id}_${bid.created_at}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${bid.bid_id}">
|
||||||
|
<td class="relative w-0 p-0 m-0">
|
||||||
|
<div class="absolute top-0 bottom-0 left-0 w-1"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Time Column -->
|
||||||
|
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="relative" data-tooltip-target="tooltip-time-${uniqueId}">
|
||||||
|
<svg class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${timeColor}" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col hidden xl:block">
|
||||||
|
<div class="text-xs whitespace-nowrap">
|
||||||
|
<span class="bold">Posted:</span> ${formatTimeAgo(bid.created_at)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs whitespace-nowrap">
|
||||||
|
<span class="bold">Expires in:</span> ${formatTime(bid.expire_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Details Column -->
|
||||||
|
<td class="py-8 px-4 text-xs text-left hidden xl:block">
|
||||||
|
<div class="flex flex-col gap-2 relative">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="/identity/${bid.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
|
||||||
|
${identity?.label || formatAddress(bid.addr_from)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Offer ID:</span>
|
||||||
|
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(bid.offer_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Bid ID:</span>
|
||||||
|
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(bid.bid_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- You Send Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-left">
|
||||||
|
<div class="items-center monospace">
|
||||||
|
<div class="pr-2">
|
||||||
|
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">${bid.coin_from}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Swap Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-center">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
|
<img class="h-12"
|
||||||
|
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
|
||||||
|
alt="${bid.coin_from}"
|
||||||
|
onerror="this.src='/static/images/coins/default.png'">
|
||||||
|
</span>
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
|
<img class="h-12"
|
||||||
|
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
|
||||||
|
alt="${bid.coin_to}"
|
||||||
|
onerror="this.src='/static/images/coins/default.png'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- You Get Column -->
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="py-3 px-4 text-right">
|
||||||
|
<div class="items-center monospace">
|
||||||
|
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">${bid.coin_to}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Rate Column -->
|
||||||
|
<td class="py-3 px-4 text-right semibold monospace item-center text-xs">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<span class="bold text-gray-700 dark:text-white">
|
||||||
|
${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
|
||||||
|
</span>
|
||||||
|
<span class="semibold text-gray-400 dark:text-gray-300">
|
||||||
|
${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<a href="/bid/${bid.bid_id}/accept"
|
||||||
|
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
|
||||||
|
Accept
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Tooltips -->
|
||||||
|
<div id="tooltip-time-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="active-revoked-expired">
|
||||||
|
<span class="bold">
|
||||||
|
<div class="text-xs"><span class="bold">Posted:</span> ${formatTimeAgo(bid.created_at)}</div>
|
||||||
|
<div class="text-xs"><span class="bold">Expires in:</span> ${formatTime(bid.expire_at)}</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 text-xs">
|
||||||
|
<p class="font-bold mb-3">Time Indicator Colors:</p>
|
||||||
|
<p class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Green: More than 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Blue: Between 5 and 30 minutes left
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center mt-3 mb-3">
|
||||||
|
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Grey: Less than 5 minutes left or expired
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
|
||||||
|
${createIdentityTooltip(identity)}
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Offer ID:</div>
|
||||||
|
<div class="monospace text-xs break-all">
|
||||||
|
${bid.offer_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tooltip-bid-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-white text-xs tracking-wide font-semibold">Bid ID:</div>
|
||||||
|
<div class="monospace text-xs break-all">
|
||||||
|
${bid.bid_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayText = (identity, address) => {
|
||||||
|
if (identity?.label) {
|
||||||
|
return identity.label;
|
||||||
|
}
|
||||||
|
return formatAddress(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDetailsColumn = (bid, identity, uniqueId) => `
|
||||||
|
<td class="py-8 px-4 text-xs text-left hidden xl:block">
|
||||||
|
<div class="flex flex-col gap-2 relative">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="/identity/${bid.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
|
||||||
|
${getDisplayText(identity, bid.addr_from)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Offer ID:</span>
|
||||||
|
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(bid.offer_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
<span class="font-semibold">Bid ID:</span>
|
||||||
|
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
|
||||||
|
${formatAddress(bid.bid_id)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function updateBidsTable(options = {}) {
|
||||||
|
const { resetPage = false, refreshData = true } = options;
|
||||||
|
|
||||||
|
if (state.refreshPromise) {
|
||||||
|
await state.refreshPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateLoadingState(true);
|
||||||
|
|
||||||
|
if (refreshData) {
|
||||||
|
state.refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/json/bids', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sort_by: "created_at",
|
||||||
|
sort_dir: "desc",
|
||||||
|
with_available_or_active: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allBids = await response.json();
|
||||||
|
if (!Array.isArray(allBids)) {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.jsonData = allBids.filter(bid => bid.bid_state === "Received");
|
||||||
|
state.originalJsonData = [...state.jsonData];
|
||||||
|
} finally {
|
||||||
|
state.refreshPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await state.refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.availableBidsCount) {
|
||||||
|
elements.availableBidsCount.textContent = state.jsonData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE);
|
||||||
|
|
||||||
|
if (resetPage && state.jsonData.length > 0) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentPage = Math.min(Math.max(1, state.currentPage), Math.max(1, totalPages));
|
||||||
|
|
||||||
|
const startIndex = (state.currentPage - 1) * PAGE_SIZE;
|
||||||
|
const endIndex = startIndex + PAGE_SIZE;
|
||||||
|
const currentPageBids = state.jsonData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (elements.bidsBody) {
|
||||||
|
if (currentPageBids.length > 0) {
|
||||||
|
const rowPromises = currentPageBids.map(bid => createBidTableRow(bid));
|
||||||
|
const rows = await Promise.all(rowPromises);
|
||||||
|
elements.bidsBody.innerHTML = rows.join('');
|
||||||
|
|
||||||
|
if (window.TooltipManager) {
|
||||||
|
window.TooltipManager.cleanup();
|
||||||
|
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
|
||||||
|
tooltipTriggers.forEach(trigger => {
|
||||||
|
const targetId = trigger.getAttribute('data-tooltip-target');
|
||||||
|
const tooltipContent = document.getElementById(targetId);
|
||||||
|
if (tooltipContent) {
|
||||||
|
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
|
||||||
|
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.bidsBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
||||||
|
No available bids requests found
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.paginationControls) {
|
||||||
|
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.currentPageSpan) {
|
||||||
|
elements.currentPageSpan.textContent = state.currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.prevPageButton) {
|
||||||
|
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nextPageButton) {
|
||||||
|
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating bids table:', error);
|
||||||
|
if (elements.bidsBody) {
|
||||||
|
elements.bidsBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-red-500">
|
||||||
|
Error loading bids. Please try again later.
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
if (elements.refreshBidsButton) {
|
||||||
|
elements.refreshBidsButton.addEventListener('click', async () => {
|
||||||
|
if (state.isRefreshing) return;
|
||||||
|
|
||||||
|
updateLoadingState(true);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateBidsTable({ resetPage: true, refreshData: true });
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.prevPageButton) {
|
||||||
|
elements.prevPageButton.addEventListener('click', async () => {
|
||||||
|
if (state.isLoading) return;
|
||||||
|
if (state.currentPage > 1) {
|
||||||
|
state.currentPage--;
|
||||||
|
await updateBidsTable({ resetPage: false, refreshData: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nextPageButton) {
|
||||||
|
elements.nextPageButton.addEventListener('click', async () => {
|
||||||
|
if (state.isLoading) return;
|
||||||
|
const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE);
|
||||||
|
if (state.currentPage < totalPages) {
|
||||||
|
state.currentPage++;
|
||||||
|
await updateBidsTable({ resetPage: false, refreshData: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
WebSocketManager.initialize();
|
||||||
|
setupEventListeners();
|
||||||
|
});
|
||||||
141
basicswap/static/js/bids_export.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const BidExporter = {
|
||||||
|
toCSV(bids, type) {
|
||||||
|
if (!bids || !bids.length) {
|
||||||
|
return 'No data to export';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSent = type === 'sent';
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Date/Time',
|
||||||
|
'Bid ID',
|
||||||
|
'Offer ID',
|
||||||
|
'From Address',
|
||||||
|
isSent ? 'You Send Amount' : 'You Receive Amount',
|
||||||
|
isSent ? 'You Send Coin' : 'You Receive Coin',
|
||||||
|
isSent ? 'You Receive Amount' : 'You Send Amount',
|
||||||
|
isSent ? 'You Receive Coin' : 'You Send Coin',
|
||||||
|
'Status',
|
||||||
|
'Created At',
|
||||||
|
'Expires At'
|
||||||
|
];
|
||||||
|
|
||||||
|
let csvContent = headers.join(',') + '\n';
|
||||||
|
|
||||||
|
bids.forEach(bid => {
|
||||||
|
const row = [
|
||||||
|
`"${formatTime(bid.created_at)}"`,
|
||||||
|
`"${bid.bid_id}"`,
|
||||||
|
`"${bid.offer_id}"`,
|
||||||
|
`"${bid.addr_from}"`,
|
||||||
|
isSent ? bid.amount_from : bid.amount_to,
|
||||||
|
`"${isSent ? bid.coin_from : bid.coin_to}"`,
|
||||||
|
isSent ? bid.amount_to : bid.amount_from,
|
||||||
|
`"${isSent ? bid.coin_to : bid.coin_from}"`,
|
||||||
|
`"${bid.bid_state}"`,
|
||||||
|
bid.created_at,
|
||||||
|
bid.expire_at
|
||||||
|
];
|
||||||
|
|
||||||
|
csvContent += row.join(',') + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
return csvContent;
|
||||||
|
},
|
||||||
|
|
||||||
|
download(content, filename) {
|
||||||
|
try {
|
||||||
|
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
|
||||||
|
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
||||||
|
window.navigator.msSaveOrOpenBlob(blob, filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.style.display = 'none';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading CSV:', error);
|
||||||
|
|
||||||
|
const csvData = 'data:text/csv;charset=utf-8,' + encodeURIComponent(content);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute('href', csvData);
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
link.style.display = 'none';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exportCurrentView() {
|
||||||
|
const type = state.currentTab;
|
||||||
|
const data = state.data[type];
|
||||||
|
|
||||||
|
if (!data || !data.length) {
|
||||||
|
alert('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent = this.toCSV(data, type);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toISOString().split('T')[0];
|
||||||
|
const filename = `bsx_${type}_bids_${dateStr}.csv`;
|
||||||
|
|
||||||
|
this.download(csvContent, filename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
|
||||||
|
const exportSentButton = document.getElementById('exportSentBids');
|
||||||
|
if (exportSentButton) {
|
||||||
|
EventManager.add(exportSentButton, 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.currentTab = 'sent';
|
||||||
|
BidExporter.exportCurrentView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportReceivedButton = document.getElementById('exportReceivedBids');
|
||||||
|
if (exportReceivedButton) {
|
||||||
|
EventManager.add(exportReceivedButton, 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.currentTab = 'received';
|
||||||
|
BidExporter.exportCurrentView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCleanup = window.cleanup || function(){};
|
||||||
|
window.cleanup = function() {
|
||||||
|
originalCleanup();
|
||||||
|
|
||||||
|
const exportSentButton = document.getElementById('exportSentBids');
|
||||||
|
const exportReceivedButton = document.getElementById('exportReceivedBids');
|
||||||
|
|
||||||
|
if (exportSentButton && typeof EventManager !== 'undefined') {
|
||||||
|
EventManager.remove(exportSentButton, 'click');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportReceivedButton && typeof EventManager !== 'undefined') {
|
||||||
|
EventManager.remove(exportReceivedButton, 'click');
|
||||||
|
}
|
||||||
|
};
|
||||||
1988
basicswap/static/js/bids_sentreceived.js
Normal file
190
basicswap/static/js/dropdown.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
(function(window) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
|
||||||
|
targetEl.style.visibility = 'hidden';
|
||||||
|
targetEl.style.display = 'block';
|
||||||
|
|
||||||
|
const triggerRect = triggerEl.getBoundingClientRect();
|
||||||
|
const targetRect = targetEl.getBoundingClientRect();
|
||||||
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||||
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
let top, left;
|
||||||
|
|
||||||
|
top = triggerRect.bottom + offsetDistance;
|
||||||
|
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
|
||||||
|
|
||||||
|
switch (placement) {
|
||||||
|
case 'bottom-start':
|
||||||
|
left = triggerRect.left;
|
||||||
|
break;
|
||||||
|
case 'bottom-end':
|
||||||
|
left = triggerRect.right - targetRect.width;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
if (left < 10) left = 10;
|
||||||
|
if (left + targetRect.width > viewport.width - 10) {
|
||||||
|
left = viewport.width - targetRect.width - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEl.style.position = 'fixed';
|
||||||
|
targetEl.style.top = `${Math.round(top)}px`;
|
||||||
|
targetEl.style.left = `${Math.round(left)}px`;
|
||||||
|
targetEl.style.margin = '0';
|
||||||
|
targetEl.style.maxHeight = `${viewport.height - top - 10}px`;
|
||||||
|
targetEl.style.overflow = 'auto';
|
||||||
|
targetEl.style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dropdown {
|
||||||
|
constructor(targetEl, triggerEl, options = {}) {
|
||||||
|
this._targetEl = targetEl;
|
||||||
|
this._triggerEl = triggerEl;
|
||||||
|
this._options = {
|
||||||
|
placement: options.placement || 'bottom',
|
||||||
|
offset: options.offset || 5,
|
||||||
|
onShow: options.onShow || function() {},
|
||||||
|
onHide: options.onHide || function() {}
|
||||||
|
};
|
||||||
|
this._visible = false;
|
||||||
|
this._initialized = false;
|
||||||
|
this._handleScroll = this._handleScroll.bind(this);
|
||||||
|
this._handleResize = this._handleResize.bind(this);
|
||||||
|
this._handleOutsideClick = this._handleOutsideClick.bind(this);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this._initialized) {
|
||||||
|
this._targetEl.style.margin = '0';
|
||||||
|
this._targetEl.style.display = 'none';
|
||||||
|
this._targetEl.style.position = 'fixed';
|
||||||
|
this._targetEl.style.zIndex = '50';
|
||||||
|
|
||||||
|
this._setupEventListeners();
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupEventListeners() {
|
||||||
|
this._triggerEl.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', this._handleOutsideClick);
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') this.hide();
|
||||||
|
});
|
||||||
|
window.addEventListener('scroll', this._handleScroll, true);
|
||||||
|
window.addEventListener('resize', this._handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleScroll() {
|
||||||
|
if (this._visible) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
positionElement(
|
||||||
|
this._targetEl,
|
||||||
|
this._triggerEl,
|
||||||
|
this._options.placement,
|
||||||
|
this._options.offset
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleResize() {
|
||||||
|
if (this._visible) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
positionElement(
|
||||||
|
this._targetEl,
|
||||||
|
this._triggerEl,
|
||||||
|
this._options.placement,
|
||||||
|
this._options.offset
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleOutsideClick(e) {
|
||||||
|
if (this._visible &&
|
||||||
|
!this._targetEl.contains(e.target) &&
|
||||||
|
!this._triggerEl.contains(e.target)) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
if (!this._visible) {
|
||||||
|
this._targetEl.style.display = 'block';
|
||||||
|
this._targetEl.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
positionElement(
|
||||||
|
this._targetEl,
|
||||||
|
this._triggerEl,
|
||||||
|
this._options.placement,
|
||||||
|
this._options.offset
|
||||||
|
);
|
||||||
|
|
||||||
|
this._visible = true;
|
||||||
|
this._options.onShow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
if (this._visible) {
|
||||||
|
this._targetEl.style.display = 'none';
|
||||||
|
this._visible = false;
|
||||||
|
this._options.onHide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this._visible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', this._handleOutsideClick);
|
||||||
|
window.removeEventListener('scroll', this._handleScroll, true);
|
||||||
|
window.removeEventListener('resize', this._handleResize);
|
||||||
|
this._initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDropdowns() {
|
||||||
|
document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
|
||||||
|
const targetId = triggerEl.getAttribute('data-dropdown-toggle');
|
||||||
|
const targetEl = document.getElementById(targetId);
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
const placement = triggerEl.getAttribute('data-dropdown-placement');
|
||||||
|
new Dropdown(targetEl, triggerEl, {
|
||||||
|
placement: placement || 'bottom-start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initDropdowns);
|
||||||
|
} else {
|
||||||
|
initDropdowns();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Dropdown = Dropdown;
|
||||||
|
window.initDropdowns = initDropdowns;
|
||||||
|
|
||||||
|
})(window);
|
||||||
7
basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
1825
basicswap/static/js/libs/popper.js
Normal file
2516
basicswap/static/js/libs/tippy.js
Normal file
@@ -19,8 +19,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
||||||
|
|
||||||
if (close.length) {
|
if (close.length) {
|
||||||
for (var i = 0; i < close.length; i++) {
|
for (var k = 0; k < close.length; k++) {
|
||||||
close[i].addEventListener('click', function() {
|
close[k].addEventListener('click', function() {
|
||||||
for (var j = 0; j < menu.length; j++) {
|
for (var j = 0; j < menu.length; j++) {
|
||||||
menu[j].classList.toggle('hidden');
|
menu[j].classList.toggle('hidden');
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (backdrop.length) {
|
if (backdrop.length) {
|
||||||
for (var i = 0; i < backdrop.length; i++) {
|
for (var l = 0; l < backdrop.length; l++) {
|
||||||
backdrop[i].addEventListener('click', function() {
|
backdrop[l].addEventListener('click', function() {
|
||||||
for (var j = 0; j < menu.length; j++) {
|
for (var j = 0; j < menu.length; j++) {
|
||||||
menu[j].classList.toggle('hidden');
|
menu[j].classList.toggle('hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
window.addEventListener('DOMContentLoaded', (event) => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
let err_msgs = document.querySelectorAll('p.error_msg');
|
const err_msgs = document.querySelectorAll('p.error_msg');
|
||||||
for (let i = 0; i < err_msgs.length; i++) {
|
for (let i = 0; i < err_msgs.length; i++) {
|
||||||
err_msg = err_msgs[i].innerText;
|
err_msg = err_msgs[i].innerText;
|
||||||
if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
|
if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
|
||||||
@@ -29,9 +29,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove error class on input or select focus
|
// remove error class on input or select focus
|
||||||
let inputs = document.querySelectorAll('input.error');
|
const inputs = document.querySelectorAll('input.error');
|
||||||
let selects = document.querySelectorAll('select.error');
|
const selects = document.querySelectorAll('select.error');
|
||||||
let elements = [...inputs, ...selects];
|
const elements = [...inputs, ...selects];
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
element.addEventListener('focus', (event) => {
|
element.addEventListener('focus', (event) => {
|
||||||
event.target.classList.remove('error');
|
event.target.classList.remove('error');
|
||||||
|
|||||||